From 1a46158a1daf970ed54fce24aabf0365b9181fee Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Wed, 27 Mar 2024 18:53:18 +0100 Subject: [PATCH 001/967] Bump zha-quirks to 0.0.113 (#114311) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index e85966e870f..e9d75584064 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -24,7 +24,7 @@ "bellows==0.38.1", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.112", + "zha-quirks==0.0.113", "zigpy-deconz==0.23.1", "zigpy==0.63.5", "zigpy-xbee==0.20.1", diff --git a/requirements_all.txt b/requirements_all.txt index 1aedcb73671..42e92c3de6f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2931,7 +2931,7 @@ zeroconf==0.131.0 zeversolar==0.3.1 # homeassistant.components.zha -zha-quirks==0.0.112 +zha-quirks==0.0.113 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9795ef99481..3548eb7fadc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2266,7 +2266,7 @@ zeroconf==0.131.0 zeversolar==0.3.1 # homeassistant.components.zha -zha-quirks==0.0.112 +zha-quirks==0.0.113 # homeassistant.components.zha zigpy-deconz==0.23.1 From 1120745d4e9fd97985e13386bda79d86eedcc9a5 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 27 Mar 2024 19:41:11 +0100 Subject: [PATCH 002/967] Update SignalType imports (#114287) --- homeassistant/components/cast/const.py | 2 +- homeassistant/components/cloud/__init__.py | 2 +- homeassistant/components/cloud/const.py | 2 +- homeassistant/components/ffmpeg/__init__.py | 2 +- tests/common.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/cast/const.py b/homeassistant/components/cast/const.py index c57b686143d..056ee054d1d 100644 --- a/homeassistant/components/cast/const.py +++ b/homeassistant/components/cast/const.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, TypedDict -from homeassistant.helpers.dispatcher import SignalType +from homeassistant.util.signal_type import SignalType if TYPE_CHECKING: from .helpers import ChromecastInfo diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index aefab869955..d32278ba8f0 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -27,7 +27,6 @@ from homeassistant.helpers import config_validation as cv, entityfilter from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import ( - SignalType, async_dispatcher_connect, async_dispatcher_send, ) @@ -35,6 +34,7 @@ from homeassistant.helpers.event import async_call_later from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass +from homeassistant.util.signal_type import SignalType from . import account_link, http_api from .client import CloudClient diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index 1ee7392eccf..2c58dd57340 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import Any -from homeassistant.helpers.dispatcher import SignalType +from homeassistant.util.signal_type import SignalType DOMAIN = "cloud" DATA_PLATFORMS_SETUP = "cloud_platforms_setup" diff --git a/homeassistant/components/ffmpeg/__init__.py b/homeassistant/components/ffmpeg/__init__.py index 2045b6bb06b..b4c919fcb79 100644 --- a/homeassistant/components/ffmpeg/__init__.py +++ b/homeassistant/components/ffmpeg/__init__.py @@ -19,7 +19,6 @@ from homeassistant.const import ( from homeassistant.core import Event, HomeAssistant, ServiceCall, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( - SignalType, async_dispatcher_connect, async_dispatcher_send, ) @@ -27,6 +26,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.system_info import is_official_image from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass +from homeassistant.util.signal_type import SignalType if TYPE_CHECKING: from functools import cached_property diff --git a/tests/common.py b/tests/common.py index a7d4cf6b83a..54b0193091d 100644 --- a/tests/common.py +++ b/tests/common.py @@ -76,7 +76,6 @@ from homeassistant.helpers import ( translation, ) from homeassistant.helpers.dispatcher import ( - SignalType, async_dispatcher_connect, async_dispatcher_send, ) @@ -95,6 +94,7 @@ from homeassistant.util.json import ( json_loads_array, json_loads_object, ) +from homeassistant.util.signal_type import SignalType from homeassistant.util.unit_system import METRIC_SYSTEM import homeassistant.util.uuid as uuid_util import homeassistant.util.yaml.loader as yaml_loader From 3d4733a02668dd1fa3550e5cd997752cd70a52a7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 27 Mar 2024 19:43:38 +0100 Subject: [PATCH 003/967] Bump version to 2024.5.0dev0 (#114324) --- .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 4a7e38f0110..ab81b8f356d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -36,7 +36,7 @@ env: CACHE_VERSION: 5 UV_CACHE_VERSION: 1 MYPY_CACHE_VERSION: 8 - HA_SHORT_VERSION: "2024.4" + HA_SHORT_VERSION: "2024.5" 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 ee15cfd72c3..d52deb98d5b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -17,7 +17,7 @@ from .util.signal_type import SignalType APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 -MINOR_VERSION: Final = 4 +MINOR_VERSION: Final = 5 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 965827f41ea..a11ab82452b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.4.0.dev0" +version = "2024.5.0.dev0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From d6ac8ba5c8044131a27891ba626f4c0e03e667b0 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 27 Mar 2024 22:33:06 +0100 Subject: [PATCH 004/967] Remove checked in translations (#114336) --- .../components/devialet/translations/en.json | 22 ------------------- 1 file changed, 22 deletions(-) delete mode 100644 homeassistant/components/devialet/translations/en.json diff --git a/homeassistant/components/devialet/translations/en.json b/homeassistant/components/devialet/translations/en.json deleted file mode 100644 index af0cfc4c122..00000000000 --- a/homeassistant/components/devialet/translations/en.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Service is already configured" - }, - "error": { - "cannot_connect": "Failed to connect" - }, - "flow_title": "{title}", - "step": { - "confirm": { - "description": "Do you want to set up Devialet device {device}?" - }, - "user": { - "data": { - "host": "Host" - }, - "description": "Please enter the host name or IP address of the Devialet device." - } - } - } -} \ No newline at end of file From 0e12fea0cbb406ec64da241a1c8487abfaadbd9c Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 27 Mar 2024 22:35:08 +0100 Subject: [PATCH 005/967] Fix Matter airconditioner discovery of climate platform (#114326) * Discover Thermostat platform for Room Airconditioner device * add test * Adjust docstring Co-authored-by: TheJulianJES --------- Co-authored-by: Martin Hjelmare Co-authored-by: TheJulianJES --- homeassistant/components/matter/climate.py | 2 +- homeassistant/components/matter/switch.py | 1 + .../fixtures/nodes/room-airconditioner.json | 256 ++++++++++++++++++ tests/components/matter/test_climate.py | 25 ++ 4 files changed, 283 insertions(+), 1 deletion(-) create mode 100644 tests/components/matter/fixtures/nodes/room-airconditioner.json diff --git a/homeassistant/components/matter/climate.py b/homeassistant/components/matter/climate.py index 5ae1f7ca486..1b949d3ebfb 100644 --- a/homeassistant/components/matter/climate.py +++ b/homeassistant/components/matter/climate.py @@ -313,6 +313,6 @@ DISCOVERY_SCHEMAS = [ clusters.Thermostat.Attributes.UnoccupiedCoolingSetpoint, clusters.Thermostat.Attributes.UnoccupiedHeatingSetpoint, ), - device_type=(device_types.Thermostat,), + device_type=(device_types.Thermostat, device_types.RoomAirConditioner), ), ] diff --git a/homeassistant/components/matter/switch.py b/homeassistant/components/matter/switch.py index 91a28bdab8c..9bc858d40c0 100644 --- a/homeassistant/components/matter/switch.py +++ b/homeassistant/components/matter/switch.py @@ -86,6 +86,7 @@ DISCOVERY_SCHEMAS = [ device_types.ColorDimmerSwitch, device_types.DimmerSwitch, device_types.Thermostat, + device_types.RoomAirConditioner, ), ), ] diff --git a/tests/components/matter/fixtures/nodes/room-airconditioner.json b/tests/components/matter/fixtures/nodes/room-airconditioner.json new file mode 100644 index 00000000000..11c29b0d8f4 --- /dev/null +++ b/tests/components/matter/fixtures/nodes/room-airconditioner.json @@ -0,0 +1,256 @@ +{ + "node_id": 36, + "date_commissioned": "2024-03-27T17:31:23.745932", + "last_interview": "2024-03-27T17:31:23.745939", + "interview_version": 6, + "available": true, + "is_bridge": false, + "attributes": { + "0/29/0": [ + { + "0": 22, + "1": 1 + } + ], + "0/29/1": [29, 31, 40, 48, 49, 51, 60, 62, 63], + "0/29/2": [], + "0/29/3": [1, 2], + "0/29/65532": 0, + "0/29/65533": 2, + "0/29/65528": [], + "0/29/65529": [], + "0/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "0/31/0": [ + { + "254": 5 + }, + { + "1": 5, + "2": 2, + "3": [112233], + "4": null, + "254": 6 + } + ], + "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/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533], + "0/40/0": 17, + "0/40/1": "TEST_VENDOR", + "0/40/2": 65521, + "0/40/3": "Room AirConditioner", + "0/40/4": 32774, + "0/40/5": "", + "0/40/6": "**REDACTED**", + "0/40/7": 0, + "0/40/8": "TEST_VERSION", + "0/40/9": 1, + "0/40/10": "1.0", + "0/40/11": "20200101", + "0/40/12": "", + "0/40/13": "", + "0/40/14": "", + "0/40/15": "TEST_SN", + "0/40/16": false, + "0/40/18": "E47F334E22A56610", + "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, 11, 12, 13, 14, 15, 16, 18, 19, 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": 0, + "0/49/1": null, + "0/49/2": 0, + "0/49/3": 0, + "0/49/4": false, + "0/49/5": 0, + "0/49/6": "", + "0/49/7": 0, + "0/49/65532": 2, + "0/49/65533": 1, + "0/49/65528": [1, 5, 7], + "0/49/65529": [0, 2, 3, 4, 6, 8], + "0/49/65531": [0, 1, 2, 3, 4, 5, 6, 7, 65528, 65529, 65531, 65532, 65533], + "0/51/0": [], + "0/51/1": 0, + "0/51/8": false, + "0/51/65532": 0, + "0/51/65533": 1, + "0/51/65528": [], + "0/51/65529": [], + "0/51/65531": [0, 1, 8, 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/0": [ + { + "254": 5 + }, + { + "1": "FTABAQEkAgE3AyQTAhgmBIAigScmBYAlTTo3BiQVAiQRJBgkBwEkCAEwCUEE7pKHHHlljFuw2MAQJFOAzVR5tPPIXOjxHrLr7el8KqThQ6CuCFwdmNztUaIQgBcPZm6QRoEn6OGoFoAG8vB0KTcKNQEoARgkAgE2AwQCBAEYMAQUEvPPXEC80Bhik9ZDF3HK0Jo0RG0wBRQ2kjqIaJL5W4CHyhTHPUFcjBrNmxgwC0BJN+cSZw9fkFlIZGzsfS4WYFxzouEZ6LXLjqJXqwhi6uoQqoEhHPITp6sQ8u1ZF7OuQ35q0tZBwt84ZvAo+i59GA==", + "2": "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQTAhgkBwEkCAEwCUEEB0u1A8srBwhdMy9S5+W8C38qv6l9JxhOaVO1E8f3FHDpv6eTSEDWXvUKEOxZcce5cGUF/9tdW2z5M+pwjt2B9jcKNQEpARgkAmAwBBQ2kjqIaJL5W4CHyhTHPUFcjBrNmzAFFJOvH2V2J30vUkl3ZbhqhwBP2wVXGDALQJHZ9heIDcBg2DGc2b18rirq/5aZ2rsyP9BAE1zeTqSYj/pqKyeMS+hCx69jOqh/eAeDpeAzvL7JmKVLB0JLV1sY", + "254": 6 + } + ], + "0/62/1": [ + { + "1": "BER19ZLOakFRLvKKC9VsWzN+xv5V5yHHBFdX7ip/cNhnzVfnaNLLHKGU/DtcNZtU/YH+8kUcWKYvknk1TCcrG4k=", + "2": 24582, + "3": 9865010379846957597, + "4": 3118002441518404838, + "5": "", + "254": 5 + }, + { + "1": "BJUrvCXfXiwdfapIXt1qCtJoem+s2gZJ2KBDQZcPVP1cAYECu6Fjjz2MhMy6OW8ASGmWuke+YavIzIZWYEd6BJU=", + "2": 4939, + "3": 2, + "4": 36, + "5": "", + "254": 6 + } + ], + "0/62/2": 5, + "0/62/3": 2, + "0/62/4": [ + "FTABAQEkAgE3AycU3rGzlMtTrxYYJgQAus0sJgUAwGVSNwYnFN6xs5TLU68WGCQHASQIATAJQQREdfWSzmpBUS7yigvVbFszfsb+VechxwRXV+4qf3DYZ81X52jSyxyhlPw7XDWbVP2B/vJFHFimL5J5NUwnKxuJNwo1ASkBGCQCYDAEFMurIH6818tAIcTnwEZO5c+1WAH8MAUUy6sgfrzXy0AhxOfARk7lz7VYAfwYMAtAM2db17wMsM+JMtR4c2Iaz8nHLI4mVbsPGILOBujrzguB2C7p8Q9x8Cw0NgJP7hDV52F9j7IfHjO37aXZA4LqqBg=", + "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQUARgkBwEkCAEwCUEElSu8Jd9eLB19qkhe3WoK0mh6b6zaBknYoENBlw9U/VwBgQK7oWOPPYyEzLo5bwBIaZa6R75hq8jMhlZgR3oElTcKNQEpARgkAmAwBBSTrx9ldid9L1JJd2W4aocAT9sFVzAFFJOvH2V2J30vUkl3ZbhqhwBP2wVXGDALQPMYkhQcsrqT5v1vgN1LXJr9skDJ6nnuG0QWfs8SVODLGjU73iO1aQVq+Ir5et9RTD/4VrfnI63DW9RA0N+qgCkY" + ], + "0/62/5": 6, + "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": 4, + "0/63/3": 3, + "0/63/65532": 0, + "0/63/65533": 2, + "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": 0, + "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/6/0": false, + "1/6/65532": 0, + "1/6/65533": 5, + "1/6/65528": [], + "1/6/65529": [0, 1, 2], + "1/6/65531": [0, 65528, 65529, 65531, 65532, 65533], + "1/29/0": [ + { + "0": 114, + "1": 1 + } + ], + "1/29/1": [3, 6, 29, 513, 514], + "1/29/2": [], + "1/29/3": [2], + "1/29/65532": 0, + "1/29/65533": 2, + "1/29/65528": [], + "1/29/65529": [], + "1/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "1/513/0": 2000, + "1/513/3": 1600, + "1/513/4": 3200, + "1/513/5": 1600, + "1/513/6": 3200, + "1/513/17": 2600, + "1/513/18": 2000, + "1/513/25": 0, + "1/513/27": 4, + "1/513/28": 1, + "1/513/65532": 35, + "1/513/65533": 6, + "1/513/65528": [], + "1/513/65529": [0], + "1/513/65531": [ + 0, 3, 4, 5, 6, 17, 18, 25, 27, 28, 65528, 65529, 65531, 65532, 65533 + ], + "1/514/0": 0, + "1/514/1": 2, + "1/514/2": 0, + "1/514/3": 0, + "1/514/4": 3, + "1/514/5": 0, + "1/514/6": 0, + "1/514/9": 1, + "1/514/10": 0, + "1/514/65532": 11, + "1/514/65533": 4, + "1/514/65528": [], + "1/514/65529": [], + "1/514/65531": [ + 0, 1, 2, 3, 4, 5, 6, 9, 10, 65528, 65529, 65531, 65532, 65533 + ], + "2/3/0": 0, + "2/3/1": 0, + "2/3/65532": 0, + "2/3/65533": 4, + "2/3/65528": [], + "2/3/65529": [0, 64], + "2/3/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "2/29/0": [ + { + "0": 770, + "1": 1 + } + ], + "2/29/1": [3, 29, 1026], + "2/29/2": [], + "2/29/3": [], + "2/29/65532": 0, + "2/29/65533": 2, + "2/29/65528": [], + "2/29/65529": [], + "2/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "2/1026/0": 0, + "2/1026/1": -500, + "2/1026/2": 6000, + "2/1026/65532": 0, + "2/1026/65533": 1, + "2/1026/65528": [], + "2/1026/65529": [], + "2/1026/65531": [0, 1, 2, 65528, 65529, 65531, 65532, 65533] + }, + "attribute_subscriptions": [] +} diff --git a/tests/components/matter/test_climate.py b/tests/components/matter/test_climate.py index 80e2d1b72da..de4626ef3d1 100644 --- a/tests/components/matter/test_climate.py +++ b/tests/components/matter/test_climate.py @@ -25,6 +25,16 @@ async def thermostat_fixture( return await setup_integration_with_node_fixture(hass, "thermostat", matter_client) +@pytest.fixture(name="room_airconditioner") +async def room_airconditioner( + hass: HomeAssistant, matter_client: MagicMock +) -> MatterNode: + """Fixture for a room air conditioner node.""" + return await setup_integration_with_node_fixture( + hass, "room-airconditioner", matter_client + ) + + # This tests needs to be adjusted to remove lingering tasks @pytest.mark.parametrize("expected_lingering_tasks", [True]) async def test_thermostat( @@ -387,3 +397,18 @@ async def test_thermostat( clusters.Thermostat.Enums.SetpointAdjustMode.kCool, -40 ), ) + + +# This tests needs to be adjusted to remove lingering tasks +@pytest.mark.parametrize("expected_lingering_tasks", [True]) +async def test_room_airconditioner( + hass: HomeAssistant, + matter_client: MagicMock, + room_airconditioner: MatterNode, +) -> None: + """Test if a climate entity is created for a Room Airconditioner device.""" + state = hass.states.get("climate.room_airconditioner") + assert state + assert state.attributes["current_temperature"] == 20 + assert state.attributes["min_temp"] == 16 + assert state.attributes["max_temp"] == 32 From afcc4b58f438bdd5271841608ff8f38aca246aa3 Mon Sep 17 00:00:00 2001 From: Arie Catsman <120491684+catsmanac@users.noreply.github.com> Date: Wed, 27 Mar 2024 22:49:34 +0100 Subject: [PATCH 006/967] Remove suggested_uom from frequency in Enphase (#114340) remove suggested_uom from frequency --- homeassistant/components/enphase_envoy/sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index 329dc67e9e1..344bb47e025 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -276,7 +276,6 @@ CT_NET_CONSUMPTION_SENSORS = ( native_unit_of_measurement=UnitOfFrequency.HERTZ, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.FREQUENCY, - suggested_unit_of_measurement=UnitOfFrequency.HERTZ, suggested_display_precision=1, entity_registry_enabled_default=False, value_fn=lambda ct: ct.frequency, From acb9eb4818d75d42ad0bf1d98d0624a2d3239ddf Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 27 Mar 2024 22:52:02 +0100 Subject: [PATCH 007/967] Download translations only once in the build pipeline (#114335) --- .github/workflows/builder.yml | 63 ++++++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 19 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 1dc6f7a3938..5dc01eee21e 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -51,6 +51,32 @@ jobs: with: ignore-dev: true + - name: Fail if translations files are checked in + run: | + files=$(find homeassistant/components/*/translations -type f) + + if [ -n "$files" ]; then + echo "Translations files are checked in, please remove the following files:" + echo "$files" + exit 1 + fi + + - name: Download Translations + run: python3 -m script.translations download + env: + LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }} + + - name: Archive translations + shell: bash + run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T - + + - name: Upload translations + uses: actions/upload-artifact@v4.3.1 + with: + name: translations + path: translations.tar.gz + if-no-files-found: error + build_base: name: Build ${{ matrix.arch }} base core image if: github.repository_owner == 'home-assistant' @@ -159,10 +185,15 @@ jobs: # are not available. sed -i "s|aiohttp-zlib-ng|aiohttp-zlib-ng\[isal\]|g" requirements_all.txt - - name: Download Translations - run: python3 -m script.translations download - env: - LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }} + - name: Download translations + uses: actions/download-artifact@v4.1.4 + with: + name: translations + + - name: Extract translations + run: | + tar xvf translations.tar.gz + rm translations.tar.gz - name: Write meta info file shell: bash @@ -186,17 +217,6 @@ jobs: --target /data \ --generic ${{ needs.init.outputs.version }} - - name: Archive translations - shell: bash - run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T - - - - name: Upload translations - uses: actions/upload-artifact@v3 - with: - name: translations - path: translations.tar.gz - if-no-files-found: error - build_machine: name: Build ${{ matrix.machine }} machine core image if: github.repository_owner == 'home-assistant' @@ -448,10 +468,15 @@ jobs: with: python-version: ${{ env.DEFAULT_PYTHON }} - - name: Download Translations - run: python3 -m script.translations download - env: - LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }} + - name: Download translations + uses: actions/download-artifact@v4.1.4 + with: + name: translations + + - name: Extract translations + run: | + tar xvf translations.tar.gz + rm translations.tar.gz - name: Build package shell: bash From b8e9a78d6f5e2d595dea948c10d15759e3cd9abc Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 27 Mar 2024 17:19:34 -0500 Subject: [PATCH 008/967] Add more Ollama models (#114339) Add more models --- homeassistant/components/ollama/const.py | 41 ++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/homeassistant/components/ollama/const.py b/homeassistant/components/ollama/const.py index 59f1888cfc7..853370066dc 100644 --- a/homeassistant/components/ollama/const.py +++ b/homeassistant/components/ollama/const.py @@ -110,5 +110,46 @@ MODEL_NAMES = [ # https://ollama.com/library "starcoder", "phind-codellama", "starcoder2", + "yi", + "orca2", + "falcon", + "wizard-math", + "dolphin-phi", + "starling-lm", + "nous-hermes", + "stable-code", + "medllama2", + "bakllava", + "codeup", + "wizardlm-uncensored", + "solar", + "everythinglm", + "sqlcoder", + "dolphincoder", + "nous-hermes2-mixtral", + "stable-beluga", + "yarn-mistral", + "stablelm2", + "samantha-mistral", + "meditron", + "stablelm-zephyr", + "magicoder", + "yarn-llama2", + "llama-pro", + "deepseek-llm", + "wizard-vicuna", + "codebooga", + "mistrallite", + "all-minilm", + "nexusraven", + "open-orca-platypus2", + "goliath", + "notux", + "megadolphin", + "alfred", + "xwinlm", + "wizardlm", + "duckdb-nsql", + "notus", ] DEFAULT_MODEL = "llama2:latest" From 4ea8185d4da8c3b0306db793dd73124c7efbfef6 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Thu, 28 Mar 2024 00:43:34 +0100 Subject: [PATCH 009/967] Bump pyduotecno to 2024.3.2 (#114320) --- homeassistant/components/duotecno/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/duotecno/manifest.json b/homeassistant/components/duotecno/manifest.json index 7b33784a612..0c8eab8f0a0 100644 --- a/homeassistant/components/duotecno/manifest.json +++ b/homeassistant/components/duotecno/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_push", "loggers": ["pyduotecno", "pyduotecno-node", "pyduotecno-unit"], "quality_scale": "silver", - "requirements": ["pyDuotecno==2024.1.2"] + "requirements": ["pyDuotecno==2024.3.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 42e92c3de6f..ace129c69f2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1648,7 +1648,7 @@ pyCEC==0.5.2 pyControl4==1.1.0 # homeassistant.components.duotecno -pyDuotecno==2024.1.2 +pyDuotecno==2024.3.2 # homeassistant.components.electrasmart pyElectra==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3548eb7fadc..b01a7ca8ba4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1298,7 +1298,7 @@ pyCEC==0.5.2 pyControl4==1.1.0 # homeassistant.components.duotecno -pyDuotecno==2024.1.2 +pyDuotecno==2024.3.2 # homeassistant.components.electrasmart pyElectra==1.2.0 From 4d7a43425474da09a2d8b24a766db5193e9e2b16 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 28 Mar 2024 00:44:01 +0100 Subject: [PATCH 010/967] Don't access States.last_reported_ts before it's added (#114333) --- homeassistant/components/recorder/const.py | 1 + homeassistant/components/recorder/core.py | 3 +- .../components/recorder/history/modern.py | 27 +- tests/components/recorder/db_schema_42.py | 838 +++++++++++ .../recorder/test_history_db_schema_42.py | 1278 +++++++++++++++++ 5 files changed, 2138 insertions(+), 9 deletions(-) create mode 100644 tests/components/recorder/db_schema_42.py create mode 100644 tests/components/recorder/test_history_db_schema_42.py diff --git a/homeassistant/components/recorder/const.py b/homeassistant/components/recorder/const.py index 66d46c0c20e..1869bb32239 100644 --- a/homeassistant/components/recorder/const.py +++ b/homeassistant/components/recorder/const.py @@ -53,6 +53,7 @@ STATISTICS_ROWS_SCHEMA_VERSION = 23 CONTEXT_ID_AS_BINARY_SCHEMA_VERSION = 36 EVENT_TYPE_IDS_SCHEMA_VERSION = 37 STATES_META_SCHEMA_VERSION = 38 +LAST_REPORTED_SCHEMA_VERSION = 43 LEGACY_STATES_EVENT_ID_INDEX_SCHEMA_VERSION = 28 diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 7de9cf46311..0e404ce4da0 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -47,6 +47,7 @@ from .const import ( DOMAIN, ESTIMATED_QUEUE_ITEM_SIZE, KEEPALIVE_TIME, + LAST_REPORTED_SCHEMA_VERSION, LEGACY_STATES_EVENT_ID_INDEX_SCHEMA_VERSION, MARIADB_PYMYSQL_URL_PREFIX, MARIADB_URL_PREFIX, @@ -1203,7 +1204,7 @@ class Recorder(threading.Thread): if ( pending_last_reported := self.states_manager.get_pending_last_reported_timestamp() - ): + ) and self.schema_version >= LAST_REPORTED_SCHEMA_VERSION: with session.no_autoflush: session.execute( update(States), diff --git a/homeassistant/components/recorder/history/modern.py b/homeassistant/components/recorder/history/modern.py index a909f799ea9..5fd4f415e02 100644 --- a/homeassistant/components/recorder/history/modern.py +++ b/homeassistant/components/recorder/history/modern.py @@ -27,6 +27,7 @@ from homeassistant.core import HomeAssistant, State, split_entity_id import homeassistant.util.dt as dt_util from ... import recorder +from ..const import LAST_REPORTED_SCHEMA_VERSION from ..db_schema import SHARED_ATTR_OR_LEGACY_ATTRIBUTES, StateAttributes, States from ..filters import Filters from ..models import ( @@ -327,9 +328,10 @@ def _state_changed_during_period_stmt( limit: int | None, include_start_time_state: bool, run_start_ts: float | None, + include_last_reported: bool, ) -> Select | CompoundSelect: stmt = ( - _stmt_and_join_attributes(no_attributes, False, True) + _stmt_and_join_attributes(no_attributes, False, include_last_reported) .filter( ( (States.last_changed_ts == States.last_updated_ts) @@ -361,22 +363,22 @@ def _state_changed_during_period_stmt( single_metadata_id, no_attributes, False, - True, + include_last_reported, ).subquery(), no_attributes, False, - True, + include_last_reported, ), _select_from_subquery( stmt.subquery(), no_attributes, False, - True, + include_last_reported, ), ).subquery(), no_attributes, False, - True, + include_last_reported, ) @@ -391,6 +393,9 @@ def state_changes_during_period( include_start_time_state: bool = True, ) -> MutableMapping[str, list[State]]: """Return states changes during UTC period start_time - end_time.""" + has_last_reported = ( + recorder.get_instance(hass).schema_version >= LAST_REPORTED_SCHEMA_VERSION + ) if not entity_id: raise ValueError("entity_id must be provided") entity_ids = [entity_id.lower()] @@ -423,12 +428,14 @@ def state_changes_during_period( limit, include_start_time_state, run_start_ts, + has_last_reported, ), track_on=[ bool(end_time_ts), no_attributes, bool(limit), include_start_time_state, + has_last_reported, ], ) return cast( @@ -475,10 +482,10 @@ def _get_last_state_changes_single_stmt(metadata_id: int) -> Select: def _get_last_state_changes_multiple_stmt( - number_of_states: int, metadata_id: int + number_of_states: int, metadata_id: int, include_last_reported: bool ) -> Select: return ( - _stmt_and_join_attributes(False, False, True) + _stmt_and_join_attributes(False, False, include_last_reported) .where( States.state_id == ( @@ -500,6 +507,9 @@ def get_last_state_changes( hass: HomeAssistant, number_of_states: int, entity_id: str ) -> MutableMapping[str, list[State]]: """Return the last number_of_states.""" + has_last_reported = ( + recorder.get_instance(hass).schema_version >= LAST_REPORTED_SCHEMA_VERSION + ) entity_id_lower = entity_id.lower() entity_ids = [entity_id_lower] @@ -524,8 +534,9 @@ def get_last_state_changes( else: stmt = lambda_stmt( lambda: _get_last_state_changes_multiple_stmt( - number_of_states, metadata_id + number_of_states, metadata_id, has_last_reported ), + track_on=[has_last_reported], ) states = list(execute_stmt_lambda_element(session, stmt, orm_rows=False)) return cast( diff --git a/tests/components/recorder/db_schema_42.py b/tests/components/recorder/db_schema_42.py new file mode 100644 index 00000000000..b8e49aef592 --- /dev/null +++ b/tests/components/recorder/db_schema_42.py @@ -0,0 +1,838 @@ +"""Models for SQLAlchemy. + +This file contains the model definitions for schema version 42. +It is used to test the schema migration logic. +""" + +from __future__ import annotations + +from collections.abc import Callable +from datetime import datetime, timedelta +import logging +import time +from typing import Any, Self, cast + +import ciso8601 +from fnv_hash_fast import fnv1a_32 +from sqlalchemy import ( + CHAR, + JSON, + BigInteger, + Boolean, + ColumnElement, + DateTime, + Float, + ForeignKey, + Identity, + Index, + Integer, + LargeBinary, + SmallInteger, + String, + Text, + case, + type_coerce, +) +from sqlalchemy.dialects import mysql, oracle, postgresql, sqlite +from sqlalchemy.engine.interfaces import Dialect +from sqlalchemy.ext.compiler import compiles +from sqlalchemy.orm import DeclarativeBase, Mapped, aliased, mapped_column, relationship +from sqlalchemy.types import TypeDecorator + +from homeassistant.components.recorder.const import ( + ALL_DOMAIN_EXCLUDE_ATTRS, + SupportedDialect, +) +from homeassistant.components.recorder.models import ( + StatisticData, + StatisticDataTimestamp, + StatisticMetaData, + bytes_to_ulid_or_none, + bytes_to_uuid_hex_or_none, + datetime_to_timestamp_or_none, + process_timestamp, + ulid_to_bytes_or_none, + uuid_hex_to_bytes_or_none, +) +from homeassistant.const import ( + MAX_LENGTH_EVENT_EVENT_TYPE, + MAX_LENGTH_STATE_ENTITY_ID, + MAX_LENGTH_STATE_STATE, +) +from homeassistant.core import Context, Event, EventOrigin, State +from homeassistant.helpers.json import JSON_DUMP, json_bytes, json_bytes_strip_null +import homeassistant.util.dt as dt_util +from homeassistant.util.json import ( + JSON_DECODE_EXCEPTIONS, + json_loads, + json_loads_object, +) + + +# SQLAlchemy Schema +class Base(DeclarativeBase): + """Base class for tables.""" + + +SCHEMA_VERSION = 42 + +_LOGGER = logging.getLogger(__name__) + +TABLE_EVENTS = "events" +TABLE_EVENT_DATA = "event_data" +TABLE_EVENT_TYPES = "event_types" +TABLE_STATES = "states" +TABLE_STATE_ATTRIBUTES = "state_attributes" +TABLE_STATES_META = "states_meta" +TABLE_RECORDER_RUNS = "recorder_runs" +TABLE_SCHEMA_CHANGES = "schema_changes" +TABLE_STATISTICS = "statistics" +TABLE_STATISTICS_META = "statistics_meta" +TABLE_STATISTICS_RUNS = "statistics_runs" +TABLE_STATISTICS_SHORT_TERM = "statistics_short_term" + +STATISTICS_TABLES = ("statistics", "statistics_short_term") + +MAX_STATE_ATTRS_BYTES = 16384 +MAX_EVENT_DATA_BYTES = 32768 + +PSQL_DIALECT = SupportedDialect.POSTGRESQL + +ALL_TABLES = [ + TABLE_STATES, + TABLE_STATE_ATTRIBUTES, + TABLE_EVENTS, + TABLE_EVENT_DATA, + TABLE_EVENT_TYPES, + TABLE_RECORDER_RUNS, + TABLE_SCHEMA_CHANGES, + TABLE_STATES_META, + TABLE_STATISTICS, + TABLE_STATISTICS_META, + TABLE_STATISTICS_RUNS, + TABLE_STATISTICS_SHORT_TERM, +] + +TABLES_TO_CHECK = [ + TABLE_STATES, + TABLE_EVENTS, + TABLE_RECORDER_RUNS, + TABLE_SCHEMA_CHANGES, +] + +LAST_UPDATED_INDEX_TS = "ix_states_last_updated_ts" +METADATA_ID_LAST_UPDATED_INDEX_TS = "ix_states_metadata_id_last_updated_ts" +EVENTS_CONTEXT_ID_BIN_INDEX = "ix_events_context_id_bin" +STATES_CONTEXT_ID_BIN_INDEX = "ix_states_context_id_bin" +LEGACY_STATES_EVENT_ID_INDEX = "ix_states_event_id" +LEGACY_STATES_ENTITY_ID_LAST_UPDATED_INDEX = "ix_states_entity_id_last_updated_ts" +CONTEXT_ID_BIN_MAX_LENGTH = 16 + +MYSQL_COLLATE = "utf8mb4_unicode_ci" +MYSQL_DEFAULT_CHARSET = "utf8mb4" +MYSQL_ENGINE = "InnoDB" + +_DEFAULT_TABLE_ARGS = { + "mysql_default_charset": MYSQL_DEFAULT_CHARSET, + "mysql_collate": MYSQL_COLLATE, + "mysql_engine": MYSQL_ENGINE, + "mariadb_default_charset": MYSQL_DEFAULT_CHARSET, + "mariadb_collate": MYSQL_COLLATE, + "mariadb_engine": MYSQL_ENGINE, +} + + +class UnusedDateTime(DateTime): + """An unused column type that behaves like a datetime.""" + + +class Unused(CHAR): + """An unused column type that behaves like a string.""" + + +@compiles(UnusedDateTime, "mysql", "mariadb", "sqlite") # type: ignore[misc,no-untyped-call] +@compiles(Unused, "mysql", "mariadb", "sqlite") # type: ignore[misc,no-untyped-call] +def compile_char_zero(type_: TypeDecorator, compiler: Any, **kw: Any) -> str: + """Compile UnusedDateTime and Unused as CHAR(0) on mysql, mariadb, and sqlite.""" + return "CHAR(0)" # Uses 1 byte on MySQL (no change on sqlite) + + +@compiles(Unused, "postgresql") # type: ignore[misc,no-untyped-call] +def compile_char_one(type_: TypeDecorator, compiler: Any, **kw: Any) -> str: + """Compile Unused as CHAR(1) on postgresql.""" + return "CHAR(1)" # Uses 1 byte + + +class FAST_PYSQLITE_DATETIME(sqlite.DATETIME): + """Use ciso8601 to parse datetimes instead of sqlalchemy built-in regex.""" + + def result_processor(self, dialect, coltype): # type: ignore[no-untyped-def] + """Offload the datetime parsing to ciso8601.""" + return lambda value: None if value is None else ciso8601.parse_datetime(value) + + +class NativeLargeBinary(LargeBinary): + """A faster version of LargeBinary for engines that support python bytes natively.""" + + def result_processor(self, dialect, coltype): # type: ignore[no-untyped-def] + """No conversion needed for engines that support native bytes.""" + return None + + +# For MariaDB and MySQL we can use an unsigned integer type since it will fit 2**32 +# for sqlite and postgresql we use a bigint +UINT_32_TYPE = BigInteger().with_variant( + mysql.INTEGER(unsigned=True), # type: ignore[no-untyped-call] + "mysql", + "mariadb", +) +JSON_VARIANT_CAST = Text().with_variant( + postgresql.JSON(none_as_null=True), # type: ignore[no-untyped-call] + "postgresql", +) +JSONB_VARIANT_CAST = Text().with_variant( + postgresql.JSONB(none_as_null=True), # type: ignore[no-untyped-call] + "postgresql", +) +DATETIME_TYPE = ( + DateTime(timezone=True) + .with_variant(mysql.DATETIME(timezone=True, fsp=6), "mysql", "mariadb") # type: ignore[no-untyped-call] + .with_variant(FAST_PYSQLITE_DATETIME(), "sqlite") # type: ignore[no-untyped-call] +) +DOUBLE_TYPE = ( + Float() + .with_variant(mysql.DOUBLE(asdecimal=False), "mysql", "mariadb") # type: ignore[no-untyped-call] + .with_variant(oracle.DOUBLE_PRECISION(), "oracle") + .with_variant(postgresql.DOUBLE_PRECISION(), "postgresql") +) +UNUSED_LEGACY_COLUMN = Unused(0) +UNUSED_LEGACY_DATETIME_COLUMN = UnusedDateTime(timezone=True) +UNUSED_LEGACY_INTEGER_COLUMN = SmallInteger() +DOUBLE_PRECISION_TYPE_SQL = "DOUBLE PRECISION" +CONTEXT_BINARY_TYPE = LargeBinary(CONTEXT_ID_BIN_MAX_LENGTH).with_variant( + NativeLargeBinary(CONTEXT_ID_BIN_MAX_LENGTH), "mysql", "mariadb", "sqlite" +) + +TIMESTAMP_TYPE = DOUBLE_TYPE + + +class JSONLiteral(JSON): + """Teach SA how to literalize json.""" + + def literal_processor(self, dialect: Dialect) -> Callable[[Any], str]: + """Processor to convert a value to JSON.""" + + def process(value: Any) -> str: + """Dump json.""" + return JSON_DUMP(value) + + return process + + +EVENT_ORIGIN_ORDER = [EventOrigin.local, EventOrigin.remote] +EVENT_ORIGIN_TO_IDX = {origin: idx for idx, origin in enumerate(EVENT_ORIGIN_ORDER)} + + +class Events(Base): + """Event history data.""" + + __table_args__ = ( + # Used for fetching events at a specific time + # see logbook + Index( + "ix_events_event_type_id_time_fired_ts", "event_type_id", "time_fired_ts" + ), + Index( + EVENTS_CONTEXT_ID_BIN_INDEX, + "context_id_bin", + mysql_length=CONTEXT_ID_BIN_MAX_LENGTH, + mariadb_length=CONTEXT_ID_BIN_MAX_LENGTH, + ), + _DEFAULT_TABLE_ARGS, + ) + __tablename__ = TABLE_EVENTS + event_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) + event_type: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) + event_data: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) + origin: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) + origin_idx: Mapped[int | None] = mapped_column(SmallInteger) + time_fired: Mapped[datetime | None] = mapped_column(UNUSED_LEGACY_DATETIME_COLUMN) + time_fired_ts: Mapped[float | None] = mapped_column(TIMESTAMP_TYPE, index=True) + context_id: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) + context_user_id: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) + context_parent_id: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) + data_id: Mapped[int | None] = mapped_column( + Integer, ForeignKey("event_data.data_id"), index=True + ) + context_id_bin: Mapped[bytes | None] = mapped_column(CONTEXT_BINARY_TYPE) + context_user_id_bin: Mapped[bytes | None] = mapped_column(CONTEXT_BINARY_TYPE) + context_parent_id_bin: Mapped[bytes | None] = mapped_column(CONTEXT_BINARY_TYPE) + event_type_id: Mapped[int | None] = mapped_column( + Integer, ForeignKey("event_types.event_type_id") + ) + event_data_rel: Mapped[EventData | None] = relationship("EventData") + event_type_rel: Mapped[EventTypes | None] = relationship("EventTypes") + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + "" + ) + + @property + def _time_fired_isotime(self) -> str | None: + """Return time_fired as an isotime string.""" + date_time: datetime | None + if self.time_fired_ts is not None: + date_time = dt_util.utc_from_timestamp(self.time_fired_ts) + else: + date_time = process_timestamp(self.time_fired) + if date_time is None: + return None + return date_time.isoformat(sep=" ", timespec="seconds") + + @staticmethod + def from_event(event: Event) -> Events: + """Create an event database object from a native event.""" + return Events( + event_type=None, + event_data=None, + origin_idx=EVENT_ORIGIN_TO_IDX.get(event.origin), + time_fired=None, + time_fired_ts=event.time_fired_timestamp, + context_id=None, + context_id_bin=ulid_to_bytes_or_none(event.context.id), + context_user_id=None, + context_user_id_bin=uuid_hex_to_bytes_or_none(event.context.user_id), + context_parent_id=None, + context_parent_id_bin=ulid_to_bytes_or_none(event.context.parent_id), + ) + + def to_native(self, validate_entity_id: bool = True) -> Event | None: + """Convert to a native HA Event.""" + context = Context( + id=bytes_to_ulid_or_none(self.context_id_bin), + user_id=bytes_to_uuid_hex_or_none(self.context_user_id_bin), + parent_id=bytes_to_ulid_or_none(self.context_parent_id_bin), + ) + try: + return Event( + self.event_type or "", + json_loads_object(self.event_data) if self.event_data else {}, + EventOrigin(self.origin) + if self.origin + else EVENT_ORIGIN_ORDER[self.origin_idx or 0], + dt_util.utc_from_timestamp(self.time_fired_ts or 0), + context=context, + ) + except JSON_DECODE_EXCEPTIONS: + # When json_loads fails + _LOGGER.exception("Error converting to event: %s", self) + return None + + +class EventData(Base): + """Event data history.""" + + __table_args__ = (_DEFAULT_TABLE_ARGS,) + __tablename__ = TABLE_EVENT_DATA + data_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) + hash: Mapped[int | None] = mapped_column(UINT_32_TYPE, index=True) + # Note that this is not named attributes to avoid confusion with the states table + shared_data: Mapped[str | None] = mapped_column( + Text().with_variant(mysql.LONGTEXT, "mysql", "mariadb") + ) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + "" + ) + + @staticmethod + def shared_data_bytes_from_event( + event: Event, dialect: SupportedDialect | None + ) -> bytes: + """Create shared_data from an event.""" + if dialect == SupportedDialect.POSTGRESQL: + bytes_result = json_bytes_strip_null(event.data) + bytes_result = json_bytes(event.data) + if len(bytes_result) > MAX_EVENT_DATA_BYTES: + _LOGGER.warning( + "Event data for %s exceed maximum size of %s bytes. " + "This can cause database performance issues; Event data " + "will not be stored", + event.event_type, + MAX_EVENT_DATA_BYTES, + ) + return b"{}" + return bytes_result + + @staticmethod + def hash_shared_data_bytes(shared_data_bytes: bytes) -> int: + """Return the hash of json encoded shared data.""" + return fnv1a_32(shared_data_bytes) + + def to_native(self) -> dict[str, Any]: + """Convert to an event data dictionary.""" + shared_data = self.shared_data + if shared_data is None: + return {} + try: + return cast(dict[str, Any], json_loads(shared_data)) + except JSON_DECODE_EXCEPTIONS: + _LOGGER.exception("Error converting row to event data: %s", self) + return {} + + +class EventTypes(Base): + """Event type history.""" + + __table_args__ = (_DEFAULT_TABLE_ARGS,) + __tablename__ = TABLE_EVENT_TYPES + event_type_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) + event_type: Mapped[str | None] = mapped_column( + String(MAX_LENGTH_EVENT_EVENT_TYPE), index=True, unique=True + ) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + "" + ) + + +class States(Base): + """State change history.""" + + __table_args__ = ( + # Used for fetching the state of entities at a specific time + # (get_states in history.py) + Index(METADATA_ID_LAST_UPDATED_INDEX_TS, "metadata_id", "last_updated_ts"), + Index( + STATES_CONTEXT_ID_BIN_INDEX, + "context_id_bin", + mysql_length=CONTEXT_ID_BIN_MAX_LENGTH, + mariadb_length=CONTEXT_ID_BIN_MAX_LENGTH, + ), + _DEFAULT_TABLE_ARGS, + ) + __tablename__ = TABLE_STATES + state_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) + entity_id: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) + state: Mapped[str | None] = mapped_column(String(MAX_LENGTH_STATE_STATE)) + attributes: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) + event_id: Mapped[int | None] = mapped_column(UNUSED_LEGACY_INTEGER_COLUMN) + last_changed: Mapped[datetime | None] = mapped_column(UNUSED_LEGACY_DATETIME_COLUMN) + last_changed_ts: Mapped[float | None] = mapped_column(TIMESTAMP_TYPE) + last_updated: Mapped[datetime | None] = mapped_column(UNUSED_LEGACY_DATETIME_COLUMN) + last_updated_ts: Mapped[float | None] = mapped_column( + TIMESTAMP_TYPE, default=time.time, index=True + ) + old_state_id: Mapped[int | None] = mapped_column( + Integer, ForeignKey("states.state_id"), index=True + ) + attributes_id: Mapped[int | None] = mapped_column( + Integer, ForeignKey("state_attributes.attributes_id"), index=True + ) + context_id: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) + context_user_id: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) + context_parent_id: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) + origin_idx: Mapped[int | None] = mapped_column( + SmallInteger + ) # 0 is local, 1 is remote + old_state: Mapped[States | None] = relationship("States", remote_side=[state_id]) + state_attributes: Mapped[StateAttributes | None] = relationship("StateAttributes") + context_id_bin: Mapped[bytes | None] = mapped_column(CONTEXT_BINARY_TYPE) + context_user_id_bin: Mapped[bytes | None] = mapped_column(CONTEXT_BINARY_TYPE) + context_parent_id_bin: Mapped[bytes | None] = mapped_column(CONTEXT_BINARY_TYPE) + metadata_id: Mapped[int | None] = mapped_column( + Integer, ForeignKey("states_meta.metadata_id") + ) + states_meta_rel: Mapped[StatesMeta | None] = relationship("StatesMeta") + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + @property + def _last_updated_isotime(self) -> str | None: + """Return last_updated as an isotime string.""" + date_time: datetime | None + if self.last_updated_ts is not None: + date_time = dt_util.utc_from_timestamp(self.last_updated_ts) + else: + date_time = process_timestamp(self.last_updated) + if date_time is None: + return None + return date_time.isoformat(sep=" ", timespec="seconds") + + @staticmethod + def from_event(event: Event) -> States: + """Create object from a state_changed event.""" + entity_id = event.data["entity_id"] + state: State | None = event.data.get("new_state") + dbstate = States( + entity_id=entity_id, + attributes=None, + context_id=None, + context_id_bin=ulid_to_bytes_or_none(event.context.id), + context_user_id=None, + context_user_id_bin=uuid_hex_to_bytes_or_none(event.context.user_id), + context_parent_id=None, + context_parent_id_bin=ulid_to_bytes_or_none(event.context.parent_id), + origin_idx=EVENT_ORIGIN_TO_IDX.get(event.origin), + last_updated=None, + last_changed=None, + ) + # None state means the state was removed from the state machine + if state is None: + dbstate.state = "" + dbstate.last_updated_ts = event.time_fired_timestamp + dbstate.last_changed_ts = None + return dbstate + + dbstate.state = state.state + dbstate.last_updated_ts = state.last_updated_timestamp + if state.last_updated == state.last_changed: + dbstate.last_changed_ts = None + else: + dbstate.last_changed_ts = state.last_changed_timestamp + + return dbstate + + def to_native(self, validate_entity_id: bool = True) -> State | None: + """Convert to an HA state object.""" + context = Context( + id=bytes_to_ulid_or_none(self.context_id_bin), + user_id=bytes_to_uuid_hex_or_none(self.context_user_id_bin), + parent_id=bytes_to_ulid_or_none(self.context_parent_id_bin), + ) + try: + attrs = json_loads_object(self.attributes) if self.attributes else {} + except JSON_DECODE_EXCEPTIONS: + # When json_loads fails + _LOGGER.exception("Error converting row to state: %s", self) + return None + if self.last_changed_ts is None or self.last_changed_ts == self.last_updated_ts: + last_changed = last_updated = dt_util.utc_from_timestamp( + self.last_updated_ts or 0 + ) + else: + last_updated = dt_util.utc_from_timestamp(self.last_updated_ts or 0) + last_changed = dt_util.utc_from_timestamp(self.last_changed_ts or 0) + return State( + self.entity_id or "", + self.state, # type: ignore[arg-type] + # Join the state_attributes table on attributes_id to get the attributes + # for newer states + attrs, + last_changed, + last_updated, + context=context, + validate_entity_id=validate_entity_id, + ) + + +class StateAttributes(Base): + """State attribute change history.""" + + __table_args__ = (_DEFAULT_TABLE_ARGS,) + __tablename__ = TABLE_STATE_ATTRIBUTES + attributes_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) + hash: Mapped[int | None] = mapped_column(UINT_32_TYPE, index=True) + # Note that this is not named attributes to avoid confusion with the states table + shared_attrs: Mapped[str | None] = mapped_column( + Text().with_variant(mysql.LONGTEXT, "mysql", "mariadb") + ) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + @staticmethod + def shared_attrs_bytes_from_event( + event: Event, + dialect: SupportedDialect | None, + ) -> bytes: + """Create shared_attrs from a state_changed event.""" + state: State | None = event.data.get("new_state") + # None state means the state was removed from the state machine + if state is None: + return b"{}" + if state_info := state.state_info: + exclude_attrs = { + *ALL_DOMAIN_EXCLUDE_ATTRS, + *state_info["unrecorded_attributes"], + } + else: + exclude_attrs = ALL_DOMAIN_EXCLUDE_ATTRS + encoder = json_bytes_strip_null if dialect == PSQL_DIALECT else json_bytes + bytes_result = encoder( + {k: v for k, v in state.attributes.items() if k not in exclude_attrs} + ) + if len(bytes_result) > MAX_STATE_ATTRS_BYTES: + _LOGGER.warning( + "State attributes for %s exceed maximum size of %s bytes. " + "This can cause database performance issues; Attributes " + "will not be stored", + state.entity_id, + MAX_STATE_ATTRS_BYTES, + ) + return b"{}" + return bytes_result + + @staticmethod + def hash_shared_attrs_bytes(shared_attrs_bytes: bytes) -> int: + """Return the hash of json encoded shared attributes.""" + return fnv1a_32(shared_attrs_bytes) + + def to_native(self) -> dict[str, Any]: + """Convert to a state attributes dictionary.""" + shared_attrs = self.shared_attrs + if shared_attrs is None: + return {} + try: + return cast(dict[str, Any], json_loads(shared_attrs)) + except JSON_DECODE_EXCEPTIONS: + # When json_loads fails + _LOGGER.exception("Error converting row to state attributes: %s", self) + return {} + + +class StatesMeta(Base): + """Metadata for states.""" + + __table_args__ = (_DEFAULT_TABLE_ARGS,) + __tablename__ = TABLE_STATES_META + metadata_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) + entity_id: Mapped[str | None] = mapped_column( + String(MAX_LENGTH_STATE_ENTITY_ID), index=True, unique=True + ) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + "" + ) + + +class StatisticsBase: + """Statistics base class.""" + + id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) + created: Mapped[datetime | None] = mapped_column(UNUSED_LEGACY_DATETIME_COLUMN) + created_ts: Mapped[float | None] = mapped_column(TIMESTAMP_TYPE, default=time.time) + metadata_id: Mapped[int | None] = mapped_column( + Integer, + ForeignKey(f"{TABLE_STATISTICS_META}.id", ondelete="CASCADE"), + ) + start: Mapped[datetime | None] = mapped_column(UNUSED_LEGACY_DATETIME_COLUMN) + start_ts: Mapped[float | None] = mapped_column(TIMESTAMP_TYPE, index=True) + mean: Mapped[float | None] = mapped_column(DOUBLE_TYPE) + min: Mapped[float | None] = mapped_column(DOUBLE_TYPE) + max: Mapped[float | None] = mapped_column(DOUBLE_TYPE) + last_reset: Mapped[datetime | None] = mapped_column(UNUSED_LEGACY_DATETIME_COLUMN) + last_reset_ts: Mapped[float | None] = mapped_column(TIMESTAMP_TYPE) + state: Mapped[float | None] = mapped_column(DOUBLE_TYPE) + sum: Mapped[float | None] = mapped_column(DOUBLE_TYPE) + + duration: timedelta + + @classmethod + def from_stats(cls, metadata_id: int, stats: StatisticData) -> Self: + """Create object from a statistics with datatime objects.""" + return cls( # type: ignore[call-arg] + metadata_id=metadata_id, + created=None, + created_ts=time.time(), + start=None, + start_ts=dt_util.utc_to_timestamp(stats["start"]), + mean=stats.get("mean"), + min=stats.get("min"), + max=stats.get("max"), + last_reset=None, + last_reset_ts=datetime_to_timestamp_or_none(stats.get("last_reset")), + state=stats.get("state"), + sum=stats.get("sum"), + ) + + @classmethod + def from_stats_ts(cls, metadata_id: int, stats: StatisticDataTimestamp) -> Self: + """Create object from a statistics with timestamps.""" + return cls( # type: ignore[call-arg] + metadata_id=metadata_id, + created=None, + created_ts=time.time(), + start=None, + start_ts=stats["start_ts"], + mean=stats.get("mean"), + min=stats.get("min"), + max=stats.get("max"), + last_reset=None, + last_reset_ts=stats.get("last_reset_ts"), + state=stats.get("state"), + sum=stats.get("sum"), + ) + + +class Statistics(Base, StatisticsBase): + """Long term statistics.""" + + duration = timedelta(hours=1) + + __table_args__ = ( + # Used for fetching statistics for a certain entity at a specific time + Index( + "ix_statistics_statistic_id_start_ts", + "metadata_id", + "start_ts", + unique=True, + ), + ) + __tablename__ = TABLE_STATISTICS + + +class StatisticsShortTerm(Base, StatisticsBase): + """Short term statistics.""" + + duration = timedelta(minutes=5) + + __table_args__ = ( + # Used for fetching statistics for a certain entity at a specific time + Index( + "ix_statistics_short_term_statistic_id_start_ts", + "metadata_id", + "start_ts", + unique=True, + ), + ) + __tablename__ = TABLE_STATISTICS_SHORT_TERM + + +class StatisticsMeta(Base): + """Statistics meta data.""" + + __table_args__ = (_DEFAULT_TABLE_ARGS,) + __tablename__ = TABLE_STATISTICS_META + id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) + statistic_id: Mapped[str | None] = mapped_column( + String(255), index=True, unique=True + ) + source: Mapped[str | None] = mapped_column(String(32)) + unit_of_measurement: Mapped[str | None] = mapped_column(String(255)) + has_mean: Mapped[bool | None] = mapped_column(Boolean) + has_sum: Mapped[bool | None] = mapped_column(Boolean) + name: Mapped[str | None] = mapped_column(String(255)) + + @staticmethod + def from_meta(meta: StatisticMetaData) -> StatisticsMeta: + """Create object from meta data.""" + return StatisticsMeta(**meta) + + +class RecorderRuns(Base): + """Representation of recorder run.""" + + __table_args__ = (Index("ix_recorder_runs_start_end", "start", "end"),) + __tablename__ = TABLE_RECORDER_RUNS + run_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) + start: Mapped[datetime] = mapped_column(DATETIME_TYPE, default=dt_util.utcnow) + end: Mapped[datetime | None] = mapped_column(DATETIME_TYPE) + closed_incorrect: Mapped[bool] = mapped_column(Boolean, default=False) + created: Mapped[datetime] = mapped_column(DATETIME_TYPE, default=dt_util.utcnow) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + end = ( + f"'{self.end.isoformat(sep=' ', timespec='seconds')}'" if self.end else None + ) + return ( + f"" + ) + + def to_native(self, validate_entity_id: bool = True) -> Self: + """Return self, native format is this model.""" + return self + + +class SchemaChanges(Base): + """Representation of schema version changes.""" + + __tablename__ = TABLE_SCHEMA_CHANGES + change_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) + schema_version: Mapped[int | None] = mapped_column(Integer) + changed: Mapped[datetime] = mapped_column(DATETIME_TYPE, default=dt_util.utcnow) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + "" + ) + + +class StatisticsRuns(Base): + """Representation of statistics run.""" + + __tablename__ = TABLE_STATISTICS_RUNS + run_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) + start: Mapped[datetime] = mapped_column(DATETIME_TYPE, index=True) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + +EVENT_DATA_JSON = type_coerce( + EventData.shared_data.cast(JSONB_VARIANT_CAST), JSONLiteral(none_as_null=True) +) +OLD_FORMAT_EVENT_DATA_JSON = type_coerce( + Events.event_data.cast(JSONB_VARIANT_CAST), JSONLiteral(none_as_null=True) +) + +SHARED_ATTRS_JSON = type_coerce( + StateAttributes.shared_attrs.cast(JSON_VARIANT_CAST), JSON(none_as_null=True) +) +OLD_FORMAT_ATTRS_JSON = type_coerce( + States.attributes.cast(JSON_VARIANT_CAST), JSON(none_as_null=True) +) + +ENTITY_ID_IN_EVENT: ColumnElement = EVENT_DATA_JSON["entity_id"] +OLD_ENTITY_ID_IN_EVENT: ColumnElement = OLD_FORMAT_EVENT_DATA_JSON["entity_id"] +DEVICE_ID_IN_EVENT: ColumnElement = EVENT_DATA_JSON["device_id"] +OLD_STATE = aliased(States, name="old_state") + +SHARED_ATTR_OR_LEGACY_ATTRIBUTES = case( + (StateAttributes.shared_attrs.is_(None), States.attributes), + else_=StateAttributes.shared_attrs, +).label("attributes") +SHARED_DATA_OR_LEGACY_EVENT_DATA = case( + (EventData.shared_data.is_(None), Events.event_data), else_=EventData.shared_data +).label("event_data") diff --git a/tests/components/recorder/test_history_db_schema_42.py b/tests/components/recorder/test_history_db_schema_42.py new file mode 100644 index 00000000000..98ed6089de6 --- /dev/null +++ b/tests/components/recorder/test_history_db_schema_42.py @@ -0,0 +1,1278 @@ +"""The tests the History component.""" + +from __future__ import annotations + +from collections.abc import Callable +from copy import copy +from datetime import datetime, timedelta +import json +from unittest.mock import patch, sentinel + +from freezegun import freeze_time +import pytest +from sqlalchemy import text + +from homeassistant.components import recorder +from homeassistant.components.recorder import Recorder, get_instance, history +from homeassistant.components.recorder.filters import Filters +from homeassistant.components.recorder.history import legacy +from homeassistant.components.recorder.models import process_timestamp +from homeassistant.components.recorder.models.legacy import ( + LegacyLazyState, + LegacyLazyStatePreSchema31, +) +from homeassistant.components.recorder.util import session_scope +import homeassistant.core as ha +from homeassistant.core import HomeAssistant, State +from homeassistant.helpers.json import JSONEncoder +import homeassistant.util.dt as dt_util + +from .common import ( + assert_dict_of_states_equal_without_context_and_last_changed, + assert_multiple_states_equal_without_context, + assert_multiple_states_equal_without_context_and_last_changed, + assert_states_equal_without_context, + async_recorder_block_till_done, + async_wait_recording_done, + old_db_schema, + wait_recording_done, +) +from .db_schema_42 import Events, RecorderRuns, StateAttributes, States, StatesMeta + +from tests.typing import RecorderInstanceGenerator + + +@pytest.fixture(autouse=True) +def db_schema_42(): + """Fixture to initialize the db with the old schema 42.""" + with old_db_schema("42"): + yield + + +async def _async_get_states( + hass: HomeAssistant, + utc_point_in_time: datetime, + entity_ids: list[str] | None = None, + run: RecorderRuns | None = None, + no_attributes: bool = False, +): + """Get states from the database.""" + + def _get_states_with_session(): + with session_scope(hass=hass, read_only=True) as session: + attr_cache = {} + pre_31_schema = get_instance(hass).schema_version < 31 + return [ + LegacyLazyStatePreSchema31(row, attr_cache, None) + if pre_31_schema + else LegacyLazyState( + row, + attr_cache, + None, + row.entity_id, + ) + for row in legacy._get_rows_with_session( + hass, + session, + utc_point_in_time, + entity_ids, + run, + no_attributes, + ) + ] + + return await recorder.get_instance(hass).async_add_executor_job( + _get_states_with_session + ) + + +def _add_db_entries( + hass: ha.HomeAssistant, point: datetime, entity_ids: list[str] +) -> None: + with session_scope(hass=hass) as session: + for idx, entity_id in enumerate(entity_ids): + session.add( + Events( + event_id=1001 + idx, + event_type="state_changed", + event_data="{}", + origin="LOCAL", + time_fired=point, + ) + ) + session.add( + States( + entity_id=entity_id, + state="on", + attributes='{"name":"the light"}', + last_changed=None, + last_updated=point, + event_id=1001 + idx, + attributes_id=1002 + idx, + ) + ) + session.add( + StateAttributes( + shared_attrs='{"name":"the shared light"}', + hash=1234 + idx, + attributes_id=1002 + idx, + ) + ) + + +def test_get_full_significant_states_with_session_entity_no_matches( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test getting states at a specific point in time for entities that never have been recorded.""" + hass = hass_recorder() + now = dt_util.utcnow() + time_before_recorder_ran = now - timedelta(days=1000) + with session_scope(hass=hass, read_only=True) as session: + assert ( + history.get_full_significant_states_with_session( + hass, session, time_before_recorder_ran, now, entity_ids=["demo.id"] + ) + == {} + ) + assert ( + history.get_full_significant_states_with_session( + hass, + session, + time_before_recorder_ran, + now, + entity_ids=["demo.id", "demo.id2"], + ) + == {} + ) + + +def test_significant_states_with_session_entity_minimal_response_no_matches( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test getting states at a specific point in time for entities that never have been recorded.""" + hass = hass_recorder() + now = dt_util.utcnow() + time_before_recorder_ran = now - timedelta(days=1000) + with session_scope(hass=hass, read_only=True) as session: + assert ( + history.get_significant_states_with_session( + hass, + session, + time_before_recorder_ran, + now, + entity_ids=["demo.id"], + minimal_response=True, + ) + == {} + ) + assert ( + history.get_significant_states_with_session( + hass, + session, + time_before_recorder_ran, + now, + entity_ids=["demo.id", "demo.id2"], + minimal_response=True, + ) + == {} + ) + + +def test_significant_states_with_session_single_entity( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test get_significant_states_with_session with a single entity.""" + hass = hass_recorder() + hass.states.set("demo.id", "any", {"attr": True}) + hass.states.set("demo.id", "any2", {"attr": True}) + wait_recording_done(hass) + now = dt_util.utcnow() + with session_scope(hass=hass, read_only=True) as session: + states = history.get_significant_states_with_session( + hass, + session, + now - timedelta(days=1), + now, + entity_ids=["demo.id"], + minimal_response=False, + ) + assert len(states["demo.id"]) == 2 + + +@pytest.mark.parametrize( + ("attributes", "no_attributes", "limit"), + [ + ({"attr": True}, False, 5000), + ({}, True, 5000), + ({"attr": True}, False, 3), + ({}, True, 3), + ], +) +def test_state_changes_during_period( + hass_recorder: Callable[..., HomeAssistant], attributes, no_attributes, limit +) -> None: + """Test state change during period.""" + hass = hass_recorder() + entity_id = "media_player.test" + + def set_state(state): + """Set the state.""" + hass.states.set(entity_id, state, attributes) + wait_recording_done(hass) + return hass.states.get(entity_id) + + start = dt_util.utcnow() + point = start + timedelta(seconds=1) + end = point + timedelta(seconds=1) + + with freeze_time(start) as freezer: + set_state("idle") + set_state("YouTube") + + freezer.move_to(point) + states = [ + set_state("idle"), + set_state("Netflix"), + set_state("Plex"), + set_state("YouTube"), + ] + + freezer.move_to(end) + set_state("Netflix") + set_state("Plex") + + hist = history.state_changes_during_period( + hass, start, end, entity_id, no_attributes, limit=limit + ) + + assert_multiple_states_equal_without_context(states[:limit], hist[entity_id]) + + +def test_state_changes_during_period_last_reported( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test state change during period.""" + hass = hass_recorder() + entity_id = "media_player.test" + + def set_state(state): + """Set the state.""" + hass.states.set(entity_id, state) + wait_recording_done(hass) + return ha.State.from_dict(hass.states.get(entity_id).as_dict()) + + start = dt_util.utcnow() + point1 = start + timedelta(seconds=1) + point2 = point1 + timedelta(seconds=1) + end = point2 + timedelta(seconds=1) + + with freeze_time(start) as freezer: + set_state("idle") + + freezer.move_to(point1) + states = [set_state("YouTube")] + + freezer.move_to(point2) + set_state("YouTube") + + freezer.move_to(end) + set_state("Netflix") + + hist = history.state_changes_during_period(hass, start, end, entity_id) + + assert_multiple_states_equal_without_context(states, hist[entity_id]) + + +def test_state_changes_during_period_descending( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test state change during period descending.""" + hass = hass_recorder() + entity_id = "media_player.test" + + def set_state(state): + """Set the state.""" + hass.states.set(entity_id, state, {"any": 1}) + wait_recording_done(hass) + return hass.states.get(entity_id) + + start = dt_util.utcnow().replace(microsecond=0) + point = start + timedelta(seconds=1) + point2 = start + timedelta(seconds=1, microseconds=100) + point3 = start + timedelta(seconds=1, microseconds=200) + point4 = start + timedelta(seconds=1, microseconds=300) + end = point + timedelta(seconds=1, microseconds=400) + + with freeze_time(start) as freezer: + set_state("idle") + set_state("YouTube") + + freezer.move_to(point) + states = [set_state("idle")] + + freezer.move_to(point2) + states.append(set_state("Netflix")) + + freezer.move_to(point3) + states.append(set_state("Plex")) + + freezer.move_to(point4) + states.append(set_state("YouTube")) + + freezer.move_to(end) + set_state("Netflix") + set_state("Plex") + + hist = history.state_changes_during_period( + hass, start, end, entity_id, no_attributes=False, descending=False + ) + + assert_multiple_states_equal_without_context(states, hist[entity_id]) + + hist = history.state_changes_during_period( + hass, start, end, entity_id, no_attributes=False, descending=True + ) + assert_multiple_states_equal_without_context( + states, list(reversed(list(hist[entity_id]))) + ) + + start_time = point2 + timedelta(microseconds=10) + hist = history.state_changes_during_period( + hass, + start_time, # Pick a point where we will generate a start time state + end, + entity_id, + no_attributes=False, + descending=True, + include_start_time_state=True, + ) + hist_states = list(hist[entity_id]) + assert hist_states[-1].last_updated == start_time + assert hist_states[-1].last_changed == start_time + assert len(hist_states) == 3 + # Make sure they are in descending order + assert ( + hist_states[0].last_updated + > hist_states[1].last_updated + > hist_states[2].last_updated + ) + assert ( + hist_states[0].last_changed + > hist_states[1].last_changed + > hist_states[2].last_changed + ) + hist = history.state_changes_during_period( + hass, + start_time, # Pick a point where we will generate a start time state + end, + entity_id, + no_attributes=False, + descending=False, + include_start_time_state=True, + ) + hist_states = list(hist[entity_id]) + assert hist_states[0].last_updated == start_time + assert hist_states[0].last_changed == start_time + assert len(hist_states) == 3 + # Make sure they are in ascending order + assert ( + hist_states[0].last_updated + < hist_states[1].last_updated + < hist_states[2].last_updated + ) + assert ( + hist_states[0].last_changed + < hist_states[1].last_changed + < hist_states[2].last_changed + ) + + +def test_get_last_state_changes(hass_recorder: Callable[..., HomeAssistant]) -> None: + """Test number of state changes.""" + hass = hass_recorder() + entity_id = "sensor.test" + + def set_state(state): + """Set the state.""" + hass.states.set(entity_id, state) + wait_recording_done(hass) + return hass.states.get(entity_id) + + start = dt_util.utcnow() - timedelta(minutes=2) + point = start + timedelta(minutes=1) + point2 = point + timedelta(minutes=1, seconds=1) + states = [] + + with freeze_time(start) as freezer: + set_state("1") + + freezer.move_to(point) + states.append(set_state("2")) + + freezer.move_to(point2) + states.append(set_state("3")) + + hist = history.get_last_state_changes(hass, 2, entity_id) + + assert_multiple_states_equal_without_context(states, hist[entity_id]) + + +def test_get_last_state_changes_last_reported( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test number of state changes.""" + hass = hass_recorder() + entity_id = "sensor.test" + + def set_state(state): + """Set the state.""" + hass.states.set(entity_id, state) + wait_recording_done(hass) + return ha.State.from_dict(hass.states.get(entity_id).as_dict()) + + start = dt_util.utcnow() - timedelta(minutes=2) + point = start + timedelta(minutes=1) + point2 = point + timedelta(minutes=1, seconds=1) + states = [] + + with freeze_time(start) as freezer: + states.append(set_state("1")) + + freezer.move_to(point) + set_state("1") + + freezer.move_to(point2) + states.append(set_state("2")) + + hist = history.get_last_state_changes(hass, 2, entity_id) + + assert_multiple_states_equal_without_context(states, hist[entity_id]) + + +def test_get_last_state_change(hass_recorder: Callable[..., HomeAssistant]) -> None: + """Test getting the last state change for an entity.""" + hass = hass_recorder() + entity_id = "sensor.test" + + def set_state(state): + """Set the state.""" + hass.states.set(entity_id, state) + wait_recording_done(hass) + return hass.states.get(entity_id) + + start = dt_util.utcnow() - timedelta(minutes=2) + point = start + timedelta(minutes=1) + point2 = point + timedelta(minutes=1, seconds=1) + states = [] + + with freeze_time(start) as freezer: + set_state("1") + + freezer.move_to(point) + set_state("2") + + freezer.move_to(point2) + states.append(set_state("3")) + + hist = history.get_last_state_changes(hass, 1, entity_id) + + assert_multiple_states_equal_without_context(states, hist[entity_id]) + + +def test_ensure_state_can_be_copied( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Ensure a state can pass though copy(). + + The filter integration uses copy() on states + from history. + """ + hass = hass_recorder() + entity_id = "sensor.test" + + def set_state(state): + """Set the state.""" + hass.states.set(entity_id, state) + wait_recording_done(hass) + return hass.states.get(entity_id) + + start = dt_util.utcnow() - timedelta(minutes=2) + point = start + timedelta(minutes=1) + + with freeze_time(start) as freezer: + set_state("1") + + freezer.move_to(point) + set_state("2") + + hist = history.get_last_state_changes(hass, 2, entity_id) + + assert_states_equal_without_context(copy(hist[entity_id][0]), hist[entity_id][0]) + assert_states_equal_without_context(copy(hist[entity_id][1]), hist[entity_id][1]) + + +def test_get_significant_states(hass_recorder: Callable[..., HomeAssistant]) -> None: + """Test that only significant states are returned. + + We should get back every thermostat change that + includes an attribute change, but only the state updates for + media player (attribute changes are not significant and not returned). + """ + hass = hass_recorder() + zero, four, states = record_states(hass) + hist = history.get_significant_states(hass, zero, four, entity_ids=list(states)) + assert_dict_of_states_equal_without_context_and_last_changed(states, hist) + + +def test_get_significant_states_minimal_response( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test that only significant states are returned. + + When minimal responses is set only the first and + last states return a complete state. + + We should get back every thermostat change that + includes an attribute change, but only the state updates for + media player (attribute changes are not significant and not returned). + """ + hass = hass_recorder() + zero, four, states = record_states(hass) + hist = history.get_significant_states( + hass, zero, four, minimal_response=True, entity_ids=list(states) + ) + entites_with_reducable_states = [ + "media_player.test", + "media_player.test3", + ] + + # All states for media_player.test state are reduced + # down to last_changed and state when minimal_response + # is set except for the first state. + # is set. We use JSONEncoder to make sure that are + # pre-encoded last_changed is always the same as what + # will happen with encoding a native state + for entity_id in entites_with_reducable_states: + entity_states = states[entity_id] + for state_idx in range(1, len(entity_states)): + input_state = entity_states[state_idx] + orig_last_changed = orig_last_changed = json.dumps( + process_timestamp(input_state.last_changed), + cls=JSONEncoder, + ).replace('"', "") + orig_state = input_state.state + entity_states[state_idx] = { + "last_changed": orig_last_changed, + "state": orig_state, + } + + assert len(hist) == len(states) + assert_states_equal_without_context( + states["media_player.test"][0], hist["media_player.test"][0] + ) + assert states["media_player.test"][1] == hist["media_player.test"][1] + assert states["media_player.test"][2] == hist["media_player.test"][2] + + assert_multiple_states_equal_without_context( + states["media_player.test2"], hist["media_player.test2"] + ) + assert_states_equal_without_context( + states["media_player.test3"][0], hist["media_player.test3"][0] + ) + assert states["media_player.test3"][1] == hist["media_player.test3"][1] + + assert_multiple_states_equal_without_context( + states["script.can_cancel_this_one"], hist["script.can_cancel_this_one"] + ) + assert_multiple_states_equal_without_context_and_last_changed( + states["thermostat.test"], hist["thermostat.test"] + ) + assert_multiple_states_equal_without_context_and_last_changed( + states["thermostat.test2"], hist["thermostat.test2"] + ) + + +@pytest.mark.parametrize("time_zone", ["Europe/Berlin", "US/Hawaii", "UTC"]) +def test_get_significant_states_with_initial( + time_zone, hass_recorder: Callable[..., HomeAssistant] +) -> None: + """Test that only significant states are returned. + + We should get back every thermostat change that + includes an attribute change, but only the state updates for + media player (attribute changes are not significant and not returned). + """ + hass = hass_recorder() + hass.config.set_time_zone(time_zone) + zero, four, states = record_states(hass) + one_and_half = zero + timedelta(seconds=1.5) + for entity_id in states: + if entity_id == "media_player.test": + states[entity_id] = states[entity_id][1:] + for state in states[entity_id]: + # If the state is recorded before the start time + # start it will have its last_updated and last_changed + # set to the start time. + if state.last_updated < one_and_half: + state.last_updated = one_and_half + state.last_changed = one_and_half + + hist = history.get_significant_states( + hass, one_and_half, four, include_start_time_state=True, entity_ids=list(states) + ) + assert_dict_of_states_equal_without_context_and_last_changed(states, hist) + + +def test_get_significant_states_without_initial( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test that only significant states are returned. + + We should get back every thermostat change that + includes an attribute change, but only the state updates for + media player (attribute changes are not significant and not returned). + """ + hass = hass_recorder() + zero, four, states = record_states(hass) + one = zero + timedelta(seconds=1) + one_with_microsecond = zero + timedelta(seconds=1, microseconds=1) + one_and_half = zero + timedelta(seconds=1.5) + for entity_id in states: + states[entity_id] = list( + filter( + lambda s: s.last_changed != one + and s.last_changed != one_with_microsecond, + states[entity_id], + ) + ) + del states["media_player.test2"] + del states["thermostat.test3"] + + hist = history.get_significant_states( + hass, + one_and_half, + four, + include_start_time_state=False, + entity_ids=list(states), + ) + assert_dict_of_states_equal_without_context_and_last_changed(states, hist) + + +def test_get_significant_states_entity_id( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test that only significant states are returned for one entity.""" + hass = hass_recorder() + zero, four, states = record_states(hass) + del states["media_player.test2"] + del states["media_player.test3"] + del states["thermostat.test"] + del states["thermostat.test2"] + del states["thermostat.test3"] + del states["script.can_cancel_this_one"] + + hist = history.get_significant_states(hass, zero, four, ["media_player.test"]) + assert_dict_of_states_equal_without_context_and_last_changed(states, hist) + + +def test_get_significant_states_multiple_entity_ids( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test that only significant states are returned for one entity.""" + hass = hass_recorder() + zero, four, states = record_states(hass) + + hist = history.get_significant_states( + hass, + zero, + four, + ["media_player.test", "thermostat.test"], + ) + + assert_multiple_states_equal_without_context_and_last_changed( + states["media_player.test"], hist["media_player.test"] + ) + assert_multiple_states_equal_without_context_and_last_changed( + states["thermostat.test"], hist["thermostat.test"] + ) + + +def test_get_significant_states_are_ordered( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test order of results from get_significant_states. + + When entity ids are given, the results should be returned with the data + in the same order. + """ + hass = hass_recorder() + zero, four, _states = record_states(hass) + entity_ids = ["media_player.test", "media_player.test2"] + hist = history.get_significant_states(hass, zero, four, entity_ids) + assert list(hist.keys()) == entity_ids + entity_ids = ["media_player.test2", "media_player.test"] + hist = history.get_significant_states(hass, zero, four, entity_ids) + assert list(hist.keys()) == entity_ids + + +def test_get_significant_states_only( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test significant states when significant_states_only is set.""" + hass = hass_recorder() + entity_id = "sensor.test" + + def set_state(state, **kwargs): + """Set the state.""" + hass.states.set(entity_id, state, **kwargs) + wait_recording_done(hass) + return hass.states.get(entity_id) + + start = dt_util.utcnow() - timedelta(minutes=4) + points = [start + timedelta(minutes=i) for i in range(1, 4)] + + states = [] + with freeze_time(start) as freezer: + set_state("123", attributes={"attribute": 10.64}) + + freezer.move_to(points[0]) + # Attributes are different, state not + states.append(set_state("123", attributes={"attribute": 21.42})) + + freezer.move_to(points[1]) + # state is different, attributes not + states.append(set_state("32", attributes={"attribute": 21.42})) + + freezer.move_to(points[2]) + # everything is different + states.append(set_state("412", attributes={"attribute": 54.23})) + + hist = history.get_significant_states( + hass, + start, + significant_changes_only=True, + entity_ids=list({state.entity_id for state in states}), + ) + + assert len(hist[entity_id]) == 2 + assert not any( + state.last_updated == states[0].last_updated for state in hist[entity_id] + ) + assert any( + state.last_updated == states[1].last_updated for state in hist[entity_id] + ) + assert any( + state.last_updated == states[2].last_updated for state in hist[entity_id] + ) + + hist = history.get_significant_states( + hass, + start, + significant_changes_only=False, + entity_ids=list({state.entity_id for state in states}), + ) + + assert len(hist[entity_id]) == 3 + assert_multiple_states_equal_without_context_and_last_changed( + states, hist[entity_id] + ) + + +async def test_get_significant_states_only_minimal_response( + recorder_mock: Recorder, hass: HomeAssistant +) -> None: + """Test significant states when significant_states_only is True.""" + now = dt_util.utcnow() + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "on", attributes={"any": "attr"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "off", attributes={"any": "attr"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "off", attributes={"any": "changed"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "off", attributes={"any": "again"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "on", attributes={"any": "attr"}) + await async_wait_recording_done(hass) + + hist = history.get_significant_states( + hass, + now, + minimal_response=True, + significant_changes_only=False, + entity_ids=["sensor.test"], + ) + assert len(hist["sensor.test"]) == 3 + + +def record_states(hass) -> tuple[datetime, datetime, dict[str, list[State]]]: + """Record some test states. + + We inject a bunch of state updates from media player, zone and + thermostat. + """ + mp = "media_player.test" + mp2 = "media_player.test2" + mp3 = "media_player.test3" + therm = "thermostat.test" + therm2 = "thermostat.test2" + therm3 = "thermostat.test3" + zone = "zone.home" + script_c = "script.can_cancel_this_one" + + def set_state(entity_id, state, **kwargs): + """Set the state.""" + hass.states.set(entity_id, state, **kwargs) + wait_recording_done(hass) + return hass.states.get(entity_id) + + zero = dt_util.utcnow() + one = zero + timedelta(seconds=1) + two = one + timedelta(seconds=1) + three = two + timedelta(seconds=1) + four = three + timedelta(seconds=1) + + states = {therm: [], therm2: [], therm3: [], mp: [], mp2: [], mp3: [], script_c: []} + with freeze_time(one) as freezer: + states[mp].append( + set_state(mp, "idle", attributes={"media_title": str(sentinel.mt1)}) + ) + states[mp2].append( + set_state(mp2, "YouTube", attributes={"media_title": str(sentinel.mt2)}) + ) + states[mp3].append( + set_state(mp3, "idle", attributes={"media_title": str(sentinel.mt1)}) + ) + states[therm].append( + set_state(therm, 20, attributes={"current_temperature": 19.5}) + ) + # This state will be updated + set_state(therm3, 20, attributes={"current_temperature": 19.5}) + + freezer.move_to(one + timedelta(microseconds=1)) + states[mp].append( + set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt2)}) + ) + + freezer.move_to(two) + # This state will be skipped only different in time + set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt3)}) + # This state will be skipped because domain is excluded + set_state(zone, "zoning") + states[script_c].append( + set_state(script_c, "off", attributes={"can_cancel": True}) + ) + states[therm].append( + set_state(therm, 21, attributes={"current_temperature": 19.8}) + ) + states[therm2].append( + set_state(therm2, 20, attributes={"current_temperature": 19}) + ) + # This state will be updated + set_state(therm3, 20, attributes={"current_temperature": 19.5}) + + freezer.move_to(three) + states[mp].append( + set_state(mp, "Netflix", attributes={"media_title": str(sentinel.mt4)}) + ) + states[mp3].append( + set_state(mp3, "Netflix", attributes={"media_title": str(sentinel.mt3)}) + ) + # Attributes changed even though state is the same + states[therm].append( + set_state(therm, 21, attributes={"current_temperature": 20}) + ) + states[therm3].append( + set_state(therm3, 20, attributes={"current_temperature": 19.5}) + ) + + return zero, four, states + + +async def test_state_changes_during_period_query_during_migration_to_schema_25( + async_setup_recorder_instance: RecorderInstanceGenerator, + hass: HomeAssistant, + recorder_db_url: str, +) -> None: + """Test we can query data prior to schema 25 and during migration to schema 25.""" + if recorder_db_url.startswith(("mysql://", "postgresql://")): + # This test doesn't run on MySQL / MariaDB / Postgresql; we can't drop table state_attributes + return + + instance = await async_setup_recorder_instance(hass, {}) + + with patch.object(instance.states_meta_manager, "active", False): + start = dt_util.utcnow() + point = start + timedelta(seconds=1) + end = point + timedelta(seconds=1) + entity_id = "light.test" + await recorder.get_instance(hass).async_add_executor_job( + _add_db_entries, hass, point, [entity_id] + ) + + no_attributes = True + hist = history.state_changes_during_period( + hass, start, end, entity_id, no_attributes, include_start_time_state=False + ) + state = hist[entity_id][0] + assert state.attributes == {} + + no_attributes = False + hist = history.state_changes_during_period( + hass, start, end, entity_id, no_attributes, include_start_time_state=False + ) + state = hist[entity_id][0] + assert state.attributes == {"name": "the shared light"} + + with instance.engine.connect() as conn: + conn.execute(text("update states set attributes_id=NULL;")) + conn.execute(text("drop table state_attributes;")) + conn.commit() + + with patch.object(instance, "schema_version", 24): + instance.states_meta_manager.active = False + no_attributes = True + hist = history.state_changes_during_period( + hass, + start, + end, + entity_id, + no_attributes, + include_start_time_state=False, + ) + state = hist[entity_id][0] + assert state.attributes == {} + + no_attributes = False + hist = history.state_changes_during_period( + hass, + start, + end, + entity_id, + no_attributes, + include_start_time_state=False, + ) + state = hist[entity_id][0] + assert state.attributes == {"name": "the light"} + + +async def test_get_states_query_during_migration_to_schema_25( + async_setup_recorder_instance: RecorderInstanceGenerator, + hass: HomeAssistant, + recorder_db_url: str, +) -> None: + """Test we can query data prior to schema 25 and during migration to schema 25.""" + if recorder_db_url.startswith(("mysql://", "postgresql://")): + # This test doesn't run on MySQL / MariaDB / Postgresql; we can't drop table state_attributes + return + + instance = await async_setup_recorder_instance(hass, {}) + + start = dt_util.utcnow() + point = start + timedelta(seconds=1) + end = point + timedelta(seconds=1) + entity_id = "light.test" + await instance.async_add_executor_job(_add_db_entries, hass, point, [entity_id]) + assert instance.states_meta_manager.active + + no_attributes = True + hist = await _async_get_states(hass, end, [entity_id], no_attributes=no_attributes) + state = hist[0] + assert state.attributes == {} + + no_attributes = False + hist = await _async_get_states(hass, end, [entity_id], no_attributes=no_attributes) + state = hist[0] + assert state.attributes == {"name": "the shared light"} + + with instance.engine.connect() as conn: + conn.execute(text("update states set attributes_id=NULL;")) + conn.execute(text("drop table state_attributes;")) + conn.commit() + + with patch.object(instance, "schema_version", 24): + instance.states_meta_manager.active = False + no_attributes = True + hist = await _async_get_states( + hass, end, [entity_id], no_attributes=no_attributes + ) + state = hist[0] + assert state.attributes == {} + + no_attributes = False + hist = await _async_get_states( + hass, end, [entity_id], no_attributes=no_attributes + ) + state = hist[0] + assert state.attributes == {"name": "the light"} + + +async def test_get_states_query_during_migration_to_schema_25_multiple_entities( + async_setup_recorder_instance: RecorderInstanceGenerator, + hass: HomeAssistant, + recorder_db_url: str, +) -> None: + """Test we can query data prior to schema 25 and during migration to schema 25.""" + if recorder_db_url.startswith(("mysql://", "postgresql://")): + # This test doesn't run on MySQL / MariaDB / Postgresql; we can't drop table state_attributes + return + + instance = await async_setup_recorder_instance(hass, {}) + + start = dt_util.utcnow() + point = start + timedelta(seconds=1) + end = point + timedelta(seconds=1) + entity_id_1 = "light.test" + entity_id_2 = "switch.test" + entity_ids = [entity_id_1, entity_id_2] + + await instance.async_add_executor_job(_add_db_entries, hass, point, entity_ids) + assert instance.states_meta_manager.active + + no_attributes = True + hist = await _async_get_states(hass, end, entity_ids, no_attributes=no_attributes) + assert hist[0].attributes == {} + assert hist[1].attributes == {} + + no_attributes = False + hist = await _async_get_states(hass, end, entity_ids, no_attributes=no_attributes) + assert hist[0].attributes == {"name": "the shared light"} + assert hist[1].attributes == {"name": "the shared light"} + + with instance.engine.connect() as conn: + conn.execute(text("update states set attributes_id=NULL;")) + conn.execute(text("drop table state_attributes;")) + conn.commit() + + with patch.object(instance, "schema_version", 24): + instance.states_meta_manager.active = False + no_attributes = True + hist = await _async_get_states( + hass, end, entity_ids, no_attributes=no_attributes + ) + assert hist[0].attributes == {} + assert hist[1].attributes == {} + + no_attributes = False + hist = await _async_get_states( + hass, end, entity_ids, no_attributes=no_attributes + ) + assert hist[0].attributes == {"name": "the light"} + assert hist[1].attributes == {"name": "the light"} + + +async def test_get_full_significant_states_handles_empty_last_changed( + async_setup_recorder_instance: RecorderInstanceGenerator, + hass: HomeAssistant, +) -> None: + """Test getting states when last_changed is null.""" + await async_setup_recorder_instance(hass, {}) + + now = dt_util.utcnow() + hass.states.async_set("sensor.one", "on", {"attr": "original"}) + state0 = hass.states.get("sensor.one") + await hass.async_block_till_done() + hass.states.async_set("sensor.one", "on", {"attr": "new"}) + state1 = hass.states.get("sensor.one") + + assert state0.last_changed == state1.last_changed + assert state0.last_updated != state1.last_updated + await async_wait_recording_done(hass) + + def _get_entries(): + with session_scope(hass=hass, read_only=True) as session: + return history.get_full_significant_states_with_session( + hass, + session, + now, + dt_util.utcnow(), + entity_ids=["sensor.one"], + significant_changes_only=False, + ) + + states = await recorder.get_instance(hass).async_add_executor_job(_get_entries) + sensor_one_states: list[State] = states["sensor.one"] + assert_states_equal_without_context(sensor_one_states[0], state0) + assert_states_equal_without_context(sensor_one_states[1], state1) + assert sensor_one_states[0].last_changed == sensor_one_states[1].last_changed + assert sensor_one_states[0].last_updated != sensor_one_states[1].last_updated + + def _fetch_native_states() -> list[State]: + with session_scope(hass=hass, read_only=True) as session: + native_states = [] + db_state_attributes = { + state_attributes.attributes_id: state_attributes + for state_attributes in session.query(StateAttributes) + } + metadata_id_to_entity_id = { + states_meta.metadata_id: states_meta + for states_meta in session.query(StatesMeta) + } + for db_state in session.query(States): + db_state.entity_id = metadata_id_to_entity_id[ + db_state.metadata_id + ].entity_id + state = db_state.to_native() + state.attributes = db_state_attributes[ + db_state.attributes_id + ].to_native() + native_states.append(state) + return native_states + + native_sensor_one_states = await recorder.get_instance(hass).async_add_executor_job( + _fetch_native_states + ) + assert_states_equal_without_context(native_sensor_one_states[0], state0) + assert_states_equal_without_context(native_sensor_one_states[1], state1) + assert ( + native_sensor_one_states[0].last_changed + == native_sensor_one_states[1].last_changed + ) + assert ( + native_sensor_one_states[0].last_updated + != native_sensor_one_states[1].last_updated + ) + + def _fetch_db_states() -> list[States]: + with session_scope(hass=hass, read_only=True) as session: + states = list(session.query(States)) + session.expunge_all() + return states + + db_sensor_one_states = await recorder.get_instance(hass).async_add_executor_job( + _fetch_db_states + ) + assert db_sensor_one_states[0].last_changed is None + assert db_sensor_one_states[0].last_changed_ts is None + + assert ( + process_timestamp( + dt_util.utc_from_timestamp(db_sensor_one_states[1].last_changed_ts) + ) + == state0.last_changed + ) + assert db_sensor_one_states[0].last_updated_ts is not None + assert db_sensor_one_states[1].last_updated_ts is not None + assert ( + db_sensor_one_states[0].last_updated_ts + != db_sensor_one_states[1].last_updated_ts + ) + + +def test_state_changes_during_period_multiple_entities_single_test( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test state change during period with multiple entities in the same test. + + This test ensures the sqlalchemy query cache does not + generate incorrect results. + """ + hass = hass_recorder() + start = dt_util.utcnow() + test_entites = {f"sensor.{i}": str(i) for i in range(30)} + for entity_id, value in test_entites.items(): + hass.states.set(entity_id, value) + + wait_recording_done(hass) + end = dt_util.utcnow() + + for entity_id, value in test_entites.items(): + hist = history.state_changes_during_period(hass, start, end, entity_id) + assert len(hist) == 1 + assert hist[entity_id][0].state == value + + +@pytest.mark.freeze_time("2039-01-19 03:14:07.555555-00:00") +async def test_get_full_significant_states_past_year_2038( + async_setup_recorder_instance: RecorderInstanceGenerator, + hass: HomeAssistant, +) -> None: + """Test we can store times past year 2038.""" + await async_setup_recorder_instance(hass, {}) + past_2038_time = dt_util.parse_datetime("2039-01-19 03:14:07.555555-00:00") + hass.states.async_set("sensor.one", "on", {"attr": "original"}) + state0 = hass.states.get("sensor.one") + await hass.async_block_till_done() + + hass.states.async_set("sensor.one", "on", {"attr": "new"}) + state1 = hass.states.get("sensor.one") + + await async_wait_recording_done(hass) + + def _get_entries(): + with session_scope(hass=hass, read_only=True) as session: + return history.get_full_significant_states_with_session( + hass, + session, + past_2038_time - timedelta(days=365), + past_2038_time + timedelta(days=365), + entity_ids=["sensor.one"], + significant_changes_only=False, + ) + + states = await recorder.get_instance(hass).async_add_executor_job(_get_entries) + sensor_one_states: list[State] = states["sensor.one"] + assert_states_equal_without_context(sensor_one_states[0], state0) + assert_states_equal_without_context(sensor_one_states[1], state1) + assert sensor_one_states[0].last_changed == past_2038_time + assert sensor_one_states[0].last_updated == past_2038_time + + +def test_get_significant_states_without_entity_ids_raises( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test at least one entity id is required for get_significant_states.""" + hass = hass_recorder() + now = dt_util.utcnow() + with pytest.raises(ValueError, match="entity_ids must be provided"): + history.get_significant_states(hass, now, None) + + +def test_state_changes_during_period_without_entity_ids_raises( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test at least one entity id is required for state_changes_during_period.""" + hass = hass_recorder() + now = dt_util.utcnow() + with pytest.raises(ValueError, match="entity_id must be provided"): + history.state_changes_during_period(hass, now, None) + + +def test_get_significant_states_with_filters_raises( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test passing filters is no longer supported.""" + hass = hass_recorder() + now = dt_util.utcnow() + with pytest.raises(NotImplementedError, match="Filters are no longer supported"): + history.get_significant_states( + hass, now, None, ["media_player.test"], Filters() + ) + + +def test_get_significant_states_with_non_existent_entity_ids_returns_empty( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test get_significant_states returns an empty dict when entities not in the db.""" + hass = hass_recorder() + now = dt_util.utcnow() + assert history.get_significant_states(hass, now, None, ["nonexistent.entity"]) == {} + + +def test_state_changes_during_period_with_non_existent_entity_ids_returns_empty( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test state_changes_during_period returns an empty dict when entities not in the db.""" + hass = hass_recorder() + now = dt_util.utcnow() + assert ( + history.state_changes_during_period(hass, now, None, "nonexistent.entity") == {} + ) + + +def test_get_last_state_changes_with_non_existent_entity_ids_returns_empty( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test get_last_state_changes returns an empty dict when entities not in the db.""" + hass = hass_recorder() + assert history.get_last_state_changes(hass, 1, "nonexistent.entity") == {} From f4922edb4b8a9935e15bda87a8155b6164381ee0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 27 Mar 2024 18:29:43 -1000 Subject: [PATCH 011/967] Fix empty delays in script helper (#114346) fixes ``` Logger: homeassistant.components.automation.kamermaster_knop_4_acties_licht Bron: components/automation/__init__.py:726 integratie: Automatisering (documentatie, problemen) Eerst voorgekomen: 22:17:29 (5 gebeurtenissen) Laatst gelogd: 22:59:24 While executing automation automation.kamermaster_knop_4_acties_licht Traceback (most recent call last): File "/usr/src/homeassistant/homeassistant/components/automation/__init__.py", line 726, in async_trigger return await self.action_script.async_run( ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 1645, in async_run return await asyncio.shield(create_eager_task(run.async_run())) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 454, in async_run await self._async_step(log_exceptions=False) File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 506, in _async_step self._handle_exception( File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 536, in _handle_exception raise exception File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 504, in _async_step await getattr(self, handler)() File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 626, in _async_delay_step if timeout_future.done(): ^^^^^^^^^^^^^^^^^^^ AttributeError: 'NoneType' object has no attribute 'done' ``` --- homeassistant/helpers/script.py | 5 +++++ tests/helpers/test_script.py | 25 +++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 560f3227c4f..a86df259f11 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -615,6 +615,11 @@ class _ScriptRun: delay = delay_delta.total_seconds() self._changed() + if not delay: + # Handle an empty delay + trace_set_result(delay=delay, done=True) + return + trace_set_result(delay=delay, done=False) futures, timeout_handle, timeout_future = self._async_futures_with_timeout( delay diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index c1462ccfc2f..86fb84eb582 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -672,6 +672,31 @@ async def test_delay_basic(hass: HomeAssistant) -> None: ) +async def test_empty_delay(hass: HomeAssistant) -> None: + """Test an empty delay.""" + delay_alias = "delay step" + sequence = cv.SCRIPT_SCHEMA({"delay": {"seconds": 0}, "alias": delay_alias}) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + delay_started_flag = async_watch_for_action(script_obj, delay_alias) + + try: + await script_obj.async_run(context=Context()) + await asyncio.wait_for(delay_started_flag.wait(), 1) + except (AssertionError, TimeoutError): + await script_obj.async_stop() + raise + else: + await hass.async_block_till_done() + assert not script_obj.is_running + assert script_obj.last_action is None + + assert_action_trace( + { + "0": [{"result": {"delay": 0.0, "done": True}}], + } + ) + + async def test_multiple_runs_delay(hass: HomeAssistant) -> None: """Test multiple runs with delay in script.""" event = "test_event" From ae0b41f7a79c04c8ddcab3fa9cf26f0acfbe7889 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 28 Mar 2024 06:57:02 +0100 Subject: [PATCH 012/967] Bump fjaraskupan to 2.3.0 (#114344) Update fjarakupen to 2.3.0 - Support delayed disconnection - Speed up on/off transitions --- homeassistant/components/fjaraskupan/light.py | 5 +++-- homeassistant/components/fjaraskupan/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/fjaraskupan/light.py b/homeassistant/components/fjaraskupan/light.py index 7f33d7806ee..b33904c805d 100644 --- a/homeassistant/components/fjaraskupan/light.py +++ b/homeassistant/components/fjaraskupan/light.py @@ -54,13 +54,14 @@ class Light(CoordinatorEntity[FjaraskupanCoordinator], LightEntity): async with self.coordinator.async_connect_and_update() as device: if ATTR_BRIGHTNESS in kwargs: await device.send_dim(int(kwargs[ATTR_BRIGHTNESS] * (100.0 / 255.0))) - elif not self.is_on: - await device.send_command(COMMAND_LIGHT_ON_OFF) + else: + await device.send_dim(100) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" if self.is_on: async with self.coordinator.async_connect_and_update() as device: + await device.send_dim(0) await device.send_command(COMMAND_LIGHT_ON_OFF) @property diff --git a/homeassistant/components/fjaraskupan/manifest.json b/homeassistant/components/fjaraskupan/manifest.json index f7ad701a756..91c74b68e01 100644 --- a/homeassistant/components/fjaraskupan/manifest.json +++ b/homeassistant/components/fjaraskupan/manifest.json @@ -14,5 +14,5 @@ "documentation": "https://www.home-assistant.io/integrations/fjaraskupan", "iot_class": "local_polling", "loggers": ["bleak", "fjaraskupan"], - "requirements": ["fjaraskupan==2.2.0"] + "requirements": ["fjaraskupan==2.3.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index ace129c69f2..a5a0ba98d00 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -864,7 +864,7 @@ fivem-api==0.1.2 fixerio==1.0.0a0 # homeassistant.components.fjaraskupan -fjaraskupan==2.2.0 +fjaraskupan==2.3.0 # homeassistant.components.flexit_bacnet flexit_bacnet==2.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b01a7ca8ba4..56e25ae045f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -702,7 +702,7 @@ fitbit==0.3.1 fivem-api==0.1.2 # homeassistant.components.fjaraskupan -fjaraskupan==2.2.0 +fjaraskupan==2.3.0 # homeassistant.components.flexit_bacnet flexit_bacnet==2.1.0 From bec45dacf01f348a6ad8dcd13c4c1ab7e31b7da9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 27 Mar 2024 20:52:45 -1000 Subject: [PATCH 013/967] Add additional coverage to the ESPHome manager (#114265) --- homeassistant/components/esphome/manager.py | 2 +- tests/components/esphome/conftest.py | 12 ++++ tests/components/esphome/test_manager.py | 71 ++++++++++++++++++++- 3 files changed, 83 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index dc95952194e..bbd54154521 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -170,7 +170,7 @@ class ESPHomeManager: self.entry_data = entry_data async def on_stop(self, event: Event) -> None: - """Cleanup the socket client on HA stop.""" + """Cleanup the socket client on HA close.""" await cleanup_instance(self.hass, self.entry) @property diff --git a/tests/components/esphome/conftest.py b/tests/components/esphome/conftest.py index e51fc663b59..cb6655f710c 100644 --- a/tests/components/esphome/conftest.py +++ b/tests/components/esphome/conftest.py @@ -188,6 +188,7 @@ class MockESPHomeDevice: self.service_call_callback: Callable[[HomeassistantServiceCall], None] self.on_disconnect: Callable[[bool], None] self.on_connect: Callable[[bool], None] + self.on_connect_error: Callable[[Exception], None] self.home_assistant_state_subscription_callback: Callable[ [str, str | None], None ] @@ -222,10 +223,20 @@ class MockESPHomeDevice: """Set the connect callback.""" self.on_connect = on_connect + def set_on_connect_error( + self, on_connect_error: Callable[[Exception], None] + ) -> None: + """Set the connect error callback.""" + self.on_connect_error = on_connect_error + async def mock_connect(self) -> None: """Mock connecting.""" await self.on_connect() + async def mock_connect_error(self, exc: Exception) -> None: + """Mock connect error.""" + await self.on_connect_error(exc) + def set_home_assistant_state_subscription_callback( self, on_state_sub: Callable[[str, str | None], None], @@ -309,6 +320,7 @@ async def _mock_generic_device_entry( super().__init__(*args, **kwargs) mock_device.set_on_disconnect(kwargs["on_disconnect"]) mock_device.set_on_connect(kwargs["on_connect"]) + mock_device.set_on_connect_error(kwargs["on_connect_error"]) self._try_connect = self.mock_try_connect async def mock_try_connect(self): diff --git a/tests/components/esphome/test_manager.py b/tests/components/esphome/test_manager.py index 55369e54b53..e4d816089b8 100644 --- a/tests/components/esphome/test_manager.py +++ b/tests/components/esphome/test_manager.py @@ -11,6 +11,9 @@ from aioesphomeapi import ( EntityInfo, EntityState, HomeassistantServiceCall, + InvalidAuthAPIError, + InvalidEncryptionKeyAPIError, + RequiresEncryptionAPIError, UserService, UserServiceArg, UserServiceArgType, @@ -25,7 +28,12 @@ from homeassistant.components.esphome.const import ( DOMAIN, STABLE_BLE_VERSION_STR, ) -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + EVENT_HOMEASSISTANT_CLOSE, +) from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import device_registry as dr, issue_registry as ir @@ -1083,3 +1091,64 @@ async def test_esphome_device_with_compilation_time( connections={(dr.CONNECTION_NETWORK_MAC, entry.unique_id)} ) assert "comp_time" in dev.sw_version + + +async def test_disconnects_at_close_event( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], +) -> None: + """Test the device is disconnected at the close event.""" + await mock_esphome_device( + mock_client=mock_client, + entity_info=[], + user_service=[], + device_info={"compilation_time": "comp_time"}, + states=[], + ) + await hass.async_block_till_done() + + assert mock_client.disconnect.call_count == 0 + + hass.bus.async_fire(EVENT_HOMEASSISTANT_CLOSE) + await hass.async_block_till_done() + assert mock_client.disconnect.call_count == 1 + + +@pytest.mark.parametrize( + "error", + [ + RequiresEncryptionAPIError, + InvalidEncryptionKeyAPIError, + InvalidAuthAPIError, + ], +) +async def test_start_reauth( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], + error: Exception, +) -> None: + """Test exceptions on connect error trigger reauth.""" + device = await mock_esphome_device( + mock_client=mock_client, + entity_info=[], + user_service=[], + device_info={"compilation_time": "comp_time"}, + states=[], + ) + await hass.async_block_till_done() + + await device.mock_connect_error(error("fail")) + await hass.async_block_till_done() + + flows = hass.config_entries.flow.async_progress(DOMAIN) + assert len(flows) == 1 + flow = flows[0] + assert flow["context"]["source"] == "reauth" From a07dc85bf4c889f35da31e5f2eb6a98844952087 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 27 Mar 2024 20:58:07 -1000 Subject: [PATCH 014/967] Revert velocity change in powerview (#114337) --- .../components/hunterdouglas_powerview/number.py | 2 +- .../hunterdouglas_powerview/shade_data.py | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/hunterdouglas_powerview/number.py b/homeassistant/components/hunterdouglas_powerview/number.py index 8551a11337e..b37331c08df 100644 --- a/homeassistant/components/hunterdouglas_powerview/number.py +++ b/homeassistant/components/hunterdouglas_powerview/number.py @@ -41,7 +41,7 @@ def store_velocity( value: float | None, ) -> None: """Store the desired shade velocity in the coordinator.""" - coordinator.data.update_shade_position(shade_id, ShadePosition(velocity=value)) + coordinator.data.update_shade_velocity(shade_id, ShadePosition(velocity=value)) NUMBERS: Final = ( diff --git a/homeassistant/components/hunterdouglas_powerview/shade_data.py b/homeassistant/components/hunterdouglas_powerview/shade_data.py index e6b20312f27..fd2f0466467 100644 --- a/homeassistant/components/hunterdouglas_powerview/shade_data.py +++ b/homeassistant/components/hunterdouglas_powerview/shade_data.py @@ -13,14 +13,11 @@ from .util import async_map_data_by_id _LOGGER = logging.getLogger(__name__) -POSITION_FIELDS = fields(ShadePosition) +POSITION_FIELDS = [field for field in fields(ShadePosition) if field.name != "velocity"] def copy_position_data(source: ShadePosition, target: ShadePosition) -> ShadePosition: """Copy position data from source to target for None values only.""" - # the hub will always return a velocity of 0 on initial connect, - # separate definition to store consistent value in HA - # this value is purely driven from HA for field in POSITION_FIELDS: if (value := getattr(source, field.name)) is not None: setattr(target, field.name, value) @@ -76,3 +73,11 @@ class PowerviewShadeData: def update_shade_position(self, shade_id: int, new_position: ShadePosition) -> None: """Update a single shades position.""" copy_position_data(new_position, self.get_shade_position(shade_id)) + + def update_shade_velocity(self, shade_id: int, shade_data: ShadePosition) -> None: + """Update a single shades velocity.""" + # the hub will always return a velocity of 0 on initial connect, + # separate definition to store consistent value in HA + # this value is purely driven from HA + if shade_data.velocity is not None: + self.get_shade_position(shade_id).velocity = shade_data.velocity From ed90df309c78677f52132593edc26c18c6ed16ea Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Thu, 28 Mar 2024 09:11:02 +0100 Subject: [PATCH 015/967] Fix script for checking on existing translations (#114354) --- .github/workflows/builder.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 5dc01eee21e..217093793d1 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -53,11 +53,9 @@ jobs: - name: Fail if translations files are checked in run: | - files=$(find homeassistant/components/*/translations -type f) - - if [ -n "$files" ]; then + if [ -n "$(find homeassistant/components/*/translations -type f)" ]; then echo "Translations files are checked in, please remove the following files:" - echo "$files" + find homeassistant/components/*/translations -type f exit 1 fi From 071c3abb69383355e3b8202d80ce1505263485ee Mon Sep 17 00:00:00 2001 From: Yuxin Wang Date: Thu, 28 Mar 2024 05:17:12 -0400 Subject: [PATCH 016/967] Define PARALLEL_UPDATES for APCUPSD (#114134) --- homeassistant/components/apcupsd/binary_sensor.py | 2 ++ homeassistant/components/apcupsd/sensor.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/homeassistant/components/apcupsd/binary_sensor.py b/homeassistant/components/apcupsd/binary_sensor.py index bc214e56d80..77b2b8591e5 100644 --- a/homeassistant/components/apcupsd/binary_sensor.py +++ b/homeassistant/components/apcupsd/binary_sensor.py @@ -17,6 +17,8 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN from .coordinator import APCUPSdCoordinator +PARALLEL_UPDATES = 0 + _LOGGER = logging.getLogger(__name__) _DESCRIPTION = BinarySensorEntityDescription( key="statflag", diff --git a/homeassistant/components/apcupsd/sensor.py b/homeassistant/components/apcupsd/sensor.py index 008171cfe3c..6ac33072856 100644 --- a/homeassistant/components/apcupsd/sensor.py +++ b/homeassistant/components/apcupsd/sensor.py @@ -28,6 +28,8 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN from .coordinator import APCUPSdCoordinator +PARALLEL_UPDATES = 0 + _LOGGER = logging.getLogger(__name__) SENSORS: dict[str, SensorEntityDescription] = { From f7b7f74d10bee236d2d32ad885a33b5e60a69a47 Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Thu, 28 Mar 2024 10:18:07 +0100 Subject: [PATCH 017/967] Enable Ruff TRY201 (#114269) * Enable Ruff TRY201 * remove redundant rules --- .../components/airthings_ble/config_flow.py | 2 +- .../components/aladdin_connect/config_flow.py | 4 ++-- .../components/aurora_abb_powerone/config_flow.py | 4 ++-- homeassistant/components/automation/trace.py | 2 +- homeassistant/components/deluge/coordinator.py | 2 +- homeassistant/components/ecovacs/config_flow.py | 2 +- homeassistant/components/freebox/router.py | 2 +- homeassistant/components/fronius/coordinator.py | 4 ++-- homeassistant/components/generic/config_flow.py | 2 +- .../components/google_assistant_sdk/helpers.py | 2 +- homeassistant/components/google_sheets/__init__.py | 4 ++-- homeassistant/components/http/headers.py | 2 +- homeassistant/components/lametric/config_flow.py | 8 ++++---- homeassistant/components/onkyo/media_player.py | 4 ++-- homeassistant/components/onvif/config_flow.py | 2 +- homeassistant/components/onvif/device.py | 2 +- homeassistant/components/reolink/host.py | 4 ++-- homeassistant/components/risco/config_flow.py | 4 ++-- homeassistant/components/roborock/__init__.py | 2 +- homeassistant/components/script/trace.py | 2 +- .../components/signal_messenger/notify.py | 6 +++--- homeassistant/components/stream/worker.py | 8 ++++---- homeassistant/components/syncthru/__init__.py | 2 +- homeassistant/components/synology_dsm/common.py | 4 ++-- homeassistant/components/tank_utility/sensor.py | 2 +- homeassistant/components/tessie/coordinator.py | 2 +- homeassistant/components/twitch/config_flow.py | 4 ++-- homeassistant/components/voip/voip.py | 4 ++-- homeassistant/components/wallbox/coordinator.py | 4 ++-- homeassistant/components/wiz/__init__.py | 4 ++-- homeassistant/helpers/condition.py | 2 +- homeassistant/helpers/script.py | 14 +++++++------- homeassistant/helpers/template.py | 4 ++-- homeassistant/helpers/update_coordinator.py | 2 +- pyproject.toml | 11 ++++++++--- script/lint_and_test.py | 4 ++-- 36 files changed, 71 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/airthings_ble/config_flow.py b/homeassistant/components/airthings_ble/config_flow.py index 5f08f198761..d525aee04b1 100644 --- a/homeassistant/components/airthings_ble/config_flow.py +++ b/homeassistant/components/airthings_ble/config_flow.py @@ -87,7 +87,7 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN): _LOGGER.error( "Unknown error occurred from %s: %s", discovery_info.address, err ) - raise err + raise return data async def async_step_bluetooth( diff --git a/homeassistant/components/aladdin_connect/config_flow.py b/homeassistant/components/aladdin_connect/config_flow.py index df822086db7..e960138853a 100644 --- a/homeassistant/components/aladdin_connect/config_flow.py +++ b/homeassistant/components/aladdin_connect/config_flow.py @@ -41,8 +41,8 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None: ) try: await acc.login() - except (ClientError, TimeoutError, Aladdin.ConnectionError) as ex: - raise ex + except (ClientError, TimeoutError, Aladdin.ConnectionError): + raise except Aladdin.InvalidPasswordError as ex: raise InvalidAuth from ex diff --git a/homeassistant/components/aurora_abb_powerone/config_flow.py b/homeassistant/components/aurora_abb_powerone/config_flow.py index 3f635595258..a1e046f302f 100644 --- a/homeassistant/components/aurora_abb_powerone/config_flow.py +++ b/homeassistant/components/aurora_abb_powerone/config_flow.py @@ -46,9 +46,9 @@ def validate_and_connect( ret[ATTR_MODEL] = f"{client.version()} ({client.pn()})" ret[ATTR_FIRMWARE] = client.firmware(1) _LOGGER.info("Returning device info=%s", ret) - except AuroraError as err: + except AuroraError: _LOGGER.warning("Could not connect to device=%s", comport) - raise err + raise finally: if client.serline.isOpen(): client.close() diff --git a/homeassistant/components/automation/trace.py b/homeassistant/components/automation/trace.py index 754c062ec2c..e7f671e6f05 100644 --- a/homeassistant/components/automation/trace.py +++ b/homeassistant/components/automation/trace.py @@ -65,7 +65,7 @@ def trace_automation( except Exception as ex: if automation_id: trace.set_error(ex) - raise ex + raise finally: if automation_id: trace.finished() diff --git a/homeassistant/components/deluge/coordinator.py b/homeassistant/components/deluge/coordinator.py index 6b3c177b90d..c3dd25609fe 100644 --- a/homeassistant/components/deluge/coordinator.py +++ b/homeassistant/components/deluge/coordinator.py @@ -63,5 +63,5 @@ class DelugeDataUpdateCoordinator( "Credentials for Deluge client are not valid" ) from ex LOGGER.error("Unknown error connecting to Deluge: %s", ex) - raise ex + raise return data diff --git a/homeassistant/components/ecovacs/config_flow.py b/homeassistant/components/ecovacs/config_flow.py index 8cf82f6237c..a1ea19144b0 100644 --- a/homeassistant/components/ecovacs/config_flow.py +++ b/homeassistant/components/ecovacs/config_flow.py @@ -303,7 +303,7 @@ class EcovacsConfigFlow(ConfigFlow, domain=DOMAIN): except AbortFlow as ex: if ex.reason == "already_configured": create_repair() - raise ex + raise if errors := result.get("errors"): error = errors["base"] diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index 26b3e37beb3..ef16a9df1b1 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -87,7 +87,7 @@ async def get_hosts_list_if_supported( ) else: - raise err + raise return supports_hosts, fbx_devices diff --git a/homeassistant/components/fronius/coordinator.py b/homeassistant/components/fronius/coordinator.py index d0a20b25bee..1ecd74a6e09 100644 --- a/homeassistant/components/fronius/coordinator.py +++ b/homeassistant/components/fronius/coordinator.py @@ -152,9 +152,9 @@ class FroniusInverterUpdateCoordinator(FroniusCoordinatorBase): data = await self.solar_net.fronius.current_inverter_data( self.inverter_info.solar_net_id ) - except BadStatusError as err: + except BadStatusError: if silent_retry == (self.SILENT_RETRIES - 1): - raise err + raise continue break # wrap a single devices data in a dict with solar_net_id key for diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py index 8fdc0143700..af33ae3b36f 100644 --- a/homeassistant/components/generic/config_flow.py +++ b/homeassistant/components/generic/config_flow.py @@ -291,7 +291,7 @@ async def async_test_stream( return {CONF_STREAM_SOURCE: "stream_no_route_to_host"} if err.errno == EIO: # input/output error return {CONF_STREAM_SOURCE: "stream_io_error"} - raise err + raise return {} diff --git a/homeassistant/components/google_assistant_sdk/helpers.py b/homeassistant/components/google_assistant_sdk/helpers.py index 0c4d081961f..ccd0fe765ac 100644 --- a/homeassistant/components/google_assistant_sdk/helpers.py +++ b/homeassistant/components/google_assistant_sdk/helpers.py @@ -70,7 +70,7 @@ async def async_send_text_commands( except aiohttp.ClientResponseError as err: if 400 <= err.status < 500: entry.async_start_reauth(hass) - raise err + raise credentials = Credentials(session.token[CONF_ACCESS_TOKEN]) language_code = entry.options.get(CONF_LANGUAGE_CODE, default_language_code(hass)) diff --git a/homeassistant/components/google_sheets/__init__.py b/homeassistant/components/google_sheets/__init__.py index bf7cf7c40df..f346f913e0c 100644 --- a/homeassistant/components/google_sheets/__init__.py +++ b/homeassistant/components/google_sheets/__init__.py @@ -96,9 +96,9 @@ async def async_setup_service(hass: HomeAssistant) -> None: service = Client(Credentials(entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN])) try: sheet = service.open_by_key(entry.unique_id) - except RefreshError as ex: + except RefreshError: entry.async_start_reauth(hass) - raise ex + raise except APIError as ex: raise HomeAssistantError("Failed to write data") from ex diff --git a/homeassistant/components/http/headers.py b/homeassistant/components/http/headers.py index bd05401ebce..3c845601183 100644 --- a/homeassistant/components/http/headers.py +++ b/homeassistant/components/http/headers.py @@ -33,7 +33,7 @@ def setup_headers(app: Application, use_x_frame_options: bool) -> None: except HTTPException as err: for key, value in added_headers.items(): err.headers[key] = value - raise err + raise for key, value in added_headers.items(): response.headers[key] = value diff --git a/homeassistant/components/lametric/config_flow.py b/homeassistant/components/lametric/config_flow.py index ed1477e1149..f21b0cb0a3c 100644 --- a/homeassistant/components/lametric/config_flow.py +++ b/homeassistant/components/lametric/config_flow.py @@ -147,8 +147,8 @@ class LaMetricFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): return await self._async_step_create_entry( host, user_input[CONF_API_KEY] ) - except AbortFlow as ex: - raise ex + except AbortFlow: + raise except LaMetricConnectionError as ex: LOGGER.error("Error connecting to LaMetric: %s", ex) errors["base"] = "cannot_connect" @@ -209,8 +209,8 @@ class LaMetricFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): return await self._async_step_create_entry( str(device.ip), device.api_key ) - except AbortFlow as ex: - raise ex + except AbortFlow: + raise except LaMetricConnectionError as ex: LOGGER.error("Error connecting to LaMetric: %s", ex) errors["base"] = "cannot_connect" diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py index ef0105bd6d2..c0503e6e850 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -143,7 +143,7 @@ def determine_zones(receiver): _LOGGER.debug("Zone 2 not available") except ValueError as error: if str(error) != TIMEOUT_MESSAGE: - raise error + raise _LOGGER.debug("Zone 2 timed out, assuming no functionality") try: _LOGGER.debug("Checking for zone 3 capability") @@ -154,7 +154,7 @@ def determine_zones(receiver): _LOGGER.debug("Zone 3 not available") except ValueError as error: if str(error) != TIMEOUT_MESSAGE: - raise error + raise _LOGGER.debug("Zone 3 timed out, assuming no functionality") except AssertionError: _LOGGER.error("Zone 3 detection failed") diff --git a/homeassistant/components/onvif/config_flow.py b/homeassistant/components/onvif/config_flow.py index 515f9cd5f68..5bd81f2bdea 100644 --- a/homeassistant/components/onvif/config_flow.py +++ b/homeassistant/components/onvif/config_flow.py @@ -311,7 +311,7 @@ class OnvifFlowHandler(ConfigFlow, domain=DOMAIN): self.device_id = interface.Info.HwAddress except Fault as fault: if "not implemented" not in fault.message: - raise fault + raise LOGGER.debug( "%s: Could not get network interfaces: %s", self.onvif_config[CONF_NAME], diff --git a/homeassistant/components/onvif/device.py b/homeassistant/components/onvif/device.py index 2001d95e2d4..71acf62f97d 100644 --- a/homeassistant/components/onvif/device.py +++ b/homeassistant/components/onvif/device.py @@ -344,7 +344,7 @@ class ONVIFDevice: mac = interface.Info.HwAddress except Fault as fault: if "not implemented" not in fault.message: - raise fault + raise LOGGER.debug( "Couldn't get network interfaces from ONVIF device '%s'. Error: %s", diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index 44750cdeb3c..73e6ddd6115 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -351,7 +351,7 @@ class ReolinkHost: await self._api.subscribe(sub_type=SubType.long_poll) except NotSupportedError as err: if initial: - raise err + raise # make sure the long_poll_task is always created to try again later if not self._lost_subscription: self._lost_subscription = True @@ -552,7 +552,7 @@ class ReolinkHost: "Unexpected exception while requesting ONVIF pull point: %s", ex ) await self._api.unsubscribe(sub_type=SubType.long_poll) - raise ex + raise self._long_poll_error = False diff --git a/homeassistant/components/risco/config_flow.py b/homeassistant/components/risco/config_flow.py index 0f13721856c..ab372be3a14 100644 --- a/homeassistant/components/risco/config_flow.py +++ b/homeassistant/components/risco/config_flow.py @@ -103,9 +103,9 @@ async def validate_local_input( ) try: await risco.connect() - except CannotConnectError as e: + except CannotConnectError: if comm_delay >= MAX_COMMUNICATION_DELAY: - raise e + raise comm_delay += 1 else: break diff --git a/homeassistant/components/roborock/__init__.py b/homeassistant/components/roborock/__init__.py index c01d1fc7c9b..e64b83be1dd 100644 --- a/homeassistant/components/roborock/__init__.py +++ b/homeassistant/components/roborock/__init__.py @@ -115,7 +115,7 @@ async def setup_device( ) _LOGGER.debug(err) await mqtt_client.async_release() - raise err + raise coordinator = RoborockDataUpdateCoordinator( hass, device, networking, product_info, mqtt_client ) diff --git a/homeassistant/components/script/trace.py b/homeassistant/components/script/trace.py index a50cda752d0..0013f1411dd 100644 --- a/homeassistant/components/script/trace.py +++ b/homeassistant/components/script/trace.py @@ -40,7 +40,7 @@ def trace_script( except Exception as ex: if item_id: trace.set_error(ex) - raise ex + raise finally: if item_id: trace.finished() diff --git a/homeassistant/components/signal_messenger/notify.py b/homeassistant/components/signal_messenger/notify.py index 58cd85fb26e..9c8846b2767 100644 --- a/homeassistant/components/signal_messenger/notify.py +++ b/homeassistant/components/signal_messenger/notify.py @@ -94,7 +94,7 @@ class SignalNotificationService(BaseNotificationService): data = DATA_SCHEMA(data) except vol.Invalid as ex: _LOGGER.error("Invalid message data: %s", ex) - raise ex + raise filenames = self.get_filenames(data) attachments_as_bytes = self.get_attachments_as_bytes( @@ -107,7 +107,7 @@ class SignalNotificationService(BaseNotificationService): ) except SignalCliRestApiError as ex: _LOGGER.error("%s", ex) - raise ex + raise @staticmethod def get_filenames(data: Any) -> list[str] | None: @@ -174,7 +174,7 @@ class SignalNotificationService(BaseNotificationService): attachments_as_bytes.append(chunks) except Exception as ex: _LOGGER.error("%s", ex) - raise ex + raise if not attachments_as_bytes: return None diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 87d9118f3a5..670d6b93c0e 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -583,9 +583,9 @@ def stream_worker( # dts. Use "or 1" to deal with this. start_dts = next_video_packet.dts - (next_video_packet.duration or 1) first_keyframe.dts = first_keyframe.pts = start_dts - except StreamWorkerError as ex: + except StreamWorkerError: container.close() - raise ex + raise except StopIteration as ex: container.close() raise StreamEndedError("Stream ended; no additional packets") from ex @@ -612,8 +612,8 @@ def stream_worker( while not quit_event.is_set(): try: packet = next(container_packets) - except StreamWorkerError as ex: - raise ex + except StreamWorkerError: + raise except StopIteration as ex: raise StreamEndedError("Stream ended; no additional packets") from ex except av.AVError as ex: diff --git a/homeassistant/components/syncthru/__init__.py b/homeassistant/components/syncthru/__init__.py index 5ad4a85cc09..c6764de51a7 100644 --- a/homeassistant/components/syncthru/__init__.py +++ b/homeassistant/components/syncthru/__init__.py @@ -42,7 +42,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: printer.url, exc_info=api_error, ) - raise api_error + raise # if the printer is offline, we raise an UpdateFailed if printer.is_unknown_state(): diff --git a/homeassistant/components/synology_dsm/common.py b/homeassistant/components/synology_dsm/common.py index 4bb52383148..d8a2f1ede62 100644 --- a/homeassistant/components/synology_dsm/common.py +++ b/homeassistant/components/synology_dsm/common.py @@ -129,7 +129,7 @@ class SynoApi: self._entry.unique_id, err, ) - raise err + raise @callback def subscribe(self, api_key: str, unique_id: str) -> Callable[[], None]: @@ -268,7 +268,7 @@ class SynoApi: LOGGER.debug( "Error from '%s': %s", self._entry.unique_id, err, exc_info=True ) - raise err + raise async def async_reboot(self) -> None: """Reboot NAS.""" diff --git a/homeassistant/components/tank_utility/sensor.py b/homeassistant/components/tank_utility/sensor.py index e0b3376ca2e..b4d972f7c06 100644 --- a/homeassistant/components/tank_utility/sensor.py +++ b/homeassistant/components/tank_utility/sensor.py @@ -126,7 +126,7 @@ class TankUtilitySensor(SensorEntity): self._token = auth.get_token(self._email, self._password, force=True) data = tank_monitor.get_device_data(self._token, self.device) else: - raise http_error + raise data.update(data.pop("device", {})) data.update(data.pop("lastReading", {})) return data diff --git a/homeassistant/components/tessie/coordinator.py b/homeassistant/components/tessie/coordinator.py index 19d2d2c4869..bea1bf72a8d 100644 --- a/homeassistant/components/tessie/coordinator.py +++ b/homeassistant/components/tessie/coordinator.py @@ -66,7 +66,7 @@ class TessieStateUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): if e.status == HTTPStatus.UNAUTHORIZED: # Auth Token is no longer valid raise ConfigEntryAuthFailed from e - raise e + raise return self._flatten(vehicle) diff --git a/homeassistant/components/twitch/config_flow.py b/homeassistant/components/twitch/config_flow.py index f9e121f3a17..186d097a22b 100644 --- a/homeassistant/components/twitch/config_flow.py +++ b/homeassistant/components/twitch/config_flow.py @@ -154,7 +154,7 @@ class OAuth2FlowHandler( await self.async_set_unique_id(user.id) try: self._abort_if_unique_id_configured() - except AbortFlow as err: + except AbortFlow: async_create_issue( self.hass, DOMAIN, @@ -168,7 +168,7 @@ class OAuth2FlowHandler( "integration_title": "Twitch", }, ) - raise err + raise async_create_issue( self.hass, HOMEASSISTANT_DOMAIN, diff --git a/homeassistant/components/voip/voip.py b/homeassistant/components/voip/voip.py index 4d97720934c..5770d9d2b4a 100644 --- a/homeassistant/components/voip/voip.py +++ b/homeassistant/components/voip/voip.py @@ -445,9 +445,9 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol): async with asyncio.timeout(tts_seconds + self.tts_extra_timeout): # TTS audio is 16Khz 16-bit mono await self._async_send_audio(audio_bytes) - except TimeoutError as err: + except TimeoutError: _LOGGER.warning("TTS timeout") - raise err + raise finally: # Signal pipeline to restart self._tts_done.set() diff --git a/homeassistant/components/wallbox/coordinator.py b/homeassistant/components/wallbox/coordinator.py index 4725e92ca84..bf7c6d1f654 100644 --- a/homeassistant/components/wallbox/coordinator.py +++ b/homeassistant/components/wallbox/coordinator.py @@ -154,7 +154,7 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]): except requests.exceptions.HTTPError as wallbox_connection_error: if wallbox_connection_error.response.status_code == 403: raise InvalidAuth from wallbox_connection_error - raise wallbox_connection_error + raise async def async_set_charging_current(self, charging_current: float) -> None: """Set maximum charging current for Wallbox.""" @@ -185,7 +185,7 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]): except requests.exceptions.HTTPError as wallbox_connection_error: if wallbox_connection_error.response.status_code == 403: raise InvalidAuth from wallbox_connection_error - raise wallbox_connection_error + raise async def async_set_lock_unlock(self, lock: bool) -> None: """Set wallbox to locked or unlocked.""" diff --git a/homeassistant/components/wiz/__init__.py b/homeassistant/components/wiz/__init__.py index 130cc73efd3..6b1ac2a7721 100644 --- a/homeassistant/components/wiz/__init__.py +++ b/homeassistant/components/wiz/__init__.py @@ -113,9 +113,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await coordinator.async_config_entry_first_refresh() - except ConfigEntryNotReady as err: + except ConfigEntryNotReady: await bulb.async_close() - raise err + raise async def _async_shutdown_on_stop(event: Event) -> None: await bulb.async_close() diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index e906148efdb..b8c85902f7f 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -164,7 +164,7 @@ def trace_condition(variables: TemplateVarsType) -> Generator[TraceElement, None yield trace_element except Exception as ex: trace_element.set_error(ex) - raise ex + raise finally: if should_pop: trace_stack_pop(trace_stack_cv) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index a86df259f11..2ea7b259872 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -240,16 +240,16 @@ async def trace_action( yield trace_element except _AbortScript as ex: trace_element.set_error(ex.__cause__ or ex) - raise ex - except _ConditionFail as ex: + raise + except _ConditionFail: # Clear errors which may have been set when evaluating the condition trace_element.set_error(None) - raise ex - except _StopScript as ex: - raise ex + raise + except _StopScript: + raise except Exception as ex: trace_element.set_error(ex) - raise ex + raise finally: trace_stack_pop(trace_stack_cv) @@ -469,7 +469,7 @@ class _ScriptRun: if not self._script.top_level: # We already consumed the response, do not pass it on err.response = None - raise err + raise except Exception: script_execution_set("error") raise diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index a48f0133e84..f51cda6927f 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -2559,9 +2559,9 @@ def make_logging_undefined( def _fail_with_undefined_error(self, *args, **kwargs): try: return super()._fail_with_undefined_error(*args, **kwargs) - except self._undefined_exception as ex: + except self._undefined_exception: _log_fn(logging.ERROR, self._undefined_message) - raise ex + raise def __str__(self) -> str: """Log undefined __str___.""" diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index 287e69f7085..c52be9982c5 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -378,7 +378,7 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]): self.config_entry.async_start_reauth(self.hass) except NotImplementedError as err: self.last_exception = err - raise err + raise except Exception as err: # pylint: disable=broad-except self.last_exception = err diff --git a/pyproject.toml b/pyproject.toml index a11ab82452b..b7d2cc1c9d5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -672,8 +672,7 @@ select = [ "T100", # Trace found: {name} used "T20", # flake8-print "TID251", # Banned imports - "TRY004", # Prefer TypeError exception for invalid type - "TRY302", # Remove exception handler; error is immediately re-raised + "TRY", # tryceratops "UP", # pyupgrade "W", # pycodestyle ] @@ -701,6 +700,8 @@ ignore = [ "SIM102", # Use a single if statement instead of nested if statements "SIM108", # Use ternary operator {contents} instead of if-else-block "SIM115", # Use context handler for opening files + "TRY003", # Avoid specifying long messages outside the exception class + "TRY400", # Use `logging.exception` instead of `logging.error` "UP006", # keep type annotation style as is "UP007", # keep type annotation style as is # Ignored due to performance: https://github.com/charliermarsh/ruff/issues/2923 @@ -724,7 +725,11 @@ ignore = [ "PLE0605", # temporarily disabled - "PT019" + "PT019", + "TRY002", + "TRY301", + "TRY300", + "TRY401" ] [tool.ruff.lint.flake8-import-conventions.extend-aliases] diff --git a/script/lint_and_test.py b/script/lint_and_test.py index 0b0562a0a84..393c5961c7a 100755 --- a/script/lint_and_test.py +++ b/script/lint_and_test.py @@ -76,9 +76,9 @@ async def async_exec(*args, display=False): if display: kwargs["stderr"] = asyncio.subprocess.PIPE proc = await asyncio.create_subprocess_exec(*args, **kwargs) - except FileNotFoundError as err: + except FileNotFoundError: printc(FAIL, f"Could not execute {args[0]}. Did you install test requirements?") - raise err + raise if not display: # Readin stdout into log From 393dc289c4bc3f9ace7da13adb809e9a33073cb5 Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Thu, 28 Mar 2024 10:24:55 +0100 Subject: [PATCH 018/967] Set fastdotcom to gold quality (#105598) Gold code quality application --- homeassistant/components/fastdotcom/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/fastdotcom/manifest.json b/homeassistant/components/fastdotcom/manifest.json index 02fd3ade205..311f3000b1e 100644 --- a/homeassistant/components/fastdotcom/manifest.json +++ b/homeassistant/components/fastdotcom/manifest.json @@ -6,5 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/fastdotcom", "iot_class": "cloud_polling", "loggers": ["fastdotcom"], + "quality_scale": "gold", "requirements": ["fastdotcom==0.0.3"] } From a29dc86f62381bd9018c8728d9d89499c82fc35e Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 28 Mar 2024 10:31:42 +0100 Subject: [PATCH 019/967] Fix ruff error (#114364) --- tests/components/zha/test_discover.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/zha/test_discover.py b/tests/components/zha/test_discover.py index f9242eb1d96..c32b9252f4d 100644 --- a/tests/components/zha/test_discover.py +++ b/tests/components/zha/test_discover.py @@ -1001,7 +1001,7 @@ async def test_quirks_v2_metadata_errors( # 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 as e: + 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( @@ -1010,7 +1010,7 @@ async def test_quirks_v2_metadata_errors( "TRADFRI remote control4", ) ) - raise e + raise class BadDeviceClass(enum.Enum): From aa9d58df6741571b50e532850d1e49956da2cef9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 28 Mar 2024 10:42:52 +0100 Subject: [PATCH 020/967] Improve utility meter restore state tests (#114356) --- tests/components/utility_meter/test_sensor.py | 91 +++++++++++-------- 1 file changed, 53 insertions(+), 38 deletions(-) diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index c250a66b87a..13b367b1fb7 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -610,7 +610,7 @@ async def test_device_class( "utility_meter": { "energy_bill": { "source": "sensor.energy", - "tariffs": ["onpeak", "midpeak", "offpeak", "superpeak"], + "tariffs": ["tariff1", "tariff2", "tariff3", "tariff4"], } } }, @@ -626,7 +626,7 @@ async def test_device_class( "offset": 0, "periodically_resetting": True, "source": "sensor.energy", - "tariffs": ["onpeak", "midpeak", "offpeak", "superpeak"], + "tariffs": ["tariff1", "tariff2", "tariff3", "tariff4"], }, ), ], @@ -638,82 +638,89 @@ async def test_restore_state( # Home assistant is not runnit yet hass.set_state(CoreState.not_running) - last_reset = "2020-12-21T00:00:00.013073+00:00" + last_reset_1 = "2020-12-21T00:00:00.013073+00:00" + last_reset_2 = "2020-12-22T00:00:00.013073+00:00" mock_restore_cache_with_extra_data( hass, [ + # sensor.energy_bill_tariff1 is restored as expected ( State( - "sensor.energy_bill_onpeak", - "3", + "sensor.energy_bill_tariff1", + "1.1", attributes={ ATTR_STATUS: PAUSED, - ATTR_LAST_RESET: last_reset, - ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, + ATTR_LAST_RESET: last_reset_1, + ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.MEGA_WATT_HOUR, }, ), { "native_value": { "__type": "", - "decimal_str": "3", + "decimal_str": "1.2", }, "native_unit_of_measurement": "kWh", - "last_reset": last_reset, - "last_period": "7", - "last_valid_state": "None", + "last_reset": last_reset_2, + "last_period": "1.3", + "last_valid_state": None, "status": "paused", }, ), + # sensor.energy_bill_tariff2 has missing keys and falls back to + # saved state ( State( - "sensor.energy_bill_midpeak", - "5", + "sensor.energy_bill_tariff2", + "2.1", attributes={ ATTR_STATUS: PAUSED, - ATTR_LAST_RESET: last_reset, + ATTR_LAST_RESET: last_reset_1, ATTR_LAST_VALID_STATE: None, - ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, + ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.MEGA_WATT_HOUR, }, ), { "native_value": { "__type": "", - "decimal_str": "3", + "decimal_str": "2.2", }, "native_unit_of_measurement": "kWh", "last_valid_state": "None", }, ), + # sensor.energy_bill_tariff3 has invalid data and falls back to + # saved state ( State( - "sensor.energy_bill_offpeak", - "6", + "sensor.energy_bill_tariff3", + "3.1", attributes={ ATTR_STATUS: COLLECTING, - ATTR_LAST_RESET: last_reset, + ATTR_LAST_RESET: last_reset_1, ATTR_LAST_VALID_STATE: None, - ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, + ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.MEGA_WATT_HOUR, }, ), { "native_value": { "__type": "", - "decimal_str": "3f", + "decimal_str": "3f", # Invalid }, "native_unit_of_measurement": "kWh", "last_valid_state": "None", }, ), + # No extra saved data, fall back to saved state ( State( - "sensor.energy_bill_superpeak", + "sensor.energy_bill_tariff4", "error", attributes={ ATTR_STATUS: COLLECTING, - ATTR_LAST_RESET: last_reset, + ATTR_LAST_RESET: last_reset_1, ATTR_LAST_VALID_STATE: None, - ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, + ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.MEGA_WATT_HOUR, }, ), {}, @@ -736,25 +743,28 @@ async def test_restore_state( await hass.async_block_till_done() # restore from cache - state = hass.states.get("sensor.energy_bill_onpeak") - assert state.state == "3" + state = hass.states.get("sensor.energy_bill_tariff1") + assert state.state == "1.2" assert state.attributes.get("status") == PAUSED - assert state.attributes.get("last_reset") == last_reset + assert state.attributes.get("last_reset") == last_reset_2 assert state.attributes.get("last_valid_state") == "None" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR - state = hass.states.get("sensor.energy_bill_midpeak") - assert state.state == "5" + state = hass.states.get("sensor.energy_bill_tariff2") + assert state.state == "2.1" + assert state.attributes.get("status") == PAUSED + assert state.attributes.get("last_reset") == last_reset_1 assert state.attributes.get("last_valid_state") == "None" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.MEGA_WATT_HOUR - state = hass.states.get("sensor.energy_bill_offpeak") - assert state.state == "6" + state = hass.states.get("sensor.energy_bill_tariff3") + assert state.state == "3.1" assert state.attributes.get("status") == COLLECTING - assert state.attributes.get("last_reset") == last_reset + assert state.attributes.get("last_reset") == last_reset_1 assert state.attributes.get("last_valid_state") == "None" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.MEGA_WATT_HOUR - state = hass.states.get("sensor.energy_bill_superpeak") + state = hass.states.get("sensor.energy_bill_tariff4") assert state.state == STATE_UNKNOWN # utility_meter is loaded, now set sensors according to utility_meter: @@ -764,13 +774,18 @@ async def test_restore_state( await hass.async_block_till_done() state = hass.states.get("select.energy_bill") - assert state.state == "onpeak" + assert state.state == "tariff1" - state = hass.states.get("sensor.energy_bill_onpeak") + state = hass.states.get("sensor.energy_bill_tariff1") assert state.attributes.get("status") == COLLECTING - state = hass.states.get("sensor.energy_bill_offpeak") - assert state.attributes.get("status") == PAUSED + for entity_id in ( + "sensor.energy_bill_tariff2", + "sensor.energy_bill_tariff3", + "sensor.energy_bill_tariff4", + ): + state = hass.states.get(entity_id) + assert state.attributes.get("status") == PAUSED @pytest.mark.parametrize( From fc4d960d17ff27dbfc58ef00f317c5aa53cce2ff Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 28 Mar 2024 10:52:21 +0100 Subject: [PATCH 021/967] Add translation support to Config Entry errors (#106305) * Config Entry error translation * split key and placeholders * Fix config entries tests * translation optional * Mods --- homeassistant/config_entries.py | 52 ++++++++++++-- .../components/config/test_config_entries.py | 72 +++++++++++++++++++ tests/test_config_entries.py | 2 + 3 files changed, 122 insertions(+), 4 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 42194641f7f..890db26810a 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -243,7 +243,14 @@ class OperationNotAllowed(ConfigError): UpdateListenerType = Callable[[HomeAssistant, "ConfigEntry"], Coroutine[Any, Any, None]] -FROZEN_CONFIG_ENTRY_ATTRS = {"entry_id", "domain", "state", "reason"} +FROZEN_CONFIG_ENTRY_ATTRS = { + "entry_id", + "domain", + "state", + "reason", + "error_reason_translation_key", + "error_reason_translation_placeholders", +} UPDATE_ENTRY_CONFIG_ENTRY_ATTRS = { "unique_id", "title", @@ -274,6 +281,8 @@ class ConfigEntry: unique_id: str | None state: ConfigEntryState reason: str | None + error_reason_translation_key: str | None + error_reason_translation_placeholders: dict[str, Any] | None pref_disable_new_entities: bool pref_disable_polling: bool version: int @@ -369,6 +378,8 @@ class ConfigEntry: # Reason why config entry is in a failed state _setter(self, "reason", None) + _setter(self, "error_reason_translation_key", None) + _setter(self, "error_reason_translation_placeholders", None) # Function to cancel a scheduled retry self._async_cancel_retry_setup: Callable[[], Any] | None = None @@ -472,6 +483,8 @@ class ConfigEntry: "pref_disable_polling": self.pref_disable_polling, "disabled_by": self.disabled_by, "reason": self.reason, + "error_reason_translation_key": self.error_reason_translation_key, + "error_reason_translation_placeholders": self.error_reason_translation_placeholders, } return json_fragment(json_bytes(json_repr)) @@ -543,6 +556,8 @@ class ConfigEntry: setup_phase = SetupPhases.CONFIG_ENTRY_PLATFORM_SETUP error_reason = None + error_reason_translation_key = None + error_reason_translation_placeholders = None try: with async_start_setup( @@ -557,6 +572,8 @@ class ConfigEntry: result = False except ConfigEntryError as exc: error_reason = str(exc) or "Unknown fatal config entry error" + error_reason_translation_key = exc.translation_key + error_reason_translation_placeholders = exc.translation_placeholders _LOGGER.exception( "Error setting up entry %s for %s: %s", self.title, @@ -569,6 +586,8 @@ class ConfigEntry: message = str(exc) auth_base_message = "could not authenticate" error_reason = message or auth_base_message + error_reason_translation_key = exc.translation_key + error_reason_translation_placeholders = exc.translation_placeholders auth_message = ( f"{auth_base_message}: {message}" if message else auth_base_message ) @@ -583,7 +602,15 @@ class ConfigEntry: result = False except ConfigEntryNotReady as exc: message = str(exc) - self._async_set_state(hass, ConfigEntryState.SETUP_RETRY, message or None) + error_reason_translation_key = exc.translation_key + error_reason_translation_placeholders = exc.translation_placeholders + self._async_set_state( + hass, + ConfigEntryState.SETUP_RETRY, + message or None, + error_reason_translation_key, + error_reason_translation_placeholders, + ) wait_time = 2 ** min(self._tries, 4) * 5 + ( randint(RANDOM_MICROSECOND_MIN, RANDOM_MICROSECOND_MAX) / 1000000 ) @@ -644,7 +671,13 @@ class ConfigEntry: if result: self._async_set_state(hass, ConfigEntryState.LOADED, None) else: - self._async_set_state(hass, ConfigEntryState.SETUP_ERROR, error_reason) + self._async_set_state( + hass, + ConfigEntryState.SETUP_ERROR, + error_reason, + error_reason_translation_key, + error_reason_translation_placeholders, + ) @callback def _async_setup_again(self, hass: HomeAssistant, *_: Any) -> None: @@ -771,7 +804,12 @@ class ConfigEntry: @callback def _async_set_state( - self, hass: HomeAssistant, state: ConfigEntryState, reason: str | None + self, + hass: HomeAssistant, + state: ConfigEntryState, + reason: str | None, + error_reason_translation_key: str | None = None, + error_reason_translation_placeholders: dict[str, str] | None = None, ) -> None: """Set the state of the config entry.""" if state not in NO_RESET_TRIES_STATES: @@ -779,6 +817,12 @@ class ConfigEntry: _setter = object.__setattr__ _setter(self, "state", state) _setter(self, "reason", reason) + _setter(self, "error_reason_translation_key", error_reason_translation_key) + _setter( + self, + "error_reason_translation_placeholders", + error_reason_translation_placeholders, + ) self.clear_cache() async_dispatcher_send( hass, SIGNAL_CONFIG_ENTRY_CHANGED, ConfigEntryChange.UPDATED, self diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index b4ef32b864c..e2929a0f1d5 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -131,6 +131,8 @@ async def test_get_entries(hass: HomeAssistant, client, clear_handlers) -> None: "pref_disable_polling": False, "disabled_by": None, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, }, { "domain": "comp2", @@ -145,6 +147,8 @@ async def test_get_entries(hass: HomeAssistant, client, clear_handlers) -> None: "pref_disable_polling": False, "disabled_by": None, "reason": "Unsupported API", + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, }, { "domain": "comp3", @@ -159,6 +163,8 @@ async def test_get_entries(hass: HomeAssistant, client, clear_handlers) -> None: "pref_disable_polling": False, "disabled_by": core_ce.ConfigEntryDisabler.USER, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, }, { "domain": "comp4", @@ -173,6 +179,8 @@ async def test_get_entries(hass: HomeAssistant, client, clear_handlers) -> None: "pref_disable_polling": False, "disabled_by": None, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, }, { "domain": "comp5", @@ -187,6 +195,8 @@ async def test_get_entries(hass: HomeAssistant, client, clear_handlers) -> None: "pref_disable_polling": False, "disabled_by": None, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, }, ] @@ -536,6 +546,8 @@ async def test_create_account( "pref_disable_polling": False, "title": "Test Entry", "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, }, "description": None, "description_placeholders": None, @@ -615,6 +627,8 @@ async def test_two_step_flow( "pref_disable_polling": False, "title": "user-title", "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, }, "description": None, "description_placeholders": None, @@ -1058,6 +1072,8 @@ async def test_get_single( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "user", "state": "loaded", "supports_reconfigure": False, @@ -1393,6 +1409,8 @@ async def test_get_matching_entries_ws( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla", "state": "not_loaded", "supports_reconfigure": False, @@ -1408,6 +1426,8 @@ async def test_get_matching_entries_ws( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": "Unsupported API", + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla2", "state": "setup_error", "supports_reconfigure": False, @@ -1423,6 +1443,8 @@ async def test_get_matching_entries_ws( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla3", "state": "not_loaded", "supports_reconfigure": False, @@ -1438,6 +1460,8 @@ async def test_get_matching_entries_ws( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla4", "state": "not_loaded", "supports_reconfigure": False, @@ -1453,6 +1477,8 @@ async def test_get_matching_entries_ws( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla5", "state": "not_loaded", "supports_reconfigure": False, @@ -1479,6 +1505,8 @@ async def test_get_matching_entries_ws( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla", "state": "not_loaded", "supports_reconfigure": False, @@ -1504,6 +1532,8 @@ async def test_get_matching_entries_ws( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla4", "state": "not_loaded", "supports_reconfigure": False, @@ -1519,6 +1549,8 @@ async def test_get_matching_entries_ws( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla5", "state": "not_loaded", "supports_reconfigure": False, @@ -1544,6 +1576,8 @@ async def test_get_matching_entries_ws( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla", "state": "not_loaded", "supports_reconfigure": False, @@ -1559,6 +1593,8 @@ async def test_get_matching_entries_ws( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla3", "state": "not_loaded", "supports_reconfigure": False, @@ -1590,6 +1626,8 @@ async def test_get_matching_entries_ws( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla", "state": "not_loaded", "supports_reconfigure": False, @@ -1605,6 +1643,8 @@ async def test_get_matching_entries_ws( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": "Unsupported API", + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla2", "state": "setup_error", "supports_reconfigure": False, @@ -1620,6 +1660,8 @@ async def test_get_matching_entries_ws( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla3", "state": "not_loaded", "supports_reconfigure": False, @@ -1635,6 +1677,8 @@ async def test_get_matching_entries_ws( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla4", "state": "not_loaded", "supports_reconfigure": False, @@ -1650,6 +1694,8 @@ async def test_get_matching_entries_ws( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla5", "state": "not_loaded", "supports_reconfigure": False, @@ -1749,6 +1795,8 @@ async def test_subscribe_entries_ws( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla", "state": "not_loaded", "supports_reconfigure": False, @@ -1767,6 +1815,8 @@ async def test_subscribe_entries_ws( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": "Unsupported API", + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla2", "state": "setup_error", "supports_reconfigure": False, @@ -1785,6 +1835,8 @@ async def test_subscribe_entries_ws( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla3", "state": "not_loaded", "supports_reconfigure": False, @@ -1807,6 +1859,8 @@ async def test_subscribe_entries_ws( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla", "state": "not_loaded", "supports_reconfigure": False, @@ -1830,6 +1884,8 @@ async def test_subscribe_entries_ws( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla", "state": "not_loaded", "supports_reconfigure": False, @@ -1853,6 +1909,8 @@ async def test_subscribe_entries_ws( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla", "state": "not_loaded", "supports_reconfigure": False, @@ -1935,6 +1993,8 @@ async def test_subscribe_entries_ws_filtered( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla", "state": "not_loaded", "supports_reconfigure": False, @@ -1953,6 +2013,8 @@ async def test_subscribe_entries_ws_filtered( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla3", "state": "not_loaded", "supports_reconfigure": False, @@ -1977,6 +2039,8 @@ async def test_subscribe_entries_ws_filtered( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla", "state": "not_loaded", "supports_reconfigure": False, @@ -1999,6 +2063,8 @@ async def test_subscribe_entries_ws_filtered( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla3", "state": "not_loaded", "supports_reconfigure": False, @@ -2023,6 +2089,8 @@ async def test_subscribe_entries_ws_filtered( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla", "state": "not_loaded", "supports_reconfigure": False, @@ -2046,6 +2114,8 @@ async def test_subscribe_entries_ws_filtered( "pref_disable_new_entities": False, "pref_disable_polling": False, "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, "source": "bla", "state": "not_loaded", "supports_reconfigure": False, @@ -2225,6 +2295,8 @@ async def test_supports_reconfigure( "pref_disable_polling": False, "title": "Test Entry", "reason": None, + "error_reason_translation_key": None, + "error_reason_translation_placeholders": None, }, "description": None, "description_placeholders": None, diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 7d564f1cf12..6af5f2cde3f 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -821,6 +821,8 @@ async def test_as_dict(snapshot: SnapshotAssertion) -> None: "_setup_lock", "update_listeners", "reason", + "error_reason_translation_key", + "error_reason_translation_placeholders", "_async_cancel_retry_setup", "_on_unload", "reload_lock", From 4a9c592f3c230290be79f3971549d22f49867b13 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Thu, 28 Mar 2024 10:54:10 +0100 Subject: [PATCH 022/967] Mark core as codeowner for some folders (#114357) --- CODEOWNERS | 19 ++++++++++++++++++- script/hassfest/codeowners.py | 19 ++++++++++++++++++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 85603250b7c..77d70fe5ede 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -5,13 +5,30 @@ # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners # Home Assistant Core -setup.cfg @home-assistant/core +.core_files.yaml @home-assistant/core +.git-blame-ignore-revs @home-assistant/core +.gitattributes @home-assistant/core +.gitignore @home-assistant/core +.hadolint.yaml @home-assistant/core +.pre-commit-config.yaml @home-assistant/core +.prettierignore @home-assistant/core +.yamllint @home-assistant/core pyproject.toml @home-assistant/core +requirements_test.txt @home-assistant/core +/.devcontainer/ @home-assistant/core +/.github/ @home-assistant/core +/.vscode/ @home-assistant/core /homeassistant/*.py @home-assistant/core +/homeassistant/auth/ @home-assistant/core +/homeassistant/backports/ @home-assistant/core /homeassistant/helpers/ @home-assistant/core +/homeassistant/scripts/ @home-assistant/core /homeassistant/util/ @home-assistant/core +/pylint/ @home-assistant/core +/script/ @home-assistant/core # Home Assistant Supervisor +.dockerignore @home-assistant/supervisor build.json @home-assistant/supervisor /machine/ @home-assistant/supervisor /rootfs/ @home-assistant/supervisor diff --git a/script/hassfest/codeowners.py b/script/hassfest/codeowners.py index 15e34c23416..04150836dd5 100644 --- a/script/hassfest/codeowners.py +++ b/script/hassfest/codeowners.py @@ -12,13 +12,30 @@ BASE = """ # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners # Home Assistant Core -setup.cfg @home-assistant/core +.core_files.yaml @home-assistant/core +.git-blame-ignore-revs @home-assistant/core +.gitattributes @home-assistant/core +.gitignore @home-assistant/core +.hadolint.yaml @home-assistant/core +.pre-commit-config.yaml @home-assistant/core +.prettierignore @home-assistant/core +.yamllint @home-assistant/core pyproject.toml @home-assistant/core +requirements_test.txt @home-assistant/core +/.devcontainer/ @home-assistant/core +/.github/ @home-assistant/core +/.vscode/ @home-assistant/core /homeassistant/*.py @home-assistant/core +/homeassistant/auth/ @home-assistant/core +/homeassistant/backports/ @home-assistant/core /homeassistant/helpers/ @home-assistant/core +/homeassistant/scripts/ @home-assistant/core /homeassistant/util/ @home-assistant/core +/pylint/ @home-assistant/core +/script/ @home-assistant/core # Home Assistant Supervisor +.dockerignore @home-assistant/supervisor build.json @home-assistant/supervisor /machine/ @home-assistant/supervisor /rootfs/ @home-assistant/supervisor From 4d4d2850bec4d002cd728acfaffd3cd7645fe3c4 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 28 Mar 2024 11:01:12 +0100 Subject: [PATCH 023/967] Update pyudev to 0.24.1 (#114359) --- homeassistant/components/usb/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 -- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/usb/manifest.json b/homeassistant/components/usb/manifest.json index 71df5ba2c05..19269801c11 100644 --- a/homeassistant/components/usb/manifest.json +++ b/homeassistant/components/usb/manifest.json @@ -7,5 +7,5 @@ "integration_type": "system", "iot_class": "local_push", "quality_scale": "internal", - "requirements": ["pyudev==0.23.2", "pyserial==3.5"] + "requirements": ["pyudev==0.24.1", "pyserial==3.5"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 9af8c2f3e0a..9f6ee1cf56c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -49,7 +49,7 @@ pyOpenSSL==24.1.0 pyserial==3.5 python-slugify==8.0.4 PyTurboJPEG==1.7.1 -pyudev==0.23.2 +pyudev==0.24.1 PyYAML==6.0.1 requests==2.31.0 SQLAlchemy==2.0.29 diff --git a/pyproject.toml b/pyproject.toml index b7d2cc1c9d5..35ecb9949ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -513,8 +513,6 @@ filterwarnings = [ "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:onvif.client", # https://github.com/okunishinishi/python-stringcase/commit/6a5c5bbd3fe5337862abc7fd0853a0f36e18b2e1 - >1.2.0 "ignore:invalid escape sequence:SyntaxWarning:.*stringcase", - # https://github.com/pyudev/pyudev/pull/466 - >=0.24.0 - "ignore:invalid escape sequence:SyntaxWarning:.*pyudev.monitor", # https://github.com/xeniter/romy/pull/1 - >=0.0.8 "ignore:with timeout\\(\\) is deprecated, use async with timeout\\(\\) instead:DeprecationWarning:romy.utils", # https://github.com/grahamwetzler/smart-meter-texas/pull/143 - >0.5.3 diff --git a/requirements_all.txt b/requirements_all.txt index a5a0ba98d00..b5cd02d6db5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2336,7 +2336,7 @@ pytrafikverket==0.3.10 pytrydan==0.4.0 # homeassistant.components.usb -pyudev==0.23.2 +pyudev==0.24.1 # homeassistant.components.unifiprotect pyunifiprotect==5.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 56e25ae045f..60977e44306 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1803,7 +1803,7 @@ pytrafikverket==0.3.10 pytrydan==0.4.0 # homeassistant.components.usb -pyudev==0.23.2 +pyudev==0.24.1 # homeassistant.components.unifiprotect pyunifiprotect==5.0.2 From fa72d70726274980a574c408600bd6011fbb7f01 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 28 Mar 2024 11:08:38 +0100 Subject: [PATCH 024/967] Update aioazuredevops to 1.4.3 (#114361) --- homeassistant/components/azure_devops/manifest.json | 2 +- pyproject.toml | 2 -- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/azure_devops/manifest.json b/homeassistant/components/azure_devops/manifest.json index c97d81046da..391cad570f2 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==1.3.5"] + "requirements": ["aioazuredevops==1.4.3"] } diff --git a/pyproject.toml b/pyproject.toml index 35ecb9949ab..0bf159a8b12 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -479,8 +479,6 @@ filterwarnings = [ # -- 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/timmo001/aioazuredevops/commit/7c6a41bed45805396cd96e0696372c79b5416612 - >=1.4.0 - "ignore:\"(is|is not)\" with 'int' literal. Did you mean \"(==|!=)\"?:SyntaxWarning:.*aioazuredevops.client", # https://github.com/ludeeus/aiogithubapi/pull/208 - >=23.9.0 "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:aiogithubapi.namespaces.events", # https://github.com/bachya/aiopurpleair/pull/200 - >=2023.10.0 diff --git a/requirements_all.txt b/requirements_all.txt index b5cd02d6db5..9c227840a36 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -206,7 +206,7 @@ aioasuswrt==1.4.0 aioautomower==2024.3.4 # homeassistant.components.azure_devops -aioazuredevops==1.3.5 +aioazuredevops==1.4.3 # homeassistant.components.baf aiobafi6==0.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 60977e44306..aaebc636b4a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -185,7 +185,7 @@ aioasuswrt==1.4.0 aioautomower==2024.3.4 # homeassistant.components.azure_devops -aioazuredevops==1.3.5 +aioazuredevops==1.4.3 # homeassistant.components.baf aiobafi6==0.9.0 From 72cfeff5b24169bb47ee1e42784a9bbfcc989227 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 28 Mar 2024 11:09:59 +0100 Subject: [PATCH 025/967] Fix streamlabswater feedback (#114371) --- homeassistant/components/streamlabswater/__init__.py | 4 ++-- homeassistant/components/streamlabswater/strings.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/streamlabswater/__init__.py b/homeassistant/components/streamlabswater/__init__.py index c3bbe5a96d4..46acc443d2e 100644 --- a/homeassistant/components/streamlabswater/__init__.py +++ b/homeassistant/components/streamlabswater/__init__.py @@ -81,12 +81,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async_create_issue( hass, DOMAIN, - f"deprecated_yaml_import_issue_${result['reason']}", + 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_key=f"deprecated_yaml_import_issue_{result['reason']}", translation_placeholders=ISSUE_PLACEHOLDER, ) return True diff --git a/homeassistant/components/streamlabswater/strings.json b/homeassistant/components/streamlabswater/strings.json index 204f7e831ef..872a0d1f6ac 100644 --- a/homeassistant/components/streamlabswater/strings.json +++ b/homeassistant/components/streamlabswater/strings.json @@ -52,7 +52,7 @@ "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 an 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." + "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", From fc672be0ca9fed9e01e91c95477af3441e3fe4cd Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 28 Mar 2024 11:10:46 +0100 Subject: [PATCH 026/967] Fix Suez water feedback (#114372) --- homeassistant/components/suez_water/sensor.py | 4 ++-- homeassistant/components/suez_water/strings.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/suez_water/sensor.py b/homeassistant/components/suez_water/sensor.py index 7060339250c..f48e78bb153 100644 --- a/homeassistant/components/suez_water/sensor.py +++ b/homeassistant/components/suez_water/sensor.py @@ -74,12 +74,12 @@ async def async_setup_platform( async_create_issue( hass, DOMAIN, - f"deprecated_yaml_import_issue_${result['reason']}", + 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_key=f"deprecated_yaml_import_issue_{result['reason']}", translation_placeholders=ISSUE_PLACEHOLDER, ) diff --git a/homeassistant/components/suez_water/strings.json b/homeassistant/components/suez_water/strings.json index b4b81a788b5..fd85565d297 100644 --- a/homeassistant/components/suez_water/strings.json +++ b/homeassistant/components/suez_water/strings.json @@ -32,7 +32,7 @@ }, "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 an 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." + "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", From e1bff6dac4564f6bf0cce41aa6ba155804747722 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 28 Mar 2024 11:11:28 +0100 Subject: [PATCH 027/967] Fix Swiss public transport feedback (#114373) --- homeassistant/components/swiss_public_transport/sensor.py | 4 ++-- homeassistant/components/swiss_public_transport/strings.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/swiss_public_transport/sensor.py b/homeassistant/components/swiss_public_transport/sensor.py index 7c712c8c189..a4a9605a603 100644 --- a/homeassistant/components/swiss_public_transport/sensor.py +++ b/homeassistant/components/swiss_public_transport/sensor.py @@ -131,12 +131,12 @@ async def async_setup_platform( async_create_issue( hass, DOMAIN, - f"deprecated_yaml_import_issue_${result['reason']}", + 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_key=f"deprecated_yaml_import_issue_{result['reason']}", translation_placeholders=PLACEHOLDERS, ) diff --git a/homeassistant/components/swiss_public_transport/strings.json b/homeassistant/components/swiss_public_transport/strings.json index c0e88f08b8d..c080e785f2c 100644 --- a/homeassistant/components/swiss_public_transport/strings.json +++ b/homeassistant/components/swiss_public_transport/strings.json @@ -38,7 +38,7 @@ "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 an 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." + "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", From 795cc361eeb3aa2beef7ce32100a184c3c5b75af Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 28 Mar 2024 11:12:02 +0100 Subject: [PATCH 028/967] Fix Lupusec feedback (#114374) --- homeassistant/components/lupusec/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lupusec/__init__.py b/homeassistant/components/lupusec/__init__.py index c471902813a..51bba44aef0 100644 --- a/homeassistant/components/lupusec/__init__.py +++ b/homeassistant/components/lupusec/__init__.py @@ -79,12 +79,12 @@ async def handle_async_init_result(hass: HomeAssistant, domain: str, conf: dict) async_create_issue( hass, DOMAIN, - f"deprecated_yaml_import_issue_${result['reason']}", + f"deprecated_yaml_import_issue_{result['reason']}", breaks_in_ha_version="2024.8.0", is_fixable=False, issue_domain=DOMAIN, severity=IssueSeverity.WARNING, - translation_key=f"deprecated_yaml_import_issue_${result['reason']}", + translation_key=f"deprecated_yaml_import_issue_{result['reason']}", translation_placeholders=ISSUE_PLACEHOLDER, ) From f0eca8233ea5ad4c24c08dce2881f56eb503e6e5 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 28 Mar 2024 11:28:28 +0100 Subject: [PATCH 029/967] Update aprslib to 0.7.2 (#114365) --- homeassistant/components/aprs/manifest.json | 2 +- pyproject.toml | 2 -- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/aprs/manifest.json b/homeassistant/components/aprs/manifest.json index a3dac880746..63826f5a385 100644 --- a/homeassistant/components/aprs/manifest.json +++ b/homeassistant/components/aprs/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/aprs", "iot_class": "cloud_push", "loggers": ["aprslib", "geographiclib", "geopy"], - "requirements": ["aprslib==0.7.0", "geopy==2.3.0"] + "requirements": ["aprslib==0.7.2", "geopy==2.3.0"] } diff --git a/pyproject.toml b/pyproject.toml index 0bf159a8b12..458da9cdd24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -483,8 +483,6 @@ filterwarnings = [ "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:aiogithubapi.namespaces.events", # 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/rossengeorgiev/aprs-python/commit/5e79c810355fc2df4348581779815f2981493e3f - >=0.7.1 - "ignore:invalid escape sequence:SyntaxWarning:.*aprslib.parsing.weather", # https://github.com/tschamm/boschshcpy/pull/39 - >=0.2.89 "ignore:pkg_resources is deprecated as an API:DeprecationWarning:boschshcpy.api", # https://github.com/DataDog/datadogpy/pull/290 - >=0.23.0 diff --git a/requirements_all.txt b/requirements_all.txt index 9c227840a36..48d5eb2c5e0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -461,7 +461,7 @@ apple_weatherkit==1.1.2 apprise==1.7.4 # homeassistant.components.aprs -aprslib==0.7.0 +aprslib==0.7.2 # homeassistant.components.aqualogic aqualogic==2.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aaebc636b4a..2b00ffb9f84 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -425,7 +425,7 @@ apple_weatherkit==1.1.2 apprise==1.7.4 # homeassistant.components.aprs -aprslib==0.7.0 +aprslib==0.7.2 # homeassistant.components.aranet aranet4==2.2.2 From d2b57691234e4dc6d469dde192a4d0c19895ef58 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 28 Mar 2024 11:29:31 +0100 Subject: [PATCH 030/967] Update ovoenergy to 1.3.1 (#114367) --- homeassistant/components/ovo_energy/__init__.py | 2 +- homeassistant/components/ovo_energy/manifest.json | 2 +- homeassistant/components/ovo_energy/sensor.py | 2 +- pyproject.toml | 2 -- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/ovo_energy/__init__.py b/homeassistant/components/ovo_energy/__init__.py index 6ad39ad82cb..e0c2b77664a 100644 --- a/homeassistant/components/ovo_energy/__init__.py +++ b/homeassistant/components/ovo_energy/__init__.py @@ -7,7 +7,7 @@ from datetime import timedelta import logging import aiohttp -from ovoenergy import OVODailyUsage +from ovoenergy.models import OVODailyUsage from ovoenergy.ovoenergy import OVOEnergy from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/ovo_energy/manifest.json b/homeassistant/components/ovo_energy/manifest.json index 87e356417b0..9435958f1fe 100644 --- a/homeassistant/components/ovo_energy/manifest.json +++ b/homeassistant/components/ovo_energy/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["ovoenergy"], - "requirements": ["ovoenergy==1.2.0"] + "requirements": ["ovoenergy==1.3.1"] } diff --git a/homeassistant/components/ovo_energy/sensor.py b/homeassistant/components/ovo_energy/sensor.py index 76c084b368f..d5384837e9c 100644 --- a/homeassistant/components/ovo_energy/sensor.py +++ b/homeassistant/components/ovo_energy/sensor.py @@ -7,7 +7,7 @@ import dataclasses from datetime import datetime, timedelta from typing import Final -from ovoenergy import OVODailyUsage +from ovoenergy.models import OVODailyUsage from ovoenergy.ovoenergy import OVOEnergy from homeassistant.components.sensor import ( diff --git a/pyproject.toml b/pyproject.toml index 458da9cdd24..45f382daf9e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -495,8 +495,6 @@ filterwarnings = [ "ignore:\"is not\" with 'str' literal. Did you mean \"!=\"?:SyntaxWarning:.*lupupy.devices.alarm", # https://github.com/nextcord/nextcord/pull/1095 - >2.6.1 "ignore:pkg_resources is deprecated as an API:DeprecationWarning:nextcord.health_check", - # https://github.com/timmo001/ovoenergy/pull/68 - >=1.3.0 - "ignore:\"is not\" with 'int' literal. Did you mean \"!=\"?:SyntaxWarning:.*ovoenergy.ovoenergy", # 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", diff --git a/requirements_all.txt b/requirements_all.txt index 48d5eb2c5e0..76bf3f6adf8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1497,7 +1497,7 @@ orvibo==1.1.2 ourgroceries==1.5.4 # homeassistant.components.ovo_energy -ovoenergy==1.2.0 +ovoenergy==1.3.1 # homeassistant.components.p1_monitor p1monitor==3.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2b00ffb9f84..198d6ef14e4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1185,7 +1185,7 @@ oralb-ble==0.17.6 ourgroceries==1.5.4 # homeassistant.components.ovo_energy -ovoenergy==1.2.0 +ovoenergy==1.3.1 # homeassistant.components.p1_monitor p1monitor==3.0.0 From 41bd3d0853e9de7860c3f6f4dbafd8fcd73af7d0 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 28 Mar 2024 11:37:57 +0100 Subject: [PATCH 031/967] Update pytile to 2023.12.0 (#114370) --- homeassistant/components/tile/manifest.json | 2 +- pyproject.toml | 2 -- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/tile/manifest.json b/homeassistant/components/tile/manifest.json index 6f311fc5593..8dceddcb77f 100644 --- a/homeassistant/components/tile/manifest.json +++ b/homeassistant/components/tile/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "cloud_polling", "loggers": ["pytile"], - "requirements": ["pytile==2023.04.0"] + "requirements": ["pytile==2023.12.0"] } diff --git a/pyproject.toml b/pyproject.toml index 45f382daf9e..0516f7222e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -498,8 +498,6 @@ 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/bachya/pytile/pull/280 - >=2023.10.0 - "ignore:datetime.*utcfromtimestamp\\(\\) is deprecated and scheduled for removal:DeprecationWarning:pytile.tile", # 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", diff --git a/requirements_all.txt b/requirements_all.txt index 76bf3f6adf8..e298e36ba9d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2311,7 +2311,7 @@ python-vlc==3.0.18122 pythonegardia==1.0.52 # homeassistant.components.tile -pytile==2023.04.0 +pytile==2023.12.0 # homeassistant.components.tomorrowio pytomorrowio==0.3.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 198d6ef14e4..c5a12b34ce6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1781,7 +1781,7 @@ python-technove==1.2.2 python-telegram-bot[socks]==21.0.1 # homeassistant.components.tile -pytile==2023.04.0 +pytile==2023.12.0 # homeassistant.components.tomorrowio pytomorrowio==0.3.6 From 22b14d83e8f2aa662e5845c120b53f6943c7873f Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Thu, 28 Mar 2024 12:07:55 +0100 Subject: [PATCH 032/967] Use `setup_test_component_platform` helper for sensor entity component tests instead of `hass.components` (#114316) * Use `setup_test_component_platform` helper for sensor entity component tests instead of `hass.components` * Missing file * Fix import * Remove invalid device class --- tests/common.py | 14 +- tests/components/conftest.py | 9 + tests/components/mqtt/test_init.py | 14 +- .../sensor.py => components/sensor/common.py} | 49 +-- .../sensor/test_device_condition.py | 32 +- .../components/sensor/test_device_trigger.py | 32 +- tests/components/sensor/test_init.py | 360 +++++++----------- tests/components/sensor/test_recorder.py | 15 +- 8 files changed, 219 insertions(+), 306 deletions(-) rename tests/{testing_config/custom_components/test/sensor.py => components/sensor/common.py} (84%) diff --git a/tests/common.py b/tests/common.py index 54b0193091d..210eb07d668 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1461,7 +1461,10 @@ def mock_integration( def mock_platform( - hass: HomeAssistant, platform_path: str, module: Mock | MockPlatform | None = None + hass: HomeAssistant, + platform_path: str, + module: Mock | MockPlatform | None = None, + built_in=True, ) -> None: """Mock a platform. @@ -1472,7 +1475,7 @@ def mock_platform( module_cache = hass.data[loader.DATA_COMPONENTS] if domain not in integration_cache: - mock_integration(hass, MockModule(domain)) + mock_integration(hass, MockModule(domain), built_in=built_in) integration_cache[domain]._top_level_files.add(f"{platform_name}.py") _LOGGER.info("Adding mock integration platform: %s", platform_path) @@ -1665,6 +1668,7 @@ def setup_test_component_platform( domain: str, entities: Sequence[Entity], from_config_entry: bool = False, + built_in: bool = True, ) -> MockPlatform: """Mock a test component platform for tests.""" @@ -1695,9 +1699,5 @@ def setup_test_component_platform( platform.async_setup_entry = _async_setup_entry platform.async_setup_platform = None - mock_platform( - hass, - f"test.{domain}", - platform, - ) + mock_platform(hass, f"test.{domain}", platform, built_in=built_in) return platform diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 4669e17c8e7..d84fb3600ab 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -10,6 +10,7 @@ from homeassistant.const import STATE_OFF, STATE_ON if TYPE_CHECKING: from tests.components.light.common import MockLight + from tests.components.sensor.common import MockSensor @pytest.fixture(scope="session", autouse=True) @@ -118,3 +119,11 @@ def mock_light_entities() -> list["MockLight"]: MockLight("Ceiling", STATE_OFF), MockLight(None, STATE_OFF), ] + + +@pytest.fixture +def mock_sensor_entities() -> dict[str, "MockSensor"]: + """Return mocked sensor entities.""" + from tests.components.sensor.common import get_mock_sensor_entities + + return get_mock_sensor_entities() diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 3459e6fc058..a9f2ba4354b 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -23,6 +23,7 @@ from homeassistant.components.mqtt.models import ( MqttValueTemplateException, ReceiveMessage, ) +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState from homeassistant.const import ( ATTR_ASSUMED_STATE, @@ -52,10 +53,9 @@ from tests.common import ( async_fire_mqtt_message, async_fire_time_changed, mock_restore_cache, + setup_test_component_platform, ) -from tests.testing_config.custom_components.test.sensor import ( # type: ignore[attr-defined] - DEVICE_CLASSES, -) +from tests.components.sensor.common import MockSensor from tests.typing import ( MqttMockHAClient, MqttMockHAClientGenerator, @@ -3142,12 +3142,12 @@ async def test_debug_info_non_mqtt( device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, mqtt_mock_entry: MqttMockHAClientGenerator, + mock_sensor_entities: dict[str, MockSensor], ) -> None: """Test we get empty debug_info for a device with non MQTT entities.""" await mqtt_mock_entry() domain = "sensor" - platform = getattr(hass.components, f"test.{domain}") - platform.init() + setup_test_component_platform(hass, domain, mock_sensor_entities) config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -3155,11 +3155,11 @@ async def test_debug_info_non_mqtt( config_entry_id=config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - for device_class in DEVICE_CLASSES: + for device_class in SensorDeviceClass: entity_registry.async_get_or_create( domain, "test", - platform.ENTITIES[device_class].unique_id, + mock_sensor_entities[device_class].unique_id, device_id=device_entry.id, ) diff --git a/tests/testing_config/custom_components/test/sensor.py b/tests/components/sensor/common.py similarity index 84% rename from tests/testing_config/custom_components/test/sensor.py rename to tests/components/sensor/common.py index 9ebf16b9dcd..53a93b73da3 100644 --- a/tests/testing_config/custom_components/test/sensor.py +++ b/tests/components/sensor/common.py @@ -1,10 +1,6 @@ -"""Provide a mock sensor platform. - -Call init before using it in your tests to ensure clean test data. -""" +"""Common test utilities for sensor entity component tests.""" from homeassistant.components.sensor import ( - DEVICE_CLASSES, RestoreSensor, SensorDeviceClass, SensorEntity, @@ -24,8 +20,6 @@ from homeassistant.const import ( from tests.common import MockEntity -DEVICE_CLASSES.append("none") - UNITS_OF_MEASUREMENT = { SensorDeviceClass.APPARENT_POWER: UnitOfApparentPower.VOLT_AMPERE, # apparent power (VA) SensorDeviceClass.BATTERY: PERCENTAGE, # % of battery that is left @@ -56,34 +50,6 @@ UNITS_OF_MEASUREMENT = { SensorDeviceClass.GAS: UnitOfVolume.CUBIC_METERS, # gas (m³) } -ENTITIES = {} - - -def init(empty=False): - """Initialize the platform with entities.""" - global ENTITIES - - ENTITIES = ( - {} - if empty - else { - device_class: MockSensor( - name=f"{device_class} sensor", - unique_id=f"unique_{device_class}", - device_class=device_class, - native_unit_of_measurement=UNITS_OF_MEASUREMENT.get(device_class), - ) - for device_class in DEVICE_CLASSES - } - ) - - -async def async_setup_platform( - hass, config, async_add_entities_callback, discovery_info=None -): - """Return mock entities.""" - async_add_entities_callback(list(ENTITIES.values())) - class MockSensor(MockEntity, SensorEntity): """Mock Sensor class.""" @@ -141,3 +107,16 @@ class MockRestoreSensor(MockSensor, RestoreSensor): self._values["native_unit_of_measurement"] = ( last_sensor_data.native_unit_of_measurement ) + + +def get_mock_sensor_entities() -> dict[str, MockSensor]: + """Get mock sensor entities.""" + return { + device_class: MockSensor( + name=f"{device_class} sensor", + unique_id=f"unique_{device_class}", + device_class=device_class, + native_unit_of_measurement=UNITS_OF_MEASUREMENT.get(device_class), + ) + for device_class in SensorDeviceClass + } diff --git a/tests/components/sensor/test_device_condition.py b/tests/components/sensor/test_device_condition.py index 7263154c1dc..b633c744205 100644 --- a/tests/components/sensor/test_device_condition.py +++ b/tests/components/sensor/test_device_condition.py @@ -26,8 +26,9 @@ from tests.common import ( async_get_device_automation_capabilities, async_get_device_automations, async_mock_service, + setup_test_component_platform, ) -from tests.testing_config.custom_components.test.sensor import UNITS_OF_MEASUREMENT +from tests.components.sensor.common import UNITS_OF_MEASUREMENT, MockSensor @pytest.fixture(autouse=True, name="stub_blueprint_populate") @@ -85,11 +86,10 @@ async def test_get_conditions( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - enable_custom_integrations: None, + mock_sensor_entities: dict[str, MockSensor], ) -> None: """Test we get the expected conditions from a sensor.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_sensor_entities.values()) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() sensor_entries = {} @@ -104,7 +104,7 @@ async def test_get_conditions( sensor_entries[device_class] = entity_registry.async_get_or_create( DOMAIN, "test", - platform.ENTITIES[device_class].unique_id, + mock_sensor_entities[device_class].unique_id, device_id=device_entry.id, ) @@ -284,6 +284,7 @@ async def test_get_condition_capabilities( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, + mock_sensor_entities: dict[str, MockSensor], set_state, device_class_reg, device_class_state, @@ -291,8 +292,7 @@ async def test_get_condition_capabilities( unit_state, ) -> None: """Test we get the expected capabilities from a sensor condition.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_sensor_entities.values()) config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -303,7 +303,7 @@ async def test_get_condition_capabilities( entity_id = entity_registry.async_get_or_create( DOMAIN, "test", - platform.ENTITIES["battery"].unique_id, + mock_sensor_entities["battery"].unique_id, device_id=device_entry.id, original_device_class=device_class_reg, unit_of_measurement=unit_reg, @@ -353,6 +353,7 @@ async def test_get_condition_capabilities_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, + mock_sensor_entities: dict[str, MockSensor], set_state, device_class_reg, device_class_state, @@ -360,8 +361,7 @@ async def test_get_condition_capabilities_legacy( unit_state, ) -> None: """Test we get the expected capabilities from a sensor condition.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_sensor_entities.values()) config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -372,7 +372,7 @@ async def test_get_condition_capabilities_legacy( entity_id = entity_registry.async_get_or_create( DOMAIN, "test", - platform.ENTITIES["battery"].unique_id, + mock_sensor_entities["battery"].unique_id, device_id=device_entry.id, original_device_class=device_class_reg, unit_of_measurement=unit_reg, @@ -417,11 +417,13 @@ async def test_get_condition_capabilities_legacy( async def test_get_condition_capabilities_none( hass: HomeAssistant, entity_registry: er.EntityRegistry, - enable_custom_integrations: None, ) -> None: """Test we get the expected capabilities from a sensor condition.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + entity = MockSensor( + name="none sensor", + unique_id="unique_none", + ) + setup_test_component_platform(hass, DOMAIN, [entity]) config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -429,7 +431,7 @@ async def test_get_condition_capabilities_none( entry_none = entity_registry.async_get_or_create( DOMAIN, "test", - platform.ENTITIES["none"].unique_id, + entity.unique_id, ) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index 4193adc9299..98bea960fcc 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -30,8 +30,9 @@ from tests.common import ( async_get_device_automation_capabilities, async_get_device_automations, async_mock_service, + setup_test_component_platform, ) -from tests.testing_config.custom_components.test.sensor import UNITS_OF_MEASUREMENT +from tests.components.sensor.common import UNITS_OF_MEASUREMENT, MockSensor @pytest.fixture(autouse=True, name="stub_blueprint_populate") @@ -87,11 +88,10 @@ async def test_get_triggers( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - enable_custom_integrations: None, + mock_sensor_entities: dict[str, MockSensor], ) -> None: """Test we get the expected triggers from a sensor.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_sensor_entities.values()) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() sensor_entries: dict[SensorDeviceClass, er.RegistryEntry] = {} @@ -106,7 +106,7 @@ async def test_get_triggers( sensor_entries[device_class] = entity_registry.async_get_or_create( DOMAIN, "test", - platform.ENTITIES[device_class].unique_id, + mock_sensor_entities[device_class].unique_id, device_id=device_entry.id, ) @@ -241,6 +241,7 @@ async def test_get_trigger_capabilities( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, + mock_sensor_entities: dict[str, MockSensor], set_state, device_class_reg, device_class_state, @@ -248,8 +249,7 @@ async def test_get_trigger_capabilities( unit_state, ) -> None: """Test we get the expected capabilities from a sensor trigger.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_sensor_entities) config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -260,7 +260,7 @@ async def test_get_trigger_capabilities( entity_id = entity_registry.async_get_or_create( DOMAIN, "test", - platform.ENTITIES["battery"].unique_id, + mock_sensor_entities["battery"].unique_id, device_id=device_entry.id, original_device_class=device_class_reg, unit_of_measurement=unit_reg, @@ -311,6 +311,7 @@ async def test_get_trigger_capabilities_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, + mock_sensor_entities: dict[str, MockSensor], set_state, device_class_reg, device_class_state, @@ -318,8 +319,7 @@ async def test_get_trigger_capabilities_legacy( unit_state, ) -> None: """Test we get the expected capabilities from a sensor trigger.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_sensor_entities) config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -330,7 +330,7 @@ async def test_get_trigger_capabilities_legacy( entity_id = entity_registry.async_get_or_create( DOMAIN, "test", - platform.ENTITIES["battery"].unique_id, + mock_sensor_entities["battery"].unique_id, device_id=device_entry.id, original_device_class=device_class_reg, unit_of_measurement=unit_reg, @@ -374,11 +374,13 @@ async def test_get_trigger_capabilities_legacy( async def test_get_trigger_capabilities_none( hass: HomeAssistant, entity_registry: er.EntityRegistry, - enable_custom_integrations: None, ) -> None: """Test we get the expected capabilities from a sensor trigger.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + entity = MockSensor( + name="none sensor", + unique_id="unique_none", + ) + setup_test_component_platform(hass, DOMAIN, [entity]) config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -386,7 +388,7 @@ async def test_get_trigger_capabilities_none( entry_none = entity_registry.async_get_or_create( DOMAIN, "test", - platform.ENTITIES["none"].unique_id, + entity.unique_id, ) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 59df07bb0b9..0ecb4b9c60f 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -63,7 +63,9 @@ from tests.common import ( mock_integration, mock_platform, mock_restore_cache_with_extra_data, + setup_test_component_platform, ) +from tests.components.sensor.common import MockRestoreSensor, MockSensor TEST_DOMAIN = "test" @@ -103,7 +105,6 @@ TEST_DOMAIN = "test" ) async def test_temperature_conversion( hass: HomeAssistant, - enable_custom_integrations: None, unit_system, native_unit, state_unit, @@ -112,16 +113,14 @@ async def test_temperature_conversion( ) -> None: """Test temperature conversion.""" hass.config.units = unit_system - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value=str(native_value), native_unit_of_measurement=native_unit, device_class=SensorDeviceClass.TEMPERATURE, ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) - entity0 = platform.ENTITIES["0"] assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -132,19 +131,17 @@ async def test_temperature_conversion( @pytest.mark.parametrize("device_class", [None, SensorDeviceClass.PRESSURE]) async def test_temperature_conversion_wrong_device_class( - hass: HomeAssistant, device_class, enable_custom_integrations: None + hass: HomeAssistant, device_class ) -> None: """Test temperatures are not converted if the sensor has wrong device class.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value="0.0", native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, device_class=device_class, ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) - entity0 = platform.ENTITIES["0"] assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -158,21 +155,19 @@ async def test_temperature_conversion_wrong_device_class( async def test_deprecated_last_reset( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, state_class, ) -> None: """Test warning on deprecated last reset.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", state_class=state_class, last_reset=dt_util.utc_from_timestamp(0) ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() assert ( - "Entity sensor.test () " + "Entity sensor.test () " f"with state_class {state_class} has set last_reset. Setting last_reset for " "entities with state_class other than 'total' is not supported. Please update " "your configuration if state_class is manually configured." @@ -185,7 +180,6 @@ async def test_deprecated_last_reset( async def test_datetime_conversion( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, ) -> None: """Test conversion of datetime.""" test_timestamp = datetime(2017, 12, 19, 18, 29, 42, tzinfo=UTC) @@ -193,51 +187,49 @@ async def test_datetime_conversion( dt_util.get_time_zone("Europe/Amsterdam") ) test_date = date(2017, 12, 19) - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( - name="Test", - native_value=test_timestamp, - device_class=SensorDeviceClass.TIMESTAMP, - ) - platform.ENTITIES["1"] = platform.MockSensor( - name="Test", native_value=test_date, device_class=SensorDeviceClass.DATE - ) - platform.ENTITIES["2"] = platform.MockSensor( - name="Test", native_value=None, device_class=SensorDeviceClass.TIMESTAMP - ) - platform.ENTITIES["3"] = platform.MockSensor( - name="Test", native_value=None, device_class=SensorDeviceClass.DATE - ) - platform.ENTITIES["4"] = platform.MockSensor( - name="Test", - native_value=test_local_timestamp, - device_class=SensorDeviceClass.TIMESTAMP, - ) + entities = [ + MockSensor( + name="Test", + native_value=test_timestamp, + device_class=SensorDeviceClass.TIMESTAMP, + ), + MockSensor( + name="Test", native_value=test_date, device_class=SensorDeviceClass.DATE + ), + MockSensor( + name="Test", native_value=None, device_class=SensorDeviceClass.TIMESTAMP + ), + MockSensor(name="Test", native_value=None, device_class=SensorDeviceClass.DATE), + MockSensor( + name="Test", + native_value=test_local_timestamp, + device_class=SensorDeviceClass.TIMESTAMP, + ), + ] + setup_test_component_platform(hass, sensor.DOMAIN, entities) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() - state = hass.states.get(platform.ENTITIES["0"].entity_id) + state = hass.states.get(entities[0].entity_id) assert state.state == test_timestamp.isoformat() - state = hass.states.get(platform.ENTITIES["1"].entity_id) + state = hass.states.get(entities[1].entity_id) assert state.state == test_date.isoformat() - state = hass.states.get(platform.ENTITIES["2"].entity_id) + state = hass.states.get(entities[2].entity_id) assert state.state == STATE_UNKNOWN - state = hass.states.get(platform.ENTITIES["3"].entity_id) + state = hass.states.get(entities[3].entity_id) assert state.state == STATE_UNKNOWN - state = hass.states.get(platform.ENTITIES["4"].entity_id) + state = hass.states.get(entities[4].entity_id) assert state.state == test_timestamp.isoformat() async def test_a_sensor_with_a_non_numeric_device_class( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, ) -> None: """Test that a sensor with a non numeric device class will be non numeric. @@ -249,29 +241,29 @@ async def test_a_sensor_with_a_non_numeric_device_class( dt_util.get_time_zone("Europe/Amsterdam") ) - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( - name="Test", - native_value=test_local_timestamp, - native_unit_of_measurement="", - device_class=SensorDeviceClass.TIMESTAMP, - ) - - platform.ENTITIES["1"] = platform.MockSensor( - name="Test", - native_value=test_local_timestamp, - state_class="", - device_class=SensorDeviceClass.TIMESTAMP, - ) + entities = [ + MockSensor( + name="Test", + native_value=test_local_timestamp, + native_unit_of_measurement="", + device_class=SensorDeviceClass.TIMESTAMP, + ), + MockSensor( + name="Test", + native_value=test_local_timestamp, + state_class="", + device_class=SensorDeviceClass.TIMESTAMP, + ), + ] + setup_test_component_platform(hass, sensor.DOMAIN, entities) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() - state = hass.states.get(platform.ENTITIES["0"].entity_id) + state = hass.states.get(entities[0].entity_id) assert state.state == test_timestamp.isoformat() - state = hass.states.get(platform.ENTITIES["1"].entity_id) + state = hass.states.get(entities[1].entity_id) assert state.state == test_timestamp.isoformat() @@ -285,17 +277,15 @@ async def test_a_sensor_with_a_non_numeric_device_class( async def test_deprecated_datetime_str( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, device_class, state_value, provides, ) -> None: """Test warning on deprecated str for a date(time) value.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value=state_value, device_class=device_class ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -309,17 +299,15 @@ async def test_deprecated_datetime_str( async def test_reject_timezoneless_datetime_str( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, ) -> None: """Test rejection of timezone-less datetime objects as timestamp.""" test_timestamp = datetime(2017, 12, 19, 18, 29, 42, tzinfo=None) - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value=test_timestamp, device_class=SensorDeviceClass.TIMESTAMP, ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -403,7 +391,6 @@ RESTORE_DATA = { ) async def test_restore_sensor_save_state( hass: HomeAssistant, - enable_custom_integrations: None, hass_storage: dict[str, Any], native_value, native_value_type, @@ -412,16 +399,14 @@ async def test_restore_sensor_save_state( uom, ) -> None: """Test RestoreSensor.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockRestoreSensor( + entity0 = MockRestoreSensor( name="Test", native_value=native_value, native_unit_of_measurement=uom, device_class=device_class, ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) - entity0 = platform.ENTITIES["0"] assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -472,7 +457,6 @@ async def test_restore_sensor_save_state( ) async def test_restore_sensor_restore_state( hass: HomeAssistant, - enable_custom_integrations: None, hass_storage: dict[str, Any], native_value, native_value_type, @@ -483,14 +467,12 @@ async def test_restore_sensor_restore_state( """Test RestoreSensor.""" mock_restore_cache_with_extra_data(hass, ((State("sensor.test", ""), extra_data),)) - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockRestoreSensor( + entity0 = MockRestoreSensor( name="Test", device_class=device_class, ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) - entity0 = platform.ENTITIES["0"] assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -621,7 +603,6 @@ async def test_restore_sensor_restore_state( ) async def test_custom_unit( hass: HomeAssistant, - enable_custom_integrations: None, device_class, native_unit, custom_unit, @@ -638,17 +619,15 @@ async def test_custom_unit( ) await hass.async_block_till_done() - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value=str(native_value), native_unit_of_measurement=native_unit, device_class=device_class, unique_id="very_unique", ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) - entity0 = platform.ENTITIES["0"] assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -884,7 +863,6 @@ async def test_custom_unit( ) async def test_custom_unit_change( hass: HomeAssistant, - enable_custom_integrations: None, native_unit, custom_unit, state_unit, @@ -895,17 +873,15 @@ async def test_custom_unit_change( ) -> None: """Test custom unit changes are picked up.""" entity_registry = er.async_get(hass) - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value=str(native_value), native_unit_of_measurement=native_unit, device_class=device_class, unique_id="very_unique", ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) - entity0 = platform.ENTITIES["0"] assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -972,7 +948,6 @@ async def test_custom_unit_change( ) async def test_unit_conversion_priority( hass: HomeAssistant, - enable_custom_integrations: None, unit_system, native_unit, automatic_unit, @@ -990,27 +965,21 @@ async def test_unit_conversion_priority( hass.config.units = unit_system entity_registry = er.async_get(hass) - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=native_unit, native_value=str(native_value), unique_id="very_unique", ) - entity0 = platform.ENTITIES["0"] - - platform.ENTITIES["1"] = platform.MockSensor( + entity1 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=native_unit, native_value=str(native_value), ) - entity1 = platform.ENTITIES["1"] - - platform.ENTITIES["2"] = platform.MockSensor( + entity2 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=native_unit, @@ -1018,16 +987,23 @@ async def test_unit_conversion_priority( suggested_unit_of_measurement=suggested_unit, unique_id="very_unique_2", ) - entity2 = platform.ENTITIES["2"] - - platform.ENTITIES["3"] = platform.MockSensor( + entity3 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=native_unit, native_value=str(native_value), suggested_unit_of_measurement=suggested_unit, ) - entity3 = platform.ENTITIES["3"] + setup_test_component_platform( + hass, + sensor.DOMAIN, + [ + entity0, + entity1, + entity2, + entity3, + ], + ) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -1119,7 +1095,6 @@ async def test_unit_conversion_priority( ) async def test_unit_conversion_priority_precision( hass: HomeAssistant, - enable_custom_integrations: None, unit_system, native_unit, automatic_unit, @@ -1138,10 +1113,8 @@ async def test_unit_conversion_priority_precision( hass.config.units = unit_system entity_registry = er.async_get(hass) - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=native_unit, @@ -1149,18 +1122,14 @@ async def test_unit_conversion_priority_precision( suggested_display_precision=suggested_precision, unique_id="very_unique", ) - entity0 = platform.ENTITIES["0"] - - platform.ENTITIES["1"] = platform.MockSensor( + entity1 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=native_unit, native_value=str(native_value), suggested_display_precision=suggested_precision, ) - entity1 = platform.ENTITIES["1"] - - platform.ENTITIES["2"] = platform.MockSensor( + entity2 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=native_unit, @@ -1169,9 +1138,7 @@ async def test_unit_conversion_priority_precision( suggested_unit_of_measurement=suggested_unit, unique_id="very_unique_2", ) - entity2 = platform.ENTITIES["2"] - - platform.ENTITIES["3"] = platform.MockSensor( + entity3 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=native_unit, @@ -1179,7 +1146,16 @@ async def test_unit_conversion_priority_precision( suggested_display_precision=suggested_precision, suggested_unit_of_measurement=suggested_unit, ) - entity3 = platform.ENTITIES["3"] + setup_test_component_platform( + hass, + sensor.DOMAIN, + [ + entity0, + entity1, + entity2, + entity3, + ], + ) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -1280,7 +1256,6 @@ async def test_unit_conversion_priority_precision( ) async def test_unit_conversion_priority_suggested_unit_change( hass: HomeAssistant, - enable_custom_integrations: None, unit_system, native_unit, original_unit, @@ -1294,8 +1269,6 @@ async def test_unit_conversion_priority_suggested_unit_change( hass.config.units = unit_system entity_registry = er.async_get(hass) - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) # Pre-register entities entry = entity_registry.async_get_or_create( @@ -1315,16 +1288,14 @@ async def test_unit_conversion_priority_suggested_unit_change( {"suggested_unit_of_measurement": original_unit}, ) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=native_unit, native_value=str(native_value), unique_id="very_unique", ) - entity0 = platform.ENTITIES["0"] - - platform.ENTITIES["1"] = platform.MockSensor( + entity1 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=native_unit, @@ -1332,7 +1303,7 @@ async def test_unit_conversion_priority_suggested_unit_change( suggested_unit_of_measurement=suggested_unit, unique_id="very_unique_2", ) - entity1 = platform.ENTITIES["1"] + setup_test_component_platform(hass, sensor.DOMAIN, [entity0, entity1]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -1392,7 +1363,6 @@ async def test_unit_conversion_priority_suggested_unit_change( ) async def test_unit_conversion_priority_suggested_unit_change_2( hass: HomeAssistant, - enable_custom_integrations: None, native_unit_1, native_unit_2, suggested_unit, @@ -1405,8 +1375,6 @@ async def test_unit_conversion_priority_suggested_unit_change_2( hass.config.units = METRIC_SYSTEM entity_registry = er.async_get(hass) - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) # Pre-register entities entity_registry.async_get_or_create( @@ -1416,16 +1384,14 @@ async def test_unit_conversion_priority_suggested_unit_change_2( "sensor", "test", "very_unique_2", unit_of_measurement=native_unit_1 ) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=native_unit_2, native_value=str(native_value), unique_id="very_unique", ) - entity0 = platform.ENTITIES["0"] - - platform.ENTITIES["1"] = platform.MockSensor( + entity1 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=native_unit_2, @@ -1433,7 +1399,7 @@ async def test_unit_conversion_priority_suggested_unit_change_2( suggested_unit_of_measurement=suggested_unit, unique_id="very_unique_2", ) - entity1 = platform.ENTITIES["1"] + setup_test_component_platform(hass, sensor.DOMAIN, [entity0, entity1]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -1496,7 +1462,6 @@ async def test_unit_conversion_priority_suggested_unit_change_2( ) async def test_suggested_precision_option( hass: HomeAssistant, - enable_custom_integrations: None, unit_system, native_unit, integration_suggested_precision, @@ -1510,10 +1475,7 @@ async def test_suggested_precision_option( hass.config.units = unit_system entity_registry = er.async_get(hass) - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=native_unit, @@ -1521,7 +1483,7 @@ async def test_suggested_precision_option( suggested_display_precision=integration_suggested_precision, unique_id="very_unique", ) - entity0 = platform.ENTITIES["0"] + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -1574,7 +1536,6 @@ async def test_suggested_precision_option( ) async def test_suggested_precision_option_update( hass: HomeAssistant, - enable_custom_integrations: None, unit_system, native_unit, suggested_unit, @@ -1590,8 +1551,6 @@ async def test_suggested_precision_option_update( hass.config.units = unit_system entity_registry = er.async_get(hass) - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) # Pre-register entities entry = entity_registry.async_get_or_create("sensor", "test", "very_unique") @@ -1610,7 +1569,7 @@ async def test_suggested_precision_option_update( }, ) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=native_unit, @@ -1618,7 +1577,7 @@ async def test_suggested_precision_option_update( suggested_display_precision=new_precision, unique_id="very_unique", ) - entity0 = platform.ENTITIES["0"] + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -1666,7 +1625,6 @@ async def test_suggested_precision_option_update( ) async def test_unit_conversion_priority_legacy_conversion_removed( hass: HomeAssistant, - enable_custom_integrations: None, unit_system, native_unit, original_unit, @@ -1679,22 +1637,20 @@ async def test_unit_conversion_priority_legacy_conversion_removed( hass.config.units = unit_system entity_registry = er.async_get(hass) - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) # Pre-register entities entity_registry.async_get_or_create( "sensor", "test", "very_unique", unit_of_measurement=original_unit ) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=native_unit, native_value=str(native_value), unique_id="very_unique", ) - entity0 = platform.ENTITIES["0"] + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -1715,17 +1671,15 @@ def test_device_classes_aligned() -> None: async def test_value_unknown_in_enumeration( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, ) -> None: """Test warning on invalid enum value.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value="invalid_option", device_class=SensorDeviceClass.ENUM, options=["option1", "option2"], ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -1739,17 +1693,15 @@ async def test_value_unknown_in_enumeration( async def test_invalid_enumeration_entity_with_device_class( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, ) -> None: """Test warning on entities that provide an enum with a device class.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value=21, device_class=SensorDeviceClass.POWER, options=["option1", "option2"], ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -1763,16 +1715,14 @@ async def test_invalid_enumeration_entity_with_device_class( async def test_invalid_enumeration_entity_without_device_class( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, ) -> None: """Test warning on entities that provide an enum without a device class.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value=21, options=["option1", "option2"], ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -1794,19 +1744,17 @@ async def test_invalid_enumeration_entity_without_device_class( async def test_non_numeric_device_class_with_unit_of_measurement( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, device_class: SensorDeviceClass, ) -> None: """Test error on numeric entities that provide an unit of measurement.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value=None, device_class=device_class, native_unit_of_measurement=UnitOfTemperature.CELSIUS, options=["option1", "option2"], ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -1869,18 +1817,16 @@ async def test_non_numeric_device_class_with_unit_of_measurement( async def test_device_classes_with_invalid_unit_of_measurement( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, device_class: SensorDeviceClass, ) -> None: """Test error when unit of measurement is not valid for used device class.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value="1.0", device_class=device_class, native_unit_of_measurement="INVALID!", ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) units = [ str(unit) if unit else "no unit of measurement" for unit in DEVICE_CLASS_UNITS.get(device_class, set()) @@ -1920,7 +1866,6 @@ async def test_device_classes_with_invalid_unit_of_measurement( async def test_non_numeric_validation_error( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, native_value: Any, problem: str, device_class: SensorDeviceClass | None, @@ -1928,16 +1873,14 @@ async def test_non_numeric_validation_error( unit: str | None, ) -> None: """Test error on expected numeric entities.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value=native_value, device_class=device_class, native_unit_of_measurement=unit, state_class=state_class, ) - entity0 = platform.ENTITIES["0"] + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -1966,7 +1909,6 @@ async def test_non_numeric_validation_error( async def test_non_numeric_validation_raise( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, native_value: Any, expected: str, device_class: SensorDeviceClass | None, @@ -1975,9 +1917,7 @@ async def test_non_numeric_validation_raise( precision, ) -> None: """Test error on expected numeric entities.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=unit, @@ -1985,7 +1925,7 @@ async def test_non_numeric_validation_raise( state_class=state_class, suggested_display_precision=precision, ) - entity0 = platform.ENTITIES["0"] + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -2018,7 +1958,6 @@ async def test_non_numeric_validation_raise( async def test_numeric_validation( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, native_value: Any, expected: str, device_class: SensorDeviceClass | None, @@ -2026,16 +1965,14 @@ async def test_numeric_validation( unit: str | None, ) -> None: """Test does not error on expected numeric entities.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value=native_value, device_class=device_class, native_unit_of_measurement=unit, state_class=state_class, ) - entity0 = platform.ENTITIES["0"] + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -2052,18 +1989,15 @@ async def test_numeric_validation( async def test_numeric_validation_ignores_custom_device_class( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, ) -> None: """Test does not error on expected numeric entities.""" native_value = "Three elephants" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value=native_value, device_class="custom__deviceclass", ) - entity0 = platform.ENTITIES["0"] + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -2084,18 +2018,16 @@ async def test_numeric_validation_ignores_custom_device_class( async def test_device_classes_with_invalid_state_class( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, device_class: SensorDeviceClass, ) -> None: """Test error when unit of measurement is not valid for used device class.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value=None, state_class="INVALID!", device_class=device_class, ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -2133,7 +2065,6 @@ async def test_device_classes_with_invalid_state_class( async def test_numeric_state_expected_helper( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, device_class: SensorDeviceClass | None, state_class: SensorStateClass | None, native_unit_of_measurement: str | None, @@ -2141,9 +2072,7 @@ async def test_numeric_state_expected_helper( is_numeric: bool, ) -> None: """Test numeric_state_expected helper.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value=None, device_class=device_class, @@ -2151,11 +2080,11 @@ async def test_numeric_state_expected_helper( native_unit_of_measurement=native_unit_of_measurement, suggested_display_precision=suggested_precision, ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() - entity0 = platform.ENTITIES["0"] state = hass.states.get(entity0.entity_id) assert state is not None @@ -2199,7 +2128,6 @@ async def test_numeric_state_expected_helper( ) async def test_unit_conversion_update( hass: HomeAssistant, - enable_custom_integrations: None, unit_system_1, unit_system_2, native_unit, @@ -2219,9 +2147,8 @@ async def test_unit_conversion_update( hass.config.units = unit_system_1 entity_registry = er.async_get(hass) - platform = getattr(hass.components, "test.sensor") - entity0 = platform.MockSensor( + entity0 = MockSensor( name="Test 0", device_class=device_class, native_unit_of_measurement=native_unit, @@ -2229,7 +2156,7 @@ async def test_unit_conversion_update( unique_id="very_unique", ) - entity1 = platform.MockSensor( + entity1 = MockSensor( name="Test 1", device_class=device_class, native_unit_of_measurement=native_unit, @@ -2237,7 +2164,7 @@ async def test_unit_conversion_update( unique_id="very_unique_1", ) - entity2 = platform.MockSensor( + entity2 = MockSensor( name="Test 2", device_class=device_class, native_unit_of_measurement=native_unit, @@ -2246,7 +2173,7 @@ async def test_unit_conversion_update( unique_id="very_unique_2", ) - entity3 = platform.MockSensor( + entity3 = MockSensor( name="Test 3", device_class=device_class, native_unit_of_measurement=native_unit, @@ -2255,7 +2182,7 @@ async def test_unit_conversion_update( unique_id="very_unique_3", ) - entity4 = platform.MockSensor( + entity4 = MockSensor( name="Test 4", device_class=device_class, native_unit_of_measurement=native_unit, @@ -2544,11 +2471,8 @@ async def test_entity_category_config_raises_error( caplog: pytest.LogCaptureFixture, ) -> None: """Test error is raised when entity category is set to config.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( - name="Test", entity_category=EntityCategory.CONFIG - ) + entity0 = MockSensor(name="Test", entity_category=EntityCategory.CONFIG) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -2644,13 +2568,11 @@ async def test_suggested_unit_guard_invalid_unit( An invalid suggested unit creates a log entry and the suggested unit will be ignored. """ entity_registry = er.async_get(hass) - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) state_value = 10 invalid_suggested_unit = "invalid_unit" - entity = platform.ENTITIES["0"] = platform.MockSensor( + entity = MockSensor( name="Invalid", device_class=device_class, native_unit_of_measurement=native_unit, @@ -2658,6 +2580,7 @@ async def test_suggested_unit_guard_invalid_unit( native_value=str(state_value), unique_id="invalid", ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -2674,10 +2597,10 @@ async def test_suggested_unit_guard_invalid_unit( "homeassistant.components.sensor", logging.WARNING, ( - " sets an" - " invalid suggested_unit_of_measurement. Please report it to the author" - " of the 'test' custom integration. This warning will become an error in" - " Home Assistant Core 2024.5" + " 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 @@ -2715,10 +2638,8 @@ async def test_suggested_unit_guard_valid_unit( in the entity registry. """ entity_registry = er.async_get(hass) - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - entity = platform.ENTITIES["0"] = platform.MockSensor( + entity = MockSensor( name="Valid", device_class=device_class, native_unit_of_measurement=native_unit, @@ -2726,6 +2647,7 @@ async def test_suggested_unit_guard_valid_unit( suggested_unit_of_measurement=suggested_unit, unique_id="valid", ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 40b38b2e57a..8084fe69e89 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -33,13 +33,14 @@ from homeassistant.components.recorder.statistics import ( list_statistic_ids, ) from homeassistant.components.recorder.util import get_instance, session_scope -from homeassistant.components.sensor import ATTR_OPTIONS, SensorDeviceClass +from homeassistant.components.sensor import ATTR_OPTIONS, DOMAIN, SensorDeviceClass from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant, State from homeassistant.setup import async_setup_component, setup_component import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM +from tests.common import setup_test_component_platform from tests.components.recorder.common import ( assert_dict_of_states_equal_without_context_and_last_changed, assert_multiple_states_equal_without_context_and_last_changed, @@ -49,6 +50,7 @@ from tests.components.recorder.common import ( statistics_during_period, wait_recording_done, ) +from tests.components.sensor.common import MockSensor from tests.typing import WebSocketGenerator BATTERY_SENSOR_ATTRIBUTES = { @@ -1363,11 +1365,9 @@ def test_compile_hourly_sum_statistics_negative_state( hass = hass_recorder() hass.data.pop(loader.DATA_CUSTOM_COMPONENTS) - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - mocksensor = platform.MockSensor(name="custom_sensor") + mocksensor = MockSensor(name="custom_sensor") mocksensor._attr_should_poll = False - platform.ENTITIES["custom_sensor"] = mocksensor + setup_test_component_platform(hass, DOMAIN, [mocksensor], built_in=False) setup_component(hass, "homeassistant", {}) setup_component( @@ -5178,9 +5178,7 @@ async def test_exclude_attributes( recorder_mock: Recorder, hass: HomeAssistant, enable_custom_integrations: None ) -> None: """Test sensor attributes to be excluded.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( has_entity_name=True, unique_id="test", name="Test", @@ -5188,6 +5186,7 @@ async def test_exclude_attributes( device_class=SensorDeviceClass.ENUM, options=["option1", "option2"], ) + setup_test_component_platform(hass, DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() await async_wait_recording_done(hass) From 62816e20414e4f1b89788b9485a1d587f4793c00 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 28 Mar 2024 12:41:08 +0100 Subject: [PATCH 033/967] Update aiogithubapi to 23.11.0 (#114362) --- homeassistant/components/github/manifest.json | 2 +- pyproject.toml | 2 -- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/github/manifest.json b/homeassistant/components/github/manifest.json index 1bc6c96c4b8..cae2e7faca9 100644 --- a/homeassistant/components/github/manifest.json +++ b/homeassistant/components/github/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/github", "iot_class": "cloud_polling", "loggers": ["aiogithubapi"], - "requirements": ["aiogithubapi==22.10.1"] + "requirements": ["aiogithubapi==23.11.0"] } diff --git a/pyproject.toml b/pyproject.toml index 0516f7222e5..3f25e7ace4d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -479,8 +479,6 @@ filterwarnings = [ # -- 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/ludeeus/aiogithubapi/pull/208 - >=23.9.0 - "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:aiogithubapi.namespaces.events", # 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/tschamm/boschshcpy/pull/39 - >=0.2.89 diff --git a/requirements_all.txt b/requirements_all.txt index e298e36ba9d..4c45b697950 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -251,7 +251,7 @@ aioflo==2021.11.0 aioftp==0.21.3 # homeassistant.components.github -aiogithubapi==22.10.1 +aiogithubapi==23.11.0 # homeassistant.components.guardian aioguardian==2022.07.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c5a12b34ce6..9fb6be9d43b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -227,7 +227,7 @@ aioesphomeapi==23.2.0 aioflo==2021.11.0 # homeassistant.components.github -aiogithubapi==22.10.1 +aiogithubapi==23.11.0 # homeassistant.components.guardian aioguardian==2022.07.0 From 68d6f96a9defd305b7d805b68590627951fb608c Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 28 Mar 2024 12:41:30 +0100 Subject: [PATCH 034/967] Update boschshcpy to 0.2.91 (#114366) --- homeassistant/components/bosch_shc/manifest.json | 2 +- pyproject.toml | 2 -- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/bosch_shc/manifest.json b/homeassistant/components/bosch_shc/manifest.json index efae159adc2..0c99324efbb 100644 --- a/homeassistant/components/bosch_shc/manifest.json +++ b/homeassistant/components/bosch_shc/manifest.json @@ -7,7 +7,7 @@ "documentation": "https://www.home-assistant.io/integrations/bosch_shc", "iot_class": "local_push", "loggers": ["boschshcpy"], - "requirements": ["boschshcpy==0.2.82"], + "requirements": ["boschshcpy==0.2.91"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/pyproject.toml b/pyproject.toml index 3f25e7ace4d..87ccffd7a66 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -481,8 +481,6 @@ filterwarnings = [ "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/tschamm/boschshcpy/pull/39 - >=0.2.89 - "ignore:pkg_resources is deprecated as an API:DeprecationWarning:boschshcpy.api", # https://github.com/DataDog/datadogpy/pull/290 - >=0.23.0 "ignore:invalid escape sequence:SyntaxWarning:.*datadog.dogstatsd.base", # https://github.com/fwestenberg/devialet/pull/6 - >1.4.5 diff --git a/requirements_all.txt b/requirements_all.txt index 4c45b697950..d294221ef78 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -596,7 +596,7 @@ bluetooth-data-tools==1.19.0 bond-async==0.2.1 # homeassistant.components.bosch_shc -boschshcpy==0.2.82 +boschshcpy==0.2.91 # homeassistant.components.amazon_polly # homeassistant.components.route53 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9fb6be9d43b..a00dd5f840b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -511,7 +511,7 @@ bluetooth-data-tools==1.19.0 bond-async==0.2.1 # homeassistant.components.bosch_shc -boschshcpy==0.2.82 +boschshcpy==0.2.91 # homeassistant.components.bring bring-api==0.5.7 From 5b98a8458f3bf90af1cff20f3cfbe08b03edb041 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 28 Mar 2024 13:24:44 +0100 Subject: [PATCH 035/967] Improve device class of utility meter (#114368) --- .../components/utility_meter/sensor.py | 37 +++-- tests/components/utility_meter/test_sensor.py | 138 +++++++++++++++--- 2 files changed, 146 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index 4e9be403cf7..26582df1b44 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Mapping from dataclasses import dataclass from datetime import datetime, timedelta from decimal import Decimal, DecimalException, InvalidOperation @@ -13,6 +14,7 @@ import voluptuous as vol from homeassistant.components.sensor import ( ATTR_LAST_RESET, + DEVICE_CLASS_UNITS, RestoreSensor, SensorDeviceClass, SensorExtraStoredData, @@ -21,12 +23,12 @@ from homeassistant.components.sensor import ( from homeassistant.components.sensor.recorder import _suggest_report_issue from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, CONF_UNIQUE_ID, STATE_UNAVAILABLE, STATE_UNKNOWN, - UnitOfEnergy, ) from homeassistant.core import Event, HomeAssistant, State, callback from homeassistant.helpers import ( @@ -47,6 +49,7 @@ from homeassistant.helpers.template import is_number from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import slugify import homeassistant.util.dt as dt_util +from homeassistant.util.enum import try_parse_enum from .const import ( ATTR_CRON_PATTERN, @@ -97,12 +100,6 @@ ATTR_LAST_PERIOD = "last_period" ATTR_LAST_VALID_STATE = "last_valid_state" ATTR_TARIFF = "tariff" -DEVICE_CLASS_MAP = { - UnitOfEnergy.WATT_HOUR: SensorDeviceClass.ENERGY, - UnitOfEnergy.KILO_WATT_HOUR: SensorDeviceClass.ENERGY, -} - - PRECISION = 3 PAUSED = "paused" COLLECTING = "collecting" @@ -313,6 +310,7 @@ class UtilitySensorExtraStoredData(SensorExtraStoredData): last_reset: datetime | None last_valid_state: Decimal | None status: str + input_device_class: SensorDeviceClass | None def as_dict(self) -> dict[str, Any]: """Return a dict representation of the utility sensor data.""" @@ -324,6 +322,7 @@ class UtilitySensorExtraStoredData(SensorExtraStoredData): str(self.last_valid_state) if self.last_valid_state else None ) data["status"] = self.status + data["input_device_class"] = str(self.input_device_class) return data @@ -343,6 +342,9 @@ class UtilitySensorExtraStoredData(SensorExtraStoredData): else None ) status: str = restored["status"] + input_device_class = try_parse_enum( + SensorDeviceClass, restored.get("input_device_class") + ) except KeyError: # restored is a dict, but does not have all values return None @@ -357,6 +359,7 @@ class UtilitySensorExtraStoredData(SensorExtraStoredData): last_reset, last_valid_state, status, + input_device_class, ) @@ -397,6 +400,7 @@ class UtilityMeterSensor(RestoreSensor): self._last_valid_state = None self._collecting = None self._name = name + self._input_device_class = None self._unit_of_measurement = None self._period = meter_type if meter_type is not None: @@ -416,9 +420,10 @@ class UtilityMeterSensor(RestoreSensor): self._tariff = tariff self._tariff_entity = tariff_entity - def start(self, unit): + def start(self, attributes: Mapping[str, Any]) -> None: """Initialize unit and state upon source initial update.""" - self._unit_of_measurement = unit + self._input_device_class = attributes.get(ATTR_DEVICE_CLASS) + self._unit_of_measurement = attributes.get(ATTR_UNIT_OF_MEASUREMENT) self._state = 0 self.async_write_ha_state() @@ -482,6 +487,7 @@ class UtilityMeterSensor(RestoreSensor): new_state = event.data["new_state"] if new_state is None: return + new_state_attributes: Mapping[str, Any] = new_state.attributes or {} # First check if the new_state is valid (see discussion in PR #88446) if (new_state_val := self._validate_state(new_state)) is None: @@ -498,7 +504,7 @@ class UtilityMeterSensor(RestoreSensor): for sensor in self.hass.data[DATA_UTILITY][self._parent_meter][ DATA_TARIFF_SENSORS ]: - sensor.start(new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)) + sensor.start(new_state_attributes) if self._unit_of_measurement is None: _LOGGER.warning( "Source sensor %s has no unit of measurement. Please %s", @@ -512,7 +518,8 @@ class UtilityMeterSensor(RestoreSensor): # If net_consumption is off, the adjustment must be non-negative self._state += adjustment # type: ignore[operator] # self._state will be set to by the start function if it is None, therefore it always has a valid Decimal value at this line - self._unit_of_measurement = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + self._input_device_class = new_state_attributes.get(ATTR_DEVICE_CLASS) + self._unit_of_measurement = new_state_attributes.get(ATTR_UNIT_OF_MEASUREMENT) self._last_valid_state = new_state_val self.async_write_ha_state() @@ -600,6 +607,7 @@ class UtilityMeterSensor(RestoreSensor): if (last_sensor_data := await self.async_get_last_sensor_data()) is not None: # new introduced in 2022.04 self._state = last_sensor_data.native_value + self._input_device_class = last_sensor_data.input_device_class self._unit_of_measurement = last_sensor_data.native_unit_of_measurement self._last_period = last_sensor_data.last_period self._last_reset = last_sensor_data.last_reset @@ -693,7 +701,11 @@ class UtilityMeterSensor(RestoreSensor): @property def device_class(self): """Return the device class of the sensor.""" - return DEVICE_CLASS_MAP.get(self._unit_of_measurement) + if self._input_device_class is not None: + return self._input_device_class + if self._unit_of_measurement in DEVICE_CLASS_UNITS[SensorDeviceClass.ENERGY]: + return SensorDeviceClass.ENERGY + return None @property def state_class(self): @@ -744,6 +756,7 @@ class UtilityMeterSensor(RestoreSensor): self._last_reset, self._last_valid_state, PAUSED if self._collecting is None else COLLECTING, + self._input_device_class, ) async def async_get_last_sensor_data(self) -> UtilitySensorExtraStoredData | None: diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index 13b367b1fb7..99a63809329 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -40,6 +40,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, UnitOfEnergy, + UnitOfVolume, ) from homeassistant.core import CoreState, HomeAssistant, State from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -553,8 +554,66 @@ async def test_entity_name(hass: HomeAssistant, yaml_config, entity_id, name) -> ), ], ) +@pytest.mark.parametrize( + ( + "energy_sensor_attributes", + "gas_sensor_attributes", + "energy_meter_attributes", + "gas_meter_attributes", + ), + [ + ( + {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: "some_archaic_unit"}, + { + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, + }, + { + ATTR_DEVICE_CLASS: None, + ATTR_UNIT_OF_MEASUREMENT: "some_archaic_unit", + }, + ), + ( + {}, + {}, + { + ATTR_DEVICE_CLASS: None, + ATTR_UNIT_OF_MEASUREMENT: None, + }, + { + ATTR_DEVICE_CLASS: None, + ATTR_UNIT_OF_MEASUREMENT: None, + }, + ), + ( + { + ATTR_DEVICE_CLASS: SensorDeviceClass.GAS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, + }, + { + ATTR_DEVICE_CLASS: SensorDeviceClass.WATER, + ATTR_UNIT_OF_MEASUREMENT: "some_archaic_unit", + }, + { + ATTR_DEVICE_CLASS: SensorDeviceClass.GAS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, + }, + { + ATTR_DEVICE_CLASS: SensorDeviceClass.WATER, + ATTR_UNIT_OF_MEASUREMENT: "some_archaic_unit", + }, + ), + ], +) async def test_device_class( - hass: HomeAssistant, yaml_config, config_entry_configs + hass: HomeAssistant, + yaml_config, + config_entry_configs, + energy_sensor_attributes, + gas_sensor_attributes, + energy_meter_attributes, + gas_meter_attributes, ) -> None: """Test utility device_class.""" if yaml_config: @@ -579,27 +638,23 @@ async def test_device_class( await hass.async_block_till_done() - hass.states.async_set( - entity_id_energy, 2, {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR} - ) - hass.states.async_set( - entity_id_gas, 2, {ATTR_UNIT_OF_MEASUREMENT: "some_archaic_unit"} - ) + hass.states.async_set(entity_id_energy, 2, energy_sensor_attributes) + hass.states.async_set(entity_id_gas, 2, gas_sensor_attributes) await hass.async_block_till_done() state = hass.states.get("sensor.energy_meter") assert state is not None assert state.state == "0" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR + for attr, value in energy_meter_attributes.items(): + assert state.attributes.get(attr) == value state = hass.states.get("sensor.gas_meter") assert state is not None assert state.state == "0" - assert state.attributes.get(ATTR_DEVICE_CLASS) is None assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL_INCREASING - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "some_archaic_unit" + for attr, value in gas_meter_attributes.items(): + assert state.attributes.get(attr) == value @pytest.mark.parametrize( @@ -610,7 +665,13 @@ async def test_device_class( "utility_meter": { "energy_bill": { "source": "sensor.energy", - "tariffs": ["tariff1", "tariff2", "tariff3", "tariff4"], + "tariffs": [ + "tariff0", + "tariff1", + "tariff2", + "tariff3", + "tariff4", + ], } } }, @@ -626,7 +687,13 @@ async def test_device_class( "offset": 0, "periodically_resetting": True, "source": "sensor.energy", - "tariffs": ["tariff1", "tariff2", "tariff3", "tariff4"], + "tariffs": [ + "tariff0", + "tariff1", + "tariff2", + "tariff3", + "tariff4", + ], }, ), ], @@ -644,7 +711,33 @@ async def test_restore_state( mock_restore_cache_with_extra_data( hass, [ - # sensor.energy_bill_tariff1 is restored as expected + # sensor.energy_bill_tariff0 is restored as expected, including device + # class + ( + State( + "sensor.energy_bill_tariff0", + "0.1", + attributes={ + ATTR_STATUS: PAUSED, + ATTR_LAST_RESET: last_reset_1, + ATTR_UNIT_OF_MEASUREMENT: UnitOfVolume.CUBIC_METERS, + }, + ), + { + "native_value": { + "__type": "", + "decimal_str": "0.2", + }, + "native_unit_of_measurement": "gal", + "last_reset": last_reset_2, + "last_period": "1.3", + "last_valid_state": None, + "status": "collecting", + "input_device_class": "water", + }, + ), + # sensor.energy_bill_tariff1 is restored as expected, except device + # class ( State( "sensor.energy_bill_tariff1", @@ -743,12 +836,21 @@ async def test_restore_state( await hass.async_block_till_done() # restore from cache + state = hass.states.get("sensor.energy_bill_tariff0") + assert state.state == "0.2" + assert state.attributes.get("status") == COLLECTING + assert state.attributes.get("last_reset") == last_reset_2 + assert state.attributes.get("last_valid_state") == "None" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfVolume.GALLONS + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WATER + state = hass.states.get("sensor.energy_bill_tariff1") assert state.state == "1.2" assert state.attributes.get("status") == PAUSED assert state.attributes.get("last_reset") == last_reset_2 assert state.attributes.get("last_valid_state") == "None" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY state = hass.states.get("sensor.energy_bill_tariff2") assert state.state == "2.1" @@ -756,6 +858,7 @@ async def test_restore_state( assert state.attributes.get("last_reset") == last_reset_1 assert state.attributes.get("last_valid_state") == "None" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.MEGA_WATT_HOUR + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY state = hass.states.get("sensor.energy_bill_tariff3") assert state.state == "3.1" @@ -763,6 +866,7 @@ async def test_restore_state( assert state.attributes.get("last_reset") == last_reset_1 assert state.attributes.get("last_valid_state") == "None" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.MEGA_WATT_HOUR + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY state = hass.states.get("sensor.energy_bill_tariff4") assert state.state == STATE_UNKNOWN @@ -770,16 +874,16 @@ async def test_restore_state( # utility_meter is loaded, now set sensors according to utility_meter: hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await hass.async_block_till_done() state = hass.states.get("select.energy_bill") - assert state.state == "tariff1" + assert state.state == "tariff0" - state = hass.states.get("sensor.energy_bill_tariff1") + state = hass.states.get("sensor.energy_bill_tariff0") assert state.attributes.get("status") == COLLECTING for entity_id in ( + "sensor.energy_bill_tariff1", "sensor.energy_bill_tariff2", "sensor.energy_bill_tariff3", "sensor.energy_bill_tariff4", From c9c0625fa55a7b8d00c3c936c19813d6470a2d2e Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 28 Mar 2024 13:25:01 +0100 Subject: [PATCH 036/967] Adapt Tractive integration the latest API changes (#114380) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/tractive/__init__.py | 15 ++------------- homeassistant/components/tractive/const.py | 1 - homeassistant/components/tractive/sensor.py | 5 ++--- 3 files changed, 4 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/tractive/__init__.py b/homeassistant/components/tractive/__init__.py index 41e691f783e..136e8b3632a 100644 --- a/homeassistant/components/tractive/__init__.py +++ b/homeassistant/components/tractive/__init__.py @@ -40,7 +40,6 @@ from .const import ( SERVER_UNAVAILABLE, SWITCH_KEY_MAP, TRACKABLES, - TRACKER_ACTIVITY_STATUS_UPDATED, TRACKER_HARDWARE_STATUS_UPDATED, TRACKER_POSITION_UPDATED, TRACKER_SWITCH_STATUS_UPDATED, @@ -220,9 +219,6 @@ class TractiveClient: if server_was_unavailable: _LOGGER.debug("Tractive is back online") server_was_unavailable = False - if event["message"] == "activity_update": - self._send_activity_update(event) - continue if event["message"] == "wellness_overview": self._send_wellness_update(event) continue @@ -291,15 +287,6 @@ class TractiveClient: TRACKER_SWITCH_STATUS_UPDATED, event["tracker_id"], payload ) - def _send_activity_update(self, event: dict[str, Any]) -> None: - payload = { - ATTR_MINUTES_ACTIVE: event["progress"]["achieved_minutes"], - ATTR_DAILY_GOAL: event["progress"]["goal_minutes"], - } - self._dispatch_tracker_event( - TRACKER_ACTIVITY_STATUS_UPDATED, event["pet_id"], payload - ) - def _send_wellness_update(self, event: dict[str, Any]) -> None: sleep_day = None sleep_night = None @@ -309,6 +296,8 @@ class TractiveClient: payload = { ATTR_ACTIVITY_LABEL: event["wellness"].get("activity_label"), ATTR_CALORIES: event["activity"]["calories"], + ATTR_DAILY_GOAL: event["activity"]["minutes_goal"], + ATTR_MINUTES_ACTIVE: event["activity"]["minutes_active"], ATTR_MINUTES_DAY_SLEEP: sleep_day, ATTR_MINUTES_NIGHT_SLEEP: sleep_night, ATTR_MINUTES_REST: event["activity"]["minutes_rest"], diff --git a/homeassistant/components/tractive/const.py b/homeassistant/components/tractive/const.py index acb4f6f7487..f26c0ee2345 100644 --- a/homeassistant/components/tractive/const.py +++ b/homeassistant/components/tractive/const.py @@ -26,7 +26,6 @@ CLIENT_ID = "625e5349c3c3b41c28a669f1" CLIENT = "client" TRACKABLES = "trackables" -TRACKER_ACTIVITY_STATUS_UPDATED = f"{DOMAIN}_tracker_activity_updated" TRACKER_HARDWARE_STATUS_UPDATED = f"{DOMAIN}_tracker_hardware_status_updated" TRACKER_POSITION_UPDATED = f"{DOMAIN}_tracker_position_updated" TRACKER_SWITCH_STATUS_UPDATED = f"{DOMAIN}_tracker_switch_updated" diff --git a/homeassistant/components/tractive/sensor.py b/homeassistant/components/tractive/sensor.py index b73b5faba05..5e2f3288f57 100644 --- a/homeassistant/components/tractive/sensor.py +++ b/homeassistant/components/tractive/sensor.py @@ -37,7 +37,6 @@ from .const import ( CLIENT, DOMAIN, TRACKABLES, - TRACKER_ACTIVITY_STATUS_UPDATED, TRACKER_HARDWARE_STATUS_UPDATED, TRACKER_WELLNESS_STATUS_UPDATED, ) @@ -118,7 +117,7 @@ SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = ( key=ATTR_MINUTES_ACTIVE, translation_key="activity_time", native_unit_of_measurement=UnitOfTime.MINUTES, - signal_prefix=TRACKER_ACTIVITY_STATUS_UPDATED, + signal_prefix=TRACKER_WELLNESS_STATUS_UPDATED, state_class=SensorStateClass.TOTAL, ), TractiveSensorEntityDescription( @@ -139,7 +138,7 @@ SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = ( key=ATTR_DAILY_GOAL, translation_key="daily_goal", native_unit_of_measurement=UnitOfTime.MINUTES, - signal_prefix=TRACKER_ACTIVITY_STATUS_UPDATED, + signal_prefix=TRACKER_WELLNESS_STATUS_UPDATED, ), TractiveSensorEntityDescription( key=ATTR_MINUTES_DAY_SLEEP, From f80e319a4d2833051ce831910cbc8960c514fb5b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 28 Mar 2024 13:29:43 +0100 Subject: [PATCH 037/967] Update pytest-xdist to 3.4.0 (#114377) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 4dd02246a6e..d57d1c4a5df 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -28,7 +28,7 @@ pytest-sugar==1.0.0 pytest-timeout==2.3.1 pytest-unordered==0.6.0 pytest-picked==0.5.0 -pytest-xdist==3.3.1 +pytest-xdist==3.4.0 pytest==8.1.1 requests-mock==1.11.0 respx==0.21.0 From 596436d67952fe8e4e3fc359b99385ed63e6ab4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Matheson=20Wergeland?= Date: Thu, 28 Mar 2024 13:31:55 +0100 Subject: [PATCH 038/967] =?UTF-8?q?Avoid=20changing=20local=20time=20on=20?= =?UTF-8?q?Nob=C3=B8=20Ecohub=20(#114332)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit nobo_hub: Pass timezone to avoid changing local time on Nobø Ecohub in handshake --- homeassistant/components/nobo_hub/__init__.py | 9 ++++++++- homeassistant/components/nobo_hub/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/nobo_hub/__init__.py b/homeassistant/components/nobo_hub/__init__.py index 15a4b48c315..f9d2ce2e3da 100644 --- a/homeassistant/components/nobo_hub/__init__.py +++ b/homeassistant/components/nobo_hub/__init__.py @@ -7,6 +7,7 @@ from pynobo import nobo from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_IP_ADDRESS, EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.core import HomeAssistant +import homeassistant.util.dt as dt_util from .const import CONF_AUTO_DISCOVERED, CONF_SERIAL, DOMAIN @@ -19,7 +20,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: serial = entry.data[CONF_SERIAL] discover = entry.data[CONF_AUTO_DISCOVERED] ip_address = None if discover else entry.data[CONF_IP_ADDRESS] - hub = nobo(serial=serial, ip=ip_address, discover=discover, synchronous=False) + hub = nobo( + serial=serial, + ip=ip_address, + discover=discover, + synchronous=False, + timezone=dt_util.DEFAULT_TIME_ZONE, + ) await hub.connect() hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/nobo_hub/manifest.json b/homeassistant/components/nobo_hub/manifest.json index 9ddbed7dadc..4741eb39e29 100644 --- a/homeassistant/components/nobo_hub/manifest.json +++ b/homeassistant/components/nobo_hub/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/nobo_hub", "integration_type": "hub", "iot_class": "local_push", - "requirements": ["pynobo==1.6.0"] + "requirements": ["pynobo==1.8.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index d294221ef78..cccc7cc9bdf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1991,7 +1991,7 @@ pynetgear==0.10.10 pynetio==0.1.9.1 # homeassistant.components.nobo_hub -pynobo==1.6.0 +pynobo==1.8.0 # homeassistant.components.nuki pynuki==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a00dd5f840b..391cac327b7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1545,7 +1545,7 @@ pymysensors==0.24.0 pynetgear==0.10.10 # homeassistant.components.nobo_hub -pynobo==1.6.0 +pynobo==1.8.0 # homeassistant.components.nuki pynuki==1.6.3 From 5fb12c93aaaad1ac2e9539d9e084d77f21957e7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Mind=C3=AAllo=20de=20Andrade?= Date: Thu, 28 Mar 2024 09:53:32 -0300 Subject: [PATCH 039/967] SunWEG reauth flow (#105861) * feat(sunweg): reauth flow * fix(sunweg): autentication as sunweg 2.1.0 * fix: configflowresult * chore(sunweg): dedupe code * chore(sunweg): using entry_id instead of unique_id * test(sunweg): added test launch reauth flow * chore(sunweg): moved test_reauth_started test * chore(sunweg): formatting * chore(sunweg): formating --------- Co-authored-by: J. Nick Koston --- homeassistant/components/sunweg/__init__.py | 4 +- .../components/sunweg/config_flow.py | 89 +++++++++++--- homeassistant/components/sunweg/strings.json | 13 +- tests/components/sunweg/common.py | 1 + tests/components/sunweg/test_config_flow.py | 112 +++++++++++++++--- tests/components/sunweg/test_init.py | 14 +++ 6 files changed, 201 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/sunweg/__init__.py b/homeassistant/components/sunweg/__init__.py index 6c39a04127e..86da0a247b1 100644 --- a/homeassistant/components/sunweg/__init__.py +++ b/homeassistant/components/sunweg/__init__.py @@ -11,6 +11,7 @@ from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.typing import StateType, UndefinedType from homeassistant.util import Throttle @@ -27,8 +28,7 @@ async def async_setup_entry( """Load the saved entities.""" api = APIHelper(entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD]) if not await hass.async_add_executor_job(api.authenticate): - _LOGGER.error("Username or Password may be incorrect!") - return False + raise ConfigEntryAuthFailed("Username or Password may be incorrect!") hass.data.setdefault(DOMAIN, {})[entry.entry_id] = SunWEGData( api, entry.data[CONF_PLANT_ID] ) diff --git a/homeassistant/components/sunweg/config_flow.py b/homeassistant/components/sunweg/config_flow.py index c4af05a0cc9..2b5e49c2cb9 100644 --- a/homeassistant/components/sunweg/config_flow.py +++ b/homeassistant/components/sunweg/config_flow.py @@ -1,6 +1,9 @@ """Config flow for Sun WEG integration.""" -from sunweg.api import APIHelper +from collections.abc import Mapping +from typing import Any + +from sunweg.api import APIHelper, SunWegApiError import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult @@ -18,37 +21,61 @@ class SunWEGConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialise sun weg server flow.""" self.api: APIHelper = None - self.data: dict = {} + self.data: dict[str, Any] = {} @callback - def _async_show_user_form(self, errors=None) -> ConfigFlowResult: + def _async_show_user_form(self, step_id: str, errors=None) -> ConfigFlowResult: """Show the form to the user.""" + default_username = "" + if CONF_USERNAME in self.data: + default_username = self.data[CONF_USERNAME] data_schema = vol.Schema( { - vol.Required(CONF_USERNAME): str, + vol.Required(CONF_USERNAME, default=default_username): str, vol.Required(CONF_PASSWORD): str, } ) return self.async_show_form( - step_id="user", data_schema=data_schema, errors=errors + step_id=step_id, data_schema=data_schema, errors=errors ) + def _set_auth_data( + self, step: str, username: str, password: str + ) -> ConfigFlowResult | None: + """Set username and password.""" + if self.api: + # Set username and password + self.api.username = username + self.api.password = password + else: + # Initialise the library with the username & password + self.api = APIHelper(username, password) + + try: + if not self.api.authenticate(): + return self._async_show_user_form(step, {"base": "invalid_auth"}) + except SunWegApiError: + return self._async_show_user_form(step, {"base": "timeout_connect"}) + + return None + async def async_step_user(self, user_input=None) -> ConfigFlowResult: """Handle the start of the config flow.""" if not user_input: - return self._async_show_user_form() - - # Initialise the library with the username & password - self.api = APIHelper(user_input[CONF_USERNAME], user_input[CONF_PASSWORD]) - login_response = await self.hass.async_add_executor_job(self.api.authenticate) - - if not login_response: - return self._async_show_user_form({"base": "invalid_auth"}) + return self._async_show_user_form("user") # Store authentication info self.data = user_input - return await self.async_step_plant() + + conf_result = await self.hass.async_add_executor_job( + self._set_auth_data, + "user", + user_input[CONF_USERNAME], + user_input[CONF_PASSWORD], + ) + + return await self.async_step_plant() if conf_result is None else conf_result async def async_step_plant(self, user_input=None) -> ConfigFlowResult: """Handle adding a "plant" to Home Assistant.""" @@ -72,3 +99,37 @@ class SunWEGConfigFlow(ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() self.data.update(user_input) return self.async_create_entry(title=self.data[CONF_NAME], data=self.data) + + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: + """Handle reauthorization request from SunWEG.""" + self.data.update(entry_data) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle reauthorization flow.""" + if user_input is None: + return self._async_show_user_form("reauth_confirm") + + self.data.update(user_input) + conf_result = await self.hass.async_add_executor_job( + self._set_auth_data, + "reauth_confirm", + user_input[CONF_USERNAME], + user_input[CONF_PASSWORD], + ) + if conf_result is not None: + return conf_result + + entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + if entry is not None: + data: Mapping[str, Any] = self.data + self.hass.config_entries.async_update_entry(entry, data=data) + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) + + return self.async_abort(reason="reauth_successful") diff --git a/homeassistant/components/sunweg/strings.json b/homeassistant/components/sunweg/strings.json index 3a910e62940..6033bc314bc 100644 --- a/homeassistant/components/sunweg/strings.json +++ b/homeassistant/components/sunweg/strings.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "no_plants": "No plants have been found on this account" + "no_plants": "No plants have been found on this account", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "error": { - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "timeout_connect": "[%key:common::config_flow::error::timeout_connect%]" }, "step": { "plant": { @@ -19,6 +21,13 @@ "username": "[%key:common::config_flow::data::username%]" }, "title": "Enter your Sun WEG information" + }, + "reauth_confirm": { + "data": { + "password": "[%key:common::config_flow::data::password%]", + "username": "[%key:common::config_flow::data::username%]" + }, + "title": "[%key:common::config_flow::title::reauth%]" } } } diff --git a/tests/components/sunweg/common.py b/tests/components/sunweg/common.py index 616f5c0137f..096113f6609 100644 --- a/tests/components/sunweg/common.py +++ b/tests/components/sunweg/common.py @@ -12,6 +12,7 @@ SUNWEG_USER_INPUT = { SUNWEG_MOCK_ENTRY = MockConfigEntry( domain=DOMAIN, + unique_id=0, data={ CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password", diff --git a/tests/components/sunweg/test_config_flow.py b/tests/components/sunweg/test_config_flow.py index 84957a419dd..54ad4f3f234 100644 --- a/tests/components/sunweg/test_config_flow.py +++ b/tests/components/sunweg/test_config_flow.py @@ -2,14 +2,14 @@ from unittest.mock import patch -from sunweg.api import APIHelper +from sunweg.api import APIHelper, SunWegApiError from homeassistant import config_entries, data_entry_flow from homeassistant.components.sunweg.const import CONF_PLANT_ID, DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from .common import SUNWEG_USER_INPUT +from .common import SUNWEG_MOCK_ENTRY, SUNWEG_USER_INPUT from tests.common import MockConfigEntry @@ -40,12 +40,99 @@ async def test_incorrect_login(hass: HomeAssistant) -> None: assert result["errors"] == {"base": "invalid_auth"} -async def test_no_plants_on_account(hass: HomeAssistant) -> None: - """Test registering an integration with no plants available.""" +async def test_server_unavailable(hass: HomeAssistant) -> None: + """Test when the SunWEG server don't respond.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) + with patch.object( + APIHelper, "authenticate", side_effect=SunWegApiError("Internal Server Error") + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], SUNWEG_USER_INPUT + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "timeout_connect"} + + +async def test_reauth(hass: HomeAssistant) -> None: + """Test reauth flow.""" + mock_entry = SUNWEG_MOCK_ENTRY + mock_entry.add_to_hass(hass) + + entries = hass.config_entries.async_entries() + assert len(entries) == 1 + assert entries[0].data[CONF_USERNAME] == SUNWEG_MOCK_ENTRY.data[CONF_USERNAME] + assert entries[0].data[CONF_PASSWORD] == SUNWEG_MOCK_ENTRY.data[CONF_PASSWORD] + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": mock_entry.entry_id, + }, + data=mock_entry.data, + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + with patch.object(APIHelper, "authenticate", return_value=False): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=SUNWEG_USER_INPUT, + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert result["errors"] == {"base": "invalid_auth"} + + with patch.object( + APIHelper, "authenticate", side_effect=SunWegApiError("Internal Server Error") + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=SUNWEG_USER_INPUT, + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert result["errors"] == {"base": "timeout_connect"} + + with patch.object(APIHelper, "authenticate", return_value=True): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=SUNWEG_USER_INPUT, + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + entries = hass.config_entries.async_entries() + + assert len(entries) == 1 + assert entries[0].data[CONF_USERNAME] == SUNWEG_USER_INPUT[CONF_USERNAME] + assert entries[0].data[CONF_PASSWORD] == SUNWEG_USER_INPUT[CONF_PASSWORD] + + +async def test_no_plants_on_account(hass: HomeAssistant) -> None: + """Test registering an integration with wrong auth then with no plants available.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch.object(APIHelper, "authenticate", return_value=False): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], SUNWEG_USER_INPUT + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "invalid_auth"} + with ( patch.object(APIHelper, "authenticate", return_value=True), patch.object(APIHelper, "listPlants", return_value=[]), @@ -63,22 +150,21 @@ async def test_multiple_plant_ids(hass: HomeAssistant, plant_fixture) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - user_input = SUNWEG_USER_INPUT.copy() - plant_list = [plant_fixture, plant_fixture] with ( patch.object(APIHelper, "authenticate", return_value=True), - patch.object(APIHelper, "listPlants", return_value=plant_list), + patch.object( + APIHelper, "listPlants", return_value=[plant_fixture, plant_fixture] + ), ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input + result["flow_id"], SUNWEG_USER_INPUT ) assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "plant" - user_input = {CONF_PLANT_ID: 123456} result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input + result["flow_id"], {CONF_PLANT_ID: 123456} ) await hass.async_block_till_done() @@ -93,7 +179,6 @@ async def test_one_plant_on_account(hass: HomeAssistant, plant_fixture) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - user_input = SUNWEG_USER_INPUT.copy() with ( patch.object(APIHelper, "authenticate", return_value=True), @@ -104,7 +189,7 @@ async def test_one_plant_on_account(hass: HomeAssistant, plant_fixture) -> None: ), ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input + result["flow_id"], SUNWEG_USER_INPUT ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY @@ -120,7 +205,6 @@ async def test_existing_plant_configured(hass: HomeAssistant, plant_fixture) -> result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - user_input = SUNWEG_USER_INPUT.copy() with ( patch.object(APIHelper, "authenticate", return_value=True), @@ -131,7 +215,7 @@ async def test_existing_plant_configured(hass: HomeAssistant, plant_fixture) -> ), ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input + result["flow_id"], SUNWEG_USER_INPUT ) assert result["type"] == "abort" diff --git a/tests/components/sunweg/test_init.py b/tests/components/sunweg/test_init.py index cc2e880d82e..41edda38a5a 100644 --- a/tests/components/sunweg/test_init.py +++ b/tests/components/sunweg/test_init.py @@ -10,6 +10,7 @@ from homeassistant.components.sunweg.const import DOMAIN, DeviceType from homeassistant.components.sunweg.sensor_types.sensor_entity_description import ( SunWEGSensorEntityDescription, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -193,3 +194,16 @@ async def test_sunwegdata_get_data_never_reset() -> None: never_resets=entity_description.never_resets, previous_value_drop_threshold=entity_description.previous_value_drop_threshold, ) == (2.8, None) + + +async def test_reauth_started(hass: HomeAssistant) -> None: + """Test reauth flow started.""" + mock_entry = SUNWEG_MOCK_ENTRY + mock_entry.add_to_hass(hass) + with patch.object(APIHelper, "authenticate", return_value=False): + await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + assert mock_entry.state is ConfigEntryState.SETUP_ERROR + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["step_id"] == "reauth_confirm" From a3f251674ac6e1b3298bcb4a4f046c744e7ad115 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 28 Mar 2024 13:56:08 +0100 Subject: [PATCH 040/967] Update romy to 0.0.9 (#114360) --- homeassistant/components/romy/manifest.json | 2 +- pyproject.toml | 2 -- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/romy/manifest.json b/homeassistant/components/romy/manifest.json index 1257c2d1d60..7e30c418599 100644 --- a/homeassistant/components/romy/manifest.json +++ b/homeassistant/components/romy/manifest.json @@ -5,6 +5,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/romy", "iot_class": "local_polling", - "requirements": ["romy==0.0.7"], + "requirements": ["romy==0.0.9"], "zeroconf": ["_aicu-http._tcp.local."] } diff --git a/pyproject.toml b/pyproject.toml index 87ccffd7a66..4309e107046 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -501,8 +501,6 @@ filterwarnings = [ "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:onvif.client", # https://github.com/okunishinishi/python-stringcase/commit/6a5c5bbd3fe5337862abc7fd0853a0f36e18b2e1 - >1.2.0 "ignore:invalid escape sequence:SyntaxWarning:.*stringcase", - # https://github.com/xeniter/romy/pull/1 - >=0.0.8 - "ignore:with timeout\\(\\) is deprecated, use async with timeout\\(\\) instead:DeprecationWarning:romy.utils", # https://github.com/grahamwetzler/smart-meter-texas/pull/143 - >0.5.3 "ignore:ssl.OP_NO_SSL\\*/ssl.OP_NO_TLS\\* options are deprecated:DeprecationWarning:smart_meter_texas", # https://github.com/mvantellingen/python-zeep/pull/1364 - >4.2.1 diff --git a/requirements_all.txt b/requirements_all.txt index cccc7cc9bdf..9c39c47f1d7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2459,7 +2459,7 @@ rocketchat-API==0.6.1 rokuecp==0.19.2 # homeassistant.components.romy -romy==0.0.7 +romy==0.0.9 # homeassistant.components.roomba roombapy==1.6.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 391cac327b7..d44aa8a6ea0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1893,7 +1893,7 @@ ring-doorbell[listen]==0.8.7 rokuecp==0.19.2 # homeassistant.components.romy -romy==0.0.7 +romy==0.0.9 # homeassistant.components.roomba roombapy==1.6.13 From 71a0a7fe0011db7643fa217782496ccc09c5f0dd Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Thu, 28 Mar 2024 13:56:23 +0100 Subject: [PATCH 041/967] Use `setup_test_component_platform` helper for switch entity component tests instead of `hass.components` (#114305) * Use `setup_test_component_platform` helper for switch entity component tests instead of `hass.components` * Do not import fixtures * Re-add switch.py to testing_config as stub * Rename to mock_toggle_entities --- tests/components/conftest.py | 10 +++++++ .../generic_hygrostat/test_humidifier.py | 9 +++--- .../generic_thermostat/test_climate.py | 9 +++--- tests/components/switch/common.py | 13 ++++++++ tests/components/switch/conftest.py | 3 -- tests/components/switch/test_init.py | 9 +++--- .../custom_components/test/switch.py | 30 ++----------------- 7 files changed, 41 insertions(+), 42 deletions(-) delete mode 100644 tests/components/switch/conftest.py diff --git a/tests/components/conftest.py b/tests/components/conftest.py index d84fb3600ab..0831c566666 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -8,6 +8,8 @@ import pytest from homeassistant.const import STATE_OFF, STATE_ON +from tests.common import MockToggleEntity + if TYPE_CHECKING: from tests.components.light.common import MockLight from tests.components.sensor.common import MockSensor @@ -127,3 +129,11 @@ def mock_sensor_entities() -> dict[str, "MockSensor"]: from tests.components.sensor.common import get_mock_sensor_entities return get_mock_sensor_entities() + + +@pytest.fixture +def mock_toggle_entities() -> list[MockToggleEntity]: + """Return mocked toggle entities.""" + from tests.components.switch.common import get_mock_toggle_entities + + return get_mock_toggle_entities() diff --git a/tests/components/generic_hygrostat/test_humidifier.py b/tests/components/generic_hygrostat/test_humidifier.py index fdad20f5b2d..528418b9974 100644 --- a/tests/components/generic_hygrostat/test_humidifier.py +++ b/tests/components/generic_hygrostat/test_humidifier.py @@ -37,9 +37,11 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.common import ( + MockToggleEntity, assert_setup_component, async_fire_time_changed, mock_restore_cache, + setup_test_component_platform, ) ENTITY = "humidifier.test" @@ -127,12 +129,11 @@ async def test_humidifier_input_boolean(hass: HomeAssistant, setup_comp_1) -> No async def test_humidifier_switch( - hass: HomeAssistant, setup_comp_1, enable_custom_integrations: None + hass: HomeAssistant, setup_comp_1, mock_toggle_entities: list[MockToggleEntity] ) -> None: """Test humidifier switching test switch.""" - platform = getattr(hass.components, "test.switch") - platform.init() - switch_1 = platform.ENTITIES[1] + setup_test_component_platform(hass, switch.DOMAIN, mock_toggle_entities) + switch_1 = mock_toggle_entities[1] assert await async_setup_component( hass, switch.DOMAIN, {"switch": {"platform": "test"}} ) diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py index fdcad219d93..f6424f894cf 100644 --- a/tests/components/generic_thermostat/test_climate.py +++ b/tests/components/generic_thermostat/test_climate.py @@ -50,11 +50,13 @@ from homeassistant.util import dt as dt_util from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM from tests.common import ( + MockToggleEntity, assert_setup_component, async_fire_time_changed, async_mock_service, get_fixture_path, mock_restore_cache, + setup_test_component_platform, ) from tests.components.climate import common @@ -140,12 +142,11 @@ async def test_heater_input_boolean(hass: HomeAssistant, setup_comp_1) -> None: async def test_heater_switch( - hass: HomeAssistant, setup_comp_1, enable_custom_integrations: None + hass: HomeAssistant, setup_comp_1, mock_toggle_entities: list[MockToggleEntity] ) -> None: """Test heater switching test switch.""" - platform = getattr(hass.components, "test.switch") - platform.init() - switch_1 = platform.ENTITIES[1] + setup_test_component_platform(hass, switch.DOMAIN, mock_toggle_entities) + switch_1 = mock_toggle_entities[1] assert await async_setup_component( hass, switch.DOMAIN, {"switch": {"platform": "test"}} ) diff --git a/tests/components/switch/common.py b/tests/components/switch/common.py index 60c79fdf6a8..cb30efe47d0 100644 --- a/tests/components/switch/common.py +++ b/tests/components/switch/common.py @@ -10,9 +10,13 @@ from homeassistant.const import ( ENTITY_MATCH_ALL, SERVICE_TURN_OFF, SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, ) from homeassistant.loader import bind_hass +from tests.common import MockToggleEntity + @bind_hass def turn_on(hass, entity_id=ENTITY_MATCH_ALL): @@ -36,3 +40,12 @@ async def async_turn_off(hass, entity_id=ENTITY_MATCH_ALL): """Turn all or specified switch off.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None await hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data, blocking=True) + + +def get_mock_toggle_entities() -> list[MockToggleEntity]: + """Return a list of mock toggle entities.""" + return [ + MockToggleEntity("AC", STATE_ON), + MockToggleEntity("AC", STATE_OFF), + MockToggleEntity(None, STATE_OFF), + ] diff --git a/tests/components/switch/conftest.py b/tests/components/switch/conftest.py deleted file mode 100644 index c526ef4c4fe..00000000000 --- a/tests/components/switch/conftest.py +++ /dev/null @@ -1,3 +0,0 @@ -"""switch conftest.""" - -from tests.components.light.conftest import mock_light_profiles # noqa: F401 diff --git a/tests/components/switch/test_init.py b/tests/components/switch/test_init.py index 62801346744..28e9d273570 100644 --- a/tests/components/switch/test_init.py +++ b/tests/components/switch/test_init.py @@ -11,18 +11,19 @@ from homeassistant.setup import async_setup_component from . import common from tests.common import ( + MockToggleEntity, MockUser, help_test_all, import_and_test_deprecated_constant_enum, + setup_test_component_platform, ) @pytest.fixture(autouse=True) -def entities(hass): +def entities(hass: HomeAssistant, mock_toggle_entities: list[MockToggleEntity]): """Initialize the test switch.""" - platform = getattr(hass.components, "test.switch") - platform.init() - return platform.ENTITIES + setup_test_component_platform(hass, switch.DOMAIN, mock_toggle_entities) + return mock_toggle_entities async def test_methods( diff --git a/tests/testing_config/custom_components/test/switch.py b/tests/testing_config/custom_components/test/switch.py index 5a2cd7bc17d..b06db33746f 100644 --- a/tests/testing_config/custom_components/test/switch.py +++ b/tests/testing_config/custom_components/test/switch.py @@ -1,32 +1,8 @@ -"""Provide a mock switch platform. - -Call init before using it in your tests to ensure clean test data. -""" - -from homeassistant.const import STATE_OFF, STATE_ON - -from tests.common import MockToggleEntity - -ENTITIES = [] - - -def init(empty=False): - """Initialize the platform with entities.""" - global ENTITIES - - ENTITIES = ( - [] - if empty - else [ - MockToggleEntity("AC", STATE_ON), - MockToggleEntity("AC", STATE_OFF), - MockToggleEntity(None, STATE_OFF), - ] - ) +"""Stub switch platform for translation tests.""" async def async_setup_platform( hass, config, async_add_entities_callback, discovery_info=None ): - """Return mock entities.""" - async_add_entities_callback(ENTITIES) + """Stub setup for translation tests.""" + async_add_entities_callback([]) From b90542077c6b00740771e47aa10045c08b4235f0 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 28 Mar 2024 14:18:44 +0100 Subject: [PATCH 042/967] Update boto3 to 1.34.51 and aiobotocore to 2.12.1 (#114379) --- homeassistant/components/amazon_polly/manifest.json | 2 +- homeassistant/components/aws/manifest.json | 2 +- homeassistant/components/route53/manifest.json | 2 +- requirements_all.txt | 4 ++-- requirements_test_all.txt | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/amazon_polly/manifest.json b/homeassistant/components/amazon_polly/manifest.json index 55137b58832..803bf8b80aa 100644 --- a/homeassistant/components/amazon_polly/manifest.json +++ b/homeassistant/components/amazon_polly/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/amazon_polly", "iot_class": "cloud_push", "loggers": ["boto3", "botocore", "s3transfer"], - "requirements": ["boto3==1.33.13"] + "requirements": ["boto3==1.34.51"] } diff --git a/homeassistant/components/aws/manifest.json b/homeassistant/components/aws/manifest.json index 61caf4c2318..470ccc0e409 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.9.1"] + "requirements": ["aiobotocore==2.12.1"] } diff --git a/homeassistant/components/route53/manifest.json b/homeassistant/components/route53/manifest.json index 3db91f7926f..d4ce0d2cc97 100644 --- a/homeassistant/components/route53/manifest.json +++ b/homeassistant/components/route53/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/route53", "iot_class": "cloud_push", "loggers": ["boto3", "botocore", "s3transfer"], - "requirements": ["boto3==1.33.13"] + "requirements": ["boto3==1.34.51"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9c39c47f1d7..19b3fe975ff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -212,7 +212,7 @@ aioazuredevops==1.4.3 aiobafi6==0.9.0 # homeassistant.components.aws -aiobotocore==2.9.1 +aiobotocore==2.12.1 # homeassistant.components.comelit aiocomelit==0.9.0 @@ -600,7 +600,7 @@ boschshcpy==0.2.91 # homeassistant.components.amazon_polly # homeassistant.components.route53 -boto3==1.33.13 +boto3==1.34.51 # homeassistant.components.bring bring-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d44aa8a6ea0..c8f3b80d4fb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -191,7 +191,7 @@ aioazuredevops==1.4.3 aiobafi6==0.9.0 # homeassistant.components.aws -aiobotocore==2.9.1 +aiobotocore==2.12.1 # homeassistant.components.comelit aiocomelit==0.9.0 From 2511a9a087bc8a0a5d4024917caf636ebd29e384 Mon Sep 17 00:00:00 2001 From: Mark Adkins Date: Thu, 28 Mar 2024 09:19:25 -0400 Subject: [PATCH 043/967] Add SharkIQ room targeting (#89350) * SharkIQ Dep & Codeowner Update * Update code owners * SharkIQ Room-Targeting Support * Add Tests for New Service * Remove unreachable code * Refine tests to reflect unreachable code changes * Updates based on PR comments * Updates based on PR review comments * Address issues found in PR Review * Update Exception type, add excption message to strings. Do not save room list in state history. * Update message to be more clear that only one faild room is listed * couple more updates based on comments --------- Co-authored-by: jrlambs Co-authored-by: Robert Resch --- homeassistant/components/sharkiq/const.py | 1 + homeassistant/components/sharkiq/icons.json | 5 ++ .../components/sharkiq/services.yaml | 15 ++++++ homeassistant/components/sharkiq/strings.json | 17 +++++++ homeassistant/components/sharkiq/vacuum.py | 46 +++++++++++++++++-- tests/components/sharkiq/const.py | 5 ++ tests/components/sharkiq/test_vacuum.py | 40 ++++++++++++++++ 7 files changed, 126 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/sharkiq/icons.json create mode 100644 homeassistant/components/sharkiq/services.yaml diff --git a/homeassistant/components/sharkiq/const.py b/homeassistant/components/sharkiq/const.py index 8d5d4708e0e..f328e6453cc 100644 --- a/homeassistant/components/sharkiq/const.py +++ b/homeassistant/components/sharkiq/const.py @@ -12,6 +12,7 @@ PLATFORMS = [Platform.VACUUM] DOMAIN = "sharkiq" SHARK = "Shark" UPDATE_INTERVAL = timedelta(seconds=30) +SERVICE_CLEAN_ROOM = "clean_room" SHARKIQ_REGION_EUROPE = "europe" SHARKIQ_REGION_ELSEWHERE = "elsewhere" diff --git a/homeassistant/components/sharkiq/icons.json b/homeassistant/components/sharkiq/icons.json new file mode 100644 index 00000000000..13fd58ce66d --- /dev/null +++ b/homeassistant/components/sharkiq/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "clean_room": "mdi:robot-vacuum" + } +} diff --git a/homeassistant/components/sharkiq/services.yaml b/homeassistant/components/sharkiq/services.yaml new file mode 100644 index 00000000000..7f82ed40702 --- /dev/null +++ b/homeassistant/components/sharkiq/services.yaml @@ -0,0 +1,15 @@ +clean_room: + target: + entity: + integration: "sharkiq" + domain: "vacuum" + + fields: + rooms: + required: true + advanced: false + example: "Kitchen" + default: "" + selector: + area: + multiple: true diff --git a/homeassistant/components/sharkiq/strings.json b/homeassistant/components/sharkiq/strings.json index 23f949be4cc..c1648332975 100644 --- a/homeassistant/components/sharkiq/strings.json +++ b/homeassistant/components/sharkiq/strings.json @@ -40,5 +40,22 @@ "elsewhere": "Everywhere Else" } } + }, + "exceptions": { + "invalid_room": { + "message": "The room { room } is unavailable to your vacuum. Make sure all rooms match the Shark App, including capitalization." + } + }, + "services": { + "clean_room": { + "name": "Clean Room", + "description": "Cleans a specific user-defined room or set of rooms.", + "fields": { + "rooms": { + "name": "Rooms", + "description": "List of rooms to clean" + } + } + } } } diff --git a/homeassistant/components/sharkiq/vacuum.py b/homeassistant/components/sharkiq/vacuum.py index 658d446b9cb..6647b79c892 100644 --- a/homeassistant/components/sharkiq/vacuum.py +++ b/homeassistant/components/sharkiq/vacuum.py @@ -6,6 +6,7 @@ from collections.abc import Iterable from typing import Any from sharkiq import OperatingModes, PowerModes, Properties, SharkIqVacuum +import voluptuous as vol from homeassistant.components.vacuum import ( STATE_CLEANING, @@ -18,11 +19,14 @@ from homeassistant.components.vacuum import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError +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.update_coordinator import CoordinatorEntity -from .const import DOMAIN, LOGGER, SHARK +from .const import DOMAIN, LOGGER, SERVICE_CLEAN_ROOM, SHARK from .update_coordinator import SharkIqUpdateCoordinator OPERATING_STATE_MAP = { @@ -45,7 +49,7 @@ ATTR_ERROR_CODE = "last_error_code" ATTR_ERROR_MSG = "last_error_message" ATTR_LOW_LIGHT = "low_light" ATTR_RECHARGE_RESUME = "recharge_and_resume" -ATTR_RSSI = "rssi" +ATTR_ROOMS = "rooms" async def async_setup_entry( @@ -64,6 +68,17 @@ async def async_setup_entry( ) async_add_entities([SharkVacuumEntity(d, coordinator) for d in devices]) + platform = entity_platform.async_get_current_platform() + platform.async_register_entity_service( + SERVICE_CLEAN_ROOM, + { + vol.Required(ATTR_ROOMS): vol.All( + cv.ensure_list, vol.Length(min=1), [cv.string] + ), + }, + "async_clean_room", + ) + class SharkVacuumEntity(CoordinatorEntity[SharkIqUpdateCoordinator], StateVacuumEntity): """Shark IQ vacuum entity.""" @@ -81,6 +96,7 @@ class SharkVacuumEntity(CoordinatorEntity[SharkIqUpdateCoordinator], StateVacuum | VacuumEntityFeature.STOP | VacuumEntityFeature.LOCATE ) + _unrecorded_attributes = frozenset({ATTR_ROOMS}) def __init__( self, sharkiq: SharkIqVacuum, coordinator: SharkIqUpdateCoordinator @@ -136,7 +152,7 @@ class SharkVacuumEntity(CoordinatorEntity[SharkIqUpdateCoordinator], StateVacuum @property def operating_mode(self) -> str | None: - """Operating mode..""" + """Operating mode.""" op_mode = self.sharkiq.get_property_value(Properties.OPERATING_MODE) return OPERATING_STATE_MAP.get(op_mode) @@ -192,6 +208,24 @@ class SharkVacuumEntity(CoordinatorEntity[SharkIqUpdateCoordinator], StateVacuum """Cause the device to generate a loud chirp.""" await self.sharkiq.async_find_device() + async def async_clean_room(self, rooms: list[str], **kwargs: Any) -> None: + """Clean specific rooms.""" + rooms_to_clean = [] + valid_rooms = self.available_rooms or [] + for room in rooms: + if room in valid_rooms: + rooms_to_clean.append(room) + else: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_room", + translation_placeholders={"room": room}, + ) + + LOGGER.debug("Cleaning room(s): %s", rooms_to_clean) + await self.sharkiq.async_clean_rooms(rooms_to_clean) + await self.coordinator.async_refresh() + @property def fan_speed(self) -> str | None: """Return the current fan speed.""" @@ -225,6 +259,11 @@ class SharkVacuumEntity(CoordinatorEntity[SharkIqUpdateCoordinator], StateVacuum """Let us know if the robot is operating in low-light mode.""" return self.sharkiq.get_property_value(Properties.LOW_LIGHT_MISSION) + @property + def available_rooms(self) -> list | None: + """Return a list of rooms available to clean.""" + return self.sharkiq.get_room_list() + @property def extra_state_attributes(self) -> dict[str, Any]: """Return a dictionary of device state attributes specific to sharkiq.""" @@ -233,5 +272,6 @@ class SharkVacuumEntity(CoordinatorEntity[SharkIqUpdateCoordinator], StateVacuum ATTR_ERROR_MSG: self.sharkiq.error_text, ATTR_LOW_LIGHT: self.low_light, ATTR_RECHARGE_RESUME: self.recharge_resume, + ATTR_ROOMS: self.available_rooms, } return data diff --git a/tests/components/sharkiq/const.py b/tests/components/sharkiq/const.py index b4f9d72dafd..e8d920e7763 100644 --- a/tests/components/sharkiq/const.py +++ b/tests/components/sharkiq/const.py @@ -65,6 +65,11 @@ SHARK_PROPERTIES_DICT = { "read_only": True, "value": "Dummy Firmware 1.0", }, + "Robot_Room_List": { + "base_type": "string", + "read_only": True, + "value": "Kitchen", + }, } TEST_USERNAME = "test-username" diff --git a/tests/components/sharkiq/test_vacuum.py b/tests/components/sharkiq/test_vacuum.py index 4a1671a616f..c72ad1a8c36 100644 --- a/tests/components/sharkiq/test_vacuum.py +++ b/tests/components/sharkiq/test_vacuum.py @@ -11,7 +11,9 @@ from unittest.mock import patch import pytest from sharkiq import AylaApi, SharkIqAuthError, SharkIqNotAuthedError, SharkIqVacuum +from voluptuous.error import MultipleInvalid +from homeassistant import exceptions from homeassistant.components.homeassistant import SERVICE_UPDATE_ENTITY from homeassistant.components.sharkiq import DOMAIN from homeassistant.components.sharkiq.vacuum import ( @@ -19,7 +21,9 @@ from homeassistant.components.sharkiq.vacuum import ( ATTR_ERROR_MSG, ATTR_LOW_LIGHT, ATTR_RECHARGE_RESUME, + ATTR_ROOMS, FAN_SPEEDS_MAP, + SERVICE_CLEAN_ROOM, ) from homeassistant.components.vacuum import ( ATTR_BATTERY_LEVEL, @@ -58,6 +62,7 @@ from .const import ( from tests.common import MockConfigEntry VAC_ENTITY_ID = f"vacuum.{SHARK_DEVICE_DICT['product_name'].lower()}" +ROOM_LIST = ["Kitchen", "Living Room"] EXPECTED_FEATURES = ( VacuumEntityFeature.BATTERY | VacuumEntityFeature.FAN_SPEED @@ -129,6 +134,10 @@ class MockShark(SharkIqVacuum): """Set a property locally without hitting the API.""" self.set_property_value(property_name, value) + def get_room_list(self): + """Return the list of available rooms without hitting the API.""" + return ROOM_LIST + @pytest.fixture(autouse=True) @patch("sharkiq.ayla_api.AylaApi", MockAyla) @@ -165,6 +174,7 @@ async def test_simple_properties(hass: HomeAssistant) -> None: (ATTR_ERROR_MSG, "Cliff sensor is blocked"), (ATTR_LOW_LIGHT, False), (ATTR_RECHARGE_RESUME, True), + (ATTR_ROOMS, ROOM_LIST), ], ) async def test_initial_attributes( @@ -223,6 +233,24 @@ async def test_device_properties( assert getattr(device, device_property) == target_value +@pytest.mark.parametrize( + ("room_list", "exception"), + [ + (["KITCHEN"], exceptions.ServiceValidationError), + (["KITCHEN", "MUD_ROOM", "DOG HOUSE"], exceptions.ServiceValidationError), + (["Office"], exceptions.ServiceValidationError), + ([], MultipleInvalid), + ], +) +async def test_clean_room_error( + hass: HomeAssistant, room_list: list, exception: Exception +) -> None: + """Test clean_room errors.""" + 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) + + async def test_locate(hass: HomeAssistant) -> None: """Test that the locate command works.""" with patch.object(SharkIqVacuum, "async_find_device") as mock_locate: @@ -231,6 +259,18 @@ async def test_locate(hass: HomeAssistant) -> None: mock_locate.assert_called_once() +@pytest.mark.parametrize( + ("room_list"), + [(ROOM_LIST), (["Kitchen"])], +) +async def test_clean_room(hass: HomeAssistant, room_list: list) -> None: + """Test that the clean_room command works.""" + with patch.object(SharkIqVacuum, "async_clean_rooms") as mock_clean_room: + data = {ATTR_ENTITY_ID: VAC_ENTITY_ID, ATTR_ROOMS: room_list} + await hass.services.async_call(DOMAIN, SERVICE_CLEAN_ROOM, data, blocking=True) + mock_clean_room.assert_called_once_with(room_list) + + @pytest.mark.parametrize( ("side_effect", "success"), [ From 52ca14de4826a3501a392f110e15ff48aecd5488 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 28 Mar 2024 14:57:18 +0100 Subject: [PATCH 044/967] Add matter zeroconf (#114385) * Add matter zeroconf * Clean up --- homeassistant/components/matter/manifest.json | 3 +- homeassistant/generated/zeroconf.py | 5 +++ tests/components/matter/test_config_flow.py | 44 +++++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/matter/manifest.json b/homeassistant/components/matter/manifest.json index 716e296ec15..0e27eb36f85 100644 --- a/homeassistant/components/matter/manifest.json +++ b/homeassistant/components/matter/manifest.json @@ -6,5 +6,6 @@ "dependencies": ["websocket_api"], "documentation": "https://www.home-assistant.io/integrations/matter", "iot_class": "local_push", - "requirements": ["python-matter-server==5.7.0"] + "requirements": ["python-matter-server==5.7.0"], + "zeroconf": ["_matter._tcp.local."] } diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index baf922cdc99..060084209fd 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -573,6 +573,11 @@ ZEROCONF = { }, }, ], + "_matter._tcp.local.": [ + { + "domain": "matter", + }, + ], "_mediaremotetv._tcp.local.": [ { "domain": "apple_tv", diff --git a/tests/components/matter/test_config_flow.py b/tests/components/matter/test_config_flow.py index e690844c228..283642c8964 100644 --- a/tests/components/matter/test_config_flow.py +++ b/tests/components/matter/test_config_flow.py @@ -3,6 +3,7 @@ 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 @@ -12,6 +13,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.hassio import HassioAPIError, HassioServiceInfo from homeassistant.components.matter.const import ADDON_SLUG, DOMAIN +from homeassistant.components.zeroconf import ZeroconfServiceInfo from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -177,6 +179,48 @@ async def test_manual_already_configured( assert setup_entry.call_count == 1 +async def test_zeroconf_discovery( + hass: HomeAssistant, + client_connect: AsyncMock, + setup_entry: AsyncMock, +) -> None: + """Test flow started from Zeroconf discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=ZeroconfServiceInfo( + ip_address=ip_address("fd11:be53:8d46:0:729e:5a4f:539d:1ee6"), + ip_addresses=[ip_address("fd11:be53:8d46:0:729e:5a4f:539d:1ee6")], + port=5540, + hostname="CDEFGHIJ12345678.local.", + type="_matter._tcp.local.", + name="ABCDEFGH123456789-0000000012345678._matter._tcp.local.", + properties={"SII": "3300", "SAI": "1100", "T": "0"}, + ), + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "manual" + assert result["errors"] is None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "url": "ws://localhost:5580/ws", + }, + ) + await hass.async_block_till_done() + + assert client_connect.call_count == 1 + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Matter" + assert result["data"] == { + "url": "ws://localhost:5580/ws", + "integration_created_addon": False, + "use_addon": False, + } + assert setup_entry.call_count == 1 + + @pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}]) async def test_supervisor_discovery( hass: HomeAssistant, From f9aa7d34f82e2b750ccfb1b6e8be8d89c4bffffe Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 28 Mar 2024 15:44:50 +0100 Subject: [PATCH 045/967] Use fallback voice for selected language in cloud (#114246) Co-authored-by: Erik Montnemery --- homeassistant/components/cloud/tts.py | 24 +++++- tests/components/cloud/test_tts.py | 109 ++++++++++++++++++++++---- 2 files changed, 115 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/cloud/tts.py b/homeassistant/components/cloud/tts.py index 7922fc80201..42e4b94a189 100644 --- a/homeassistant/components/cloud/tts.py +++ b/homeassistant/components/cloud/tts.py @@ -140,7 +140,6 @@ class CloudTTSEntity(TextToSpeechEntity): """Return a dict include default options.""" return { ATTR_AUDIO_OUTPUT: AudioOutput.MP3, - ATTR_VOICE: self._voice, } @property @@ -178,7 +177,18 @@ class CloudTTSEntity(TextToSpeechEntity): gender: Gender | str | None = options.get(ATTR_GENDER) gender = handle_deprecated_gender(self.hass, gender) original_voice: str | None = options.get(ATTR_VOICE) + if original_voice is None and language == self._language: + original_voice = self._voice voice = handle_deprecated_voice(self.hass, original_voice) + if voice not in TTS_VOICES[language]: + default_voice = TTS_VOICES[language][0] + _LOGGER.debug( + "Unsupported voice %s detected, falling back to default %s for %s", + voice, + default_voice, + language, + ) + voice = default_voice # Process TTS try: data = await self.cloud.voice.process_tts( @@ -237,7 +247,6 @@ class CloudProvider(Provider): """Return a dict include default options.""" return { ATTR_AUDIO_OUTPUT: AudioOutput.MP3, - ATTR_VOICE: self._voice, } async def async_get_tts_audio( @@ -248,7 +257,18 @@ class CloudProvider(Provider): gender: Gender | str | None = options.get(ATTR_GENDER) gender = handle_deprecated_gender(self.hass, gender) original_voice: str | None = options.get(ATTR_VOICE) + if original_voice is None and language == self._language: + original_voice = self._voice voice = handle_deprecated_voice(self.hass, original_voice) + if voice not in TTS_VOICES[language]: + default_voice = TTS_VOICES[language][0] + _LOGGER.debug( + "Unsupported voice %s detected, falling back to default %s for %s", + voice, + default_voice, + language, + ) + voice = default_voice # Process TTS try: data = await self.cloud.voice.process_tts( diff --git a/tests/components/cloud/test_tts.py b/tests/components/cloud/test_tts.py index 3fd9ec5e4a4..06dbcf174a7 100644 --- a/tests/components/cloud/test_tts.py +++ b/tests/components/cloud/test_tts.py @@ -12,10 +12,20 @@ import voluptuous as vol from homeassistant.components.assist_pipeline.pipeline import STORAGE_KEY from homeassistant.components.cloud import DOMAIN, const, tts -from homeassistant.components.tts import DOMAIN as TTS_DOMAIN +from homeassistant.components.media_player import ( + ATTR_MEDIA_CONTENT_ID, + DOMAIN as DOMAIN_MP, + SERVICE_PLAY_MEDIA, +) +from homeassistant.components.tts import ( + ATTR_LANGUAGE, + ATTR_MEDIA_PLAYER_ENTITY_ID, + ATTR_MESSAGE, + DOMAIN as TTS_DOMAIN, +) from homeassistant.components.tts.helper import get_engine_instance from homeassistant.config import async_process_ha_core_config -from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_registry import EntityRegistry from homeassistant.helpers.issue_registry import IssueRegistry, IssueSeverity @@ -23,6 +33,8 @@ from homeassistant.setup import async_setup_component from . import PIPELINE_DATA +from tests.common import async_mock_service +from tests.components.tts.common import get_media_source_url from tests.typing import ClientSessionGenerator @@ -120,13 +132,13 @@ async def test_prefs_default_voice( assert engine is not None # The platform config provider will be overridden by the discovery info provider. assert engine.default_language == "en-US" - assert engine.default_options == {"audio_output": "mp3", "voice": "JennyNeural"} + assert engine.default_options == {"audio_output": "mp3"} await set_cloud_prefs({"tts_default_voice": ("nl-NL", "MaartenNeural")}) await hass.async_block_till_done() assert engine.default_language == "nl-NL" - assert engine.default_options == {"audio_output": "mp3", "voice": "MaartenNeural"} + assert engine.default_options == {"audio_output": "mp3"} async def test_deprecated_platform_config( @@ -228,11 +240,11 @@ async def test_get_tts_audio( "url": ( "http://example.local:8123/api/tts_proxy/" "42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_en-us_5c97d21c48_{expected_url_suffix}.mp3" + f"_en-us_6e8b81ac47_{expected_url_suffix}.mp3" ), "path": ( "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_en-us_5c97d21c48_{expected_url_suffix}.mp3" + f"_en-us_6e8b81ac47_{expected_url_suffix}.mp3" ), } await hass.async_block_till_done() @@ -242,6 +254,7 @@ async def test_get_tts_audio( assert mock_process_tts.call_args.kwargs["text"] == "There is someone at the door." assert mock_process_tts.call_args.kwargs["language"] == "en-US" assert mock_process_tts.call_args.kwargs["gender"] is None + assert mock_process_tts.call_args.kwargs["voice"] == "JennyNeural" assert mock_process_tts.call_args.kwargs["output"] == "mp3" @@ -280,11 +293,11 @@ async def test_get_tts_audio_logged_out( "url": ( "http://example.local:8123/api/tts_proxy/" "42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_en-us_5c97d21c48_{expected_url_suffix}.mp3" + f"_en-us_6e8b81ac47_{expected_url_suffix}.mp3" ), "path": ( "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_en-us_5c97d21c48_{expected_url_suffix}.mp3" + f"_en-us_6e8b81ac47_{expected_url_suffix}.mp3" ), } await hass.async_block_till_done() @@ -294,6 +307,7 @@ async def test_get_tts_audio_logged_out( assert mock_process_tts.call_args.kwargs["text"] == "There is someone at the door." assert mock_process_tts.call_args.kwargs["language"] == "en-US" assert mock_process_tts.call_args.kwargs["gender"] is None + assert mock_process_tts.call_args.kwargs["voice"] == "JennyNeural" assert mock_process_tts.call_args.kwargs["output"] == "mp3" @@ -344,11 +358,11 @@ async def test_tts_entity( "url": ( "http://example.local:8123/api/tts_proxy/" "42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_en-us_5c97d21c48_{entity_id}.mp3" + f"_en-us_6e8b81ac47_{entity_id}.mp3" ), "path": ( "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_en-us_5c97d21c48_{entity_id}.mp3" + f"_en-us_6e8b81ac47_{entity_id}.mp3" ), } await hass.async_block_till_done() @@ -358,6 +372,7 @@ async def test_tts_entity( assert mock_process_tts.call_args.kwargs["text"] == "There is someone at the door." assert mock_process_tts.call_args.kwargs["language"] == "en-US" assert mock_process_tts.call_args.kwargs["gender"] is None + assert mock_process_tts.call_args.kwargs["voice"] == "JennyNeural" assert mock_process_tts.call_args.kwargs["output"] == "mp3" state = hass.states.get(entity_id) @@ -632,11 +647,11 @@ async def test_deprecated_gender( "url": ( "http://example.local:8123/api/tts_proxy/" "42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_{language.lower()}_5c97d21c48_{expected_url_suffix}.mp3" + f"_{language.lower()}_6e8b81ac47_{expected_url_suffix}.mp3" ), "path": ( "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_{language.lower()}_5c97d21c48_{expected_url_suffix}.mp3" + f"_{language.lower()}_6e8b81ac47_{expected_url_suffix}.mp3" ), } await hass.async_block_till_done() @@ -645,7 +660,7 @@ async def test_deprecated_gender( assert mock_process_tts.call_args is not None assert mock_process_tts.call_args.kwargs["text"] == "There is someone at the door." assert mock_process_tts.call_args.kwargs["language"] == language - assert mock_process_tts.call_args.kwargs["voice"] == "JennyNeural" + assert mock_process_tts.call_args.kwargs["voice"] == "XiaoxiaoNeural" assert mock_process_tts.call_args.kwargs["output"] == "mp3" issue = issue_registry.async_get_issue("cloud", "deprecated_gender") assert issue is None @@ -662,11 +677,11 @@ async def test_deprecated_gender( "url": ( "http://example.local:8123/api/tts_proxy/" "42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_{language.lower()}_5dded72256_{expected_url_suffix}.mp3" + f"_{language.lower()}_dd0e95eb04_{expected_url_suffix}.mp3" ), "path": ( "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_{language.lower()}_5dded72256_{expected_url_suffix}.mp3" + f"_{language.lower()}_dd0e95eb04_{expected_url_suffix}.mp3" ), } await hass.async_block_till_done() @@ -678,7 +693,7 @@ async def test_deprecated_gender( assert mock_process_tts.call_args.kwargs["text"] == "There is someone at the door." assert mock_process_tts.call_args.kwargs["language"] == language assert mock_process_tts.call_args.kwargs["gender"] == gender_option - assert mock_process_tts.call_args.kwargs["voice"] == "JennyNeural" + assert mock_process_tts.call_args.kwargs["voice"] == "XiaoxiaoNeural" assert mock_process_tts.call_args.kwargs["output"] == "mp3" issue = issue_registry.async_get_issue("cloud", issue_id) assert issue is not None @@ -733,3 +748,65 @@ async def test_deprecated_gender( } assert not issue_registry.async_get_issue(DOMAIN, issue_id) + + +@pytest.mark.parametrize( + ("service", "service_data"), + [ + ( + "speak", + { + ATTR_ENTITY_ID: "tts.home_assistant_cloud", + ATTR_LANGUAGE: "id-ID", + ATTR_MEDIA_PLAYER_ENTITY_ID: "media_player.something", + ATTR_MESSAGE: "There is someone at the door.", + }, + ), + ( + "cloud_say", + { + ATTR_ENTITY_ID: "media_player.something", + ATTR_LANGUAGE: "id-ID", + ATTR_MESSAGE: "There is someone at the door.", + }, + ), + ], +) +async def test_tts_services( + hass: HomeAssistant, + cloud: MagicMock, + hass_client: ClientSessionGenerator, + service: str, + service_data: dict[str, Any], +) -> None: + """Test tts services.""" + calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + mock_process_tts = AsyncMock(return_value=b"") + cloud.voice.process_tts = mock_process_tts + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + await cloud.login("test-user", "test-pass") + client = await hass_client() + + await hass.services.async_call( + domain=TTS_DOMAIN, + service=service, + service_data=service_data, + blocking=True, + ) + + assert len(calls) == 1 + + url = await get_media_source_url(hass, calls[0].data[ATTR_MEDIA_CONTENT_ID]) + await hass.async_block_till_done() + response = await client.get(url) + assert response.status == HTTPStatus.OK + await hass.async_block_till_done() + + assert mock_process_tts.call_count == 1 + assert mock_process_tts.call_args is not None + assert mock_process_tts.call_args.kwargs["text"] == "There is someone at the door." + assert mock_process_tts.call_args.kwargs["language"] == service_data[ATTR_LANGUAGE] + assert mock_process_tts.call_args.kwargs["voice"] == "GadisNeural" + assert mock_process_tts.call_args.kwargs["output"] == "mp3" From 192fad040ad05a35c7f6c41fba6417bf661547b8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 28 Mar 2024 16:20:20 +0100 Subject: [PATCH 046/967] Fix hassfest service icons check for custom integrations (#114389) --- script/hassfest/services.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/script/hassfest/services.py b/script/hassfest/services.py index 34f9b906fb5..c962d84e6e1 100644 --- a/script/hassfest/services.py +++ b/script/hassfest/services.py @@ -168,7 +168,8 @@ def validate_services(config: Config, integration: Integration) -> None: # 2. Check if the service has an icon set in icons.json. # raise an error if not., for service_name, service_schema in services.items(): - if service_name not in service_icons: + if integration.core and service_name not in service_icons: + # This is enforced for Core integrations only integration.add_error( "services", f"Service {service_name} has no icon in icons.json.", From 3df03f5be513d402a39ef9a64ea081766df324d9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 28 Mar 2024 16:57:29 +0100 Subject: [PATCH 047/967] Fix area search for entities of devices (#114394) --- homeassistant/components/search/__init__.py | 9 +++++--- tests/components/search/test_init.py | 24 ++++++++++++++++++--- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/search/__init__.py b/homeassistant/components/search/__init__.py index 71b51210a25..a85a21e8102 100644 --- a/homeassistant/components/search/__init__.py +++ b/homeassistant/components/search/__init__.py @@ -136,6 +136,9 @@ class Searcher: # Scripts referencing this area self._add(ItemType.SCRIPT, script.scripts_with_area(self.hass, area_id)) + # Entity in this area, will extend this with the entities of the devices in this area + entity_entries = er.async_entries_for_area(self._entity_registry, area_id) + # Devices in this area for device in dr.async_entries_for_area(self._device_registry, area_id): self._add(ItemType.DEVICE, device.id) @@ -160,10 +163,10 @@ class Searcher: # Skip the entity if it's in a different area if entity_entry.area_id is not None: continue - self._add(ItemType.ENTITY, entity_entry.entity_id) + entity_entries.append(entity_entry) - # Entities in this area - for entity_entry in er.async_entries_for_area(self._entity_registry, area_id): + # Process entities in this area + for entity_entry in entity_entries: self._add(ItemType.ENTITY, entity_entry.entity_id) # If this entity also exists as a resource, we add it. diff --git a/tests/components/search/test_init.py b/tests/components/search/test_init.py index ee7b60dc9ac..a817fbfc39e 100644 --- a/tests/components/search/test_init.py +++ b/tests/components/search/test_init.py @@ -496,11 +496,14 @@ async def test_search( ItemType.SCRIPT: {script_scene_entity.entity_id, "script.nested"}, } assert search(ItemType.AREA, living_room_area.id) == { - ItemType.AUTOMATION: {"automation.wled_device"}, + ItemType.AUTOMATION: {"automation.wled_device", "automation.wled_entity"}, ItemType.CONFIG_ENTRY: {wled_config_entry.entry_id}, ItemType.DEVICE: {wled_device.id}, ItemType.ENTITY: {wled_segment_1_entity.entity_id}, ItemType.FLOOR: {first_floor.floor_id}, + ItemType.GROUP: {"group.wled", "group.wled_hue"}, + ItemType.SCENE: {"scene.scene_wled_seg_1", scene_wled_hue_entity.entity_id}, + ItemType.SCRIPT: {"script.wled"}, } assert search(ItemType.AREA, kitchen_area.id) == { ItemType.AUTOMATION: {"automation.area"}, @@ -511,7 +514,9 @@ async def test_search( hue_segment_2_entity.entity_id, }, ItemType.FLOOR: {first_floor.floor_id}, - ItemType.SCRIPT: {"script.area", "script.device"}, + ItemType.GROUP: {"group.hue", "group.wled_hue"}, + ItemType.SCENE: {"scene.scene_hue_seg_1", scene_wled_hue_entity.entity_id}, + ItemType.SCRIPT: {"script.area", "script.device", "script.hue"}, } assert not search(ItemType.AUTOMATION, "automation.unknown") @@ -726,6 +731,7 @@ async def test_search( "automation.area", "automation.floor", "automation.wled_device", + "automation.wled_entity", }, ItemType.CONFIG_ENTRY: {hue_config_entry.entry_id, wled_config_entry.entry_id}, ItemType.DEVICE: {hue_device.id, wled_device.id}, @@ -734,7 +740,19 @@ async def test_search( hue_segment_1_entity.entity_id, hue_segment_2_entity.entity_id, }, - ItemType.SCRIPT: {"script.device", "script.area", "script.floor"}, + ItemType.GROUP: {"group.hue", "group.wled", "group.wled_hue"}, + ItemType.SCENE: { + "scene.scene_hue_seg_1", + "scene.scene_wled_seg_1", + scene_wled_hue_entity.entity_id, + }, + ItemType.SCRIPT: { + "script.device", + "script.area", + "script.floor", + "script.hue", + "script.wled", + }, } assert search(ItemType.FLOOR, second_floor.floor_id) == { ItemType.AREA: {bedroom_area.id}, From 6fafb9c9b433e000a7c649ecaa388f09da6331db Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Thu, 28 Mar 2024 11:09:15 -0500 Subject: [PATCH 048/967] Filter preferred TTS format options if not supported (#114392) Filter preferred format options if not supported --- homeassistant/components/tts/__init__.py | 74 ++++++++---- tests/components/assist_pipeline/conftest.py | 3 +- tests/components/assist_pipeline/test_init.py | 105 ++++++++++++++++-- 3 files changed, 150 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index c88e0e83334..8ea4617bbf3 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -16,7 +16,7 @@ import os import re import subprocess import tempfile -from typing import Any, TypedDict, final +from typing import Any, Final, TypedDict, final from aiohttp import web import mutagen @@ -99,6 +99,13 @@ ATTR_PREFERRED_SAMPLE_CHANNELS = "preferred_sample_channels" ATTR_MEDIA_PLAYER_ENTITY_ID = "media_player_entity_id" ATTR_VOICE = "voice" +_DEFAULT_FORMAT = "mp3" +_PREFFERED_FORMAT_OPTIONS: Final[set[str]] = { + ATTR_PREFERRED_FORMAT, + ATTR_PREFERRED_SAMPLE_RATE, + ATTR_PREFERRED_SAMPLE_CHANNELS, +} + CONF_LANG = "language" SERVICE_CLEAR_CACHE = "clear_cache" @@ -569,25 +576,23 @@ class SpeechManager: ): raise HomeAssistantError(f"Language '{language}' not supported") + options = options or {} + supported_options = engine_instance.supported_options or [] + # Update default options with provided options + invalid_opts: list[str] = [] merged_options = dict(engine_instance.default_options or {}) - merged_options.update(options or {}) + for option_name, option_value in options.items(): + # Only count an option as invalid if it's not a "preferred format" + # option. These are used as hints to the TTS system if supported, + # and otherwise as parameters to ffmpeg conversion. + if (option_name in supported_options) or ( + option_name in _PREFFERED_FORMAT_OPTIONS + ): + merged_options[option_name] = option_value + else: + invalid_opts.append(option_name) - supported_options = list(engine_instance.supported_options or []) - - # ATTR_PREFERRED_* options are always "supported" since they're used to - # convert audio after the TTS has run (if necessary). - supported_options.extend( - ( - ATTR_PREFERRED_FORMAT, - ATTR_PREFERRED_SAMPLE_RATE, - ATTR_PREFERRED_SAMPLE_CHANNELS, - ) - ) - - invalid_opts = [ - opt_name for opt_name in merged_options if opt_name not in supported_options - ] if invalid_opts: raise HomeAssistantError(f"Invalid options found: {invalid_opts}") @@ -687,10 +692,31 @@ class SpeechManager: This method is a coroutine. """ - options = options or {} + options = dict(options or {}) + supported_options = engine_instance.supported_options or [] - # Default to MP3 unless a different format is preferred - final_extension = options.get(ATTR_PREFERRED_FORMAT, "mp3") + # Extract preferred format options. + # + # These options are used by Assist pipelines, etc. to get a format that + # the voice satellite will support. + # + # The TTS system ideally supports options directly so we won't have + # to convert with ffmpeg later. If not, we pop the options here and + # perform the conversation after receiving the audio. + if ATTR_PREFERRED_FORMAT in supported_options: + final_extension = options.get(ATTR_PREFERRED_FORMAT, _DEFAULT_FORMAT) + else: + final_extension = options.pop(ATTR_PREFERRED_FORMAT, _DEFAULT_FORMAT) + + if ATTR_PREFERRED_SAMPLE_RATE in supported_options: + sample_rate = options.get(ATTR_PREFERRED_SAMPLE_RATE) + else: + sample_rate = options.pop(ATTR_PREFERRED_SAMPLE_RATE, None) + + if ATTR_PREFERRED_SAMPLE_CHANNELS in supported_options: + sample_channels = options.get(ATTR_PREFERRED_SAMPLE_CHANNELS) + else: + sample_channels = options.pop(ATTR_PREFERRED_SAMPLE_CHANNELS, None) async def get_tts_data() -> str: """Handle data available.""" @@ -716,8 +742,8 @@ class SpeechManager: # rate/format/channel count is requested. needs_conversion = ( (final_extension != extension) - or (ATTR_PREFERRED_SAMPLE_RATE in options) - or (ATTR_PREFERRED_SAMPLE_CHANNELS in options) + or (sample_rate is not None) + or (sample_channels is not None) ) if needs_conversion: @@ -726,8 +752,8 @@ class SpeechManager: extension, data, to_extension=final_extension, - to_sample_rate=options.get(ATTR_PREFERRED_SAMPLE_RATE), - to_sample_channels=options.get(ATTR_PREFERRED_SAMPLE_CHANNELS), + to_sample_rate=sample_rate, + to_sample_channels=sample_channels, ) # Create file infos diff --git a/tests/components/assist_pipeline/conftest.py b/tests/components/assist_pipeline/conftest.py index 8c5cfe9d599..9f098150288 100644 --- a/tests/components/assist_pipeline/conftest.py +++ b/tests/components/assist_pipeline/conftest.py @@ -111,6 +111,7 @@ class MockTTSProvider(tts.Provider): tts.Voice("fran_drescher", "Fran Drescher"), ] } + _supported_options = ["voice", "age", tts.ATTR_AUDIO_OUTPUT] @property def default_language(self) -> str: @@ -130,7 +131,7 @@ class MockTTSProvider(tts.Provider): @property def supported_options(self) -> list[str]: """Return list of supported options like voice, emotions.""" - return ["voice", "age", tts.ATTR_AUDIO_OUTPUT] + return self._supported_options def get_tts_audio( self, message: str, language: str, options: dict[str, Any] diff --git a/tests/components/assist_pipeline/test_init.py b/tests/components/assist_pipeline/test_init.py index 81347e96235..c6f45044cb3 100644 --- a/tests/components/assist_pipeline/test_init.py +++ b/tests/components/assist_pipeline/test_init.py @@ -11,7 +11,7 @@ import wave import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components import assist_pipeline, stt, tts +from homeassistant.components import assist_pipeline, media_source, stt, tts from homeassistant.components.assist_pipeline.const import ( CONF_DEBUG_RECORDING_DIR, DOMAIN, @@ -19,9 +19,14 @@ from homeassistant.components.assist_pipeline.const import ( from homeassistant.core import Context, HomeAssistant from homeassistant.setup import async_setup_component -from .conftest import MockSttProvider, MockSttProviderEntity, MockWakeWordEntity +from .conftest import ( + MockSttProvider, + MockSttProviderEntity, + MockTTSProvider, + MockWakeWordEntity, +) -from tests.typing import WebSocketGenerator +from tests.typing import ClientSessionGenerator, WebSocketGenerator BYTES_ONE_SECOND = 16000 * 2 @@ -729,15 +734,17 @@ def test_pipeline_run_equality(hass: HomeAssistant, init_components) -> None: async def test_tts_audio_output( hass: HomeAssistant, - mock_stt_provider: MockSttProvider, + hass_client: ClientSessionGenerator, + mock_tts_provider: MockTTSProvider, init_components, pipeline_data: assist_pipeline.pipeline.PipelineData, snapshot: SnapshotAssertion, ) -> None: """Test using tts_audio_output with wav sets options correctly.""" + client = await hass_client() + assert await async_setup_component(hass, media_source.DOMAIN, {}) - def event_callback(event): - pass + events: list[assist_pipeline.PipelineEvent] = [] pipeline_store = pipeline_data.pipeline_store pipeline_id = pipeline_store.async_get_preferred_item() @@ -753,7 +760,7 @@ async def test_tts_audio_output( pipeline=pipeline, start_stage=assist_pipeline.PipelineStage.TTS, end_stage=assist_pipeline.PipelineStage.TTS, - event_callback=event_callback, + event_callback=events.append, tts_audio_output="wav", ), ) @@ -764,3 +771,87 @@ async def test_tts_audio_output( assert pipeline_input.run.tts_options.get(tts.ATTR_PREFERRED_FORMAT) == "wav" assert pipeline_input.run.tts_options.get(tts.ATTR_PREFERRED_SAMPLE_RATE) == 16000 assert pipeline_input.run.tts_options.get(tts.ATTR_PREFERRED_SAMPLE_CHANNELS) == 1 + + with patch.object(mock_tts_provider, "get_tts_audio") as mock_get_tts_audio: + await pipeline_input.execute() + + for event in events: + if event.type == assist_pipeline.PipelineEventType.TTS_END: + # We must fetch the media URL to trigger the TTS + assert event.data + media_id = event.data["tts_output"]["media_id"] + resolved = await media_source.async_resolve_media(hass, media_id, None) + await client.get(resolved.url) + + # Ensure that no unsupported options were passed in + assert mock_get_tts_audio.called + options = mock_get_tts_audio.call_args_list[0].kwargs["options"] + extra_options = set(options).difference(mock_tts_provider.supported_options) + assert len(extra_options) == 0, extra_options + + +async def test_tts_supports_preferred_format( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + mock_tts_provider: MockTTSProvider, + init_components, + pipeline_data: assist_pipeline.pipeline.PipelineData, + snapshot: SnapshotAssertion, +) -> None: + """Test that preferred format options are given to the TTS system if supported.""" + client = await hass_client() + assert await async_setup_component(hass, media_source.DOMAIN, {}) + + events: list[assist_pipeline.PipelineEvent] = [] + + pipeline_store = pipeline_data.pipeline_store + pipeline_id = pipeline_store.async_get_preferred_item() + pipeline = assist_pipeline.pipeline.async_get_pipeline(hass, pipeline_id) + + pipeline_input = assist_pipeline.pipeline.PipelineInput( + tts_input="This is a test.", + conversation_id=None, + device_id=None, + run=assist_pipeline.pipeline.PipelineRun( + hass, + context=Context(), + pipeline=pipeline, + start_stage=assist_pipeline.PipelineStage.TTS, + end_stage=assist_pipeline.PipelineStage.TTS, + event_callback=events.append, + tts_audio_output="wav", + ), + ) + await pipeline_input.validate() + + # Make the TTS provider support preferred format options + supported_options = list(mock_tts_provider.supported_options or []) + supported_options.extend( + [ + tts.ATTR_PREFERRED_FORMAT, + tts.ATTR_PREFERRED_SAMPLE_RATE, + tts.ATTR_PREFERRED_SAMPLE_CHANNELS, + ] + ) + + with ( + patch.object(mock_tts_provider, "_supported_options", supported_options), + patch.object(mock_tts_provider, "get_tts_audio") as mock_get_tts_audio, + ): + await pipeline_input.execute() + + for event in events: + if event.type == assist_pipeline.PipelineEventType.TTS_END: + # We must fetch the media URL to trigger the TTS + assert event.data + media_id = event.data["tts_output"]["media_id"] + resolved = await media_source.async_resolve_media(hass, media_id, None) + await client.get(resolved.url) + + assert mock_get_tts_audio.called + options = mock_get_tts_audio.call_args_list[0].kwargs["options"] + + # We should have received preferred format options in get_tts_audio + assert tts.ATTR_PREFERRED_FORMAT in options + assert tts.ATTR_PREFERRED_SAMPLE_RATE in options + assert tts.ATTR_PREFERRED_SAMPLE_CHANNELS in options From 443bfee16d0fec0be6689155fd0628aa0891eb8e Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 28 Mar 2024 18:33:39 +0100 Subject: [PATCH 049/967] Replace partial annotations (#114177) --- homeassistant/components/mqtt/mixins.py | 18 +++++++++++++----- homeassistant/util/dt.py | 2 +- tests/components/mqtt/test_init.py | 7 ------- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 42ad807d2f1..aa0ca3f8585 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -30,7 +30,7 @@ from homeassistant.const import ( CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, ) -from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import ( config_validation as cv, device_registry as dr, @@ -278,8 +278,8 @@ def async_handle_schema_error( async def _async_discover( hass: HomeAssistant, domain: str, - setup: partial[CALLBACK_TYPE] | None, - async_setup: partial[Coroutine[Any, Any, None]] | None, + setup: Callable[[MQTTDiscoveryPayload], None] | None, + async_setup: Callable[[MQTTDiscoveryPayload], Coroutine[Any, Any, None]] | None, discovery_payload: MQTTDiscoveryPayload, ) -> None: """Discover and add an MQTT entity, automation or tag. @@ -314,10 +314,18 @@ async def _async_discover( raise +class _SetupNonEntityHelperCallbackProtocol(Protocol): # pragma: no cover + """Callback protocol for async_setup in async_setup_non_entity_entry_helper.""" + + async def __call__( + self, config: ConfigType, discovery_data: DiscoveryInfoType + ) -> None: ... + + async def async_setup_non_entity_entry_helper( hass: HomeAssistant, domain: str, - async_setup: partial[Coroutine[Any, Any, None]], + async_setup: _SetupNonEntityHelperCallbackProtocol, discovery_schema: vol.Schema, ) -> None: """Set up automation or tag creation dynamically through MQTT discovery.""" @@ -327,7 +335,7 @@ async def async_setup_non_entity_entry_helper( discovery_payload: MQTTDiscoveryPayload, ) -> None: """Set up an MQTT entity, automation or tag from discovery.""" - config: DiscoveryInfoType = discovery_schema(discovery_payload) + config: ConfigType = discovery_schema(discovery_payload) await async_setup(config, discovery_data=discovery_payload.discovery_data) mqtt_data.reload_dispatchers.append( diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index 39976cce5f7..e85a302f371 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -100,7 +100,7 @@ def get_time_zone(time_zone_str: str) -> dt.tzinfo | None: # We use a partial here since it is implemented in native code # and avoids the global lookup of UTC -utcnow: partial[dt.datetime] = partial(dt.datetime.now, UTC) +utcnow = partial(dt.datetime.now, UTC) utcnow.__doc__ = "Get now in UTC time." diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index a9f2ba4354b..3e444e8d4c8 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -3,7 +3,6 @@ import asyncio from copy import deepcopy from datetime import datetime, timedelta -from functools import partial import json import ssl from typing import Any, TypedDict @@ -84,12 +83,6 @@ class _DebugInfo(TypedDict): config: _DebugDeviceInfo -class RecordCallsPartial(partial[Any]): - """Wrapper class for partial.""" - - __name__ = "RecordCallPartialTest" - - @pytest.fixture(autouse=True) def mock_storage(hass_storage: dict[str, Any]) -> None: """Autouse hass_storage for the TestCase tests.""" From 5523cb6be8c3c72569feabb67f9af3e62f228676 Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Thu, 28 Mar 2024 18:45:07 +0100 Subject: [PATCH 050/967] Set ruff requires-version to 0.3.4 (#114388) --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 4309e107046..062b8aaf77a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -598,6 +598,9 @@ filterwarnings = [ "ignore:pkg_resources is deprecated as an API:DeprecationWarning:webrtcvad", ] +[tool.ruff] +required-version = ">=0.3.4" + [tool.ruff.lint] select = [ "B002", # Python does not support the unary prefix increment From 435781be456341c15fb1890d0dbe9bc5d3d3b170 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Thu, 28 Mar 2024 13:48:51 -0400 Subject: [PATCH 051/967] Bump pyunifiprotect to 5.1.2 (#114348) --- homeassistant/components/unifiprotect/config_flow.py | 3 ++- homeassistant/components/unifiprotect/manifest.json | 2 +- homeassistant/components/unifiprotect/utils.py | 3 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/unifiprotect/config_flow.py b/homeassistant/components/unifiprotect/config_flow.py index 555ddcb8d5e..19561a6003d 100644 --- a/homeassistant/components/unifiprotect/config_flow.py +++ b/homeassistant/components/unifiprotect/config_flow.py @@ -261,7 +261,8 @@ class ProtectFlowHandler(ConfigFlow, domain=DOMAIN): username=user_input[CONF_USERNAME], password=user_input[CONF_PASSWORD], verify_ssl=verify_ssl, - cache_dir=Path(self.hass.config.path(STORAGE_DIR, "unifiprotect_cache")), + cache_dir=Path(self.hass.config.path(STORAGE_DIR, "unifiprotect")), + config_dir=Path(self.hass.config.path(STORAGE_DIR, "unifiprotect")), ) errors = {} diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 7cfb0ddcc9e..a26fab2e80b 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -41,7 +41,7 @@ "iot_class": "local_push", "loggers": ["pyunifiprotect", "unifi_discovery"], "quality_scale": "platinum", - "requirements": ["pyunifiprotect==5.0.2", "unifi-discovery==1.1.8"], + "requirements": ["pyunifiprotect==5.1.2", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/homeassistant/components/unifiprotect/utils.py b/homeassistant/components/unifiprotect/utils.py index 58474e6a531..8199d729943 100644 --- a/homeassistant/components/unifiprotect/utils.py +++ b/homeassistant/components/unifiprotect/utils.py @@ -145,7 +145,8 @@ def async_create_api_client( override_connection_host=entry.options.get(CONF_OVERRIDE_CHOST, False), ignore_stats=not entry.options.get(CONF_ALL_UPDATES, False), ignore_unadopted=False, - cache_dir=Path(hass.config.path(STORAGE_DIR, "unifiprotect_cache")), + cache_dir=Path(hass.config.path(STORAGE_DIR, "unifiprotect")), + config_dir=Path(hass.config.path(STORAGE_DIR, "unifiprotect")), ) diff --git a/requirements_all.txt b/requirements_all.txt index 19b3fe975ff..625552555c7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2339,7 +2339,7 @@ pytrydan==0.4.0 pyudev==0.24.1 # homeassistant.components.unifiprotect -pyunifiprotect==5.0.2 +pyunifiprotect==5.1.2 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c8f3b80d4fb..12a106b2170 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1806,7 +1806,7 @@ pytrydan==0.4.0 pyudev==0.24.1 # homeassistant.components.unifiprotect -pyunifiprotect==5.0.2 +pyunifiprotect==5.1.2 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From 78c1efa7d4b3faaf6a2bf63d71a64095453e0c97 Mon Sep 17 00:00:00 2001 From: Scott K Logan Date: Thu, 28 Mar 2024 12:52:17 -0500 Subject: [PATCH 052/967] Bump aioraven to 0.5.3 (#114397) --- 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 ad161d32201..a2717f0e886 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.2"], + "requirements": ["aioraven==0.5.3"], "usb": [ { "vid": "0403", diff --git a/requirements_all.txt b/requirements_all.txt index 625552555c7..f02945f4cd6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -350,7 +350,7 @@ aiopyarr==23.4.0 aioqsw==0.3.5 # homeassistant.components.rainforest_raven -aioraven==0.5.2 +aioraven==0.5.3 # homeassistant.components.recollect_waste aiorecollect==2023.09.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 12a106b2170..a641d44ce51 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -323,7 +323,7 @@ aiopyarr==23.4.0 aioqsw==0.3.5 # homeassistant.components.rainforest_raven -aioraven==0.5.2 +aioraven==0.5.3 # homeassistant.components.recollect_waste aiorecollect==2023.09.0 From dbbc6914c43079d3c23da89a2d271b215af6f0bd Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 28 Mar 2024 20:38:12 +0100 Subject: [PATCH 053/967] Update frontend to 20240328.0 (#114396) --- 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 10917bb7f70..9e86436bd68 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==20240327.0"] + "requirements": ["home-assistant-frontend==20240328.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 9f6ee1cf56c..2386845a2ad 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -30,7 +30,7 @@ habluetooth==2.4.2 hass-nabucasa==0.79.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240327.0 +home-assistant-frontend==20240328.0 home-assistant-intents==2024.3.27 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index f02945f4cd6..eed1bbd05c7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1077,7 +1077,7 @@ hole==0.8.0 holidays==0.45 # homeassistant.components.frontend -home-assistant-frontend==20240327.0 +home-assistant-frontend==20240328.0 # homeassistant.components.conversation home-assistant-intents==2024.3.27 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a641d44ce51..414a791391f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -876,7 +876,7 @@ hole==0.8.0 holidays==0.45 # homeassistant.components.frontend -home-assistant-frontend==20240327.0 +home-assistant-frontend==20240328.0 # homeassistant.components.conversation home-assistant-intents==2024.3.27 From 3fd24989c60663a897aa011ca12ae42caf84b362 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Thu, 28 Mar 2024 21:08:25 +0100 Subject: [PATCH 054/967] Use `setup_test_component_platform` helper for text entity component tests instead of `hass.components` (#114400) --- tests/components/text/common.py | 45 ++++++++++ tests/components/text/test_init.py | 57 +++--------- .../custom_components/test/text.py | 86 ------------------- 3 files changed, 59 insertions(+), 129 deletions(-) create mode 100644 tests/components/text/common.py delete mode 100644 tests/testing_config/custom_components/test/text.py diff --git a/tests/components/text/common.py b/tests/components/text/common.py new file mode 100644 index 00000000000..ff989ebc26a --- /dev/null +++ b/tests/components/text/common.py @@ -0,0 +1,45 @@ +"""Common test helpers for the text entity component tests.""" + +from typing import Any + +from homeassistant.components.text import RestoreText, TextEntity + + +class MockTextEntity(TextEntity): + """Mock text class.""" + + def __init__( + self, native_value="test", native_min=None, native_max=None, pattern=None + ) -> None: + """Initialize mock text entity.""" + + self._attr_native_value = native_value + if native_min is not None: + self._attr_native_min = native_min + if native_max is not None: + self._attr_native_max = native_max + if pattern is not None: + self._attr_pattern = pattern + + def set_value(self, value: str) -> None: + """Change the selected option.""" + self._attr_native_value = value + + +class MockRestoreText(MockTextEntity, RestoreText): + """Mock RestoreText class.""" + + def __init__(self, name: str, **values: Any) -> None: + """Initialize the MockRestoreText.""" + super().__init__(**values) + + self._attr_name = name + + async def async_added_to_hass(self) -> None: + """Restore native_*.""" + await super().async_added_to_hass() + if (last_text_data := await self.async_get_last_text_data()) is None: + return + self._attr_native_max = last_text_data.native_max + self._attr_native_min = last_text_data.native_min + self._attr_native_value = last_text_data.native_value diff --git a/tests/components/text/test_init.py b/tests/components/text/test_init.py index 7b44903eec3..deacf029ced 100644 --- a/tests/components/text/test_init.py +++ b/tests/components/text/test_init.py @@ -12,7 +12,6 @@ from homeassistant.components.text import ( ATTR_VALUE, DOMAIN, SERVICE_SET_VALUE, - TextEntity, TextMode, _async_set_value, ) @@ -24,27 +23,9 @@ from homeassistant.setup import async_setup_component from tests.common import ( async_mock_restore_state_shutdown_restart, mock_restore_cache_with_extra_data, + setup_test_component_platform, ) - - -class MockTextEntity(TextEntity): - """Mock text device to use in tests.""" - - def __init__( - self, native_value="test", native_min=None, native_max=None, pattern=None - ): - """Initialize mock text entity.""" - self._attr_native_value = native_value - if native_min is not None: - self._attr_native_min = native_min - if native_max is not None: - self._attr_native_max = native_max - if pattern is not None: - self._attr_pattern = pattern - - async def async_set_value(self, value: str) -> None: - """Set the value of the text.""" - self._attr_native_value = value +from tests.components.text.common import MockRestoreText, MockTextEntity async def test_text_default(hass: HomeAssistant) -> None: @@ -126,21 +107,16 @@ RESTORE_DATA = { async def test_restore_number_save_state( hass: HomeAssistant, hass_storage: dict[str, Any], - enable_custom_integrations: None, ) -> None: """Test RestoreNumber.""" - platform = getattr(hass.components, "test.text") - platform.init(empty=True) - platform.ENTITIES.append( - platform.MockRestoreText( - name="Test", - native_max=5, - native_min=1, - native_value="Hello", - ) + entity0 = MockRestoreText( + name="Test", + native_max=5, + native_min=1, + native_value="Hello", ) + setup_test_component_platform(hass, DOMAIN, [entity0]) - entity0 = platform.ENTITIES[0] assert await async_setup_component(hass, "text", {"text": {"platform": "test"}}) await hass.async_block_till_done() @@ -167,7 +143,6 @@ async def test_restore_number_save_state( ) async def test_restore_number_restore_state( hass: HomeAssistant, - enable_custom_integrations: None, hass_storage: dict[str, Any], native_max, native_min, @@ -178,18 +153,14 @@ async def test_restore_number_restore_state( """Test RestoreNumber.""" mock_restore_cache_with_extra_data(hass, ((State("text.test", ""), extra_data),)) - platform = getattr(hass.components, "test.text") - platform.init(empty=True) - platform.ENTITIES.append( - platform.MockRestoreText( - native_max=native_max, - native_min=native_min, - name="Test", - native_value=None, - ) + entity0 = MockRestoreText( + native_max=native_max, + native_min=native_min, + name="Test", + native_value=None, ) + setup_test_component_platform(hass, DOMAIN, [entity0]) - entity0 = platform.ENTITIES[0] assert await async_setup_component(hass, "text", {"text": {"platform": "test"}}) await hass.async_block_till_done() diff --git a/tests/testing_config/custom_components/test/text.py b/tests/testing_config/custom_components/test/text.py deleted file mode 100644 index d3b048747bf..00000000000 --- a/tests/testing_config/custom_components/test/text.py +++ /dev/null @@ -1,86 +0,0 @@ -"""Provide a mock text platform. - -Call init before using it in your tests to ensure clean test data. -""" - -from homeassistant.components.text import RestoreText, TextEntity, TextMode - -from tests.common import MockEntity - -UNIQUE_TEXT = "unique_text" - -ENTITIES = [] - - -class MockTextEntity(MockEntity, TextEntity): - """Mock text class.""" - - @property - def native_max(self): - """Return the native native_max.""" - return self._handle("native_max") - - @property - def native_min(self): - """Return the native native_min.""" - return self._handle("native_min") - - @property - def mode(self): - """Return the mode.""" - return self._handle("mode") - - @property - def pattern(self): - """Return the pattern.""" - return self._handle("pattern") - - @property - def native_value(self): - """Return the native value of this text.""" - return self._handle("native_value") - - def set_native_value(self, value: str) -> None: - """Change the selected option.""" - self._values["native_value"] = value - - -class MockRestoreText(MockTextEntity, RestoreText): - """Mock RestoreText class.""" - - async def async_added_to_hass(self) -> None: - """Restore native_*.""" - await super().async_added_to_hass() - if (last_text_data := await self.async_get_last_text_data()) is None: - return - self._values["native_max"] = last_text_data.native_max - self._values["native_min"] = last_text_data.native_min - self._values["native_value"] = last_text_data.native_value - - -def init(empty=False): - """Initialize the platform with entities.""" - global ENTITIES - - ENTITIES = ( - [] - if empty - else [ - MockTextEntity( - name="test", - native_min=1, - native_max=5, - mode=TextMode.TEXT, - pattern=r"[A-Za-z0-9]", - unique_id=UNIQUE_TEXT, - native_value="Hello", - ), - ] - ) - - -async def async_setup_platform( - hass, config, async_add_entities_callback, discovery_info=None -): - """Return mock entities.""" - async_add_entities_callback(ENTITIES) From 7d0437808afe39d8aff63824bbe6ab89038c8cc9 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Thu, 28 Mar 2024 21:28:02 +0100 Subject: [PATCH 055/967] Migrate moon to use single_config_entry (#114404) --- homeassistant/components/moon/config_flow.py | 3 --- homeassistant/components/moon/manifest.json | 3 ++- homeassistant/components/moon/strings.json | 3 --- homeassistant/generated/integrations.json | 3 ++- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/moon/config_flow.py b/homeassistant/components/moon/config_flow.py index 1c424c866e4..d8aa082ee3a 100644 --- a/homeassistant/components/moon/config_flow.py +++ b/homeassistant/components/moon/config_flow.py @@ -18,9 +18,6 @@ class MoonConfigFlow(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - if user_input is not None: return self.async_create_entry(title=DEFAULT_NAME, data={}) diff --git a/homeassistant/components/moon/manifest.json b/homeassistant/components/moon/manifest.json index 6102b37fb13..519df85fc9c 100644 --- a/homeassistant/components/moon/manifest.json +++ b/homeassistant/components/moon/manifest.json @@ -6,5 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/moon", "integration_type": "service", "iot_class": "calculated", - "quality_scale": "internal" + "quality_scale": "internal", + "single_config_entry": true } diff --git a/homeassistant/components/moon/strings.json b/homeassistant/components/moon/strings.json index 22b430731e0..e0e2c9ea6f4 100644 --- a/homeassistant/components/moon/strings.json +++ b/homeassistant/components/moon/strings.json @@ -5,9 +5,6 @@ "user": { "description": "[%key:common::config_flow::description::confirm_setup%]" } - }, - "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" } }, "entity": { diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 53b885ea853..701b2084d7d 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -3720,7 +3720,8 @@ "moon": { "integration_type": "service", "config_flow": true, - "iot_class": "calculated" + "iot_class": "calculated", + "single_config_entry": true }, "mopeka": { "name": "Mopeka", From 4adbf7c73076571ebbfe6779591e84f5b820404a Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Thu, 28 Mar 2024 23:05:47 +0100 Subject: [PATCH 056/967] Migrate nina to use single_config_entry (#114408) --- homeassistant/components/nina/config_flow.py | 3 --- homeassistant/components/nina/manifest.json | 3 ++- homeassistant/components/nina/strings.json | 3 --- homeassistant/generated/integrations.json | 3 ++- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/nina/config_flow.py b/homeassistant/components/nina/config_flow.py index 07c3f6fe9a1..3b8b290d6c8 100644 --- a/homeassistant/components/nina/config_flow.py +++ b/homeassistant/components/nina/config_flow.py @@ -107,9 +107,6 @@ class NinaConfigFlow(ConfigFlow, domain=DOMAIN): """Handle the initial step.""" errors: dict[str, Any] = {} - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - if not self._all_region_codes_sorted: nina: Nina = Nina(async_get_clientsession(self.hass)) diff --git a/homeassistant/components/nina/manifest.json b/homeassistant/components/nina/manifest.json index 1bf670aedf0..53a54f26dcf 100644 --- a/homeassistant/components/nina/manifest.json +++ b/homeassistant/components/nina/manifest.json @@ -6,5 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/nina", "iot_class": "cloud_polling", "loggers": ["pynina"], - "requirements": ["PyNINA==0.3.3"] + "requirements": ["PyNINA==0.3.3"], + "single_config_entry": true } diff --git a/homeassistant/components/nina/strings.json b/homeassistant/components/nina/strings.json index 5e0393d024f..98ea88d8798 100644 --- a/homeassistant/components/nina/strings.json +++ b/homeassistant/components/nina/strings.json @@ -15,9 +15,6 @@ } } }, - "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" - }, "error": { "no_selection": "Please select at least one city/county", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 701b2084d7d..06b325f7999 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -3998,7 +3998,8 @@ "name": "NINA", "integration_type": "hub", "config_flow": true, - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "single_config_entry": true }, "nissan_leaf": { "name": "Nissan Leaf", From 282cbfc048a8bbd8663a5f36ead2ec92e17cbb4d Mon Sep 17 00:00:00 2001 From: Lennard Beers Date: Fri, 29 Mar 2024 02:20:56 +0100 Subject: [PATCH 057/967] Add eq3btsmart integration (#109291) Co-authored-by: Sid <27780930+autinerd@users.noreply.github.com> Co-authored-by: J. Nick Koston --- .coveragerc | 5 + .strict-typing | 1 + CODEOWNERS | 2 + homeassistant/brands/eq3.json | 2 +- .../components/eq3btsmart/__init__.py | 145 +++++++++ .../components/eq3btsmart/climate.py | 306 ++++++++++++++++++ .../components/eq3btsmart/config_flow.py | 96 ++++++ homeassistant/components/eq3btsmart/const.py | 73 +++++ homeassistant/components/eq3btsmart/entity.py | 19 ++ .../components/eq3btsmart/manifest.json | 27 ++ homeassistant/components/eq3btsmart/models.py | 35 ++ .../components/eq3btsmart/schemas.py | 15 + .../components/eq3btsmart/strings.json | 19 ++ homeassistant/generated/bluetooth.py | 15 + homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + mypy.ini | 10 + requirements_all.txt | 4 + requirements_test_all.txt | 4 + tests/components/eq3btsmart/__init__.py | 1 + tests/components/eq3btsmart/conftest.py | 41 +++ tests/components/eq3btsmart/const.py | 4 + .../components/eq3btsmart/test_config_flow.py | 135 ++++++++ 23 files changed, 965 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/eq3btsmart/__init__.py create mode 100644 homeassistant/components/eq3btsmart/climate.py create mode 100644 homeassistant/components/eq3btsmart/config_flow.py create mode 100644 homeassistant/components/eq3btsmart/const.py create mode 100644 homeassistant/components/eq3btsmart/entity.py create mode 100644 homeassistant/components/eq3btsmart/manifest.json create mode 100644 homeassistant/components/eq3btsmart/models.py create mode 100644 homeassistant/components/eq3btsmart/schemas.py create mode 100644 homeassistant/components/eq3btsmart/strings.json create mode 100644 tests/components/eq3btsmart/__init__.py create mode 100644 tests/components/eq3btsmart/conftest.py create mode 100644 tests/components/eq3btsmart/const.py create mode 100644 tests/components/eq3btsmart/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index b88db04035a..d51cc28c7fc 100644 --- a/.coveragerc +++ b/.coveragerc @@ -362,6 +362,11 @@ omit = homeassistant/components/epson/__init__.py homeassistant/components/epson/media_player.py homeassistant/components/epsonworkforce/sensor.py + homeassistant/components/eq3btsmart/__init__.py + homeassistant/components/eq3btsmart/climate.py + homeassistant/components/eq3btsmart/const.py + homeassistant/components/eq3btsmart/entity.py + homeassistant/components/eq3btsmart/models.py homeassistant/components/escea/__init__.py homeassistant/components/escea/climate.py homeassistant/components/escea/discovery.py diff --git a/.strict-typing b/.strict-typing index fb621d3e53a..39ff23a472e 100644 --- a/.strict-typing +++ b/.strict-typing @@ -170,6 +170,7 @@ homeassistant.components.energy.* homeassistant.components.energyzero.* homeassistant.components.enigma2.* homeassistant.components.enphase_envoy.* +homeassistant.components.eq3btsmart.* homeassistant.components.esphome.* homeassistant.components.event.* homeassistant.components.evil_genius_labs.* diff --git a/CODEOWNERS b/CODEOWNERS index 77d70fe5ede..81add403413 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -396,6 +396,8 @@ build.json @home-assistant/supervisor /homeassistant/components/epson/ @pszafer /tests/components/epson/ @pszafer /homeassistant/components/epsonworkforce/ @ThaStealth +/homeassistant/components/eq3btsmart/ @eulemitkeule @dbuezas +/tests/components/eq3btsmart/ @eulemitkeule @dbuezas /homeassistant/components/escea/ @lazdavila /tests/components/escea/ @lazdavila /homeassistant/components/esphome/ @OttoWinter @jesserockz @kbx81 @bdraco diff --git a/homeassistant/brands/eq3.json b/homeassistant/brands/eq3.json index f5b1c8aeb87..4cdfbb015f4 100644 --- a/homeassistant/brands/eq3.json +++ b/homeassistant/brands/eq3.json @@ -1,5 +1,5 @@ { "domain": "eq3", "name": "eQ-3", - "integrations": ["maxcube"] + "integrations": ["maxcube", "eq3btsmart"] } diff --git a/homeassistant/components/eq3btsmart/__init__.py b/homeassistant/components/eq3btsmart/__init__.py new file mode 100644 index 00000000000..f63e627ea7d --- /dev/null +++ b/homeassistant/components/eq3btsmart/__init__.py @@ -0,0 +1,145 @@ +"""Support for EQ3 devices.""" + +import asyncio +import logging +from typing import TYPE_CHECKING + +from eq3btsmart import Thermostat +from eq3btsmart.exceptions import Eq3Exception +from eq3btsmart.thermostat_config import ThermostatConfig + +from homeassistant.components import bluetooth +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.dispatcher import async_dispatcher_send + +from .const import DOMAIN, SIGNAL_THERMOSTAT_CONNECTED, SIGNAL_THERMOSTAT_DISCONNECTED +from .models import Eq3Config, Eq3ConfigEntryData + +PLATFORMS = [ + Platform.CLIMATE, +] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Handle config entry setup.""" + + mac_address: str | None = entry.unique_id + + if TYPE_CHECKING: + assert mac_address is not None + + eq3_config = Eq3Config( + mac_address=mac_address, + ) + + device = bluetooth.async_ble_device_from_address( + hass, mac_address.upper(), connectable=True + ) + + if device is None: + raise ConfigEntryNotReady( + f"[{eq3_config.mac_address}] Device could not be found" + ) + + thermostat = Thermostat( + thermostat_config=ThermostatConfig( + mac_address=mac_address, + ), + ble_device=device, + ) + + eq3_config_entry = Eq3ConfigEntryData(eq3_config=eq3_config, thermostat=thermostat) + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = eq3_config_entry + + entry.async_on_unload(entry.add_update_listener(update_listener)) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + entry.async_create_background_task( + hass, _async_run_thermostat(hass, entry), entry.entry_id + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Handle config entry unload.""" + + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + eq3_config_entry: Eq3ConfigEntryData = hass.data[DOMAIN].pop(entry.entry_id) + await eq3_config_entry.thermostat.async_disconnect() + + return unload_ok + + +async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle config entry update.""" + + await hass.config_entries.async_reload(entry.entry_id) + + +async def _async_run_thermostat(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Run the thermostat.""" + + eq3_config_entry: Eq3ConfigEntryData = hass.data[DOMAIN][entry.entry_id] + thermostat = eq3_config_entry.thermostat + mac_address = eq3_config_entry.eq3_config.mac_address + scan_interval = eq3_config_entry.eq3_config.scan_interval + + await _async_reconnect_thermostat(hass, entry) + + while True: + try: + await thermostat.async_get_status() + except Eq3Exception as e: + if not thermostat.is_connected: + _LOGGER.error( + "[%s] eQ-3 device disconnected", + mac_address, + ) + async_dispatcher_send( + hass, + f"{SIGNAL_THERMOSTAT_DISCONNECTED}_{mac_address}", + ) + await _async_reconnect_thermostat(hass, entry) + continue + + _LOGGER.error( + "[%s] Error updating eQ-3 device: %s", + mac_address, + e, + ) + + await asyncio.sleep(scan_interval) + + +async def _async_reconnect_thermostat(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Reconnect the thermostat.""" + + eq3_config_entry: Eq3ConfigEntryData = hass.data[DOMAIN][entry.entry_id] + thermostat = eq3_config_entry.thermostat + mac_address = eq3_config_entry.eq3_config.mac_address + scan_interval = eq3_config_entry.eq3_config.scan_interval + + while True: + try: + await thermostat.async_connect() + except Eq3Exception: + await asyncio.sleep(scan_interval) + continue + + _LOGGER.debug( + "[%s] eQ-3 device connected", + mac_address, + ) + + async_dispatcher_send( + hass, + f"{SIGNAL_THERMOSTAT_CONNECTED}_{mac_address}", + ) + + return diff --git a/homeassistant/components/eq3btsmart/climate.py b/homeassistant/components/eq3btsmart/climate.py new file mode 100644 index 00000000000..326655d4e59 --- /dev/null +++ b/homeassistant/components/eq3btsmart/climate.py @@ -0,0 +1,306 @@ +"""Platform for eQ-3 climate entities.""" + +import logging +from typing import Any + +from eq3btsmart import Thermostat +from eq3btsmart.const import EQ3BT_MAX_TEMP, EQ3BT_OFF_TEMP, Eq3Preset, OperationMode +from eq3btsmart.exceptions import Eq3Exception + +from homeassistant.components.climate import ( + ATTR_HVAC_MODE, + PRESET_NONE, + ClimateEntity, + ClimateEntityFeature, + HVACAction, + HVACMode, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_TEMPERATURE, PRECISION_HALVES, UnitOfTemperature +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers.device_registry import ( + CONNECTION_BLUETOOTH, + DeviceInfo, + async_get, + format_mac, +) +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util import slugify + +from .const import ( + DEVICE_MODEL, + DOMAIN, + EQ_TO_HA_HVAC, + HA_TO_EQ_HVAC, + MANUFACTURER, + SIGNAL_THERMOSTAT_CONNECTED, + SIGNAL_THERMOSTAT_DISCONNECTED, + CurrentTemperatureSelector, + Preset, + TargetTemperatureSelector, +) +from .entity import Eq3Entity +from .models import Eq3Config, Eq3ConfigEntryData + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Handle config entry setup.""" + + eq3_config_entry: Eq3ConfigEntryData = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities( + [Eq3Climate(eq3_config_entry.eq3_config, eq3_config_entry.thermostat)], + ) + + +class Eq3Climate(Eq3Entity, ClimateEntity): + """Climate entity to represent a eQ-3 thermostat.""" + + _attr_name = None + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ) + _attr_temperature_unit = UnitOfTemperature.CELSIUS + _attr_min_temp = EQ3BT_OFF_TEMP + _attr_max_temp = EQ3BT_MAX_TEMP + _attr_precision = PRECISION_HALVES + _attr_hvac_modes = list(HA_TO_EQ_HVAC.keys()) + _attr_preset_modes = list(Preset) + _attr_should_poll = False + _attr_available = False + _attr_hvac_mode: HVACMode | None = None + _attr_hvac_action: HVACAction | None = None + _attr_preset_mode: str | None = None + _target_temperature: float | None = None + + def __init__(self, eq3_config: Eq3Config, thermostat: Thermostat) -> None: + """Initialize the climate entity.""" + + super().__init__(eq3_config, thermostat) + self._attr_unique_id = format_mac(eq3_config.mac_address) + self._attr_device_info = DeviceInfo( + name=slugify(self._eq3_config.mac_address), + manufacturer=MANUFACTURER, + model=DEVICE_MODEL, + connections={(CONNECTION_BLUETOOTH, self._eq3_config.mac_address)}, + ) + + async def async_added_to_hass(self) -> None: + """Run when entity about to be added to hass.""" + + self._thermostat.register_update_callback(self._async_on_updated) + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{SIGNAL_THERMOSTAT_DISCONNECTED}_{self._eq3_config.mac_address}", + self._async_on_disconnected, + ) + ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{SIGNAL_THERMOSTAT_CONNECTED}_{self._eq3_config.mac_address}", + self._async_on_connected, + ) + ) + + async def async_will_remove_from_hass(self) -> None: + """Run when entity will be removed from hass.""" + + self._thermostat.unregister_update_callback(self._async_on_updated) + + @callback + def _async_on_disconnected(self) -> None: + self._attr_available = False + self.async_write_ha_state() + + @callback + def _async_on_connected(self) -> None: + self._attr_available = True + self.async_write_ha_state() + + @callback + def _async_on_updated(self) -> None: + """Handle updated data from the thermostat.""" + + if self._thermostat.status is not None: + self._async_on_status_updated() + + if self._thermostat.device_data is not None: + self._async_on_device_updated() + + self.async_write_ha_state() + + @callback + def _async_on_status_updated(self) -> None: + """Handle updated status from the thermostat.""" + + self._target_temperature = self._thermostat.status.target_temperature.value + self._attr_hvac_mode = EQ_TO_HA_HVAC[self._thermostat.status.operation_mode] + self._attr_current_temperature = self._get_current_temperature() + self._attr_target_temperature = self._get_target_temperature() + self._attr_preset_mode = self._get_current_preset_mode() + self._attr_hvac_action = self._get_current_hvac_action() + + @callback + def _async_on_device_updated(self) -> None: + """Handle updated device data from the thermostat.""" + + device_registry = async_get(self.hass) + if device := device_registry.async_get_device( + connections={(CONNECTION_BLUETOOTH, self._eq3_config.mac_address)}, + ): + device_registry.async_update_device( + device.id, + sw_version=self._thermostat.device_data.firmware_version, + serial_number=self._thermostat.device_data.device_serial.value, + ) + + def _get_current_temperature(self) -> float | None: + """Return the current temperature.""" + + match self._eq3_config.current_temp_selector: + case CurrentTemperatureSelector.NOTHING: + return None + case CurrentTemperatureSelector.VALVE: + if self._thermostat.status is None: + return None + + return float(self._thermostat.status.valve_temperature) + case CurrentTemperatureSelector.UI: + return self._target_temperature + case CurrentTemperatureSelector.DEVICE: + if self._thermostat.status is None: + return None + + return float(self._thermostat.status.target_temperature.value) + case CurrentTemperatureSelector.ENTITY: + state = self.hass.states.get(self._eq3_config.external_temp_sensor) + if state is not None: + try: + return float(state.state) + except ValueError: + pass + + return None + + def _get_target_temperature(self) -> float | None: + """Return the target temperature.""" + + match self._eq3_config.target_temp_selector: + case TargetTemperatureSelector.TARGET: + return self._target_temperature + case TargetTemperatureSelector.LAST_REPORTED: + if self._thermostat.status is None: + return None + + return float(self._thermostat.status.target_temperature.value) + + def _get_current_preset_mode(self) -> str: + """Return the current preset mode.""" + + if (status := self._thermostat.status) is None: + return PRESET_NONE + if status.is_window_open: + return Preset.WINDOW_OPEN + if status.is_boost: + return Preset.BOOST + if status.is_low_battery: + return Preset.LOW_BATTERY + if status.is_away: + return Preset.AWAY + if status.operation_mode is OperationMode.ON: + return Preset.OPEN + if status.presets is None: + return PRESET_NONE + if status.target_temperature == status.presets.eco_temperature: + return Preset.ECO + if status.target_temperature == status.presets.comfort_temperature: + return Preset.COMFORT + + return PRESET_NONE + + def _get_current_hvac_action(self) -> HVACAction: + """Return the current hvac action.""" + + if ( + self._thermostat.status is None + or self._thermostat.status.operation_mode is OperationMode.OFF + ): + return HVACAction.OFF + if self._thermostat.status.valve == 0: + return HVACAction.IDLE + return HVACAction.HEATING + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new target temperature.""" + + if ATTR_HVAC_MODE in kwargs: + mode: HVACMode | None + if (mode := kwargs.get(ATTR_HVAC_MODE)) is None: + return + + if mode is not HVACMode.OFF: + await self.async_set_hvac_mode(mode) + else: + raise ServiceValidationError( + f"[{self._eq3_config.mac_address}] Can't change HVAC mode to off while changing temperature", + ) + + temperature: float | None + if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: + return + + previous_temperature = self._target_temperature + self._target_temperature = temperature + + self.async_write_ha_state() + + try: + await self._thermostat.async_set_temperature(self._target_temperature) + except Eq3Exception: + _LOGGER.error( + "[%s] Failed setting temperature", self._eq3_config.mac_address + ) + self._target_temperature = previous_temperature + self.async_write_ha_state() + except ValueError as ex: + raise ServiceValidationError("Invalid temperature") from ex + + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set new target hvac mode.""" + + if hvac_mode is HVACMode.OFF: + await self.async_set_temperature(temperature=EQ3BT_OFF_TEMP) + + try: + await self._thermostat.async_set_mode(HA_TO_EQ_HVAC[hvac_mode]) + except Eq3Exception: + _LOGGER.error("[%s] Failed setting HVAC mode", self._eq3_config.mac_address) + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set new preset mode.""" + + match preset_mode: + case Preset.BOOST: + await self._thermostat.async_set_boost(True) + case Preset.AWAY: + await self._thermostat.async_set_away(True) + case Preset.ECO: + await self._thermostat.async_set_preset(Eq3Preset.ECO) + case Preset.COMFORT: + await self._thermostat.async_set_preset(Eq3Preset.COMFORT) + case Preset.OPEN: + await self._thermostat.async_set_mode(OperationMode.ON) diff --git a/homeassistant/components/eq3btsmart/config_flow.py b/homeassistant/components/eq3btsmart/config_flow.py new file mode 100644 index 00000000000..228127d7705 --- /dev/null +++ b/homeassistant/components/eq3btsmart/config_flow.py @@ -0,0 +1,96 @@ +"""Config flow for eQ-3 Bluetooth Smart thermostats.""" + +from typing import Any + +from homeassistant import config_entries +from homeassistant.components.bluetooth import BluetoothServiceInfoBleak +from homeassistant.config_entries import ConfigFlowResult +from homeassistant.const import CONF_MAC +from homeassistant.helpers.device_registry import format_mac +from homeassistant.util import slugify + +from .const import DOMAIN +from .schemas import SCHEMA_MAC + + +class EQ3ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Config flow for eQ-3 Bluetooth Smart thermostats.""" + + def __init__(self) -> None: + """Initialize the config flow.""" + + self.mac_address: str = "" + + 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 is None: + return self.async_show_form( + step_id="user", + data_schema=SCHEMA_MAC, + errors=errors, + ) + + mac_address = format_mac(user_input[CONF_MAC]) + + if not validate_mac(mac_address): + errors[CONF_MAC] = "invalid_mac_address" + return self.async_show_form( + step_id="user", + data_schema=SCHEMA_MAC, + errors=errors, + ) + + await self.async_set_unique_id(mac_address) + self._abort_if_unique_id_configured(updates=user_input) + + # We can not validate if this mac actually is an eQ-3 thermostat, + # since the thermostat probably is not advertising right now. + return self.async_create_entry(title=slugify(mac_address), data={}) + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfoBleak + ) -> ConfigFlowResult: + """Handle bluetooth discovery.""" + + self.mac_address = format_mac(discovery_info.address) + + await self.async_set_unique_id(self.mac_address) + self._abort_if_unique_id_configured() + + self.context.update({"title_placeholders": {CONF_MAC: self.mac_address}}) + + return await self.async_step_init() + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle flow start.""" + + if user_input is None: + return self.async_show_form( + step_id="init", + description_placeholders={CONF_MAC: self.mac_address}, + ) + + await self.async_set_unique_id(self.mac_address) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=slugify(self.mac_address), + data={}, + ) + + +def validate_mac(mac: str) -> bool: + """Return whether or not given value is a valid MAC address.""" + + return bool( + mac + and len(mac) == 17 + and mac.count(":") == 5 + and all(int(part, 16) < 256 for part in mac.split(":") if part) + ) diff --git a/homeassistant/components/eq3btsmart/const.py b/homeassistant/components/eq3btsmart/const.py new file mode 100644 index 00000000000..111c4d0eba4 --- /dev/null +++ b/homeassistant/components/eq3btsmart/const.py @@ -0,0 +1,73 @@ +"""Constants for EQ3 Bluetooth Smart Radiator Valves.""" + +from enum import Enum + +from eq3btsmart.const import OperationMode + +from homeassistant.components.climate import ( + PRESET_AWAY, + PRESET_BOOST, + PRESET_COMFORT, + PRESET_ECO, + PRESET_NONE, + HVACMode, +) + +DOMAIN = "eq3btsmart" + +MANUFACTURER = "eQ-3 AG" +DEVICE_MODEL = "CC-RT-BLE-EQ" + +GET_DEVICE_TIMEOUT = 5 # seconds + + +EQ_TO_HA_HVAC: dict[OperationMode, HVACMode] = { + OperationMode.OFF: HVACMode.OFF, + OperationMode.ON: HVACMode.HEAT, + OperationMode.AUTO: HVACMode.AUTO, + OperationMode.MANUAL: HVACMode.HEAT, +} + +HA_TO_EQ_HVAC = { + HVACMode.OFF: OperationMode.OFF, + HVACMode.AUTO: OperationMode.AUTO, + HVACMode.HEAT: OperationMode.MANUAL, +} + + +class Preset(str, Enum): + """Preset modes for the eQ-3 radiator valve.""" + + NONE = PRESET_NONE + ECO = PRESET_ECO + COMFORT = PRESET_COMFORT + BOOST = PRESET_BOOST + AWAY = PRESET_AWAY + OPEN = "Open" + LOW_BATTERY = "Low Battery" + WINDOW_OPEN = "Window" + + +class CurrentTemperatureSelector(str, Enum): + """Selector for current temperature.""" + + NOTHING = "NOTHING" + UI = "UI" + DEVICE = "DEVICE" + VALVE = "VALVE" + ENTITY = "ENTITY" + + +class TargetTemperatureSelector(str, Enum): + """Selector for target temperature.""" + + TARGET = "TARGET" + LAST_REPORTED = "LAST_REPORTED" + + +DEFAULT_CURRENT_TEMP_SELECTOR = CurrentTemperatureSelector.DEVICE +DEFAULT_TARGET_TEMP_SELECTOR = TargetTemperatureSelector.TARGET +DEFAULT_SCAN_INTERVAL = 10 # seconds + +SIGNAL_THERMOSTAT_DISCONNECTED = f"{DOMAIN}.thermostat_disconnected" +SIGNAL_THERMOSTAT_CONNECTED = f"{DOMAIN}.thermostat_connected" diff --git a/homeassistant/components/eq3btsmart/entity.py b/homeassistant/components/eq3btsmart/entity.py new file mode 100644 index 00000000000..e8c00d4e3cf --- /dev/null +++ b/homeassistant/components/eq3btsmart/entity.py @@ -0,0 +1,19 @@ +"""Base class for all eQ-3 entities.""" + +from eq3btsmart.thermostat import Thermostat + +from homeassistant.helpers.entity import Entity + +from .models import Eq3Config + + +class Eq3Entity(Entity): + """Base class for all eQ-3 entities.""" + + _attr_has_entity_name = True + + def __init__(self, eq3_config: Eq3Config, thermostat: Thermostat) -> None: + """Initialize the eq3 entity.""" + + self._eq3_config = eq3_config + self._thermostat = thermostat diff --git a/homeassistant/components/eq3btsmart/manifest.json b/homeassistant/components/eq3btsmart/manifest.json new file mode 100644 index 00000000000..6c4a59962ff --- /dev/null +++ b/homeassistant/components/eq3btsmart/manifest.json @@ -0,0 +1,27 @@ +{ + "domain": "eq3btsmart", + "name": "eQ-3 Bluetooth Smart Thermostats", + "bluetooth": [ + { + "local_name": "CC-RT-BLE", + "connectable": true + }, + { + "local_name": "CC-RT-M-BLE", + "connectable": true + }, + { + "local_name": "CC-RT-BLE-EQ", + "connectable": true + } + ], + "codeowners": ["@eulemitkeule", "@dbuezas"], + "config_flow": true, + "dependencies": ["bluetooth", "bluetooth_adapters"], + "documentation": "https://www.home-assistant.io/integrations/eq3btsmart", + "integration_type": "device", + "iot_class": "local_polling", + "loggers": ["eq3btsmart"], + "quality_scale": "silver", + "requirements": ["eq3btsmart==1.1.6", "bleak-esphome==1.0.0"] +} diff --git a/homeassistant/components/eq3btsmart/models.py b/homeassistant/components/eq3btsmart/models.py new file mode 100644 index 00000000000..8ea0955dbdd --- /dev/null +++ b/homeassistant/components/eq3btsmart/models.py @@ -0,0 +1,35 @@ +"""Models for eq3btsmart integration.""" + +from dataclasses import dataclass + +from eq3btsmart.const import DEFAULT_AWAY_HOURS, DEFAULT_AWAY_TEMP +from eq3btsmart.thermostat import Thermostat + +from .const import ( + DEFAULT_CURRENT_TEMP_SELECTOR, + DEFAULT_SCAN_INTERVAL, + DEFAULT_TARGET_TEMP_SELECTOR, + CurrentTemperatureSelector, + TargetTemperatureSelector, +) + + +@dataclass(slots=True) +class Eq3Config: + """Config for a single eQ-3 device.""" + + mac_address: str + current_temp_selector: CurrentTemperatureSelector = DEFAULT_CURRENT_TEMP_SELECTOR + target_temp_selector: TargetTemperatureSelector = DEFAULT_TARGET_TEMP_SELECTOR + external_temp_sensor: str = "" + scan_interval: int = DEFAULT_SCAN_INTERVAL + default_away_hours: float = DEFAULT_AWAY_HOURS + default_away_temperature: float = DEFAULT_AWAY_TEMP + + +@dataclass(slots=True) +class Eq3ConfigEntryData: + """Config entry for a single eQ-3 device.""" + + eq3_config: Eq3Config + thermostat: Thermostat diff --git a/homeassistant/components/eq3btsmart/schemas.py b/homeassistant/components/eq3btsmart/schemas.py new file mode 100644 index 00000000000..643bb4a02a6 --- /dev/null +++ b/homeassistant/components/eq3btsmart/schemas.py @@ -0,0 +1,15 @@ +"""Voluptuous schemas for eq3btsmart.""" + +from eq3btsmart.const import EQ3BT_MAX_TEMP, EQ3BT_MIN_TEMP +import voluptuous as vol + +from homeassistant.const import CONF_MAC +from homeassistant.helpers import config_validation as cv + +SCHEMA_TEMPERATURE = vol.Range(min=EQ3BT_MIN_TEMP, max=EQ3BT_MAX_TEMP) +SCHEMA_DEVICE = vol.Schema({vol.Required(CONF_MAC): cv.string}) +SCHEMA_MAC = vol.Schema( + { + vol.Required(CONF_MAC): str, + } +) diff --git a/homeassistant/components/eq3btsmart/strings.json b/homeassistant/components/eq3btsmart/strings.json new file mode 100644 index 00000000000..7477aab4cfb --- /dev/null +++ b/homeassistant/components/eq3btsmart/strings.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, + "flow_title": "eQ-3 Device [{mac}]", + "step": { + "user": { + "title": "Configure new eQ-3 device", + "data": { + "mac": "MAC address" + } + }, + "init": { + "title": "Configure new eQ-3 device" + } + } + } +} diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index cd8174bab1f..3c18c27057a 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -66,6 +66,21 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "domain": "dormakaba_dkey", "service_uuid": "e7a60001-6639-429f-94fd-86de8ea26897", }, + { + "connectable": True, + "domain": "eq3btsmart", + "local_name": "CC-RT-BLE", + }, + { + "connectable": True, + "domain": "eq3btsmart", + "local_name": "CC-RT-M-BLE", + }, + { + "connectable": True, + "domain": "eq3btsmart", + "local_name": "CC-RT-BLE-EQ", + }, { "domain": "eufylife_ble", "local_name": "eufy T9140", diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 8d46c8be240..283cdf1a0de 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -150,6 +150,7 @@ FLOWS = { "environment_canada", "epion", "epson", + "eq3btsmart", "escea", "esphome", "eufylife_ble", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 06b325f7999..7c068de51ba 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1660,6 +1660,12 @@ "config_flow": false, "iot_class": "local_polling", "name": "eQ-3 MAX!" + }, + "eq3btsmart": { + "integration_type": "device", + "config_flow": true, + "iot_class": "local_polling", + "name": "eQ-3 Bluetooth Smart Thermostats" } } }, diff --git a/mypy.ini b/mypy.ini index 81f6f553eb6..66af4c9c25a 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1461,6 +1461,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.eq3btsmart.*] +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.esphome.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index eed1bbd05c7..27040f835e2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -552,6 +552,7 @@ bimmer-connected[china]==0.14.6 # homeassistant.components.bizkaibus bizkaibus==0.1.1 +# homeassistant.components.eq3btsmart # homeassistant.components.esphome bleak-esphome==1.0.0 @@ -820,6 +821,9 @@ epson-projector==0.5.1 # homeassistant.components.epsonworkforce epsonprinter==0.0.9 +# homeassistant.components.eq3btsmart +eq3btsmart==1.1.6 + # homeassistant.components.esphome esphome-dashboard-api==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 414a791391f..254c8923f39 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -474,6 +474,7 @@ bellows==0.38.1 # homeassistant.components.bmw_connected_drive bimmer-connected[china]==0.14.6 +# homeassistant.components.eq3btsmart # homeassistant.components.esphome bleak-esphome==1.0.0 @@ -671,6 +672,9 @@ epion==0.0.3 # homeassistant.components.epson epson-projector==0.5.1 +# homeassistant.components.eq3btsmart +eq3btsmart==1.1.6 + # homeassistant.components.esphome esphome-dashboard-api==1.2.3 diff --git a/tests/components/eq3btsmart/__init__.py b/tests/components/eq3btsmart/__init__.py new file mode 100644 index 00000000000..2d5fa84a9b8 --- /dev/null +++ b/tests/components/eq3btsmart/__init__.py @@ -0,0 +1 @@ +"""Tests for the eq3btsmart component.""" diff --git a/tests/components/eq3btsmart/conftest.py b/tests/components/eq3btsmart/conftest.py new file mode 100644 index 00000000000..19e10d6b59c --- /dev/null +++ b/tests/components/eq3btsmart/conftest.py @@ -0,0 +1,41 @@ +"""Fixtures for eq3btsmart tests.""" + +from bleak.backends.scanner import AdvertisementData +import pytest + +from homeassistant.components.bluetooth import BluetoothServiceInfoBleak + +from .const import MAC + +from tests.components.bluetooth import generate_ble_device + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" + + +@pytest.fixture +def fake_service_info(): + """Return a BluetoothServiceInfoBleak for use in testing.""" + return BluetoothServiceInfoBleak( + name="CC-RT-BLE", + address=MAC, + rssi=0, + manufacturer_data={}, + service_data={}, + service_uuids=[], + source="local", + connectable=False, + time=0, + device=generate_ble_device(address=MAC, name="CC-RT-BLE", rssi=0), + advertisement=AdvertisementData( + local_name="CC-RT-BLE", + manufacturer_data={}, + service_data={}, + service_uuids=[], + rssi=0, + tx_power=-127, + platform_data=(), + ), + ) diff --git a/tests/components/eq3btsmart/const.py b/tests/components/eq3btsmart/const.py new file mode 100644 index 00000000000..71b6564965c --- /dev/null +++ b/tests/components/eq3btsmart/const.py @@ -0,0 +1,4 @@ +"""Constants for the eq3btsmart tests.""" + +MAC = "aa:bb:cc:dd:ee:ff" +RSSI = -60 diff --git a/tests/components/eq3btsmart/test_config_flow.py b/tests/components/eq3btsmart/test_config_flow.py new file mode 100644 index 00000000000..f9db434850a --- /dev/null +++ b/tests/components/eq3btsmart/test_config_flow.py @@ -0,0 +1,135 @@ +"""Test the eq3btsmart config flow.""" + +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.bluetooth import BluetoothServiceInfoBleak +from homeassistant.components.eq3btsmart.const import DOMAIN +from homeassistant.const import CONF_MAC +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.device_registry import format_mac +from homeassistant.util import slugify + +from .const import MAC + +from tests.common import MockConfigEntry + + +async def test_user_flow(hass: HomeAssistant) -> None: + """Test we can handle a regular successflow setup flow.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.eq3btsmart.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_MAC: MAC}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == slugify(MAC) + assert result["data"] == {} + assert result["context"]["unique_id"] == MAC + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_flow_invalid_mac(hass: HomeAssistant) -> None: + """Test we handle invalid mac address.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.eq3btsmart.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_MAC: "invalid"}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {CONF_MAC: "invalid_mac_address"} + assert len(mock_setup_entry.mock_calls) == 0 + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_MAC: MAC}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == slugify(MAC) + assert result["data"] == {} + assert result["context"]["unique_id"] == MAC + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_bluetooth_flow( + hass: HomeAssistant, fake_service_info: BluetoothServiceInfoBleak +) -> None: + """Test we can handle a bluetooth discovery flow.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=fake_service_info, + ) + + with patch( + "homeassistant.components.eq3btsmart.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == slugify(MAC) + assert result["data"] == {} + assert result["context"]["unique_id"] == MAC + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_duplicate_entry(hass: HomeAssistant) -> None: + """Test duplicate setup handling.""" + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_MAC: MAC, + }, + unique_id=format_mac(MAC), + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.eq3btsmart.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_MAC: MAC, + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert mock_setup_entry.call_count == 0 From 530552b4f5523539531a74cf0d11be13f54ae30f Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Fri, 29 Mar 2024 02:23:21 +0100 Subject: [PATCH 058/967] Use `mock_platform` for device_tracker entity component tests instead of `hass.components` (#114398) Co-authored-by: Martin Hjelmare --- tests/components/conftest.py | 22 ++++- .../device_sun_light_trigger/test_init.py | 23 +++-- tests/components/device_tracker/common.py | 99 +++++++++++++++++++ tests/components/device_tracker/test_init.py | 56 +++++------ .../custom_components/test/device_tracker.py | 99 ------------------- 5 files changed, 158 insertions(+), 141 deletions(-) delete mode 100644 tests/testing_config/custom_components/test/device_tracker.py diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 0831c566666..09e74142ad3 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -1,16 +1,18 @@ """Fixtures for component testing.""" -from collections.abc import Generator +from collections.abc import Callable, Generator from typing import TYPE_CHECKING, Any from unittest.mock import MagicMock, patch import pytest from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant from tests.common import MockToggleEntity if TYPE_CHECKING: + from tests.components.device_tracker.common import MockScanner from tests.components.light.common import MockLight from tests.components.sensor.common import MockSensor @@ -137,3 +139,21 @@ def mock_toggle_entities() -> list[MockToggleEntity]: from tests.components.switch.common import get_mock_toggle_entities return get_mock_toggle_entities() + + +@pytest.fixture +def mock_legacy_device_scanner() -> "MockScanner": + """Return mocked legacy device scanner entity.""" + from tests.components.device_tracker.common import MockScanner + + return 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 + + return mock_legacy_device_tracker_setup diff --git a/tests/components/device_sun_light_trigger/test_init.py b/tests/components/device_sun_light_trigger/test_init.py index 570708cec79..b373bd4401f 100644 --- a/tests/components/device_sun_light_trigger/test_init.py +++ b/tests/components/device_sun_light_trigger/test_init.py @@ -1,5 +1,6 @@ """The tests device sun light trigger component.""" +from collections.abc import Callable from datetime import datetime from unittest.mock import patch @@ -26,20 +27,24 @@ from homeassistant.core import CoreState, HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from tests.common import async_fire_time_changed +from tests.common import async_fire_time_changed, setup_test_component_platform +from tests.components.device_tracker.common import MockScanner +from tests.components.light.common import MockLight @pytest.fixture -async def scanner(hass, enable_custom_integrations): +async def scanner( + hass: HomeAssistant, + mock_light_entities: list[MockLight], + mock_legacy_device_scanner: MockScanner, + mock_legacy_device_tracker_setup: Callable[[HomeAssistant, MockScanner], None], +) -> None: """Initialize components.""" - scanner = await getattr(hass.components, "test.device_tracker").async_get_scanner( - None, None - ) + mock_legacy_device_tracker_setup(hass, mock_legacy_device_scanner) + mock_legacy_device_scanner.reset() + mock_legacy_device_scanner.come_home("DEV1") - scanner.reset() - scanner.come_home("DEV1") - - getattr(hass.components, "test.light").init() + setup_test_component_platform(hass, "light", mock_light_entities) with patch( "homeassistant.components.device_tracker.legacy.load_yaml_config_file", diff --git a/tests/components/device_tracker/common.py b/tests/components/device_tracker/common.py index 973eb7d8820..eb88f9dfefc 100644 --- a/tests/components/device_tracker/common.py +++ b/tests/components/device_tracker/common.py @@ -15,11 +15,16 @@ from homeassistant.components.device_tracker import ( ATTR_MAC, DOMAIN, SERVICE_SEE, + DeviceScanner, + ScannerEntity, + SourceType, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.typing import GPSType from homeassistant.loader import bind_hass +from tests.common import MockPlatform, mock_platform + @callback @bind_hass @@ -51,3 +56,97 @@ def async_see( if attributes: data[ATTR_ATTRIBUTES] = attributes hass.async_create_task(hass.services.async_call(DOMAIN, SERVICE_SEE, data)) + + +class MockScannerEntity(ScannerEntity): + """Test implementation of a ScannerEntity.""" + + def __init__(self): + """Init.""" + self.connected = False + self._hostname = "test.hostname.org" + self._ip_address = "0.0.0.0" + self._mac_address = "ad:de:ef:be:ed:fe" + + @property + def source_type(self): + """Return the source type, eg gps or router, of the device.""" + return SourceType.ROUTER + + @property + def battery_level(self): + """Return the battery level of the device. + + Percentage from 0-100. + """ + return 100 + + @property + def ip_address(self) -> str: + """Return the primary ip address of the device.""" + return self._ip_address + + @property + def mac_address(self) -> str: + """Return the mac address of the device.""" + return self._mac_address + + @property + def hostname(self) -> str: + """Return hostname of the device.""" + return self._hostname + + @property + def is_connected(self): + """Return true if the device is connected to the network.""" + return self.connected + + def set_connected(self): + """Set connected to True.""" + self.connected = True + self.async_write_ha_state() + + +class MockScanner(DeviceScanner): + """Mock device scanner.""" + + def __init__(self): + """Initialize the MockScanner.""" + self.devices_home = [] + + def come_home(self, device): + """Make a device come home.""" + self.devices_home.append(device) + + def leave_home(self, device): + """Make a device leave the house.""" + self.devices_home.remove(device) + + def reset(self): + """Reset which devices are home.""" + self.devices_home = [] + + def scan_devices(self): + """Return a list of fake devices.""" + return list(self.devices_home) + + def get_device_name(self, device): + """Return a name for a mock device. + + Return None for dev1 for testing. + """ + return None if device == "DEV1" else device.lower() + + +def mock_legacy_device_tracker_setup( + hass: HomeAssistant, legacy_device_scanner: MockScanner +) -> None: + """Mock legacy device tracker platform setup.""" + + async def _async_get_scanner(hass, config) -> MockScanner: + """Return the test scanner.""" + return legacy_device_scanner + + mocked_platform = MockPlatform() + mocked_platform.async_get_scanner = _async_get_scanner + mock_platform(hass, "test.device_tracker", mocked_platform) diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index 3b95fc9582c..b36ffdf14f6 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -31,6 +31,7 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from . import common +from .common import MockScanner, mock_legacy_device_tracker_setup from tests.common import ( assert_setup_component, @@ -58,6 +59,14 @@ def mock_yaml_devices(hass): os.remove(yaml_devices) +@pytest.fixture(autouse=True) +def _mock_legacy_device_tracker_setup( + hass: HomeAssistant, mock_legacy_device_scanner: MockScanner +) -> None: + """Mock legacy device tracker setup.""" + mock_legacy_device_tracker_setup(hass, mock_legacy_device_scanner) + + async def test_is_on(hass: HomeAssistant) -> None: """Test is_on method.""" entity_id = f"{const.DOMAIN}.test" @@ -99,9 +108,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, enable_custom_integrations: None -) -> None: +async def test_reading_yaml_config(hass: HomeAssistant, yaml_devices) -> None: """Test the rendering of the YAML configuration.""" dev_id = "test" device = legacy.Device( @@ -179,9 +186,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, enable_custom_integrations: None -) -> None: +async def test_setup_without_yaml_file(hass: HomeAssistant, yaml_devices) -> 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) @@ -280,13 +285,11 @@ async def test_discover_platform_missing_platform( async def test_update_stale( hass: HomeAssistant, mock_device_tracker_conf: list[legacy.Device], - enable_custom_integrations: None, + mock_legacy_device_scanner: MockScanner, ) -> None: """Test stalled update.""" - - scanner = getattr(hass.components, "test.device_tracker").SCANNER - scanner.reset() - scanner.come_home("DEV1") + mock_legacy_device_scanner.reset() + mock_legacy_device_scanner.come_home("DEV1") now = dt_util.utcnow() register_time = datetime(now.year + 1, 9, 15, 23, tzinfo=dt_util.UTC) @@ -313,7 +316,7 @@ async def test_update_stale( assert hass.states.get("device_tracker.dev1").state == STATE_HOME - scanner.leave_home("DEV1") + mock_legacy_device_scanner.leave_home("DEV1") with patch( "homeassistant.components.device_tracker.legacy.dt_util.utcnow", @@ -328,7 +331,6 @@ async def test_update_stale( async def test_entity_attributes( hass: HomeAssistant, mock_device_tracker_conf: list[legacy.Device], - enable_custom_integrations: None, ) -> None: """Test the entity attributes.""" devices = mock_device_tracker_conf @@ -362,9 +364,7 @@ async def test_entity_attributes( @patch("homeassistant.components.device_tracker.legacy.DeviceTracker.async_see") -async def test_see_service( - mock_see, hass: HomeAssistant, enable_custom_integrations: None -) -> None: +async def test_see_service(mock_see, hass: HomeAssistant) -> None: """Test the see service with a unicode dev_id and NO MAC.""" with assert_setup_component(1, device_tracker.DOMAIN): assert await async_setup_component(hass, device_tracker.DOMAIN, TEST_PLATFORM) @@ -395,7 +395,6 @@ async def test_see_service( async def test_see_service_guard_config_entry( hass: HomeAssistant, mock_device_tracker_conf: list[legacy.Device], - enable_custom_integrations: None, ) -> None: """Test the guard if the device is registered in the entity registry.""" mock_entry = Mock() @@ -416,7 +415,6 @@ async def test_see_service_guard_config_entry( async def test_new_device_event_fired( hass: HomeAssistant, mock_device_tracker_conf: list[legacy.Device], - enable_custom_integrations: None, ) -> None: """Test that the device tracker will fire an event.""" with assert_setup_component(1, device_tracker.DOMAIN): @@ -451,7 +449,6 @@ async def test_new_device_event_fired( async def test_duplicate_yaml_keys( hass: HomeAssistant, mock_device_tracker_conf: list[legacy.Device], - enable_custom_integrations: None, ) -> None: """Test that the device tracker will not generate invalid YAML.""" devices = mock_device_tracker_conf @@ -471,7 +468,6 @@ async def test_duplicate_yaml_keys( async def test_invalid_dev_id( hass: HomeAssistant, mock_device_tracker_conf: list[legacy.Device], - enable_custom_integrations: None, ) -> None: """Test that the device tracker will not allow invalid dev ids.""" devices = mock_device_tracker_conf @@ -485,9 +481,7 @@ async def test_invalid_dev_id( assert not devices -async def test_see_state( - hass: HomeAssistant, yaml_devices, enable_custom_integrations: None -) -> None: +async def test_see_state(hass: HomeAssistant, yaml_devices) -> 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() @@ -527,7 +521,7 @@ async def test_see_state( async def test_see_passive_zone_state( hass: HomeAssistant, mock_device_tracker_conf: list[legacy.Device], - enable_custom_integrations: None, + mock_legacy_device_scanner: MockScanner, ) -> None: """Test that the device tracker sets gps for passive trackers.""" now = dt_util.utcnow() @@ -547,9 +541,8 @@ async def test_see_passive_zone_state( await async_setup_component(hass, zone.DOMAIN, {"zone": zone_info}) await hass.async_block_till_done() - scanner = getattr(hass.components, "test.device_tracker").SCANNER - scanner.reset() - scanner.come_home("dev1") + mock_legacy_device_scanner.reset() + mock_legacy_device_scanner.come_home("dev1") with ( patch( @@ -581,7 +574,7 @@ async def test_see_passive_zone_state( assert attrs.get("gps_accuracy") == 0 assert attrs.get("source_type") == SourceType.ROUTER - scanner.leave_home("dev1") + mock_legacy_device_scanner.leave_home("dev1") with patch( "homeassistant.components.device_tracker.legacy.dt_util.utcnow", @@ -668,12 +661,11 @@ async def test_bad_platform(hass: HomeAssistant) -> None: async def test_adding_unknown_device_to_config( mock_device_tracker_conf: list[legacy.Device], hass: HomeAssistant, - enable_custom_integrations: None, + mock_legacy_device_scanner: MockScanner, ) -> None: """Test the adding of unknown devices to configuration file.""" - scanner = getattr(hass.components, "test.device_tracker").SCANNER - scanner.reset() - scanner.come_home("DEV1") + mock_legacy_device_scanner.reset() + mock_legacy_device_scanner.come_home("DEV1") await async_setup_component( hass, device_tracker.DOMAIN, {device_tracker.DOMAIN: {CONF_PLATFORM: "test"}} diff --git a/tests/testing_config/custom_components/test/device_tracker.py b/tests/testing_config/custom_components/test/device_tracker.py deleted file mode 100644 index 11eb366f2fc..00000000000 --- a/tests/testing_config/custom_components/test/device_tracker.py +++ /dev/null @@ -1,99 +0,0 @@ -"""Provide a mock device scanner.""" - -from homeassistant.components.device_tracker import DeviceScanner -from homeassistant.components.device_tracker.config_entry import ScannerEntity -from homeassistant.components.device_tracker.const import SourceType - - -async def async_get_scanner(hass, config): - """Return a mock scanner.""" - return SCANNER - - -class MockScannerEntity(ScannerEntity): - """Test implementation of a ScannerEntity.""" - - def __init__(self): - """Init.""" - self.connected = False - self._hostname = "test.hostname.org" - self._ip_address = "0.0.0.0" - self._mac_address = "ad:de:ef:be:ed:fe" - - @property - def source_type(self): - """Return the source type, eg gps or router, of the device.""" - return SourceType.ROUTER - - @property - def battery_level(self): - """Return the battery level of the device. - - Percentage from 0-100. - """ - return 100 - - @property - def ip_address(self) -> str: - """Return the primary ip address of the device.""" - return self._ip_address - - @property - def mac_address(self) -> str: - """Return the mac address of the device.""" - return self._mac_address - - @property - def hostname(self) -> str: - """Return hostname of the device.""" - return self._hostname - - @property - def is_connected(self): - """Return true if the device is connected to the network.""" - return self.connected - - def set_connected(self): - """Set connected to True.""" - self.connected = True - self.async_schedule_update_ha_state() - - -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up the config entry.""" - entity = MockScannerEntity() - async_add_entities([entity]) - - -class MockScanner(DeviceScanner): - """Mock device scanner.""" - - def __init__(self): - """Initialize the MockScanner.""" - self.devices_home = [] - - def come_home(self, device): - """Make a device come home.""" - self.devices_home.append(device) - - def leave_home(self, device): - """Make a device leave the house.""" - self.devices_home.remove(device) - - def reset(self): - """Reset which devices are home.""" - self.devices_home = [] - - def scan_devices(self): - """Return a list of fake devices.""" - return list(self.devices_home) - - def get_device_name(self, device): - """Return a name for a mock device. - - Return None for dev1 for testing. - """ - return None if device == "DEV1" else device.lower() - - -SCANNER = MockScanner() From a5b609f081fa925ff234f8c6ffcc904a4bcf725e Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Fri, 29 Mar 2024 07:20:36 +0100 Subject: [PATCH 059/967] Enable ruff TRY401 (#114395) * Enable ruff TRY401 * fix tests --- homeassistant/components/alexa/entities.py | 6 ++--- homeassistant/components/alexa/handlers.py | 4 +-- homeassistant/components/alexa/intent.py | 4 +-- .../bluetooth/passive_update_processor.py | 10 +++---- homeassistant/components/brunt/config_flow.py | 6 ++--- .../components/denonavr/media_player.py | 6 ++--- .../components/discord/config_flow.py | 4 +-- homeassistant/components/dlink/config_flow.py | 4 +-- .../components/esphome/config_flow.py | 4 +-- .../components/esphome/entry_data.py | 4 +-- .../components/faa_delays/config_flow.py | 4 +-- homeassistant/components/flipr/config_flow.py | 4 +-- .../frontier_silicon/config_flow.py | 8 +++--- homeassistant/components/google_cloud/tts.py | 4 +-- .../components/google_tasks/config_flow.py | 4 +-- .../components/google_translate/tts.py | 4 +-- .../here_travel_time/config_flow.py | 4 +-- .../components/homewizard/config_flow.py | 2 +- homeassistant/components/http/static.py | 2 +- homeassistant/components/ipma/config_flow.py | 4 +-- .../components/jellyfin/config_flow.py | 8 +++--- .../components/litterrobot/config_flow.py | 4 +-- .../components/logentries/__init__.py | 4 +-- homeassistant/components/mailgun/notify.py | 8 +++--- .../components/medcom_ble/config_flow.py | 5 ++-- .../components/minecraft_server/__init__.py | 5 ++-- homeassistant/components/mochad/__init__.py | 4 +-- homeassistant/components/msteams/notify.py | 4 +-- .../components/octoprint/config_flow.py | 8 +++--- .../components/osramlightify/light.py | 4 +-- .../components/panasonic_viera/__init__.py | 8 +++--- .../components/panasonic_viera/config_flow.py | 12 ++++----- .../components/permobil/coordinator.py | 3 +-- homeassistant/components/plex/config_flow.py | 4 +-- .../components/prosegur/config_flow.py | 4 +-- .../components/python_script/__init__.py | 2 +- .../recorder/auto_repairs/schema.py | 12 ++++----- homeassistant/components/recorder/core.py | 27 ++++++++----------- .../components/recorder/migration.py | 8 +++--- .../components/recorder/models/context.py | 8 +++--- homeassistant/components/recorder/util.py | 2 +- homeassistant/components/reolink/host.py | 10 +++---- homeassistant/components/rest/switch.py | 4 +-- .../components/roborock/config_flow.py | 16 +++++------ homeassistant/components/serial/sensor.py | 10 +++---- .../components/shell_command/__init__.py | 4 +-- homeassistant/components/sia/config_flow.py | 4 +-- homeassistant/components/slack/config_flow.py | 4 +-- .../tesla_wall_connector/config_flow.py | 4 +-- .../components/unifiprotect/__init__.py | 2 +- homeassistant/components/waqi/config_flow.py | 12 ++++----- .../components/websocket_api/commands.py | 4 +-- homeassistant/components/wemo/wemo_device.py | 2 +- homeassistant/components/wyoming/stt.py | 4 +-- homeassistant/components/wyoming/wake_word.py | 4 +-- homeassistant/core.py | 6 ++--- homeassistant/data_entry_flow.py | 4 +-- homeassistant/helpers/update_coordinator.py | 4 +-- pyproject.toml | 3 +-- tests/components/python_script/test_init.py | 2 +- tests/test_data_entry_flow.py | 2 +- 61 files changed, 159 insertions(+), 182 deletions(-) diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 61ab220c60c..240f676b5f3 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -384,10 +384,8 @@ def async_get_entities( try: alexa_entity = ENTITY_ADAPTERS[state.domain](hass, config, state) interfaces = list(alexa_entity.interfaces()) - except Exception as exc: # pylint: disable=broad-except - _LOGGER.exception( - "Unable to serialize %s for discovery: %s", state.entity_id, exc - ) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unable to serialize %s for discovery", state.entity_id) else: if not interfaces: continue diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 30c2fecccf8..c28b1923399 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -126,9 +126,9 @@ async def async_api_discovery( continue try: discovered_serialized_entity = alexa_entity.serialize_discovery() - except Exception as exc: # pylint: disable=broad-except + except Exception: # pylint: disable=broad-except _LOGGER.exception( - "Unable to serialize %s for discovery: %s", alexa_entity.entity_id, exc + "Unable to serialize %s for discovery", alexa_entity.entity_id ) else: discovery_endpoints.append(discovered_serialized_entity) diff --git a/homeassistant/components/alexa/intent.py b/homeassistant/components/alexa/intent.py index 8d266e4a634..fdf72ccce28 100644 --- a/homeassistant/components/alexa/intent.py +++ b/homeassistant/components/alexa/intent.py @@ -94,8 +94,8 @@ class AlexaIntentsView(http.HomeAssistantView): ) ) - except intent.IntentError as err: - _LOGGER.exception(str(err)) + except intent.IntentError: + _LOGGER.exception("Error handling intent") return self.json( intent_error_response(hass, message, "Error handling intent.") ) diff --git a/homeassistant/components/bluetooth/passive_update_processor.py b/homeassistant/components/bluetooth/passive_update_processor.py index b7a7a165f71..b3bf3adf93c 100644 --- a/homeassistant/components/bluetooth/passive_update_processor.py +++ b/homeassistant/components/bluetooth/passive_update_processor.py @@ -376,11 +376,9 @@ class PassiveBluetoothProcessorCoordinator( try: update = self._update_method(service_info) - except Exception as err: # pylint: disable=broad-except + except Exception: # pylint: disable=broad-except self.last_update_success = False - self.logger.exception( - "Unexpected error updating %s data: %s", self.name, err - ) + self.logger.exception("Unexpected error updating %s data", self.name) return if not self.last_update_success: @@ -588,10 +586,10 @@ class PassiveBluetoothDataProcessor(Generic[_T]): """Handle a Bluetooth event.""" try: new_data = self.update_method(update) - except Exception as err: # pylint: disable=broad-except + except Exception: # pylint: disable=broad-except self.last_update_success = False self.coordinator.logger.exception( - "Unexpected error updating %s data: %s", self.coordinator.name, err + "Unexpected error updating %s data", self.coordinator.name ) return diff --git a/homeassistant/components/brunt/config_flow.py b/homeassistant/components/brunt/config_flow.py index 789a5a48bd9..65886c3081c 100644 --- a/homeassistant/components/brunt/config_flow.py +++ b/homeassistant/components/brunt/config_flow.py @@ -38,13 +38,13 @@ async def validate_input(user_input: dict[str, Any]) -> dict[str, str] | None: _LOGGER.warning("Brunt Credentials are incorrect") errors = {"base": "invalid_auth"} else: - _LOGGER.exception("Unknown error when trying to login to Brunt: %s", exc) + _LOGGER.exception("Unknown error when trying to login to Brunt") errors = {"base": "unknown"} except ServerDisconnectedError: _LOGGER.warning("Cannot connect to Brunt") errors = {"base": "cannot_connect"} - except Exception as exc: # pylint: disable=broad-except - _LOGGER.exception("Unknown error when trying to login to Brunt: %s", exc) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unknown error when trying to login to Brunt") errors = {"base": "unknown"} finally: await bapi.async_close() diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index 2f9b96d9471..25e4cc0119c 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -232,12 +232,10 @@ def async_log_errors( func.__name__, err, ) - except DenonAvrError as err: + except DenonAvrError: available = False _LOGGER.exception( - "Error %s occurred in method %s for Denon AVR receiver", - err, - func.__name__, + "Error occurred in method %s for Denon AVR receiver", func.__name__ ) finally: if available and not self.available: diff --git a/homeassistant/components/discord/config_flow.py b/homeassistant/components/discord/config_flow.py index a2747c1d803..a25a86cab3a 100644 --- a/homeassistant/components/discord/config_flow.py +++ b/homeassistant/components/discord/config_flow.py @@ -89,8 +89,8 @@ async def _async_try_connect(token: str) -> tuple[str | None, nextcord.AppInfo | return "invalid_auth", None except (ClientConnectorError, nextcord.HTTPException, nextcord.NotFound): return "cannot_connect", None - except Exception as ex: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception: %s", ex) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") return "unknown", None await discord_bot.close() return None, info diff --git a/homeassistant/components/dlink/config_flow.py b/homeassistant/components/dlink/config_flow.py index 52937d26b7d..4613aeb9cef 100644 --- a/homeassistant/components/dlink/config_flow.py +++ b/homeassistant/components/dlink/config_flow.py @@ -121,8 +121,8 @@ class DLinkFlowHandler(ConfigFlow, domain=DOMAIN): user_input[CONF_USERNAME], user_input[CONF_USE_LEGACY_PROTOCOL], ) - except Exception as ex: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception: %s", ex) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") return "unknown" if not smartplug.authenticated and smartplug.use_legacy_protocol: return "cannot_connect" diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 5e166db7092..67e94121e1d 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -430,8 +430,8 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): except aiohttp.ClientError as err: _LOGGER.error("Error talking to the dashboard: %s", err) return False - except json.JSONDecodeError as err: - _LOGGER.exception("Error parsing response from dashboard: %s", err) + except json.JSONDecodeError: + _LOGGER.exception("Error parsing response from dashboard") return False self._noise_psk = noise_psk diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index da0dae52569..877c099deee 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -353,11 +353,11 @@ class RuntimeEntryData: if subscription := self.state_subscriptions.get(subscription_key): try: subscription() - except Exception as ex: # pylint: disable=broad-except + except Exception: # pylint: disable=broad-except # If we allow this exception to raise it will # make it all the way to data_received in aioesphomeapi # which will cause the connection to be closed. - _LOGGER.exception("Error while calling subscription: %s", ex) + _LOGGER.exception("Error while calling subscription") @callback def async_update_device_state(self) -> None: diff --git a/homeassistant/components/faa_delays/config_flow.py b/homeassistant/components/faa_delays/config_flow.py index 5a42c9f7602..935831c467d 100644 --- a/homeassistant/components/faa_delays/config_flow.py +++ b/homeassistant/components/faa_delays/config_flow.py @@ -43,8 +43,8 @@ class FAADelaysConfigFlow(ConfigFlow, domain=DOMAIN): _LOGGER.error("Error connecting to FAA API") errors["base"] = "cannot_connect" - except Exception as error: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception: %s", error) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" if not errors: diff --git a/homeassistant/components/flipr/config_flow.py b/homeassistant/components/flipr/config_flow.py index 9d177e4c2b6..fb7985b9602 100644 --- a/homeassistant/components/flipr/config_flow.py +++ b/homeassistant/components/flipr/config_flow.py @@ -44,9 +44,9 @@ class FliprConfigFlow(ConfigFlow, domain=DOMAIN): errors["base"] = "invalid_auth" except (Timeout, ConnectionError): errors["base"] = "cannot_connect" - except Exception as exception: # pylint: disable=broad-except + except Exception: # pylint: disable=broad-except errors["base"] = "unknown" - _LOGGER.exception(exception) + _LOGGER.exception("Unexpected exception") if not errors and not flipr_ids: # No flipr_id found. Tell the user with an error message. diff --git a/homeassistant/components/frontier_silicon/config_flow.py b/homeassistant/components/frontier_silicon/config_flow.py index a9c87cd9d4a..cf775b15138 100644 --- a/homeassistant/components/frontier_silicon/config_flow.py +++ b/homeassistant/components/frontier_silicon/config_flow.py @@ -74,8 +74,8 @@ class FrontierSiliconConfigFlow(ConfigFlow, domain=DOMAIN): self._webfsapi_url = await AFSAPI.get_webfsapi_endpoint(device_url) except FSConnectionError: errors["base"] = "cannot_connect" - except Exception as exception: # pylint: disable=broad-except - _LOGGER.exception(exception) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: return await self._async_step_device_config_if_needed() @@ -206,8 +206,8 @@ class FrontierSiliconConfigFlow(ConfigFlow, domain=DOMAIN): errors["base"] = "cannot_connect" except InvalidPinException: errors["base"] = "invalid_auth" - except Exception as exception: # pylint: disable=broad-except - _LOGGER.exception(exception) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: if self._reauth_entry: diff --git a/homeassistant/components/google_cloud/tts.py b/homeassistant/components/google_cloud/tts.py index 6f4751850aa..cd5c53b5fd7 100644 --- a/homeassistant/components/google_cloud/tts.py +++ b/homeassistant/components/google_cloud/tts.py @@ -295,7 +295,7 @@ class GoogleCloudTTSProvider(Provider): except TimeoutError as ex: _LOGGER.error("Timeout for Google Cloud TTS call: %s", ex) - except Exception as ex: # pylint: disable=broad-except - _LOGGER.exception("Error occurred during Google Cloud TTS call: %s", ex) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error occurred during Google Cloud TTS call") return None, None diff --git a/homeassistant/components/google_tasks/config_flow.py b/homeassistant/components/google_tasks/config_flow.py index 14cd89fcec7..a8e283b55c8 100644 --- a/homeassistant/components/google_tasks/config_flow.py +++ b/homeassistant/components/google_tasks/config_flow.py @@ -53,7 +53,7 @@ class OAuth2FlowHandler( reason="access_not_configured", description_placeholders={"message": error}, ) - except Exception as ex: # pylint: disable=broad-except - self.logger.exception("Unknown error occurred: %s", ex) + except Exception: # pylint: disable=broad-except + self.logger.exception("Unknown error occurred") return self.async_abort(reason="unknown") return self.async_create_entry(title=self.flow_impl.name, data=data) diff --git a/homeassistant/components/google_translate/tts.py b/homeassistant/components/google_translate/tts.py index df7130e09e0..c34713caef7 100644 --- a/homeassistant/components/google_translate/tts.py +++ b/homeassistant/components/google_translate/tts.py @@ -162,8 +162,8 @@ class GoogleProvider(Provider): try: tts.write_to_fp(mp3_data) - except gTTSError as exc: - _LOGGER.exception("Error during processing of TTS request %s", exc) + except gTTSError: + _LOGGER.exception("Error during processing of TTS request") return None, None return "mp3", mp3_data.getvalue() diff --git a/homeassistant/components/here_travel_time/config_flow.py b/homeassistant/components/here_travel_time/config_flow.py index d27ea577c29..36d5c1efe1e 100644 --- a/homeassistant/components/here_travel_time/config_flow.py +++ b/homeassistant/components/here_travel_time/config_flow.py @@ -124,8 +124,8 @@ class HERETravelTimeConfigFlow(ConfigFlow, domain=DOMAIN): await async_validate_api_key(user_input[CONF_API_KEY]) except HERERoutingUnauthorizedError: errors["base"] = "invalid_auth" - except (HERERoutingError, HERETransitError) as error: - _LOGGER.exception("Unexpected exception: %s", error) + except (HERERoutingError, HERETransitError): + _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" if not errors: self._config = user_input diff --git a/homeassistant/components/homewizard/config_flow.py b/homeassistant/components/homewizard/config_flow.py index 70ef47a4f03..06dbb9c8333 100644 --- a/homeassistant/components/homewizard/config_flow.py +++ b/homeassistant/components/homewizard/config_flow.py @@ -201,7 +201,7 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN): ) from ex except Exception as ex: - _LOGGER.exception(ex) + _LOGGER.exception("Unexpected exception") raise AbortFlow("unknown_error") from ex finally: diff --git a/homeassistant/components/http/static.py b/homeassistant/components/http/static.py index fd6cd742ce4..b7bb9d4f3a8 100644 --- a/homeassistant/components/http/static.py +++ b/homeassistant/components/http/static.py @@ -58,7 +58,7 @@ class CachingStaticResource(StaticResource): raise except Exception as error: # perm error or other kind! - request.app.logger.exception(error) + request.app.logger.exception("Unexpected exception") raise HTTPNotFound from error content_type: str | None = None diff --git a/homeassistant/components/ipma/config_flow.py b/homeassistant/components/ipma/config_flow.py index 36e70243c93..a0ecf1f582e 100644 --- a/homeassistant/components/ipma/config_flow.py +++ b/homeassistant/components/ipma/config_flow.py @@ -40,8 +40,8 @@ class IpmaFlowHandler(ConfigFlow, domain=DOMAIN): user_input[CONF_LATITUDE], user_input[CONF_LONGITUDE], ) - except IPMAException as err: - _LOGGER.exception(err) + except IPMAException: + _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: return self.async_create_entry(title=location.name, data=user_input) diff --git a/homeassistant/components/jellyfin/config_flow.py b/homeassistant/components/jellyfin/config_flow.py index c6e447d18e8..9a1e3d5985c 100644 --- a/homeassistant/components/jellyfin/config_flow.py +++ b/homeassistant/components/jellyfin/config_flow.py @@ -66,9 +66,9 @@ class JellyfinConfigFlow(ConfigFlow, domain=DOMAIN): errors["base"] = "cannot_connect" except InvalidAuth: errors["base"] = "invalid_auth" - except Exception as ex: # pylint: disable=broad-except + except Exception: # pylint: disable=broad-except errors["base"] = "unknown" - _LOGGER.exception(ex) + _LOGGER.exception("Unexpected exception") else: entry_title = user_input[CONF_URL] @@ -116,9 +116,9 @@ class JellyfinConfigFlow(ConfigFlow, domain=DOMAIN): errors["base"] = "cannot_connect" except InvalidAuth: errors["base"] = "invalid_auth" - except Exception as ex: # pylint: disable=broad-except + except Exception: # pylint: disable=broad-except errors["base"] = "unknown" - _LOGGER.exception(ex) + _LOGGER.exception("Unexpected exception") else: self.hass.config_entries.async_update_entry(self.entry, data=new_input) diff --git a/homeassistant/components/litterrobot/config_flow.py b/homeassistant/components/litterrobot/config_flow.py index 39a1646a8b7..aada2f6c9cb 100644 --- a/homeassistant/components/litterrobot/config_flow.py +++ b/homeassistant/components/litterrobot/config_flow.py @@ -94,7 +94,7 @@ class LitterRobotConfigFlow(ConfigFlow, domain=DOMAIN): return "invalid_auth" except LitterRobotException: return "cannot_connect" - except Exception as ex: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception: %s", ex) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") return "unknown" return "" diff --git a/homeassistant/components/logentries/__init__.py b/homeassistant/components/logentries/__init__.py index 6357510a07e..8ddf4a1a543 100644 --- a/homeassistant/components/logentries/__init__.py +++ b/homeassistant/components/logentries/__init__.py @@ -49,8 +49,8 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: try: payload = {"host": le_wh, "event": json_body} requests.post(le_wh, data=json.dumps(payload), timeout=10) - except requests.exceptions.RequestException as error: - _LOGGER.exception("Error sending to Logentries: %s", error) + except requests.exceptions.RequestException: + _LOGGER.exception("Error sending to Logentries") hass.bus.listen(EVENT_STATE_CHANGED, logentries_event_listener) diff --git a/homeassistant/components/mailgun/notify.py b/homeassistant/components/mailgun/notify.py index ed5b3a69135..39aea79d15e 100644 --- a/homeassistant/components/mailgun/notify.py +++ b/homeassistant/components/mailgun/notify.py @@ -86,8 +86,8 @@ class MailgunNotificationService(BaseNotificationService): except MailgunCredentialsError: _LOGGER.exception("Invalid credentials") return False - except MailgunDomainError as mailgun_error: - _LOGGER.exception(mailgun_error) + except MailgunDomainError: + _LOGGER.exception("Unexpected exception") return False return True @@ -110,5 +110,5 @@ class MailgunNotificationService(BaseNotificationService): files=files, ) _LOGGER.debug("Message sent: %s", resp) - except MailgunError as mailgun_error: - _LOGGER.exception("Failed to send message: %s", mailgun_error) + except MailgunError: + _LOGGER.exception("Failed to send message") diff --git a/homeassistant/components/medcom_ble/config_flow.py b/homeassistant/components/medcom_ble/config_flow.py index faf482ca1f9..a50a5876cc7 100644 --- a/homeassistant/components/medcom_ble/config_flow.py +++ b/homeassistant/components/medcom_ble/config_flow.py @@ -136,11 +136,10 @@ class InspectorBLEConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_abort(reason="cannot_connect") except AbortFlow: raise - except Exception as err: # pylint: disable=broad-except + except Exception: # pylint: disable=broad-except _LOGGER.exception( - "Error occurred reading information from %s: %s", + "Error occurred reading information from %s", self._discovery_info.address, - err, ) return self.async_abort(reason="unknown") _LOGGER.debug("Device connection successful, proceeding") diff --git a/homeassistant/components/minecraft_server/__init__.py b/homeassistant/components/minecraft_server/__init__.py index 5ec737c3f73..0a9eee6a0d5 100644 --- a/homeassistant/components/minecraft_server/__init__.py +++ b/homeassistant/components/minecraft_server/__init__.py @@ -121,10 +121,9 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> try: await api.async_initialize() - except MinecraftServerAddressError as error: + except MinecraftServerAddressError: _LOGGER.exception( - "Can't migrate configuration entry due to error while parsing server address, try again later: %s", - error, + "Can't migrate configuration entry due to error while parsing server address, try again later" ) return False diff --git a/homeassistant/components/mochad/__init__.py b/homeassistant/components/mochad/__init__.py index 36ebb74edc3..c8714c902a3 100644 --- a/homeassistant/components/mochad/__init__.py +++ b/homeassistant/components/mochad/__init__.py @@ -45,8 +45,8 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: try: mochad_controller = MochadCtrl(host, port) - except exceptions.ConfigurationError as err: - _LOGGER.exception(str(err)) + except exceptions.ConfigurationError: + _LOGGER.exception("Unexpected exception") return False def stop_mochad(event): diff --git a/homeassistant/components/msteams/notify.py b/homeassistant/components/msteams/notify.py index d6ebdf3e711..d1118ed7ab5 100644 --- a/homeassistant/components/msteams/notify.py +++ b/homeassistant/components/msteams/notify.py @@ -37,8 +37,8 @@ def get_service( try: return MSTeamsNotificationService(webhook_url) - except RuntimeError as err: - _LOGGER.exception("Error in creating a new Microsoft Teams message: %s", err) + except RuntimeError: + _LOGGER.exception("Error in creating a new Microsoft Teams message") return None diff --git a/homeassistant/components/octoprint/config_flow.py b/homeassistant/components/octoprint/config_flow.py index 6bd592c38bf..0e2fad21871 100644 --- a/homeassistant/components/octoprint/config_flow.py +++ b/homeassistant/components/octoprint/config_flow.py @@ -115,11 +115,11 @@ class OctoPrintConfigFlow(ConfigFlow, domain=DOMAIN): try: await self.api_key_task - except OctoprintException as err: - _LOGGER.exception("Failed to get an application key: %s", err) + except OctoprintException: + _LOGGER.exception("Failed to get an application key") return self.async_show_progress_done(next_step_id="auth_failed") - except Exception as err: # pylint: disable=broad-except - _LOGGER.exception("Failed to get an application key : %s", err) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Failed to get an application key") return self.async_show_progress_done(next_step_id="auth_failed") finally: self.api_key_task = None diff --git a/homeassistant/components/osramlightify/light.py b/homeassistant/components/osramlightify/light.py index 2f0ba1bccb4..50696530e8a 100644 --- a/homeassistant/components/osramlightify/light.py +++ b/homeassistant/components/osramlightify/light.py @@ -83,8 +83,8 @@ def setup_platform( host = config[CONF_HOST] try: bridge = Lightify(host, log_level=logging.NOTSET) - except OSError as err: - _LOGGER.exception("Error connecting to bridge: %s due to: %s", host, err) + except OSError: + _LOGGER.exception("Error connecting to bridge %s", host) return setup_bridge(bridge, add_entities, config) diff --git a/homeassistant/components/panasonic_viera/__init__.py b/homeassistant/components/panasonic_viera/__init__.py index 7d224c7126f..8ec825c5974 100644 --- a/homeassistant/components/panasonic_viera/__init__.py +++ b/homeassistant/components/panasonic_viera/__init__.py @@ -177,8 +177,8 @@ class Remote: self._control = None self.state = STATE_OFF self.available = self._on_action is not None - except Exception as err: # pylint: disable=broad-except - _LOGGER.exception("An unknown error occurred: %s", err) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("An unknown error occurred") self._control = None self.state = STATE_OFF self.available = self._on_action is not None @@ -265,7 +265,7 @@ class Remote: self.state = STATE_OFF self.available = self._on_action is not None await self.async_create_remote_control() - except Exception as err: # pylint: disable=broad-except - _LOGGER.exception("An unknown error occurred: %s", err) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("An unknown error occurred") self.state = STATE_OFF self.available = self._on_action is not None diff --git a/homeassistant/components/panasonic_viera/config_flow.py b/homeassistant/components/panasonic_viera/config_flow.py index c06de119244..65a830c9b1a 100644 --- a/homeassistant/components/panasonic_viera/config_flow.py +++ b/homeassistant/components/panasonic_viera/config_flow.py @@ -60,8 +60,8 @@ class PanasonicVieraConfigFlow(ConfigFlow, domain=DOMAIN): except (URLError, SOAPError, OSError) as err: _LOGGER.error("Could not establish remote connection: %s", err) errors["base"] = "cannot_connect" - except Exception as err: # pylint: disable=broad-except - _LOGGER.exception("An unknown error occurred: %s", err) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("An unknown error occurred") return self.async_abort(reason="unknown") if "base" not in errors: @@ -118,8 +118,8 @@ class PanasonicVieraConfigFlow(ConfigFlow, domain=DOMAIN): except (URLError, OSError) as err: _LOGGER.error("The remote connection was lost: %s", err) return self.async_abort(reason="cannot_connect") - except Exception as err: # pylint: disable=broad-except - _LOGGER.exception("Unknown error: %s", err) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unknown error") return self.async_abort(reason="unknown") if "base" not in errors: @@ -142,8 +142,8 @@ class PanasonicVieraConfigFlow(ConfigFlow, domain=DOMAIN): except (URLError, SOAPError, OSError) as err: _LOGGER.error("The remote connection was lost: %s", err) return self.async_abort(reason="cannot_connect") - except Exception as err: # pylint: disable=broad-except - _LOGGER.exception("Unknown error: %s", err) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unknown error") return self.async_abort(reason="unknown") return self.async_show_form( diff --git a/homeassistant/components/permobil/coordinator.py b/homeassistant/components/permobil/coordinator.py index f505e73fa23..6efde26d341 100644 --- a/homeassistant/components/permobil/coordinator.py +++ b/homeassistant/components/permobil/coordinator.py @@ -50,8 +50,7 @@ class MyPermobilCoordinator(DataUpdateCoordinator[MyPermobilData]): except MyPermobilAPIException as err: _LOGGER.exception( - "Error fetching data from MyPermobil API for account %s %s", + "Error fetching data from MyPermobil API for account %s", self.p_api.email, - err, ) raise UpdateFailed from err diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index 301716e14d5..dabde0b0490 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -216,8 +216,8 @@ class PlexFlowHandler(ConfigFlow, domain=DOMAIN): self.available_servers = available_servers.args[0] return await self.async_step_select_server() - except Exception as error: # pylint: disable=broad-except - _LOGGER.exception("Unknown error connecting to Plex server: %s", error) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unknown error connecting to Plex server") return self.async_abort(reason="unknown") if errors: diff --git a/homeassistant/components/prosegur/config_flow.py b/homeassistant/components/prosegur/config_flow.py index ca8e4db35cc..36d1ae9d10f 100644 --- a/homeassistant/components/prosegur/config_flow.py +++ b/homeassistant/components/prosegur/config_flow.py @@ -62,8 +62,8 @@ class ProsegurConfigFlow(ConfigFlow, domain=DOMAIN): errors["base"] = "cannot_connect" except InvalidAuth: errors["base"] = "invalid_auth" - except Exception as exception: # pylint: disable=broad-except - _LOGGER.exception(exception) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: self.user_input = user_input diff --git a/homeassistant/components/python_script/__init__.py b/homeassistant/components/python_script/__init__.py index e976ae5d1b0..89e9eb5a9eb 100644 --- a/homeassistant/components/python_script/__init__.py +++ b/homeassistant/components/python_script/__init__.py @@ -290,7 +290,7 @@ def execute(hass, filename, source, data=None, return_response=False): raise HomeAssistantError( f"Error executing script ({type(err).__name__}): {err}" ) from err - logger.exception("Error executing script: %s", err) + logger.exception("Error executing script") return None return restricted_globals["output"] diff --git a/homeassistant/components/recorder/auto_repairs/schema.py b/homeassistant/components/recorder/auto_repairs/schema.py index aa2fc1bb8cb..41be13312d0 100644 --- a/homeassistant/components/recorder/auto_repairs/schema.py +++ b/homeassistant/components/recorder/auto_repairs/schema.py @@ -55,8 +55,8 @@ def validate_table_schema_supports_utf8( schema_errors = _validate_table_schema_supports_utf8( instance, table_object, columns ) - except Exception as exc: # pylint: disable=broad-except - _LOGGER.exception("Error when validating DB schema: %s", exc) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error when validating DB schema") _log_schema_errors(table_object, schema_errors) return schema_errors @@ -76,8 +76,8 @@ def validate_table_schema_has_correct_collation( schema_errors = _validate_table_schema_has_correct_collation( instance, table_object ) - except Exception as exc: # pylint: disable=broad-except - _LOGGER.exception("Error when validating DB schema: %s", exc) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error when validating DB schema") _log_schema_errors(table_object, schema_errors) return schema_errors @@ -159,8 +159,8 @@ def validate_db_schema_precision( return schema_errors try: schema_errors = _validate_db_schema_precision(instance, table_object) - except Exception as exc: # pylint: disable=broad-except - _LOGGER.exception("Error when validating DB schema: %s", exc) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error when validating DB schema") _log_schema_errors(table_object, schema_errors) return schema_errors diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 0e404ce4da0..3268bae4d49 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -898,8 +898,8 @@ class Recorder(threading.Thread): _LOGGER.debug("Processing task: %s", task) try: self._process_one_task_or_event_or_recover(task) - except Exception as err: # pylint: disable=broad-except - _LOGGER.exception("Error while processing event %s: %s", task, err) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error while processing event %s", task) def _process_one_task_or_event_or_recover(self, task: RecorderTask | Event) -> None: """Process a task or event, reconnect, or recover a malformed database.""" @@ -921,11 +921,9 @@ class Recorder(threading.Thread): except exc.DatabaseError as err: if self._handle_database_error(err): return - _LOGGER.exception( - "Unhandled database error while processing task %s: %s", task, err - ) - except SQLAlchemyError as err: - _LOGGER.exception("SQLAlchemyError error processing task %s: %s", task, err) + _LOGGER.exception("Unhandled database error while processing task %s", task) + except SQLAlchemyError: + _LOGGER.exception("SQLAlchemyError error processing task %s", task) # Reset the session if an SQLAlchemyError (including DatabaseError) # happens to rollback and recover @@ -941,10 +939,9 @@ class Recorder(threading.Thread): return migration.initialize_database(self.get_session) except UnsupportedDialect: break - except Exception as err: # pylint: disable=broad-except + except Exception: # pylint: disable=broad-except _LOGGER.exception( - "Error during connection setup: %s (retrying in %s seconds)", - err, + "Error during connection setup: (retrying in %s seconds)", self.db_retry_wait, ) tries += 1 @@ -1262,10 +1259,8 @@ class Recorder(threading.Thread): try: self.event_session.rollback() self.event_session.close() - except SQLAlchemyError as err: - _LOGGER.exception( - "Error while rolling back and closing the event session: %s", err - ) + except SQLAlchemyError: + _LOGGER.exception("Error while rolling back and closing the event session") def _reopen_event_session(self) -> None: """Rollback the event session and reopen it after a failure.""" @@ -1473,8 +1468,8 @@ class Recorder(threading.Thread): self.recorder_runs_manager.end(self.event_session) try: self._commit_event_session_or_retry() - except Exception as err: # pylint: disable=broad-except - _LOGGER.exception("Error saving the event session during shutdown: %s", err) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error saving the event session during shutdown") self.event_session.close() self.recorder_runs_manager.clear() diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index fc2e6ec2b3f..0d882ed3b66 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -183,8 +183,8 @@ def get_schema_version(session_maker: Callable[[], Session]) -> int | None: try: with session_scope(session=session_maker(), read_only=True) as session: return _get_schema_version(session) - except Exception as err: # pylint: disable=broad-except - _LOGGER.exception("Error when determining DB schema version: %s", err) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error when determining DB schema version") return None @@ -1786,8 +1786,8 @@ def initialize_database(session_maker: Callable[[], Session]) -> bool: with session_scope(session=session_maker()) as session: return _initialize_database(session) - except Exception as err: # pylint: disable=broad-except - _LOGGER.exception("Error when initialise database: %s", err) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error when initialise database") return False diff --git a/homeassistant/components/recorder/models/context.py b/homeassistant/components/recorder/models/context.py index c09ee366b84..0d81bab879a 100644 --- a/homeassistant/components/recorder/models/context.py +++ b/homeassistant/components/recorder/models/context.py @@ -18,8 +18,8 @@ def ulid_to_bytes_or_none(ulid: str | None) -> bytes | None: return None try: return ulid_to_bytes(ulid) - except ValueError as ex: - _LOGGER.exception("Error converting ulid %s to bytes: %s", ulid, ex) + except ValueError: + _LOGGER.exception("Error converting ulid %s to bytes", ulid) return None @@ -29,8 +29,8 @@ def bytes_to_ulid_or_none(_bytes: bytes | None) -> str | None: return None try: return bytes_to_ulid(_bytes) - except ValueError as ex: - _LOGGER.exception("Error converting bytes %s to ulid: %s", _bytes, ex) + except ValueError: + _LOGGER.exception("Error converting bytes %s to ulid", _bytes) return None diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 770dc91353c..77467ec1171 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -143,7 +143,7 @@ def session_scope( need_rollback = True session.commit() except Exception as err: # pylint: disable=broad-except - _LOGGER.exception("Error executing query: %s", err) + _LOGGER.exception("Error executing query") if need_rollback: session.rollback() if not exception_filter or not exception_filter(err): diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index 73e6ddd6115..4f5487a6a04 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -547,9 +547,9 @@ class ReolinkHost: self._long_poll_error = True await asyncio.sleep(LONG_POLL_ERROR_COOLDOWN) continue - except Exception as ex: + except Exception: _LOGGER.exception( - "Unexpected exception while requesting ONVIF pull point: %s", ex + "Unexpected exception while requesting ONVIF pull point" ) await self._api.unsubscribe(sub_type=SubType.long_poll) raise @@ -652,11 +652,9 @@ class ReolinkHost: message = data.decode("utf-8") channels = await self._api.ONVIF_event_callback(message) - except Exception as ex: # pylint: disable=broad-except + except Exception: # pylint: disable=broad-except _LOGGER.exception( - "Error processing ONVIF event for Reolink %s: %s", - self._api.nvr_name, - ex, + "Error processing ONVIF event for Reolink %s", self._api.nvr_name ) return diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py index f0da6366cfc..99aadce6620 100644 --- a/homeassistant/components/rest/switch.py +++ b/homeassistant/components/rest/switch.py @@ -219,8 +219,8 @@ class RestSwitch(ManualTriggerEntity, SwitchEntity): req = await self.get_device_state(self.hass) except (TimeoutError, httpx.TimeoutException): _LOGGER.exception("Timed out while fetching data") - except httpx.RequestError as err: - _LOGGER.exception("Error while fetching data: %s", err) + except httpx.RequestError: + _LOGGER.exception("Error while fetching data") if req: self._process_manual_data(req.text) diff --git a/homeassistant/components/roborock/config_flow.py b/homeassistant/components/roborock/config_flow.py index ede9afc826d..5715aba3bba 100644 --- a/homeassistant/components/roborock/config_flow.py +++ b/homeassistant/components/roborock/config_flow.py @@ -69,11 +69,11 @@ class RoborockFlowHandler(ConfigFlow, domain=DOMAIN): errors["base"] = "unknown_url" except RoborockInvalidEmail: errors["base"] = "invalid_email_format" - except RoborockException as ex: - _LOGGER.exception(ex) + except RoborockException: + _LOGGER.exception("Unexpected exception") errors["base"] = "unknown_roborock" - except Exception as ex: # pylint: disable=broad-except - _LOGGER.exception(ex) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" return errors @@ -92,11 +92,11 @@ class RoborockFlowHandler(ConfigFlow, domain=DOMAIN): login_data = await self._client.code_login(code) except RoborockInvalidCode: errors["base"] = "invalid_code" - except RoborockException as ex: - _LOGGER.exception(ex) + except RoborockException: + _LOGGER.exception("Unexpected exception") errors["base"] = "unknown_roborock" - except Exception as ex: # pylint: disable=broad-except - _LOGGER.exception(ex) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: if self.reauth_entry is not None: diff --git a/homeassistant/components/serial/sensor.py b/homeassistant/components/serial/sensor.py index 7f40279df85..5f2b1ea3c3c 100644 --- a/homeassistant/components/serial/sensor.py +++ b/homeassistant/components/serial/sensor.py @@ -187,12 +187,10 @@ class SerialSensor(SensorEntity): **kwargs, ) - except SerialException as exc: + except SerialException: if not logged_error: _LOGGER.exception( - "Unable to connect to the serial device %s: %s. Will retry", - device, - exc, + "Unable to connect to the serial device %s. Will retry", device ) logged_error = True await self._handle_error() @@ -201,9 +199,9 @@ class SerialSensor(SensorEntity): while True: try: line = await reader.readline() - except SerialException as exc: + except SerialException: _LOGGER.exception( - "Error while reading serial device %s: %s", device, exc + "Error while reading serial device %s", device ) await self._handle_error() break diff --git a/homeassistant/components/shell_command/__init__.py b/homeassistant/components/shell_command/__init__.py index 91cb48e9988..736654fc399 100644 --- a/homeassistant/components/shell_command/__init__.py +++ b/homeassistant/components/shell_command/__init__.py @@ -58,8 +58,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: rendered_args = args_compiled.async_render( variables=service.data, parse_result=False ) - except TemplateError as ex: - _LOGGER.exception("Error rendering command template: %s", ex) + except TemplateError: + _LOGGER.exception("Error rendering command template") raise else: rendered_args = None diff --git a/homeassistant/components/sia/config_flow.py b/homeassistant/components/sia/config_flow.py index f7a7a1f06e4..4329154b069 100644 --- a/homeassistant/components/sia/config_flow.py +++ b/homeassistant/components/sia/config_flow.py @@ -77,8 +77,8 @@ def validate_input(data: dict[str, Any]) -> dict[str, str] | None: return {"base": "invalid_account_format"} except InvalidAccountLengthError: return {"base": "invalid_account_length"} - except Exception as exc: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception from SIAAccount: %s", exc) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception from SIAAccount") return {"base": "unknown"} if not 1 <= data[CONF_PING_INTERVAL] <= 1440: return {"base": "invalid_ping"} diff --git a/homeassistant/components/slack/config_flow.py b/homeassistant/components/slack/config_flow.py index b23dc60da60..03f3683e5a9 100644 --- a/homeassistant/components/slack/config_flow.py +++ b/homeassistant/components/slack/config_flow.py @@ -68,7 +68,7 @@ class SlackFlowHandler(ConfigFlow, domain=DOMAIN): if ex.response["error"] == "invalid_auth": return "invalid_auth", None return "cannot_connect", None - except Exception as ex: # pylint:disable=broad-except - _LOGGER.exception("Unexpected exception: %s", ex) + except Exception: # pylint:disable=broad-except + _LOGGER.exception("Unexpected exception") return "unknown", None return None, info diff --git a/homeassistant/components/tesla_wall_connector/config_flow.py b/homeassistant/components/tesla_wall_connector/config_flow.py index b00dd8f2b9d..44848cb1dfe 100644 --- a/homeassistant/components/tesla_wall_connector/config_flow.py +++ b/homeassistant/components/tesla_wall_connector/config_flow.py @@ -104,8 +104,8 @@ class TeslaWallConnectorConfigFlow(ConfigFlow, domain=DOMAIN): info = await validate_input(self.hass, user_input) except WallConnectorError: errors["base"] = "cannot_connect" - except Exception as ex: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception: %s", ex) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" if not errors: diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index 71c887cd870..5943cc4f85e 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -154,7 +154,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: }, ) ir.async_delete_issue(hass, DOMAIN, "ea_channel_warning") - _LOGGER.exception("Error setting up UniFi Protect integration: %s", err) + _LOGGER.exception("Error setting up UniFi Protect integration") raise return True diff --git a/homeassistant/components/waqi/config_flow.py b/homeassistant/components/waqi/config_flow.py index 068cb1a5020..e7e7a536654 100644 --- a/homeassistant/components/waqi/config_flow.py +++ b/homeassistant/components/waqi/config_flow.py @@ -45,8 +45,8 @@ async def get_by_station_number( measuring_station = await client.get_by_station_number(station_number) except WAQIConnectionError: errors["base"] = "cannot_connect" - except Exception as exc: # pylint: disable=broad-except - _LOGGER.exception(exc) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" return measuring_station, errors @@ -76,8 +76,8 @@ class WAQIConfigFlow(ConfigFlow, domain=DOMAIN): errors["base"] = "invalid_auth" except WAQIConnectionError: errors["base"] = "cannot_connect" - except Exception as exc: # pylint: disable=broad-except - _LOGGER.exception(exc) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: self.data = user_input @@ -118,8 +118,8 @@ class WAQIConfigFlow(ConfigFlow, domain=DOMAIN): ) except WAQIConnectionError: errors["base"] = "cannot_connect" - except Exception as exc: # pylint: disable=broad-except - _LOGGER.exception(exc) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: return await self._async_create_entry(measuring_station) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 191ea1ea996..11210fcfcbc 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -289,7 +289,7 @@ async def handle_call_service( translation_placeholders=err.translation_placeholders, ) except HomeAssistantError as err: - connection.logger.exception(err) + connection.logger.exception("Unexpected exception") connection.send_error( msg["id"], const.ERR_HOME_ASSISTANT_ERROR, @@ -299,7 +299,7 @@ async def handle_call_service( translation_placeholders=err.translation_placeholders, ) except Exception as err: # pylint: disable=broad-except - connection.logger.exception(err) + connection.logger.exception("Unexpected exception") connection.send_error(msg["id"], const.ERR_UNKNOWN_ERROR, str(err)) diff --git a/homeassistant/components/wemo/wemo_device.py b/homeassistant/components/wemo/wemo_device.py index 2a4185a7640..148646736bc 100644 --- a/homeassistant/components/wemo/wemo_device.py +++ b/homeassistant/components/wemo/wemo_device.py @@ -205,7 +205,7 @@ class DeviceCoordinator(DataUpdateCoordinator[None]): # pylint: disable=hass-en except Exception as err: # pylint: disable=broad-except self.last_exception = err self.last_update_success = False - _LOGGER.exception("Unexpected error fetching %s data: %s", self.name, err) + _LOGGER.exception("Unexpected error fetching %s data", self.name) else: self.async_set_updated_data(None) diff --git a/homeassistant/components/wyoming/stt.py b/homeassistant/components/wyoming/stt.py index 227fa3a0eca..a28e5fdb527 100644 --- a/homeassistant/components/wyoming/stt.py +++ b/homeassistant/components/wyoming/stt.py @@ -126,8 +126,8 @@ class WyomingSttProvider(stt.SpeechToTextEntity): text = transcript.text break - except (OSError, WyomingError) as err: - _LOGGER.exception("Error processing audio stream: %s", err) + except (OSError, WyomingError): + _LOGGER.exception("Error processing audio stream") return stt.SpeechResult(None, stt.SpeechResultState.ERROR) return stt.SpeechResult( diff --git a/homeassistant/components/wyoming/wake_word.py b/homeassistant/components/wyoming/wake_word.py index 303a87e99bd..6eba0f7ca6d 100644 --- a/homeassistant/components/wyoming/wake_word.py +++ b/homeassistant/components/wyoming/wake_word.py @@ -187,8 +187,8 @@ class WyomingWakeWordProvider(wake_word.WakeWordDetectionEntity): for task in pending: task.cancel() - except (OSError, WyomingError) as err: - _LOGGER.exception("Error processing audio stream: %s", err) + except (OSError, WyomingError): + _LOGGER.exception("Error processing audio stream") return None diff --git a/homeassistant/core.py b/homeassistant/core.py index 3b52b020957..76a3f55527c 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1101,10 +1101,8 @@ class HomeAssistant: _LOGGER.exception( "Task %s could not be canceled during final shutdown stage", task ) - except Exception as exc: # pylint: disable=broad-except - _LOGGER.exception( - "Task %s error during final shutdown stage: %s", task, exc - ) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Task %s error during final shutdown stage", task) # Prevent run_callback_threadsafe from scheduling any additional # callbacks in the event loop as callbacks created on the futures diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 6a1453c9ff3..649c9fdf8a4 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -489,8 +489,8 @@ class FlowManager(abc.ABC, Generic[_FlowResultT, _HandlerT]): flow.async_cancel_progress_task() try: flow.async_remove() - except Exception as err: # pylint: disable=broad-except - _LOGGER.exception("Error removing %s flow: %s", flow.handler, err) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error removing %s flow", flow.handler) async def _async_handle_step( self, diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index c52be9982c5..0d7365c25bd 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -383,9 +383,7 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]): except Exception as err: # pylint: disable=broad-except self.last_exception = err self.last_update_success = False - self.logger.exception( - "Unexpected error fetching %s data: %s", self.name, err - ) + self.logger.exception("Unexpected error fetching %s data", self.name) else: if not self.last_update_success: diff --git a/pyproject.toml b/pyproject.toml index 062b8aaf77a..891ea511ba5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -715,8 +715,7 @@ ignore = [ "PT019", "TRY002", "TRY301", - "TRY300", - "TRY401" + "TRY300" ] [tool.ruff.lint.flake8-import-conventions.extend-aliases] diff --git a/tests/components/python_script/test_init.py b/tests/components/python_script/test_init.py index bec94db71f9..8e317e2163e 100644 --- a/tests/components/python_script/test_init.py +++ b/tests/components/python_script/test_init.py @@ -143,7 +143,7 @@ raise Exception('boom') hass.async_add_executor_job(execute, hass, "test.py", source, {}) await hass.async_block_till_done() - assert "Error executing script: boom" in caplog.text + assert "Error executing script" in caplog.text async def test_execute_runtime_error_with_response(hass: HomeAssistant) -> None: diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index 5c3ad2a3b39..c8c6b21951d 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -189,7 +189,7 @@ async def test_abort_calls_async_remove_with_exception( with caplog.at_level(logging.ERROR): await manager.async_init("test") - assert "Error removing test flow: error" in caplog.text + assert "Error removing test flow" in caplog.text TestFlow.async_remove.assert_called_once() From ae635a55861b3b299d551fb46cbbeb32373271f2 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Fri, 29 Mar 2024 07:32:41 +0100 Subject: [PATCH 060/967] Use `setup_test_component_platform` helper for fan entity component tests instead of `hass.components` (#114409) --- tests/components/fan/common.py | 27 ++++++++ tests/components/fan/test_init.py | 22 ++++--- .../custom_components/test/fan.py | 65 ------------------- 3 files changed, 40 insertions(+), 74 deletions(-) delete mode 100644 tests/testing_config/custom_components/test/fan.py diff --git a/tests/components/fan/common.py b/tests/components/fan/common.py index 7955a91bc0a..e9919c865c8 100644 --- a/tests/components/fan/common.py +++ b/tests/components/fan/common.py @@ -17,6 +17,7 @@ from homeassistant.components.fan import ( SERVICE_SET_DIRECTION, SERVICE_SET_PERCENTAGE, SERVICE_SET_PRESET_MODE, + FanEntity, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -25,6 +26,8 @@ from homeassistant.const import ( SERVICE_TURN_ON, ) +from tests.common import MockEntity + async def async_turn_on( hass, @@ -146,3 +149,27 @@ async def async_set_direction( await hass.services.async_call(DOMAIN, SERVICE_SET_DIRECTION, data, blocking=True) await hass.async_block_till_done() + + +class MockFan(MockEntity, FanEntity): + """Mock Fan class.""" + + @property + def preset_mode(self) -> str | None: + """Return preset mode.""" + return self._handle("preset_mode") + + @property + def preset_modes(self) -> list[str] | None: + """Return preset mode.""" + return self._handle("preset_modes") + + @property + def supported_features(self): + """Return the class of this fan.""" + return self._handle("supported_features") + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set preset mode.""" + self._attr_preset_mode = preset_mode + self.async_write_ha_state() diff --git a/tests/components/fan/test_init.py b/tests/components/fan/test_init.py index 911954d1ecd..e6bcc5542bd 100644 --- a/tests/components/fan/test_init.py +++ b/tests/components/fan/test_init.py @@ -16,8 +16,12 @@ from homeassistant.core import HomeAssistant import homeassistant.helpers.entity_registry as er from homeassistant.setup import async_setup_component -from tests.common import help_test_all, import_and_test_deprecated_constant_enum -from tests.testing_config.custom_components.test.fan 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): @@ -103,21 +107,21 @@ async def test_preset_mode_validation( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, entity_registry: er.EntityRegistry, - enable_custom_integrations: None, ) -> None: """Test preset mode validation.""" - await hass.async_block_till_done() - platform = getattr(hass.components, "test.fan") - platform.init(empty=False) + test_fan = MockFan( + name="Support fan with preset_mode support", + supported_features=FanEntityFeature.PRESET_MODE, + unique_id="unique_support_preset_mode", + preset_modes=["auto", "eco"], + ) + setup_test_component_platform(hass, "fan", [test_fan]) assert await async_setup_component(hass, "fan", {"fan": {"platform": "test"}}) await hass.async_block_till_done() - test_fan: MockFan = platform.ENTITIES["support_preset_mode"] - await hass.async_block_till_done() - state = hass.states.get("fan.support_fan_with_preset_mode_support") assert state.attributes.get(ATTR_PRESET_MODES) == ["auto", "eco"] diff --git a/tests/testing_config/custom_components/test/fan.py b/tests/testing_config/custom_components/test/fan.py deleted file mode 100644 index cc38972bc71..00000000000 --- a/tests/testing_config/custom_components/test/fan.py +++ /dev/null @@ -1,65 +0,0 @@ -"""Provide a mock fan platform. - -Call init before using it in your tests to ensure clean test data. -""" - -from homeassistant.components.fan import FanEntity, FanEntityFeature -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -from tests.common import MockEntity - -ENTITIES = {} - - -def init(empty=False): - """Initialize the platform with entities.""" - global ENTITIES - - ENTITIES = ( - {} - if empty - else { - "support_preset_mode": MockFan( - name="Support fan with preset_mode support", - supported_features=FanEntityFeature.PRESET_MODE, - unique_id="unique_support_preset_mode", - preset_modes=["auto", "eco"], - ) - } - ) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities_callback: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -): - """Return mock entities.""" - async_add_entities_callback(list(ENTITIES.values())) - - -class MockFan(MockEntity, FanEntity): - """Mock Fan class.""" - - @property - def preset_mode(self) -> str | None: - """Return preset mode.""" - return self._handle("preset_mode") - - @property - def preset_modes(self) -> list[str] | None: - """Return preset mode.""" - return self._handle("preset_modes") - - @property - def supported_features(self): - """Return the class of this fan.""" - return self._handle("supported_features") - - async def async_set_preset_mode(self, preset_mode: str) -> None: - """Set preset mode.""" - self._attr_preset_mode = preset_mode - await self.async_update_ha_state() From a1022304985df141d24d9a0119a7466e68f4858d Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Fri, 29 Mar 2024 07:34:00 +0100 Subject: [PATCH 061/967] Use `setup_test_component_platform` helper for time entity component tests instead of `hass.components` (#114411) --- tests/components/time/common.py | 20 ++++++++ tests/components/time/test_init.py | 25 ++++----- .../custom_components/test/time.py | 51 ------------------- 3 files changed, 30 insertions(+), 66 deletions(-) create mode 100644 tests/components/time/common.py delete mode 100644 tests/testing_config/custom_components/test/time.py diff --git a/tests/components/time/common.py b/tests/components/time/common.py new file mode 100644 index 00000000000..f0a1c04a93f --- /dev/null +++ b/tests/components/time/common.py @@ -0,0 +1,20 @@ +"""Common helpers for time entity component tests.""" + +from datetime import time + +from homeassistant.components.time import TimeEntity + +from tests.common import MockEntity + + +class MockTimeEntity(MockEntity, TimeEntity): + """Mock time class.""" + + @property + def native_value(self) -> time | None: + """Return the current time.""" + return self._handle("native_value") + + def set_value(self, value: time) -> None: + """Change the time.""" + self._values["native_value"] = value diff --git a/tests/components/time/test_init.py b/tests/components/time/test_init.py index 20360279217..0f0dbe05e5b 100644 --- a/tests/components/time/test_init.py +++ b/tests/components/time/test_init.py @@ -2,7 +2,7 @@ from datetime import time -from homeassistant.components.time import DOMAIN, SERVICE_SET_VALUE, TimeEntity +from homeassistant.components.time import DOMAIN, SERVICE_SET_VALUE from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, @@ -12,23 +12,18 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component - -class MockTimeEntity(TimeEntity): - """Mock time device to use in tests.""" - - def __init__(self, native_value=time(12, 0, 0)) -> None: - """Initialize mock time entity.""" - self._attr_native_value = native_value - - async def async_set_value(self, value: time) -> None: - """Set the value of the time.""" - self._attr_native_value = value +from tests.common import setup_test_component_platform +from tests.components.time.common import MockTimeEntity -async def test_date(hass: HomeAssistant, enable_custom_integrations: None) -> None: +async def test_date(hass: HomeAssistant) -> None: """Test time entity.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + entity = MockTimeEntity( + name="test", + unique_id="unique_time", + native_value=time(1, 2, 3), + ) + setup_test_component_platform(hass, DOMAIN, [entity]) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() diff --git a/tests/testing_config/custom_components/test/time.py b/tests/testing_config/custom_components/test/time.py deleted file mode 100644 index 998406d7830..00000000000 --- a/tests/testing_config/custom_components/test/time.py +++ /dev/null @@ -1,51 +0,0 @@ -"""Provide a mock time platform. - -Call init before using it in your tests to ensure clean test data. -""" - -from datetime import time - -from homeassistant.components.time import TimeEntity - -from tests.common import MockEntity - -UNIQUE_TIME = "unique_time" - -ENTITIES = [] - - -class MockTimeEntity(MockEntity, TimeEntity): - """Mock time class.""" - - @property - def native_value(self): - """Return the native value of this time.""" - return self._handle("native_value") - - def set_value(self, value: time) -> None: - """Change the time.""" - self._values["native_value"] = value - - -def init(empty=False): - """Initialize the platform with entities.""" - global ENTITIES - - ENTITIES = ( - [] - if empty - else [ - MockTimeEntity( - name="test", - unique_id=UNIQUE_TIME, - native_value=time(1, 2, 3), - ), - ] - ) - - -async def async_setup_platform( - hass, config, async_add_entities_callback, discovery_info=None -): - """Return mock entities.""" - async_add_entities_callback(ENTITIES) From 61982acb17ca11a705e4f3adfb11aa8eeb3fd2ab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Mar 2024 20:36:33 -1000 Subject: [PATCH 062/967] Cleanup some plex tasks that delayed startup (#114418) --- homeassistant/components/plex/__init__.py | 19 ++++--------------- homeassistant/components/plex/const.py | 1 - homeassistant/components/plex/helpers.py | 2 -- homeassistant/components/plex/server.py | 1 + 4 files changed, 5 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 4e17e4032aa..eb57dc46727 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -42,7 +42,6 @@ from .const import ( DOMAIN, INVALID_TOKEN_MESSAGE, PLATFORMS, - PLATFORMS_COMPLETED, PLEX_SERVER_CONFIG, PLEX_UPDATE_LIBRARY_SIGNAL, PLEX_UPDATE_PLATFORMS_SIGNAL, @@ -94,18 +93,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: gdm.scan(scan_for_clients=True) debouncer = Debouncer[None]( - hass, - _LOGGER, - cooldown=10, - immediate=True, - function=gdm_scan, + hass, _LOGGER, cooldown=10, immediate=True, function=gdm_scan, background=True ).async_call hass_data = PlexData( servers={}, dispatchers={}, websockets={}, - platforms_completed={}, gdm_scanner=gdm, gdm_debouncer=debouncer, ) @@ -180,7 +174,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: server_id = plex_server.machine_identifier hass_data = get_plex_data(hass) hass_data[SERVERS][server_id] = plex_server - hass_data[PLATFORMS_COMPLETED][server_id] = set() entry.add_update_listener(async_options_updated) @@ -233,11 +226,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) hass_data[WEBSOCKETS][server_id] = websocket - def start_websocket_session(platform): - hass_data[PLATFORMS_COMPLETED][server_id].add(platform) - if hass_data[PLATFORMS_COMPLETED][server_id] == PLATFORMS: - hass.loop.create_task(websocket.listen()) - def close_websocket_session(_): websocket.close() @@ -248,8 +236,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - for platform in PLATFORMS: - start_websocket_session(platform) + entry.async_create_background_task( + hass, websocket.listen(), f"plex websocket listener {entry.entry_id}" + ) async_cleanup_plex_devices(hass, entry) diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index 8dc75a447af..d5d70219471 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -24,7 +24,6 @@ GDM_SCANNER: Final = "gdm_scanner" PLATFORMS = frozenset( [Platform.BUTTON, Platform.MEDIA_PLAYER, Platform.SENSOR, Platform.UPDATE] ) -PLATFORMS_COMPLETED: Final = "platforms_completed" PLAYER_SOURCE = "player_source" SERVERS: Final = "servers" WEBSOCKETS: Final = "websockets" diff --git a/homeassistant/components/plex/helpers.py b/homeassistant/components/plex/helpers.py index f51350ac597..3c7ff8180c8 100644 --- a/homeassistant/components/plex/helpers.py +++ b/homeassistant/components/plex/helpers.py @@ -8,7 +8,6 @@ from typing import TYPE_CHECKING, Any, TypedDict from plexapi.gdm import GDM from plexwebsocket import PlexWebsocket -from homeassistant.const import Platform from homeassistant.core import CALLBACK_TYPE, HomeAssistant from .const import DOMAIN, SERVERS @@ -23,7 +22,6 @@ class PlexData(TypedDict): servers: dict[str, PlexServer] dispatchers: dict[str, list[CALLBACK_TYPE]] websockets: dict[str, PlexWebsocket] - platforms_completed: dict[str, set[Platform]] gdm_scanner: GDM gdm_debouncer: Callable[[], Coroutine[Any, Any, None]] diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 9e2bf63ce55..584378d51f9 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -97,6 +97,7 @@ class PlexServer: cooldown=DEBOUNCE_TIMEOUT, immediate=True, function=self._async_update_platforms, + background=True, ).async_call self.thumbnail_cache = {} From 74d614243b5de47b3892425729f7331cff16c0c7 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Fri, 29 Mar 2024 07:43:19 +0100 Subject: [PATCH 063/967] Use `setup_test_component_platform` helper for date entity component tests instead of `hass.components` (#114413) * Use `setup_test_component_platform` helper for date entity component tests instead of `hass.components` * Remove missing --- tests/components/date/common.py | 20 ++++++++ tests/components/date/test_init.py | 27 ++++------ .../custom_components/test/date.py | 51 ------------------- 3 files changed, 30 insertions(+), 68 deletions(-) create mode 100644 tests/components/date/common.py delete mode 100644 tests/testing_config/custom_components/test/date.py diff --git a/tests/components/date/common.py b/tests/components/date/common.py new file mode 100644 index 00000000000..38641ba63fb --- /dev/null +++ b/tests/components/date/common.py @@ -0,0 +1,20 @@ +"""Common helpers for date entity component tests.""" + +from datetime import date + +from homeassistant.components.date import DateEntity + +from tests.common import MockEntity + + +class MockDateEntity(MockEntity, DateEntity): + """Mock date class.""" + + @property + def native_value(self): + """Return the native value of this date.""" + return self._handle("native_value") + + def set_value(self, value: date) -> None: + """Change the date.""" + self._values["native_value"] = value diff --git a/tests/components/date/test_init.py b/tests/components/date/test_init.py index f0a0094f8b8..a6c517c7b9e 100644 --- a/tests/components/date/test_init.py +++ b/tests/components/date/test_init.py @@ -2,7 +2,7 @@ from datetime import date -from homeassistant.components.date import DOMAIN, SERVICE_SET_VALUE, DateEntity +from homeassistant.components.date import DOMAIN, SERVICE_SET_VALUE from homeassistant.const import ( ATTR_DATE, ATTR_ENTITY_ID, @@ -12,25 +12,18 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component - -class MockDateEntity(DateEntity): - """Mock date device to use in tests.""" - - _attr_name = "date" - - def __init__(self, native_value=date(2020, 1, 1)) -> None: - """Initialize mock date entity.""" - self._attr_native_value = native_value - - async def async_set_value(self, value: date) -> None: - """Set the value of the date.""" - self._attr_native_value = value +from tests.common import setup_test_component_platform +from tests.components.date.common import MockDateEntity -async def test_date(hass: HomeAssistant, enable_custom_integrations: None) -> None: +async def test_date(hass: HomeAssistant) -> None: """Test date entity.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + entity = MockDateEntity( + name="test", + unique_id="unique_date", + native_value=date(2020, 1, 1), + ) + setup_test_component_platform(hass, DOMAIN, [entity]) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() diff --git a/tests/testing_config/custom_components/test/date.py b/tests/testing_config/custom_components/test/date.py deleted file mode 100644 index 0a51bea029d..00000000000 --- a/tests/testing_config/custom_components/test/date.py +++ /dev/null @@ -1,51 +0,0 @@ -"""Provide a mock date platform. - -Call init before using it in your tests to ensure clean test data. -""" - -from datetime import date - -from homeassistant.components.date import DateEntity - -from tests.common import MockEntity - -UNIQUE_DATE = "unique_date" - -ENTITIES = [] - - -class MockDateEntity(MockEntity, DateEntity): - """Mock date class.""" - - @property - def native_value(self): - """Return the native value of this date.""" - return self._handle("native_value") - - def set_value(self, value: date) -> None: - """Change the date.""" - self._values["native_value"] = value - - -def init(empty=False): - """Initialize the platform with entities.""" - global ENTITIES - - ENTITIES = ( - [] - if empty - else [ - MockDateEntity( - name="test", - unique_id=UNIQUE_DATE, - native_value=date(2020, 1, 1), - ), - ] - ) - - -async def async_setup_platform( - hass, config, async_add_entities_callback, discovery_info=None -): - """Return mock entities.""" - async_add_entities_callback(ENTITIES) From 045dc3f1fb37a4eb462f1caa5b7e84ab150b9703 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Fri, 29 Mar 2024 07:51:10 +0100 Subject: [PATCH 064/967] Use `setup_test_component_platform` helper for datetime entity component tests instead of `hass.components` (#114415) --- tests/components/datetime/common.py | 20 ++++++++ tests/components/datetime/test_init.py | 37 ++++++-------- .../custom_components/test/datetime.py | 51 ------------------- 3 files changed, 36 insertions(+), 72 deletions(-) create mode 100644 tests/components/datetime/common.py delete mode 100644 tests/testing_config/custom_components/test/datetime.py diff --git a/tests/components/datetime/common.py b/tests/components/datetime/common.py new file mode 100644 index 00000000000..2a4ba542950 --- /dev/null +++ b/tests/components/datetime/common.py @@ -0,0 +1,20 @@ +"""Common helpers for the datetime entity component tests.""" + +from datetime import datetime + +from homeassistant.components.datetime import DateTimeEntity + +from tests.common import MockEntity + + +class MockDateTimeEntity(MockEntity, DateTimeEntity): + """Mock date/time class.""" + + @property + def native_value(self): + """Return the native value of this date/time.""" + return self._handle("native_value") + + def set_value(self, value: datetime) -> None: + """Change the time.""" + self._values["native_value"] = value diff --git a/tests/components/datetime/test_init.py b/tests/components/datetime/test_init.py index f85754f5e1f..da65e1bce9e 100644 --- a/tests/components/datetime/test_init.py +++ b/tests/components/datetime/test_init.py @@ -5,36 +5,31 @@ from zoneinfo import ZoneInfo import pytest -from homeassistant.components.datetime import ( - ATTR_DATETIME, - DOMAIN, - SERVICE_SET_VALUE, - DateTimeEntity, -) +from homeassistant.components.datetime import ATTR_DATETIME, DOMAIN, SERVICE_SET_VALUE from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, CONF_PLATFORM from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +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) -class MockDateTimeEntity(DateTimeEntity): - """Mock datetime device to use in tests.""" - - def __init__(self, native_value: datetime | None = DEFAULT_VALUE) -> None: - """Initialize mock datetime entity.""" - self._attr_native_value = native_value - - async def async_set_value(self, value: datetime) -> None: - """Change the date/time.""" - self._attr_native_value = value - - -async def test_datetime(hass: HomeAssistant, enable_custom_integrations: None) -> None: +async def test_datetime(hass: HomeAssistant) -> None: """Test date/time entity.""" hass.config.set_time_zone("UTC") - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform( + hass, + DOMAIN, + [ + MockDateTimeEntity( + name="test", + unique_id="unique_datetime", + native_value=datetime(2020, 1, 1, 1, 2, 3, tzinfo=UTC), + ) + ], + ) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() diff --git a/tests/testing_config/custom_components/test/datetime.py b/tests/testing_config/custom_components/test/datetime.py deleted file mode 100644 index fa9dfff8a60..00000000000 --- a/tests/testing_config/custom_components/test/datetime.py +++ /dev/null @@ -1,51 +0,0 @@ -"""Provide a mock time platform. - -Call init before using it in your tests to ensure clean test data. -""" - -from datetime import UTC, datetime - -from homeassistant.components.datetime import DateTimeEntity - -from tests.common import MockEntity - -UNIQUE_DATETIME = "unique_datetime" - -ENTITIES = [] - - -class MockDateTimeEntity(MockEntity, DateTimeEntity): - """Mock date/time class.""" - - @property - def native_value(self): - """Return the native value of this date/time.""" - return self._handle("native_value") - - def set_value(self, value: datetime) -> None: - """Change the time.""" - self._values["native_value"] = value - - -def init(empty=False): - """Initialize the platform with entities.""" - global ENTITIES - - ENTITIES = ( - [] - if empty - else [ - MockDateTimeEntity( - name="test", - unique_id=UNIQUE_DATETIME, - native_value=datetime(2020, 1, 1, 1, 2, 3, tzinfo=UTC), - ), - ] - ) - - -async def async_setup_platform( - hass, config, async_add_entities_callback, discovery_info=None -): - """Return mock entities.""" - async_add_entities_callback(ENTITIES) From 247ee6e4f01580cf90aeb2e834b1b4b226aa9a9f Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Fri, 29 Mar 2024 10:18:57 +0100 Subject: [PATCH 065/967] Address late review comments for fan entity component test (#114425) * Address late review comments for fan entity component test * Update tests/components/fan/common.py Co-authored-by: Martin Hjelmare --------- Co-authored-by: Martin Hjelmare --- tests/components/fan/common.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/tests/components/fan/common.py b/tests/components/fan/common.py index e9919c865c8..a48e66c08f4 100644 --- a/tests/components/fan/common.py +++ b/tests/components/fan/common.py @@ -154,22 +154,11 @@ async def async_set_direction( class MockFan(MockEntity, FanEntity): """Mock Fan class.""" - @property - def preset_mode(self) -> str | None: - """Return preset mode.""" - return self._handle("preset_mode") - @property def preset_modes(self) -> list[str] | None: """Return preset mode.""" return self._handle("preset_modes") - @property - def supported_features(self): - """Return the class of this fan.""" - return self._handle("supported_features") - - async def async_set_preset_mode(self, preset_mode: str) -> None: + def set_preset_mode(self, preset_mode: str) -> None: """Set preset mode.""" self._attr_preset_mode = preset_mode - self.async_write_ha_state() From ed7e5c4de6b01a1070fd1a3813dacde33863e1c5 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 29 Mar 2024 11:04:37 +0100 Subject: [PATCH 066/967] Add single config entry to Mullvad (#114426) * Add single config entry to Mullvad * Add single config entry to Mullvad * Add single config entry to Mullvad * Fix --- homeassistant/components/mullvad/config_flow.py | 8 +++++--- homeassistant/components/mullvad/manifest.json | 3 ++- homeassistant/components/mullvad/strings.json | 3 --- homeassistant/generated/integrations.json | 3 ++- tests/components/mullvad/test_config_flow.py | 2 +- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/mullvad/config_flow.py b/homeassistant/components/mullvad/config_flow.py index 55957f160a3..0ffcc11c97e 100644 --- a/homeassistant/components/mullvad/config_flow.py +++ b/homeassistant/components/mullvad/config_flow.py @@ -1,5 +1,7 @@ """Config flow for Mullvad VPN integration.""" +from typing import Any + from mullvad_api import MullvadAPI, MullvadAPIError from homeassistant.config_entries import ConfigFlow, ConfigFlowResult @@ -12,10 +14,10 @@ class MullvadConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 - async def async_step_user(self, user_input=None) -> ConfigFlowResult: + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: """Handle the initial step.""" - self._async_abort_entries_match() - errors = {} if user_input is not None: try: diff --git a/homeassistant/components/mullvad/manifest.json b/homeassistant/components/mullvad/manifest.json index 13dd27375cf..fc3faefe1e3 100644 --- a/homeassistant/components/mullvad/manifest.json +++ b/homeassistant/components/mullvad/manifest.json @@ -5,5 +5,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/mullvad", "iot_class": "cloud_polling", - "requirements": ["mullvad-api==1.0.0"] + "requirements": ["mullvad-api==1.0.0"], + "single_config_entry": true } diff --git a/homeassistant/components/mullvad/strings.json b/homeassistant/components/mullvad/strings.json index 3e029184155..d3f757e829c 100644 --- a/homeassistant/components/mullvad/strings.json +++ b/homeassistant/components/mullvad/strings.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" - }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "unknown": "[%key:common::config_flow::error::unknown%]" diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 7c068de51ba..c30f22acc6e 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -3815,7 +3815,8 @@ "name": "Mullvad VPN", "integration_type": "hub", "config_flow": true, - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "single_config_entry": true }, "mutesync": { "name": "mutesync", diff --git a/tests/components/mullvad/test_config_flow.py b/tests/components/mullvad/test_config_flow.py index e1e6570fa67..da9ce91eeed 100644 --- a/tests/components/mullvad/test_config_flow.py +++ b/tests/components/mullvad/test_config_flow.py @@ -51,7 +51,7 @@ async def test_form_user_only_once(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "already_configured" + assert result["reason"] == "single_instance_allowed" async def test_connection_error(hass: HomeAssistant) -> None: From 72614c86c2b72ce8573bf89234887f7dc95f7898 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Fri, 29 Mar 2024 11:47:21 +0000 Subject: [PATCH 067/967] Bump python-ring-doorbell to 0.8.8 (#114431) * Bump ring_doorbell to 0.8.8 * Fix intercom history test for new library version --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/ring/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ring/test_sensor.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index 0390db640e5..764557a3a1d 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -13,5 +13,5 @@ "documentation": "https://www.home-assistant.io/integrations/ring", "iot_class": "cloud_polling", "loggers": ["ring_doorbell"], - "requirements": ["ring-doorbell[listen]==0.8.7"] + "requirements": ["ring-doorbell[listen]==0.8.8"] } diff --git a/requirements_all.txt b/requirements_all.txt index 27040f835e2..06d4ca4f6c8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2448,7 +2448,7 @@ rfk101py==0.0.1 rflink==0.0.66 # homeassistant.components.ring -ring-doorbell[listen]==0.8.7 +ring-doorbell[listen]==0.8.8 # homeassistant.components.fleetgo ritassist==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 254c8923f39..3e5f81a0767 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1891,7 +1891,7 @@ reolink-aio==0.8.9 rflink==0.0.66 # homeassistant.components.ring -ring-doorbell[listen]==0.8.7 +ring-doorbell[listen]==0.8.8 # homeassistant.components.roku rokuecp==0.19.2 diff --git a/tests/components/ring/test_sensor.py b/tests/components/ring/test_sensor.py index aadea6f0ba1..2c866586c6c 100644 --- a/tests/components/ring/test_sensor.py +++ b/tests/components/ring/test_sensor.py @@ -87,7 +87,7 @@ async def test_history( 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 == "unknown" + assert ingress_last_activity_state.state == "2024-02-02T11:21:24+00:00" async def test_only_chime_devices( From 6d54f686a65214d05d4cf54cc8edd64865d0e4f5 Mon Sep 17 00:00:00 2001 From: Mischa Siekmann <45062894+gnumpi@users.noreply.github.com> Date: Fri, 29 Mar 2024 13:29:14 +0100 Subject: [PATCH 068/967] Add Integration for Energenie Power-Sockets (#113097) * Integration for Energenie Power-Strips (EGPS) * cleanups reocommended by reviewer * Adds missing exception handling when trying to send a command to an unreachable device. * fix: incorrect handling of already opened devices in pyegps api. bump to pyegps=0.2.4 * Add blank line after file docstring, and other cosmetics * change asyncio.to_thread to async_add_executer_job * raises HomeAssistantError EgpsException in switch services. * switch test parameterized by entity name * reoved unused device registry * add translation_key and update_before_add * bump pyegps dependency to version to 0.2.5 * combined get_device patches and put into conftest.py * changed switch entity to use _attr_is_on and cleanups * further cleanup * Apply suggestions from code review * refactor: rename egps to energenie_power_sockets * updated test snapshot --------- Co-authored-by: Joost Lekkerkerker --- .strict-typing | 1 + CODEOWNERS | 2 + .../energenie_power_sockets/__init__.py | 44 ++++ .../energenie_power_sockets/config_flow.py | 55 +++++ .../energenie_power_sockets/const.py | 8 + .../energenie_power_sockets/manifest.json | 11 + .../energenie_power_sockets/strings.json | 27 +++ .../energenie_power_sockets/switch.py | 77 +++++++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + mypy.ini | 10 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + .../energenie_power_sockets/__init__.py | 1 + .../energenie_power_sockets/conftest.py | 83 ++++++++ .../snapshots/test_switch.ambr | 189 ++++++++++++++++++ .../test_config_flow.py | 140 +++++++++++++ .../energenie_power_sockets/test_init.py | 64 ++++++ .../energenie_power_sockets/test_switch.py | 134 +++++++++++++ 19 files changed, 859 insertions(+) create mode 100644 homeassistant/components/energenie_power_sockets/__init__.py create mode 100644 homeassistant/components/energenie_power_sockets/config_flow.py create mode 100644 homeassistant/components/energenie_power_sockets/const.py create mode 100644 homeassistant/components/energenie_power_sockets/manifest.json create mode 100644 homeassistant/components/energenie_power_sockets/strings.json create mode 100644 homeassistant/components/energenie_power_sockets/switch.py create mode 100644 tests/components/energenie_power_sockets/__init__.py create mode 100644 tests/components/energenie_power_sockets/conftest.py create mode 100644 tests/components/energenie_power_sockets/snapshots/test_switch.ambr create mode 100644 tests/components/energenie_power_sockets/test_config_flow.py create mode 100644 tests/components/energenie_power_sockets/test_init.py create mode 100644 tests/components/energenie_power_sockets/test_switch.py diff --git a/.strict-typing b/.strict-typing index 39ff23a472e..b1d6df7c9b8 100644 --- a/.strict-typing +++ b/.strict-typing @@ -166,6 +166,7 @@ homeassistant.components.electric_kiwi.* homeassistant.components.elgato.* homeassistant.components.elkm1.* homeassistant.components.emulated_hue.* +homeassistant.components.energenie_power_sockets.* homeassistant.components.energy.* homeassistant.components.energyzero.* homeassistant.components.enigma2.* diff --git a/CODEOWNERS b/CODEOWNERS index 81add403413..59359e708f9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -378,6 +378,8 @@ build.json @home-assistant/supervisor /tests/components/emulated_hue/ @bdraco @Tho85 /homeassistant/components/emulated_kasa/ @kbickar /tests/components/emulated_kasa/ @kbickar +/homeassistant/components/energenie_power_sockets/ @gnumpi +/tests/components/energenie_power_sockets/ @gnumpi /homeassistant/components/energy/ @home-assistant/core /tests/components/energy/ @home-assistant/core /homeassistant/components/energyzero/ @klaasnicolaas diff --git a/homeassistant/components/energenie_power_sockets/__init__.py b/homeassistant/components/energenie_power_sockets/__init__.py new file mode 100644 index 00000000000..12ddb0d1389 --- /dev/null +++ b/homeassistant/components/energenie_power_sockets/__init__.py @@ -0,0 +1,44 @@ +"""Energenie Power-Sockets (EGPS) integration.""" + +from pyegps import PowerStripUSB, get_device +from pyegps.exceptions import MissingLibrary, UsbError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady + +from .const import CONF_DEVICE_API_ID, DOMAIN + +PLATFORMS = [Platform.SWITCH] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Energenie Power Sockets.""" + try: + powerstrip: PowerStripUSB | None = get_device(entry.data[CONF_DEVICE_API_ID]) + + except (MissingLibrary, UsbError) as ex: + raise ConfigEntryError("Can't access usb devices.") from ex + + if powerstrip is None: + raise ConfigEntryNotReady( + "Can't access Energenie Power Sockets, will retry later." + ) + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = powerstrip + 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.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + powerstrip = hass.data[DOMAIN].pop(entry.entry_id) + powerstrip.release() + + if not hass.data[DOMAIN]: + hass.data.pop(DOMAIN) + + return unload_ok diff --git a/homeassistant/components/energenie_power_sockets/config_flow.py b/homeassistant/components/energenie_power_sockets/config_flow.py new file mode 100644 index 00000000000..ab39427f15a --- /dev/null +++ b/homeassistant/components/energenie_power_sockets/config_flow.py @@ -0,0 +1,55 @@ +"""ConfigFlow for Energenie-Power-Sockets devices.""" + +from typing import Any + +from pyegps import get_device, search_for_devices +from pyegps.exceptions import MissingLibrary, UsbError +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult + +from .const import CONF_DEVICE_API_ID, DOMAIN, LOGGER + + +class EGPSConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle the config flow for EGPS devices.""" + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Initiate user flow.""" + + if user_input is not None: + dev_id = user_input[CONF_DEVICE_API_ID] + dev = await self.hass.async_add_executor_job(get_device, dev_id) + if dev is not None: + await self.async_set_unique_id(dev.device_id) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=dev_id, + data={CONF_DEVICE_API_ID: dev_id}, + ) + return self.async_abort(reason="device_not_found") + + currently_configured = self._async_current_ids(include_ignore=True) + try: + found_devices = await self.hass.async_add_executor_job(search_for_devices) + except (MissingLibrary, UsbError): + LOGGER.exception("Unable to access USB devices") + return self.async_abort(reason="usb_error") + + devices = [ + d + for d in found_devices + if d.get_device_type() == "PowerStrip" + and d.device_id not in currently_configured + ] + LOGGER.debug("Found %d devices", len(devices)) + if len(devices) > 0: + options = {d.device_id: f"{d.name} ({d.device_id})" for d in devices} + data_schema = {CONF_DEVICE_API_ID: vol.In(options)} + else: + return self.async_abort(reason="no_device") + + return self.async_show_form(step_id="user", data_schema=vol.Schema(data_schema)) diff --git a/homeassistant/components/energenie_power_sockets/const.py b/homeassistant/components/energenie_power_sockets/const.py new file mode 100644 index 00000000000..a02373815c2 --- /dev/null +++ b/homeassistant/components/energenie_power_sockets/const.py @@ -0,0 +1,8 @@ +"""Constants for Energenie Power Sockets.""" + +import logging + +LOGGER = logging.getLogger(__package__) + +CONF_DEVICE_API_ID = "api-device-id" +DOMAIN = "energenie_power_sockets" diff --git a/homeassistant/components/energenie_power_sockets/manifest.json b/homeassistant/components/energenie_power_sockets/manifest.json new file mode 100644 index 00000000000..8a55a539e7f --- /dev/null +++ b/homeassistant/components/energenie_power_sockets/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "energenie_power_sockets", + "name": "Energenie Power Sockets", + "codeowners": ["@gnumpi"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/energenie_power_sockets", + "integration_type": "device", + "iot_class": "local_polling", + "loggers": ["pyegps"], + "requirements": ["pyegps==0.2.5"] +} diff --git a/homeassistant/components/energenie_power_sockets/strings.json b/homeassistant/components/energenie_power_sockets/strings.json new file mode 100644 index 00000000000..e193b06b25f --- /dev/null +++ b/homeassistant/components/energenie_power_sockets/strings.json @@ -0,0 +1,27 @@ +{ + "title": "Energenie Power Sockets Integration.", + "config": { + "step": { + "user": { + "title": "Searching for Energenie-Power-Sockets Devices.", + "description": "Choose a discovered device.", + "data": { + "device": "[%key:common::config_flow::data::device%]" + } + } + }, + "abort": { + "usb_error": "Couldn't access USB devices!", + "no_device": "Unable to discover any (new) supported device.", + "device_not_found": "No device was found for the given id.", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "entity": { + "switch": { + "socket": { + "name": "Socket {socket_id}" + } + } + } +} diff --git a/homeassistant/components/energenie_power_sockets/switch.py b/homeassistant/components/energenie_power_sockets/switch.py new file mode 100644 index 00000000000..1d5b9ed5197 --- /dev/null +++ b/homeassistant/components/energenie_power_sockets/switch.py @@ -0,0 +1,77 @@ +"""Switch implementation for Energenie-Power-Sockets Platform.""" + +from typing import Any + +from pyegps import __version__ as PYEGPS_VERSION +from pyegps.exceptions import EgpsException +from pyegps.powerstrip import PowerStrip + +from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Add EGPS sockets for passed config_entry in HA.""" + powerstrip: PowerStrip = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities( + ( + EGPowerStripSocket(powerstrip, socket) + for socket in range(powerstrip.numberOfSockets) + ), + update_before_add=True, + ) + + +class EGPowerStripSocket(SwitchEntity): + """Represents a socket of an Energenie-Socket-Strip.""" + + _attr_device_class = SwitchDeviceClass.OUTLET + _attr_has_entity_name = True + _attr_translation_key = "socket" + + def __init__(self, dev: PowerStrip, socket: int) -> None: + """Initiate a new socket.""" + self._dev = dev + self._socket = socket + self._attr_translation_placeholders = {"socket_id": str(socket)} + + self._attr_unique_id = f"{dev.device_id}_{socket}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, dev.device_id)}, + name=dev.name, + manufacturer=dev.manufacturer, + model=dev.name, + sw_version=PYEGPS_VERSION, + ) + + def turn_on(self, **kwargs: Any) -> None: + """Switch the socket on.""" + try: + self._dev.switch_on(self._socket) + except EgpsException as err: + raise HomeAssistantError(f"Couldn't access USB device: {err}") from err + + def turn_off(self, **kwargs: Any) -> None: + """Switch the socket off.""" + try: + self._dev.switch_off(self._socket) + except EgpsException as err: + raise HomeAssistantError(f"Couldn't access USB device: {err}") from err + + def update(self) -> None: + """Read the current state from the device.""" + try: + self._attr_is_on = self._dev.get_status(self._socket) + except EgpsException as err: + raise HomeAssistantError(f"Couldn't access USB device: {err}") from err diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 283cdf1a0de..acac5f8df5d 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -144,6 +144,7 @@ FLOWS = { "elvia", "emonitor", "emulated_roku", + "energenie_power_sockets", "energyzero", "enocean", "enphase_envoy", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index c30f22acc6e..631c8b1e73c 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1571,6 +1571,11 @@ "config_flow": true, "iot_class": "local_push" }, + "energenie_power_sockets": { + "integration_type": "device", + "config_flow": true, + "iot_class": "local_polling" + }, "energie_vanons": { "name": "Energie VanOns", "integration_type": "virtual", @@ -7179,6 +7184,7 @@ "demo", "derivative", "emulated_roku", + "energenie_power_sockets", "filesize", "garages_amsterdam", "generic", diff --git a/mypy.ini b/mypy.ini index 66af4c9c25a..159101a21b3 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1421,6 +1421,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.energenie_power_sockets.*] +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.energy.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 06d4ca4f6c8..aba5ae375e0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1799,6 +1799,9 @@ pyedimax==0.2.1 # homeassistant.components.efergy pyefergy==22.1.1 +# homeassistant.components.energenie_power_sockets +pyegps==0.2.5 + # homeassistant.components.enphase_envoy pyenphase==1.20.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3e5f81a0767..f672c943803 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1398,6 +1398,9 @@ pyeconet==0.1.22 # homeassistant.components.efergy pyefergy==22.1.1 +# homeassistant.components.energenie_power_sockets +pyegps==0.2.5 + # homeassistant.components.enphase_envoy pyenphase==1.20.1 diff --git a/tests/components/energenie_power_sockets/__init__.py b/tests/components/energenie_power_sockets/__init__.py new file mode 100644 index 00000000000..8397567ef82 --- /dev/null +++ b/tests/components/energenie_power_sockets/__init__.py @@ -0,0 +1 @@ +"""Tests for Energenie-Power-Sockets (EGPS) integration.""" diff --git a/tests/components/energenie_power_sockets/conftest.py b/tests/components/energenie_power_sockets/conftest.py new file mode 100644 index 00000000000..f119c0008f7 --- /dev/null +++ b/tests/components/energenie_power_sockets/conftest.py @@ -0,0 +1,83 @@ +"""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 homeassistant.components.energenie_power_sockets.const import ( + CONF_DEVICE_API_ID, + DOMAIN, +) +from homeassistant.const import CONF_NAME + +from tests.common import MockConfigEntry + +DEMO_CONFIG_DATA: Final = { + CONF_NAME: "Unit Test", + CONF_DEVICE_API_ID: "DYPS:00:11:22", +} + + +@pytest.fixture +def demo_config_data() -> dict: + """Return valid user input.""" + return {CONF_DEVICE_API_ID: DEMO_CONFIG_DATA[CONF_DEVICE_API_ID]} + + +@pytest.fixture +def valid_config_entry() -> MockConfigEntry: + """Return a valid egps config entry.""" + return MockConfigEntry( + domain=DOMAIN, + data=DEMO_CONFIG_DATA, + unique_id=DEMO_CONFIG_DATA[CONF_DEVICE_API_ID], + ) + + +@pytest.fixture(name="pyegps_device_mock") +def get_pyegps_device_mock() -> MagicMock: + """Fixture for a mocked FakePowerStrip.""" + + fkObj = FakePowerStrip( + devId=DEMO_CONFIG_DATA[CONF_DEVICE_API_ID], number_of_sockets=4 + ) + fkObj.release = lambda: True + fkObj._status = [0, 1, 0, 1] + + usb_device_mock = MagicMock(wraps=fkObj) + usb_device_mock.get_device_type.return_value = "PowerStrip" + usb_device_mock.numberOfSockets = 4 + usb_device_mock.device_id = DEMO_CONFIG_DATA[CONF_DEVICE_API_ID] + usb_device_mock.manufacturer = "Energenie" + usb_device_mock.name = "MockedUSBDevice" + + return usb_device_mock + + +@pytest.fixture(name="mock_get_device") +def patch_get_device(pyegps_device_mock: MagicMock) -> Generator[MagicMock, None, None]: + """Fixture to patch the `get_device` api method.""" + with ( + patch("homeassistant.components.energenie_power_sockets.get_device") as m1, + patch( + "homeassistant.components.energenie_power_sockets.config_flow.get_device", + new=m1, + ) as mock, + ): + mock.return_value = pyegps_device_mock + yield mock + + +@pytest.fixture(name="mock_search_for_devices") +def patch_search_devices( + pyegps_device_mock: MagicMock, +) -> Generator[MagicMock, None, None]: + """Fixture to patch the `search_for_devices` api method.""" + with patch( + "homeassistant.components.energenie_power_sockets.config_flow.search_for_devices", + return_value=[pyegps_device_mock], + ) as mock: + yield mock diff --git a/tests/components/energenie_power_sockets/snapshots/test_switch.ambr b/tests/components/energenie_power_sockets/snapshots/test_switch.ambr new file mode 100644 index 00000000000..d462d6ca6d4 --- /dev/null +++ b/tests/components/energenie_power_sockets/snapshots/test_switch.ambr @@ -0,0 +1,189 @@ +# serializer version: 1 +# name: test_switch_setup[mockedusbdevice_socket_0] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'MockedUSBDevice Socket 0', + }), + 'context': , + 'entity_id': 'switch.mockedusbdevice_socket_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switch_setup[mockedusbdevice_socket_0].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.mockedusbdevice_socket_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': 'Socket 0', + 'platform': 'energenie_power_sockets', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'socket', + 'unique_id': 'DYPS:00:11:22_0', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch_setup[mockedusbdevice_socket_1] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'MockedUSBDevice Socket 1', + }), + 'context': , + 'entity_id': 'switch.mockedusbdevice_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch_setup[mockedusbdevice_socket_1].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.mockedusbdevice_socket_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': 'Socket 1', + 'platform': 'energenie_power_sockets', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'socket', + 'unique_id': 'DYPS:00:11:22_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch_setup[mockedusbdevice_socket_2] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'MockedUSBDevice Socket 2', + }), + 'context': , + 'entity_id': 'switch.mockedusbdevice_socket_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switch_setup[mockedusbdevice_socket_2].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.mockedusbdevice_socket_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': 'Socket 2', + 'platform': 'energenie_power_sockets', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'socket', + 'unique_id': 'DYPS:00:11:22_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch_setup[mockedusbdevice_socket_3] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'MockedUSBDevice Socket 3', + }), + 'context': , + 'entity_id': 'switch.mockedusbdevice_socket_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch_setup[mockedusbdevice_socket_3].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.mockedusbdevice_socket_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': 'Socket 3', + 'platform': 'energenie_power_sockets', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'socket', + 'unique_id': 'DYPS:00:11:22_3', + 'unit_of_measurement': None, + }) +# --- diff --git a/tests/components/energenie_power_sockets/test_config_flow.py b/tests/components/energenie_power_sockets/test_config_flow.py new file mode 100644 index 00000000000..ef433d0ef09 --- /dev/null +++ b/tests/components/energenie_power_sockets/test_config_flow.py @@ -0,0 +1,140 @@ +"""Tests for Energenie-Power-Sockets config flow.""" + +from unittest.mock import MagicMock + +from pyegps.exceptions import UsbError + +from homeassistant.components.energenie_power_sockets.const import ( + CONF_DEVICE_API_ID, + DOMAIN, +) +from homeassistant.config_entries import SOURCE_USER +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + + +async def test_user_flow( + hass: HomeAssistant, + demo_config_data: dict, + mock_get_device: MagicMock, + mock_search_for_devices: MagicMock, +) -> None: + """Test configuration flow initialized by the user.""" + + result1 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result1["type"] == FlowResultType.FORM + assert not result1["errors"] + + # check with valid data + result2 = await hass.config_entries.flow.async_configure( + result1["flow_id"], user_input=demo_config_data + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + + +async def test_user_flow_already_exists( + hass: HomeAssistant, + valid_config_entry: MockConfigEntry, + mock_get_device: MagicMock, + mock_search_for_devices: MagicMock, +) -> None: + """Test the flow when device has been already configured.""" + valid_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_DEVICE_API_ID: valid_config_entry.data[CONF_DEVICE_API_ID]}, + ) + + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_user_flow_no_new_device( + hass: HomeAssistant, + valid_config_entry: MockConfigEntry, + mock_get_device: MagicMock, + mock_search_for_devices: MagicMock, +) -> None: + """Test the flow when the found device has been already included.""" + valid_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=None, + ) + + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_device" + + +async def test_user_flow_no_device_found( + hass: HomeAssistant, + demo_config_data: dict, + mock_get_device: MagicMock, + mock_search_for_devices: MagicMock, +) -> None: + """Test configuration flow when no device is found.""" + + mock_search_for_devices.return_value = [] + + result1 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result1["type"] == FlowResultType.ABORT + assert result1["reason"] == "no_device" + + +async def test_user_flow_device_not_found( + hass: HomeAssistant, + demo_config_data: dict, + mock_get_device: MagicMock, + mock_search_for_devices: MagicMock, +) -> None: + """Test configuration flow when the given device_id does not match any found devices.""" + + mock_get_device.return_value = None + + result1 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result1["type"] == FlowResultType.FORM + assert not result1["errors"] + + # check with valid data + result2 = await hass.config_entries.flow.async_configure( + result1["flow_id"], user_input=demo_config_data + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "device_not_found" + + +async def test_user_flow_no_usb_access( + hass: HomeAssistant, + mock_get_device: MagicMock, + mock_search_for_devices: MagicMock, +) -> None: + """Test configuration flow when USB devices can't be accessed.""" + + mock_get_device.return_value = None + mock_search_for_devices.side_effect = UsbError + + result1 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result1["type"] == FlowResultType.ABORT + assert result1["reason"] == "usb_error" diff --git a/tests/components/energenie_power_sockets/test_init.py b/tests/components/energenie_power_sockets/test_init.py new file mode 100644 index 00000000000..a60949c34cc --- /dev/null +++ b/tests/components/energenie_power_sockets/test_init.py @@ -0,0 +1,64 @@ +"""Tests for setting up Energenie-Power-Sockets integration.""" + +from unittest.mock import MagicMock + +from pyegps.exceptions import UsbError + +from homeassistant.components.energenie_power_sockets.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, + valid_config_entry: MockConfigEntry, + mock_get_device: MagicMock, +) -> None: + """Test loading and unloading the integration.""" + entry = valid_config_entry + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.LOADED + assert entry.entry_id in hass.data[DOMAIN] + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.NOT_LOADED + assert DOMAIN not in hass.data + + +async def test_device_not_found_on_load_entry( + hass: HomeAssistant, + valid_config_entry: MockConfigEntry, + mock_get_device: MagicMock, +) -> None: + """Test device not available on config entry setup.""" + + mock_get_device.return_value = None + + valid_config_entry.add_to_hass(hass) + assert not await hass.config_entries.async_setup(valid_config_entry.entry_id) + await hass.async_block_till_done() + + assert valid_config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_usb_error( + hass: HomeAssistant, valid_config_entry: MockConfigEntry, mock_get_device: MagicMock +) -> None: + """Test no USB access on config entry setup.""" + + mock_get_device.side_effect = UsbError + + valid_config_entry.add_to_hass(hass) + + assert not await hass.config_entries.async_setup(valid_config_entry.entry_id) + await hass.async_block_till_done() + + assert valid_config_entry.state is ConfigEntryState.SETUP_ERROR diff --git a/tests/components/energenie_power_sockets/test_switch.py b/tests/components/energenie_power_sockets/test_switch.py new file mode 100644 index 00000000000..b98a3e07f56 --- /dev/null +++ b/tests/components/energenie_power_sockets/test_switch.py @@ -0,0 +1,134 @@ +"""Test the switch functionality.""" + +from unittest.mock import MagicMock + +from pyegps.exceptions import EgpsException +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.energenie_power_sockets.const import DOMAIN +from homeassistant.components.homeassistant import ( + DOMAIN as HOME_ASSISTANT_DOMAIN, + SERVICE_UPDATE_ENTITY, +) +from homeassistant.components.switch import ( + DOMAIN as SWITCH_DOMAIN, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import STATE_OFF, STATE_ON +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_switch_on_off( + hass: HomeAssistant, entity_id: str, dev: MagicMock +) -> None: + """Call switch on/off service.""" + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {"entity_id": entity_id}, + blocking=True, + ) + assert hass.states.get(entity_id).state == STATE_ON + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {"entity_id": entity_id}, + blocking=True, + ) + + assert hass.states.get(entity_id).state == STATE_OFF + + +async def _test_switch_on_exeception( + hass: HomeAssistant, entity_id: str, dev: MagicMock +) -> None: + """Call switch on service with USBError side effect.""" + dev.switch_on.side_effect = EgpsException + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + HOME_ASSISTANT_DOMAIN, + SERVICE_TURN_ON, + {"entity_id": entity_id}, + blocking=True, + ) + dev.switch_on.side_effect = None + + +async def _test_switch_off_exeception( + hass: HomeAssistant, entity_id: str, dev: MagicMock +) -> None: + """Call switch off service with USBError side effect.""" + dev.switch_off.side_effect = EgpsException + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {"entity_id": entity_id}, + blocking=True, + ) + dev.switch_off.side_effect = None + + +async def _test_switch_update_exception( + hass: HomeAssistant, entity_id: str, dev: MagicMock +) -> None: + """Call switch update with USBError side effect.""" + dev.get_status.side_effect = EgpsException + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_UPDATE_ENTITY, + {"entity_id": entity_id}, + blocking=True, + ) + dev.get_status.side_effect = None + + +@pytest.mark.parametrize( + "entity_name", + [ + "mockedusbdevice_socket_0", + "mockedusbdevice_socket_1", + "mockedusbdevice_socket_2", + "mockedusbdevice_socket_3", + ], +) +async def test_switch_setup( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + valid_config_entry: MockConfigEntry, + mock_get_device: MagicMock, + entity_name: str, + snapshot: SnapshotAssertion, +) -> None: + """Test setup and functionality of device switches.""" + + entry = valid_config_entry + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.LOADED + assert entry.entry_id in hass.data[DOMAIN] + + state = hass.states.get(f"switch.{entity_name}") + assert state == snapshot + assert entity_registry.async_get(state.entity_id) == snapshot + + device_mock = mock_get_device.return_value + await _test_switch_on_off(hass, state.entity_id, device_mock) + await _test_switch_on_exeception(hass, state.entity_id, device_mock) + await _test_switch_off_exeception(hass, state.entity_id, device_mock) + await _test_switch_update_exception(hass, state.entity_id, device_mock) + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From 8ed03112f03c9a62c6c0183d6fdf284c9f5c2401 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexey=20ALERT=20Rubash=D1=91ff?= Date: Fri, 29 Mar 2024 15:05:18 +0200 Subject: [PATCH 069/967] Add overkiz bottom tank water temperature and core control water temperature for Atlantic Water Heater (#114186) * Adds bottom tank water temperature and core conrol water temperature sensors for Atlantic water heater * Update homeassistant/components/overkiz/sensor.py Co-authored-by: TheJulianJES * Update homeassistant/components/overkiz/sensor.py Co-authored-by: TheJulianJES --------- Co-authored-by: TheJulianJES --- homeassistant/components/overkiz/sensor.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/homeassistant/components/overkiz/sensor.py b/homeassistant/components/overkiz/sensor.py index 2b0a222f96f..c62840eea97 100644 --- a/homeassistant/components/overkiz/sensor.py +++ b/homeassistant/components/overkiz/sensor.py @@ -399,6 +399,20 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ native_unit_of_measurement=UnitOfTime.SECONDS, entity_category=EntityCategory.DIAGNOSTIC, ), + OverkizSensorDescription( + key=OverkizState.CORE_BOTTOM_TANK_WATER_TEMPERATURE, + name="Bottom tank water temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + ), + OverkizSensorDescription( + key=OverkizState.CORE_CONTROL_WATER_TARGET_TEMPERATURE, + name="Control water target temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + ), # Cover OverkizSensorDescription( key=OverkizState.CORE_TARGET_CLOSURE, From 3381469076ab5873f31caf843e14c25cca368df3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexey=20ALERT=20Rubash=D1=91ff?= Date: Fri, 29 Mar 2024 15:07:22 +0200 Subject: [PATCH 070/967] Add overkiz heating status, absence mode, and boost mode binary sensors for Atlantic Water Heater (#114184) * Adds heating status, absense mode, and boost mode binary sensors for Atlantic water heater * Renamed absence mode and boost mode binary sensors * Update homeassistant/components/overkiz/binary_sensor.py Co-authored-by: TheJulianJES * Update homeassistant/components/overkiz/binary_sensor.py Co-authored-by: TheJulianJES * Update homeassistant/components/overkiz/binary_sensor.py Co-authored-by: TheJulianJES --------- Co-authored-by: TheJulianJES --- .../components/overkiz/binary_sensor.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/homeassistant/components/overkiz/binary_sensor.py b/homeassistant/components/overkiz/binary_sensor.py index 871a70b3e0a..c37afc9cb0c 100644 --- a/homeassistant/components/overkiz/binary_sensor.py +++ b/homeassistant/components/overkiz/binary_sensor.py @@ -105,6 +105,22 @@ BINARY_SENSOR_DESCRIPTIONS: list[OverkizBinarySensorDescription] = [ ) == 1, ), + OverkizBinarySensorDescription( + key=OverkizState.CORE_HEATING_STATUS, + name="Heating status", + device_class=BinarySensorDeviceClass.HEAT, + value_fn=lambda state: state == OverkizCommandParam.ON, + ), + OverkizBinarySensorDescription( + key=OverkizState.MODBUSLINK_DHW_ABSENCE_MODE, + name="Absence mode", + value_fn=lambda state: state == OverkizCommandParam.ON, + ), + OverkizBinarySensorDescription( + key=OverkizState.MODBUSLINK_DHW_BOOST_MODE, + name="Boost mode", + value_fn=lambda state: state == OverkizCommandParam.ON, + ), ] SUPPORTED_STATES = { From 8d6d70d6b59d697d1b2b4e639d74a8769bc600c2 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Fri, 29 Mar 2024 14:36:33 +0100 Subject: [PATCH 071/967] Use `setup_test_component_platform` helper for select entity component tests instead of `hass.components` (#114412) * Use `setup_test_component_platform` helper for select entity component tests instead of `hass.components` * Use _values instead of _attr_current_option * Clean up * Set default current_option for second mock entity --------- Co-authored-by: Martin Hjelmare --- tests/components/select/common.py | 23 +++++++ tests/components/select/conftest.py | 24 +++++++ tests/components/select/test_init.py | 8 ++- .../custom_components/test/select.py | 63 ------------------- 4 files changed, 52 insertions(+), 66 deletions(-) create mode 100644 tests/components/select/common.py create mode 100644 tests/components/select/conftest.py delete mode 100644 tests/testing_config/custom_components/test/select.py diff --git a/tests/components/select/common.py b/tests/components/select/common.py new file mode 100644 index 00000000000..c2a401a038b --- /dev/null +++ b/tests/components/select/common.py @@ -0,0 +1,23 @@ +"""Common helpers for select entity component tests.""" + +from homeassistant.components.select import SelectEntity + +from tests.common import MockEntity + + +class MockSelectEntity(MockEntity, SelectEntity): + """Mock Select class.""" + + @property + def current_option(self): + """Return the current option of this select.""" + return self._handle("current_option") + + @property + def options(self) -> list: + """Return the list of available options of this select.""" + return self._handle("options") + + def select_option(self, option: str) -> None: + """Change the selected option.""" + self._values["current_option"] = option diff --git a/tests/components/select/conftest.py b/tests/components/select/conftest.py new file mode 100644 index 00000000000..700749f9aba --- /dev/null +++ b/tests/components/select/conftest.py @@ -0,0 +1,24 @@ +"""Fixtures for the select entity component tests.""" + +import pytest + +from tests.components.select.common import MockSelectEntity + + +@pytest.fixture +def mock_select_entities() -> list[MockSelectEntity]: + """Return a list of mock select entities.""" + return [ + MockSelectEntity( + name="select 1", + unique_id="unique_select_1", + options=["option 1", "option 2", "option 3"], + current_option="option 1", + ), + MockSelectEntity( + name="select 2", + unique_id="unique_select_2", + options=["option 1", "option 2", "option 3"], + current_option=None, + ), + ] diff --git a/tests/components/select/test_init.py b/tests/components/select/test_init.py index b135a6e1ab0..a5be7921fcd 100644 --- a/tests/components/select/test_init.py +++ b/tests/components/select/test_init.py @@ -21,6 +21,8 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError from homeassistant.setup import async_setup_component +from tests.common import setup_test_component_platform + class MockSelectEntity(SelectEntity): """Mock SelectEntity to use in tests.""" @@ -91,11 +93,11 @@ async def test_select(hass: HomeAssistant) -> None: async def test_custom_integration_and_validation( - hass: HomeAssistant, enable_custom_integrations: None + hass: HomeAssistant, + mock_select_entities: list[MockSelectEntity], ) -> None: """Test we can only select valid options.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_select_entities) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() diff --git a/tests/testing_config/custom_components/test/select.py b/tests/testing_config/custom_components/test/select.py deleted file mode 100644 index fece370bdf1..00000000000 --- a/tests/testing_config/custom_components/test/select.py +++ /dev/null @@ -1,63 +0,0 @@ -"""Provide a mock select platform. - -Call init before using it in your tests to ensure clean test data. -""" - -from homeassistant.components.select import SelectEntity - -from tests.common import MockEntity - -UNIQUE_SELECT_1 = "unique_select_1" -UNIQUE_SELECT_2 = "unique_select_2" - -ENTITIES = [] - - -class MockSelectEntity(MockEntity, SelectEntity): - """Mock Select class.""" - - _attr_current_option = None - - @property - def current_option(self): - """Return the current option of this select.""" - return self._handle("current_option") - - @property - def options(self) -> list: - """Return the list of available options of this select.""" - return self._handle("options") - - def select_option(self, option: str) -> None: - """Change the selected option.""" - self._attr_current_option = option - - -def init(empty=False): - """Initialize the platform with entities.""" - global ENTITIES - - ENTITIES = ( - [] - if empty - else [ - MockSelectEntity( - name="select 1", - unique_id="unique_select_1", - options=["option 1", "option 2", "option 3"], - current_option="option 1", - ), - MockSelectEntity( - name="select 2", - unique_id="unique_select_2", - options=["option 1", "option 2", "option 3"], - ), - ] - ) - - -async def async_setup_platform( - hass, config, async_add_entities_callback, discovery_info=None -): - """Return mock entities.""" - async_add_entities_callback(ENTITIES) From dc557fca1e07ed69d1c1d67b232f44a7931091f4 Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Fri, 29 Mar 2024 14:38:58 +0100 Subject: [PATCH 072/967] Refactor conversation mock_agent (#114428) * Refactor conversation mock_agent * Address review comments --- tests/components/conftest.py | 11 ++++++++ tests/components/conversation/common.py | 17 ++++++++++++ tests/components/conversation/conftest.py | 10 ------- tests/components/conversation/test_init.py | 30 ++++++++++++--------- tests/components/mobile_app/test_webhook.py | 12 ++++----- 5 files changed, 52 insertions(+), 28 deletions(-) create mode 100644 tests/components/conversation/common.py diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 09e74142ad3..958c7fe3c86 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -10,6 +10,7 @@ from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from tests.common import MockToggleEntity +from tests.components.conversation import MockAgent if TYPE_CHECKING: from tests.components.device_tracker.common import MockScanner @@ -104,6 +105,16 @@ def tts_mutagen_mock_fixture(): yield from tts_mutagen_mock_fixture_helper() +@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, + ) + + return mock_conversation_agent_fixture_helper(hass) + + @pytest.fixture(scope="session", autouse=True) def prevent_ffmpeg_subprocess() -> Generator[None, None, None]: """Prevent ffmpeg from creating a subprocess.""" diff --git a/tests/components/conversation/common.py b/tests/components/conversation/common.py new file mode 100644 index 00000000000..2fa152b1eb2 --- /dev/null +++ b/tests/components/conversation/common.py @@ -0,0 +1,17 @@ +"""Provide common tests tools for conversation.""" + +from homeassistant.components import conversation +from homeassistant.core import HomeAssistant + +from . import MockAgent + +from tests.common import MockConfigEntry + + +def mock_conversation_agent_fixture_helper(hass: HomeAssistant) -> MockAgent: + """Mock agent.""" + entry = MockConfigEntry(entry_id="mock-entry") + entry.add_to_hass(hass) + agent = MockAgent(entry.entry_id, ["smurfish"]) + conversation.async_set_agent(hass, entry, agent) + return agent diff --git a/tests/components/conversation/conftest.py b/tests/components/conversation/conftest.py index cf6b4567228..d6c2d9e2e5e 100644 --- a/tests/components/conversation/conftest.py +++ b/tests/components/conversation/conftest.py @@ -13,16 +13,6 @@ from . import MockAgent from tests.common import MockConfigEntry -@pytest.fixture -def mock_agent(hass): - """Mock agent.""" - entry = MockConfigEntry(entry_id="mock-entry") - entry.add_to_hass(hass) - agent = MockAgent(entry.entry_id, ["smurfish"]) - conversation.async_set_agent(hass, entry, agent) - return agent - - @pytest.fixture def mock_agent_support_all(hass): """Mock agent that supports all languages.""" diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index 1ef8c8b30d7..7b2c44a755d 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -94,7 +94,7 @@ async def test_http_processing_intent_target_ha_agent( init_components, hass_client: ClientSessionGenerator, hass_admin_user: MockUser, - mock_agent, + mock_conversation_agent, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, ) -> None: @@ -658,7 +658,7 @@ async def test_custom_agent( hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_admin_user: MockUser, - mock_agent, + mock_conversation_agent, snapshot: SnapshotAssertion, ) -> None: """Test a custom conversation agent.""" @@ -672,7 +672,7 @@ async def test_custom_agent( "text": "Test Text", "conversation_id": "test-conv-id", "language": "test-language", - "agent_id": mock_agent.agent_id, + "agent_id": mock_conversation_agent.agent_id, } resp = await client.post("/api/conversation/process", json=data) @@ -683,14 +683,14 @@ async def test_custom_agent( assert data["response"]["speech"]["plain"]["speech"] == "Test response" assert data["conversation_id"] == "test-conv-id" - assert len(mock_agent.calls) == 1 - assert mock_agent.calls[0].text == "Test Text" - assert mock_agent.calls[0].context.user_id == hass_admin_user.id - assert mock_agent.calls[0].conversation_id == "test-conv-id" - assert mock_agent.calls[0].language == "test-language" + assert len(mock_conversation_agent.calls) == 1 + assert mock_conversation_agent.calls[0].text == "Test Text" + assert mock_conversation_agent.calls[0].context.user_id == hass_admin_user.id + assert mock_conversation_agent.calls[0].conversation_id == "test-conv-id" + assert mock_conversation_agent.calls[0].language == "test-language" conversation.async_unset_agent( - hass, hass.config_entries.async_get_entry(mock_agent.agent_id) + hass, hass.config_entries.async_get_entry(mock_conversation_agent.agent_id) ) @@ -1072,7 +1072,7 @@ async def test_agent_id_validator_invalid_agent(hass: HomeAssistant) -> None: async def test_get_agent_list( hass: HomeAssistant, init_components, - mock_agent, + mock_conversation_agent, mock_agent_support_all, hass_ws_client: WebSocketGenerator, snapshot: SnapshotAssertion, @@ -1128,14 +1128,20 @@ async def test_get_agent_list( async def test_get_agent_info( - hass: HomeAssistant, init_components, mock_agent, snapshot: SnapshotAssertion + hass: HomeAssistant, + init_components, + mock_conversation_agent, + snapshot: SnapshotAssertion, ) -> None: """Test get agent info.""" agent_info = conversation.async_get_agent_info(hass) # Test it's the default assert conversation.async_get_agent_info(hass, "homeassistant") == agent_info assert conversation.async_get_agent_info(hass, "homeassistant") == snapshot - assert conversation.async_get_agent_info(hass, mock_agent.agent_id) == snapshot + assert ( + conversation.async_get_agent_info(hass, mock_conversation_agent.agent_id) + == snapshot + ) assert conversation.async_get_agent_info(hass, "not exist") is None # Test the name when config entry title is empty diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index 4a5f472221f..dfab474f127 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -24,10 +24,6 @@ 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.conftest import mock_agent - -# To avoid autoflake8 removing the import -mock_agent = mock_agent @pytest.fixture @@ -1027,14 +1023,18 @@ async def test_reregister_sensor( async def test_webhook_handle_conversation_process( - hass: HomeAssistant, homeassistant, create_registrations, webhook_client, mock_agent + hass: HomeAssistant, + homeassistant, + create_registrations, + webhook_client, + mock_conversation_agent, ) -> None: """Test that we can converse.""" webhook_client.server.app.router._frozen = False with patch( "homeassistant.components.conversation.AgentManager.async_get_agent", - return_value=mock_agent, + return_value=mock_conversation_agent, ): resp = await webhook_client.post( "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), From 5b4452a57961e31a5c0431c1da23dfbca1c48c5d Mon Sep 17 00:00:00 2001 From: Jeremy TRUFIER Date: Fri, 29 Mar 2024 14:51:44 +0100 Subject: [PATCH 073/967] Follow real AtlanticPassAPCZoneControlZone physical mode on Overkiz (HEAT, COOL or HEAT_COOL) (#111830) * Support HEAT_COOL when mode is Auto on overkiz AtlanticPassAPCZoneControlZone * Refactor ZoneControlZone to simplify usic by only using a single hvac mode * Fix linting issues * Makes more sense to use halves there * Fix PR feedback --- homeassistant/components/overkiz/climate.py | 25 +- .../atlantic_pass_apc_heating_zone.py | 4 +- .../atlantic_pass_apc_zone_control_zone.py | 394 ++++++++++++++---- 3 files changed, 325 insertions(+), 98 deletions(-) diff --git a/homeassistant/components/overkiz/climate.py b/homeassistant/components/overkiz/climate.py index e23403c2162..b569d05d2d7 100644 --- a/homeassistant/components/overkiz/climate.py +++ b/homeassistant/components/overkiz/climate.py @@ -7,6 +7,7 @@ from typing import cast from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HomeAssistantOverkizData @@ -27,15 +28,16 @@ async def async_setup_entry( """Set up the Overkiz climate from a config entry.""" data: HomeAssistantOverkizData = hass.data[DOMAIN][entry.entry_id] - async_add_entities( + # Match devices based on the widget. + entities_based_on_widget: list[Entity] = [ WIDGET_TO_CLIMATE_ENTITY[device.widget](device.device_url, data.coordinator) for device in data.platforms[Platform.CLIMATE] if device.widget in WIDGET_TO_CLIMATE_ENTITY - ) + ] - # Match devices based on the widget and controllableName - # This is for example used for Atlantic APC, where devices with different functionality share the same uiClass and widget. - async_add_entities( + # Match devices based on the widget and controllableName. + # ie Atlantic APC + entities_based_on_widget_and_controllable: list[Entity] = [ WIDGET_AND_CONTROLLABLE_TO_CLIMATE_ENTITY[device.widget][ cast(Controllable, device.controllable_name) ](device.device_url, data.coordinator) @@ -43,14 +45,21 @@ async def async_setup_entry( if device.widget in WIDGET_AND_CONTROLLABLE_TO_CLIMATE_ENTITY and device.controllable_name in WIDGET_AND_CONTROLLABLE_TO_CLIMATE_ENTITY[device.widget] - ) + ] - # Hitachi Air To Air Heat Pumps - async_add_entities( + # Match devices based on the widget and protocol. + # #ie Hitachi Air To Air Heat Pumps + entities_based_on_widget_and_protocol: list[Entity] = [ WIDGET_AND_PROTOCOL_TO_CLIMATE_ENTITY[device.widget][device.protocol]( device.device_url, data.coordinator ) for device in data.platforms[Platform.CLIMATE] if device.widget in WIDGET_AND_PROTOCOL_TO_CLIMATE_ENTITY and device.protocol in WIDGET_AND_PROTOCOL_TO_CLIMATE_ENTITY[device.widget] + ] + + async_add_entities( + entities_based_on_widget + + entities_based_on_widget_and_controllable + + entities_based_on_widget_and_protocol ) diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heating_zone.py b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heating_zone.py index bf6aa43644e..3da2ccc922b 100644 --- a/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heating_zone.py +++ b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heating_zone.py @@ -159,7 +159,7 @@ class AtlanticPassAPCHeatingZone(OverkizEntity, ClimateEntity): await self.async_set_heating_mode(PRESET_MODES_TO_OVERKIZ[preset_mode]) @property - def preset_mode(self) -> str: + def preset_mode(self) -> str | None: """Return the current preset mode, e.g., home, away, temp.""" heating_mode = cast( str, self.executor.select_state(OverkizState.IO_PASS_APC_HEATING_MODE) @@ -179,7 +179,7 @@ class AtlanticPassAPCHeatingZone(OverkizEntity, ClimateEntity): return OVERKIZ_TO_PRESET_MODES[heating_mode] @property - def target_temperature(self) -> float: + def target_temperature(self) -> float | None: """Return hvac target temperature.""" current_heating_profile = self.current_heating_profile if current_heating_profile in OVERKIZ_TEMPERATURE_STATE_BY_PROFILE: diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control_zone.py b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control_zone.py index 261acc2838c..f18edd0cfe6 100644 --- a/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control_zone.py +++ b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control_zone.py @@ -3,16 +3,24 @@ from __future__ import annotations from asyncio import sleep +from functools import cached_property from typing import Any, cast from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState -from homeassistant.components.climate import PRESET_NONE, HVACMode -from homeassistant.const import ATTR_TEMPERATURE +from homeassistant.components.climate import ( + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + PRESET_NONE, + ClimateEntityFeature, + HVACAction, + HVACMode, +) +from homeassistant.const import ATTR_TEMPERATURE, PRECISION_HALVES from ..coordinator import OverkizDataUpdateCoordinator +from ..executor import OverkizExecutor from .atlantic_pass_apc_heating_zone import AtlanticPassAPCHeatingZone -from .atlantic_pass_apc_zone_control import OVERKIZ_TO_HVAC_MODE PRESET_SCHEDULE = "schedule" PRESET_MANUAL = "manual" @@ -24,32 +32,127 @@ OVERKIZ_MODE_TO_PRESET_MODES: dict[str, str] = { PRESET_MODES_TO_OVERKIZ = {v: k for k, v in OVERKIZ_MODE_TO_PRESET_MODES.items()} -TEMPERATURE_ZONECONTROL_DEVICE_INDEX = 20 +# Maps the HVAC current ZoneControl system operating mode. +OVERKIZ_TO_HVAC_ACTION: dict[str, HVACAction] = { + OverkizCommandParam.COOLING: HVACAction.COOLING, + OverkizCommandParam.DRYING: HVACAction.DRYING, + OverkizCommandParam.HEATING: HVACAction.HEATING, + # There is no known way to differentiate OFF from Idle. + OverkizCommandParam.STOP: HVACAction.OFF, +} + +HVAC_ACTION_TO_OVERKIZ_PROFILE_STATE: dict[HVACAction, OverkizState] = { + HVACAction.COOLING: OverkizState.IO_PASS_APC_COOLING_PROFILE, + HVACAction.HEATING: OverkizState.IO_PASS_APC_HEATING_PROFILE, +} + +HVAC_ACTION_TO_OVERKIZ_MODE_STATE: dict[HVACAction, OverkizState] = { + HVACAction.COOLING: OverkizState.IO_PASS_APC_COOLING_MODE, + HVACAction.HEATING: OverkizState.IO_PASS_APC_HEATING_MODE, +} + +TEMPERATURE_ZONECONTROL_DEVICE_INDEX = 1 + +SUPPORTED_FEATURES: ClimateEntityFeature = ( + ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON +) + +OVERKIZ_THERMAL_CONFIGURATION_TO_HVAC_MODE: dict[ + OverkizCommandParam, tuple[HVACMode, ClimateEntityFeature] +] = { + OverkizCommandParam.COOLING: ( + HVACMode.COOL, + SUPPORTED_FEATURES | ClimateEntityFeature.TARGET_TEMPERATURE, + ), + OverkizCommandParam.HEATING: ( + HVACMode.HEAT, + SUPPORTED_FEATURES | ClimateEntityFeature.TARGET_TEMPERATURE, + ), + OverkizCommandParam.HEATING_AND_COOLING: ( + HVACMode.HEAT_COOL, + SUPPORTED_FEATURES | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE, + ), +} -# Those device depends on a main probe that choose the operating mode (heating, cooling, ...) +# Those device depends on a main probe that choose the operating mode (heating, cooling, ...). class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone): """Representation of Atlantic Pass APC Heating And Cooling Zone Control.""" + _attr_target_temperature_step = PRECISION_HALVES + def __init__( self, device_url: str, coordinator: OverkizDataUpdateCoordinator ) -> None: """Init method.""" super().__init__(device_url, coordinator) - # There is less supported functions, because they depend on the ZoneControl. - if not self.is_using_derogated_temperature_fallback: - # Modes are not configurable, they will follow current HVAC Mode of Zone Control. - self._attr_hvac_modes = [] + # When using derogated temperature, we fallback to legacy behavior. + if self.is_using_derogated_temperature_fallback: + return - # Those are available and tested presets on Shogun. - self._attr_preset_modes = [*PRESET_MODES_TO_OVERKIZ] + self._attr_hvac_modes = [] + self._attr_supported_features = ClimateEntityFeature(0) + + # Modes depends on device capabilities. + if (thermal_configuration := self.thermal_configuration) is not None: + ( + device_hvac_mode, + climate_entity_feature, + ) = thermal_configuration + self._attr_hvac_modes = [device_hvac_mode, HVACMode.OFF] + self._attr_supported_features = climate_entity_feature + + # Those are available and tested presets on Shogun. + self._attr_preset_modes = [*PRESET_MODES_TO_OVERKIZ] # Those APC Heating and Cooling probes depends on the zone control device (main probe). # Only the base device (#1) can be used to get/set some states. # Like to retrieve and set the current operating mode (heating, cooling, drying, off). - self.zone_control_device = self.executor.linked_device( - TEMPERATURE_ZONECONTROL_DEVICE_INDEX + + self.zone_control_executor: OverkizExecutor | None = None + + if ( + zone_control_device := self.executor.linked_device( + TEMPERATURE_ZONECONTROL_DEVICE_INDEX + ) + ) is not None: + self.zone_control_executor = OverkizExecutor( + zone_control_device.device_url, + coordinator, + ) + + @cached_property + def thermal_configuration(self) -> tuple[HVACMode, ClimateEntityFeature] | None: + """Retrieve thermal configuration for this devices.""" + + if ( + ( + state_thermal_configuration := cast( + OverkizCommandParam | None, + self.executor.select_state(OverkizState.CORE_THERMAL_CONFIGURATION), + ) + ) + is not None + and state_thermal_configuration + in OVERKIZ_THERMAL_CONFIGURATION_TO_HVAC_MODE + ): + return OVERKIZ_THERMAL_CONFIGURATION_TO_HVAC_MODE[ + state_thermal_configuration + ] + + return None + + @cached_property + def device_hvac_mode(self) -> HVACMode | None: + """ZoneControlZone device has a single possible mode.""" + + return ( + None + if self.thermal_configuration is None + else self.thermal_configuration[0] ) @property @@ -61,21 +164,37 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone): ) @property - def zone_control_hvac_mode(self) -> HVACMode: + def zone_control_hvac_action(self) -> HVACAction: """Return hvac operation ie. heat, cool, dry, off mode.""" - if ( - self.zone_control_device is not None - and ( - state := self.zone_control_device.states[ + if self.zone_control_executor is not None and ( + ( + state := self.zone_control_executor.select_state( OverkizState.IO_PASS_APC_OPERATING_MODE - ] + ) ) is not None - and (value := state.value_as_str) is not None ): - return OVERKIZ_TO_HVAC_MODE[value] - return HVACMode.OFF + return OVERKIZ_TO_HVAC_ACTION[cast(str, state)] + + return HVACAction.OFF + + @property + def hvac_action(self) -> HVACAction | None: + """Return the current running hvac operation.""" + + # When ZoneControl action is heating/cooling but Zone is stopped, means the zone is idle. + if ( + hvac_action := self.zone_control_hvac_action + ) in HVAC_ACTION_TO_OVERKIZ_PROFILE_STATE and cast( + str, + self.executor.select_state( + HVAC_ACTION_TO_OVERKIZ_PROFILE_STATE[hvac_action] + ), + ) == OverkizCommandParam.STOP: + return HVACAction.IDLE + + return hvac_action @property def hvac_mode(self) -> HVACMode: @@ -84,30 +203,32 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone): if self.is_using_derogated_temperature_fallback: return super().hvac_mode - zone_control_hvac_mode = self.zone_control_hvac_mode + if (device_hvac_mode := self.device_hvac_mode) is None: + return HVACMode.OFF - # Should be same, because either thermostat or this integration change both. - on_off_state = cast( + cooling_is_off = cast( str, - self.executor.select_state( - OverkizState.CORE_COOLING_ON_OFF - if zone_control_hvac_mode == HVACMode.COOL - else OverkizState.CORE_HEATING_ON_OFF - ), - ) + self.executor.select_state(OverkizState.CORE_COOLING_ON_OFF), + ) in (OverkizCommandParam.OFF, None) + + heating_is_off = cast( + str, + self.executor.select_state(OverkizState.CORE_HEATING_ON_OFF), + ) in (OverkizCommandParam.OFF, None) # Device is Stopped, it means the air flux is flowing but its venting door is closed. - if on_off_state == OverkizCommandParam.OFF: - hvac_mode = HVACMode.OFF - else: - hvac_mode = zone_control_hvac_mode + if ( + (device_hvac_mode == HVACMode.COOL and cooling_is_off) + or (device_hvac_mode == HVACMode.HEAT and heating_is_off) + or ( + device_hvac_mode == HVACMode.HEAT_COOL + and cooling_is_off + and heating_is_off + ) + ): + return HVACMode.OFF - # It helps keep it consistent with the Zone Control, within the interface. - if self._attr_hvac_modes != [zone_control_hvac_mode, HVACMode.OFF]: - self._attr_hvac_modes = [zone_control_hvac_mode, HVACMode.OFF] - self.async_write_ha_state() - - return hvac_mode + return device_hvac_mode async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target hvac mode.""" @@ -118,46 +239,49 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone): # They are mainly managed by the Zone Control device # However, it make sense to map the OFF Mode to the Overkiz STOP Preset - if hvac_mode == HVACMode.OFF: - await self.executor.async_execute_command( - OverkizCommand.SET_COOLING_ON_OFF, - OverkizCommandParam.OFF, - ) - await self.executor.async_execute_command( - OverkizCommand.SET_HEATING_ON_OFF, - OverkizCommandParam.OFF, - ) - else: - await self.executor.async_execute_command( - OverkizCommand.SET_COOLING_ON_OFF, - OverkizCommandParam.ON, - ) - await self.executor.async_execute_command( - OverkizCommand.SET_HEATING_ON_OFF, - OverkizCommandParam.ON, - ) + on_off_target_command_param = ( + OverkizCommandParam.OFF + if hvac_mode == HVACMode.OFF + else OverkizCommandParam.ON + ) + + await self.executor.async_execute_command( + OverkizCommand.SET_COOLING_ON_OFF, + on_off_target_command_param, + ) + await self.executor.async_execute_command( + OverkizCommand.SET_HEATING_ON_OFF, + on_off_target_command_param, + ) await self.async_refresh_modes() @property - def preset_mode(self) -> str: + def preset_mode(self) -> str | None: """Return the current preset mode, e.g., schedule, manual.""" if self.is_using_derogated_temperature_fallback: return super().preset_mode - mode = OVERKIZ_MODE_TO_PRESET_MODES[ - cast( - str, - self.executor.select_state( - OverkizState.IO_PASS_APC_COOLING_MODE - if self.zone_control_hvac_mode == HVACMode.COOL - else OverkizState.IO_PASS_APC_HEATING_MODE - ), + if ( + self.zone_control_hvac_action in HVAC_ACTION_TO_OVERKIZ_MODE_STATE + and ( + mode_state := HVAC_ACTION_TO_OVERKIZ_MODE_STATE[ + self.zone_control_hvac_action + ] ) - ] + and ( + ( + mode := OVERKIZ_MODE_TO_PRESET_MODES[ + cast(str, self.executor.select_state(mode_state)) + ] + ) + is not None + ) + ): + return mode - return mode if mode is not None else PRESET_NONE + return PRESET_NONE async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" @@ -178,13 +302,18 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone): await self.async_refresh_modes() @property - def target_temperature(self) -> float: + def target_temperature(self) -> float | None: """Return hvac target temperature.""" if self.is_using_derogated_temperature_fallback: return super().target_temperature - if self.zone_control_hvac_mode == HVACMode.COOL: + device_hvac_mode = self.device_hvac_mode + + if device_hvac_mode == HVACMode.HEAT_COOL: + return None + + if device_hvac_mode == HVACMode.COOL: return cast( float, self.executor.select_state( @@ -192,7 +321,7 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone): ), ) - if self.zone_control_hvac_mode == HVACMode.HEAT: + if device_hvac_mode == HVACMode.HEAT: return cast( float, self.executor.select_state( @@ -204,32 +333,73 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone): float, self.executor.select_state(OverkizState.CORE_TARGET_TEMPERATURE) ) + @property + def target_temperature_high(self) -> float | None: + """Return the highbound target temperature we try to reach (cooling).""" + + if self.device_hvac_mode != HVACMode.HEAT_COOL: + return None + + return cast( + float, + self.executor.select_state(OverkizState.CORE_COOLING_TARGET_TEMPERATURE), + ) + + @property + def target_temperature_low(self) -> float | None: + """Return the lowbound target temperature we try to reach (heating).""" + + if self.device_hvac_mode != HVACMode.HEAT_COOL: + return None + + return cast( + float, + self.executor.select_state(OverkizState.CORE_HEATING_TARGET_TEMPERATURE), + ) + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new temperature.""" if self.is_using_derogated_temperature_fallback: return await super().async_set_temperature(**kwargs) - temperature = kwargs[ATTR_TEMPERATURE] + target_temperature = kwargs.get(ATTR_TEMPERATURE) + target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) + target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) + hvac_mode = self.hvac_mode + + if hvac_mode == HVACMode.HEAT_COOL: + if target_temp_low is not None: + await self.executor.async_execute_command( + OverkizCommand.SET_HEATING_TARGET_TEMPERATURE, + target_temp_low, + ) + + if target_temp_high is not None: + await self.executor.async_execute_command( + OverkizCommand.SET_COOLING_TARGET_TEMPERATURE, + target_temp_high, + ) + + elif target_temperature is not None: + if hvac_mode == HVACMode.HEAT: + await self.executor.async_execute_command( + OverkizCommand.SET_HEATING_TARGET_TEMPERATURE, + target_temperature, + ) + + elif hvac_mode == HVACMode.COOL: + await self.executor.async_execute_command( + OverkizCommand.SET_COOLING_TARGET_TEMPERATURE, + target_temperature, + ) - # Change both (heating/cooling) temperature is a good way to have consistency - await self.executor.async_execute_command( - OverkizCommand.SET_HEATING_TARGET_TEMPERATURE, - temperature, - ) - await self.executor.async_execute_command( - OverkizCommand.SET_COOLING_TARGET_TEMPERATURE, - temperature, - ) await self.executor.async_execute_command( OverkizCommand.SET_DEROGATION_ON_OFF_STATE, - OverkizCommandParam.OFF, + OverkizCommandParam.ON, ) - # Target temperature may take up to 1 minute to get refreshed. - await self.executor.async_execute_command( - OverkizCommand.REFRESH_TARGET_TEMPERATURE - ) + await self.async_refresh_modes() async def async_refresh_modes(self) -> None: """Refresh the device modes to have new states.""" @@ -256,3 +426,51 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone): await self.executor.async_execute_command( OverkizCommand.REFRESH_TARGET_TEMPERATURE ) + + @property + def min_temp(self) -> float: + """Return Minimum Temperature for AC of this group.""" + + device_hvac_mode = self.device_hvac_mode + + if device_hvac_mode in (HVACMode.HEAT, HVACMode.HEAT_COOL): + return cast( + float, + self.executor.select_state( + OverkizState.CORE_MINIMUM_HEATING_TARGET_TEMPERATURE + ), + ) + + if device_hvac_mode == HVACMode.COOL: + return cast( + float, + self.executor.select_state( + OverkizState.CORE_MINIMUM_COOLING_TARGET_TEMPERATURE + ), + ) + + return super().min_temp + + @property + def max_temp(self) -> float: + """Return Max Temperature for AC of this group.""" + + device_hvac_mode = self.device_hvac_mode + + if device_hvac_mode == HVACMode.HEAT: + return cast( + float, + self.executor.select_state( + OverkizState.CORE_MAXIMUM_HEATING_TARGET_TEMPERATURE + ), + ) + + if device_hvac_mode in (HVACMode.COOL, HVACMode.HEAT_COOL): + return cast( + float, + self.executor.select_state( + OverkizState.CORE_MAXIMUM_COOLING_TARGET_TEMPERATURE + ), + ) + + return super().max_temp From 54c5f18aac1a6df29fbb8b0ca29095932abb2b32 Mon Sep 17 00:00:00 2001 From: Luca Angemi Date: Fri, 29 Mar 2024 14:04:24 +0000 Subject: [PATCH 074/967] Add `uid` attribute to `imap_content` event data (#114432) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add uid to imap event * Add ´uid´ to tests * Update test_init.py --- homeassistant/components/imap/coordinator.py | 1 + tests/components/imap/test_init.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/homeassistant/components/imap/coordinator.py b/homeassistant/components/imap/coordinator.py index 78b52e06db3..f7f2ef457c7 100644 --- a/homeassistant/components/imap/coordinator.py +++ b/homeassistant/components/imap/coordinator.py @@ -264,6 +264,7 @@ class ImapDataUpdateCoordinator(DataUpdateCoordinator[int | None]): "sender": message.sender, "subject": message.subject, "headers": message.headers, + "uid": last_message_uid, } if self.custom_event_template is not None: try: diff --git a/tests/components/imap/test_init.py b/tests/components/imap/test_init.py index 8608963413a..aba9bd88c44 100644 --- a/tests/components/imap/test_init.py +++ b/tests/components/imap/test_init.py @@ -164,6 +164,7 @@ async def test_receiving_message_successfully( assert data["folder"] == "INBOX" assert data["sender"] == "john.doe@example.com" assert data["subject"] == "Test subject" + assert data["uid"] == "1" assert "Test body" in data["text"] assert ( valid_date @@ -213,6 +214,7 @@ async def test_receiving_message_with_invalid_encoding( assert data["sender"] == "john.doe@example.com" assert data["subject"] == "Test subject" assert data["text"] == TEST_BADLY_ENCODED_CONTENT + assert data["uid"] == "1" @pytest.mark.parametrize("imap_search", [TEST_SEARCH_RESPONSE]) @@ -251,6 +253,7 @@ async def test_receiving_message_no_subject_to_from( assert data["text"] == "Test body\r\n" assert data["headers"]["Return-Path"] == ("",) assert data["headers"]["Delivered-To"] == ("notify@example.com",) + assert data["uid"] == "1" @pytest.mark.parametrize("imap_has_capability", [True, False], ids=["push", "poll"]) From 5e3ce804887d7fed63da5d6822a7a620c323c375 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 29 Mar 2024 16:01:50 +0100 Subject: [PATCH 075/967] Remove stale test for mqtt climate (#114443) --- tests/components/mqtt/test_climate.py | 68 --------------------------- 1 file changed, 68 deletions(-) diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 1224fce098d..821a3f911b7 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -148,74 +148,6 @@ async def test_preset_none_in_preset_modes( assert "preset_modes must not include preset mode 'none'" in caplog.text -@pytest.mark.parametrize( - ("hass_config", "parameter"), - [ - ( - help_custom_config( - climate.DOMAIN, - DEFAULT_CONFIG, - ({"away_mode_command_topic": "away-mode-command-topic"},), - ), - "away_mode_command_topic", - ), - ( - help_custom_config( - climate.DOMAIN, - DEFAULT_CONFIG, - ({"away_mode_state_topic": "away-mode-state-topic"},), - ), - "away_mode_state_topic", - ), - ( - help_custom_config( - climate.DOMAIN, - DEFAULT_CONFIG, - ({"away_mode_state_template": "{{ value_json }}"},), - ), - "away_mode_state_template", - ), - ( - help_custom_config( - climate.DOMAIN, - DEFAULT_CONFIG, - ({"hold_mode_command_topic": "hold-mode-command-topic"},), - ), - "hold_mode_command_topic", - ), - ( - help_custom_config( - climate.DOMAIN, - DEFAULT_CONFIG, - ({"hold_mode_command_template": "hold-mode-command-template"},), - ), - "hold_mode_command_template", - ), - ( - help_custom_config( - climate.DOMAIN, - DEFAULT_CONFIG, - ({"hold_mode_state_topic": "hold-mode-state-topic"},), - ), - "hold_mode_state_topic", - ), - ( - help_custom_config( - climate.DOMAIN, - DEFAULT_CONFIG, - ({"hold_mode_state_template": "{{ value_json }}"},), - ), - "hold_mode_state_template", - ), - ], -) -async def test_preset_modes_deprecation_guard( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, parameter: str -) -> None: - """Test the configuration for invalid legacy parameters.""" - assert f"[{parameter}] is an invalid option for [mqtt]. Check: mqtt->mqtt->climate->0->{parameter}" - - @pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) async def test_supported_features( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator From 63ccdcb78685ba74fb2ac24ec4743544f7afd476 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 29 Mar 2024 05:20:26 -1000 Subject: [PATCH 076/967] Avoid concurrent radio operations with powerview hubs (#114399) Co-authored-by: kingy444 --- .../components/hunterdouglas_powerview/button.py | 3 ++- .../components/hunterdouglas_powerview/coordinator.py | 5 +++++ .../components/hunterdouglas_powerview/cover.py | 11 ++++++++--- .../components/hunterdouglas_powerview/select.py | 3 ++- .../components/hunterdouglas_powerview/sensor.py | 3 ++- 5 files changed, 19 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/hunterdouglas_powerview/button.py b/homeassistant/components/hunterdouglas_powerview/button.py index f7c90f3420b..ecb71f9653a 100644 --- a/homeassistant/components/hunterdouglas_powerview/button.py +++ b/homeassistant/components/hunterdouglas_powerview/button.py @@ -119,4 +119,5 @@ class PowerviewShadeButton(ShadeEntity, ButtonEntity): async def async_press(self) -> None: """Handle the button press.""" - await self.entity_description.press_action(self._shade) + async with self.coordinator.radio_operation_lock: + await self.entity_description.press_action(self._shade) diff --git a/homeassistant/components/hunterdouglas_powerview/coordinator.py b/homeassistant/components/hunterdouglas_powerview/coordinator.py index 1ea47ca9d1f..f074b06b2bc 100644 --- a/homeassistant/components/hunterdouglas_powerview/coordinator.py +++ b/homeassistant/components/hunterdouglas_powerview/coordinator.py @@ -2,6 +2,7 @@ from __future__ import annotations +import asyncio from datetime import timedelta import logging @@ -25,6 +26,10 @@ class PowerviewShadeUpdateCoordinator(DataUpdateCoordinator[PowerviewShadeData]) """Initialize DataUpdateCoordinator to gather data for specific Powerview Hub.""" self.shades = shades self.hub = hub + # The hub tends to crash if there are multiple radio operations at the same time + # but it seems to handle all other requests that do not use RF without issue + # so we have a lock to prevent multiple radio operations at the same time + self.radio_operation_lock = asyncio.Lock() super().__init__( hass, _LOGGER, diff --git a/homeassistant/components/hunterdouglas_powerview/cover.py b/homeassistant/components/hunterdouglas_powerview/cover.py index 453d5c4e920..57409f37ac9 100644 --- a/homeassistant/components/hunterdouglas_powerview/cover.py +++ b/homeassistant/components/hunterdouglas_powerview/cover.py @@ -67,7 +67,8 @@ async def async_setup_entry( for shade in pv_entry.shade_data.values(): _LOGGER.debug("Initial refresh of shade: %s", shade.name) - await shade.refresh(suppress_timeout=True) # default 15 second timeout + async with coordinator.radio_operation_lock: + await shade.refresh(suppress_timeout=True) # default 15 second timeout entities: list[ShadeEntity] = [] for shade in pv_entry.shade_data.values(): @@ -207,7 +208,8 @@ class PowerViewShadeBase(ShadeEntity, CoverEntity): async def _async_execute_move(self, move: ShadePosition) -> None: """Execute a move that can affect multiple positions.""" _LOGGER.debug("Move request %s: %s", self.name, move) - response = await self._shade.move(move) + async with self.coordinator.radio_operation_lock: + response = await self._shade.move(move) _LOGGER.debug("Move response %s: %s", self.name, response) # Process the response from the hub (including new positions) @@ -318,7 +320,10 @@ class PowerViewShadeBase(ShadeEntity, CoverEntity): # error if are already have one in flight return # suppress timeouts caused by hub nightly reboot - await self._shade.refresh(suppress_timeout=True) # default 15 second timeout + async with self.coordinator.radio_operation_lock: + await self._shade.refresh( + suppress_timeout=True + ) # default 15 second timeout _LOGGER.debug("Process update %s: %s", self.name, self._shade.current_position) self._async_update_shade_data(self._shade.current_position) diff --git a/homeassistant/components/hunterdouglas_powerview/select.py b/homeassistant/components/hunterdouglas_powerview/select.py index 66207f6da7c..f1e9c491659 100644 --- a/homeassistant/components/hunterdouglas_powerview/select.py +++ b/homeassistant/components/hunterdouglas_powerview/select.py @@ -114,5 +114,6 @@ class PowerViewSelect(ShadeEntity, SelectEntity): """Change the selected option.""" await self.entity_description.select_fn(self._shade, option) # force update data to ensure new info is in coordinator - await self._shade.refresh() + async with self.coordinator.radio_operation_lock: + await self._shade.refresh(suppress_timeout=True) self.async_write_ha_state() diff --git a/homeassistant/components/hunterdouglas_powerview/sensor.py b/homeassistant/components/hunterdouglas_powerview/sensor.py index bca87189e56..b24193ac438 100644 --- a/homeassistant/components/hunterdouglas_powerview/sensor.py +++ b/homeassistant/components/hunterdouglas_powerview/sensor.py @@ -153,5 +153,6 @@ class PowerViewSensor(ShadeEntity, SensorEntity): async def async_update(self) -> None: """Refresh sensor entity.""" - await self.entity_description.update_fn(self._shade) + async with self.coordinator.radio_operation_lock: + await self.entity_description.update_fn(self._shade) self.async_write_ha_state() From 60d545e5f9d013f3c3b230e2969c20ff912fcefa Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 29 Mar 2024 16:20:43 +0100 Subject: [PATCH 077/967] Log warnings in Renault initialisation (#114445) --- homeassistant/components/renault/renault_vehicle.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/renault/renault_vehicle.py b/homeassistant/components/renault/renault_vehicle.py index 55a5574a444..59e1826ce1b 100644 --- a/homeassistant/components/renault/renault_vehicle.py +++ b/homeassistant/components/renault/renault_vehicle.py @@ -125,16 +125,16 @@ class RenaultVehicleProxy: coordinator = self.coordinators[key] if coordinator.not_supported: # Remove endpoint as it is not supported for this vehicle. - LOGGER.info( - "Ignoring endpoint %s as it is not supported for this vehicle: %s", + LOGGER.warning( + "Ignoring endpoint %s as it is not supported: %s", coordinator.name, coordinator.last_exception, ) del self.coordinators[key] elif coordinator.access_denied: # Remove endpoint as it is denied for this vehicle. - LOGGER.info( - "Ignoring endpoint %s as it is denied for this vehicle: %s", + LOGGER.warning( + "Ignoring endpoint %s as it is denied: %s", coordinator.name, coordinator.last_exception, ) From 87f4b1414f03c88f88c488a2dd612a4cb843a5bf Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Fri, 29 Mar 2024 17:46:21 +0100 Subject: [PATCH 078/967] Bump pyoverkiz to 1.13.9 (#114442) --- 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 db24a299f2a..2ef0f0ebef4 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.8"], + "requirements": ["pyoverkiz==1.13.9"], "zeroconf": [ { "type": "_kizbox._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index aba5ae375e0..7343b5fa4af 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2042,7 +2042,7 @@ pyotgw==2.1.3 pyotp==2.8.0 # homeassistant.components.overkiz -pyoverkiz==1.13.8 +pyoverkiz==1.13.9 # homeassistant.components.openweathermap pyowm==3.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f672c943803..377dacdd327 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1590,7 +1590,7 @@ pyotgw==2.1.3 pyotp==2.8.0 # homeassistant.components.overkiz -pyoverkiz==1.13.8 +pyoverkiz==1.13.9 # homeassistant.components.openweathermap pyowm==3.2.0 From eb0aa6bb820806e9029ac98c80a0777ce32701da Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Fri, 29 Mar 2024 19:08:07 +0100 Subject: [PATCH 079/967] Bump async-upnp-client to 0.38.3 (#114447) --- 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/manifest.json | 2 +- homeassistant/components/yeelight/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 128822cf289..41fa49f1a94 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.2", "getmac==0.9.4"], + "requirements": ["async-upnp-client==0.38.3", "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 aaa6e1ee7de..c87e5e87779 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.2"], + "requirements": ["async-upnp-client==0.38.3"], "ssdp": [ { "deviceType": "urn:schemas-upnp-org:device:MediaServer:1", diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index 00b8fec8e6a..460e191828e 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.2" + "async-upnp-client==0.38.3" ], "ssdp": [ { diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index a9ef8af8c90..5e549c31806 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.2"] + "requirements": ["async-upnp-client==0.38.3"] } diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index edfde84a2ac..7d353a475c7 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.2", "getmac==0.9.4"], + "requirements": ["async-upnp-client==0.38.3", "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 20f8ed3ed4d..e9f304d38cb 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.2"], + "requirements": ["yeelight==0.7.14", "async-upnp-client==0.38.3"], "zeroconf": [ { "type": "_miio._udp.local.", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2386845a2ad..90c256006fa 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -8,7 +8,7 @@ aiohttp==3.9.3 aiohttp_cors==0.7.0 astral==2.2 async-interrupt==1.1.1 -async-upnp-client==0.38.2 +async-upnp-client==0.38.3 atomicwrites-homeassistant==1.4.1 attrs==23.2.0 awesomeversion==24.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 7343b5fa4af..88b409036ee 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -487,7 +487,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.38.2 +async-upnp-client==0.38.3 # homeassistant.components.keyboard_remote asyncinotify==4.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 377dacdd327..d49906e2f62 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -442,7 +442,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.38.2 +async-upnp-client==0.38.3 # homeassistant.components.sleepiq asyncsleepiq==1.5.2 From 1c151d78a45ba8f8761c3a8b6557c2a04a6a4f21 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 29 Mar 2024 19:29:09 +0100 Subject: [PATCH 080/967] Don't store analytics insights info on entry level (#114429) --- homeassistant/components/analytics_insights/__init__.py | 6 ++---- homeassistant/components/analytics_insights/sensor.py | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/analytics_insights/__init__.py b/homeassistant/components/analytics_insights/__init__.py index 65c3930e97d..79556fb68c2 100644 --- a/homeassistant/components/analytics_insights/__init__.py +++ b/homeassistant/components/analytics_insights/__init__.py @@ -49,9 +49,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] = AnalyticsInsightsData( - coordinator=coordinator, names=names - ) + hass.data[DOMAIN] = AnalyticsInsightsData(coordinator=coordinator, names=names) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(update_listener)) @@ -62,7 +60,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.""" if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data[DOMAIN].pop(entry.entry_id) + hass.data.pop(DOMAIN) return unload_ok diff --git a/homeassistant/components/analytics_insights/sensor.py b/homeassistant/components/analytics_insights/sensor.py index e776ddb9f41..ee1496eb52c 100644 --- a/homeassistant/components/analytics_insights/sensor.py +++ b/homeassistant/components/analytics_insights/sensor.py @@ -65,7 +65,7 @@ async def async_setup_entry( ) -> None: """Initialize the entries.""" - analytics_data: AnalyticsInsightsData = hass.data[DOMAIN][entry.entry_id] + analytics_data: AnalyticsInsightsData = hass.data[DOMAIN] coordinator: HomeassistantAnalyticsDataUpdateCoordinator = ( analytics_data.coordinator ) From 6c6d1fb71dc8fe16e0b0301e6fb8216f2b2ca8b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexey=20ALERT=20Rubash=D1=91ff?= Date: Fri, 29 Mar 2024 20:33:13 +0200 Subject: [PATCH 081/967] Add overkiz water targets temperature numbers for Atlantic water heater (#114185) * Adds water targets temperature numbers for Atlantic water heater * Update homeassistant/components/overkiz/number.py Co-authored-by: Mick Vleeshouwer * Update homeassistant/components/overkiz/number.py Co-authored-by: Mick Vleeshouwer * ruff formatting reverted * Update homeassistant/components/overkiz/number.py Co-authored-by: TheJulianJES * Update homeassistant/components/overkiz/number.py Co-authored-by: TheJulianJES * changed command hardcode to a constant --------- Co-authored-by: Mick Vleeshouwer Co-authored-by: TheJulianJES --- homeassistant/components/overkiz/number.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/homeassistant/components/overkiz/number.py b/homeassistant/components/overkiz/number.py index f81ed82f7b1..494d430c393 100644 --- a/homeassistant/components/overkiz/number.py +++ b/homeassistant/components/overkiz/number.py @@ -97,6 +97,28 @@ NUMBER_DESCRIPTIONS: list[OverkizNumberDescription] = [ max_value_state_name=OverkizState.CORE_MAXIMAL_SHOWER_MANUAL_MODE, entity_category=EntityCategory.CONFIG, ), + OverkizNumberDescription( + key=OverkizState.CORE_TARGET_DWH_TEMPERATURE, + name="Target temperature", + device_class=NumberDeviceClass.TEMPERATURE, + command=OverkizCommand.SET_TARGET_DHW_TEMPERATURE, + native_min_value=50, + native_max_value=65, + min_value_state_name=OverkizState.CORE_MINIMAL_TEMPERATURE_MANUAL_MODE, + max_value_state_name=OverkizState.CORE_MAXIMAL_TEMPERATURE_MANUAL_MODE, + entity_category=EntityCategory.CONFIG, + ), + OverkizNumberDescription( + key=OverkizState.CORE_WATER_TARGET_TEMPERATURE, + name="Water target temperature", + device_class=NumberDeviceClass.TEMPERATURE, + command=OverkizCommand.SET_WATER_TARGET_TEMPERATURE, + native_min_value=50, + native_max_value=65, + min_value_state_name=OverkizState.CORE_MINIMAL_TEMPERATURE_MANUAL_MODE, + max_value_state_name=OverkizState.CORE_MAXIMAL_TEMPERATURE_MANUAL_MODE, + entity_category=EntityCategory.CONFIG, + ), # SomfyHeatingTemperatureInterface OverkizNumberDescription( key=OverkizState.CORE_ECO_ROOM_TEMPERATURE, From 0f710f9fe0d02fe5548bffad86249d9734822ac9 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Fri, 29 Mar 2024 19:34:16 +0100 Subject: [PATCH 082/967] Update frontend to 20240329.0 (#114452) --- 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 9e86436bd68..a8f14187d48 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==20240328.0"] + "requirements": ["home-assistant-frontend==20240329.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 90c256006fa..d65fa6e7e03 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -30,7 +30,7 @@ habluetooth==2.4.2 hass-nabucasa==0.79.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240328.0 +home-assistant-frontend==20240329.0 home-assistant-intents==2024.3.27 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 88b409036ee..7db30e61c3d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1081,7 +1081,7 @@ hole==0.8.0 holidays==0.45 # homeassistant.components.frontend -home-assistant-frontend==20240328.0 +home-assistant-frontend==20240329.0 # homeassistant.components.conversation home-assistant-intents==2024.3.27 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d49906e2f62..c4d94dc8348 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -880,7 +880,7 @@ hole==0.8.0 holidays==0.45 # homeassistant.components.frontend -home-assistant-frontend==20240328.0 +home-assistant-frontend==20240329.0 # homeassistant.components.conversation home-assistant-intents==2024.3.27 From 0554ac18b854c10d255e8b0820ac4ea757ef08ab Mon Sep 17 00:00:00 2001 From: Tereza Tomcova Date: Fri, 29 Mar 2024 21:57:08 +0100 Subject: [PATCH 083/967] Address late code review comment of Prusa MK3 support (#114455) Address code review comment from #114210 (Prusa MK3 support) --- homeassistant/components/prusalink/sensor.py | 24 +++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/prusalink/sensor.py b/homeassistant/components/prusalink/sensor.py index 604b029fc92..e8d357726bc 100644 --- a/homeassistant/components/prusalink/sensor.py +++ b/homeassistant/components/prusalink/sensor.py @@ -146,15 +146,19 @@ SENSORS: dict[str, tuple[PrusaLinkSensorEntityDescription, ...]] = { translation_key="progress", native_unit_of_measurement=PERCENTAGE, value_fn=lambda data: cast(float, data["progress"]), - available_fn=lambda data: data.get("progress") is not None - and data.get("state") != PrinterState.IDLE.value, + available_fn=lambda data: ( + data.get("progress") is not None + and data.get("state") != PrinterState.IDLE.value + ), ), PrusaLinkSensorEntityDescription[JobInfo]( key="job.filename", translation_key="filename", value_fn=lambda data: cast(str, data["file"]["display_name"]), - available_fn=lambda data: data.get("file") is not None - and data.get("state") != PrinterState.IDLE.value, + available_fn=lambda data: ( + data.get("file") is not None + and data.get("state") != PrinterState.IDLE.value + ), ), PrusaLinkSensorEntityDescription[JobInfo]( key="job.start", @@ -164,8 +168,10 @@ SENSORS: dict[str, tuple[PrusaLinkSensorEntityDescription, ...]] = { lambda data: (utcnow() - timedelta(seconds=data["time_printing"])), timedelta(minutes=2), ), - available_fn=lambda data: data.get("time_printing") is not None - and data.get("state") != PrinterState.IDLE.value, + available_fn=lambda data: ( + data.get("time_printing") is not None + and data.get("state") != PrinterState.IDLE.value + ), ), PrusaLinkSensorEntityDescription[JobInfo]( key="job.finish", @@ -175,8 +181,10 @@ SENSORS: dict[str, tuple[PrusaLinkSensorEntityDescription, ...]] = { lambda data: (utcnow() + timedelta(seconds=data["time_remaining"])), timedelta(minutes=2), ), - available_fn=lambda data: data.get("time_remaining") is not None - and data.get("state") != PrinterState.IDLE.value, + available_fn=lambda data: ( + data.get("time_remaining") is not None + and data.get("state") != PrinterState.IDLE.value + ), ), ), } From 3b4958d9722c973d914dddea589c55ad0ea7e008 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Fri, 29 Mar 2024 22:13:31 +0100 Subject: [PATCH 084/967] Update frontend to 20240329.1 (#114459) --- 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 a8f14187d48..7864801a986 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==20240329.0"] + "requirements": ["home-assistant-frontend==20240329.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d65fa6e7e03..b2a8898bf47 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -30,7 +30,7 @@ habluetooth==2.4.2 hass-nabucasa==0.79.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240329.0 +home-assistant-frontend==20240329.1 home-assistant-intents==2024.3.27 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 7db30e61c3d..f928cf8333a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1081,7 +1081,7 @@ hole==0.8.0 holidays==0.45 # homeassistant.components.frontend -home-assistant-frontend==20240329.0 +home-assistant-frontend==20240329.1 # homeassistant.components.conversation home-assistant-intents==2024.3.27 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c4d94dc8348..aabaf488caa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -880,7 +880,7 @@ hole==0.8.0 holidays==0.45 # homeassistant.components.frontend -home-assistant-frontend==20240329.0 +home-assistant-frontend==20240329.1 # homeassistant.components.conversation home-assistant-intents==2024.3.27 From 969b027a46ba0701ef7297e58548606f5a0b00a4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 29 Mar 2024 11:18:21 -1000 Subject: [PATCH 085/967] Avoid tracking import executor jobs (#114453) --- homeassistant/core.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 76a3f55527c..3895496506c 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -783,11 +783,11 @@ class HomeAssistant: def async_add_import_executor_job( self, target: Callable[[*_Ts], _T], *args: *_Ts ) -> asyncio.Future[_T]: - """Add an import executor job from within the event loop.""" - task = self.loop.run_in_executor(self.import_executor, target, *args) - self._tasks.add(task) - task.add_done_callback(self._tasks.remove) - return task + """Add an import executor job from within the event loop. + + The future returned from this method must be awaited in the event loop. + """ + return self.loop.run_in_executor(self.import_executor, target, *args) @overload @callback From aec7a67a58e9da61f5f6620a61ea81990f14d1ca Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Fri, 29 Mar 2024 23:58:30 +0100 Subject: [PATCH 086/967] Unignore Ruff PLE, PLW in tests (#114406) * Unignore Ruff PLE, PLW in tests * fix tests --- tests/components/google_assistant/test_helpers.py | 5 ++++- tests/components/history/test_init.py | 2 +- tests/components/history/test_init_db_schema_30.py | 2 +- tests/components/recorder/test_history.py | 2 +- .../components/recorder/test_history_db_schema_30.py | 2 +- .../components/recorder/test_history_db_schema_32.py | 2 +- .../components/recorder/test_history_db_schema_42.py | 2 +- tests/components/template/test_config_flow.py | 2 +- tests/components/xiaomi/test_device_tracker.py | 2 +- tests/ruff.toml | 12 ------------ tests/testing_config/custom_components/test/light.py | 2 +- tests/testing_config/custom_components/test/lock.py | 2 +- .../testing_config/custom_components/test/remote.py | 2 +- .../testing_config/custom_components/test/weather.py | 2 +- 14 files changed, 16 insertions(+), 25 deletions(-) diff --git a/tests/components/google_assistant/test_helpers.py b/tests/components/google_assistant/test_helpers.py index 3f7fd91fed2..492f1be1829 100644 --- a/tests/components/google_assistant/test_helpers.py +++ b/tests/components/google_assistant/test_helpers.py @@ -344,7 +344,10 @@ def test_supported_features_string(caplog: pytest.LogCaptureFixture) -> None: State("test.entity_id", "on", {"supported_features": "invalid"}), ) assert entity.is_supported() is False - assert "Entity test.entity_id contains invalid supported_features value invalid" + assert ( + "Entity test.entity_id contains invalid supported_features value invalid" + in caplog.text + ) def test_request_data() -> None: diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 13574bb2bb2..5d9cb86f9b6 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -87,7 +87,7 @@ def test_get_significant_states_minimal_response(hass_history) -> None: entity_states = states[entity_id] for state_idx in range(1, len(entity_states)): input_state = entity_states[state_idx] - orig_last_changed = orig_last_changed = json.dumps( + orig_last_changed = json.dumps( process_timestamp(input_state.last_changed), cls=JSONEncoder, ).replace('"', "") diff --git a/tests/components/history/test_init_db_schema_30.py b/tests/components/history/test_init_db_schema_30.py index 0bbd913ce2b..ce5c5a4b6c6 100644 --- a/tests/components/history/test_init_db_schema_30.py +++ b/tests/components/history/test_init_db_schema_30.py @@ -96,7 +96,7 @@ def test_get_significant_states_minimal_response(legacy_hass_history) -> None: entity_states = states[entity_id] for state_idx in range(1, len(entity_states)): input_state = entity_states[state_idx] - orig_last_changed = orig_last_changed = json.dumps( + orig_last_changed = json.dumps( process_timestamp(input_state.last_changed), cls=JSONEncoder, ).replace('"', "") diff --git a/tests/components/recorder/test_history.py b/tests/components/recorder/test_history.py index d16a6856399..04204cf84a6 100644 --- a/tests/components/recorder/test_history.py +++ b/tests/components/recorder/test_history.py @@ -554,7 +554,7 @@ def test_get_significant_states_minimal_response( entity_states = states[entity_id] for state_idx in range(1, len(entity_states)): input_state = entity_states[state_idx] - orig_last_changed = orig_last_changed = json.dumps( + orig_last_changed = json.dumps( process_timestamp(input_state.last_changed), cls=JSONEncoder, ).replace('"', "") diff --git a/tests/components/recorder/test_history_db_schema_30.py b/tests/components/recorder/test_history_db_schema_30.py index cbe4c3ac5c8..0aaf1ebb094 100644 --- a/tests/components/recorder/test_history_db_schema_30.py +++ b/tests/components/recorder/test_history_db_schema_30.py @@ -328,7 +328,7 @@ def test_get_significant_states_minimal_response( entity_states = states[entity_id] for state_idx in range(1, len(entity_states)): input_state = entity_states[state_idx] - orig_last_changed = orig_last_changed = json.dumps( + orig_last_changed = json.dumps( process_timestamp(input_state.last_changed), cls=JSONEncoder, ).replace('"', "") diff --git a/tests/components/recorder/test_history_db_schema_32.py b/tests/components/recorder/test_history_db_schema_32.py index b926aa1903b..9bb6d70b125 100644 --- a/tests/components/recorder/test_history_db_schema_32.py +++ b/tests/components/recorder/test_history_db_schema_32.py @@ -327,7 +327,7 @@ def test_get_significant_states_minimal_response( entity_states = states[entity_id] for state_idx in range(1, len(entity_states)): input_state = entity_states[state_idx] - orig_last_changed = orig_last_changed = json.dumps( + orig_last_changed = json.dumps( process_timestamp(input_state.last_changed), cls=JSONEncoder, ).replace('"', "") diff --git a/tests/components/recorder/test_history_db_schema_42.py b/tests/components/recorder/test_history_db_schema_42.py index 98ed6089de6..a72345e71bd 100644 --- a/tests/components/recorder/test_history_db_schema_42.py +++ b/tests/components/recorder/test_history_db_schema_42.py @@ -556,7 +556,7 @@ def test_get_significant_states_minimal_response( entity_states = states[entity_id] for state_idx in range(1, len(entity_states)): input_state = entity_states[state_idx] - orig_last_changed = orig_last_changed = json.dumps( + orig_last_changed = json.dumps( process_timestamp(input_state.last_changed), cls=JSONEncoder, ).replace('"', "") diff --git a/tests/components/template/test_config_flow.py b/tests/components/template/test_config_flow.py index 30d6942750c..0a34dff9776 100644 --- a/tests/components/template/test_config_flow.py +++ b/tests/components/template/test_config_flow.py @@ -754,7 +754,7 @@ async def test_option_flow_preview( """Test the option flow preview.""" client = await hass_ws_client(hass) - input_entities = input_entities = ["one", "two"] + input_entities = ["one", "two"] # Setup the config entry config_entry = MockConfigEntry( diff --git a/tests/components/xiaomi/test_device_tracker.py b/tests/components/xiaomi/test_device_tracker.py index 0a576b70bdf..1b1d898add1 100644 --- a/tests/components/xiaomi/test_device_tracker.py +++ b/tests/components/xiaomi/test_device_tracker.py @@ -48,7 +48,7 @@ def mocked_requests(*args, **kwargs): raise requests.HTTPError(self.status_code) data = kwargs.get("data") - global FIRST_CALL + global FIRST_CALL # noqa: PLW0603 if data and data.get("username", None) == INVALID_USERNAME: # deliver an invalid token diff --git a/tests/ruff.toml b/tests/ruff.toml index 76e4feacdd2..1a8876b9171 100644 --- a/tests/ruff.toml +++ b/tests/ruff.toml @@ -2,22 +2,10 @@ extend = "../pyproject.toml" [lint] -extend-select = [ - "PT001", # Use @pytest.fixture without parentheses - "PT002", # Configuration for fixture specified via positional args, use kwargs - "PT003", # The scope='function' is implied in @pytest.fixture() - "PT006", # Single parameter in parameterize is a string, multiple a tuple - "PT013", # Found incorrect pytest import, use simple import pytest instead - "PT015", # Assertion always fails, replace with pytest.fail() - "PT021", # use yield instead of request.addfinalizer - "PT022", # No teardown in fixture, replace useless yield with return -] extend-ignore = [ "PLC", # pylint - "PLE", # pylint "PLR", # pylint - "PLW", # pylint "B904", # Use raise from to specify exception cause "N815", # Variable {name} in class scope should not be mixedCase ] diff --git a/tests/testing_config/custom_components/test/light.py b/tests/testing_config/custom_components/test/light.py index eed98a8210a..4cd49fec606 100644 --- a/tests/testing_config/custom_components/test/light.py +++ b/tests/testing_config/custom_components/test/light.py @@ -13,7 +13,7 @@ ENTITIES = [] def init(empty=False): """Initialize the platform with entities.""" - global ENTITIES + global ENTITIES # noqa: PLW0603 ENTITIES = ( [] diff --git a/tests/testing_config/custom_components/test/lock.py b/tests/testing_config/custom_components/test/lock.py index ba5a91e2d24..e97d3f8de22 100644 --- a/tests/testing_config/custom_components/test/lock.py +++ b/tests/testing_config/custom_components/test/lock.py @@ -12,7 +12,7 @@ ENTITIES = {} def init(empty=False): """Initialize the platform with entities.""" - global ENTITIES + global ENTITIES # noqa: PLW0603 ENTITIES = ( {} diff --git a/tests/testing_config/custom_components/test/remote.py b/tests/testing_config/custom_components/test/remote.py index 541215f1c47..3226c93310c 100644 --- a/tests/testing_config/custom_components/test/remote.py +++ b/tests/testing_config/custom_components/test/remote.py @@ -13,7 +13,7 @@ ENTITIES = [] def init(empty=False): """Initialize the platform with entities.""" - global ENTITIES + global ENTITIES # noqa: PLW0603 ENTITIES = ( [] diff --git a/tests/testing_config/custom_components/test/weather.py b/tests/testing_config/custom_components/test/weather.py index 0e99ef48680..b051531b9e8 100644 --- a/tests/testing_config/custom_components/test/weather.py +++ b/tests/testing_config/custom_components/test/weather.py @@ -33,7 +33,7 @@ ENTITIES = [] def init(empty=False): """Initialize the platform with entities.""" - global ENTITIES + global ENTITIES # noqa: PLW0603 ENTITIES = [] if empty else [MockWeather()] From 9a79320861cac4cbf0610fc54eb8da09425d0827 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 29 Mar 2024 18:16:53 -1000 Subject: [PATCH 087/967] Mark executor jobs as background unless created from a tracked task (#114450) * Mark executor jobs as background unless created from a tracked task If the current task is not tracked the executor job should not be a background task to avoid delaying startup and shutdown. Currently any executor job created in a untracked task or background task would end up being tracked and delaying startup/shutdown * import exec has the same issue * Avoid tracking import executor jobs There is no reason to track these jobs as they are always awaited and we do not want to support fire and forget import executor jobs * fix xiaomi_miio * lots of fire time changed without background await * revert changes moved to other PR * more * more * more * m * m * p * fix fire and forget tests * scrape * sonos * system * more * capture callback before block * coverage * more * more races * more races * more * missed some * more fixes * missed some more * fix * remove unneeded * one more race * two --- homeassistant/core.py | 7 +++- .../aurora_abb_powerone/test_sensor.py | 8 ++-- tests/components/cast/test_config_flow.py | 4 +- tests/components/cast/test_media_player.py | 8 ++-- tests/components/fritz/test_image.py | 4 +- tests/components/fritz/test_sensor.py | 2 +- .../components/fritzbox/test_binary_sensor.py | 6 +-- tests/components/fritzbox/test_button.py | 2 +- tests/components/fritzbox/test_climate.py | 12 +++--- tests/components/fritzbox/test_cover.py | 2 +- tests/components/fritzbox/test_light.py | 6 +-- tests/components/fritzbox/test_sensor.py | 6 +-- tests/components/fritzbox/test_switch.py | 6 +-- .../components/geo_rss_events/test_sensor.py | 4 +- tests/components/google_mail/test_sensor.py | 4 +- .../maxcube/test_maxcube_binary_sensor.py | 6 +-- .../maxcube/test_maxcube_climate.py | 22 +++++----- tests/components/metoffice/test_weather.py | 10 ++--- .../mikrotik/test_device_tracker.py | 8 ++-- .../components/monoprice/test_media_player.py | 18 ++++----- .../panasonic_viera/test_media_player.py | 4 +- tests/components/pjlink/test_media_player.py | 4 +- tests/components/profiler/test_init.py | 10 ++--- tests/components/ps4/test_media_player.py | 2 + tests/components/python_script/test_init.py | 40 +++++++++---------- .../components/samsungtv/test_media_player.py | 10 ++--- .../components/schlage/test_binary_sensor.py | 4 +- tests/components/schlage/test_lock.py | 2 +- tests/components/scrape/test_sensor.py | 12 +++--- .../components/solaredge/test_coordinator.py | 10 ++--- tests/components/sonos/conftest.py | 3 +- tests/components/sonos/test_repairs.py | 5 ++- tests/components/sonos/test_sensor.py | 29 +++++++++----- tests/components/sonos/test_speaker.py | 16 +++++++- .../soundtouch/test_media_player.py | 2 +- tests/components/speedtestdotnet/test_init.py | 2 +- .../systemmonitor/test_binary_sensor.py | 2 +- tests/components/systemmonitor/test_sensor.py | 22 +++++----- tests/components/tcp/test_binary_sensor.py | 2 +- tests/components/temper/test_sensor.py | 2 +- .../totalconnect/test_alarm_control_panel.py | 10 ++--- tests/components/uvc/test_camera.py | 12 +++--- tests/components/ws66i/test_media_player.py | 20 +++++----- tests/components/xiaomi_miio/test_vacuum.py | 4 +- .../yale_smart_alarm/test_coordinator.py | 12 +++--- tests/test_core.py | 40 +++++++++++++++++++ 46 files changed, 246 insertions(+), 180 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 3895496506c..082c1a756c3 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -774,8 +774,11 @@ class HomeAssistant: ) -> asyncio.Future[_T]: """Add an executor job from within the event loop.""" task = self.loop.run_in_executor(None, target, *args) - self._tasks.add(task) - task.add_done_callback(self._tasks.remove) + + tracked = asyncio.current_task() in self._tasks + task_bucket = self._tasks if tracked else self._background_tasks + task_bucket.add(task) + task.add_done_callback(task_bucket.remove) return task diff --git a/tests/components/aurora_abb_powerone/test_sensor.py b/tests/components/aurora_abb_powerone/test_sensor.py index 178cf165f67..4bc5a5d3086 100644 --- a/tests/components/aurora_abb_powerone/test_sensor.py +++ b/tests/components/aurora_abb_powerone/test_sensor.py @@ -201,7 +201,7 @@ async def test_sensor_dark(hass: HomeAssistant, freezer: FrozenDateTimeFactory) ): freezer.tick(SCAN_INTERVAL * 2) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) power = hass.states.get("sensor.mydevicename_total_energy") assert power.state == "unknown" # sun rose again @@ -218,7 +218,7 @@ async def test_sensor_dark(hass: HomeAssistant, freezer: FrozenDateTimeFactory) ): freezer.tick(SCAN_INTERVAL * 4) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) power = hass.states.get("sensor.mydevicename_power_output") assert power is not None assert power.state == "45.7" @@ -237,7 +237,7 @@ async def test_sensor_dark(hass: HomeAssistant, freezer: FrozenDateTimeFactory) ): freezer.tick(SCAN_INTERVAL * 6) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) power = hass.states.get("sensor.mydevicename_power_output") assert power.state == "unknown" # should this be 'available'? @@ -277,7 +277,7 @@ async def test_sensor_unknown_error( ): freezer.tick(SCAN_INTERVAL * 2) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert ( "Exception: AuroraError('another error') occurred, 2 retries remaining" in caplog.text diff --git a/tests/components/cast/test_config_flow.py b/tests/components/cast/test_config_flow.py index 62c21fc95ee..a7b9311e88b 100644 --- a/tests/components/cast/test_config_flow.py +++ b/tests/components/cast/test_config_flow.py @@ -278,7 +278,7 @@ async def test_known_hosts(hass: HomeAssistant, castbrowser_mock) -> None: result["flow_id"], {"known_hosts": "192.168.0.1, 192.168.0.2"} ) assert result["type"] == "create_entry" - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) config_entry = hass.config_entries.async_entries("cast")[0] assert castbrowser_mock.return_value.start_discovery.call_count == 1 @@ -291,7 +291,7 @@ async def test_known_hosts(hass: HomeAssistant, castbrowser_mock) -> None: user_input={"known_hosts": "192.168.0.11, 192.168.0.12"}, ) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) castbrowser_mock.return_value.start_discovery.assert_not_called() castbrowser_mock.assert_not_called() diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 9ef31457d5c..8381f27398a 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -137,8 +137,8 @@ async def async_setup_cast_internal_discovery(hass, config=None): return_value=browser, ) as cast_browser: add_entities = await async_setup_cast(hass, config) - await hass.async_block_till_done() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) + await hass.async_block_till_done(wait_background_tasks=True) assert browser.start_discovery.call_count == 1 @@ -209,8 +209,8 @@ async def async_setup_media_player_cast(hass: HomeAssistant, info: ChromecastInf entry = MockConfigEntry(data=data, domain="cast") entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) + await hass.async_block_till_done(wait_background_tasks=True) discovery_callback = cast_browser.call_args[0][0].add_cast diff --git a/tests/components/fritz/test_image.py b/tests/components/fritz/test_image.py index 85d02eff153..5d6b9265760 100644 --- a/tests/components/fritz/test_image.py +++ b/tests/components/fritz/test_image.py @@ -199,7 +199,7 @@ async def test_image_update_unavailable( # fritzbox becomes unavailable fc_class_mock().call_action_side_effect(ReadTimeout) async_fire_time_changed(hass, utcnow() + timedelta(seconds=60)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("image.mock_title_guestwifi") assert state.state == STATE_UNKNOWN @@ -207,7 +207,7 @@ async def test_image_update_unavailable( # fritzbox is available again fc_class_mock().call_action_side_effect(None) async_fire_time_changed(hass, utcnow() + timedelta(seconds=60)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("image.mock_title_guestwifi") assert state.state != STATE_UNKNOWN diff --git a/tests/components/fritz/test_sensor.py b/tests/components/fritz/test_sensor.py index 4427fc6961e..37116e66719 100644 --- a/tests/components/fritz/test_sensor.py +++ b/tests/components/fritz/test_sensor.py @@ -134,7 +134,7 @@ async def test_sensor_update_fail( fc_class_mock().call_action_side_effect(FritzConnectionException) async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=300)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) sensors = hass.states.async_all(SENSOR_DOMAIN) for sensor in sensors: diff --git a/tests/components/fritzbox/test_binary_sensor.py b/tests/components/fritzbox/test_binary_sensor.py index 3828cedc67f..3e1a2691f67 100644 --- a/tests/components/fritzbox/test_binary_sensor.py +++ b/tests/components/fritzbox/test_binary_sensor.py @@ -104,7 +104,7 @@ async def test_update(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert fritz().update_devices.call_count == 2 assert fritz().login.call_count == 1 @@ -123,7 +123,7 @@ async def test_update_error(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert fritz().update_devices.call_count == 2 assert fritz().login.call_count == 1 @@ -146,7 +146,7 @@ async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(f"{DOMAIN}.new_device_alarm") assert state diff --git a/tests/components/fritzbox/test_button.py b/tests/components/fritzbox/test_button.py index f254b2e0710..89e8d8357dd 100644 --- a/tests/components/fritzbox/test_button.py +++ b/tests/components/fritzbox/test_button.py @@ -65,7 +65,7 @@ async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(f"{DOMAIN}.new_template") assert state diff --git a/tests/components/fritzbox/test_climate.py b/tests/components/fritzbox/test_climate.py index a201eab3665..073a67f22c1 100644 --- a/tests/components/fritzbox/test_climate.py +++ b/tests/components/fritzbox/test_climate.py @@ -145,7 +145,7 @@ async def test_setup(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_next_scheduled_preset") assert state @@ -203,7 +203,7 @@ async def test_update(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert fritz().update_devices.call_count == 2 @@ -243,7 +243,7 @@ async def test_update_error(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert fritz().update_devices.call_count == 4 assert fritz().login.call_count == 4 @@ -386,7 +386,7 @@ async def test_preset_mode_update(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert fritz().update_devices.call_count == 2 @@ -397,7 +397,7 @@ async def test_preset_mode_update(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert fritz().update_devices.call_count == 3 @@ -422,7 +422,7 @@ async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(f"{DOMAIN}.new_climate") assert state diff --git a/tests/components/fritzbox/test_cover.py b/tests/components/fritzbox/test_cover.py index b723ac97d06..6c301fc8f46 100644 --- a/tests/components/fritzbox/test_cover.py +++ b/tests/components/fritzbox/test_cover.py @@ -108,7 +108,7 @@ async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(f"{DOMAIN}.new_climate") assert state diff --git a/tests/components/fritzbox/test_light.py b/tests/components/fritzbox/test_light.py index b750a2e9275..45920c7c3ee 100644 --- a/tests/components/fritzbox/test_light.py +++ b/tests/components/fritzbox/test_light.py @@ -237,7 +237,7 @@ async def test_update(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert fritz().update_devices.call_count == 2 assert fritz().login.call_count == 1 @@ -259,7 +259,7 @@ async def test_update_error(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert fritz().update_devices.call_count == 4 assert fritz().login.call_count == 4 @@ -294,7 +294,7 @@ async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(f"{DOMAIN}.new_light") assert state diff --git a/tests/components/fritzbox/test_sensor.py b/tests/components/fritzbox/test_sensor.py index 48b769eaac2..63d0b67d7f4 100644 --- a/tests/components/fritzbox/test_sensor.py +++ b/tests/components/fritzbox/test_sensor.py @@ -87,7 +87,7 @@ async def test_update(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert fritz().update_devices.call_count == 2 assert fritz().login.call_count == 1 @@ -105,7 +105,7 @@ async def test_update_error(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert fritz().update_devices.call_count == 4 assert fritz().login.call_count == 4 @@ -128,7 +128,7 @@ async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(f"{DOMAIN}.new_device_temperature") assert state diff --git a/tests/components/fritzbox/test_switch.py b/tests/components/fritzbox/test_switch.py index 67393bc09a5..417b355b396 100644 --- a/tests/components/fritzbox/test_switch.py +++ b/tests/components/fritzbox/test_switch.py @@ -151,7 +151,7 @@ async def test_update(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert fritz().update_devices.call_count == 2 assert fritz().login.call_count == 1 @@ -169,7 +169,7 @@ async def test_update_error(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert fritz().update_devices.call_count == 4 assert fritz().login.call_count == 4 @@ -207,7 +207,7 @@ async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(f"{DOMAIN}.new_switch") assert state diff --git a/tests/components/geo_rss_events/test_sensor.py b/tests/components/geo_rss_events/test_sensor.py index 76f1709bd75..d19262c3339 100644 --- a/tests/components/geo_rss_events/test_sensor.py +++ b/tests/components/geo_rss_events/test_sensor.py @@ -99,7 +99,7 @@ async def test_setup( # so no changes to entities. mock_feed.return_value.update.return_value = "OK_NO_DATA", None async_fire_time_changed(hass, utcnow + geo_rss_events.SCAN_INTERVAL) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) all_states = hass.states.async_all() assert len(all_states) == 1 @@ -109,7 +109,7 @@ async def test_setup( # Simulate an update - empty data, removes all entities mock_feed.return_value.update.return_value = "ERROR", None async_fire_time_changed(hass, utcnow + 2 * geo_rss_events.SCAN_INTERVAL) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) all_states = hass.states.async_all() assert len(all_states) == 1 diff --git a/tests/components/google_mail/test_sensor.py b/tests/components/google_mail/test_sensor.py index e0b072d4b7d..6f2f1a4ec32 100644 --- a/tests/components/google_mail/test_sensor.py +++ b/tests/components/google_mail/test_sensor.py @@ -46,7 +46,7 @@ async def test_sensors( ): next_update = dt_util.utcnow() + timedelta(minutes=15) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(SENSOR) assert state.state == result @@ -61,7 +61,7 @@ async def test_sensor_reauth_trigger( with patch(TOKEN, side_effect=RefreshError): next_update = dt_util.utcnow() + timedelta(minutes=15) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) flows = hass.config_entries.flow.async_progress() diff --git a/tests/components/maxcube/test_maxcube_binary_sensor.py b/tests/components/maxcube/test_maxcube_binary_sensor.py index cc86f389884..32ec4e92ee1 100644 --- a/tests/components/maxcube/test_maxcube_binary_sensor.py +++ b/tests/components/maxcube/test_maxcube_binary_sensor.py @@ -43,7 +43,7 @@ async def test_window_shuttler( windowshutter.is_open = False async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == STATE_OFF @@ -68,12 +68,12 @@ async def test_window_shuttler_battery( windowshutter.battery = 1 # maxcube-api MAX_DEVICE_BATTERY_LOW async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(BATTERY_ENTITY_ID) assert state.state == STATE_ON # on means low windowshutter.battery = 0 # maxcube-api MAX_DEVICE_BATTERY_OK async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(BATTERY_ENTITY_ID) assert state.state == STATE_OFF # off means normal diff --git a/tests/components/maxcube/test_maxcube_climate.py b/tests/components/maxcube/test_maxcube_climate.py index cb4dc510605..e1e7dc57c47 100644 --- a/tests/components/maxcube/test_maxcube_climate.py +++ b/tests/components/maxcube/test_maxcube_climate.py @@ -140,7 +140,7 @@ async def test_thermostat_set_hvac_mode_off( thermostat.valve_position = 0 async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == HVACMode.OFF @@ -168,8 +168,8 @@ async def test_thermostat_set_hvac_mode_heat( thermostat.mode = MAX_DEVICE_MODE_MANUAL async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == HVACMode.HEAT @@ -204,7 +204,7 @@ async def test_thermostat_set_temperature( thermostat.valve_position = 0 async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == HVACMode.AUTO @@ -248,7 +248,7 @@ async def test_thermostat_set_preset_on( thermostat.target_temperature = ON_TEMPERATURE async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == HVACMode.HEAT @@ -273,7 +273,7 @@ async def test_thermostat_set_preset_comfort( thermostat.target_temperature = thermostat.comfort_temperature async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == HVACMode.HEAT @@ -298,7 +298,7 @@ async def test_thermostat_set_preset_eco( thermostat.target_temperature = thermostat.eco_temperature async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == HVACMode.HEAT @@ -323,7 +323,7 @@ async def test_thermostat_set_preset_away( thermostat.target_temperature = thermostat.eco_temperature async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == HVACMode.HEAT @@ -348,7 +348,7 @@ async def test_thermostat_set_preset_boost( thermostat.target_temperature = thermostat.eco_temperature async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == HVACMode.AUTO @@ -401,7 +401,7 @@ async def test_wallthermostat_set_hvac_mode_heat( wallthermostat.target_temperature = MIN_TEMPERATURE async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(WALL_ENTITY_ID) assert state.state == HVACMode.HEAT @@ -425,7 +425,7 @@ async def test_wallthermostat_set_hvac_mode_auto( wallthermostat.target_temperature = 23.0 async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(WALL_ENTITY_ID) assert state.state == HVACMode.AUTO diff --git a/tests/components/metoffice/test_weather.py b/tests/components/metoffice/test_weather.py index 2aa673d4010..64a85897738 100644 --- a/tests/components/metoffice/test_weather.py +++ b/tests/components/metoffice/test_weather.py @@ -125,7 +125,7 @@ async def test_site_cannot_update( future_time = utcnow() + timedelta(minutes=20) async_fire_time_changed(hass, future_time) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) weather = hass.states.get("weather.met_office_wavertree_daily") assert weather.state == STATE_UNAVAILABLE @@ -297,7 +297,7 @@ async def test_forecast_service( # Trigger data refetch freezer.tick(DEFAULT_SCAN_INTERVAL + timedelta(seconds=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert wavertree_data["wavertree_daily_mock"].call_count == 2 assert wavertree_data["wavertree_hourly_mock"].call_count == 1 @@ -324,7 +324,7 @@ async def test_forecast_service( freezer.tick(DEFAULT_SCAN_INTERVAL + timedelta(seconds=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) response = await hass.services.async_call( WEATHER_DOMAIN, @@ -412,7 +412,7 @@ async def test_forecast_subscription( freezer.tick(DEFAULT_SCAN_INTERVAL + timedelta(seconds=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) msg = await client.receive_json() assert msg["id"] == subscription_id @@ -430,6 +430,6 @@ async def test_forecast_subscription( ) freezer.tick(timedelta(seconds=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) msg = await client.receive_json() assert msg["success"] diff --git a/tests/components/mikrotik/test_device_tracker.py b/tests/components/mikrotik/test_device_tracker.py index 47ddc038f69..89dc37fd781 100644 --- a/tests/components/mikrotik/test_device_tracker.py +++ b/tests/components/mikrotik/test_device_tracker.py @@ -88,7 +88,7 @@ async def test_device_trackers( WIRELESS_DATA.append(DEVICE_2_WIRELESS) async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) device_2 = hass.states.get("device_tracker.device_2") assert device_2 @@ -101,7 +101,7 @@ async def test_device_trackers( del WIRELESS_DATA[1] # device 2 is removed from wireless list with freeze_time(utcnow() + timedelta(minutes=4)): async_fire_time_changed(hass, utcnow() + timedelta(minutes=4)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) device_2 = hass.states.get("device_tracker.device_2") assert device_2 @@ -110,7 +110,7 @@ async def test_device_trackers( # test state changes to away if last_seen past consider_home_interval with freeze_time(utcnow() + timedelta(minutes=6)): async_fire_time_changed(hass, utcnow() + timedelta(minutes=6)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) device_2 = hass.states.get("device_tracker.device_2") assert device_2 @@ -266,7 +266,7 @@ async def test_update_failed(hass: HomeAssistant, mock_device_registry_devices) mikrotik.hub.MikrotikData, "command", side_effect=mikrotik.errors.CannotConnect ): async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) device_1 = hass.states.get("device_tracker.device_1") assert device_1 diff --git a/tests/components/monoprice/test_media_player.py b/tests/components/monoprice/test_media_player.py index a0afd37f3b2..f7d88692cf5 100644 --- a/tests/components/monoprice/test_media_player.py +++ b/tests/components/monoprice/test_media_player.py @@ -183,7 +183,7 @@ async def test_service_calls_with_entity_id(hass: HomeAssistant) -> None: # Restoring other media player to its previous state # The zone should not be restored await _call_monoprice_service(hass, SERVICE_RESTORE, {"entity_id": ZONE_2_ID}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # Checking that values were not (!) restored state = hass.states.get(ZONE_1_ID) @@ -193,7 +193,7 @@ async def test_service_calls_with_entity_id(hass: HomeAssistant) -> None: # Restoring media player to its previous state await _call_monoprice_service(hass, SERVICE_RESTORE, {"entity_id": ZONE_1_ID}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ZONE_1_ID) @@ -226,7 +226,7 @@ async def test_service_calls_with_all_entities(hass: HomeAssistant) -> None: # Restoring media player to its previous state await _call_monoprice_service(hass, SERVICE_RESTORE, {"entity_id": "all"}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ZONE_1_ID) @@ -259,7 +259,7 @@ async def test_service_calls_without_relevant_entities(hass: HomeAssistant) -> N # Restoring media player to its previous state await _call_monoprice_service(hass, SERVICE_RESTORE, {"entity_id": "light.demo"}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ZONE_1_ID) @@ -273,7 +273,7 @@ async def test_restore_without_snapshort(hass: HomeAssistant) -> None: with patch.object(MockMonoprice, "restore_zone") as method_call: await _call_monoprice_service(hass, SERVICE_RESTORE, {"entity_id": ZONE_1_ID}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert not method_call.called @@ -295,7 +295,7 @@ async def test_update(hass: HomeAssistant) -> None: monoprice.set_volume(11, 38) await async_update_entity(hass, ZONE_1_ID) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ZONE_1_ID) @@ -321,7 +321,7 @@ async def test_failed_update(hass: HomeAssistant) -> None: with patch.object(MockMonoprice, "zone_status", side_effect=SerialException): await async_update_entity(hass, ZONE_1_ID) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ZONE_1_ID) @@ -347,7 +347,7 @@ async def test_empty_update(hass: HomeAssistant) -> None: with patch.object(MockMonoprice, "zone_status", return_value=None): await async_update_entity(hass, ZONE_1_ID) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ZONE_1_ID) @@ -418,7 +418,7 @@ async def test_unknown_source(hass: HomeAssistant) -> None: monoprice.set_source(11, 5) await async_update_entity(hass, ZONE_1_ID) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ZONE_1_ID) diff --git a/tests/components/panasonic_viera/test_media_player.py b/tests/components/panasonic_viera/test_media_player.py index 1203bf1ed51..dab56542e6a 100644 --- a/tests/components/panasonic_viera/test_media_player.py +++ b/tests/components/panasonic_viera/test_media_player.py @@ -23,7 +23,7 @@ async def test_media_player_handle_URLerror( mock_remote.get_mute = Mock(side_effect=URLError(None, None)) async_fire_time_changed(hass, utcnow() + timedelta(minutes=2)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state_tv = hass.states.get("media_player.panasonic_viera_tv") assert state_tv.state == STATE_UNAVAILABLE @@ -41,7 +41,7 @@ async def test_media_player_handle_HTTPError( mock_remote.get_mute = Mock(side_effect=HTTPError(None, 400, None, None, None)) async_fire_time_changed(hass, utcnow() + timedelta(minutes=2)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state_tv = hass.states.get("media_player.panasonic_viera_tv") assert state_tv.state == STATE_OFF diff --git a/tests/components/pjlink/test_media_player.py b/tests/components/pjlink/test_media_player.py index a6d17233450..d44bc942290 100644 --- a/tests/components/pjlink/test_media_player.py +++ b/tests/components/pjlink/test_media_player.py @@ -208,7 +208,7 @@ async def test_update_unavailable(projector_from_address, hass: HomeAssistant) - projector_from_address.side_effect = socket.timeout async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("media_player.test") assert state.state == "unavailable" @@ -237,7 +237,7 @@ async def test_unavailable_time(mocked_projector, hass: HomeAssistant) -> None: mocked_projector.get_power.side_effect = ProjectorError("unavailable time") async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("media_player.test") assert state.state == "off" diff --git a/tests/components/profiler/test_init.py b/tests/components/profiler/test_init.py index 1140dc74849..3cade465347 100644 --- a/tests/components/profiler/test_init.py +++ b/tests/components/profiler/test_init.py @@ -332,7 +332,7 @@ async def test_log_object_sources( caplog.clear() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=11)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert "No new object growth found" in caplog.text fake_object2 = FakeObject() @@ -344,7 +344,7 @@ async def test_log_object_sources( caplog.clear() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=21)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert "New object FakeObject (1/2)" in caplog.text many_objects = [FakeObject() for _ in range(30)] @@ -352,7 +352,7 @@ async def test_log_object_sources( caplog.clear() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=31)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert "New object FakeObject (2/30)" in caplog.text assert "New objects overflowed by {'FakeObject': 25}" in caplog.text @@ -362,7 +362,7 @@ async def test_log_object_sources( caplog.clear() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=41)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert "FakeObject" not in caplog.text assert "No new object growth found" not in caplog.text @@ -370,7 +370,7 @@ async def test_log_object_sources( await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=51)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert "FakeObject" not in caplog.text assert "No new object growth found" not in caplog.text diff --git a/tests/components/ps4/test_media_player.py b/tests/components/ps4/test_media_player.py index 875b049d8c3..6adcad03016 100644 --- a/tests/components/ps4/test_media_player.py +++ b/tests/components/ps4/test_media_player.py @@ -234,6 +234,7 @@ async def test_media_attributes_are_fetched(hass: HomeAssistant) -> None: with patch(mock_func, return_value=mock_result) as mock_fetch: await mock_ddp_response(hass, MOCK_STATUS_PLAYING) + await hass.async_block_till_done(wait_background_tasks=True) mock_state = hass.states.get(mock_entity_id) mock_attrs = dict(mock_state.attributes) @@ -255,6 +256,7 @@ async def test_media_attributes_are_fetched(hass: HomeAssistant) -> None: with patch(mock_func, return_value=mock_result) as mock_fetch_app: await mock_ddp_response(hass, MOCK_STATUS_PLAYING) + await hass.async_block_till_done(wait_background_tasks=True) mock_state = hass.states.get(mock_entity_id) mock_attrs = dict(mock_state.attributes) diff --git a/tests/components/python_script/test_init.py b/tests/components/python_script/test_init.py index 8e317e2163e..504d61a0d8d 100644 --- a/tests/components/python_script/test_init.py +++ b/tests/components/python_script/test_init.py @@ -78,7 +78,7 @@ hass.states.set('test.entity', data.get('name', 'not set')) """ hass.async_add_executor_job(execute, hass, "test.py", source, {"name": "paulus"}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.is_state("test.entity", "paulus") @@ -96,7 +96,7 @@ print("This triggers warning.") """ hass.async_add_executor_job(execute, hass, "test.py", source, {}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert "Don't use print() inside scripts." in caplog.text @@ -111,7 +111,7 @@ logger.info('Logging from inside script') """ hass.async_add_executor_job(execute, hass, "test.py", source, {}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert "Logging from inside script" in caplog.text @@ -126,7 +126,7 @@ this is not valid Python """ hass.async_add_executor_job(execute, hass, "test.py", source, {}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert "Error loading script test.py" in caplog.text @@ -140,8 +140,8 @@ async def test_execute_runtime_error( raise Exception('boom') """ - hass.async_add_executor_job(execute, hass, "test.py", source, {}) - await hass.async_block_till_done() + await hass.async_add_executor_job(execute, hass, "test.py", source, {}) + await hass.async_block_till_done(wait_background_tasks=True) assert "Error executing script" in caplog.text @@ -153,7 +153,7 @@ raise Exception('boom') """ task = hass.async_add_executor_job(execute, hass, "test.py", source, {}, True) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert type(task.exception()) == HomeAssistantError assert "Error executing script (Exception): boom" in str(task.exception()) @@ -168,7 +168,7 @@ async def test_accessing_async_methods( hass.async_stop() """ - hass.async_add_executor_job(execute, hass, "test.py", source, {}) + await hass.async_add_executor_job(execute, hass, "test.py", source, {}) await hass.async_block_till_done() assert "Not allowed to access async methods" in caplog.text @@ -181,7 +181,7 @@ hass.async_stop() """ task = hass.async_add_executor_job(execute, hass, "test.py", source, {}, True) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert type(task.exception()) == ServiceValidationError assert "Not allowed to access async methods" in str(task.exception()) @@ -198,7 +198,7 @@ mylist = [1, 2, 3, 4] logger.info('Logging from inside script: %s %s' % (mydict["a"], mylist[2])) """ - hass.async_add_executor_job(execute, hass, "test.py", source, {}) + await hass.async_add_executor_job(execute, hass, "test.py", source, {}) await hass.async_block_till_done() assert "Logging from inside script: 1 3" in caplog.text @@ -217,7 +217,7 @@ async def test_accessing_forbidden_methods( "time.tzset()": "TimeWrapper.tzset", }.items(): caplog.records.clear() - hass.async_add_executor_job(execute, hass, "test.py", source, {}) + await hass.async_add_executor_job(execute, hass, "test.py", source, {}) await hass.async_block_till_done() assert f"Not allowed to access {name}" in caplog.text @@ -231,7 +231,7 @@ async def test_accessing_forbidden_methods_with_response(hass: HomeAssistant) -> "time.tzset()": "TimeWrapper.tzset", }.items(): task = hass.async_add_executor_job(execute, hass, "test.py", source, {}, True) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert type(task.exception()) == ServiceValidationError assert f"Not allowed to access {name}" in str(task.exception()) @@ -244,7 +244,7 @@ for i in [1, 2]: hass.states.set('hello.{}'.format(i), 'world') """ - hass.async_add_executor_job(execute, hass, "test.py", source, {}) + await hass.async_add_executor_job(execute, hass, "test.py", source, {}) await hass.async_block_till_done() assert hass.states.is_state("hello.1", "world") @@ -279,7 +279,7 @@ hass.states.set('hello.ab_list', '{}'.format(ab_list)) """ hass.async_add_executor_job(execute, hass, "test.py", source, {}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.is_state("hello.a", "1") assert hass.states.is_state("hello.b", "2") @@ -302,7 +302,7 @@ hass.states.set('hello.b', a[1]) hass.states.set('hello.c', a[2]) """ hass.async_add_executor_job(execute, hass, "test.py", source, {}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.is_state("hello.a", "1") assert hass.states.is_state("hello.b", "2") @@ -325,7 +325,7 @@ hass.states.set('module.datetime', """ hass.async_add_executor_job(execute, hass, "test.py", source, {}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.is_state("module.time", "1986") assert hass.states.is_state("module.time_strptime", "12:34") @@ -351,7 +351,7 @@ def b(): b() """ hass.async_add_executor_job(execute, hass, "test.py", source, {}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.is_state("hello.a", "one") assert hass.states.is_state("hello.b", "two") @@ -517,7 +517,7 @@ time.sleep(5) with patch("homeassistant.components.python_script.time.sleep"): hass.async_add_executor_job(execute, hass, "test.py", source, {}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert caplog.text.count("time.sleep") == 1 @@ -664,7 +664,7 @@ hass.states.set('hello.c', c) """ hass.async_add_executor_job(execute, hass, "aug_assign.py", source, {}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get("hello.a").state == str(((10 + 20) * 5) - 8) assert hass.states.get("hello.b").state == ("foo" + "bar") * 2 @@ -686,5 +686,5 @@ async def test_prohibited_augmented_assignment_operations( ) -> None: """Test that prohibited augmented assignment operations raise an error.""" hass.async_add_executor_job(execute, hass, "aug_assign_prohibited.py", case, {}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert error in caplog.text diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index f874b92305b..db4f3f0e41f 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -200,7 +200,7 @@ async def test_setup_websocket_2( next_update = mock_now + timedelta(minutes=5) freezer.move_to(next_update) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(entity_id) assert state @@ -225,7 +225,7 @@ async def test_setup_encrypted_websocket( next_update = mock_now + timedelta(minutes=5) freezer.move_to(next_update) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state @@ -242,7 +242,7 @@ async def test_update_on( next_update = mock_now + timedelta(minutes=5) freezer.move_to(next_update) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == STATE_ON @@ -262,7 +262,7 @@ async def test_update_off( next_update = mock_now + timedelta(minutes=5) freezer.move_to(next_update) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == STATE_UNAVAILABLE @@ -290,7 +290,7 @@ async def test_update_off_ws_no_power_state( next_update = mock_now + timedelta(minutes=5) freezer.move_to(next_update) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == STATE_OFF diff --git a/tests/components/schlage/test_binary_sensor.py b/tests/components/schlage/test_binary_sensor.py index 4673f263c8c..97f11577b86 100644 --- a/tests/components/schlage/test_binary_sensor.py +++ b/tests/components/schlage/test_binary_sensor.py @@ -22,7 +22,7 @@ async def test_keypad_disabled_binary_sensor( # Make the coordinator refresh data. async_fire_time_changed(hass, utcnow() + timedelta(seconds=31)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) keypad = hass.states.get("binary_sensor.vault_door_keypad_disabled") assert keypad is not None @@ -43,7 +43,7 @@ async def test_keypad_disabled_binary_sensor_use_previous_logs_on_failure( # Make the coordinator refresh data. async_fire_time_changed(hass, utcnow() + timedelta(seconds=31)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) keypad = hass.states.get("binary_sensor.vault_door_keypad_disabled") assert keypad is not None diff --git a/tests/components/schlage/test_lock.py b/tests/components/schlage/test_lock.py index 0972aa97033..5b26da7b27e 100644 --- a/tests/components/schlage/test_lock.py +++ b/tests/components/schlage/test_lock.py @@ -59,7 +59,7 @@ async def test_changed_by( # Make the coordinator refresh data. async_fire_time_changed(hass, utcnow() + timedelta(seconds=31)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) mock_lock.last_changed_by.assert_called_once_with() lock_device = hass.states.get("lock.vault_door") diff --git a/tests/components/scrape/test_sensor.py b/tests/components/scrape/test_sensor.py index 41da2eb9a79..4d9c2b732dc 100644 --- a/tests/components/scrape/test_sensor.py +++ b/tests/components/scrape/test_sensor.py @@ -261,7 +261,7 @@ async def test_scrape_sensor_no_data_refresh(hass: HomeAssistant) -> None: mocker.payload = "test_scrape_sensor_no_data" async_fire_time_changed(hass, dt_util.utcnow() + DEFAULT_SCAN_INTERVAL) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.ha_version") assert state is not None @@ -541,7 +541,7 @@ async def test_templates_with_yaml(hass: HomeAssistant) -> None: hass, dt_util.utcnow() + timedelta(minutes=10), ) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.get_values_with_template") assert state.state == "Current Version: 2021.12.10" @@ -555,7 +555,7 @@ async def test_templates_with_yaml(hass: HomeAssistant) -> None: hass, dt_util.utcnow() + timedelta(minutes=20), ) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.get_values_with_template") assert state.state == STATE_UNAVAILABLE @@ -568,7 +568,7 @@ async def test_templates_with_yaml(hass: HomeAssistant) -> None: hass, dt_util.utcnow() + timedelta(minutes=30), ) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.get_values_with_template") assert state.state == "Current Version: 2021.12.10" @@ -608,7 +608,7 @@ async def test_availability( hass.states.async_set("sensor.input1", "on") freezer.tick(timedelta(minutes=10)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.current_version") assert state.state == "2021.12.10" @@ -618,7 +618,7 @@ async def test_availability( freezer.tick(timedelta(minutes=10)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.current_version") assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/solaredge/test_coordinator.py b/tests/components/solaredge/test_coordinator.py index 4bd9dee930c..b1496d18d93 100644 --- a/tests/components/solaredge/test_coordinator.py +++ b/tests/components/solaredge/test_coordinator.py @@ -53,7 +53,7 @@ async def test_solaredgeoverviewdataservice_energy_values_validity( mock_solaredge().get_overview.return_value = mock_overview_data freezer.tick(OVERVIEW_UPDATE_DELAY) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.solaredge_lifetime_energy") assert state assert state.state == str(mock_overview_data["overview"]["lifeTimeData"]["energy"]) @@ -63,7 +63,7 @@ async def test_solaredgeoverviewdataservice_energy_values_validity( mock_solaredge().get_overview.return_value = mock_overview_data freezer.tick(OVERVIEW_UPDATE_DELAY) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.solaredge_lifetime_energy") assert state @@ -74,7 +74,7 @@ async def test_solaredgeoverviewdataservice_energy_values_validity( mock_solaredge().get_overview.return_value = mock_overview_data freezer.tick(OVERVIEW_UPDATE_DELAY) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.solaredge_lifetime_energy") assert state @@ -85,7 +85,7 @@ async def test_solaredgeoverviewdataservice_energy_values_validity( mock_solaredge().get_overview.return_value = mock_overview_data freezer.tick(OVERVIEW_UPDATE_DELAY) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.solaredge_energy_this_year") assert state @@ -103,7 +103,7 @@ async def test_solaredgeoverviewdataservice_energy_values_validity( mock_solaredge().get_overview.return_value = mock_overview_data freezer.tick(OVERVIEW_UPDATE_DELAY) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.solaredge_lifetime_energy") assert state diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 0b3834992d8..00858a180a3 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -94,8 +94,9 @@ def async_setup_sonos(hass, config_entry, fire_zgs_event): async def _wrapper(): config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) await fire_zgs_event() + await hass.async_block_till_done(wait_background_tasks=True) return _wrapper diff --git a/tests/components/sonos/test_repairs.py b/tests/components/sonos/test_repairs.py index cc1f59c5cd0..cf64912e498 100644 --- a/tests/components/sonos/test_repairs.py +++ b/tests/components/sonos/test_repairs.py @@ -28,10 +28,12 @@ async def test_subscription_repair_issues( config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() + await hass.async_block_till_done() # Ensure an issue is registered on subscription failure + sub_callback = subscription.callback async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert issue_registry.async_get_issue(DOMAIN, SUB_FAIL_ISSUE_ID) # Ensure the issue still exists after reload @@ -42,7 +44,6 @@ async def test_subscription_repair_issues( # Ensure the issue has been removed after a successful subscription callback variables = {"ZoneGroupState": zgs_discovery} event = SonosMockEvent(soco, soco.zoneGroupTopology, variables) - sub_callback = subscription.callback sub_callback(event) await hass.async_block_till_done() assert not issue_registry.async_get_issue(DOMAIN, SUB_FAIL_ISSUE_ID) diff --git a/tests/components/sonos/test_sensor.py b/tests/components/sonos/test_sensor.py index 6e4461e5397..1f4ba8d22cd 100644 --- a/tests/components/sonos/test_sensor.py +++ b/tests/components/sonos/test_sensor.py @@ -26,6 +26,7 @@ async def test_entity_registry_unsupported( soco.get_battery_info.side_effect = NotSupportedException await async_setup_sonos() + await hass.async_block_till_done(wait_background_tasks=True) assert "media_player.zone_a" in entity_registry.entities assert "sensor.zone_a_battery" not in entity_registry.entities @@ -36,6 +37,8 @@ async def test_entity_registry_supported( hass: HomeAssistant, async_autosetup_sonos, soco, entity_registry: er.EntityRegistry ) -> None: """Test sonos device with battery registered in the device registry.""" + await hass.async_block_till_done(wait_background_tasks=True) + assert "media_player.zone_a" in entity_registry.entities assert "sensor.zone_a_battery" in entity_registry.entities assert "binary_sensor.zone_a_charging" in entity_registry.entities @@ -69,6 +72,7 @@ async def test_battery_on_s1( soco.get_battery_info.return_value = {} await async_setup_sonos() + await hass.async_block_till_done(wait_background_tasks=True) subscription = soco.deviceProperties.subscribe.return_value sub_callback = subscription.callback @@ -78,7 +82,7 @@ async def test_battery_on_s1( # Update the speaker with a callback event sub_callback(device_properties_event) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) battery = entity_registry.entities["sensor.zone_a_battery"] battery_state = hass.states.get(battery.entity_id) @@ -101,6 +105,7 @@ async def test_device_payload_without_battery( soco.get_battery_info.return_value = None await async_setup_sonos() + await hass.async_block_till_done(wait_background_tasks=True) subscription = soco.deviceProperties.subscribe.return_value sub_callback = subscription.callback @@ -109,7 +114,7 @@ async def test_device_payload_without_battery( device_properties_event.variables["more_info"] = bad_payload sub_callback(device_properties_event) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert bad_payload in caplog.text @@ -125,6 +130,7 @@ async def test_device_payload_without_battery_and_ignored_keys( soco.get_battery_info.return_value = None await async_setup_sonos() + await hass.async_block_till_done(wait_background_tasks=True) subscription = soco.deviceProperties.subscribe.return_value sub_callback = subscription.callback @@ -133,7 +139,7 @@ async def test_device_payload_without_battery_and_ignored_keys( device_properties_event.variables["more_info"] = ignored_payload sub_callback(device_properties_event) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert ignored_payload not in caplog.text @@ -150,7 +156,7 @@ async def test_audio_input_sensor( subscription = soco.avTransport.subscribe.return_value sub_callback = subscription.callback sub_callback(tv_event) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) audio_input_sensor = entity_registry.entities["sensor.zone_a_audio_input_format"] audio_input_state = hass.states.get(audio_input_sensor.entity_id) @@ -161,7 +167,7 @@ async def test_audio_input_sensor( type(soco).soundbar_audio_input_format = no_input_mock async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) no_input_mock.assert_called_once() audio_input_state = hass.states.get(audio_input_sensor.entity_id) @@ -169,13 +175,13 @@ async def test_audio_input_sensor( # Ensure state is not polled when source is not TV and state is already "No input" sub_callback(no_media_event) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) unpolled_mock = PropertyMock(return_value="Will not be polled") type(soco).soundbar_audio_input_format = unpolled_mock async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) unpolled_mock.assert_not_called() audio_input_state = hass.states.get(audio_input_sensor.entity_id) @@ -199,7 +205,7 @@ async def test_microphone_binary_sensor( # Update the speaker with a callback event subscription = soco.deviceProperties.subscribe.return_value subscription.callback(device_properties_event) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) mic_binary_sensor_state = hass.states.get(mic_binary_sensor.entity_id) assert mic_binary_sensor_state.state == STATE_ON @@ -225,17 +231,18 @@ async def test_favorites_sensor( empty_event = SonosMockEvent(soco, service, {}) subscription = service.subscribe.return_value subscription.callback(event=empty_event) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # Reload the integration to enable the sensor async_fire_time_changed( hass, dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), ) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # Trigger subscription callback for speaker discovery await fire_zgs_event() + await hass.async_block_till_done(wait_background_tasks=True) favorites_updated_event = SonosMockEvent( soco, service, {"favorites_update_id": "2", "container_update_i_ds": "FV:2,2"} @@ -245,4 +252,4 @@ async def test_favorites_sensor( return_value=True, ): subscription.callback(event=favorites_updated_event) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) diff --git a/tests/components/sonos/test_speaker.py b/tests/components/sonos/test_speaker.py index e0fc4c3baf9..2c4357060be 100644 --- a/tests/components/sonos/test_speaker.py +++ b/tests/components/sonos/test_speaker.py @@ -12,9 +12,20 @@ from tests.common import async_fire_time_changed async def test_fallback_to_polling( - hass: HomeAssistant, async_autosetup_sonos, soco, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + config_entry, + soco, + fire_zgs_event, + caplog: pytest.LogCaptureFixture, ) -> None: """Test that polling fallback works.""" + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + # Do not wait on background tasks here because the + # subscription callback will fire an unsub the polling check + await hass.async_block_till_done() + await fire_zgs_event() + speaker = list(hass.data[DATA_SONOS].discovered.values())[0] assert speaker.soco is soco assert speaker._subscriptions @@ -30,7 +41,7 @@ async def test_fallback_to_polling( ), ): async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert not speaker._subscriptions assert speaker.subscriptions_failed @@ -46,6 +57,7 @@ async def test_subscription_creation_fails( side_effect=ConnectionError("Took too long"), ): await async_setup_sonos() + await hass.async_block_till_done(wait_background_tasks=True) speaker = list(hass.data[DATA_SONOS].discovered.values())[0] assert not speaker._subscriptions diff --git a/tests/components/soundtouch/test_media_player.py b/tests/components/soundtouch/test_media_player.py index 94e6965a571..61d0c7b4ea5 100644 --- a/tests/components/soundtouch/test_media_player.py +++ b/tests/components/soundtouch/test_media_player.py @@ -665,7 +665,7 @@ async def test_zone_attributes( hass, dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), ) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) entity_1_state = hass.states.get(DEVICE_1_ENTITY_ID) assert entity_1_state.attributes[ATTR_SOUNDTOUCH_ZONE]["is_master"] diff --git a/tests/components/speedtestdotnet/test_init.py b/tests/components/speedtestdotnet/test_init.py index 5083f56a8e2..2b0f803eb6f 100644 --- a/tests/components/speedtestdotnet/test_init.py +++ b/tests/components/speedtestdotnet/test_init.py @@ -74,7 +74,7 @@ async def test_server_not_found(hass: HomeAssistant, mock_api: MagicMock) -> Non hass, dt_util.utcnow() + timedelta(minutes=61), ) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.speedtest_ping") assert state is not None assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/systemmonitor/test_binary_sensor.py b/tests/components/systemmonitor/test_binary_sensor.py index 51c8fc87a3a..e3fbdedc081 100644 --- a/tests/components/systemmonitor/test_binary_sensor.py +++ b/tests/components/systemmonitor/test_binary_sensor.py @@ -97,7 +97,7 @@ async def test_sensor_process_fails( freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) process_sensor = hass.states.get("binary_sensor.system_monitor_process_python3") assert process_sensor is not None diff --git a/tests/components/systemmonitor/test_sensor.py b/tests/components/systemmonitor/test_sensor.py index 11dd002c2f7..a11112d8f86 100644 --- a/tests/components/systemmonitor/test_sensor.py +++ b/tests/components/systemmonitor/test_sensor.py @@ -232,7 +232,7 @@ async def test_sensor_updating( mock_psutil.virtual_memory.side_effect = Exception("Failed to update") freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) memory_sensor = hass.states.get("sensor.system_monitor_memory_free") assert memory_sensor is not None @@ -248,7 +248,7 @@ async def test_sensor_updating( ) freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) memory_sensor = hass.states.get("sensor.system_monitor_memory_free") assert memory_sensor is not None @@ -293,7 +293,7 @@ async def test_sensor_process_fails( freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) process_sensor = hass.states.get("sensor.system_monitor_process_python3") assert process_sensor is not None @@ -330,7 +330,7 @@ async def test_sensor_network_sensors( freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) network_out_sensor = hass.states.get("sensor.system_monitor_network_out_eth1") packets_out_sensor = hass.states.get("sensor.system_monitor_packets_out_eth1") @@ -362,7 +362,7 @@ async def test_sensor_network_sensors( freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) network_out_sensor = hass.states.get("sensor.system_monitor_network_out_eth1") packets_out_sensor = hass.states.get("sensor.system_monitor_packets_out_eth1") @@ -470,7 +470,7 @@ async def test_exception_handling_disk_sensor( freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert "OS error for /" in caplog.text @@ -483,7 +483,7 @@ async def test_exception_handling_disk_sensor( freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert "OS error for /" in caplog.text @@ -498,7 +498,7 @@ async def test_exception_handling_disk_sensor( freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) disk_sensor = hass.states.get("sensor.system_monitor_disk_free") assert disk_sensor is not None @@ -528,7 +528,7 @@ async def test_cpu_percentage_is_zero_returns_unknown( freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) cpu_sensor = hass.states.get("sensor.system_monitor_processor_use") assert cpu_sensor is not None @@ -538,7 +538,7 @@ async def test_cpu_percentage_is_zero_returns_unknown( freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) cpu_sensor = hass.states.get("sensor.system_monitor_processor_use") assert cpu_sensor is not None @@ -573,7 +573,7 @@ async def test_remove_obsolete_entities( ) freezer.tick(timedelta(minutes=5)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # Fake an entity which should be removed as not supported and disabled entity_registry.async_get_or_create( diff --git a/tests/components/tcp/test_binary_sensor.py b/tests/components/tcp/test_binary_sensor.py index 959c1f050fd..05aa2a471db 100644 --- a/tests/components/tcp/test_binary_sensor.py +++ b/tests/components/tcp/test_binary_sensor.py @@ -79,7 +79,7 @@ async def test_state(hass: HomeAssistant, mock_socket, now) -> None: mock_socket.recv.return_value = b"on" async_fire_time_changed(hass, now + timedelta(seconds=45)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(TEST_ENTITY) diff --git a/tests/components/temper/test_sensor.py b/tests/components/temper/test_sensor.py index 94c44cc4296..d1e74f1ab0f 100644 --- a/tests/components/temper/test_sensor.py +++ b/tests/components/temper/test_sensor.py @@ -29,7 +29,7 @@ async def test_temperature_readback(hass: HomeAssistant) -> None: await hass.async_block_till_done() async_fire_time_changed(hass, utcnow + timedelta(seconds=70)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) temperature = hass.states.get("sensor.mydevicename") assert temperature diff --git a/tests/components/totalconnect/test_alarm_control_panel.py b/tests/components/totalconnect/test_alarm_control_panel.py index 7ac6540f1ff..fa2e997756d 100644 --- a/tests/components/totalconnect/test_alarm_control_panel.py +++ b/tests/components/totalconnect/test_alarm_control_panel.py @@ -548,30 +548,30 @@ async def test_other_update_failures(hass: HomeAssistant) -> None: # then an error: ServiceUnavailable --> UpdateFailed async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE assert mock_request.call_count == 2 # works again async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL * 2) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED assert mock_request.call_count == 3 # then an error: TotalConnectError --> UpdateFailed async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL * 3) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE assert mock_request.call_count == 4 # works again async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL * 4) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED assert mock_request.call_count == 5 # unknown TotalConnect status via ValueError async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL * 5) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE assert mock_request.call_count == 6 diff --git a/tests/components/uvc/test_camera.py b/tests/components/uvc/test_camera.py index 12203a3e222..522448ecfc4 100644 --- a/tests/components/uvc/test_camera.py +++ b/tests/components/uvc/test_camera.py @@ -278,7 +278,7 @@ async def test_setup_nvr_errors_during_indexing( mock_remote.return_value.index.side_effect = None async_fire_time_changed(hass, now + timedelta(seconds=31)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) camera_states = hass.states.async_all("camera") @@ -313,7 +313,7 @@ async def test_setup_nvr_errors_during_initialization( mock_remote.side_effect = None async_fire_time_changed(hass, now + timedelta(seconds=31)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) camera_states = hass.states.async_all("camera") @@ -362,7 +362,7 @@ async def test_motion_recording_mode_properties( ] = True async_fire_time_changed(hass, now + timedelta(seconds=31)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("camera.front") @@ -375,7 +375,7 @@ async def test_motion_recording_mode_properties( mock_remote.return_value.get_camera.return_value["recordingIndicator"] = "DISABLED" async_fire_time_changed(hass, now + timedelta(seconds=61)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("camera.front") @@ -387,7 +387,7 @@ async def test_motion_recording_mode_properties( ) async_fire_time_changed(hass, now + timedelta(seconds=91)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("camera.front") @@ -399,7 +399,7 @@ async def test_motion_recording_mode_properties( ) async_fire_time_changed(hass, now + timedelta(seconds=121)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("camera.front") diff --git a/tests/components/ws66i/test_media_player.py b/tests/components/ws66i/test_media_player.py index eec6bf191f7..c13f6cbd738 100644 --- a/tests/components/ws66i/test_media_player.py +++ b/tests/components/ws66i/test_media_player.py @@ -195,7 +195,7 @@ async def test_update(hass: HomeAssistant, freezer: FrozenDateTimeFactory) -> No with patch.object(MockWs66i, "open") as method_call: freezer.tick(POLL_INTERVAL) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert not method_call.called @@ -226,13 +226,13 @@ async def test_failed_update( freezer.tick(POLL_INTERVAL) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # Failed update, close called with patch.object(MockWs66i, "zone_status", return_value=None): freezer.tick(POLL_INTERVAL) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.is_state(ZONE_1_ID, STATE_UNAVAILABLE) @@ -240,12 +240,12 @@ async def test_failed_update( with patch.object(MockWs66i, "zone_status", return_value=None): freezer.tick(POLL_INTERVAL) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # A connection re-attempt succeeds freezer.tick(POLL_INTERVAL) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # confirm entity is back on state = hass.states.get(ZONE_1_ID) @@ -315,7 +315,7 @@ async def test_source_select( freezer.tick(POLL_INTERVAL) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ZONE_1_ID) @@ -370,14 +370,14 @@ async def test_volume_up_down( ) freezer.tick(POLL_INTERVAL) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # should not go below zero assert ws66i.zones[11].volume == 0 await _call_media_player_service(hass, SERVICE_VOLUME_UP, {"entity_id": ZONE_1_ID}) freezer.tick(POLL_INTERVAL) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert ws66i.zones[11].volume == 1 await _call_media_player_service( @@ -385,14 +385,14 @@ async def test_volume_up_down( ) freezer.tick(POLL_INTERVAL) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert ws66i.zones[11].volume == MAX_VOL await _call_media_player_service(hass, SERVICE_VOLUME_UP, {"entity_id": ZONE_1_ID}) freezer.tick(POLL_INTERVAL) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # should not go above 38 (MAX_VOL) assert ws66i.zones[11].volume == MAX_VOL diff --git a/tests/components/xiaomi_miio/test_vacuum.py b/tests/components/xiaomi_miio/test_vacuum.py index c5345386777..2cfc3a4f294 100644 --- a/tests/components/xiaomi_miio/test_vacuum.py +++ b/tests/components/xiaomi_miio/test_vacuum.py @@ -238,7 +238,7 @@ async def test_xiaomi_exceptions(hass: HomeAssistant, mock_mirobo_is_on) -> None mock_mirobo_is_on.status.side_effect = DeviceException("dummy exception") future = dt_util.utcnow() + timedelta(seconds=60) async_fire_time_changed(hass, future) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert not is_available() @@ -247,7 +247,7 @@ async def test_xiaomi_exceptions(hass: HomeAssistant, mock_mirobo_is_on) -> None mock_mirobo_is_on.status.reset_mock() future += timedelta(seconds=60) async_fire_time_changed(hass, future) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert not is_available() assert mock_mirobo_is_on.status.call_count == 1 diff --git a/tests/components/yale_smart_alarm/test_coordinator.py b/tests/components/yale_smart_alarm/test_coordinator.py index 5125c817567..6f1125fcf65 100644 --- a/tests/components/yale_smart_alarm/test_coordinator.py +++ b/tests/components/yale_smart_alarm/test_coordinator.py @@ -76,7 +76,7 @@ async def test_coordinator_setup_and_update_errors( client.get_all.side_effect = ConnectionError("Could not connect") async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=1)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) client.get_all.assert_called_once() state = hass.states.get("alarm_control_panel.yale_smart_alarm") assert state.state == STATE_UNAVAILABLE @@ -84,7 +84,7 @@ async def test_coordinator_setup_and_update_errors( client.get_all.side_effect = ConnectionError("Could not connect") async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=2)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) client.get_all.assert_called_once() state = hass.states.get("alarm_control_panel.yale_smart_alarm") assert state.state == STATE_UNAVAILABLE @@ -92,7 +92,7 @@ async def test_coordinator_setup_and_update_errors( client.get_all.side_effect = TimeoutError("Could not connect") async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=3)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) client.get_all.assert_called_once() state = hass.states.get("alarm_control_panel.yale_smart_alarm") assert state.state == STATE_UNAVAILABLE @@ -100,7 +100,7 @@ async def test_coordinator_setup_and_update_errors( client.get_all.side_effect = UnknownError("info") async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=4)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) client.get_all.assert_called_once() state = hass.states.get("alarm_control_panel.yale_smart_alarm") assert state.state == STATE_UNAVAILABLE @@ -110,7 +110,7 @@ async def test_coordinator_setup_and_update_errors( client.get_all.return_value = load_json client.get_armed_status.return_value = YALE_STATE_ARM_FULL async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) client.get_all.assert_called_once() state = hass.states.get("alarm_control_panel.yale_smart_alarm") assert state.state == STATE_ALARM_ARMED_AWAY @@ -118,7 +118,7 @@ async def test_coordinator_setup_and_update_errors( client.get_all.side_effect = AuthenticationError("Can not authenticate") async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=6)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) client.get_all.assert_called_once() state = hass.states.get("alarm_control_panel.yale_smart_alarm") assert state.state == STATE_UNAVAILABLE diff --git a/tests/test_core.py b/tests/test_core.py index 11fda50a180..a0a197096cd 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -588,6 +588,46 @@ async def test_async_get_hass_can_be_called(hass: HomeAssistant) -> None: my_job_create_task.join() +async def test_async_add_executor_job_background(hass: HomeAssistant) -> None: + """Test running an executor job in the background.""" + calls = [] + + def job(): + time.sleep(0.01) + calls.append(1) + + async def _async_add_executor_job(): + await hass.async_add_executor_job(job) + + task = hass.async_create_background_task( + _async_add_executor_job(), "background", eager_start=True + ) + await hass.async_block_till_done() + assert len(calls) == 0 + await hass.async_block_till_done(wait_background_tasks=True) + assert len(calls) == 1 + await task + + +async def test_async_add_executor_job(hass: HomeAssistant) -> None: + """Test running an executor job.""" + calls = [] + + def job(): + time.sleep(0.01) + calls.append(1) + + async def _async_add_executor_job(): + await hass.async_add_executor_job(job) + + task = hass.async_create_task( + _async_add_executor_job(), "background", eager_start=True + ) + await hass.async_block_till_done() + assert len(calls) == 1 + await task + + async def test_stage_shutdown(hass: HomeAssistant) -> None: """Simulate a shutdown, test calling stuff.""" test_stop = async_capture_events(hass, EVENT_HOMEASSISTANT_STOP) From 6587ee20db2c5fc8d3e8b101abdd83f36c7388bd Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Sat, 30 Mar 2024 10:37:59 +0100 Subject: [PATCH 088/967] Enable Ruff TRY300 (#114437) * Enable Ruff TRY300 * Update validation.py * Address review comments --- .../components/amberelectric/config_flow.py | 9 ++++--- .../components/apple_tv/config_flow.py | 2 +- homeassistant/components/auth/indieauth.py | 2 +- homeassistant/components/awair/config_flow.py | 9 +++---- homeassistant/components/awair/coordinator.py | 2 +- homeassistant/components/buienradar/sensor.py | 9 ++++--- .../components/cert_expiry/config_flow.py | 3 ++- homeassistant/components/citybikes/sensor.py | 5 ++-- .../components/cloud/alexa_config.py | 5 +--- homeassistant/components/cloud/http_api.py | 2 +- .../components/command_line/utils.py | 21 ++++++++-------- .../components/control4/config_flow.py | 4 +-- homeassistant/components/deconz/hub/api.py | 2 +- homeassistant/components/dominos/__init__.py | 2 +- homeassistant/components/doorbird/camera.py | 5 ++-- homeassistant/components/dovado/__init__.py | 5 ++-- homeassistant/components/ebusd/__init__.py | 25 +++++++++---------- .../components/ecoforest/coordinator.py | 5 ++-- homeassistant/components/enocean/dongle.py | 2 +- .../components/enphase_envoy/coordinator.py | 4 +-- homeassistant/components/freebox/router.py | 2 +- homeassistant/components/fritz/common.py | 6 +++-- .../components/fritzbox/config_flow.py | 2 +- .../fritzbox_callmonitor/config_flow.py | 6 ++--- homeassistant/components/hassio/data.py | 12 ++++----- homeassistant/components/hassio/http.py | 10 +++----- homeassistant/components/heos/__init__.py | 5 ++-- .../components/hitron_coda/device_tracker.py | 2 +- .../components/homekit/type_cameras.py | 3 ++- homeassistant/components/homekit/util.py | 8 +++--- .../components/homematicip_cloud/hap.py | 4 +-- homeassistant/components/huawei_lte/sensor.py | 2 +- homeassistant/components/image/__init__.py | 2 +- homeassistant/components/imap/coordinator.py | 5 ++-- .../components/insteon/config_flow.py | 5 ++-- .../components/integration/sensor.py | 2 +- homeassistant/components/kef/media_player.py | 8 +++--- homeassistant/components/knx/validation.py | 22 ++++++++-------- homeassistant/components/litterrobot/hub.py | 1 - homeassistant/components/lyric/__init__.py | 2 +- .../components/meteoclimatic/__init__.py | 2 +- homeassistant/components/mikrotik/hub.py | 5 ++-- homeassistant/components/mysensors/gateway.py | 2 +- .../components/nextbus/coordinator.py | 2 +- .../components/nissan_leaf/__init__.py | 2 +- homeassistant/components/obihai/sensor.py | 12 ++++----- homeassistant/components/onvif/device.py | 3 ++- homeassistant/components/onvif/parsers.py | 2 +- .../components/owntracks/messages.py | 4 +-- .../components/panasonic_viera/__init__.py | 9 ++++--- .../components/picnic/coordinator.py | 6 ++--- homeassistant/components/ping/helpers.py | 2 +- .../components/prosegur/config_flow.py | 2 +- homeassistant/components/qvr_pro/camera.py | 17 ++++++------- homeassistant/components/recorder/core.py | 3 ++- .../components/recorder/migration.py | 10 +++++--- .../components/recorder/statistics.py | 2 +- homeassistant/components/recorder/util.py | 6 +++-- .../components/renault/coordinator.py | 5 ++-- homeassistant/components/roku/coordinator.py | 10 ++++---- .../components/rympro/coordinator.py | 2 +- .../components/shell_command/__init__.py | 2 +- homeassistant/components/sms/gateway.py | 2 +- .../components/solarlog/config_flow.py | 4 +-- .../components/spotify/media_player.py | 4 +-- homeassistant/components/ssdp/__init__.py | 3 ++- .../components/suez_water/__init__.py | 2 +- .../components/system_health/__init__.py | 3 ++- homeassistant/components/tcp/common.py | 2 +- .../components/telegram_bot/__init__.py | 3 ++- .../components/transmission/__init__.py | 5 ++-- homeassistant/components/unifi/hub/api.py | 3 ++- homeassistant/components/update/__init__.py | 2 +- homeassistant/components/vallox/__init__.py | 9 +++---- .../components/viaggiatreno/sensor.py | 4 ++- homeassistant/components/vicare/utils.py | 7 +++--- homeassistant/components/webhook/__init__.py | 2 +- homeassistant/components/wiz/__init__.py | 2 +- .../components/xiaomi_miio/device.py | 7 +++--- homeassistant/components/xiaomi_miio/light.py | 7 +++--- .../components/xiaomi_miio/remote.py | 2 +- .../components/xiaomi_miio/switch.py | 16 ++++++------ .../components/xiaomi_miio/vacuum.py | 2 +- homeassistant/components/zamg/weather.py | 6 ++--- homeassistant/components/zha/core/device.py | 19 +++++++------- homeassistant/components/zha/core/helpers.py | 2 +- homeassistant/config.py | 4 +-- homeassistant/config_entries.py | 5 ++-- homeassistant/core.py | 3 ++- homeassistant/helpers/config_validation.py | 6 ++--- homeassistant/helpers/entity_platform.py | 8 +++--- homeassistant/helpers/entity_registry.py | 5 ++-- homeassistant/helpers/intent.py | 2 +- homeassistant/helpers/network.py | 2 +- homeassistant/setup.py | 2 +- pyproject.toml | 3 +-- .../components/gardena_bluetooth/conftest.py | 2 +- 97 files changed, 259 insertions(+), 243 deletions(-) diff --git a/homeassistant/components/amberelectric/config_flow.py b/homeassistant/components/amberelectric/config_flow.py index 174e8716e0b..a94700c27d1 100644 --- a/homeassistant/components/amberelectric/config_flow.py +++ b/homeassistant/components/amberelectric/config_flow.py @@ -60,10 +60,6 @@ class AmberElectricConfigFlow(ConfigFlow, domain=DOMAIN): try: sites: list[Site] = filter_sites(api.get_sites()) - if len(sites) == 0: - self._errors[CONF_API_TOKEN] = "no_site" - return None - return sites except amberelectric.ApiException as api_exception: if api_exception.status == 403: self._errors[CONF_API_TOKEN] = "invalid_api_token" @@ -71,6 +67,11 @@ class AmberElectricConfigFlow(ConfigFlow, domain=DOMAIN): self._errors[CONF_API_TOKEN] = "unknown_error" return None + if len(sites) == 0: + self._errors[CONF_API_TOKEN] = "no_site" + return None + return sites + async def async_step_user( self, user_input: dict[str, str] | None = None ) -> ConfigFlowResult: diff --git a/homeassistant/components/apple_tv/config_flow.py b/homeassistant/components/apple_tv/config_flow.py index 19cbb24d8a2..f9be827741b 100644 --- a/homeassistant/components/apple_tv/config_flow.py +++ b/homeassistant/components/apple_tv/config_flow.py @@ -76,9 +76,9 @@ async def device_scan( return None try: ip_address(identifier) - return [identifier] except ValueError: return None + return [identifier] # If we have an address, only probe that address to avoid # broadcast traffic on the network diff --git a/homeassistant/components/auth/indieauth.py b/homeassistant/components/auth/indieauth.py index 232f067b673..45de94d5a70 100644 --- a/homeassistant/components/auth/indieauth.py +++ b/homeassistant/components/auth/indieauth.py @@ -127,9 +127,9 @@ def verify_client_id(client_id: str) -> bool: """Verify that the client id is valid.""" try: _parse_client_id(client_id) - return True except ValueError: return False + return True def _parse_url(url: str) -> ParseResult: diff --git a/homeassistant/components/awair/config_flow.py b/homeassistant/components/awair/config_flow.py index d3c4703e89c..a6efc3640f9 100644 --- a/homeassistant/components/awair/config_flow.py +++ b/homeassistant/components/awair/config_flow.py @@ -250,13 +250,12 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN): try: user = await awair.user() devices = await user.devices() - if not devices: - return (None, "no_devices_found") - - return (user, None) - except AuthError: return (None, "invalid_access_token") except AwairError as err: LOGGER.error("Unexpected API error: %s", err) return (None, "unknown") + + if not devices: + return (None, "no_devices_found") + return (user, None) diff --git a/homeassistant/components/awair/coordinator.py b/homeassistant/components/awair/coordinator.py index 8e554b3b9e0..b63efff7733 100644 --- a/homeassistant/components/awair/coordinator.py +++ b/homeassistant/components/awair/coordinator.py @@ -111,7 +111,7 @@ class AwairLocalDataUpdateCoordinator(AwairDataUpdateCoordinator): devices = await self._awair.devices() self._device = devices[0] result = await self._fetch_air_data(self._device) - return {result.device.uuid: result} except AwairError as err: LOGGER.error("Unexpected API error: %s", err) raise UpdateFailed(err) from err + return {result.device.uuid: result} diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py index fb15aa49001..69c762c1bc1 100644 --- a/homeassistant/components/buienradar/sensor.py +++ b/homeassistant/components/buienradar/sensor.py @@ -819,22 +819,23 @@ class BrSensor(SensorEntity): self._attr_native_value = data.get(FORECAST)[fcday].get( sensor_type[:-3] ) - if self.state is not None: - self._attr_native_value = round(self.state * 3.6, 1) - return True except IndexError: _LOGGER.warning("No forecast for fcday=%s", fcday) return False + if self.state is not None: + self._attr_native_value = round(self.state * 3.6, 1) + return True + # update all other sensors try: self._attr_native_value = data.get(FORECAST)[fcday].get( sensor_type[:-3] ) - return True except IndexError: _LOGGER.warning("No forecast for fcday=%s", fcday) return False + return True if sensor_type == SYMBOL or sensor_type.startswith(CONDITION): # update weather symbol & status text diff --git a/homeassistant/components/cert_expiry/config_flow.py b/homeassistant/components/cert_expiry/config_flow.py index 60863523553..8f937ef61ea 100644 --- a/homeassistant/components/cert_expiry/config_flow.py +++ b/homeassistant/components/cert_expiry/config_flow.py @@ -43,7 +43,6 @@ class CertexpiryConfigFlow(ConfigFlow, domain=DOMAIN): user_input[CONF_HOST], user_input.get(CONF_PORT, DEFAULT_PORT), ) - return True except ResolveFailed: self._errors[CONF_HOST] = "resolve_failed" except ConnectionTimeout: @@ -52,6 +51,8 @@ class CertexpiryConfigFlow(ConfigFlow, domain=DOMAIN): self._errors[CONF_HOST] = "connection_refused" except ValidationFailure: return True + else: + return True return False async def async_step_user( diff --git a/homeassistant/components/citybikes/sensor.py b/homeassistant/components/citybikes/sensor.py index 0cf27c20fa6..de85e6309f9 100644 --- a/homeassistant/components/citybikes/sensor.py +++ b/homeassistant/components/citybikes/sensor.py @@ -228,6 +228,9 @@ class CityBikesNetworks: self.hass, NETWORKS_URI, NETWORKS_RESPONSE_SCHEMA ) self.networks = networks[ATTR_NETWORKS_LIST] + except CityBikesRequestError as err: + raise PlatformNotReady from err + else: result = None minimum_dist = None for network in self.networks: @@ -241,8 +244,6 @@ class CityBikesNetworks: result = network[ATTR_ID] return result - except CityBikesRequestError as err: - raise PlatformNotReady from err finally: self.networks_loading.release() diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index 12f2b04d856..467d1589398 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -509,16 +509,13 @@ class CloudAlexaConfig(alexa_config.AbstractConfig): try: async with asyncio.timeout(10): await asyncio.wait(tasks, return_when=asyncio.ALL_COMPLETED) - - return True - except TimeoutError: _LOGGER.warning("Timeout trying to sync entities to Alexa") return False - except aiohttp.ClientError as err: _LOGGER.warning("Error trying to sync entities to Alexa: %s", err) return False + return True async def _handle_entity_registry_updated(self, event: Event) -> None: """Handle when entity registry updated.""" diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 1a8fd7dbea9..8ca55876b28 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -135,12 +135,12 @@ def _handle_cloud_errors( """Handle exceptions that raise from the wrapped request handler.""" try: result = await handler(view, request, *args, **kwargs) - return result except Exception as err: # pylint: disable=broad-except status, msg = _process_cloud_exception(err, request.path) return view.json_message( msg, status_code=status, message_code=err.__class__.__name__.lower() ) + return result return error_handler diff --git a/homeassistant/components/command_line/utils.py b/homeassistant/components/command_line/utils.py index 067efc08e97..c1926546950 100644 --- a/homeassistant/components/command_line/utils.py +++ b/homeassistant/components/command_line/utils.py @@ -25,20 +25,21 @@ async def async_call_shell_with_timeout( ) async with asyncio.timeout(timeout): await proc.communicate() - return_code = proc.returncode - if return_code == _EXEC_FAILED_CODE: - _LOGGER.error("Error trying to exec command: %s", command) - elif log_return_code and return_code != 0: - _LOGGER.error( - "Command failed (with return code %s): %s", - proc.returncode, - command, - ) - return return_code or 0 except TimeoutError: _LOGGER.error("Timeout for command: %s", command) return -1 + return_code = proc.returncode + if return_code == _EXEC_FAILED_CODE: + _LOGGER.error("Error trying to exec command: %s", command) + elif log_return_code and return_code != 0: + _LOGGER.error( + "Command failed (with return code %s): %s", + proc.returncode, + command, + ) + return return_code or 0 + async def async_check_output_or_log(command: str, timeout: int) -> str | None: """Run a shell command with a timeout and return the output.""" diff --git a/homeassistant/components/control4/config_flow.py b/homeassistant/components/control4/config_flow.py index 2d7c6ade255..4ecc1ebe3f5 100644 --- a/homeassistant/components/control4/config_flow.py +++ b/homeassistant/components/control4/config_flow.py @@ -68,9 +68,9 @@ class Control4Validator: self.director_bearer_token = ( await account.getDirectorBearerToken(self.controller_unique_id) )["token"] - return True except (Unauthorized, NotFound): return False + return True async def connect_to_director(self) -> bool: """Test if we can connect to the local Control4 Director.""" @@ -82,10 +82,10 @@ class Control4Validator: self.host, self.director_bearer_token, director_session ) await director.getAllItemInfo() - return True except (Unauthorized, ClientError, TimeoutError): _LOGGER.error("Failed to connect to the Control4 controller") return False + return True class Control4ConfigFlow(ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/deconz/hub/api.py b/homeassistant/components/deconz/hub/api.py index 71551ead6e1..916c34672d8 100644 --- a/homeassistant/components/deconz/hub/api.py +++ b/homeassistant/components/deconz/hub/api.py @@ -26,7 +26,6 @@ async def get_deconz_api( try: async with asyncio.timeout(10): await api.refresh_state() - return api except errors.Unauthorized as err: LOGGER.warning("Invalid key for deCONZ at %s", config.host) @@ -35,3 +34,4 @@ async def get_deconz_api( except (TimeoutError, errors.RequestError, errors.ResponseError) as err: LOGGER.error("Error connecting to deCONZ gateway at %s", config.host) raise CannotConnect from err + return api diff --git a/homeassistant/components/dominos/__init__.py b/homeassistant/components/dominos/__init__.py index 48404e6dbee..ce7b36f2280 100644 --- a/homeassistant/components/dominos/__init__.py +++ b/homeassistant/components/dominos/__init__.py @@ -139,10 +139,10 @@ class Dominos: """Update the shared closest store (if open).""" try: self.closest_store = self.address.closest_store() - return True except StoreException: self.closest_store = None return False + return True def get_menu(self): """Return the products from the closest stores menu.""" diff --git a/homeassistant/components/doorbird/camera.py b/homeassistant/components/doorbird/camera.py index ddc8ae0bdc5..961a3287799 100644 --- a/homeassistant/components/doorbird/camera.py +++ b/homeassistant/components/doorbird/camera.py @@ -107,8 +107,6 @@ class DoorBirdCamera(DoorBirdEntity, Camera): response = await websession.get(self._url) self._last_image = await response.read() - self._last_update = now - return self._last_image except TimeoutError: _LOGGER.error("DoorBird %s: Camera image timed out", self.name) return self._last_image @@ -118,6 +116,9 @@ class DoorBirdCamera(DoorBirdEntity, Camera): ) return self._last_image + self._last_update = now + return self._last_image + async def async_added_to_hass(self) -> None: """Subscribe to events.""" await super().async_added_to_hass() diff --git a/homeassistant/components/dovado/__init__.py b/homeassistant/components/dovado/__init__.py index 60e8351cc24..e89fd4361a5 100644 --- a/homeassistant/components/dovado/__init__.py +++ b/homeassistant/components/dovado/__init__.py @@ -74,10 +74,11 @@ class DovadoData: if not self.state: return False self.state.update(connected=self.state.get("modem status") == "CONNECTED") - _LOGGER.debug("Received: %s", self.state) - return True except OSError as error: _LOGGER.warning("Could not contact the router: %s", error) + return None + _LOGGER.debug("Received: %s", self.state) + return True @property def client(self): diff --git a/homeassistant/components/ebusd/__init__.py b/homeassistant/components/ebusd/__init__.py index debfc335496..c9386999fae 100644 --- a/homeassistant/components/ebusd/__init__.py +++ b/homeassistant/components/ebusd/__init__.py @@ -67,21 +67,20 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: try: ebusdpy.init(server_address) - hass.data[DOMAIN] = EbusdData(server_address, circuit) - - sensor_config = { - CONF_MONITORED_CONDITIONS: monitored_conditions, - "client_name": name, - "sensor_types": SENSOR_TYPES[circuit], - } - load_platform(hass, Platform.SENSOR, DOMAIN, sensor_config, config) - - hass.services.register(DOMAIN, SERVICE_EBUSD_WRITE, hass.data[DOMAIN].write) - - _LOGGER.debug("Ebusd integration setup completed") - return True except (TimeoutError, OSError): return False + hass.data[DOMAIN] = EbusdData(server_address, circuit) + sensor_config = { + CONF_MONITORED_CONDITIONS: monitored_conditions, + "client_name": name, + "sensor_types": SENSOR_TYPES[circuit], + } + load_platform(hass, Platform.SENSOR, DOMAIN, sensor_config, config) + + hass.services.register(DOMAIN, SERVICE_EBUSD_WRITE, hass.data[DOMAIN].write) + + _LOGGER.debug("Ebusd integration setup completed") + return True class EbusdData: diff --git a/homeassistant/components/ecoforest/coordinator.py b/homeassistant/components/ecoforest/coordinator.py index ae0b353a1df..3b04325bd50 100644 --- a/homeassistant/components/ecoforest/coordinator.py +++ b/homeassistant/components/ecoforest/coordinator.py @@ -32,7 +32,8 @@ class EcoforestCoordinator(DataUpdateCoordinator[Device]): """Fetch all device and sensor data from api.""" try: data = await self.api.get() - _LOGGER.debug("Ecoforest data: %s", data) - return data except EcoforestError as err: raise UpdateFailed(f"Error communicating with API: {err}") from err + + _LOGGER.debug("Ecoforest data: %s", data) + return data diff --git a/homeassistant/components/enocean/dongle.py b/homeassistant/components/enocean/dongle.py index 6402b4c3a28..2d9a3f8787e 100644 --- a/homeassistant/components/enocean/dongle.py +++ b/homeassistant/components/enocean/dongle.py @@ -82,7 +82,7 @@ def validate_path(path: str): # Creating the serial communicator will raise an exception # if it cannot connect SerialCommunicator(port=path) - return True except serial.SerialException as exception: _LOGGER.warning("Dongle path %s is invalid: %s", path, str(exception)) return False + return True diff --git a/homeassistant/components/enphase_envoy/coordinator.py b/homeassistant/components/enphase_envoy/coordinator.py index c8152d44726..a508d5127d6 100644 --- a/homeassistant/components/enphase_envoy/coordinator.py +++ b/homeassistant/components/enphase_envoy/coordinator.py @@ -147,8 +147,6 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): self._async_mark_setup_complete() # dump all received data in debug mode to assist troubleshooting envoy_data = await envoy.update() - _LOGGER.debug("Envoy data: %s", envoy_data) - return envoy_data.raw except INVALID_AUTH_ERRORS as err: if self._setup_complete and tries == 0: # token likely expired or firmware changed, try to re-authenticate @@ -157,5 +155,7 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): raise ConfigEntryAuthFailed from err except EnvoyError as err: raise UpdateFailed(f"Error communicating with API: {err}") from err + _LOGGER.debug("Envoy data: %s", envoy_data) + return envoy_data.raw raise RuntimeError("Unreachable code in _async_update_data") # pragma: no cover diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index ef16a9df1b1..ed2fbcf1e83 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -43,7 +43,6 @@ def is_json(json_str: str) -> bool: """Validate if a String is a JSON value or not.""" try: json.loads(json_str) - return True except (ValueError, TypeError) as err: _LOGGER.error( "Failed to parse JSON '%s', error '%s'", @@ -51,6 +50,7 @@ def is_json(json_str: str) -> bool: err, ) return False + return True async def get_api(hass: HomeAssistant, host: str) -> Freepybox: diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 8e773e74c75..b9d77220d7c 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -789,24 +789,26 @@ class AvmWrapper(FritzBoxTools): # pylint: disable=hass-enforce-coordinator-mod **kwargs, ) ) - return result except FritzSecurityError: _LOGGER.exception( "Authorization Error: Please check the provided credentials and" " verify that you can log into the web interface" ) + return {} except FRITZ_EXCEPTIONS: _LOGGER.exception( "Service/Action Error: cannot execute service %s with action %s", service_name, action_name, ) + return {} except FritzConnectionException: _LOGGER.exception( "Connection Error: Please check the device is properly configured" " for remote login" ) - return {} + return {} + return result async def async_get_upnp_configuration(self) -> dict[str, Any]: """Call X_AVM-DE_UPnP service.""" diff --git a/homeassistant/components/fritzbox/config_flow.py b/homeassistant/components/fritzbox/config_flow.py index 377d46eceff..fe4cf82b29b 100644 --- a/homeassistant/components/fritzbox/config_flow.py +++ b/homeassistant/components/fritzbox/config_flow.py @@ -82,13 +82,13 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN): fritzbox.login() fritzbox.get_device_elements() fritzbox.logout() - return RESULT_SUCCESS except LoginError: return RESULT_INVALID_AUTH except HTTPError: return RESULT_NOT_SUPPORTED except OSError: return RESULT_NO_DEVICES_FOUND + return RESULT_SUCCESS async def async_step_user( self, user_input: dict[str, Any] | None = None diff --git a/homeassistant/components/fritzbox_callmonitor/config_flow.py b/homeassistant/components/fritzbox_callmonitor/config_flow.py index ac0d3ea3337..019326d840c 100644 --- a/homeassistant/components/fritzbox_callmonitor/config_flow.py +++ b/homeassistant/components/fritzbox_callmonitor/config_flow.py @@ -109,9 +109,6 @@ class FritzBoxCallMonitorConfigFlow(ConfigFlow, domain=DOMAIN): address=self._host, user=self._username, password=self._password ) info = fritz_connection.updatecheck - self._serial_number = info[FRITZ_ATTR_SERIAL_NUMBER] - - return ConnectResult.SUCCESS except RequestsConnectionError: return ConnectResult.NO_DEVIES_FOUND except FritzSecurityError: @@ -119,6 +116,9 @@ class FritzBoxCallMonitorConfigFlow(ConfigFlow, domain=DOMAIN): except FritzConnectionException: return ConnectResult.INVALID_AUTH + self._serial_number = info[FRITZ_ATTR_SERIAL_NUMBER] + return ConnectResult.SUCCESS + async def _get_name_of_phonebook(self, phonebook_id: int) -> str: """Return name of phonebook for given phonebook_id.""" phonebook_info = await self.hass.async_add_executor_job( diff --git a/homeassistant/components/hassio/data.py b/homeassistant/components/hassio/data.py index eaa7c2431fe..a00335d44a2 100644 --- a/homeassistant/components/hassio/data.py +++ b/homeassistant/components/hassio/data.py @@ -483,28 +483,28 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): # pylint: disable=has """Update single addon stats.""" try: stats = await self.hassio.get_addon_stats(slug) - return (slug, stats) except HassioAPIError as err: _LOGGER.warning("Could not fetch stats for %s: %s", slug, err) - return (slug, None) + return (slug, None) + return (slug, stats) async def _update_addon_changelog(self, slug: str) -> tuple[str, str | None]: """Return the changelog for an add-on.""" try: changelog = await self.hassio.get_addon_changelog(slug) - return (slug, changelog) except HassioAPIError as err: _LOGGER.warning("Could not fetch changelog for %s: %s", slug, err) - return (slug, None) + return (slug, None) + return (slug, changelog) async def _update_addon_info(self, slug: str) -> tuple[str, dict[str, Any] | None]: """Return the info for an add-on.""" try: info = await self.hassio.get_addon_info(slug) - return (slug, info) except HassioAPIError as err: _LOGGER.warning("Could not fetch info for %s: %s", slug, err) - return (slug, None) + return (slug, None) + return (slug, info) @callback def async_enable_container_updates( diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index ffb67730fa5..826c7a27b98 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -188,15 +188,13 @@ class HassIOView(HomeAssistantView): async for data, _ in client.content.iter_chunks(): await response.write(data) - return response - except aiohttp.ClientError as err: _LOGGER.error("Client error on api %s request %s", path, err) - - except TimeoutError: + raise HTTPBadGateway from err + except TimeoutError as err: _LOGGER.error("Client timeout error on API request %s", path) - - raise HTTPBadGateway + raise HTTPBadGateway from err + return response get = _handle post = _handle diff --git a/homeassistant/components/heos/__init__.py b/homeassistant/components/heos/__init__.py index ed7c768e161..1573ff3f23e 100644 --- a/homeassistant/components/heos/__init__.py +++ b/homeassistant/components/heos/__init__.py @@ -478,7 +478,6 @@ class SourceManager: if controller.is_signed_in: favorites = await controller.get_favorites() inputs = await controller.get_input_sources() - return favorites, inputs except HeosError as error: if retry_attempts < self.max_retry_attempts: retry_attempts += 1 @@ -488,7 +487,9 @@ class SourceManager: await asyncio.sleep(self.retry_delay) else: _LOGGER.error("Unable to update sources: %s", error) - return + return None + else: + return favorites, inputs async def update_sources(event, data=None): if event in ( diff --git a/homeassistant/components/hitron_coda/device_tracker.py b/homeassistant/components/hitron_coda/device_tracker.py index ba672e38106..dec15e25b0b 100644 --- a/homeassistant/components/hitron_coda/device_tracker.py +++ b/homeassistant/components/hitron_coda/device_tracker.py @@ -95,10 +95,10 @@ class HitronCODADeviceScanner(DeviceScanner): return False try: self._userid = res.cookies["userid"] - return True except KeyError: _LOGGER.error("Failed to log in to router") return False + return True def _update_info(self): """Get ARP from router.""" diff --git a/homeassistant/components/homekit/type_cameras.py b/homeassistant/components/homekit/type_cameras.py index d47d9775ed2..5f1a9428d86 100644 --- a/homeassistant/components/homekit/type_cameras.py +++ b/homeassistant/components/homekit/type_cameras.py @@ -494,11 +494,12 @@ class Camera(HomeAccessory, PyhapCamera): # type: ignore[misc] _LOGGER.info("[%s] %s stream", session_id, shutdown_method) try: await getattr(stream, shutdown_method)() - return except Exception: # pylint: disable=broad-except _LOGGER.exception( "[%s] Failed to %s stream", session_id, shutdown_method ) + else: + return async def reconfigure_stream( self, session_info: dict[str, Any], stream_config: dict[str, Any] diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 031cdbbc9bd..642669cfc8d 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -572,11 +572,12 @@ def _async_find_next_available_port(start_port: int, exclude_ports: set) -> int: continue try: test_socket.bind(("", port)) - return port except OSError: if port == MAX_PORT: raise continue + else: + return port raise RuntimeError("unreachable") @@ -584,10 +585,9 @@ def pid_is_alive(pid: int) -> bool: """Check to see if a process is alive.""" try: os.kill(pid, 0) - return True except OSError: - pass - return False + return False + return True def accessory_friendly_name(hass_name: str, accessory: Accessory) -> str: diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index 058b7ec6c00..7825999900e 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -39,9 +39,9 @@ class HomematicipAuth: self.auth = await self.get_auth( self.hass, self.config.get(HMIPC_HAPID), self.config.get(HMIPC_PIN) ) - return self.auth is not None except HmipcConnectionError: return False + return self.auth is not None async def async_checkbutton(self) -> bool: """Check blue butten has been pressed.""" @@ -55,9 +55,9 @@ class HomematicipAuth: try: authtoken = await self.auth.requestAuthToken() await self.auth.confirmAuthToken(authtoken) - return authtoken except HmipConnectionError: return False + return authtoken async def get_auth(self, hass: HomeAssistant, hapid, pin): """Create a HomematicIP access point object.""" diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index e75fef42ef3..cef5bc5030e 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -79,9 +79,9 @@ def format_last_reset_elapsed_seconds(value: str | None) -> datetime | None: try: last_reset = datetime.now() - timedelta(seconds=int(value)) last_reset.replace(microsecond=0) - return last_reset except ValueError: return None + return last_reset def signal_icon(limits: Sequence[int], value: StateType) -> str: diff --git a/homeassistant/components/image/__init__.py b/homeassistant/components/image/__init__.py index 47767a004cb..56dc7cc2cfa 100644 --- a/homeassistant/components/image/__init__.py +++ b/homeassistant/components/image/__init__.py @@ -199,7 +199,6 @@ class ImageEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): url, timeout=GET_IMAGE_TIMEOUT, follow_redirects=True ) response.raise_for_status() - return response except httpx.TimeoutException: _LOGGER.error("%s: Timeout getting image from %s", self.entity_id, url) return None @@ -211,6 +210,7 @@ class ImageEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): err, ) return None + return response async def _async_load_image_from_url(self, url: str) -> Image | None: """Load an image by url.""" diff --git a/homeassistant/components/imap/coordinator.py b/homeassistant/components/imap/coordinator.py index f7f2ef457c7..7f857ff857f 100644 --- a/homeassistant/components/imap/coordinator.py +++ b/homeassistant/components/imap/coordinator.py @@ -398,8 +398,6 @@ class ImapPollingDataUpdateCoordinator(ImapDataUpdateCoordinator): """Update the number of unread emails.""" try: messages = await self._async_fetch_number_of_messages() - self.auth_errors = 0 - return messages except ( AioImapException, UpdateFailed, @@ -426,6 +424,9 @@ class ImapPollingDataUpdateCoordinator(ImapDataUpdateCoordinator): self.async_set_update_error(ex) raise ConfigEntryAuthFailed from ex + self.auth_errors = 0 + return messages + class ImapPushDataUpdateCoordinator(ImapDataUpdateCoordinator): """Class for imap client.""" diff --git a/homeassistant/components/insteon/config_flow.py b/homeassistant/components/insteon/config_flow.py index 7eac51c600e..44aa1e18646 100644 --- a/homeassistant/components/insteon/config_flow.py +++ b/homeassistant/components/insteon/config_flow.py @@ -72,12 +72,13 @@ async def _async_connect(**kwargs): """Connect to the Insteon modem.""" try: await async_connect(**kwargs) - _LOGGER.info("Connected to Insteon modem") - return True except ConnectionError: _LOGGER.error("Could not connect to Insteon modem") return False + _LOGGER.info("Connected to Insteon modem") + return True + def _remove_override(address, options): """Remove a device override from config.""" diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 62a0dbdec78..ef587e405e6 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -145,9 +145,9 @@ class _Right(_IntegrationMethod): def _is_numeric_state(state: State) -> bool: try: float(state.state) - return True except (ValueError, TypeError): return False + return True _NAME_TO_INTEGRATION_METHOD: dict[str, type[_IntegrationMethod]] = { diff --git a/homeassistant/components/kef/media_player.py b/homeassistant/components/kef/media_player.py index d7d33dabd44..04ecd633d70 100644 --- a/homeassistant/components/kef/media_player.py +++ b/homeassistant/components/kef/media_player.py @@ -79,12 +79,14 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def get_ip_mode(host): """Get the 'mode' used to retrieve the MAC address.""" try: - if ipaddress.ip_address(host).version == 6: - return "ip6" - return "ip" + ip_address = ipaddress.ip_address(host) except ValueError: return "hostname" + if ip_address.version == 6: + return "ip6" + return "ip" + async def async_setup_platform( hass: HomeAssistant, diff --git a/homeassistant/components/knx/validation.py b/homeassistant/components/knx/validation.py index 9fe87a2c3f6..4e56314a677 100644 --- a/homeassistant/components/knx/validation.py +++ b/homeassistant/components/knx/validation.py @@ -37,17 +37,17 @@ string_type_validator = dpt_subclass_validator(DPTString) def ga_validator(value: Any) -> str | int: """Validate that value is parsable as GroupAddress or InternalGroupAddress.""" - if isinstance(value, (str, int)): - try: - parse_device_group_address(value) - return value - except CouldNotParseAddress as exc: - raise vol.Invalid( - f"'{value}' is not a valid KNX group address: {exc.message}" - ) from exc - raise vol.Invalid( - f"'{value}' is not a valid KNX group address: Invalid type '{type(value).__name__}'" - ) + if not isinstance(value, (str, int)): + raise vol.Invalid( + f"'{value}' is not a valid KNX group address: Invalid type '{type(value).__name__}'" + ) + try: + parse_device_group_address(value) + except CouldNotParseAddress as exc: + raise vol.Invalid( + f"'{value}' is not a valid KNX group address: {exc.message}" + ) from exc + return value ga_list_validator = vol.All( diff --git a/homeassistant/components/litterrobot/hub.py b/homeassistant/components/litterrobot/hub.py index 4af004bddf5..2e31bcf9906 100644 --- a/homeassistant/components/litterrobot/hub.py +++ b/homeassistant/components/litterrobot/hub.py @@ -55,7 +55,6 @@ class LitterRobotHub: load_robots=load_robots, subscribe_for_updates=subscribe_for_updates, ) - return except LitterRobotLoginException as ex: raise ConfigEntryAuthFailed("Invalid credentials") from ex except LitterRobotException as ex: diff --git a/homeassistant/components/lyric/__init__.py b/homeassistant/components/lyric/__init__.py index e2c85c1400b..84ef3a2b7db 100644 --- a/homeassistant/components/lyric/__init__.py +++ b/homeassistant/components/lyric/__init__.py @@ -77,7 +77,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: async with asyncio.timeout(60): await lyric.get_locations() - return lyric except LyricAuthenticationException as exception: # Attempt to refresh the token before failing. # Honeywell appear to have issues keeping tokens saved. @@ -87,6 +86,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryAuthFailed from exception except (LyricException, ClientResponseError) as exception: raise UpdateFailed(exception) from exception + return lyric coordinator = DataUpdateCoordinator[Lyric]( hass, diff --git a/homeassistant/components/meteoclimatic/__init__.py b/homeassistant/components/meteoclimatic/__init__.py index 1e729258218..2c371ebdcfd 100644 --- a/homeassistant/components/meteoclimatic/__init__.py +++ b/homeassistant/components/meteoclimatic/__init__.py @@ -25,9 +25,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: data = await hass.async_add_executor_job( meteoclimatic_client.weather_at_station, station_code ) - return data.__dict__ except MeteoclimaticError as err: raise UpdateFailed(f"Error while retrieving data: {err}") from err + return data.__dict__ coordinator = DataUpdateCoordinator( hass, diff --git a/homeassistant/components/mikrotik/hub.py b/homeassistant/components/mikrotik/hub.py index 6b94a621683..2830372f882 100644 --- a/homeassistant/components/mikrotik/hub.py +++ b/homeassistant/components/mikrotik/hub.py @@ -325,8 +325,6 @@ def get_api(entry: dict[str, Any]) -> librouteros.Api: entry[CONF_PASSWORD], **kwargs, ) - _LOGGER.debug("Connected to %s successfully", entry[CONF_HOST]) - return api except ( librouteros.exceptions.LibRouterosError, OSError, @@ -336,3 +334,6 @@ def get_api(entry: dict[str, Any]) -> librouteros.Api: if "invalid user name or password" in str(api_error): raise LoginError from api_error raise CannotConnect from api_error + + _LOGGER.debug("Connected to %s successfully", entry[CONF_HOST]) + return api diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py index b932a33d0fa..8e9fb5442ea 100644 --- a/homeassistant/components/mysensors/gateway.py +++ b/homeassistant/components/mysensors/gateway.py @@ -71,9 +71,9 @@ def is_socket_address(value: str) -> str: """Validate that value is a valid address.""" try: socket.getaddrinfo(value, None) - return value except OSError as err: raise vol.Invalid("Device is not a valid domain name or ip address") from err + return value async def try_connect( diff --git a/homeassistant/components/nextbus/coordinator.py b/homeassistant/components/nextbus/coordinator.py index abf280bece9..15377bce56b 100644 --- a/homeassistant/components/nextbus/coordinator.py +++ b/homeassistant/components/nextbus/coordinator.py @@ -72,8 +72,8 @@ class NextBusDataUpdateCoordinator(DataUpdateCoordinator): # Casting here because we expect dict and not a str due to the input format selected being JSON data = cast(dict[str, Any], data) self._calc_predictions(data) - return data except (NextBusHTTPError, NextBusFormatError) as ex: raise UpdateFailed("Failed updating nextbus data", ex) from ex + return data return await self.hass.async_add_executor_job(_update_data) diff --git a/homeassistant/components/nissan_leaf/__init__.py b/homeassistant/components/nissan_leaf/__init__.py index 9eaca66119f..2cbec236261 100644 --- a/homeassistant/components/nissan_leaf/__init__.py +++ b/homeassistant/components/nissan_leaf/__init__.py @@ -418,13 +418,13 @@ class LeafDataStore: server_info = await self.hass.async_add_executor_job( self.leaf.get_latest_battery_status ) - return server_info except CarwingsError: _LOGGER.error("An error occurred getting battery status") return None except (KeyError, TypeError): _LOGGER.error("An error occurred parsing response from server") return None + return server_info async def async_get_climate( self, diff --git a/homeassistant/components/obihai/sensor.py b/homeassistant/components/obihai/sensor.py index 91920b4c32d..344767c8cd1 100644 --- a/homeassistant/components/obihai/sensor.py +++ b/homeassistant/components/obihai/sensor.py @@ -109,15 +109,15 @@ class ObihaiServiceSensors(SensorEntity): LOGGER.info("Connection restored") self._attr_available = True - return - except RequestException as exc: if self.requester.available: LOGGER.warning("Connection failed, Obihai offline? %s", exc) + self._attr_native_value = None + self._attr_available = False + self.requester.available = False except IndexError as exc: if self.requester.available: LOGGER.warning("Connection failed, bad response: %s", exc) - - self._attr_native_value = None - self._attr_available = False - self.requester.available = False + self._attr_native_value = None + self._attr_available = False + self.requester.available = False diff --git a/homeassistant/components/onvif/device.py b/homeassistant/components/onvif/device.py index 71acf62f97d..b427cbda2f8 100644 --- a/homeassistant/components/onvif/device.py +++ b/homeassistant/components/onvif/device.py @@ -218,12 +218,13 @@ class ONVIFDevice: try: await device_mgmt.SetSystemDateAndTime(dt_param) LOGGER.debug("%s: SetSystemDateAndTime: success", self.name) - return # Some cameras don't support setting the timezone and will throw an IndexError # if we try to set it. If we get an error, try again without the timezone. except (IndexError, Fault): if idx == timezone_max_idx: raise + else: + return async def async_check_date_and_time(self) -> None: """Warns if device and system date not synced.""" diff --git a/homeassistant/components/onvif/parsers.py b/homeassistant/components/onvif/parsers.py index 690a3739b4f..29da0fee35f 100644 --- a/homeassistant/components/onvif/parsers.py +++ b/homeassistant/components/onvif/parsers.py @@ -221,9 +221,9 @@ async def async_parse_field_detector(uid: str, msg) -> Event | None: None, payload.Data.SimpleItem[0].Value == "true", ) - return evt except (AttributeError, KeyError): return None + return evt @PARSERS.register("tns1:RuleEngine/CellMotionDetector/Motion") diff --git a/homeassistant/components/owntracks/messages.py b/homeassistant/components/owntracks/messages.py index 3e669079848..011b4f75489 100644 --- a/homeassistant/components/owntracks/messages.py +++ b/homeassistant/components/owntracks/messages.py @@ -135,8 +135,6 @@ def _decrypt_payload(secret, topic, ciphertext): try: message = decrypt(ciphertext, key) message = message.decode("utf-8") - _LOGGER.debug("Decrypted payload: %s", message) - return message except ValueError: _LOGGER.warning( ( @@ -146,6 +144,8 @@ def _decrypt_payload(secret, topic, ciphertext): topic, ) return None + _LOGGER.debug("Decrypted payload: %s", message) + return message def encrypt_message(secret, topic, message): diff --git a/homeassistant/components/panasonic_viera/__init__.py b/homeassistant/components/panasonic_viera/__init__.py index 8ec825c5974..5c76a7e6900 100644 --- a/homeassistant/components/panasonic_viera/__init__.py +++ b/homeassistant/components/panasonic_viera/__init__.py @@ -247,9 +247,6 @@ class Remote: """Handle errors from func, set available and reconnect if needed.""" try: result = await self._hass.async_add_executor_job(func, *args) - self.state = STATE_ON - self.available = True - return result except EncryptionRequired: _LOGGER.error( "The connection couldn't be encrypted. Please reconfigure your TV" @@ -260,12 +257,18 @@ class Remote: self.state = STATE_OFF self.available = True await self.async_create_remote_control() + return None except (URLError, OSError) as err: _LOGGER.debug("An error occurred: %s", err) self.state = STATE_OFF self.available = self._on_action is not None await self.async_create_remote_control() + return None except Exception: # pylint: disable=broad-except _LOGGER.exception("An unknown error occurred") self.state = STATE_OFF self.available = self._on_action is not None + return None + self.state = STATE_ON + self.available = True + return result diff --git a/homeassistant/components/picnic/coordinator.py b/homeassistant/components/picnic/coordinator.py index 7a76d3174cd..c367d5ec548 100644 --- a/homeassistant/components/picnic/coordinator.py +++ b/homeassistant/components/picnic/coordinator.py @@ -50,14 +50,14 @@ class PicnicUpdateCoordinator(DataUpdateCoordinator): # Update the auth token in the config entry if applicable self._update_auth_token() - - # Return the fetched data - return data except ValueError as error: raise UpdateFailed(f"API response was malformed: {error}") from error except PicnicAuthError as error: raise ConfigEntryAuthFailed from error + # Return the fetched data + return data + def fetch_data(self): """Fetch the data from the Picnic API and return a flat dict with only needed sensor data.""" # Fetch from the API and pre-process the data diff --git a/homeassistant/components/ping/helpers.py b/homeassistant/components/ping/helpers.py index f9afcef7be9..f1fd8518d42 100644 --- a/homeassistant/components/ping/helpers.py +++ b/homeassistant/components/ping/helpers.py @@ -140,7 +140,6 @@ class PingDataSubProcess(PingData): if TYPE_CHECKING: assert match is not None rtt_min, rtt_avg, rtt_max, rtt_mdev = match.groups() - return {"min": rtt_min, "avg": rtt_avg, "max": rtt_max, "mdev": rtt_mdev} except TimeoutError: _LOGGER.exception( "Timed out running command: `%s`, after: %ss", @@ -155,6 +154,7 @@ class PingDataSubProcess(PingData): return None except AttributeError: return None + return {"min": rtt_min, "avg": rtt_avg, "max": rtt_max, "mdev": rtt_mdev} async def async_update(self) -> None: """Retrieve the latest details from the host.""" diff --git a/homeassistant/components/prosegur/config_flow.py b/homeassistant/components/prosegur/config_flow.py index 36d1ae9d10f..911ae6104fd 100644 --- a/homeassistant/components/prosegur/config_flow.py +++ b/homeassistant/components/prosegur/config_flow.py @@ -35,11 +35,11 @@ async def validate_input(hass: HomeAssistant, data): auth = Auth(session, data[CONF_USERNAME], data[CONF_PASSWORD], data[CONF_COUNTRY]) try: contracts = await Installation.list(auth) - return auth, contracts except ConnectionRefusedError: raise InvalidAuth from ConnectionRefusedError except ConnectionError: raise CannotConnect from ConnectionError + return auth, contracts class ProsegurConfigFlow(ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/qvr_pro/camera.py b/homeassistant/components/qvr_pro/camera.py index 544ef808ca7..2754ab3a1ec 100644 --- a/homeassistant/components/qvr_pro/camera.py +++ b/homeassistant/components/qvr_pro/camera.py @@ -43,19 +43,18 @@ def get_stream_source(guid, client): """Get channel stream source.""" try: resp = client.get_channel_live_stream(guid, protocol="rtsp") - - full_url = resp["resourceUris"] - - protocol = full_url[:7] - auth = f"{client.get_auth_string()}@" - url = full_url[7:] - - return f"{protocol}{auth}{url}" - except QVRResponseError as ex: _LOGGER.error(ex) return None + full_url = resp["resourceUris"] + + protocol = full_url[:7] + auth = f"{client.get_auth_string()}@" + url = full_url[7:] + + return f"{protocol}{auth}{url}" + class QVRProCamera(Camera): """Representation of a QVR Pro camera.""" diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 3268bae4d49..4ae61a0c4ba 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -1179,7 +1179,6 @@ class Recorder(threading.Thread): while tries <= self.db_max_retries: try: self._commit_event_session() - return except (exc.InternalError, exc.OperationalError) as err: _LOGGER.error( "%s: Error executing query: %s. (retrying in %s seconds)", @@ -1192,6 +1191,8 @@ class Recorder(threading.Thread): tries += 1 time.sleep(self.db_retry_wait) + else: + return def _commit_event_session(self) -> None: assert self.event_session is not None diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 0d882ed3b66..630628b2045 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -341,10 +341,10 @@ def _execute_or_collect_error( with session_scope(session=session_maker()) as session: try: session.connection().execute(text(query)) - return True except SQLAlchemyError as err: errors.append(str(err)) - return False + return False + return True def _drop_index( @@ -439,11 +439,12 @@ def _add_columns( ) ) ) - return except (InternalError, OperationalError, ProgrammingError): # Some engines support adding all columns at once, # this error is when they don't _LOGGER.info("Unable to use quick column add. Adding 1 by 1") + else: + return for column_def in columns_def: with session_scope(session=session_maker()) as session: @@ -510,9 +511,10 @@ def _modify_columns( ) ) ) - return except (InternalError, OperationalError): _LOGGER.info("Unable to use quick column modify. Modifying 1 by 1") + else: + return for column_def in columns_def: with session_scope(session=session_maker()) as session: diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index f840fdbd7b6..0c127d079ad 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -641,7 +641,6 @@ def _insert_statistics( try: stat = table.from_stats(metadata_id, statistic) session.add(stat) - return stat except SQLAlchemyError: _LOGGER.exception( "Unexpected exception when inserting statistics %s:%s ", @@ -649,6 +648,7 @@ def _insert_statistics( statistic, ) return None + return stat def _update_statistics( diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 77467ec1171..ad96833b1d7 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -192,13 +192,14 @@ def execute( elapsed, ) - return result except SQLAlchemyError as err: _LOGGER.error("Error executing query: %s", err) if tryno == RETRIES - 1: raise time.sleep(QUERY_RETRY_WAIT) + else: + return result # Unreachable raise RuntimeError # pragma: no cover @@ -685,7 +686,6 @@ def database_job_retry_wrapper( for attempt in range(attempts): try: job(instance, *args, **kwargs) - return except OperationalError as err: if attempt == attempts - 1 or not _is_retryable_error( instance, err @@ -697,6 +697,8 @@ def database_job_retry_wrapper( ) time.sleep(instance.db_retry_wait) # Failed with retryable error + else: + return return wrapper diff --git a/homeassistant/components/renault/coordinator.py b/homeassistant/components/renault/coordinator.py index f77a38f2505..d7aed6e3560 100644 --- a/homeassistant/components/renault/coordinator.py +++ b/homeassistant/components/renault/coordinator.py @@ -55,8 +55,6 @@ class RenaultDataUpdateCoordinator(DataUpdateCoordinator[T]): try: async with _PARALLEL_SEMAPHORE: data = await self.update_method() - self._has_already_worked = True - return data except AccessDeniedException as err: # This can mean both a temporary error or a permanent error. If it has @@ -76,6 +74,9 @@ class RenaultDataUpdateCoordinator(DataUpdateCoordinator[T]): # Other Renault errors. raise UpdateFailed(f"Error communicating with API: {err}") from err + self._has_already_worked = True + return data + async def async_config_entry_first_refresh(self) -> None: """Refresh data for the first time when a config entry is setup. diff --git a/homeassistant/components/roku/coordinator.py b/homeassistant/components/roku/coordinator.py index baef00b2596..303d0e91a36 100644 --- a/homeassistant/components/roku/coordinator.py +++ b/homeassistant/components/roku/coordinator.py @@ -62,10 +62,10 @@ class RokuDataUpdateCoordinator(DataUpdateCoordinator[Device]): try: data = await self.roku.update(full_update=full_update) - - if full_update: - self.last_full_update = utcnow() - - return data except RokuError as error: raise UpdateFailed(f"Invalid response from API: {error}") from error + + if full_update: + self.last_full_update = utcnow() + + return data diff --git a/homeassistant/components/rympro/coordinator.py b/homeassistant/components/rympro/coordinator.py index f2e5162a0f0..19f16005578 100644 --- a/homeassistant/components/rympro/coordinator.py +++ b/homeassistant/components/rympro/coordinator.py @@ -39,10 +39,10 @@ class RymProDataUpdateCoordinator(DataUpdateCoordinator[dict[int, dict]]): meter["consumption_forecast"] = await self.rympro.consumption_forecast( meter_id ) - return meters except UnauthorizedError as error: assert self.config_entry await self.hass.config_entries.async_reload(self.config_entry.entry_id) raise UpdateFailed(error) from error except (CannotConnectError, OperationError) as error: raise UpdateFailed(error) from error + return meters diff --git a/homeassistant/components/shell_command/__init__.py b/homeassistant/components/shell_command/__init__.py index 736654fc399..95bbb01bcfb 100644 --- a/homeassistant/components/shell_command/__init__.py +++ b/homeassistant/components/shell_command/__init__.py @@ -135,12 +135,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: service_response["stdout"] = stdout_data.decode("utf-8").strip() if stderr_data: service_response["stderr"] = stderr_data.decode("utf-8").strip() - return service_response except UnicodeDecodeError: _LOGGER.exception( "Unable to handle non-utf8 output of command: `%s`", cmd ) raise + return service_response return None for name in conf: diff --git a/homeassistant/components/sms/gateway.py b/homeassistant/components/sms/gateway.py index 50abc9b39ef..9b5b8c1f51e 100644 --- a/homeassistant/components/sms/gateway.py +++ b/homeassistant/components/sms/gateway.py @@ -210,7 +210,7 @@ async def create_sms_gateway(config, hass): _LOGGER.error("Failed to initialize, error %s", exc) await gateway.terminate_async() return None - return gateway except gammu.GSMError as exc: _LOGGER.error("Failed to create async worker, error %s", exc) return None + return gateway diff --git a/homeassistant/components/solarlog/config_flow.py b/homeassistant/components/solarlog/config_flow.py index 83b9c600de8..40343b5ac12 100644 --- a/homeassistant/components/solarlog/config_flow.py +++ b/homeassistant/components/solarlog/config_flow.py @@ -44,14 +44,14 @@ class SolarLogConfigFlow(ConfigFlow, domain=DOMAIN): """Check if we can connect to the Solar-Log device.""" try: await self.hass.async_add_executor_job(SolarLog, host) - return True 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, ) - return False + return False + return True async def async_step_user(self, user_input=None): """Step when user initializes a integration.""" diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 487e58d8f8b..2e725e8d139 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -101,8 +101,6 @@ def spotify_exception_handler( # pylint: disable=protected-access try: result = func(self, *args, **kwargs) - self._attr_available = True - return result except requests.RequestException: self._attr_available = False return None @@ -111,6 +109,8 @@ def spotify_exception_handler( if exc.reason == "NO_ACTIVE_DEVICE": raise HomeAssistantError("No active playback device found") from None raise HomeAssistantError(f"Spotify error: {exc.reason}") from exc + self._attr_available = True + return result return wrapper diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index b34105106e0..08d1bbb858e 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -735,10 +735,11 @@ async def _async_find_next_available_port(source: AddressTupleVXType) -> int: addr = (source[0],) + (port,) + source[2:] try: test_socket.bind(addr) - return port except OSError: if port == UPNP_SERVER_MAX_PORT - 1: raise + else: + return port raise RuntimeError("unreachable") diff --git a/homeassistant/components/suez_water/__init__.py b/homeassistant/components/suez_water/__init__.py index 07944de2c81..f5b2880e011 100644 --- a/homeassistant/components/suez_water/__init__.py +++ b/homeassistant/components/suez_water/__init__.py @@ -28,9 +28,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) if not client.check_credentials(): raise ConfigEntryError - return client except PySuezError as ex: raise ConfigEntryNotReady from ex + return client hass.data.setdefault(DOMAIN, {})[ entry.entry_id diff --git a/homeassistant/components/system_health/__init__.py b/homeassistant/components/system_health/__init__.py index 6a1e4830443..f61745a1407 100644 --- a/homeassistant/components/system_health/__init__.py +++ b/homeassistant/components/system_health/__init__.py @@ -235,11 +235,12 @@ async def async_check_can_reach_url( try: await session.get(url, timeout=5) - return "ok" except aiohttp.ClientError: data = {"type": "failed", "error": "unreachable"} except TimeoutError: data = {"type": "failed", "error": "timeout"} + else: + return "ok" if more_info is not None: data["more_info"] = more_info return data diff --git a/homeassistant/components/tcp/common.py b/homeassistant/components/tcp/common.py index 46520134bf6..d6a7fb28f11 100644 --- a/homeassistant/components/tcp/common.py +++ b/homeassistant/components/tcp/common.py @@ -149,7 +149,6 @@ class TcpEntity(Entity): if value_template is not None: try: self._state = value_template.render(parse_result=False, value=value) - return except TemplateError: _LOGGER.error( "Unable to render template of %r with value: %r", @@ -157,5 +156,6 @@ class TcpEntity(Entity): value, ) return + return self._state = value diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index 6338996256b..4e47be8b807 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -687,11 +687,12 @@ class TelegramNotificationService: _LOGGER.warning( "Update last message: out_type:%s, out=%s", type(out), out ) - return out except TelegramError as exc: _LOGGER.error( "%s: %s. Args: %s, kwargs: %s", msg_error, exc, args_msg, kwargs_msg ) + return None + return out async def send_message(self, message="", target=None, **kwargs): """Send a message to one or multiple pre-allowed chat IDs.""" diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index 4dcc4d41950..d7d6ae4ea0c 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -267,9 +267,6 @@ async def get_api( path=path, ) ) - _LOGGER.debug("Successfully connected to %s", host) - return api - except TransmissionAuthError as error: _LOGGER.error("Credentials for Transmission client are not valid") raise AuthenticationError from error @@ -279,3 +276,5 @@ async def get_api( except TransmissionError as error: _LOGGER.error(error) raise UnknownError from error + _LOGGER.debug("Successfully connected to %s", host) + return api diff --git a/homeassistant/components/unifi/hub/api.py b/homeassistant/components/unifi/hub/api.py index 8a1be0427b2..acdd941dd15 100644 --- a/homeassistant/components/unifi/hub/api.py +++ b/homeassistant/components/unifi/hub/api.py @@ -56,7 +56,6 @@ async def get_unifi_api( try: async with asyncio.timeout(10): await api.login() - return api except aiounifi.Unauthorized as err: LOGGER.warning( @@ -90,3 +89,5 @@ async def get_unifi_api( except aiounifi.AiounifiException as err: LOGGER.exception("Unknown UniFi Network communication error occurred: %s", err) raise AuthenticationRequired from err + + return api diff --git a/homeassistant/components/update/__init__.py b/homeassistant/components/update/__init__.py index f274da6f412..142c9b4a6c3 100644 --- a/homeassistant/components/update/__init__.py +++ b/homeassistant/components/update/__init__.py @@ -408,10 +408,10 @@ class UpdateEntity( try: newer = _version_is_newer(latest_version, installed_version) - return STATE_ON if newer else STATE_OFF except AwesomeVersionCompareException: # Can't compare versions, already tried exact match return STATE_ON + return STATE_ON if newer else STATE_OFF @final @property diff --git a/homeassistant/components/vallox/__init__.py b/homeassistant/components/vallox/__init__.py index 7c9234d35c2..b8e94e9dfb7 100644 --- a/homeassistant/components/vallox/__init__.py +++ b/homeassistant/components/vallox/__init__.py @@ -175,11 +175,10 @@ class ValloxServiceHandler: try: await self._client.set_fan_speed(Profile.HOME, fan_speed) - return True - except ValloxApiException as err: _LOGGER.error("Error setting fan speed for Home profile: %s", err) return False + return True async def async_set_profile_fan_speed_away( self, fan_speed: int = DEFAULT_FAN_SPEED_AWAY @@ -189,11 +188,10 @@ class ValloxServiceHandler: try: await self._client.set_fan_speed(Profile.AWAY, fan_speed) - return True - except ValloxApiException as err: _LOGGER.error("Error setting fan speed for Away profile: %s", err) return False + return True async def async_set_profile_fan_speed_boost( self, fan_speed: int = DEFAULT_FAN_SPEED_BOOST @@ -203,11 +201,10 @@ class ValloxServiceHandler: try: await self._client.set_fan_speed(Profile.BOOST, fan_speed) - return True - except ValloxApiException as err: _LOGGER.error("Error setting fan speed for Boost profile: %s", err) return False + return True async def async_handle(self, call: ServiceCall) -> None: """Dispatch a service call.""" diff --git a/homeassistant/components/viaggiatreno/sensor.py b/homeassistant/components/viaggiatreno/sensor.py index 3738b0f956a..9c6c6bca422 100644 --- a/homeassistant/components/viaggiatreno/sensor.py +++ b/homeassistant/components/viaggiatreno/sensor.py @@ -84,11 +84,13 @@ async def async_http_request(hass, uri): if req.status != HTTPStatus.OK: return {"error": req.status} json_response = await req.json() - return json_response except (TimeoutError, aiohttp.ClientError) as exc: _LOGGER.error("Cannot connect to ViaggiaTreno API endpoint: %s", exc) + return None except ValueError: _LOGGER.error("Received non-JSON data from ViaggiaTreno API endpoint") + return None + return json_response class ViaggiaTrenoSensor(SensorEntity): diff --git a/homeassistant/components/vicare/utils.py b/homeassistant/components/vicare/utils.py index 2019f28a896..2ba5ddbfb0a 100644 --- a/homeassistant/components/vicare/utils.py +++ b/homeassistant/components/vicare/utils.py @@ -35,13 +35,14 @@ def is_supported( """Check if the PyViCare device supports the requested sensor.""" try: entity_description.value_getter(vicare_device) - _LOGGER.debug("Found entity %s", name) - return True except PyViCareNotSupportedFeatureError: _LOGGER.debug("Feature not supported %s", name) + return False except AttributeError as error: _LOGGER.debug("Feature not supported %s: %s", name, error) - return False + return False + _LOGGER.debug("Found entity %s", name) + return True def get_burners(device: PyViCareDevice) -> list[PyViCareHeatingDeviceComponent]: diff --git a/homeassistant/components/webhook/__init__.py b/homeassistant/components/webhook/__init__.py index f2d1416e2c9..0076c85e268 100644 --- a/homeassistant/components/webhook/__init__.py +++ b/homeassistant/components/webhook/__init__.py @@ -178,10 +178,10 @@ async def async_handle_webhook( response: Response | None = await webhook["handler"](hass, webhook_id, request) if response is None: response = Response(status=HTTPStatus.OK) - return response except Exception: # pylint: disable=broad-except _LOGGER.exception("Error processing webhook %s", webhook_id) return Response(status=HTTPStatus.OK) + return response async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: diff --git a/homeassistant/components/wiz/__init__.py b/homeassistant/components/wiz/__init__.py index 6b1ac2a7721..79c317f178b 100644 --- a/homeassistant/components/wiz/__init__.py +++ b/homeassistant/components/wiz/__init__.py @@ -94,9 +94,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if bulb.power_monitoring is not False: power: float | None = await bulb.get_power() return power - return None except WIZ_EXCEPTIONS as ex: raise UpdateFailed(f"Failed to update device at {ip_address}: {ex}") from ex + return None coordinator = DataUpdateCoordinator( hass=hass, diff --git a/homeassistant/components/xiaomi_miio/device.py b/homeassistant/components/xiaomi_miio/device.py index 2f5e6e299e9..39cb0ee5f96 100644 --- a/homeassistant/components/xiaomi_miio/device.py +++ b/homeassistant/components/xiaomi_miio/device.py @@ -150,16 +150,15 @@ class XiaomiCoordinatedMiioEntity(CoordinatorEntity[_T]): result = await self.hass.async_add_executor_job( partial(func, *args, **kwargs) ) - - _LOGGER.debug("Response received from miio device: %s", result) - - return True except DeviceException as exc: if self.available: _LOGGER.error(mask_error, exc) return False + _LOGGER.debug("Response received from miio device: %s", result) + return True + @classmethod def _extract_value_from_attribute(cls, state, attribute): value = getattr(state, attribute) diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index cbbf12f9ab1..96f9595e0e8 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -292,10 +292,6 @@ class XiaomiPhilipsAbstractLight(XiaomiMiioEntity, LightEntity): result = await self.hass.async_add_executor_job( partial(func, *args, **kwargs) ) - - _LOGGER.debug("Response received from light: %s", result) - - return result == SUCCESS except DeviceException as exc: if self._available: _LOGGER.error(mask_error, exc) @@ -303,6 +299,9 @@ class XiaomiPhilipsAbstractLight(XiaomiMiioEntity, LightEntity): return False + _LOGGER.debug("Response received from light: %s", result) + return result == SUCCESS + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" if ATTR_BRIGHTNESS in kwargs: diff --git a/homeassistant/components/xiaomi_miio/remote.py b/homeassistant/components/xiaomi_miio/remote.py index c1234b77bbc..cd3b3192520 100644 --- a/homeassistant/components/xiaomi_miio/remote.py +++ b/homeassistant/components/xiaomi_miio/remote.py @@ -225,9 +225,9 @@ class XiaomiMiioRemote(RemoteEntity): """Return False if device is unreachable, else True.""" try: self.device.info() - return True except DeviceException: return False + return True async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 02517d00c57..34ebb9addf5 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -805,14 +805,6 @@ class XiaomiPlugGenericSwitch(XiaomiMiioEntity, SwitchEntity): result = await self.hass.async_add_executor_job( partial(func, *args, **kwargs) ) - - _LOGGER.debug("Response received from plug: %s", result) - - # The Chuangmi Plug V3 returns 0 on success on usb_on/usb_off. - if func in ["usb_on", "usb_off"] and result == 0: - return True - - return result == SUCCESS except DeviceException as exc: if self._available: _LOGGER.error(mask_error, exc) @@ -820,6 +812,14 @@ class XiaomiPlugGenericSwitch(XiaomiMiioEntity, SwitchEntity): return False + _LOGGER.debug("Response received from plug: %s", result) + + # The Chuangmi Plug V3 returns 0 on success on usb_on/usb_off. + if func in ["usb_on", "usb_off"] and result == 0: + return True + + return result == SUCCESS + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the plug on.""" result = await self._try_command("Turning the plug on failed", self._device.on) diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index 41f2c2386e1..ef6f94c162f 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -281,10 +281,10 @@ class MiroboVacuum( try: await self.hass.async_add_executor_job(partial(func, *args, **kwargs)) await self.coordinator.async_refresh() - return True except DeviceException as exc: _LOGGER.error(mask_error, exc) return False + return True async def async_start(self) -> None: """Start or resume the cleaning task.""" diff --git a/homeassistant/components/zamg/weather.py b/homeassistant/components/zamg/weather.py index 241b2232eeb..286a6460f19 100644 --- a/homeassistant/components/zamg/weather.py +++ b/homeassistant/components/zamg/weather.py @@ -66,9 +66,9 @@ class ZamgWeather(CoordinatorEntity, WeatherEntity): value := self.coordinator.data[self.station_id]["TL"]["data"] ) is not None: return float(value) - return None except (KeyError, ValueError, TypeError): return None + return None @property def native_pressure(self) -> float | None: @@ -98,9 +98,9 @@ class ZamgWeather(CoordinatorEntity, WeatherEntity): value := self.coordinator.data[self.station_id]["FFX"]["data"] ) is not None: return float(value) - return None except (KeyError, ValueError, TypeError): return None + return None @property def wind_bearing(self) -> float | None: @@ -114,6 +114,6 @@ class ZamgWeather(CoordinatorEntity, WeatherEntity): value := self.coordinator.data[self.station_id]["DDX"]["data"] ) is not None: return float(value) - return None except (KeyError, ValueError, TypeError): return None + return None diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index e6d9f3e66b5..e5fdfe36a9b 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -789,15 +789,6 @@ class ZHADevice(LogMixin): response = await cluster.write_attributes( {attribute: value}, manufacturer=manufacturer ) - self.debug( - "set: %s for attr: %s to cluster: %s for ept: %s - res: %s", - value, - attribute, - cluster_id, - endpoint_id, - response, - ) - return response except zigpy.exceptions.ZigbeeException as exc: raise HomeAssistantError( f"Failed to set attribute: " @@ -807,6 +798,16 @@ class ZHADevice(LogMixin): f"{ATTR_ENDPOINT_ID}: {endpoint_id}" ) from exc + self.debug( + "set: %s for attr: %s to cluster: %s for ept: %s - res: %s", + value, + attribute, + cluster_id, + endpoint_id, + response, + ) + return response + async def issue_cluster_command( self, endpoint_id: int, diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index 1a001cab381..b060d56cb04 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -102,9 +102,9 @@ async def safe_read( only_cache=only_cache, manufacturer=manufacturer, ) - return result except Exception: # pylint: disable=broad-except return {} + return result async def get_matched_clusters( diff --git a/homeassistant/config.py b/homeassistant/config.py index c570e36c6c1..d3f30f84a68 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -464,14 +464,12 @@ def _write_default_config(config_dir: str) -> bool: if not os.path.isfile(scene_yaml_path): with open(scene_yaml_path, "w", encoding="utf8"): pass - - return True - except OSError: print( # noqa: T201 f"Unable to create default configuration file {config_path}" ) return False + return True async def async_hass_config_yaml(hass: HomeAssistant) -> dict: diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 890db26810a..9f5f6b9135b 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -763,8 +763,6 @@ class ConfigEntry: self._async_set_state(hass, ConfigEntryState.NOT_LOADED, None) await self._async_process_on_unload(hass) - - return result except Exception as exc: # pylint: disable=broad-except _LOGGER.exception( "Error unloading entry %s for %s", self.title, integration.domain @@ -774,6 +772,7 @@ class ConfigEntry: hass, ConfigEntryState.FAILED_UNLOAD, str(exc) or "Unknown error" ) return False + return result async def async_remove(self, hass: HomeAssistant) -> None: """Invoke remove callback on component.""" @@ -872,12 +871,12 @@ class ConfigEntry: if result: # pylint: disable-next=protected-access hass.config_entries._async_schedule_save() - return result except Exception: # pylint: disable=broad-except _LOGGER.exception( "Error migrating entry %s for %s", self.title, self.domain ) return False + return result def add_update_listener(self, listener: UpdateListenerType) -> CALLBACK_TYPE: """Listen for when entry is updated. diff --git a/homeassistant/core.py b/homeassistant/core.py index 082c1a756c3..6a923f4ab16 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -2691,9 +2691,10 @@ class Config: for allowed_path in self.allowlist_external_dirs: try: thepath.relative_to(allowed_path) - return True except ValueError: pass + else: + return True return False diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index f7245607be7..fc39db83658 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -248,13 +248,13 @@ def is_regex(value: Any) -> re.Pattern[Any]: """Validate that a string is a valid regular expression.""" try: r = re.compile(value) - return r except TypeError as err: raise vol.Invalid( f"value {value} is of the wrong type for a regular expression" ) from err except re.error as err: raise vol.Invalid(f"value {value} is not a valid regular expression") from err + return r def isfile(value: Any) -> str: @@ -671,9 +671,9 @@ def template(value: Any | None) -> template_helper.Template: try: template_value.ensure_valid() - return template_value except TemplateError as ex: raise vol.Invalid(f"invalid template ({ex})") from ex + return template_value def dynamic_template(value: Any | None) -> template_helper.Template: @@ -693,9 +693,9 @@ def dynamic_template(value: Any | None) -> template_helper.Template: try: template_value.ensure_valid() - return template_value except TemplateError as ex: raise vol.Invalid(f"invalid template ({ex})") from ex + return template_value def template_complex(value: Any) -> Any: diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 1cff472af72..f605f8381b0 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -362,10 +362,6 @@ class EntityPlatform: pending = self._tasks.copy() self._tasks.clear() await asyncio.gather(*pending) - - hass.config.components.add(full_name) - self._setup_complete = True - return True except PlatformNotReady as ex: tries += 1 wait_time = min(tries, 6) * PLATFORM_NOT_READY_BASE_WAIT_TIME @@ -417,6 +413,10 @@ class EntityPlatform: self.domain, ) return False + else: + hass.config.components.add(full_name) + self._setup_complete = True + return True finally: warn_task.cancel() diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index ef9274c6ceb..ad9ddcd5c4c 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -243,7 +243,6 @@ class RegistryEntry: try: dict_repr = self._as_display_dict json_repr: bytes | None = json_bytes(dict_repr) if dict_repr else None - return json_repr except (ValueError, TypeError): _LOGGER.error( "Unable to serialize entry %s to JSON. Bad data found at %s", @@ -252,8 +251,8 @@ class RegistryEntry: find_paths_unserializable_data(dict_repr, dump=JSON_DUMP) ), ) - - return None + return None + return json_repr @cached_property def as_partial_dict(self) -> dict[str, Any]: diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index 63214cb135b..0a9b441d99b 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -109,7 +109,6 @@ async def async_handle( try: _LOGGER.info("Triggering intent handler %s", handler) result = await handler.async_handle(intent) - return result except vol.Invalid as err: _LOGGER.warning("Received invalid slot info for %s: %s", intent_type, err) raise InvalidSlotInfo(f"Received invalid slot info for {intent_type}") from err @@ -117,6 +116,7 @@ async def async_handle( raise # bubble up intent related errors except Exception as err: raise IntentUnexpectedError(f"Error handling {intent_type}") from err + return result class IntentError(HomeAssistantError): diff --git a/homeassistant/helpers/network.py b/homeassistant/helpers/network.py index ed6339f9996..6e8fa8dc3a3 100644 --- a/homeassistant/helpers/network.py +++ b/homeassistant/helpers/network.py @@ -31,9 +31,9 @@ def is_internal_request(hass: HomeAssistant) -> bool: get_url( hass, allow_external=False, allow_cloud=False, require_current_request=True ) - return True except NoURLAvailableError: return False + return True @bind_hass diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 178ee6425e3..32bb8f361d6 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -166,7 +166,6 @@ async def async_setup_component( setup_future.set_result(result) if setup_done_future := setup_done_futures.pop(domain, None): setup_done_future.set_result(result) - return result except BaseException as err: futures = [setup_future] if setup_done_future := setup_done_futures.pop(domain, None): @@ -185,6 +184,7 @@ async def async_setup_component( # if there are no concurrent setup attempts await future raise + return result async def _async_process_dependencies( diff --git a/pyproject.toml b/pyproject.toml index 891ea511ba5..80381b09825 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -714,8 +714,7 @@ ignore = [ # temporarily disabled "PT019", "TRY002", - "TRY301", - "TRY300" + "TRY301" ] [tool.ruff.lint.flake8-import-conventions.extend-aliases] diff --git a/tests/components/gardena_bluetooth/conftest.py b/tests/components/gardena_bluetooth/conftest.py index af882e35751..052de4bf311 100644 --- a/tests/components/gardena_bluetooth/conftest.py +++ b/tests/components/gardena_bluetooth/conftest.py @@ -88,11 +88,11 @@ def mock_client( val = mock_read_char_raw[uuid] if isinstance(val, Exception): raise val - return val except KeyError: if default is SENTINEL: raise CharacteristicNotFound from KeyError return default + return val def _all_char(): return set(mock_read_char_raw.keys()) From b7527feb5f8077cfd2411979b4d4aab01691b187 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 30 Mar 2024 14:52:33 +0100 Subject: [PATCH 089/967] Rework opensky tests (#114441) * Rework opensky tests * Rework opensky tests * Fix --- .../components/opensky/config_flow.py | 24 ++- tests/components/opensky/__init__.py | 22 +-- tests/components/opensky/conftest.py | 54 +++--- tests/components/opensky/test_config_flow.py | 164 ++++++++---------- tests/components/opensky/test_init.py | 50 +++--- tests/components/opensky/test_sensor.py | 44 ++--- 6 files changed, 166 insertions(+), 192 deletions(-) diff --git a/homeassistant/components/opensky/config_flow.py b/homeassistant/components/opensky/config_flow.py index 863b6050616..3cfd1ad30a0 100644 --- a/homeassistant/components/opensky/config_flow.py +++ b/homeassistant/components/opensky/config_flow.py @@ -100,19 +100,17 @@ class OpenSkyOptionsFlowHandler(OptionsFlowWithConfigEntry): if user_input[CONF_CONTRIBUTING_USER] and not authentication: errors["base"] = "no_authentication" if authentication and not errors: - async with OpenSky( - session=async_get_clientsession(self.hass) - ) as opensky: - try: - await opensky.authenticate( - BasicAuth( - login=user_input[CONF_USERNAME], - password=user_input[CONF_PASSWORD], - ), - contributing_user=user_input[CONF_CONTRIBUTING_USER], - ) - except OpenSkyUnauthenticatedError: - errors["base"] = "invalid_auth" + opensky = OpenSky(session=async_get_clientsession(self.hass)) + try: + await opensky.authenticate( + BasicAuth( + login=user_input[CONF_USERNAME], + password=user_input[CONF_PASSWORD], + ), + contributing_user=user_input[CONF_CONTRIBUTING_USER], + ) + except OpenSkyUnauthenticatedError: + errors["base"] = "invalid_auth" if not errors: return self.async_create_entry( title=self.options.get(CONF_NAME, "OpenSky"), diff --git a/tests/components/opensky/__init__.py b/tests/components/opensky/__init__.py index 668416c6dcb..ab23de9937b 100644 --- a/tests/components/opensky/__init__.py +++ b/tests/components/opensky/__init__.py @@ -1,20 +1,12 @@ """Opensky tests.""" -from unittest.mock import patch +from homeassistant.core import HomeAssistant -from python_opensky import StatesResponse - -from tests.common import load_json_object_fixture +from tests.common import MockConfigEntry -def patch_setup_entry() -> bool: - """Patch interface.""" - return patch( - "homeassistant.components.opensky.async_setup_entry", return_value=True - ) - - -def get_states_response_fixture(fixture: str) -> StatesResponse: - """Return the states response from json.""" - states_json = load_json_object_fixture(fixture) - return StatesResponse.from_api(states_json) +async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: + """Set up the integration.""" + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/opensky/conftest.py b/tests/components/opensky/conftest.py index 835543b632f..665fdd90e69 100644 --- a/tests/components/opensky/conftest.py +++ b/tests/components/opensky/conftest.py @@ -1,9 +1,10 @@ """Configure tests for the OpenSky integration.""" -from collections.abc import Awaitable, Callable -from unittest.mock import patch +from collections.abc import Generator +from unittest.mock import AsyncMock, patch import pytest +from python_opensky import StatesResponse from homeassistant.components.opensky.const import ( CONF_ALTITUDE, @@ -17,14 +18,18 @@ from homeassistant.const import ( CONF_RADIUS, CONF_USERNAME, ) -from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component -from . import get_states_response_fixture +from tests.common import MockConfigEntry, load_json_object_fixture -from tests.common import MockConfigEntry -ComponentSetup = Callable[[MockConfigEntry], Awaitable[None]] +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.opensky.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + yield mock_setup_entry @pytest.fixture(name="config_entry") @@ -81,19 +86,22 @@ def mock_config_entry_authenticated() -> MockConfigEntry: ) -@pytest.fixture(name="setup_integration") -async def mock_setup_integration( - hass: HomeAssistant, -) -> Callable[[MockConfigEntry], Awaitable[None]]: - """Fixture for setting up the component.""" - - async def func(mock_config_entry: MockConfigEntry) -> None: - mock_config_entry.add_to_hass(hass) - with patch( - "python_opensky.OpenSky.get_states", - return_value=get_states_response_fixture("opensky/states.json"), - ): - assert await async_setup_component(hass, DOMAIN, {}) - await hass.async_block_till_done() - - return func +@pytest.fixture +async def opensky_client() -> Generator[AsyncMock, None, None]: + """Mock the OpenSky client.""" + with ( + patch( + "homeassistant.components.opensky.OpenSky", + autospec=True, + ) as mock_client, + patch( + "homeassistant.components.opensky.config_flow.OpenSky", + new=mock_client, + ), + ): + client = mock_client.return_value + client.get_states.return_value = StatesResponse.from_api( + load_json_object_fixture("states.json", DOMAIN) + ) + client.is_authenticated = False + yield client diff --git a/tests/components/opensky/test_config_flow.py b/tests/components/opensky/test_config_flow.py index c3ae876d36e..8168511a16c 100644 --- a/tests/components/opensky/test_config_flow.py +++ b/tests/components/opensky/test_config_flow.py @@ -1,7 +1,7 @@ """Test OpenSky config flow.""" from typing import Any -from unittest.mock import patch +from unittest.mock import AsyncMock import pytest from python_opensky.exceptions import OpenSkyUnauthenticatedError @@ -23,39 +23,36 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from . import get_states_response_fixture, patch_setup_entry -from .conftest import ComponentSetup - from tests.common import MockConfigEntry +from tests.components.opensky import setup_integration -async def test_full_user_flow(hass: HomeAssistant) -> None: +async def test_full_user_flow(hass: HomeAssistant, mock_setup_entry) -> None: """Test the full user configuration flow.""" - with patch_setup_entry(): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - ) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_RADIUS: 10, - CONF_LATITUDE: 0.0, - CONF_LONGITUDE: 0.0, - CONF_ALTITUDE: 0, - }, - ) - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "OpenSky" - assert result["data"] == { + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_RADIUS: 10, CONF_LATITUDE: 0.0, CONF_LONGITUDE: 0.0, - } - assert result["options"] == { - CONF_ALTITUDE: 0.0, - CONF_RADIUS: 10.0, - } + CONF_ALTITUDE: 0, + }, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "OpenSky" + assert result["data"] == { + CONF_LATITUDE: 0.0, + CONF_LONGITUDE: 0.0, + } + assert result["options"] == { + CONF_ALTITUDE: 0.0, + CONF_RADIUS: 10.0, + } @pytest.mark.parametrize( @@ -79,92 +76,77 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: ) async def test_options_flow_failures( hass: HomeAssistant, - setup_integration: ComponentSetup, + mock_setup_entry: AsyncMock, + opensky_client: AsyncMock, config_entry: MockConfigEntry, user_input: dict[str, Any], error: str, ) -> None: """Test load and unload entry.""" - await setup_integration(config_entry) - entry = hass.config_entries.async_entries(DOMAIN)[0] - with patch( - "python_opensky.OpenSky.authenticate", - side_effect=OpenSkyUnauthenticatedError(), - ): - result = await hass.config_entries.options.async_init(entry.entry_id) - await hass.async_block_till_done() + await setup_integration(hass, config_entry) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "init" + opensky_client.authenticate.side_effect = OpenSkyUnauthenticatedError + result = await hass.config_entries.options.async_init(config_entry.entry_id) + await hass.async_block_till_done() - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={CONF_RADIUS: 10000, **user_input}, - ) - await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "init" - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "init" - assert result["errors"]["base"] == error - with ( - patch("python_opensky.OpenSky.authenticate"), - patch( - "python_opensky.OpenSky.get_states", - return_value=get_states_response_fixture("opensky/states_1.json"), - ), - ): - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - CONF_RADIUS: 10000, - CONF_USERNAME: "homeassistant", - CONF_PASSWORD: "secret", - CONF_CONTRIBUTING_USER: True, - }, - ) - await hass.async_block_till_done() + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={CONF_RADIUS: 10000, **user_input}, + ) + await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["data"] == { + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "init" + assert result["errors"]["base"] == error + opensky_client.authenticate.side_effect = None + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ CONF_RADIUS: 10000, CONF_USERNAME: "homeassistant", CONF_PASSWORD: "secret", CONF_CONTRIBUTING_USER: True, - } + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + CONF_RADIUS: 10000, + CONF_USERNAME: "homeassistant", + CONF_PASSWORD: "secret", + CONF_CONTRIBUTING_USER: True, + } async def test_options_flow( hass: HomeAssistant, - setup_integration: ComponentSetup, + mock_setup_entry: AsyncMock, + opensky_client: AsyncMock, config_entry: MockConfigEntry, ) -> None: """Test options flow.""" - await setup_integration(config_entry) - entry = hass.config_entries.async_entries(DOMAIN)[0] - result = await hass.config_entries.options.async_init(entry.entry_id) + await setup_integration(hass, config_entry) + result = await hass.config_entries.options.async_init(config_entry.entry_id) await hass.async_block_till_done() - with ( - patch("python_opensky.OpenSky.authenticate"), - patch( - "python_opensky.OpenSky.get_states", - return_value=get_states_response_fixture("opensky/states_1.json"), - ), - ): - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - CONF_RADIUS: 10000, - CONF_USERNAME: "homeassistant", - CONF_PASSWORD: "secret", - CONF_CONTRIBUTING_USER: True, - }, - ) - await hass.async_block_till_done() - - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["data"] == { + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ CONF_RADIUS: 10000, CONF_USERNAME: "homeassistant", CONF_PASSWORD: "secret", CONF_CONTRIBUTING_USER: True, - } + }, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["data"] == { + CONF_RADIUS: 10000, + CONF_USERNAME: "homeassistant", + CONF_PASSWORD: "secret", + CONF_CONTRIBUTING_USER: True, + } diff --git a/tests/components/opensky/test_init.py b/tests/components/opensky/test_init.py index a9e1668d026..f5acf7479a2 100644 --- a/tests/components/opensky/test_init.py +++ b/tests/components/opensky/test_init.py @@ -2,67 +2,59 @@ from __future__ import annotations -from unittest.mock import patch +from unittest.mock import AsyncMock from python_opensky import OpenSkyError from python_opensky.exceptions import OpenSkyUnauthenticatedError -from homeassistant.components.opensky.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component - -from .conftest import ComponentSetup from tests.common import MockConfigEntry +from tests.components.opensky import setup_integration async def test_load_unload_entry( hass: HomeAssistant, - setup_integration: ComponentSetup, config_entry: MockConfigEntry, + opensky_client: AsyncMock, ) -> None: """Test load and unload entry.""" - await setup_integration(config_entry) - entry = hass.config_entries.async_entries(DOMAIN)[0] + await setup_integration(hass, config_entry) - state = hass.states.get("sensor.opensky") - assert state + assert config_entry.state is ConfigEntryState.LOADED - await hass.config_entries.async_remove(entry.entry_id) + await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - state = hass.states.get("sensor.opensky") - assert not state + assert config_entry.state is ConfigEntryState.NOT_LOADED async def test_load_entry_failure( hass: HomeAssistant, config_entry: MockConfigEntry, + opensky_client: AsyncMock, ) -> None: """Test failure while loading.""" + opensky_client.get_states.side_effect = OpenSkyError() config_entry.add_to_hass(hass) - with patch( - "python_opensky.OpenSky.get_states", - side_effect=OpenSkyError(), - ): - assert await async_setup_component(hass, DOMAIN, {}) - await hass.async_block_till_done() - entry = hass.config_entries.async_entries(DOMAIN)[0] - assert entry.state == ConfigEntryState.SETUP_RETRY + assert not 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_load_entry_authentication_failure( hass: HomeAssistant, config_entry_authenticated: MockConfigEntry, + opensky_client: AsyncMock, ) -> None: """Test auth failure while loading.""" + opensky_client.authenticate.side_effect = OpenSkyUnauthenticatedError() config_entry_authenticated.add_to_hass(hass) - with patch( - "python_opensky.OpenSky.authenticate", - side_effect=OpenSkyUnauthenticatedError(), - ): - assert await async_setup_component(hass, DOMAIN, {}) - await hass.async_block_till_done() - entry = hass.config_entries.async_entries(DOMAIN)[0] - assert entry.state == ConfigEntryState.SETUP_RETRY + assert not await hass.config_entries.async_setup( + config_entry_authenticated.entry_id + ) + await hass.async_block_till_done() + + assert config_entry_authenticated.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/opensky/test_sensor.py b/tests/components/opensky/test_sensor.py index df4faaa3e4a..801980ec5b9 100644 --- a/tests/components/opensky/test_sensor.py +++ b/tests/components/opensky/test_sensor.py @@ -1,31 +1,35 @@ """OpenSky sensor tests.""" from datetime import timedelta -from unittest.mock import patch +from unittest.mock import AsyncMock from freezegun.api import FrozenDateTimeFactory +from python_opensky import StatesResponse from syrupy import SnapshotAssertion from homeassistant.components.opensky.const import ( + DOMAIN, EVENT_OPENSKY_ENTRY, EVENT_OPENSKY_EXIT, ) from homeassistant.core import Event, HomeAssistant -from . import get_states_response_fixture -from .conftest import ComponentSetup - -from tests.common import MockConfigEntry, async_fire_time_changed +from tests.common import ( + MockConfigEntry, + async_fire_time_changed, + load_json_object_fixture, +) +from tests.components.opensky import setup_integration async def test_sensor( hass: HomeAssistant, config_entry: MockConfigEntry, - setup_integration: ComponentSetup, snapshot: SnapshotAssertion, + opensky_client: AsyncMock, ): """Test setup sensor.""" - await setup_integration(config_entry) + await setup_integration(hass, config_entry) state = hass.states.get("sensor.opensky") assert state == snapshot @@ -42,11 +46,11 @@ async def test_sensor( async def test_sensor_altitude( hass: HomeAssistant, config_entry_altitude: MockConfigEntry, - setup_integration: ComponentSetup, + opensky_client: AsyncMock, snapshot: SnapshotAssertion, ): """Test setup sensor with a set altitude.""" - await setup_integration(config_entry_altitude) + await setup_integration(hass, config_entry_altitude) state = hass.states.get("sensor.opensky") assert state == snapshot @@ -55,12 +59,12 @@ async def test_sensor_altitude( async def test_sensor_updating( hass: HomeAssistant, config_entry: MockConfigEntry, + opensky_client: AsyncMock, freezer: FrozenDateTimeFactory, - setup_integration: ComponentSetup, snapshot: SnapshotAssertion, ): """Test updating sensor.""" - await setup_integration(config_entry) + await setup_integration(hass, config_entry) events = [] @@ -77,13 +81,11 @@ async def test_sensor_updating( assert events == snapshot - with patch( - "python_opensky.OpenSky.get_states", - return_value=get_states_response_fixture("opensky/states_1.json"), - ): - await skip_time_and_check_events() - with patch( - "python_opensky.OpenSky.get_states", - return_value=get_states_response_fixture("opensky/states.json"), - ): - await skip_time_and_check_events() + opensky_client.get_states.return_value = StatesResponse.from_api( + load_json_object_fixture("states_1.json", DOMAIN) + ) + await skip_time_and_check_events() + opensky_client.get_states.return_value = StatesResponse.from_api( + load_json_object_fixture("states.json", DOMAIN) + ) + await skip_time_and_check_events() From a1eef4732fedd6cb22dad3965618e180c856eadc Mon Sep 17 00:00:00 2001 From: Tom Matheussen Date: Sat, 30 Mar 2024 15:13:26 +0100 Subject: [PATCH 090/967] Add hourly forecast to open_meteo (#113622) * Add hourly forecast to open_meteo * Ran ruff formatting again --- .../components/open_meteo/__init__.py | 6 ++ .../components/open_meteo/weather.py | 73 ++++++++++++++++--- 2 files changed, 70 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/open_meteo/__init__.py b/homeassistant/components/open_meteo/__init__.py index ac09b0f61a2..e3bf763f429 100644 --- a/homeassistant/components/open_meteo/__init__.py +++ b/homeassistant/components/open_meteo/__init__.py @@ -5,6 +5,7 @@ from __future__ import annotations from open_meteo import ( DailyParameters, Forecast, + HourlyParameters, OpenMeteo, OpenMeteoError, PrecipitationUnit, @@ -45,6 +46,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DailyParameters.WIND_DIRECTION_10M_DOMINANT, DailyParameters.WIND_SPEED_10M_MAX, ], + hourly=[ + HourlyParameters.PRECIPITATION, + HourlyParameters.TEMPERATURE_2M, + HourlyParameters.WEATHER_CODE, + ], precipitation_unit=PrecipitationUnit.MILLIMETERS, temperature_unit=TemperatureUnit.CELSIUS, timezone="UTC", diff --git a/homeassistant/components/open_meteo/weather.py b/homeassistant/components/open_meteo/weather.py index 8ee3edd5183..a2be81f0928 100644 --- a/homeassistant/components/open_meteo/weather.py +++ b/homeassistant/components/open_meteo/weather.py @@ -5,6 +5,12 @@ from __future__ import annotations from open_meteo import Forecast as OpenMeteoForecast from homeassistant.components.weather import ( + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, + ATTR_FORECAST_WIND_BEARING, Forecast, SingleCoordinatorWeatherEntity, WeatherEntityFeature, @@ -15,6 +21,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.util import dt as dt_util from .const import DOMAIN, WMO_TO_HA_CONDITION_MAP @@ -39,7 +46,9 @@ class OpenMeteoWeatherEntity( _attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS _attr_native_temperature_unit = UnitOfTemperature.CELSIUS _attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR - _attr_supported_features = WeatherEntityFeature.FORECAST_DAILY + _attr_supported_features = ( + WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY + ) def __init__( self, @@ -95,31 +104,77 @@ class OpenMeteoWeatherEntity( return None forecasts: list[Forecast] = [] + daily = self.coordinator.data.daily - for index, time in enumerate(self.coordinator.data.daily.time): + for index, date in enumerate(self.coordinator.data.daily.time): forecast = Forecast( - datetime=time.isoformat(), + datetime=date.isoformat(), ) if daily.weathercode is not None: - forecast["condition"] = WMO_TO_HA_CONDITION_MAP.get( + forecast[ATTR_FORECAST_CONDITION] = WMO_TO_HA_CONDITION_MAP.get( daily.weathercode[index] ) if daily.precipitation_sum is not None: - forecast["native_precipitation"] = daily.precipitation_sum[index] + forecast[ATTR_FORECAST_NATIVE_PRECIPITATION] = daily.precipitation_sum[ + index + ] if daily.temperature_2m_max is not None: - forecast["native_temperature"] = daily.temperature_2m_max[index] + forecast[ATTR_FORECAST_NATIVE_TEMP] = daily.temperature_2m_max[index] if daily.temperature_2m_min is not None: - forecast["native_templow"] = daily.temperature_2m_min[index] + forecast[ATTR_FORECAST_NATIVE_TEMP_LOW] = daily.temperature_2m_min[ + index + ] if daily.wind_direction_10m_dominant is not None: - forecast["wind_bearing"] = daily.wind_direction_10m_dominant[index] + forecast[ATTR_FORECAST_WIND_BEARING] = ( + daily.wind_direction_10m_dominant[index] + ) if daily.wind_speed_10m_max is not None: - forecast["native_wind_speed"] = daily.wind_speed_10m_max[index] + forecast[ATTR_FORECAST_NATIVE_WIND_SPEED] = daily.wind_speed_10m_max[ + index + ] + + forecasts.append(forecast) + + return forecasts + + @callback + def _async_forecast_hourly(self) -> list[Forecast] | None: + """Return the daily forecast in native units.""" + if self.coordinator.data.hourly is None: + return None + + forecasts: list[Forecast] = [] + + # Can have data in the past: https://github.com/open-meteo/open-meteo/issues/699 + today = dt_util.utcnow() + + hourly = self.coordinator.data.hourly + for index, datetime in enumerate(self.coordinator.data.hourly.time): + if dt_util.as_utc(datetime) < today: + continue + + forecast = Forecast( + datetime=datetime.isoformat(), + ) + + if hourly.weather_code is not None: + forecast[ATTR_FORECAST_CONDITION] = WMO_TO_HA_CONDITION_MAP.get( + hourly.weather_code[index] + ) + + if hourly.precipitation is not None: + forecast[ATTR_FORECAST_NATIVE_PRECIPITATION] = hourly.precipitation[ + index + ] + + if hourly.temperature_2m is not None: + forecast[ATTR_FORECAST_NATIVE_TEMP] = hourly.temperature_2m[index] forecasts.append(forecast) From 53f262095c69ba51377e21ae964936796681da1e Mon Sep 17 00:00:00 2001 From: Bruno Henrique Date: Sat, 30 Mar 2024 11:53:23 -0300 Subject: [PATCH 091/967] Add UniFi WLAN regenerate password button (#114422) * Adding UniFi WLAN Change Password Button Co-authored-by: Robert Svensson * Adding UniFi WLAN Regenerate Password Button Co-authored-by: Robert Svensson --------- Co-authored-by: Robert Svensson --- homeassistant/components/unifi/button.py | 29 +++++- tests/components/unifi/test_button.py | 112 +++++++++++++++++++++++ 2 files changed, 140 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/unifi/button.py b/homeassistant/components/unifi/button.py index 45fc76c73df..86c38a5bf3d 100644 --- a/homeassistant/components/unifi/button.py +++ b/homeassistant/components/unifi/button.py @@ -7,12 +7,14 @@ from __future__ import annotations from collections.abc import Callable, Coroutine from dataclasses import dataclass +import secrets from typing import Any import aiounifi from aiounifi.interfaces.api_handlers import ItemEvent from aiounifi.interfaces.devices import Devices from aiounifi.interfaces.ports import Ports +from aiounifi.interfaces.wlans import Wlans from aiounifi.models.api import ApiItemT from aiounifi.models.device import ( Device, @@ -20,6 +22,7 @@ from aiounifi.models.device import ( DeviceRestartRequest, ) from aiounifi.models.port import Port +from aiounifi.models.wlan import Wlan, WlanChangePasswordRequest from homeassistant.components.button import ( ButtonDeviceClass, @@ -37,6 +40,8 @@ from .entity import ( UnifiEntityDescription, async_device_available_fn, async_device_device_info_fn, + async_wlan_available_fn, + async_wlan_device_info_fn, ) from .hub import UnifiHub @@ -56,6 +61,15 @@ async def async_power_cycle_port_control_fn( await api.request(DevicePowerCyclePortRequest.create(mac, int(index))) +async def async_regenerate_password_control_fn( + api: aiounifi.Controller, obj_id: str +) -> None: + """Regenerate WLAN password.""" + await api.request( + WlanChangePasswordRequest.create(obj_id, secrets.token_urlsafe(15)) + ) + + @dataclass(frozen=True, kw_only=True) class UnifiButtonEntityDescription( ButtonEntityDescription, UnifiEntityDescription[HandlerT, ApiItemT] @@ -91,6 +105,19 @@ ENTITY_DESCRIPTIONS: tuple[UnifiButtonEntityDescription, ...] = ( supported_fn=lambda hub, obj_id: bool(hub.api.ports[obj_id].port_poe), unique_id_fn=lambda hub, obj_id: f"power_cycle-{obj_id}", ), + UnifiButtonEntityDescription[Wlans, Wlan]( + key="WLAN regenerate password", + device_class=ButtonDeviceClass.UPDATE, + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + api_handler_fn=lambda api: api.wlans, + available_fn=async_wlan_available_fn, + control_fn=async_regenerate_password_control_fn, + device_info_fn=async_wlan_device_info_fn, + name_fn=lambda wlan: "Regenerate Password", + object_fn=lambda api, obj_id: api.wlans[obj_id], + unique_id_fn=lambda hub, obj_id: f"regenerate_password-{obj_id}", + ), ) @@ -109,7 +136,7 @@ async def async_setup_entry( class UnifiButtonEntity(UnifiEntity[HandlerT, ApiItemT], ButtonEntity): - """Base representation of a UniFi image.""" + """Base representation of a UniFi button.""" entity_description: UnifiButtonEntityDescription[HandlerT, ApiItemT] diff --git a/tests/components/unifi/test_button.py b/tests/components/unifi/test_button.py index d5be861139b..8f9838e3e37 100644 --- a/tests/components/unifi/test_button.py +++ b/tests/components/unifi/test_button.py @@ -1,20 +1,64 @@ """UniFi Network button platform tests.""" +from datetime import timedelta + 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.const import ( ATTR_DEVICE_CLASS, CONF_HOST, + CONTENT_TYPE_JSON, 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 +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", +} + async def test_restart_device_button( hass: HomeAssistant, @@ -168,3 +212,71 @@ async def test_power_cycle_poe( assert ( hass.states.get("button.switch_port_1_power_cycle").state != STATE_UNAVAILABLE ) + + +async def test_wlan_regenerate_password( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + aioclient_mock: AiohttpClientMocker, + websocket_mock, +) -> None: + """Test WLAN regenerate password button.""" + + config_entry = await setup_unifi_integration( + hass, aioclient_mock, wlans_response=[WLAN] + ) + 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" + 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() + + 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}, + ) + + # 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 3e99afdd540b6ddc6941e0bdf5b393f9960fce78 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 30 Mar 2024 18:48:57 +0300 Subject: [PATCH 092/967] Cleanup Shelly RGBW light entities (#114410) --- homeassistant/components/shelly/const.py | 2 + homeassistant/components/shelly/light.py | 17 +++++ homeassistant/components/shelly/utils.py | 12 ++++ tests/components/shelly/__init__.py | 12 ++++ tests/components/shelly/conftest.py | 6 ++ tests/components/shelly/test_light.py | 88 +++++++++++++++++++++++- 6 files changed, 134 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index 3580bcf9b38..2ac0416bb6c 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -234,3 +234,5 @@ DEVICES_WITHOUT_FIRMWARE_CHANGELOG = ( ) CONF_GEN = "gen" + +SHELLY_PLUS_RGBW_CHANNELS = 4 diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index 6c28023a5e3..d0590fc7c20 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -14,6 +14,7 @@ from homeassistant.components.light import ( ATTR_RGB_COLOR, ATTR_RGBW_COLOR, ATTR_TRANSITION, + DOMAIN as LIGHT_DOMAIN, ColorMode, LightEntity, LightEntityFeature, @@ -34,12 +35,14 @@ from .const import ( RGBW_MODELS, RPC_MIN_TRANSITION_TIME_SEC, SHBLB_1_RGB_EFFECTS, + SHELLY_PLUS_RGBW_CHANNELS, STANDARD_RGB_EFFECTS, ) from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator, get_entry_data from .entity import ShellyBlockEntity, ShellyRpcEntity from .utils import ( async_remove_shelly_entity, + async_remove_shelly_rpc_entities, brightness_to_percentage, get_device_entry_gen, get_rpc_key_ids, @@ -118,14 +121,28 @@ def async_setup_rpc_entry( return if light_key_ids := get_rpc_key_ids(coordinator.device.status, "light"): + # Light mode remove RGB & RGBW entities, add light entities + async_remove_shelly_rpc_entities( + hass, LIGHT_DOMAIN, coordinator.mac, ["rgb:0", "rgbw:0"] + ) async_add_entities(RpcShellyLight(coordinator, id_) for id_ in light_key_ids) return + light_keys = [f"light:{i}" for i in range(SHELLY_PLUS_RGBW_CHANNELS)] + if rgb_key_ids := get_rpc_key_ids(coordinator.device.status, "rgb"): + # RGB mode remove light & RGBW entities, add RGB entity + async_remove_shelly_rpc_entities( + hass, LIGHT_DOMAIN, coordinator.mac, [*light_keys, "rgbw:0"] + ) async_add_entities(RpcShellyRgbLight(coordinator, id_) for id_ in rgb_key_ids) return if rgbw_key_ids := get_rpc_key_ids(coordinator.device.status, "rgbw"): + # RGBW mode remove light & RGB entities, add RGBW entity + async_remove_shelly_rpc_entities( + hass, LIGHT_DOMAIN, coordinator.mac, [*light_keys, "rgb:0"] + ) async_add_entities(RpcShellyRgbwLight(coordinator, id_) for id_ in rgbw_key_ids) diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index d26e3dc11f3..ce98e0d5c12 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -488,3 +488,15 @@ async def async_shutdown_device(device: BlockDevice | RpcDevice) -> None: await device.shutdown() if isinstance(device, BlockDevice): device.shutdown() + + +@callback +def async_remove_shelly_rpc_entities( + hass: HomeAssistant, domain: str, mac: str, keys: list[str] +) -> None: + """Remove RPC based Shelly entity.""" + entity_reg = er_async_get(hass) + for key in keys: + if entity_id := entity_reg.async_get_entity_id(domain, DOMAIN, f"{mac}-{key}"): + LOGGER.debug("Removing entity: %s", entity_id) + entity_reg.async_remove(entity_id) diff --git a/tests/components/shelly/__init__.py b/tests/components/shelly/__init__.py index 2dc9012d863..348b1115a6f 100644 --- a/tests/components/shelly/__init__.py +++ b/tests/components/shelly/__init__.py @@ -126,6 +126,18 @@ def register_entity( return f"{domain}.{object_id}" +def get_entity( + hass: HomeAssistant, + domain: str, + unique_id: str, +) -> str | None: + """Get Shelly entity.""" + entity_registry = async_get(hass) + return entity_registry.async_get_entity_id( + domain, DOMAIN, f"{MOCK_MAC}-{unique_id}" + ) + + def get_entity_state(hass: HomeAssistant, entity_id: str) -> str: """Return entity state.""" entity = hass.states.get(entity_id) diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index 9a73252ca6c..3cd27101f76 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -169,6 +169,9 @@ MOCK_CONFIG = { "input:1": {"id": 1, "type": "analog", "enable": True}, "input:2": {"id": 2, "name": "Gas", "type": "count", "enable": True}, "light:0": {"name": "test light_0"}, + "light:1": {"name": "test light_1"}, + "light:2": {"name": "test light_2"}, + "light:3": {"name": "test light_3"}, "rgb:0": {"name": "test rgb_0"}, "rgbw:0": {"name": "test rgbw_0"}, "switch:0": {"name": "test switch_0"}, @@ -225,6 +228,9 @@ MOCK_STATUS_RPC = { "input:1": {"id": 1, "percent": 89, "xpercent": 8.9}, "input:2": {"id": 2, "counts": {"total": 56174, "xtotal": 561.74}}, "light:0": {"output": True, "brightness": 53.0}, + "light:1": {"output": True, "brightness": 53.0}, + "light:2": {"output": True, "brightness": 53.0}, + "light:3": {"output": True, "brightness": 53.0}, "rgb:0": {"output": True, "brightness": 53.0, "rgb": [45, 55, 65]}, "rgbw:0": {"output": True, "brightness": 53.0, "rgb": [21, 22, 23], "white": 120}, "cloud": {"connected": False}, diff --git a/tests/components/shelly/test_light.py b/tests/components/shelly/test_light.py index cca318c364d..2c464a8c39c 100644 --- a/tests/components/shelly/test_light.py +++ b/tests/components/shelly/test_light.py @@ -29,6 +29,7 @@ from homeassistant.components.light import ( ColorMode, LightEntityFeature, ) +from homeassistant.components.shelly.const import SHELLY_PLUS_RGBW_CHANNELS from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, @@ -38,7 +39,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_registry import EntityRegistry -from . import init_integration, mutate_rpc_device_status +from . import get_entity, init_integration, mutate_rpc_device_status, register_entity from .conftest import mock_white_light_set_state RELAY_BLOCK_ID = 0 @@ -587,7 +588,8 @@ async def test_rpc_device_rgb_profile( monkeypatch: pytest.MonkeyPatch, ) -> None: """Test RPC device in RGB profile.""" - monkeypatch.delitem(mock_rpc_device.status, "light:0") + for i in range(SHELLY_PLUS_RGBW_CHANNELS): + monkeypatch.delitem(mock_rpc_device.status, f"light:{i}") monkeypatch.delitem(mock_rpc_device.status, "rgbw:0") entity_id = "light.test_rgb_0" await init_integration(hass, 2) @@ -633,7 +635,8 @@ async def test_rpc_device_rgbw_profile( monkeypatch: pytest.MonkeyPatch, ) -> None: """Test RPC device in RGBW profile.""" - monkeypatch.delitem(mock_rpc_device.status, "light:0") + for i in range(SHELLY_PLUS_RGBW_CHANNELS): + monkeypatch.delitem(mock_rpc_device.status, f"light:{i}") monkeypatch.delitem(mock_rpc_device.status, "rgb:0") entity_id = "light.test_rgbw_0" await init_integration(hass, 2) @@ -673,3 +676,82 @@ async def test_rpc_device_rgbw_profile( entry = entity_registry.async_get(entity_id) assert entry assert entry.unique_id == "123456789ABC-rgbw:0" + + +async def test_rpc_rgbw_device_light_mode_remove_others( + hass: HomeAssistant, + mock_rpc_device: Mock, + entity_registry: EntityRegistry, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test Shelly RPC RGBW device in light mode removes RGB/RGBW entities.""" + # register lights + monkeypatch.delitem(mock_rpc_device.status, "rgb:0") + monkeypatch.delitem(mock_rpc_device.status, "rgbw:0") + register_entity(hass, LIGHT_DOMAIN, "test_rgb_0", "rgb:0") + register_entity(hass, LIGHT_DOMAIN, "test_rgbw_0", "rgbw:0") + + # verify RGB & RGBW entities created + assert get_entity(hass, LIGHT_DOMAIN, "rgb:0") is not None + assert get_entity(hass, LIGHT_DOMAIN, "rgbw:0") is not None + + # init to remove RGB & RGBW + await init_integration(hass, 2) + + # verify we have 4 lights + for i in range(SHELLY_PLUS_RGBW_CHANNELS): + entity_id = f"light.test_light_{i}" + assert hass.states.get(entity_id).state == STATE_ON + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == f"123456789ABC-light:{i}" + + # verify RGB & RGBW entities removed + assert get_entity(hass, LIGHT_DOMAIN, "rgb:0") is None + assert get_entity(hass, LIGHT_DOMAIN, "rgbw:0") is None + + +@pytest.mark.parametrize( + ("active_mode", "removed_mode"), + [ + ("rgb", "rgbw"), + ("rgbw", "rgb"), + ], +) +async def test_rpc_rgbw_device_rgb_w_modes_remove_others( + hass: HomeAssistant, + mock_rpc_device: Mock, + entity_registry: EntityRegistry, + monkeypatch: pytest.MonkeyPatch, + active_mode: str, + removed_mode: str, +) -> None: + """Test Shelly RPC RGBW device in RGB/W modes other lights.""" + removed_key = f"{removed_mode}:0" + + # register lights + for i in range(SHELLY_PLUS_RGBW_CHANNELS): + monkeypatch.delitem(mock_rpc_device.status, f"light:{i}") + entity_id = f"light.test_light_{i}" + register_entity(hass, LIGHT_DOMAIN, entity_id, f"light:{i}") + monkeypatch.delitem(mock_rpc_device.status, f"{removed_mode}:0") + register_entity(hass, LIGHT_DOMAIN, f"test_{removed_key}", removed_key) + + # verify lights entities created + for i in range(SHELLY_PLUS_RGBW_CHANNELS): + assert get_entity(hass, LIGHT_DOMAIN, f"light:{i}") is not None + assert get_entity(hass, LIGHT_DOMAIN, removed_key) is not None + + await init_integration(hass, 2) + + # verify we have RGB/w light + entity_id = f"light.test_{active_mode}_0" + assert hass.states.get(entity_id).state == STATE_ON + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == f"123456789ABC-{active_mode}:0" + + # verify light & RGB/W entities removed + for i in range(SHELLY_PLUS_RGBW_CHANNELS): + assert get_entity(hass, LIGHT_DOMAIN, f"light:{i}") is None + assert get_entity(hass, LIGHT_DOMAIN, removed_key) is None From d63adb63500b52bfbbdcc0e963b0706bf0ed2c3c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 30 Mar 2024 06:34:47 -1000 Subject: [PATCH 093/967] Improve sonos test synchronization (#114468) --- tests/components/sonos/conftest.py | 35 +++++++++++++++++++++++--- tests/components/sonos/test_repairs.py | 12 ++++----- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 00858a180a3..576c9a80799 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -1,16 +1,20 @@ """Configuration for Sonos tests.""" +import asyncio +from collections.abc import Callable from copy import copy from ipaddress import ip_address from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest from soco import SoCo +from soco.events_base import Event as SonosEvent from homeassistant.components import ssdp, zeroconf from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.sonos import DOMAIN from homeassistant.const import CONF_HOSTS +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_fixture @@ -30,6 +34,31 @@ class SonosMockSubscribe: """Initialize the mock subscriber.""" self.event_listener = SonosMockEventListener(ip_address) self.service = Mock() + self.callback_future: asyncio.Future[Callable[[SonosEvent], None]] = None + self._callback: Callable[[SonosEvent], None] | None = None + + @property + def callback(self) -> Callable[[SonosEvent], None] | None: + """Return the callback.""" + return self._callback + + @callback.setter + def callback(self, callback: Callable[[SonosEvent], None]) -> None: + """Set the callback.""" + self._callback = callback + future = self._get_callback_future() + if not future.done(): + future.set_result(callback) + + def _get_callback_future(self) -> asyncio.Future[Callable[[SonosEvent], None]]: + """Get the callback future.""" + if not self.callback_future: + self.callback_future = asyncio.get_running_loop().create_future() + return self.callback_future + + async def wait_for_callback_to_be_set(self) -> Callable[[SonosEvent], None]: + """Wait for the callback to be set.""" + return await self._get_callback_future() async def unsubscribe(self) -> None: """Unsubscribe mock.""" @@ -456,14 +485,14 @@ def zgs_discovery_fixture(): @pytest.fixture(name="fire_zgs_event") -def zgs_event_fixture(hass, soco, zgs_discovery): +def zgs_event_fixture(hass: HomeAssistant, soco: SoCo, zgs_discovery: str): """Create alarm_event fixture.""" variables = {"ZoneGroupState": zgs_discovery} async def _wrapper(): event = SonosMockEvent(soco, soco.zoneGroupTopology, variables) - subscription = soco.zoneGroupTopology.subscribe.return_value - sub_callback = subscription.callback + subscription: SonosMockSubscribe = soco.zoneGroupTopology.subscribe.return_value + sub_callback = await subscription.wait_for_callback_to_be_set() sub_callback(event) await hass.async_block_till_done() diff --git a/tests/components/sonos/test_repairs.py b/tests/components/sonos/test_repairs.py index cf64912e498..49b87b272d6 100644 --- a/tests/components/sonos/test_repairs.py +++ b/tests/components/sonos/test_repairs.py @@ -2,6 +2,8 @@ from unittest.mock import Mock +from soco import SoCo + from homeassistant.components.sonos.const import ( DOMAIN, SCAN_INTERVAL, @@ -11,27 +13,25 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.issue_registry import async_get as async_get_issue_registry from homeassistant.util import dt as dt_util -from .conftest import SonosMockEvent +from .conftest import SonosMockEvent, SonosMockSubscribe from tests.common import MockConfigEntry, async_fire_time_changed async def test_subscription_repair_issues( - hass: HomeAssistant, config_entry: MockConfigEntry, soco, zgs_discovery + hass: HomeAssistant, config_entry: MockConfigEntry, soco: SoCo, zgs_discovery ) -> None: """Test repair issues handling for failed subscriptions.""" issue_registry = async_get_issue_registry(hass) - subscription = soco.zoneGroupTopology.subscribe.return_value + subscription: SonosMockSubscribe = soco.zoneGroupTopology.subscribe.return_value subscription.event_listener = Mock(address=("192.168.4.2", 1400)) config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - await hass.async_block_till_done() # Ensure an issue is registered on subscription failure - sub_callback = subscription.callback + sub_callback = await subscription.wait_for_callback_to_be_set() async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) await hass.async_block_till_done(wait_background_tasks=True) assert issue_registry.async_get_issue(DOMAIN, SUB_FAIL_ISSUE_ID) From 0b9c40a2330031da0daa9aa50be0b3ef7f3a10fc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 30 Mar 2024 08:05:14 -1000 Subject: [PATCH 094/967] Fix workday doing blocking I/O in the event loop (#114492) --- homeassistant/components/workday/__init__.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/workday/__init__.py b/homeassistant/components/workday/__init__.py index 195221ef088..077a6710b8d 100644 --- a/homeassistant/components/workday/__init__.py +++ b/homeassistant/components/workday/__init__.py @@ -2,6 +2,8 @@ from __future__ import annotations +from functools import partial + from holidays import HolidayBase, country_holidays from homeassistant.config_entries import ConfigEntry @@ -13,7 +15,7 @@ from homeassistant.helpers.issue_registry import IssueSeverity, async_create_iss from .const import CONF_PROVINCE, DOMAIN, PLATFORMS -def _validate_country_and_province( +async def _async_validate_country_and_province( hass: HomeAssistant, entry: ConfigEntry, country: str | None, province: str | None ) -> None: """Validate country and province.""" @@ -21,7 +23,7 @@ def _validate_country_and_province( if not country: return try: - country_holidays(country) + await hass.async_add_executor_job(country_holidays, country) except NotImplementedError as ex: async_create_issue( hass, @@ -39,7 +41,9 @@ def _validate_country_and_province( if not province: return try: - country_holidays(country, subdiv=province) + await hass.async_add_executor_job( + partial(country_holidays, country, subdiv=province) + ) except NotImplementedError as ex: async_create_issue( hass, @@ -66,10 +70,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: country: str | None = entry.options.get(CONF_COUNTRY) province: str | None = entry.options.get(CONF_PROVINCE) - _validate_country_and_province(hass, entry, country, province) + await _async_validate_country_and_province(hass, entry, country, province) if country and CONF_LANGUAGE not in entry.options: - cls: HolidayBase = country_holidays(country, subdiv=province) + cls: HolidayBase = await hass.async_add_executor_job( + partial(country_holidays, country, subdiv=province) + ) default_language = cls.default_language new_options = entry.options.copy() new_options[CONF_LANGUAGE] = default_language From 9f9a1411125f2a783890d61e2c81e2609e872281 Mon Sep 17 00:00:00 2001 From: Bruno Henrique Date: Sat, 30 Mar 2024 15:49:33 -0300 Subject: [PATCH 095/967] Add UniFi WLAN Password sensor (#114419) * Adding UniFi WLAN Password Sensor * Adding UniFi WLAN Password Sensor * Adding UniFi WLAN Password Sensor * Adding UniFi WLAN Password Sensor Co-authored-by: Robert Svensson * Adding UniFi WLAN Password Sensor Co-authored-by: Robert Svensson * Adding UniFi WLAN Password Sensor * Adding UniFi WLAN Password Sensor * Adding UniFi WLAN Password Sensor --------- Co-authored-by: Robert Svensson --- homeassistant/components/unifi/sensor.py | 13 +++++ tests/components/unifi/test_sensor.py | 71 ++++++++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index efb3eed4de4..54ecc2ea763 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -339,6 +339,19 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = ( value_fn=async_device_state_value_fn, options=list(DEVICE_STATES.values()), ), + UnifiSensorEntityDescription[Wlans, Wlan]( + key="WLAN password", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + api_handler_fn=lambda api: api.wlans, + available_fn=async_wlan_available_fn, + device_info_fn=async_wlan_device_info_fn, + name_fn=lambda wlan: "Password", + object_fn=lambda api, obj_id: api.wlans[obj_id], + supported_fn=lambda hub, obj_id: hub.api.wlans[obj_id].x_passphrase is not None, + unique_id_fn=lambda hub, obj_id: f"password-{obj_id}", + value_fn=lambda hub, obj: obj.x_passphrase, + ), ) diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index 7a58252a6bd..239707aa4c9 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -998,3 +998,74 @@ async def test_device_state( device["state"] = i mock_unifi_websocket(message=MessageKey.DEVICE, data=device) assert hass.states.get("sensor.device_state").state == DEVICE_STATES[i] + + +async def test_wlan_password( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + aioclient_mock: AiohttpClientMocker, + mock_unifi_websocket, + websocket_mock, +) -> None: + """Test the WLAN password sensor behavior.""" + await setup_unifi_integration(hass, aioclient_mock, wlans_response=[WLAN]) + + sensor_password = "sensor.ssid_1_password" + password = "password" + new_password = "new_password" + + ent_reg_entry = entity_registry.async_get(sensor_password) + assert ent_reg_entry.unique_id == "password-012345678910111213141516" + assert ent_reg_entry.disabled_by == RegistryEntryDisabler.INTEGRATION + assert ent_reg_entry.entity_category is EntityCategory.DIAGNOSTIC + + # Enable entity + entity_registry.async_update_entity(entity_id=sensor_password, 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 + wlan_password_sensor_1 = hass.states.get(sensor_password) + assert wlan_password_sensor_1.state == password + + # Update state object - same password - no change to state + mock_unifi_websocket(message=MessageKey.WLAN_CONF_UPDATED, data=WLAN) + await hass.async_block_till_done() + wlan_password_sensor_2 = hass.states.get(sensor_password) + assert wlan_password_sensor_1.state == wlan_password_sensor_2.state + + # 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) + await hass.async_block_till_done() + wlan_password_sensor_3 = hass.states.get(sensor_password) + assert wlan_password_sensor_1.state != wlan_password_sensor_3.state + + # Availability signaling + + # Controller disconnects + await websocket_mock.disconnect() + assert hass.states.get(sensor_password).state == STATE_UNAVAILABLE + + # Controller reconnects + await websocket_mock.reconnect() + assert hass.states.get(sensor_password).state == new_password + + # WLAN gets disabled + wlan_1 = deepcopy(WLAN) + wlan_1["enabled"] = False + mock_unifi_websocket(message=MessageKey.WLAN_CONF_UPDATED, data=wlan_1) + await hass.async_block_till_done() + assert hass.states.get(sensor_password).state == STATE_UNAVAILABLE + + # WLAN gets re-enabled + wlan_1["enabled"] = True + mock_unifi_websocket(message=MessageKey.WLAN_CONF_UPDATED, data=wlan_1) + await hass.async_block_till_done() + assert hass.states.get(sensor_password).state == password From ef5f6829e75c9f61085a926a2f04bd5de1a7bdda Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 30 Mar 2024 08:56:01 -1000 Subject: [PATCH 096/967] Fix late load of anyio doing blocking I/O in the event loop (#114491) * Fix late load of anyio doing blocking I/O in the event loop httpx loads anyio which loads the asyncio backend in the event loop as soon as httpx makes the first request * tweak --- homeassistant/bootstrap.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 03c0de1ff62..5b805b6138e 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -93,6 +93,11 @@ from .util.async_ import create_eager_task from .util.logging import async_activate_log_queue_handler from .util.package import async_get_user_site, is_virtual_env +with contextlib.suppress(ImportError): + # Ensure anyio backend is imported to avoid it being imported in the event loop + from anyio._backends import _asyncio # noqa: F401 + + if TYPE_CHECKING: from .runner import RuntimeConfig From 502231b7d2b9f4add7b558cc95cfc45ce99f9bd2 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Sat, 30 Mar 2024 21:15:52 +0100 Subject: [PATCH 097/967] Avoid call to `hass.helpers.store` in CategoryRegistry (#114485) --- homeassistant/helpers/category_registry.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/category_registry.py b/homeassistant/helpers/category_registry.py index ee0c8c1bb88..fec87262374 100644 --- a/homeassistant/helpers/category_registry.py +++ b/homeassistant/helpers/category_registry.py @@ -11,6 +11,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.util.ulid import ulid_now from .registry import BaseRegistry +from .storage import Store from .typing import UNDEFINED, EventType, UndefinedType DATA_REGISTRY = "category_registry" @@ -46,7 +47,8 @@ class CategoryRegistry(BaseRegistry): """Initialize the category registry.""" self.hass = hass self.categories: dict[str, dict[str, CategoryEntry]] = {} - self._store = hass.helpers.storage.Store( + self._store: Store[dict[str, dict[str, list[dict[str, str]]]]] = Store( + hass, STORAGE_VERSION_MAJOR, STORAGE_KEY, atomic_writes=True, From d23b22b566578610c69221b42a022785041e3a16 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Sat, 30 Mar 2024 15:59:20 -0500 Subject: [PATCH 098/967] Add initial support for floors to intents (#114456) * Add initial support for floors to intents * Fix climate intent * More tests * No return value * Add requested changes * Reuse event handler --- homeassistant/components/climate/intent.py | 2 + .../components/conversation/default_agent.py | 46 +++++++- .../components/conversation/manifest.json | 2 +- homeassistant/helpers/intent.py | 110 ++++++++++++++---- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../conversation/test_default_agent.py | 73 +++++++++++- .../test_default_agent_intents.py | 105 ++++++++++++++++- tests/helpers/test_intent.py | 76 +++++++++++- 10 files changed, 384 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/climate/intent.py b/homeassistant/components/climate/intent.py index db263451f0b..3073d3e3c26 100644 --- a/homeassistant/components/climate/intent.py +++ b/homeassistant/components/climate/intent.py @@ -58,6 +58,7 @@ class GetTemperatureIntent(intent.IntentHandler): raise intent.NoStatesMatchedError( name=entity_text or entity_name, area=area_name or area_id, + floor=None, domains={DOMAIN}, device_classes=None, ) @@ -75,6 +76,7 @@ class GetTemperatureIntent(intent.IntentHandler): raise intent.NoStatesMatchedError( name=entity_name, area=None, + floor=None, domains={DOMAIN}, device_classes=None, ) diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 96b0565ebd3..c0307c68908 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -34,6 +34,7 @@ from homeassistant.helpers import ( area_registry as ar, device_registry as dr, entity_registry as er, + floor_registry as fr, intent, start, template, @@ -163,7 +164,12 @@ class DefaultAgent(AbstractConversationAgent): self.hass.bus.async_listen( ar.EVENT_AREA_REGISTRY_UPDATED, - self._async_handle_area_registry_changed, + self._async_handle_area_floor_registry_changed, + run_immediately=True, + ) + self.hass.bus.async_listen( + fr.EVENT_FLOOR_REGISTRY_UPDATED, + self._async_handle_area_floor_registry_changed, run_immediately=True, ) self.hass.bus.async_listen( @@ -696,10 +702,13 @@ class DefaultAgent(AbstractConversationAgent): return lang_intents @core.callback - def _async_handle_area_registry_changed( - self, event: core.Event[ar.EventAreaRegistryUpdatedData] + def _async_handle_area_floor_registry_changed( + self, + event: core.Event[ + ar.EventAreaRegistryUpdatedData | fr.EventFloorRegistryUpdatedData + ], ) -> None: - """Clear area area cache when the area registry has changed.""" + """Clear area/floor list cache when the area registry has changed.""" self._slot_lists = None @core.callback @@ -773,6 +782,8 @@ class DefaultAgent(AbstractConversationAgent): # Default name entity_names.append((state.name, state.name, context)) + _LOGGER.debug("Exposed entities: %s", entity_names) + # Expose all areas. # # We pass in area id here with the expectation that no two areas will @@ -788,11 +799,25 @@ class DefaultAgent(AbstractConversationAgent): area_names.append((alias, area.id)) - _LOGGER.debug("Exposed entities: %s", entity_names) + # Expose all floors. + # + # We pass in floor id here with the expectation that no two floors will + # share the same name or alias. + floors = fr.async_get(self.hass) + floor_names = [] + for floor in floors.async_list_floors(): + floor_names.append((floor.name, floor.floor_id)) + if floor.aliases: + for alias in floor.aliases: + if not alias.strip(): + continue + + floor_names.append((alias, floor.floor_id)) self._slot_lists = { "area": TextSlotList.from_tuples(area_names, allow_template=False), "name": TextSlotList.from_tuples(entity_names, allow_template=False), + "floor": TextSlotList.from_tuples(floor_names, allow_template=False), } return self._slot_lists @@ -953,6 +978,10 @@ def _get_unmatched_response(result: RecognizeResult) -> tuple[ErrorKey, dict[str # area only return ErrorKey.NO_AREA, {"area": unmatched_area} + if unmatched_floor := unmatched_text.get("floor"): + # floor only + return ErrorKey.NO_FLOOR, {"floor": unmatched_floor} + # Area may still have matched matched_area: str | None = None if matched_area_entity := result.entities.get("area"): @@ -1000,6 +1029,13 @@ def _get_no_states_matched_response( "area": no_states_error.area, } + if no_states_error.floor: + # domain in floor + return ErrorKey.NO_DOMAIN_IN_FLOOR, { + "domain": domain, + "floor": no_states_error.floor, + } + # domain only return ErrorKey.NO_DOMAIN, {"domain": domain} diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 7f3c4f5894e..7f463483bf9 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -7,5 +7,5 @@ "integration_type": "system", "iot_class": "local_push", "quality_scale": "internal", - "requirements": ["hassil==1.6.1", "home-assistant-intents==2024.3.27"] + "requirements": ["hassil==1.6.1", "home-assistant-intents==2024.3.29"] } diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index 0a9b441d99b..5fc80bedbed 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -24,7 +24,13 @@ from homeassistant.core import Context, HomeAssistant, State, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import bind_hass -from . import area_registry, config_validation as cv, device_registry, entity_registry +from . import ( + area_registry, + config_validation as cv, + device_registry, + entity_registry, + floor_registry, +) _LOGGER = logging.getLogger(__name__) _SlotsType = dict[str, Any] @@ -144,16 +150,18 @@ class NoStatesMatchedError(IntentError): def __init__( self, - name: str | None, - area: str | None, - domains: set[str] | None, - device_classes: set[str] | None, + name: str | None = None, + area: str | None = None, + floor: str | None = None, + domains: set[str] | None = None, + device_classes: set[str] | None = None, ) -> None: """Initialize error.""" super().__init__() self.name = name self.area = area + self.floor = floor self.domains = domains self.device_classes = device_classes @@ -220,12 +228,35 @@ def _find_area( return None -def _filter_by_area( +def _find_floor( + id_or_name: str, floors: floor_registry.FloorRegistry +) -> floor_registry.FloorEntry | None: + """Find an floor by id or name, checking aliases too.""" + floor = floors.async_get_floor(id_or_name) or floors.async_get_floor_by_name( + id_or_name + ) + if floor is not None: + return floor + + # Check floor aliases + for maybe_floor in floors.floors.values(): + if not maybe_floor.aliases: + continue + + for floor_alias in maybe_floor.aliases: + if id_or_name == floor_alias.casefold(): + return maybe_floor + + return None + + +def _filter_by_areas( states_and_entities: list[tuple[State, entity_registry.RegistryEntry | None]], - area: area_registry.AreaEntry, + areas: Iterable[area_registry.AreaEntry], devices: device_registry.DeviceRegistry, ) -> Iterable[tuple[State, entity_registry.RegistryEntry | None]]: """Filter state/entity pairs by an area.""" + filter_area_ids: set[str | None] = {a.id for a in areas} entity_area_ids: dict[str, str | None] = {} for _state, entity in states_and_entities: if entity is None: @@ -241,7 +272,7 @@ def _filter_by_area( entity_area_ids[entity.id] = device.area_id for state, entity in states_and_entities: - if (entity is not None) and (entity_area_ids.get(entity.id) == area.id): + if (entity is not None) and (entity_area_ids.get(entity.id) in filter_area_ids): yield (state, entity) @@ -252,11 +283,14 @@ def async_match_states( name: str | None = None, area_name: str | None = None, area: area_registry.AreaEntry | None = None, + floor_name: str | None = None, + floor: floor_registry.FloorEntry | None = None, domains: Collection[str] | None = None, device_classes: Collection[str] | None = None, states: Iterable[State] | None = None, entities: entity_registry.EntityRegistry | None = None, areas: area_registry.AreaRegistry | None = None, + floors: floor_registry.FloorRegistry | None = None, devices: device_registry.DeviceRegistry | None = None, assistant: str | None = None, ) -> Iterable[State]: @@ -268,6 +302,15 @@ def async_match_states( if entities is None: entities = entity_registry.async_get(hass) + if devices is None: + devices = device_registry.async_get(hass) + + if areas is None: + areas = area_registry.async_get(hass) + + if floors is None: + floors = floor_registry.async_get(hass) + # Gather entities states_and_entities: list[tuple[State, entity_registry.RegistryEntry | None]] = [] for state in states: @@ -294,20 +337,35 @@ def async_match_states( if _is_device_class(state, entity, device_classes) ] + filter_areas: list[area_registry.AreaEntry] = [] + + if (floor is None) and (floor_name is not None): + # Look up floor by name + floor = _find_floor(floor_name, floors) + if floor is None: + _LOGGER.warning("Floor not found: %s", floor_name) + return + + if floor is not None: + filter_areas = [ + a for a in areas.async_list_areas() if a.floor_id == floor.floor_id + ] + if (area is None) and (area_name is not None): # Look up area by name - if areas is None: - areas = area_registry.async_get(hass) - area = _find_area(area_name, areas) - assert area is not None, f"No area named {area_name}" + if area is None: + _LOGGER.warning("Area not found: %s", area_name) + return if area is not None: - # Filter by states/entities by area - if devices is None: - devices = device_registry.async_get(hass) + filter_areas = [area] - states_and_entities = list(_filter_by_area(states_and_entities, area, devices)) + if filter_areas: + # Filter by states/entities by area + states_and_entities = list( + _filter_by_areas(states_and_entities, filter_areas, devices) + ) if assistant is not None: # Filter by exposure @@ -318,9 +376,6 @@ def async_match_states( ] if name is not None: - if devices is None: - devices = device_registry.async_get(hass) - # Filter by name name = name.casefold() @@ -389,7 +444,7 @@ class DynamicServiceIntentHandler(IntentHandler): """ slot_schema = { - vol.Any("name", "area"): cv.string, + vol.Any("name", "area", "floor"): cv.string, vol.Optional("domain"): vol.All(cv.ensure_list, [cv.string]), vol.Optional("device_class"): vol.All(cv.ensure_list, [cv.string]), } @@ -453,7 +508,7 @@ class DynamicServiceIntentHandler(IntentHandler): # Don't match on name if targeting all entities entity_name = None - # Look up area first to fail early + # Look up area to fail early area_slot = slots.get("area", {}) area_id = area_slot.get("value") area_name = area_slot.get("text") @@ -464,6 +519,17 @@ class DynamicServiceIntentHandler(IntentHandler): if area is None: raise IntentHandleError(f"No area named {area_name}") + # Look up floor to fail early + floor_slot = slots.get("floor", {}) + floor_id = floor_slot.get("value") + floor_name = floor_slot.get("text") + floor: floor_registry.FloorEntry | None = None + if floor_id is not None: + floors = floor_registry.async_get(hass) + floor = floors.async_get_floor(floor_id) + if floor is None: + raise IntentHandleError(f"No floor named {floor_name}") + # Optional domain/device class filters. # Convert to sets for speed. domains: set[str] | None = None @@ -480,6 +546,7 @@ class DynamicServiceIntentHandler(IntentHandler): hass, name=entity_name, area=area, + floor=floor, domains=domains, device_classes=device_classes, assistant=intent_obj.assistant, @@ -491,6 +558,7 @@ class DynamicServiceIntentHandler(IntentHandler): raise NoStatesMatchedError( name=entity_text or entity_name, area=area_name or area_id, + floor=floor_name or floor_id, domains=domains, device_classes=device_classes, ) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b2a8898bf47..72cd71f889f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -31,7 +31,7 @@ hass-nabucasa==0.79.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 home-assistant-frontend==20240329.1 -home-assistant-intents==2024.3.27 +home-assistant-intents==2024.3.29 httpx==0.27.0 ifaddr==0.2.0 Jinja2==3.1.3 diff --git a/requirements_all.txt b/requirements_all.txt index f928cf8333a..d3cb743c8c5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1084,7 +1084,7 @@ holidays==0.45 home-assistant-frontend==20240329.1 # homeassistant.components.conversation -home-assistant-intents==2024.3.27 +home-assistant-intents==2024.3.29 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aabaf488caa..a5cf2efa79c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -883,7 +883,7 @@ holidays==0.45 home-assistant-frontend==20240329.1 # homeassistant.components.conversation -home-assistant-intents==2024.3.27 +home-assistant-intents==2024.3.29 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/tests/components/conversation/test_default_agent.py b/tests/components/conversation/test_default_agent.py index aefb37f427e..8f38459a8da 100644 --- a/tests/components/conversation/test_default_agent.py +++ b/tests/components/conversation/test_default_agent.py @@ -17,6 +17,7 @@ from homeassistant.helpers import ( device_registry as dr, entity, entity_registry as er, + floor_registry as fr, intent, ) from homeassistant.setup import async_setup_component @@ -480,6 +481,20 @@ async def test_error_no_area(hass: HomeAssistant, init_components) -> None: ) +async def test_error_no_floor(hass: HomeAssistant, init_components) -> 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 + ) + + assert result.response.response_type == intent.IntentResponseType.ERROR + assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS + assert ( + result.response.speech["plain"]["speech"] + == "Sorry, I am not aware of any floor called missing" + ) + + async def test_error_no_device_in_area( hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry ) -> None: @@ -549,6 +564,48 @@ async def test_error_no_domain_in_area( ) +async def test_error_no_domain_in_floor( + hass: HomeAssistant, + init_components, + area_registry: ar.AreaRegistry, + floor_registry: fr.FloorRegistry, +) -> None: + """Test error message when no devices/entities for a domain exist on a floor.""" + floor_ground = floor_registry.async_create("ground") + area_kitchen = area_registry.async_get_or_create("kitchen_id") + area_kitchen = area_registry.async_update( + area_kitchen.id, name="kitchen", floor_id=floor_ground.floor_id + ) + result = await conversation.async_converse( + hass, "turn on all lights on the ground floor", None, Context(), None + ) + + assert result.response.response_type == intent.IntentResponseType.ERROR + assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS + assert ( + result.response.speech["plain"]["speech"] + == "Sorry, I am not aware of any light on the ground floor" + ) + + # Add a new floor/area to trigger registry event handlers + floor_upstairs = floor_registry.async_create("upstairs") + area_bedroom = area_registry.async_get_or_create("bedroom_id") + area_bedroom = area_registry.async_update( + area_bedroom.id, name="bedroom", floor_id=floor_upstairs.floor_id + ) + + result = await conversation.async_converse( + hass, "turn on all lights upstairs", None, Context(), None + ) + + assert result.response.response_type == intent.IntentResponseType.ERROR + assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS + assert ( + result.response.speech["plain"]["speech"] + == "Sorry, I am not aware of any light on the upstairs floor" + ) + + async def test_error_no_device_class(hass: HomeAssistant, init_components) -> None: """Test error message when no entities of a device class exist.""" @@ -736,7 +793,7 @@ async def test_no_states_matched_default_error( with patch( "homeassistant.components.conversation.default_agent.intent.async_handle", - side_effect=intent.NoStatesMatchedError(None, None, None, None), + side_effect=intent.NoStatesMatchedError(), ): result = await conversation.async_converse( hass, "turn on lights in the kitchen", None, Context(), None @@ -759,11 +816,16 @@ async def test_empty_aliases( area_registry: ar.AreaRegistry, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, + floor_registry: fr.FloorRegistry, ) -> None: """Test that empty aliases are not added to slot lists.""" + floor_1 = floor_registry.async_create("first floor", aliases={" "}) + area_kitchen = area_registry.async_get_or_create("kitchen_id") area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen") - area_kitchen = area_registry.async_update(area_kitchen.id, aliases={" "}) + area_kitchen = area_registry.async_update( + area_kitchen.id, aliases={" "}, floor_id=floor_1 + ) entry = MockConfigEntry() entry.add_to_hass(hass) @@ -799,7 +861,7 @@ async def test_empty_aliases( slot_lists = mock_recognize_all.call_args[0][2] # Slot lists should only contain non-empty text - assert slot_lists.keys() == {"area", "name"} + assert slot_lists.keys() == {"area", "name", "floor"} areas = slot_lists["area"] assert len(areas.values) == 1 assert areas.values[0].value_out == area_kitchen.id @@ -810,6 +872,11 @@ async def test_empty_aliases( assert names.values[0].value_out == kitchen_light.name assert names.values[0].text_in.text == kitchen_light.name + floors = slot_lists["floor"] + assert len(floors.values) == 1 + assert floors.values[0].value_out == floor_1.floor_id + assert floors.values[0].text_in.text == floor_1.name + async def test_all_domains_loaded(hass: HomeAssistant, init_components) -> None: """Test that sentences for all domains are always loaded.""" diff --git a/tests/components/conversation/test_default_agent_intents.py b/tests/components/conversation/test_default_agent_intents.py index c57d93d8cef..9636ac07f63 100644 --- a/tests/components/conversation/test_default_agent_intents.py +++ b/tests/components/conversation/test_default_agent_intents.py @@ -2,14 +2,26 @@ import pytest -from homeassistant.components import conversation, cover, media_player, vacuum, valve +from homeassistant.components import ( + conversation, + cover, + light, + media_player, + vacuum, + valve, +) from homeassistant.components.cover import intent as cover_intent from homeassistant.components.homeassistant.exposed_entities import async_expose_entity from homeassistant.components.media_player import intent as media_player_intent from homeassistant.components.vacuum import intent as vaccum_intent from homeassistant.const import STATE_CLOSED from homeassistant.core import Context, HomeAssistant -from homeassistant.helpers import intent +from homeassistant.helpers import ( + area_registry as ar, + entity_registry as er, + floor_registry as fr, + intent, +) from homeassistant.setup import async_setup_component from tests.common import async_mock_service @@ -244,3 +256,92 @@ async def test_media_player_intents( "entity_id": entity_id, media_player.ATTR_MEDIA_VOLUME_LEVEL: 0.75, } + + +async def test_turn_floor_lights_on_off( + hass: HomeAssistant, + init_components, + entity_registry: er.EntityRegistry, + area_registry: ar.AreaRegistry, + floor_registry: fr.FloorRegistry, +) -> None: + """Test that we can turn lights on/off for an entire floor.""" + floor_ground = floor_registry.async_create("ground", aliases={"downstairs"}) + floor_upstairs = floor_registry.async_create("upstairs") + + # Kitchen and living room are on the ground floor + area_kitchen = area_registry.async_get_or_create("kitchen_id") + area_kitchen = area_registry.async_update( + area_kitchen.id, name="kitchen", floor_id=floor_ground.floor_id + ) + + area_living_room = area_registry.async_get_or_create("living_room_id") + area_living_room = area_registry.async_update( + area_living_room.id, name="living_room", floor_id=floor_ground.floor_id + ) + + # Bedroom is upstairs + area_bedroom = area_registry.async_get_or_create("bedroom_id") + area_bedroom = area_registry.async_update( + area_bedroom.id, name="bedroom", floor_id=floor_upstairs.floor_id + ) + + # One light per area + kitchen_light = entity_registry.async_get_or_create( + "light", "demo", "kitchen_light" + ) + kitchen_light = entity_registry.async_update_entity( + kitchen_light.entity_id, area_id=area_kitchen.id + ) + hass.states.async_set(kitchen_light.entity_id, "off") + + living_room_light = entity_registry.async_get_or_create( + "light", "demo", "living_room_light" + ) + living_room_light = entity_registry.async_update_entity( + living_room_light.entity_id, area_id=area_living_room.id + ) + hass.states.async_set(living_room_light.entity_id, "off") + + bedroom_light = entity_registry.async_get_or_create( + "light", "demo", "bedroom_light" + ) + bedroom_light = entity_registry.async_update_entity( + bedroom_light.entity_id, area_id=area_bedroom.id + ) + hass.states.async_set(bedroom_light.entity_id, "off") + + # Target by floor + on_calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON) + result = await conversation.async_converse( + hass, "turn on all lights downstairs", None, Context(), None + ) + + assert len(on_calls) == 2 + assert result.response.response_type == intent.IntentResponseType.ACTION_DONE + assert {s.entity_id for s in result.response.matched_states} == { + kitchen_light.entity_id, + living_room_light.entity_id, + } + + on_calls.clear() + result = await conversation.async_converse( + hass, "upstairs lights on", None, Context(), None + ) + + assert len(on_calls) == 1 + assert result.response.response_type == intent.IntentResponseType.ACTION_DONE + assert {s.entity_id for s in result.response.matched_states} == { + bedroom_light.entity_id + } + + off_calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_OFF) + result = await conversation.async_converse( + hass, "turn upstairs lights off", None, Context(), None + ) + + assert len(off_calls) == 1 + assert result.response.response_type == intent.IntentResponseType.ACTION_DONE + assert {s.entity_id for s in result.response.matched_states} == { + bedroom_light.entity_id + } diff --git a/tests/helpers/test_intent.py b/tests/helpers/test_intent.py index 1bc01c28cf2..d77eb698205 100644 --- a/tests/helpers/test_intent.py +++ b/tests/helpers/test_intent.py @@ -15,6 +15,7 @@ from homeassistant.helpers import ( config_validation as cv, device_registry as dr, entity_registry as er, + floor_registry as fr, intent, ) from homeassistant.setup import async_setup_component @@ -34,12 +35,25 @@ async def test_async_match_states( hass: HomeAssistant, area_registry: ar.AreaRegistry, entity_registry: er.EntityRegistry, + floor_registry: fr.FloorRegistry, ) -> None: """Test async_match_state helper.""" area_kitchen = area_registry.async_get_or_create("kitchen") - area_registry.async_update(area_kitchen.id, aliases={"food room"}) + area_kitchen = area_registry.async_update(area_kitchen.id, aliases={"food room"}) area_bedroom = area_registry.async_get_or_create("bedroom") + # Kitchen is on the first floor + floor_1 = floor_registry.async_create("first floor", aliases={"ground floor"}) + area_kitchen = area_registry.async_update( + area_kitchen.id, floor_id=floor_1.floor_id + ) + + # Bedroom is on the second floor + floor_2 = floor_registry.async_create("second floor") + area_bedroom = area_registry.async_update( + area_bedroom.id, floor_id=floor_2.floor_id + ) + state1 = State( "light.kitchen", "on", attributes={ATTR_FRIENDLY_NAME: "kitchen light"} ) @@ -94,6 +108,13 @@ async def test_async_match_states( ) ) + # Invalid area + assert not list( + intent.async_match_states( + hass, area_name="invalid area", states=[state1, state2] + ) + ) + # Domain + area assert list( intent.async_match_states( @@ -111,6 +132,35 @@ async def test_async_match_states( ) ) == [state2] + # Floor + assert list( + intent.async_match_states( + hass, floor_name="first floor", states=[state1, state2] + ) + ) == [state1] + + assert list( + intent.async_match_states( + # Check alias + hass, + floor_name="ground floor", + states=[state1, state2], + ) + ) == [state1] + + assert list( + intent.async_match_states( + hass, floor_name="second floor", states=[state1, state2] + ) + ) == [state2] + + # Invalid floor + assert not list( + intent.async_match_states( + hass, floor_name="invalid floor", states=[state1, state2] + ) + ) + async def test_match_device_area( hass: HomeAssistant, @@ -300,3 +350,27 @@ async def test_validate_then_run_in_background(hass: HomeAssistant) -> None: assert len(calls) == 1 assert calls[0].data == {"entity_id": "light.kitchen"} + + +async def test_invalid_area_floor_names(hass: HomeAssistant) -> None: + """Test that we throw an intent handle error with invalid area/floor names.""" + handler = intent.ServiceIntentHandler( + "TestType", "light", "turn_on", "Turned {} on" + ) + intent.async_register(hass, handler) + + with pytest.raises(intent.IntentHandleError): + await intent.async_handle( + hass, + "test", + "TestType", + slots={"area": {"value": "invalid area"}}, + ) + + with pytest.raises(intent.IntentHandleError): + await intent.async_handle( + hass, + "test", + "TestType", + slots={"floor": {"value": "invalid floor"}}, + ) From b0a1450a2b654d338d6fee7033b675b73275cb1a Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Sun, 31 Mar 2024 00:17:09 +0100 Subject: [PATCH 099/967] Fix ZHA websocket API test (#114495) * Fix result overwriting expected config * Use `BASE_CUSTOM_CONFIGURATION` for initial/expected config --- tests/components/zha/test_websocket_api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/zha/test_websocket_api.py b/tests/components/zha/test_websocket_api.py index 9cd475a7bf8..623d7acf602 100644 --- a/tests/components/zha/test_websocket_api.py +++ b/tests/components/zha/test_websocket_api.py @@ -303,7 +303,7 @@ async def test_update_zha_config( app_controller: ControllerApplication, ) -> None: """Test updating ZHA custom configuration.""" - configuration: dict = deepcopy(CONFIG_WITH_ALARM_OPTIONS) + configuration: dict = deepcopy(BASE_CUSTOM_CONFIGURATION) configuration["data"]["zha_options"]["default_light_transition"] = 10 with patch( @@ -318,8 +318,8 @@ async def test_update_zha_config( await zha_client.send_json({ID: 6, TYPE: "zha/configuration"}) msg = await zha_client.receive_json() - configuration = msg["result"] - assert configuration == configuration + test_configuration = msg["result"] + assert test_configuration == configuration await hass.config_entries.async_unload(config_entry.entry_id) From f0b07ae942317aea31e24741d4c211ac065d3eaf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 30 Mar 2024 17:35:49 -1000 Subject: [PATCH 100/967] Add pytest rewrite for wemo tests that use entity_test_helpers (#114516) --- tests/components/wemo/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/components/wemo/__init__.py b/tests/components/wemo/__init__.py index 33bdcacd37d..68d1516aed6 100644 --- a/tests/components/wemo/__init__.py +++ b/tests/components/wemo/__init__.py @@ -1 +1,5 @@ """Tests for the wemo component.""" + +import pytest + +pytest.register_assert_rewrite("tests.components.wemo.entity_test_helpers") From fb572b84137ccb1879b2b190a4e087be05c70c38 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 30 Mar 2024 23:47:13 -0400 Subject: [PATCH 101/967] Conversation to unsubscribe when no cache to invalidate (#114515) * Add event filter callbacks to conversation * Unsusbcribe when we have no slot lists --- .../components/conversation/default_agent.py | 105 +++++++++--------- 1 file changed, 53 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index c0307c68908..29a06d44c5f 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -147,6 +147,7 @@ class DefaultAgent(AbstractConversationAgent): # Sentences that will trigger a callback (skipping intent recognition) self._trigger_sentences: list[TriggerData] = [] self._trigger_intents: Intents | None = None + self._unsub_clear_slot_list: list[Callable[[], None]] | None = None @property def supported_languages(self) -> list[str]: @@ -162,30 +163,49 @@ class DefaultAgent(AbstractConversationAgent): if config_intents: self._config_intents = config_intents - self.hass.bus.async_listen( - ar.EVENT_AREA_REGISTRY_UPDATED, - self._async_handle_area_floor_registry_changed, - run_immediately=True, - ) - self.hass.bus.async_listen( - fr.EVENT_FLOOR_REGISTRY_UPDATED, - self._async_handle_area_floor_registry_changed, - run_immediately=True, - ) - self.hass.bus.async_listen( - er.EVENT_ENTITY_REGISTRY_UPDATED, - self._async_handle_entity_registry_changed, - run_immediately=True, - ) - self.hass.bus.async_listen( - EVENT_STATE_CHANGED, - self._async_handle_state_changed, - run_immediately=True, - ) - async_listen_entity_updates( - self.hass, DOMAIN, self._async_exposed_entities_updated + @core.callback + def _filter_entity_registry_changes(self, event_data: dict[str, Any]) -> bool: + """Filter entity registry changed events.""" + return event_data["action"] == "update" and any( + field in event_data["changes"] for field in _ENTITY_REGISTRY_UPDATE_FIELDS ) + @core.callback + def _filter_state_changes(self, event_data: dict[str, Any]) -> bool: + """Filter state changed events.""" + return not event_data["old_state"] or not event_data["new_state"] + + @core.callback + def _listen_clear_slot_list(self) -> None: + """Listen for changes that can invalidate slot list.""" + assert self._unsub_clear_slot_list is None + + self._unsub_clear_slot_list = [ + self.hass.bus.async_listen( + ar.EVENT_AREA_REGISTRY_UPDATED, + self._async_clear_slot_list, + run_immediately=True, + ), + self.hass.bus.async_listen( + fr.EVENT_FLOOR_REGISTRY_UPDATED, + self._async_clear_slot_list, + run_immediately=True, + ), + self.hass.bus.async_listen( + er.EVENT_ENTITY_REGISTRY_UPDATED, + self._async_clear_slot_list, + event_filter=self._filter_entity_registry_changes, + run_immediately=True, + ), + self.hass.bus.async_listen( + EVENT_STATE_CHANGED, + self._async_clear_slot_list, + event_filter=self._filter_state_changes, + run_immediately=True, + ), + async_listen_entity_updates(self.hass, DOMAIN, self._async_clear_slot_list), + ] + async def async_recognize( self, user_input: ConversationInput ) -> RecognizeResult | SentenceTriggerResult | None: @@ -543,6 +563,9 @@ class DefaultAgent(AbstractConversationAgent): if lang_intents is None: # No intents loaded _LOGGER.warning("No intents were loaded for language: %s", language) + return + + self._make_slot_lists() async def async_get_or_load_intents(self, language: str) -> LanguageIntents | None: """Load all intents of a language with lock.""" @@ -702,40 +725,17 @@ class DefaultAgent(AbstractConversationAgent): return lang_intents @core.callback - def _async_handle_area_floor_registry_changed( - self, - event: core.Event[ - ar.EventAreaRegistryUpdatedData | fr.EventFloorRegistryUpdatedData - ], + def _async_clear_slot_list( + self, event: core.Event[dict[str, Any]] | None = None ) -> None: - """Clear area/floor list cache when the area registry has changed.""" + """Clear slot lists when a registry has changed.""" self._slot_lists = None + assert self._unsub_clear_slot_list is not None + for unsub in self._unsub_clear_slot_list: + unsub() + self._unsub_clear_slot_list = None @core.callback - def _async_handle_entity_registry_changed( - self, event: core.Event[er.EventEntityRegistryUpdatedData] - ) -> None: - """Clear names list cache when an entity registry entry has changed.""" - if event.data["action"] != "update" or not any( - field in event.data["changes"] for field in _ENTITY_REGISTRY_UPDATE_FIELDS - ): - return - self._slot_lists = None - - @core.callback - def _async_handle_state_changed( - self, event: core.Event[EventStateChangedData] - ) -> None: - """Clear names list cache when a state is added or removed from the state machine.""" - if event.data["old_state"] and event.data["new_state"]: - return - self._slot_lists = None - - @core.callback - def _async_exposed_entities_updated(self) -> None: - """Handle updated preferences.""" - self._slot_lists = None - def _make_slot_lists(self) -> dict[str, SlotList]: """Create slot lists with areas and entity names/aliases.""" if self._slot_lists is not None: @@ -820,6 +820,7 @@ class DefaultAgent(AbstractConversationAgent): "floor": TextSlotList.from_tuples(floor_names, allow_template=False), } + self._listen_clear_slot_list() return self._slot_lists def _make_intent_context( From f01235ef746823a8f146c603bf732df6a6061706 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 31 Mar 2024 00:05:25 -0400 Subject: [PATCH 102/967] Re-organize conversation integration (#114502) * Re-organize conversation integration * Clean up 2 more imports * Re-export models * Fix imports * Uno mas * Rename agents to models * Fix cast test that i broke? * Just blocking till I'm done * Wrong place --- .../components/conversation/__init__.py | 533 ++---------------- .../components/conversation/agent_manager.py | 161 ++++++ .../components/conversation/const.py | 1 + .../components/conversation/default_agent.py | 2 +- homeassistant/components/conversation/http.py | 325 +++++++++++ .../conversation/{agent.py => models.py} | 0 .../components/conversation/trigger.py | 6 +- tests/components/cast/test_media_player.py | 2 + tests/components/conversation/__init__.py | 15 +- .../conversation/test_default_agent.py | 9 +- tests/components/conversation/test_init.py | 20 +- tests/components/conversation/test_trigger.py | 9 +- .../google_assistant_sdk/test_init.py | 2 +- .../test_init.py | 2 +- tests/components/mobile_app/test_webhook.py | 2 +- tests/components/ollama/test_init.py | 6 +- .../openai_conversation/test_init.py | 2 +- 17 files changed, 579 insertions(+), 518 deletions(-) create mode 100644 homeassistant/components/conversation/agent_manager.py create mode 100644 homeassistant/components/conversation/http.py rename homeassistant/components/conversation/{agent.py => models.py} (100%) diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index dd8fb967824..a0717ddaa58 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -2,43 +2,36 @@ from __future__ import annotations -import asyncio from collections.abc import Iterable -from dataclasses import dataclass import logging import re -from typing import Any, Literal +from typing import Literal -from aiohttp import web -from hassil.recognize import ( - MISSING_ENTITY, - RecognizeResult, - UnmatchedRangeEntity, - UnmatchedTextEntity, -) import voluptuous as vol -from homeassistant import core -from homeassistant.components import http, websocket_api -from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.config_entries import ConfigEntry from homeassistant.const import MATCH_ALL -from homeassistant.core import HomeAssistant +from homeassistant.core import ( + HomeAssistant, + ServiceCall, + ServiceResponse, + SupportsResponse, + callback, +) from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_validation as cv, intent, singleton +from homeassistant.helpers import config_validation as cv, intent from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass -from homeassistant.util import language as language_util -from .agent import AbstractConversationAgent, ConversationInput, ConversationResult -from .const import HOME_ASSISTANT_AGENT -from .default_agent import ( - METADATA_CUSTOM_FILE, - METADATA_CUSTOM_SENTENCE, - DefaultAgent, - SentenceTriggerResult, - async_setup as async_setup_default_agent, +from .agent_manager import ( + AgentInfo, + agent_id_validator, + async_converse, + get_agent_manager, ) +from .const import DATA_CONFIG, HOME_ASSISTANT_AGENT +from .http import async_setup as async_setup_conversation_http +from .models import AbstractConversationAgent, ConversationInput, ConversationResult __all__ = [ "DOMAIN", @@ -48,6 +41,8 @@ __all__ = [ "async_set_agent", "async_unset_agent", "async_setup", + "ConversationInput", + "ConversationResult", ] _LOGGER = logging.getLogger(__name__) @@ -60,21 +55,11 @@ ATTR_CONVERSATION_ID = "conversation_id" DOMAIN = "conversation" REGEX_TYPE = type(re.compile("")) -DATA_CONFIG = "conversation_config" SERVICE_PROCESS = "process" SERVICE_RELOAD = "reload" -def agent_id_validator(value: Any) -> str: - """Validate agent ID.""" - hass = core.async_get_hass() - manager = _get_agent_manager(hass) - if not manager.async_is_valid_agent_id(cv.string(value)): - raise vol.Invalid("invalid agent ID") - return value - - SERVICE_PROCESS_SCHEMA = vol.Schema( { vol.Required(ATTR_TEXT): cv.string, @@ -106,34 +91,25 @@ CONFIG_SCHEMA = vol.Schema( ) -@singleton.singleton("conversation_agent") -@core.callback -def _get_agent_manager(hass: HomeAssistant) -> AgentManager: - """Get the active agent.""" - manager = AgentManager(hass) - manager.async_setup() - return manager - - -@core.callback +@callback @bind_hass def async_set_agent( - hass: core.HomeAssistant, + hass: HomeAssistant, config_entry: ConfigEntry, agent: AbstractConversationAgent, ) -> None: """Set the agent to handle the conversations.""" - _get_agent_manager(hass).async_set_agent(config_entry.entry_id, agent) + get_agent_manager(hass).async_set_agent(config_entry.entry_id, agent) -@core.callback +@callback @bind_hass def async_unset_agent( - hass: core.HomeAssistant, + hass: HomeAssistant, config_entry: ConfigEntry, ) -> None: """Set the agent to handle the conversations.""" - _get_agent_manager(hass).async_unset_agent(config_entry.entry_id) + get_agent_manager(hass).async_unset_agent(config_entry.entry_id) async def async_get_conversation_languages( @@ -145,7 +121,7 @@ async def async_get_conversation_languages( If no agent is specified, return a set with the union of languages supported by all conversation agents. """ - agent_manager = _get_agent_manager(hass) + agent_manager = get_agent_manager(hass) languages: set[str] = set() agent_ids: Iterable[str] @@ -164,14 +140,32 @@ async def async_get_conversation_languages( return languages +@callback +def async_get_agent_info( + hass: HomeAssistant, + agent_id: str | None = None, +) -> AgentInfo | None: + """Get information on the agent or None if not found.""" + manager = get_agent_manager(hass) + + if agent_id is None: + agent_id = manager.default_agent + + for agent_info in manager.async_get_agent_info(): + if agent_info.id == agent_id: + return agent_info + + return None + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Register the process service.""" - agent_manager = _get_agent_manager(hass) + agent_manager = get_agent_manager(hass) if config_intents := config.get(DOMAIN, {}).get("intents"): hass.data[DATA_CONFIG] = config_intents - async def handle_process(service: core.ServiceCall) -> core.ServiceResponse: + async def handle_process(service: ServiceCall) -> ServiceResponse: """Parse text into commands.""" text = service.data[ATTR_TEXT] _LOGGER.debug("Processing: <%s>", text) @@ -192,7 +186,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return None - async def handle_reload(service: core.ServiceCall) -> None: + async def handle_reload(service: ServiceCall) -> None: """Reload intents.""" agent = await agent_manager.async_get_agent() await agent.async_reload(language=service.data.get(ATTR_LANGUAGE)) @@ -202,440 +196,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: SERVICE_PROCESS, handle_process, schema=SERVICE_PROCESS_SCHEMA, - supports_response=core.SupportsResponse.OPTIONAL, + supports_response=SupportsResponse.OPTIONAL, ) hass.services.async_register( DOMAIN, SERVICE_RELOAD, handle_reload, schema=SERVICE_RELOAD_SCHEMA ) - hass.http.register_view(ConversationProcessView()) - websocket_api.async_register_command(hass, websocket_process) - websocket_api.async_register_command(hass, websocket_prepare) - websocket_api.async_register_command(hass, websocket_list_agents) - websocket_api.async_register_command(hass, websocket_hass_agent_debug) + async_setup_conversation_http(hass) return True - - -@websocket_api.websocket_command( - { - vol.Required("type"): "conversation/process", - vol.Required("text"): str, - vol.Optional("conversation_id"): vol.Any(str, None), - vol.Optional("language"): str, - vol.Optional("agent_id"): agent_id_validator, - } -) -@websocket_api.async_response -async def websocket_process( - hass: HomeAssistant, - connection: websocket_api.ActiveConnection, - msg: dict[str, Any], -) -> None: - """Process text.""" - result = await async_converse( - hass=hass, - text=msg["text"], - conversation_id=msg.get("conversation_id"), - context=connection.context(msg), - language=msg.get("language"), - agent_id=msg.get("agent_id"), - ) - connection.send_result(msg["id"], result.as_dict()) - - -@websocket_api.websocket_command( - { - "type": "conversation/prepare", - vol.Optional("language"): str, - vol.Optional("agent_id"): agent_id_validator, - } -) -@websocket_api.async_response -async def websocket_prepare( - hass: HomeAssistant, - connection: websocket_api.ActiveConnection, - msg: dict[str, Any], -) -> None: - """Reload intents.""" - manager = _get_agent_manager(hass) - agent = await manager.async_get_agent(msg.get("agent_id")) - await agent.async_prepare(msg.get("language")) - connection.send_result(msg["id"]) - - -@websocket_api.websocket_command( - { - vol.Required("type"): "conversation/agent/list", - vol.Optional("language"): str, - vol.Optional("country"): str, - } -) -@websocket_api.async_response -async def websocket_list_agents( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict -) -> None: - """List conversation agents and, optionally, if they support a given language.""" - manager = _get_agent_manager(hass) - - country = msg.get("country") - language = msg.get("language") - agents = [] - - for agent_info in manager.async_get_agent_info(): - agent = await manager.async_get_agent(agent_info.id) - - supported_languages = agent.supported_languages - if language and supported_languages != MATCH_ALL: - supported_languages = language_util.matches( - language, supported_languages, country - ) - - agent_dict: dict[str, Any] = { - "id": agent_info.id, - "name": agent_info.name, - "supported_languages": supported_languages, - } - agents.append(agent_dict) - - connection.send_message(websocket_api.result_message(msg["id"], {"agents": agents})) - - -@websocket_api.websocket_command( - { - vol.Required("type"): "conversation/agent/homeassistant/debug", - vol.Required("sentences"): [str], - vol.Optional("language"): str, - vol.Optional("device_id"): vol.Any(str, None), - } -) -@websocket_api.async_response -async def websocket_hass_agent_debug( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict -) -> None: - """Return intents that would be matched by the default agent for a list of sentences.""" - agent = await _get_agent_manager(hass).async_get_agent(HOME_ASSISTANT_AGENT) - assert isinstance(agent, DefaultAgent) - results = [ - await agent.async_recognize( - ConversationInput( - text=sentence, - context=connection.context(msg), - conversation_id=None, - device_id=msg.get("device_id"), - language=msg.get("language", hass.config.language), - ) - ) - for sentence in msg["sentences"] - ] - - # Return results for each sentence in the same order as the input. - result_dicts: list[dict[str, Any] | None] = [] - for result in results: - result_dict: dict[str, Any] | None = None - if isinstance(result, SentenceTriggerResult): - result_dict = { - # Matched a user-defined sentence trigger. - # We can't provide the response here without executing the - # trigger. - "match": True, - "source": "trigger", - "sentence_template": result.sentence_template or "", - } - elif isinstance(result, RecognizeResult): - successful_match = not result.unmatched_entities - result_dict = { - # Name of the matching intent (or the closest) - "intent": { - "name": result.intent.name, - }, - # Slot values that would be received by the intent - "slots": { # direct access to values - entity_key: entity.text or entity.value - for entity_key, entity in result.entities.items() - }, - # Extra slot details, such as the originally matched text - "details": { - entity_key: { - "name": entity.name, - "value": entity.value, - "text": entity.text, - } - for entity_key, entity in result.entities.items() - }, - # Entities/areas/etc. that would be targeted - "targets": {}, - # True if match was successful - "match": successful_match, - # Text of the sentence template that matched (or was closest) - "sentence_template": "", - # When match is incomplete, this will contain the best slot guesses - "unmatched_slots": _get_unmatched_slots(result), - } - - if successful_match: - result_dict["targets"] = { - state.entity_id: {"matched": is_matched} - for state, is_matched in _get_debug_targets(hass, result) - } - - if result.intent_sentence is not None: - result_dict["sentence_template"] = result.intent_sentence.text - - # Inspect metadata to determine if this matched a custom sentence - if result.intent_metadata and result.intent_metadata.get( - METADATA_CUSTOM_SENTENCE - ): - result_dict["source"] = "custom" - result_dict["file"] = result.intent_metadata.get(METADATA_CUSTOM_FILE) - else: - result_dict["source"] = "builtin" - - result_dicts.append(result_dict) - - connection.send_result(msg["id"], {"results": result_dicts}) - - -def _get_debug_targets( - hass: HomeAssistant, - result: RecognizeResult, -) -> Iterable[tuple[core.State, bool]]: - """Yield state/is_matched pairs for a hassil recognition.""" - entities = result.entities - - name: str | None = None - area_name: str | None = None - domains: set[str] | None = None - device_classes: set[str] | None = None - state_names: set[str] | None = None - - if "name" in entities: - name = str(entities["name"].value) - - if "area" in entities: - area_name = str(entities["area"].value) - - if "domain" in entities: - domains = set(cv.ensure_list(entities["domain"].value)) - - if "device_class" in entities: - device_classes = set(cv.ensure_list(entities["device_class"].value)) - - if "state" in entities: - # HassGetState only - state_names = set(cv.ensure_list(entities["state"].value)) - - if ( - (name is None) - and (area_name is None) - and (not domains) - and (not device_classes) - and (not state_names) - ): - # Avoid "matching" all entities when there is no filter - return - - states = intent.async_match_states( - hass, - name=name, - area_name=area_name, - domains=domains, - device_classes=device_classes, - ) - - for state in states: - # For queries, a target is "matched" based on its state - is_matched = (state_names is None) or (state.state in state_names) - yield state, is_matched - - -def _get_unmatched_slots( - result: RecognizeResult, -) -> dict[str, str | int]: - """Return a dict of unmatched text/range slot entities.""" - unmatched_slots: dict[str, str | int] = {} - for entity in result.unmatched_entities_list: - if isinstance(entity, UnmatchedTextEntity): - if entity.text == MISSING_ENTITY: - # Don't report since these are just missing context - # slots. - continue - - unmatched_slots[entity.name] = entity.text - elif isinstance(entity, UnmatchedRangeEntity): - unmatched_slots[entity.name] = entity.value - - return unmatched_slots - - -class ConversationProcessView(http.HomeAssistantView): - """View to process text.""" - - url = "/api/conversation/process" - name = "api:conversation:process" - - @RequestDataValidator( - vol.Schema( - { - vol.Required("text"): str, - vol.Optional("conversation_id"): str, - vol.Optional("language"): str, - vol.Optional("agent_id"): agent_id_validator, - } - ) - ) - async def post(self, request: web.Request, data: dict[str, str]) -> web.Response: - """Send a request for processing.""" - hass = request.app[http.KEY_HASS] - - result = await async_converse( - hass, - text=data["text"], - conversation_id=data.get("conversation_id"), - context=self.context(request), - language=data.get("language"), - agent_id=data.get("agent_id"), - ) - - return self.json(result.as_dict()) - - -@dataclass(frozen=True) -class AgentInfo: - """Container for conversation agent info.""" - - id: str - name: str - - -@core.callback -def async_get_agent_info( - hass: core.HomeAssistant, - agent_id: str | None = None, -) -> AgentInfo | None: - """Get information on the agent or None if not found.""" - manager = _get_agent_manager(hass) - - if agent_id is None: - agent_id = manager.default_agent - - for agent_info in manager.async_get_agent_info(): - if agent_info.id == agent_id: - return agent_info - - return None - - -async def async_converse( - hass: core.HomeAssistant, - text: str, - conversation_id: str | None, - context: core.Context, - language: str | None = None, - agent_id: str | None = None, - device_id: str | None = None, -) -> ConversationResult: - """Process text and get intent.""" - agent = await _get_agent_manager(hass).async_get_agent(agent_id) - - if language is None: - language = hass.config.language - - _LOGGER.debug("Processing in %s: %s", language, text) - result = await agent.async_process( - ConversationInput( - text=text, - context=context, - conversation_id=conversation_id, - device_id=device_id, - language=language, - ) - ) - return result - - -class AgentManager: - """Class to manage conversation agents.""" - - default_agent: str = HOME_ASSISTANT_AGENT - _builtin_agent: AbstractConversationAgent | None = None - - def __init__(self, hass: HomeAssistant) -> None: - """Initialize the conversation agents.""" - self.hass = hass - self._agents: dict[str, AbstractConversationAgent] = {} - self._builtin_agent_init_lock = asyncio.Lock() - - def async_setup(self) -> None: - """Set up the conversation agents.""" - async_setup_default_agent(self.hass) - - async def async_get_agent( - self, agent_id: str | None = None - ) -> AbstractConversationAgent: - """Get the agent.""" - if agent_id is None: - agent_id = self.default_agent - - if agent_id == HOME_ASSISTANT_AGENT: - if self._builtin_agent is not None: - return self._builtin_agent - - async with self._builtin_agent_init_lock: - if self._builtin_agent is not None: - return self._builtin_agent - - self._builtin_agent = DefaultAgent(self.hass) - await self._builtin_agent.async_initialize( - self.hass.data.get(DATA_CONFIG) - ) - - return self._builtin_agent - - if agent_id not in self._agents: - raise ValueError(f"Agent {agent_id} not found") - - return self._agents[agent_id] - - @core.callback - def async_get_agent_info(self) -> list[AgentInfo]: - """List all agents.""" - agents: list[AgentInfo] = [ - AgentInfo( - id=HOME_ASSISTANT_AGENT, - name="Home Assistant", - ) - ] - for agent_id, agent in self._agents.items(): - config_entry = self.hass.config_entries.async_get_entry(agent_id) - - # Guard against potential bugs in conversation agents where the agent is not - # removed from the manager when the config entry is removed - if config_entry is None: - _LOGGER.warning( - "Conversation agent %s is still loaded after config entry removal", - agent, - ) - continue - - agents.append( - AgentInfo( - id=agent_id, - name=config_entry.title or config_entry.domain, - ) - ) - return agents - - @core.callback - def async_is_valid_agent_id(self, agent_id: str) -> bool: - """Check if the agent id is valid.""" - return agent_id in self._agents or agent_id == HOME_ASSISTANT_AGENT - - @core.callback - def async_set_agent(self, agent_id: str, agent: AbstractConversationAgent) -> None: - """Set the agent.""" - self._agents[agent_id] = agent - - @core.callback - def async_unset_agent(self, agent_id: str) -> None: - """Unset the agent.""" - self._agents.pop(agent_id, None) diff --git a/homeassistant/components/conversation/agent_manager.py b/homeassistant/components/conversation/agent_manager.py new file mode 100644 index 00000000000..f34ecfaecc9 --- /dev/null +++ b/homeassistant/components/conversation/agent_manager.py @@ -0,0 +1,161 @@ +"""Agent foundation for conversation integration.""" + +from __future__ import annotations + +import asyncio +from dataclasses import dataclass +import logging +from typing import Any + +import voluptuous as vol + +from homeassistant.core import Context, HomeAssistant, async_get_hass, callback +from homeassistant.helpers import config_validation as cv, singleton + +from .const import DATA_CONFIG, HOME_ASSISTANT_AGENT +from .default_agent import DefaultAgent, async_setup as async_setup_default_agent +from .models import AbstractConversationAgent, ConversationInput, ConversationResult + +_LOGGER = logging.getLogger(__name__) + + +@singleton.singleton("conversation_agent") +@callback +def get_agent_manager(hass: HomeAssistant) -> AgentManager: + """Get the active agent.""" + manager = AgentManager(hass) + manager.async_setup() + return manager + + +def agent_id_validator(value: Any) -> str: + """Validate agent ID.""" + hass = async_get_hass() + manager = get_agent_manager(hass) + if not manager.async_is_valid_agent_id(cv.string(value)): + raise vol.Invalid("invalid agent ID") + return value + + +async def async_converse( + hass: HomeAssistant, + text: str, + conversation_id: str | None, + context: Context, + language: str | None = None, + agent_id: str | None = None, + device_id: str | None = None, +) -> ConversationResult: + """Process text and get intent.""" + agent = await get_agent_manager(hass).async_get_agent(agent_id) + + if language is None: + language = hass.config.language + + _LOGGER.debug("Processing in %s: %s", language, text) + result = await agent.async_process( + ConversationInput( + text=text, + context=context, + conversation_id=conversation_id, + device_id=device_id, + language=language, + ) + ) + return result + + +@dataclass(frozen=True) +class AgentInfo: + """Container for conversation agent info.""" + + id: str + name: str + + +class AgentManager: + """Class to manage conversation agents.""" + + default_agent: str = HOME_ASSISTANT_AGENT + _builtin_agent: AbstractConversationAgent | None = None + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the conversation agents.""" + self.hass = hass + self._agents: dict[str, AbstractConversationAgent] = {} + self._builtin_agent_init_lock = asyncio.Lock() + + def async_setup(self) -> None: + """Set up the conversation agents.""" + async_setup_default_agent(self.hass) + + async def async_get_agent( + self, agent_id: str | None = None + ) -> AbstractConversationAgent: + """Get the agent.""" + if agent_id is None: + agent_id = self.default_agent + + if agent_id == HOME_ASSISTANT_AGENT: + if self._builtin_agent is not None: + return self._builtin_agent + + async with self._builtin_agent_init_lock: + if self._builtin_agent is not None: + return self._builtin_agent + + self._builtin_agent = DefaultAgent(self.hass) + await self._builtin_agent.async_initialize( + self.hass.data.get(DATA_CONFIG) + ) + + return self._builtin_agent + + if agent_id not in self._agents: + raise ValueError(f"Agent {agent_id} not found") + + return self._agents[agent_id] + + @callback + def async_get_agent_info(self) -> list[AgentInfo]: + """List all agents.""" + agents: list[AgentInfo] = [ + AgentInfo( + id=HOME_ASSISTANT_AGENT, + name="Home Assistant", + ) + ] + for agent_id, agent in self._agents.items(): + config_entry = self.hass.config_entries.async_get_entry(agent_id) + + # Guard against potential bugs in conversation agents where the agent is not + # removed from the manager when the config entry is removed + if config_entry is None: + _LOGGER.warning( + "Conversation agent %s is still loaded after config entry removal", + agent, + ) + continue + + agents.append( + AgentInfo( + id=agent_id, + name=config_entry.title or config_entry.domain, + ) + ) + return agents + + @callback + def async_is_valid_agent_id(self, agent_id: str) -> bool: + """Check if the agent id is valid.""" + return agent_id in self._agents or agent_id == HOME_ASSISTANT_AGENT + + @callback + def async_set_agent(self, agent_id: str, agent: AbstractConversationAgent) -> None: + """Set the agent.""" + self._agents[agent_id] = agent + + @callback + def async_unset_agent(self, agent_id: str) -> None: + """Unset the agent.""" + self._agents.pop(agent_id, None) diff --git a/homeassistant/components/conversation/const.py b/homeassistant/components/conversation/const.py index a8828fcc0e9..5cb5ca3bdea 100644 --- a/homeassistant/components/conversation/const.py +++ b/homeassistant/components/conversation/const.py @@ -3,3 +3,4 @@ DOMAIN = "conversation" DEFAULT_EXPOSED_ATTRIBUTES = {"device_class"} HOME_ASSISTANT_AGENT = "homeassistant" +DATA_CONFIG = "conversation_config" diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 29a06d44c5f..5a8d7b64eec 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -46,8 +46,8 @@ from homeassistant.helpers.event import ( ) from homeassistant.util.json import JsonObjectType, json_loads_object -from .agent import AbstractConversationAgent, ConversationInput, ConversationResult from .const import DEFAULT_EXPOSED_ATTRIBUTES, DOMAIN +from .models import AbstractConversationAgent, ConversationInput, ConversationResult _LOGGER = logging.getLogger(__name__) _DEFAULT_ERROR_TEXT = "Sorry, I couldn't understand that" diff --git a/homeassistant/components/conversation/http.py b/homeassistant/components/conversation/http.py new file mode 100644 index 00000000000..fb67d686b23 --- /dev/null +++ b/homeassistant/components/conversation/http.py @@ -0,0 +1,325 @@ +"""HTTP endpoints for conversation integration.""" + +from __future__ import annotations + +from collections.abc import Iterable +from typing import Any + +from aiohttp import web +from hassil.recognize import ( + MISSING_ENTITY, + RecognizeResult, + UnmatchedRangeEntity, + UnmatchedTextEntity, +) +import voluptuous as vol + +from homeassistant.components import http, websocket_api +from homeassistant.components.http.data_validator import RequestDataValidator +from homeassistant.const import MATCH_ALL +from homeassistant.core import HomeAssistant, State, callback +from homeassistant.helpers import config_validation as cv, intent +from homeassistant.util import language as language_util + +from .agent_manager import agent_id_validator, async_converse, get_agent_manager +from .const import HOME_ASSISTANT_AGENT +from .default_agent import ( + METADATA_CUSTOM_FILE, + METADATA_CUSTOM_SENTENCE, + DefaultAgent, + SentenceTriggerResult, +) +from .models import ConversationInput + + +@callback +def async_setup(hass: HomeAssistant) -> None: + """Set up the HTTP API for the conversation integration.""" + hass.http.register_view(ConversationProcessView()) + websocket_api.async_register_command(hass, websocket_process) + websocket_api.async_register_command(hass, websocket_prepare) + websocket_api.async_register_command(hass, websocket_list_agents) + websocket_api.async_register_command(hass, websocket_hass_agent_debug) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "conversation/process", + vol.Required("text"): str, + vol.Optional("conversation_id"): vol.Any(str, None), + vol.Optional("language"): str, + vol.Optional("agent_id"): agent_id_validator, + } +) +@websocket_api.async_response +async def websocket_process( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Process text.""" + result = await async_converse( + hass=hass, + text=msg["text"], + conversation_id=msg.get("conversation_id"), + context=connection.context(msg), + language=msg.get("language"), + agent_id=msg.get("agent_id"), + ) + connection.send_result(msg["id"], result.as_dict()) + + +@websocket_api.websocket_command( + { + "type": "conversation/prepare", + vol.Optional("language"): str, + vol.Optional("agent_id"): agent_id_validator, + } +) +@websocket_api.async_response +async def websocket_prepare( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Reload intents.""" + manager = get_agent_manager(hass) + agent = await manager.async_get_agent(msg.get("agent_id")) + await agent.async_prepare(msg.get("language")) + connection.send_result(msg["id"]) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "conversation/agent/list", + vol.Optional("language"): str, + vol.Optional("country"): str, + } +) +@websocket_api.async_response +async def websocket_list_agents( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """List conversation agents and, optionally, if they support a given language.""" + manager = get_agent_manager(hass) + + country = msg.get("country") + language = msg.get("language") + agents = [] + + for agent_info in manager.async_get_agent_info(): + agent = await manager.async_get_agent(agent_info.id) + + supported_languages = agent.supported_languages + if language and supported_languages != MATCH_ALL: + supported_languages = language_util.matches( + language, supported_languages, country + ) + + agent_dict: dict[str, Any] = { + "id": agent_info.id, + "name": agent_info.name, + "supported_languages": supported_languages, + } + agents.append(agent_dict) + + connection.send_message(websocket_api.result_message(msg["id"], {"agents": agents})) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "conversation/agent/homeassistant/debug", + vol.Required("sentences"): [str], + vol.Optional("language"): str, + vol.Optional("device_id"): vol.Any(str, None), + } +) +@websocket_api.async_response +async def websocket_hass_agent_debug( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Return intents that would be matched by the default agent for a list of sentences.""" + agent = await get_agent_manager(hass).async_get_agent(HOME_ASSISTANT_AGENT) + assert isinstance(agent, DefaultAgent) + results = [ + await agent.async_recognize( + ConversationInput( + text=sentence, + context=connection.context(msg), + conversation_id=None, + device_id=msg.get("device_id"), + language=msg.get("language", hass.config.language), + ) + ) + for sentence in msg["sentences"] + ] + + # Return results for each sentence in the same order as the input. + result_dicts: list[dict[str, Any] | None] = [] + for result in results: + result_dict: dict[str, Any] | None = None + if isinstance(result, SentenceTriggerResult): + result_dict = { + # Matched a user-defined sentence trigger. + # We can't provide the response here without executing the + # trigger. + "match": True, + "source": "trigger", + "sentence_template": result.sentence_template or "", + } + elif isinstance(result, RecognizeResult): + successful_match = not result.unmatched_entities + result_dict = { + # Name of the matching intent (or the closest) + "intent": { + "name": result.intent.name, + }, + # Slot values that would be received by the intent + "slots": { # direct access to values + entity_key: entity.text or entity.value + for entity_key, entity in result.entities.items() + }, + # Extra slot details, such as the originally matched text + "details": { + entity_key: { + "name": entity.name, + "value": entity.value, + "text": entity.text, + } + for entity_key, entity in result.entities.items() + }, + # Entities/areas/etc. that would be targeted + "targets": {}, + # True if match was successful + "match": successful_match, + # Text of the sentence template that matched (or was closest) + "sentence_template": "", + # When match is incomplete, this will contain the best slot guesses + "unmatched_slots": _get_unmatched_slots(result), + } + + if successful_match: + result_dict["targets"] = { + state.entity_id: {"matched": is_matched} + for state, is_matched in _get_debug_targets(hass, result) + } + + if result.intent_sentence is not None: + result_dict["sentence_template"] = result.intent_sentence.text + + # Inspect metadata to determine if this matched a custom sentence + if result.intent_metadata and result.intent_metadata.get( + METADATA_CUSTOM_SENTENCE + ): + result_dict["source"] = "custom" + result_dict["file"] = result.intent_metadata.get(METADATA_CUSTOM_FILE) + else: + result_dict["source"] = "builtin" + + result_dicts.append(result_dict) + + connection.send_result(msg["id"], {"results": result_dicts}) + + +def _get_debug_targets( + hass: HomeAssistant, + result: RecognizeResult, +) -> Iterable[tuple[State, bool]]: + """Yield state/is_matched pairs for a hassil recognition.""" + entities = result.entities + + name: str | None = None + area_name: str | None = None + domains: set[str] | None = None + device_classes: set[str] | None = None + state_names: set[str] | None = None + + if "name" in entities: + name = str(entities["name"].value) + + if "area" in entities: + area_name = str(entities["area"].value) + + if "domain" in entities: + domains = set(cv.ensure_list(entities["domain"].value)) + + if "device_class" in entities: + device_classes = set(cv.ensure_list(entities["device_class"].value)) + + if "state" in entities: + # HassGetState only + state_names = set(cv.ensure_list(entities["state"].value)) + + if ( + (name is None) + and (area_name is None) + and (not domains) + and (not device_classes) + and (not state_names) + ): + # Avoid "matching" all entities when there is no filter + return + + states = intent.async_match_states( + hass, + name=name, + area_name=area_name, + domains=domains, + device_classes=device_classes, + ) + + for state in states: + # For queries, a target is "matched" based on its state + is_matched = (state_names is None) or (state.state in state_names) + yield state, is_matched + + +def _get_unmatched_slots( + result: RecognizeResult, +) -> dict[str, str | int]: + """Return a dict of unmatched text/range slot entities.""" + unmatched_slots: dict[str, str | int] = {} + for entity in result.unmatched_entities_list: + if isinstance(entity, UnmatchedTextEntity): + if entity.text == MISSING_ENTITY: + # Don't report since these are just missing context + # slots. + continue + + unmatched_slots[entity.name] = entity.text + elif isinstance(entity, UnmatchedRangeEntity): + unmatched_slots[entity.name] = entity.value + + return unmatched_slots + + +class ConversationProcessView(http.HomeAssistantView): + """View to process text.""" + + url = "/api/conversation/process" + name = "api:conversation:process" + + @RequestDataValidator( + vol.Schema( + { + vol.Required("text"): str, + vol.Optional("conversation_id"): str, + vol.Optional("language"): str, + vol.Optional("agent_id"): agent_id_validator, + } + ) + ) + async def post(self, request: web.Request, data: dict[str, str]) -> web.Response: + """Send a request for processing.""" + hass = request.app[http.KEY_HASS] + + result = await async_converse( + hass, + text=data["text"], + conversation_id=data.get("conversation_id"), + context=self.context(request), + language=data.get("language"), + agent_id=data.get("agent_id"), + ) + + return self.json(result.as_dict()) diff --git a/homeassistant/components/conversation/agent.py b/homeassistant/components/conversation/models.py similarity index 100% rename from homeassistant/components/conversation/agent.py rename to homeassistant/components/conversation/models.py diff --git a/homeassistant/components/conversation/trigger.py b/homeassistant/components/conversation/trigger.py index 0fadc458352..05fea054bca 100644 --- a/homeassistant/components/conversation/trigger.py +++ b/homeassistant/components/conversation/trigger.py @@ -14,8 +14,8 @@ from homeassistant.helpers.script import ScriptRunResult from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import UNDEFINED, ConfigType -from . import HOME_ASSISTANT_AGENT, _get_agent_manager -from .const import DOMAIN +from .agent_manager import get_agent_manager +from .const import DOMAIN, HOME_ASSISTANT_AGENT from .default_agent import DefaultAgent @@ -111,7 +111,7 @@ async def async_attach_trigger( # two trigger copies for who will provide a response. return None - default_agent = await _get_agent_manager(hass).async_get_agent(HOME_ASSISTANT_AGENT) + default_agent = await get_agent_manager(hass).async_get_agent(HOME_ASSISTANT_AGENT) assert isinstance(default_agent, DefaultAgent) return default_agent.register_trigger(sentences, call_action) diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 8381f27398a..d75aebe4ded 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -453,11 +453,13 @@ async def test_stop_discovery_called_on_stop( """Test pychromecast.stop_discovery called on shutdown.""" # start_discovery should be called with empty config await async_setup_cast(hass, {}) + await hass.async_block_till_done() assert castbrowser_mock.return_value.start_discovery.call_count == 1 # stop discovery should be called on shutdown hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() + await hass.async_block_till_done() assert castbrowser_mock.return_value.stop_discovery.call_count == 1 diff --git a/tests/components/conversation/__init__.py b/tests/components/conversation/__init__.py index 7209148e21f..fb9bcab7498 100644 --- a/tests/components/conversation/__init__.py +++ b/tests/components/conversation/__init__.py @@ -5,11 +5,16 @@ from __future__ import annotations from typing import Literal from homeassistant.components import conversation +from homeassistant.components.conversation.models import ( + ConversationInput, + ConversationResult, +) from homeassistant.components.homeassistant.exposed_entities import ( DATA_EXPOSED_ENTITIES, ExposedEntities, async_expose_entity, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers import intent @@ -30,24 +35,22 @@ class MockAgent(conversation.AbstractConversationAgent): """Return a list of supported languages.""" return self._supported_languages - async def async_process( - self, user_input: conversation.ConversationInput - ) -> conversation.ConversationResult: + async def async_process(self, user_input: ConversationInput) -> ConversationResult: """Process some text.""" self.calls.append(user_input) response = intent.IntentResponse(language=user_input.language) response.async_set_speech(self.response) - return conversation.ConversationResult( + return ConversationResult( response=response, conversation_id=user_input.conversation_id ) -def expose_new(hass, expose_new): +def expose_new(hass: HomeAssistant, expose_new: bool): """Enable exposing new entities to the default agent.""" exposed_entities: ExposedEntities = hass.data[DATA_EXPOSED_ENTITIES] exposed_entities.async_set_expose_new_entities(conversation.DOMAIN, expose_new) -def expose_entity(hass, entity_id, should_expose): +def expose_entity(hass: HomeAssistant, entity_id: str, should_expose: bool): """Expose an entity to the default agent.""" async_expose_entity(hass, conversation.DOMAIN, entity_id, should_expose) diff --git a/tests/components/conversation/test_default_agent.py b/tests/components/conversation/test_default_agent.py index 8f38459a8da..c600c71711e 100644 --- a/tests/components/conversation/test_default_agent.py +++ b/tests/components/conversation/test_default_agent.py @@ -7,6 +7,7 @@ from hassil.recognize import Intent, IntentData, MatchEntity, RecognizeResult import pytest from homeassistant.components import conversation +from homeassistant.components.conversation import agent_manager, default_agent from homeassistant.components.homeassistant.exposed_entities import ( async_get_assistant_settings, ) @@ -151,7 +152,7 @@ async def test_conversation_agent( init_components, ) -> None: """Test DefaultAgent.""" - agent = await conversation._get_agent_manager(hass).async_get_agent( + agent = await agent_manager.get_agent_manager(hass).async_get_agent( conversation.HOME_ASSISTANT_AGENT ) with patch( @@ -253,10 +254,10 @@ async def test_trigger_sentences(hass: HomeAssistant, init_components) -> None: trigger_sentences = ["It's party time", "It is time to party"] trigger_response = "Cowabunga!" - agent = await conversation._get_agent_manager(hass).async_get_agent( + agent = await agent_manager.get_agent_manager(hass).async_get_agent( conversation.HOME_ASSISTANT_AGENT ) - assert isinstance(agent, conversation.DefaultAgent) + assert isinstance(agent, default_agent.DefaultAgent) callback = AsyncMock(return_value=trigger_response) unregister = agent.register_trigger(trigger_sentences, callback) @@ -850,7 +851,7 @@ async def test_empty_aliases( ) with patch( - "homeassistant.components.conversation.DefaultAgent._recognize", + "homeassistant.components.conversation.default_agent.DefaultAgent._recognize", return_value=None, ) as mock_recognize_all: await conversation.async_converse( diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index 7b2c44a755d..62f67548ece 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -9,6 +9,8 @@ from syrupy.assertion import SnapshotAssertion import voluptuous as vol from homeassistant.components import conversation +from homeassistant.components.conversation import agent_manager, 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 @@ -750,8 +752,8 @@ async def test_ws_prepare( """Test the Websocket prepare conversation API.""" assert await async_setup_component(hass, "homeassistant", {}) assert await async_setup_component(hass, "conversation", {}) - agent = await conversation._get_agent_manager(hass).async_get_agent() - assert isinstance(agent, conversation.DefaultAgent) + agent = await agent_manager.get_agent_manager(hass).async_get_agent() + assert isinstance(agent, default_agent.DefaultAgent) # No intents should be loaded yet assert not agent._lang_intents.get(hass.config.language) @@ -852,8 +854,8 @@ async def test_prepare_reload(hass: HomeAssistant) -> None: assert await async_setup_component(hass, "conversation", {}) # Load intents - agent = await conversation._get_agent_manager(hass).async_get_agent() - assert isinstance(agent, conversation.DefaultAgent) + agent = await agent_manager.get_agent_manager(hass).async_get_agent() + assert isinstance(agent, default_agent.DefaultAgent) await agent.async_prepare(language) # Confirm intents are loaded @@ -880,8 +882,8 @@ async def test_prepare_fail(hass: HomeAssistant) -> None: assert await async_setup_component(hass, "conversation", {}) # Load intents - agent = await conversation._get_agent_manager(hass).async_get_agent() - assert isinstance(agent, conversation.DefaultAgent) + agent = await agent_manager.get_agent_manager(hass).async_get_agent() + assert isinstance(agent, default_agent.DefaultAgent) await agent.async_prepare("not-a-language") # Confirm no intents were loaded @@ -917,11 +919,11 @@ async def test_non_default_response(hass: HomeAssistant, init_components) -> Non hass.states.async_set("cover.front_door", "closed") calls = async_mock_service(hass, "cover", SERVICE_OPEN_COVER) - agent = await conversation._get_agent_manager(hass).async_get_agent() - assert isinstance(agent, conversation.DefaultAgent) + agent = await agent_manager.get_agent_manager(hass).async_get_agent() + assert isinstance(agent, default_agent.DefaultAgent) result = await agent.async_process( - conversation.ConversationInput( + ConversationInput( text="open the front door", context=Context(), conversation_id=None, diff --git a/tests/components/conversation/test_trigger.py b/tests/components/conversation/test_trigger.py index 221789b49e0..33ad8efdd2e 100644 --- a/tests/components/conversation/test_trigger.py +++ b/tests/components/conversation/test_trigger.py @@ -5,7 +5,8 @@ import logging import pytest import voluptuous as vol -from homeassistant.components import conversation +from homeassistant.components.conversation import agent_manager, default_agent +from homeassistant.components.conversation.models import ConversationInput from homeassistant.core import Context, HomeAssistant from homeassistant.helpers import trigger from homeassistant.setup import async_setup_component @@ -514,11 +515,11 @@ async def test_trigger_with_device_id(hass: HomeAssistant) -> None: }, ) - agent = await conversation._get_agent_manager(hass).async_get_agent() - assert isinstance(agent, conversation.DefaultAgent) + agent = await agent_manager.get_agent_manager(hass).async_get_agent() + assert isinstance(agent, default_agent.DefaultAgent) result = await agent.async_process( - conversation.ConversationInput( + ConversationInput( text="test sentence", context=Context(), conversation_id=None, diff --git a/tests/components/google_assistant_sdk/test_init.py b/tests/components/google_assistant_sdk/test_init.py index 2d930599c24..7c2fc8291d4 100644 --- a/tests/components/google_assistant_sdk/test_init.py +++ b/tests/components/google_assistant_sdk/test_init.py @@ -334,7 +334,7 @@ async def test_conversation_agent( entry = entries[0] assert entry.state is ConfigEntryState.LOADED - agent = await conversation._get_agent_manager(hass).async_get_agent(entry.entry_id) + agent = await conversation.get_agent_manager(hass).async_get_agent(entry.entry_id) assert agent.supported_languages == SUPPORTED_LANGUAGE_CODES text1 = "tell me a joke" diff --git a/tests/components/google_generative_ai_conversation/test_init.py b/tests/components/google_generative_ai_conversation/test_init.py index b77fa14b4cf..92e84b1fd39 100644 --- a/tests/components/google_generative_ai_conversation/test_init.py +++ b/tests/components/google_generative_ai_conversation/test_init.py @@ -152,7 +152,7 @@ async def test_conversation_agent( mock_init_component, ) -> None: """Test GoogleGenerativeAIAgent.""" - agent = await conversation._get_agent_manager(hass).async_get_agent( + agent = await conversation.get_agent_manager(hass).async_get_agent( mock_config_entry.entry_id ) assert agent.supported_languages == "*" diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index dfab474f127..9d941685c09 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -1033,7 +1033,7 @@ async def test_webhook_handle_conversation_process( webhook_client.server.app.router._frozen = False with patch( - "homeassistant.components.conversation.AgentManager.async_get_agent", + "homeassistant.components.conversation.agent_manager.AgentManager.async_get_agent", return_value=mock_conversation_agent, ): resp = await webhook_client.post( diff --git a/tests/components/ollama/test_init.py b/tests/components/ollama/test_init.py index ffe69ca4628..6dd9dc73973 100644 --- a/tests/components/ollama/test_init.py +++ b/tests/components/ollama/test_init.py @@ -229,7 +229,7 @@ async def test_message_history_pruning( assert isinstance(result.conversation_id, str) conversation_ids.append(result.conversation_id) - agent = await conversation._get_agent_manager(hass).async_get_agent( + agent = await conversation.get_agent_manager(hass).async_get_agent( mock_config_entry.entry_id ) assert isinstance(agent, ollama.OllamaAgent) @@ -284,7 +284,7 @@ async def test_message_history_unlimited( result.response.response_type == intent.IntentResponseType.ACTION_DONE ), result - agent = await conversation._get_agent_manager(hass).async_get_agent( + agent = await conversation.get_agent_manager(hass).async_get_agent( mock_config_entry.entry_id ) assert isinstance(agent, ollama.OllamaAgent) @@ -340,7 +340,7 @@ async def test_conversation_agent( mock_init_component, ) -> None: """Test OllamaAgent.""" - agent = await conversation._get_agent_manager(hass).async_get_agent( + agent = await conversation.get_agent_manager(hass).async_get_agent( mock_config_entry.entry_id ) assert agent.supported_languages == MATCH_ALL diff --git a/tests/components/openai_conversation/test_init.py b/tests/components/openai_conversation/test_init.py index 3a8db2a71c0..c94fdcebcde 100644 --- a/tests/components/openai_conversation/test_init.py +++ b/tests/components/openai_conversation/test_init.py @@ -194,7 +194,7 @@ async def test_conversation_agent( mock_init_component, ) -> None: """Test OpenAIAgent.""" - agent = await conversation._get_agent_manager(hass).async_get_agent( + agent = await conversation.get_agent_manager(hass).async_get_agent( mock_config_entry.entry_id ) assert agent.supported_languages == "*" From 5038a035bd63f090c71f3e3237569b4cd8beb411 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 30 Mar 2024 19:51:31 -1000 Subject: [PATCH 103/967] Detect blocking module imports in the event loop (#114488) --- homeassistant/block_async_io.py | 43 ++++- homeassistant/bootstrap.py | 11 +- homeassistant/components/recorder/pool.py | 2 +- homeassistant/core.py | 3 +- homeassistant/util/async_.py | 107 +----------- homeassistant/util/loop.py | 146 ++++++++++++++++ tests/test_block_async_io.py | 200 ++++++++++++++++++++++ tests/util/test_async.py | 169 ------------------ tests/util/test_loop.py | 200 ++++++++++++++++++++++ 9 files changed, 600 insertions(+), 281 deletions(-) create mode 100644 homeassistant/util/loop.py create mode 100644 tests/test_block_async_io.py create mode 100644 tests/util/test_loop.py diff --git a/homeassistant/block_async_io.py b/homeassistant/block_async_io.py index bf805b5ef21..a2c187fc537 100644 --- a/homeassistant/block_async_io.py +++ b/homeassistant/block_async_io.py @@ -1,9 +1,36 @@ """Block blocking calls being done in asyncio.""" +from contextlib import suppress from http.client import HTTPConnection +import importlib +import sys import time +from typing import Any -from .util.async_ import protect_loop +from .helpers.frame import get_current_frame +from .util.loop import protect_loop + +_IN_TESTS = "unittest" in sys.modules + + +def _check_import_call_allowed(mapped_args: dict[str, Any]) -> bool: + # If the module is already imported, we can ignore it. + return bool((args := mapped_args.get("args")) and args[0] in sys.modules) + + +def _check_sleep_call_allowed(mapped_args: dict[str, Any]) -> bool: + # + # Avoid extracting the stack unless we need to since it + # will have to access the linecache which can do blocking + # I/O and we are trying to avoid blocking calls. + # + # frame[0] is us + # frame[1] is check_loop + # frame[2] is protected_loop_func + # frame[3] is the offender + with suppress(ValueError): + return get_current_frame(4).f_code.co_filename.endswith("pydevd.py") + return False def enable() -> None: @@ -14,8 +41,20 @@ def enable() -> None: ) # Prevent sleeping in event loop. Non-strict since 2022.02 - time.sleep = protect_loop(time.sleep, strict=False) + time.sleep = protect_loop( + time.sleep, strict=False, check_allowed=_check_sleep_call_allowed + ) # Currently disabled. pytz doing I/O when getting timezone. # Prevent files being opened inside the event loop # builtins.open = protect_loop(builtins.open) + + if not _IN_TESTS: + # 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, + ) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 5b805b6138e..97bdd615d69 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -23,7 +23,14 @@ import cryptography.hazmat.backends.openssl.backend # noqa: F401 import voluptuous as vol import yarl -from . import config as conf_util, config_entries, core, loader, requirements +from . import ( + block_async_io, + config as conf_util, + config_entries, + core, + loader, + requirements, +) # Pre-import frontend deps which have no requirements here to avoid # loading them at run time and blocking the event loop. We do this ahead @@ -260,6 +267,8 @@ 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 basic_setup_success = False diff --git a/homeassistant/components/recorder/pool.py b/homeassistant/components/recorder/pool.py index 27bc313b162..ec7aa5bdcb6 100644 --- a/homeassistant/components/recorder/pool.py +++ b/homeassistant/components/recorder/pool.py @@ -14,7 +14,7 @@ from sqlalchemy.pool import ( ) from homeassistant.helpers.frame import report -from homeassistant.util.async_ import check_loop +from homeassistant.util.loop import check_loop from .const import DB_WORKER_PREFIX diff --git a/homeassistant/core.py b/homeassistant/core.py index 6a923f4ab16..58e94d63352 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -50,7 +50,7 @@ from typing_extensions import TypeVar import voluptuous as vol import yarl -from . import block_async_io, util +from . import util from .const import ( ATTR_DOMAIN, ATTR_FRIENDLY_NAME, @@ -130,7 +130,6 @@ STOP_STAGE_SHUTDOWN_TIMEOUT = 100 FINAL_WRITE_STAGE_SHUTDOWN_TIMEOUT = 60 CLOSE_STAGE_SHUTDOWN_TIMEOUT = 30 -block_async_io.enable() _T = TypeVar("_T") _R = TypeVar("_R") diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py index 8c042242e0b..5ca19296b41 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -5,21 +5,15 @@ from __future__ import annotations from asyncio import AbstractEventLoop, Future, Semaphore, Task, gather, get_running_loop from collections.abc import Awaitable, Callable, Coroutine import concurrent.futures -from contextlib import suppress -import functools import logging import threading -from typing import Any, ParamSpec, TypeVar, TypeVarTuple - -from homeassistant.exceptions import HomeAssistantError +from typing import Any, TypeVar, TypeVarTuple _LOGGER = logging.getLogger(__name__) _SHUTDOWN_RUN_CALLBACK_THREADSAFE = "_shutdown_run_callback_threadsafe" _T = TypeVar("_T") -_R = TypeVar("_R") -_P = ParamSpec("_P") _Ts = TypeVarTuple("_Ts") @@ -92,105 +86,6 @@ def run_callback_threadsafe( return future -def check_loop( - func: Callable[..., Any], strict: bool = True, advise_msg: str | None = None -) -> 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. - """ - try: - get_running_loop() - in_loop = True - except RuntimeError: - in_loop = False - - if not in_loop: - return - - # Import only after we know we are running in the event loop - # so threads do not have to pay the late import cost. - # pylint: disable=import-outside-toplevel - from homeassistant.core import HomeAssistant, async_get_hass - from homeassistant.helpers.frame import ( - MissingIntegrationFrame, - get_current_frame, - get_integration_frame, - ) - from homeassistant.loader import async_suggest_report_issue - - found_frame = None - - if func.__name__ == "sleep": - # - # Avoid extracting the stack unless we need to since it - # will have to access the linecache which can do blocking - # I/O and we are trying to avoid blocking calls. - # - # frame[1] is us - # frame[2] is protected_loop_func - # frame[3] is the offender - with suppress(ValueError): - offender_frame = get_current_frame(3) - if offender_frame.f_code.co_filename.endswith("pydevd.py"): - return - - try: - integration_frame = get_integration_frame() - except MissingIntegrationFrame: - # Did not source from integration? Hard error. - if found_frame is None: - raise RuntimeError( # noqa: TRY200 - f"Detected blocking call to {func.__name__} inside the event loop. " - 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" - ) - - hass: HomeAssistant | None = None - with suppress(HomeAssistantError): - hass = async_get_hass() - report_issue = async_suggest_report_issue( - hass, - integration_domain=integration_frame.integration, - module=integration_frame.module, - ) - - _LOGGER.warning( - ( - "Detected blocking call to %s inside the event loop by %sintegration '%s' " - "at %s, line %s: %s, please %s" - ), - func.__name__, - "custom " if integration_frame.custom_integration else "", - integration_frame.integration, - integration_frame.relative_filename, - integration_frame.line_number, - integration_frame.line, - report_issue, - ) - - 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}" - ) - - -def protect_loop(func: Callable[_P, _R], strict: bool = True) -> Callable[_P, _R]: - """Protect function from running in event loop.""" - - @functools.wraps(func) - def protected_loop_func(*args: _P.args, **kwargs: _P.kwargs) -> _R: - check_loop(func, strict=strict) - return func(*args, **kwargs) - - return protected_loop_func - - async def gather_with_limited_concurrency( limit: int, *tasks: Any, return_exceptions: bool = False ) -> Any: diff --git a/homeassistant/util/loop.py b/homeassistant/util/loop.py new file mode 100644 index 00000000000..f8fe5c701f3 --- /dev/null +++ b/homeassistant/util/loop.py @@ -0,0 +1,146 @@ +"""asyncio loop utilities.""" + +from __future__ import annotations + +from asyncio import get_running_loop +from collections.abc import Callable +from contextlib import suppress +import functools +import linecache +import logging +from typing import Any, ParamSpec, TypeVar + +from homeassistant.core import HomeAssistant, async_get_hass +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.frame import ( + MissingIntegrationFrame, + get_current_frame, + get_integration_frame, +) +from homeassistant.loader import async_suggest_report_issue + +_LOGGER = logging.getLogger(__name__) + + +_R = TypeVar("_R") +_P = ParamSpec("_P") + + +def _get_line_from_cache(filename: str, lineno: int) -> str: + """Get line from cache or read from file.""" + return (linecache.getline(filename, lineno) or "?").strip() + + +def check_loop( + func: Callable[..., Any], + 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. + """ + try: + get_running_loop() + in_loop = True + except RuntimeError: + in_loop = False + + if not in_loop: + return + + if check_allowed is not None and check_allowed(mapped_args): + return + + found_frame = None + offender_frame = get_current_frame(2) + offender_filename = offender_frame.f_code.co_filename + offender_lineno = offender_frame.f_lineno + offender_line = _get_line_from_cache(offender_filename, offender_lineno) + + try: + integration_frame = get_integration_frame() + except MissingIntegrationFrame: + # Did not source from integration? Hard error. + if not strict_core: + _LOGGER.warning( + "Detected blocking call to %s with args %s in %s, " + "line %s: %s inside the event loop", + func.__name__, + mapped_args.get("args"), + offender_filename, + offender_lineno, + offender_line, + ) + 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" + ) + + hass: HomeAssistant | None = None + with suppress(HomeAssistantError): + hass = async_get_hass() + report_issue = async_suggest_report_issue( + hass, + integration_domain=integration_frame.integration, + module=integration_frame.module, + ) + + _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" + ), + func.__name__, + "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, + ) + + 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})" + ) + + +def protect_loop( + func: Callable[_P, _R], + strict: bool = True, + strict_core: bool = True, + check_allowed: Callable[[dict[str, Any]], bool] | None = None, +) -> Callable[_P, _R]: + """Protect function from running in event loop.""" + + @functools.wraps(func) + def protected_loop_func(*args: _P.args, **kwargs: _P.kwargs) -> _R: + check_loop( + func, + strict=strict, + strict_core=strict_core, + check_allowed=check_allowed, + args=args, + kwargs=kwargs, + ) + return func(*args, **kwargs) + + return protected_loop_func diff --git a/tests/test_block_async_io.py b/tests/test_block_async_io.py new file mode 100644 index 00000000000..688852ecf55 --- /dev/null +++ b/tests/test_block_async_io.py @@ -0,0 +1,200 @@ +"""Tests for async util methods from Python source.""" + +import importlib +import time +from unittest.mock import Mock, patch + +import pytest + +from homeassistant import block_async_io + +from tests.common import extract_stack_to_frame + + +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() + frames = extract_stack_to_frame( + [ + Mock( + filename="/home/paulus/homeassistant/.venv/blah/pydevd.py", + lineno="23", + line="do_something()", + ), + ] + ) + with ( + patch( + "homeassistant.block_async_io.get_current_frame", + return_value=frames, + ), + patch( + "homeassistant.helpers.frame.get_current_frame", + return_value=frames, + ), + ): + time.sleep(0) + assert "Detected blocking call inside the event loop" not in caplog.text + + +async def test_protect_loop_sleep(caplog: pytest.LogCaptureFixture) -> None: + """Test time.sleep not injected by the debugger raises.""" + block_async_io.enable() + frames = extract_stack_to_frame( + [ + Mock( + filename="/home/paulus/homeassistant/no_dev.py", + lineno="23", + line="do_something()", + ), + ] + ) + with ( + pytest.raises( + RuntimeError, match="Detected blocking call to sleep inside the event loop" + ), + patch( + "homeassistant.block_async_io.get_current_frame", + return_value=frames, + ), + patch( + "homeassistant.helpers.frame.get_current_frame", + return_value=frames, + ), + ): + time.sleep(0) + + +async def test_protect_loop_sleep_get_current_frame_raises( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test time.sleep when get_current_frame raises ValueError.""" + block_async_io.enable() + frames = extract_stack_to_frame( + [ + Mock( + filename="/home/paulus/homeassistant/no_dev.py", + lineno="23", + line="do_something()", + ), + ] + ) + with ( + pytest.raises( + RuntimeError, match="Detected blocking call to sleep inside the event loop" + ), + patch( + "homeassistant.block_async_io.get_current_frame", + side_effect=ValueError, + ), + patch( + "homeassistant.helpers.frame.get_current_frame", + return_value=frames, + ), + ): + time.sleep(0) + + +async def test_protect_loop_importlib_import_module_non_integration( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test import_module in the loop for non-loaded module.""" + frames = extract_stack_to_frame( + [ + Mock( + filename="/home/paulus/homeassistant/no_dev.py", + lineno="23", + line="do_something()", + ), + ] + ) + with ( + pytest.raises(ImportError), + patch.object(block_async_io, "_IN_TESTS", False), + patch( + "homeassistant.block_async_io.get_current_frame", + return_value=frames, + ), + patch( + "homeassistant.helpers.frame.get_current_frame", + return_value=frames, + ), + ): + block_async_io.enable() + importlib.import_module("not_loaded_module") + + assert "Detected blocking call to import_module" in caplog.text + + +async def test_protect_loop_importlib_import_loaded_module_non_integration( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test import_module in the loop for a loaded module.""" + frames = extract_stack_to_frame( + [ + Mock( + filename="/home/paulus/homeassistant/no_dev.py", + lineno="23", + line="do_something()", + ), + ] + ) + with ( + patch.object(block_async_io, "_IN_TESTS", False), + patch( + "homeassistant.block_async_io.get_current_frame", + return_value=frames, + ), + patch( + "homeassistant.helpers.frame.get_current_frame", + return_value=frames, + ), + ): + block_async_io.enable() + importlib.import_module("sys") + + assert "Detected blocking call to import_module" not in caplog.text + + +async def test_protect_loop_importlib_import_module_in_integration( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test import_module in the loop for non-loaded module in an integration.""" + 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 ( + pytest.raises(ImportError), + patch.object(block_async_io, "_IN_TESTS", False), + patch( + "homeassistant.block_async_io.get_current_frame", + return_value=frames, + ), + patch( + "homeassistant.helpers.frame.get_current_frame", + return_value=frames, + ), + ): + block_async_io.enable() + importlib.import_module("not_loaded_module") + + assert ( + "Detected blocking call to import_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_async.py b/tests/util/test_async.py index 50eecec72f6..d0131df88ee 100644 --- a/tests/util/test_async.py +++ b/tests/util/test_async.py @@ -6,12 +6,9 @@ from unittest.mock import MagicMock, Mock, patch import pytest -from homeassistant import block_async_io from homeassistant.core import HomeAssistant from homeassistant.util import async_ as hasync -from tests.common import extract_stack_to_frame - @patch("concurrent.futures.Future") @patch("threading.get_ident") @@ -38,172 +35,6 @@ def test_run_callback_threadsafe_from_inside_event_loop(mock_ident, _) -> None: assert len(loop.call_soon_threadsafe.mock_calls) == 2 -def banned_function(): - """Mock banned function.""" - - -async def test_check_loop_async() -> None: - """Test check_loop detects when called from event loop without integration context.""" - with pytest.raises(RuntimeError): - hasync.check_loop(banned_function) - - -async def test_check_loop_async_integration(caplog: pytest.LogCaptureFixture) -> None: - """Test check_loop detects and raises when called from event loop from integration context.""" - with ( - pytest.raises(RuntimeError), - patch( - "homeassistant.helpers.frame.linecache.getline", - return_value="self.light.is_on", - ), - patch( - "homeassistant.helpers.frame.get_current_frame", - return_value=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()", - ), - ] - ), - ), - ): - hasync.check_loop(banned_function) - assert ( - "Detected blocking call to banned_function inside the event loop by integration" - " 'hue' at homeassistant/components/hue/light.py, line 23: self.light.is_on, " - "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 - ) - - -async def test_check_loop_async_integration_non_strict( - caplog: pytest.LogCaptureFixture, -) -> None: - """Test check_loop detects when called from event loop from integration context.""" - with ( - patch( - "homeassistant.helpers.frame.linecache.getline", - return_value="self.light.is_on", - ), - patch( - "homeassistant.helpers.frame.get_current_frame", - return_value=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()", - ), - ] - ), - ), - ): - hasync.check_loop(banned_function, strict=False) - assert ( - "Detected blocking call to banned_function inside the event loop by integration" - " 'hue' at homeassistant/components/hue/light.py, line 23: self.light.is_on, " - "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 - ) - - -async def test_check_loop_async_custom(caplog: pytest.LogCaptureFixture) -> None: - """Test check_loop detects when called from event loop with custom component context.""" - with ( - pytest.raises(RuntimeError), - patch( - "homeassistant.helpers.frame.linecache.getline", - return_value="self.light.is_on", - ), - patch( - "homeassistant.helpers.frame.get_current_frame", - return_value=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()", - ), - ] - ), - ), - ): - hasync.check_loop(banned_function) - assert ( - "Detected blocking call to banned_function inside the event loop by custom " - "integration 'hue' at custom_components/hue/light.py, line 23: self.light.is_on" - ", 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 - - -def test_check_loop_sync(caplog: pytest.LogCaptureFixture) -> None: - """Test check_loop does nothing when called from thread.""" - hasync.check_loop(banned_function) - assert "Detected blocking call inside the event loop" not in caplog.text - - -def test_protect_loop_sync() -> None: - """Test protect_loop calls check_loop.""" - func = Mock() - with patch("homeassistant.util.async_.check_loop") as mock_check_loop: - hasync.protect_loop(func)(1, test=2) - mock_check_loop.assert_called_once_with(func, strict=True) - func.assert_called_once_with(1, test=2) - - -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() - - with patch( - "homeassistant.helpers.frame.get_current_frame", - return_value=extract_stack_to_frame( - [ - Mock( - filename="/home/paulus/homeassistant/.venv/blah/pydevd.py", - lineno="23", - line="do_something()", - ), - ] - ), - ): - time.sleep(0) - assert "Detected blocking call inside the event loop" not in caplog.text - - async def test_gather_with_limited_concurrency() -> None: """Test gather_with_limited_concurrency limits the number of running tasks.""" diff --git a/tests/util/test_loop.py b/tests/util/test_loop.py new file mode 100644 index 00000000000..8b4465bef2b --- /dev/null +++ b/tests/util/test_loop.py @@ -0,0 +1,200 @@ +"""Tests for async util methods from Python source.""" + +from unittest.mock import Mock, patch + +import pytest + +from homeassistant.util import loop as haloop + +from tests.common import extract_stack_to_frame + + +def banned_function(): + """Mock banned function.""" + + +async def test_check_loop_async() -> None: + """Test check_loop detects when called from event loop without integration context.""" + with pytest.raises(RuntimeError): + haloop.check_loop(banned_function) + + +async def test_check_loop_async_non_strict_core( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test non_strict_core check_loop detects from event loop without integration context.""" + haloop.check_loop(banned_function, strict_core=False) + assert "Detected blocking call to banned_function" in caplog.text + + +async def test_check_loop_async_integration(caplog: pytest.LogCaptureFixture) -> None: + """Test check_loop 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()", + ), + ] + ) + with ( + pytest.raises(RuntimeError), + patch( + "homeassistant.helpers.frame.linecache.getline", + return_value="self.light.is_on", + ), + patch( + "homeassistant.util.loop._get_line_from_cache", + return_value="mock_line", + ), + patch( + "homeassistant.util.loop.get_current_frame", + return_value=frames, + ), + patch( + "homeassistant.helpers.frame.get_current_frame", + return_value=frames, + ), + ): + haloop.check_loop(banned_function) + assert ( + "Detected blocking call to banned_function 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 + ) + + +async def test_check_loop_async_integration_non_strict( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test check_loop 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", + ), + patch( + "homeassistant.util.loop._get_line_from_cache", + return_value="mock_line", + ), + patch( + "homeassistant.util.loop.get_current_frame", + return_value=frames, + ), + patch( + "homeassistant.helpers.frame.get_current_frame", + return_value=frames, + ), + ): + haloop.check_loop(banned_function, strict=False) + assert ( + "Detected blocking call to banned_function 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 + ) + + +async def test_check_loop_async_custom(caplog: pytest.LogCaptureFixture) -> None: + """Test check_loop 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", + ), + patch( + "homeassistant.util.loop._get_line_from_cache", + return_value="mock_line", + ), + patch( + "homeassistant.util.loop.get_current_frame", + return_value=frames, + ), + patch( + "homeassistant.helpers.frame.get_current_frame", + return_value=frames, + ), + ): + haloop.check_loop(banned_function) + assert ( + "Detected blocking call to banned_function 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?" + "q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+hue%22" + ) in caplog.text + + +def test_check_loop_sync(caplog: pytest.LogCaptureFixture) -> None: + """Test check_loop does nothing when called from thread.""" + haloop.check_loop(banned_function) + assert "Detected blocking call inside the event loop" not in caplog.text + + +def test_protect_loop_sync() -> None: + """Test protect_loop calls check_loop.""" + func = Mock() + with patch("homeassistant.util.loop.check_loop") as mock_check_loop: + haloop.protect_loop(func)(1, test=2) + mock_check_loop.assert_called_once_with( + func, + strict=True, + args=(1,), + check_allowed=None, + kwargs={"test": 2}, + strict_core=True, + ) + func.assert_called_once_with(1, test=2) From 6e3e09f2c342126615c354a9e157638304e1d455 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Sun, 31 Mar 2024 09:08:07 +0200 Subject: [PATCH 104/967] Use entity & device registry mocks instead of `hass.helpers` in airthings_ble tests (#114520) --- tests/components/airthings_ble/__init__.py | 6 +-- tests/components/airthings_ble/test_sensor.py | 47 ++++++++++--------- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/tests/components/airthings_ble/__init__.py b/tests/components/airthings_ble/__init__.py index d60b42eddf2..ba213e0c2e7 100644 --- a/tests/components/airthings_ble/__init__.py +++ b/tests/components/airthings_ble/__init__.py @@ -12,7 +12,8 @@ from airthings_ble import ( from homeassistant.components.airthings_ble.const import DOMAIN from homeassistant.components.bluetooth.models import BluetoothServiceInfoBleak -from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceRegistry from tests.common import MockConfigEntry, MockEntity from tests.components.bluetooth import generate_advertisement_data, generate_ble_device @@ -232,9 +233,8 @@ def create_entry(hass): return entry -def create_device(hass, entry): +def create_device(entry: ConfigEntry, device_registry: DeviceRegistry): """Create a device for the given entry.""" - device_registry = hass.helpers.device_registry.async_get(hass) device = device_registry.async_get_or_create( config_entry_id=entry.entry_id, connections={(CONNECTION_BLUETOOTH, WAVE_SERVICE_INFO.address)}, diff --git a/tests/components/airthings_ble/test_sensor.py b/tests/components/airthings_ble/test_sensor.py index eee4de263d6..9949528ccc7 100644 --- a/tests/components/airthings_ble/test_sensor.py +++ b/tests/components/airthings_ble/test_sensor.py @@ -5,6 +5,7 @@ import logging from homeassistant.components.airthings_ble.const import DOMAIN 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 ( CO2_V1, @@ -25,18 +26,20 @@ from tests.components.bluetooth import inject_bluetooth_service_info _LOGGER = logging.getLogger(__name__) -async def test_migration_from_v1_to_v3_unique_id(hass: HomeAssistant): +async def test_migration_from_v1_to_v3_unique_id( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, +): """Verify that we can migrate from v1 (pre 2023.9.0) to the latest unique id format.""" entry = create_entry(hass) - device = create_device(hass, entry) + device = create_device(entry, device_registry) assert entry is not None assert device is not None new_unique_id = f"{WAVE_DEVICE_INFO.address}_temperature" - entity_registry = hass.helpers.entity_registry.async_get(hass) - sensor = entity_registry.async_get_or_create( domain=DOMAIN, platform=Platform.SENSOR, @@ -64,18 +67,18 @@ async def test_migration_from_v1_to_v3_unique_id(hass: HomeAssistant): assert entity_registry.async_get(sensor.entity_id).unique_id == new_unique_id -async def test_migration_from_v2_to_v3_unique_id(hass: HomeAssistant): +async def test_migration_from_v2_to_v3_unique_id( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, +): """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(hass, entry) + device = create_device(entry, device_registry) assert entry is not None assert device is not None - entity_registry = hass.helpers.entity_registry.async_get(hass) - - await hass.async_block_till_done() - sensor = entity_registry.async_get_or_create( domain=DOMAIN, platform=Platform.SENSOR, @@ -105,18 +108,18 @@ async def test_migration_from_v2_to_v3_unique_id(hass: HomeAssistant): assert entity_registry.async_get(sensor.entity_id).unique_id == new_unique_id -async def test_migration_from_v1_and_v2_to_v3_unique_id(hass: HomeAssistant): +async def test_migration_from_v1_and_v2_to_v3_unique_id( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, +): """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(hass, entry) + device = create_device(entry, device_registry) assert entry is not None assert device is not None - entity_registry = hass.helpers.entity_registry.async_get(hass) - - await hass.async_block_till_done() - v2 = entity_registry.async_get_or_create( domain=DOMAIN, platform=Platform.SENSOR, @@ -155,18 +158,18 @@ async def test_migration_from_v1_and_v2_to_v3_unique_id(hass: HomeAssistant): assert entity_registry.async_get(v2.entity_id).unique_id == CO2_V2.unique_id -async def test_migration_with_all_unique_ids(hass: HomeAssistant): +async def test_migration_with_all_unique_ids( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, +): """Test if migration works when we have all unique ids.""" entry = create_entry(hass) - device = create_device(hass, entry) + device = create_device(entry, device_registry) assert entry is not None assert device is not None - entity_registry = hass.helpers.entity_registry.async_get(hass) - - await hass.async_block_till_done() - v1 = entity_registry.async_get_or_create( domain=DOMAIN, platform=Platform.SENSOR, From d846676e841602b41b8d3309eefebf6588e73681 Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Sun, 31 Mar 2024 11:30:59 +0200 Subject: [PATCH 105/967] Enable first batch of Ruff RET rules (#114480) * Enable first batch of Ruff RET rules * disable pylint rules --- pyproject.toml | 9 ++++++++ script/translations/develop.py | 2 +- tests/components/hlk_sw16/test_config_flow.py | 5 ++-- .../homeassistant/test_exposed_entities.py | 5 ++-- tests/components/kodi/util.py | 3 +-- tests/components/matrix/conftest.py | 23 ++++++++----------- tests/components/rflink/test_init.py | 3 +-- tests/components/vesync/common.py | 19 +++++++-------- tests/components/zha/common.py | 5 +--- tests/test_data_entry_flow.py | 3 +-- 10 files changed, 35 insertions(+), 42 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 80381b09825..1e3e4c86372 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -304,6 +304,10 @@ disable = [ "use-list-literal", # C405 "useless-object-inheritance", # UP004 "useless-return", # PLR1711 + "no-else-break", # RET508 + "no-else-continue", # RET507 + "no-else-raise", # RET506 + "no-else-return", # RET505 # "no-self-use", # PLR6301 # Optional plugin, not enabled # Handled by mypy @@ -633,6 +637,7 @@ select = [ "PIE", # flake8-pie "PL", # pylint "PT", # flake8-pytest-style + "RET", # flake8-return "RSE", # flake8-raise "RUF005", # Consider iterable unpacking instead of concatenation "RUF006", # Store a reference to the return value of asyncio.create_task @@ -713,6 +718,10 @@ ignore = [ # temporarily disabled "PT019", + "RET504", + "RET503", + "RET502", + "RET501", "TRY002", "TRY301" ] diff --git a/script/translations/develop.py b/script/translations/develop.py index 14e3c320c3e..00465e1bc24 100644 --- a/script/translations/develop.py +++ b/script/translations/develop.py @@ -43,7 +43,7 @@ def flatten_translations(translations): if isinstance(v, dict): stack.append(iter(v.items())) break - elif isinstance(v, str): + if isinstance(v, str): common_key = "::".join(key_stack) flattened_translations[common_key] = v key_stack.pop() diff --git a/tests/components/hlk_sw16/test_config_flow.py b/tests/components/hlk_sw16/test_config_flow.py index e4770343114..e8c0d36c81c 100644 --- a/tests/components/hlk_sw16/test_config_flow.py +++ b/tests/components/hlk_sw16/test_config_flow.py @@ -32,9 +32,8 @@ class MockSW16Client: if self.disconnect_callback: self.disconnect_callback() return await self.active_transaction - else: - self.active_transaction.set_result(True) - return self.active_transaction + self.active_transaction.set_result(True) + return self.active_transaction def stop(self): """Mock client stop.""" diff --git a/tests/components/homeassistant/test_exposed_entities.py b/tests/components/homeassistant/test_exposed_entities.py index e20fcb69d00..9a14198b1ef 100644 --- a/tests/components/homeassistant/test_exposed_entities.py +++ b/tests/components/homeassistant/test_exposed_entities.py @@ -32,10 +32,9 @@ def entities_fixture( """Set up the test environment.""" if request.param == "entities_unique_id": return entities_unique_id(entity_registry) - elif request.param == "entities_no_unique_id": + if request.param == "entities_no_unique_id": return entities_no_unique_id(hass) - else: - raise RuntimeError("Invalid setup fixture") + raise RuntimeError("Invalid setup fixture") def entities_unique_id(entity_registry: er.EntityRegistry) -> dict[str, str]: diff --git a/tests/components/kodi/util.py b/tests/components/kodi/util.py index dba0822b1d8..6217a77903b 100644 --- a/tests/components/kodi/util.py +++ b/tests/components/kodi/util.py @@ -57,8 +57,7 @@ def get_kodi_connection( """Get Kodi connection.""" if ws_port is None: return MockConnection() - else: - return MockWSConnection() + return MockWSConnection() class MockConnection: diff --git a/tests/components/matrix/conftest.py b/tests/components/matrix/conftest.py index b3fefe3ac67..2c24f4d0e75 100644 --- a/tests/components/matrix/conftest.py +++ b/tests/components/matrix/conftest.py @@ -84,14 +84,12 @@ class _MockAsyncClient(AsyncClient): return RoomResolveAliasResponse( room_alias=room_alias, room_id=room_id, servers=[TEST_HOMESERVER] ) - else: - return RoomResolveAliasError(message=f"Could not resolve {room_alias}") + return RoomResolveAliasError(message=f"Could not resolve {room_alias}") async def join(self, room_id: RoomID): if room_id in TEST_JOINABLE_ROOMS.values(): return JoinResponse(room_id=room_id) - else: - return JoinError(message="Not allowed to join this room.") + return JoinError(message="Not allowed to join this room.") async def login(self, *args, **kwargs): if kwargs.get("password") == TEST_PASSWORD or kwargs.get("token") == TEST_TOKEN: @@ -101,9 +99,8 @@ class _MockAsyncClient(AsyncClient): device_id="test_device", user_id=TEST_MXID, ) - else: - self.access_token = "" - return LoginError(message="LoginError", status_code="status_code") + self.access_token = "" + return LoginError(message="LoginError", status_code="status_code") async def logout(self, *args, **kwargs): self.access_token = "" @@ -115,19 +112,17 @@ class _MockAsyncClient(AsyncClient): return WhoamiResponse( user_id=TEST_MXID, device_id=TEST_DEVICE_ID, is_guest=False ) - else: - self.access_token = "" - return WhoamiError( - message="Invalid access token passed.", status_code="M_UNKNOWN_TOKEN" - ) + self.access_token = "" + return WhoamiError( + message="Invalid access token passed.", status_code="M_UNKNOWN_TOKEN" + ) async def room_send(self, *args, **kwargs): if not self.logged_in: raise LocalProtocolError if kwargs["room_id"] not in TEST_JOINABLE_ROOMS.values(): return ErrorResponse(message="Cannot send a message in this room.") - else: - return Response() + return Response() async def sync(self, *args, **kwargs): return None diff --git a/tests/components/rflink/test_init.py b/tests/components/rflink/test_init.py index 67f6aa5e6f6..8f09c4a2e54 100644 --- a/tests/components/rflink/test_init.py +++ b/tests/components/rflink/test_init.py @@ -57,8 +57,7 @@ async def mock_rflink( if fail: raise ConnectionRefusedError - else: - return transport, protocol + return transport, protocol mock_create = Mock(wraps=create_rflink_connection) monkeypatch.setattr( diff --git a/tests/components/vesync/common.py b/tests/components/vesync/common.py index 23c57177ddd..94e1511ce19 100644 --- a/tests/components/vesync/common.py +++ b/tests/components/vesync/common.py @@ -75,22 +75,21 @@ def call_api_side_effect__no_devices(*args, **kwargs): """Build a side_effects method for the Helpers.call_api method.""" if args[0] == "/cloud/v1/user/login" and args[1] == "post": return json.loads(load_fixture("vesync_api_call__login.json", "vesync")), 200 - elif args[0] == "/cloud/v1/deviceManaged/devices" and args[1] == "post": + if args[0] == "/cloud/v1/deviceManaged/devices" and args[1] == "post": return ( json.loads( load_fixture("vesync_api_call__devices__no_devices.json", "vesync") ), 200, ) - else: - raise ValueError(f"Unhandled API call args={args}, kwargs={kwargs}") + raise ValueError(f"Unhandled API call args={args}, kwargs={kwargs}") def call_api_side_effect__single_humidifier(*args, **kwargs): """Build a side_effects method for the Helpers.call_api method.""" if args[0] == "/cloud/v1/user/login" and args[1] == "post": return json.loads(load_fixture("vesync_api_call__login.json", "vesync")), 200 - elif args[0] == "/cloud/v1/deviceManaged/devices" and args[1] == "post": + if args[0] == "/cloud/v1/deviceManaged/devices" and args[1] == "post": return ( json.loads( load_fixture( @@ -99,7 +98,7 @@ def call_api_side_effect__single_humidifier(*args, **kwargs): ), 200, ) - elif args[0] == "/cloud/v2/deviceManaged/bypassV2" and kwargs["method"] == "post": + if args[0] == "/cloud/v2/deviceManaged/bypassV2" and kwargs["method"] == "post": return ( json.loads( load_fixture( @@ -108,22 +107,21 @@ def call_api_side_effect__single_humidifier(*args, **kwargs): ), 200, ) - else: - raise ValueError(f"Unhandled API call args={args}, kwargs={kwargs}") + raise ValueError(f"Unhandled API call args={args}, kwargs={kwargs}") def call_api_side_effect__single_fan(*args, **kwargs): """Build a side_effects method for the Helpers.call_api method.""" if args[0] == "/cloud/v1/user/login" and args[1] == "post": return json.loads(load_fixture("vesync_api_call__login.json", "vesync")), 200 - elif args[0] == "/cloud/v1/deviceManaged/devices" and args[1] == "post": + if args[0] == "/cloud/v1/deviceManaged/devices" and args[1] == "post": return ( json.loads( load_fixture("vesync_api_call__devices__single_fan.json", "vesync") ), 200, ) - elif ( + if ( args[0] == "/131airPurifier/v1/device/deviceDetail" and kwargs["method"] == "post" ): @@ -135,5 +133,4 @@ def call_api_side_effect__single_fan(*args, **kwargs): ), 200, ) - else: - raise ValueError(f"Unhandled API call args={args}, kwargs={kwargs}") + raise ValueError(f"Unhandled API call args={args}, kwargs={kwargs}") diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 63d3e9cf747..6cda8b98e1e 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -244,10 +244,7 @@ def patch_zha_config(component: str, overrides: dict[tuple[str, str], Any]): def new_get_config(config_entry, section, config_key, default): if (section, config_key) in overrides: return overrides[section, config_key] - else: - return async_get_zha_config_value( - config_entry, section, config_key, default - ) + return async_get_zha_config_value(config_entry, section, config_key, default) return patch( f"homeassistant.components.zha.{component}.async_get_zha_config_value", diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index c8c6b21951d..edba232eb69 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -269,8 +269,7 @@ async def test_finish_callback_change_result_type(hass: HomeAssistant) -> None: return flow.async_show_form( step_id="init", data_schema=vol.Schema({"count": int}) ) - else: - result["result"] = result["data"]["count"] + result["result"] = result["data"]["count"] return result manager = FlowManager(hass) From f2f24a5d359dc34839f3ed314ac7496d50bfea73 Mon Sep 17 00:00:00 2001 From: dotvav Date: Sun, 31 Mar 2024 11:38:59 +0200 Subject: [PATCH 106/967] Fix Overkiz Hitachi OVP air-to-air heat pump (#114487) Unpack command parameters instead of passing a list --- .../climate_entities/hitachi_air_to_air_heat_pump_ovp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/overkiz/climate_entities/hitachi_air_to_air_heat_pump_ovp.py b/homeassistant/components/overkiz/climate_entities/hitachi_air_to_air_heat_pump_ovp.py index 86cde4fc4db..b4d6ab788a1 100644 --- a/homeassistant/components/overkiz/climate_entities/hitachi_air_to_air_heat_pump_ovp.py +++ b/homeassistant/components/overkiz/climate_entities/hitachi_air_to_air_heat_pump_ovp.py @@ -357,5 +357,5 @@ class HitachiAirToAirHeatPumpOVP(OverkizEntity, ClimateEntity): ] await self.executor.async_execute_command( - OverkizCommand.GLOBAL_CONTROL, command_data + OverkizCommand.GLOBAL_CONTROL, *command_data ) From d5da0a053b59d5aa43ec36e69596a92fb594376c Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Sun, 31 Mar 2024 11:44:11 +0200 Subject: [PATCH 107/967] Deprecate `hass.helpers` (#114484) * Deprecate hass.helpers * Patch * Patch _REPORTED_INTEGRATIONS set in test * Fix test * Update version --- homeassistant/loader.py | 14 ++++++++++++++ tests/test_loader.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index f462ea16886..722f3fd83c7 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -1550,6 +1550,20 @@ class Helpers: def __getattr__(self, helper_name: str) -> ModuleWrapper: """Fetch a helper.""" helper = importlib.import_module(f"homeassistant.helpers.{helper_name}") + + # Local import to avoid circular dependencies + from .helpers.frame import report # pylint: disable=import-outside-toplevel + + report( + ( + f"accesses hass.helpers.{helper_name}." + " This is deprecated and will stop working in Home Assistant 2024.11, it" + f" should be updated to import functions used from {helper_name} directly" + ), + error_if_core=False, + log_custom_component_only=True, + ) + wrapped = ModuleWrapper(self._hass, helper) setattr(self, helper_name, wrapped) return wrapped diff --git a/tests/test_loader.py b/tests/test_loader.py index 9e191ee9e00..4442fe5fd82 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -1779,3 +1779,34 @@ async def test_has_services(hass: HomeAssistant, enable_custom_integrations) -> assert integration.has_services is False integration = await loader.async_get_integration(hass, "test_with_services") assert integration.has_services is True + + +async def test_hass_helpers_use_reported( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_integration_frame: Mock +) -> None: + """Test that use of hass.components is reported.""" + integration_frame = frame.IntegrationFrame( + custom_integration=True, + _frame=mock_integration_frame, + integration="test_integration_frame", + module="custom_components.test_integration_frame", + relative_filename="custom_components/test_integration_frame/__init__.py", + ) + + with ( + patch.object(frame, "_REPORTED_INTEGRATIONS", new=set()), + patch( + "homeassistant.helpers.frame.get_integration_frame", + return_value=integration_frame, + ), + patch( + "homeassistant.helpers.aiohttp_client.async_get_clientsession", + return_value=None, + ), + ): + hass.helpers.aiohttp_client.async_get_clientsession() + + assert ( + "Detected that custom integration 'test_integration_frame' " + "accesses hass.helpers.aiohttp_client. This is deprecated" + ) in caplog.text From 52741d711473090104141a994256f09f3f091d7a Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 31 Mar 2024 15:47:24 +0200 Subject: [PATCH 108/967] Add single config entry to Analytics insights (#114427) * Add single config entry to Analytics insights * Add single config entry to Analytics insights * Add single config entry to Analytics insights --- homeassistant/components/analytics_insights/config_flow.py | 1 - homeassistant/components/analytics_insights/manifest.json | 3 ++- homeassistant/components/analytics_insights/strings.json | 3 +-- homeassistant/generated/integrations.json | 3 ++- tests/components/analytics_insights/test_config_flow.py | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/analytics_insights/config_flow.py b/homeassistant/components/analytics_insights/config_flow.py index 30b8ca12579..cef5ac2e9e5 100644 --- a/homeassistant/components/analytics_insights/config_flow.py +++ b/homeassistant/components/analytics_insights/config_flow.py @@ -53,7 +53,6 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle the initial step.""" - self._async_abort_entries_match() errors: dict[str, str] = {} if user_input is not None: if not user_input.get(CONF_TRACKED_INTEGRATIONS) and not user_input.get( diff --git a/homeassistant/components/analytics_insights/manifest.json b/homeassistant/components/analytics_insights/manifest.json index d33bb23b1b7..adf2d634ef8 100644 --- a/homeassistant/components/analytics_insights/manifest.json +++ b/homeassistant/components/analytics_insights/manifest.json @@ -7,5 +7,6 @@ "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["python_homeassistant_analytics"], - "requirements": ["python-homeassistant-analytics==0.6.0"] + "requirements": ["python-homeassistant-analytics==0.6.0"], + "single_config_entry": true } diff --git a/homeassistant/components/analytics_insights/strings.json b/homeassistant/components/analytics_insights/strings.json index 6de1ab9dbe4..00c9cfa4404 100644 --- a/homeassistant/components/analytics_insights/strings.json +++ b/homeassistant/components/analytics_insights/strings.json @@ -13,8 +13,7 @@ } }, "abort": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, "error": { "no_integration_selected": "You must select at least one integration to track" diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 631c8b1e73c..b4eff321e6e 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -271,7 +271,8 @@ "name": "Home Assistant Analytics Insights", "integration_type": "service", "config_flow": true, - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "single_config_entry": true }, "android_ip_webcam": { "name": "Android IP Webcam", diff --git a/tests/components/analytics_insights/test_config_flow.py b/tests/components/analytics_insights/test_config_flow.py index 49ec0ce8d52..16ca0812d7d 100644 --- a/tests/components/analytics_insights/test_config_flow.py +++ b/tests/components/analytics_insights/test_config_flow.py @@ -162,7 +162,7 @@ async def test_form_already_configured( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "already_configured" + assert result["reason"] == "single_instance_allowed" @pytest.mark.parametrize( From 8ebdd46509c8e1465c461ec105cf32a1a83bd96d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 31 Mar 2024 07:41:06 -1000 Subject: [PATCH 109/967] Bump aiodns to 3.2.0 (#114527) changelog: https://github.com/saghul/aiodns/compare/v3.1.1...v3.2.0 --- homeassistant/components/dnsip/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/dnsip/manifest.json b/homeassistant/components/dnsip/manifest.json index 17c0677e4d9..d25459b95b7 100644 --- a/homeassistant/components/dnsip/manifest.json +++ b/homeassistant/components/dnsip/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dnsip", "iot_class": "cloud_polling", - "requirements": ["aiodns==3.1.1"] + "requirements": ["aiodns==3.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index d3cb743c8c5..bc0b881b4c3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -224,7 +224,7 @@ aiodhcpwatcher==1.0.0 aiodiscover==2.0.0 # homeassistant.components.dnsip -aiodns==3.1.1 +aiodns==3.2.0 # homeassistant.components.eafm aioeafm==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a5cf2efa79c..b914a8f911f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -203,7 +203,7 @@ aiodhcpwatcher==1.0.0 aiodiscover==2.0.0 # homeassistant.components.dnsip -aiodns==3.1.1 +aiodns==3.2.0 # homeassistant.components.eafm aioeafm==0.1.2 From 7919ca63d06a3e1ae5ac1131ec38468bb4827ddb Mon Sep 17 00:00:00 2001 From: wittypluck Date: Sun, 31 Mar 2024 20:04:39 +0200 Subject: [PATCH 110/967] Add uptime sensor to Glances (#111402) * Add uptime sensor to Glances * Merge upstream * Merge upstream * Fix coverage * Add uptime sensor to Glances * Merge upstream * Merge upstream * Fix coverage * Move most uptime specific code to DataUpdateCoordinator * Add last_reported after merge with upstream * Add unit tests for uptime sensor * Add unit tests for uptime sensor * Add unit tests for uptime sensor * Add unit tests for uptime sensor * Move update code out of getter native_value() * Add unit tests for uptime sensor * Update uptime method signatures * Set uptime icon in icons.json * Use freezer.tick for uptime tests * Frozen time test fails on github * Add MIN_UPTIME_VARIATION const value * Only update uptime on startup or when remote server restarts * Fix for 0 values * Set value to None to set state to Unknown if key is not found * Add unit test for uptime change * Code reduction --------- Co-authored-by: J. Nick Koston Co-authored-by: G Johansson --- .../components/glances/coordinator.py | 14 ++++++ homeassistant/components/glances/icons.json | 3 ++ homeassistant/components/glances/sensor.py | 34 ++++++++----- homeassistant/components/glances/strings.json | 3 ++ tests/components/glances/__init__.py | 4 ++ .../glances/snapshots/test_sensor.ambr | 47 ++++++++++++++++++ tests/components/glances/test_sensor.py | 49 +++++++++++++++++-- 7 files changed, 139 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/glances/coordinator.py b/homeassistant/components/glances/coordinator.py index 9a1b281eec2..4e5bdcc1543 100644 --- a/homeassistant/components/glances/coordinator.py +++ b/homeassistant/components/glances/coordinator.py @@ -1,5 +1,6 @@ """Coordinator for Glances integration.""" +from datetime import datetime, timedelta import logging from typing import Any @@ -10,6 +11,7 @@ from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util.dt import parse_duration, utcnow from .const import DEFAULT_SCAN_INTERVAL, DOMAIN @@ -42,4 +44,16 @@ class GlancesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): raise ConfigEntryAuthFailed from err except exceptions.GlancesApiError as err: raise UpdateFailed from err + # Update computed values + uptime: datetime | None = self.data["computed"]["uptime"] if self.data else None + up_duration: timedelta | None = None + if up_duration := parse_duration(data.get("uptime")): + # Update uptime if previous value is None or previous uptime is bigger than + # new uptime (i.e. server restarted) + if ( + self.data is None + or self.data["computed"]["uptime_duration"] > up_duration + ): + uptime = utcnow() - up_duration + data["computed"] = {"uptime_duration": up_duration, "uptime": uptime} return data or {} diff --git a/homeassistant/components/glances/icons.json b/homeassistant/components/glances/icons.json index 6a8c2fa728c..06f8cd98a07 100644 --- a/homeassistant/components/glances/icons.json +++ b/homeassistant/components/glances/icons.json @@ -45,6 +45,9 @@ }, "raid_used": { "default": "mdi:harddisk" + }, + "uptime": { + "default": "mdi:clock-time-eight-outline" } } } diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index 7db06a08496..5c22154aeef 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -3,14 +3,12 @@ from __future__ import annotations from dataclasses import dataclass -from typing import cast from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, SensorStateClass, - StateType, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -19,7 +17,7 @@ from homeassistant.const import ( UnitOfInformation, UnitOfTemperature, ) -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 from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -212,6 +210,12 @@ SENSOR_TYPES = { translation_key="raid_used", state_class=SensorStateClass.MEASUREMENT, ), + ("computed", "uptime"): GlancesSensorEntityDescription( + key="uptime", + type="computed", + translation_key="uptime", + device_class=SensorDeviceClass.TIMESTAMP, + ), } @@ -276,6 +280,7 @@ class GlancesSensor(CoordinatorEntity[GlancesDataUpdateCoordinator], SensorEntit self._attr_unique_id = ( f"{coordinator.config_entry.entry_id}-{sensor_label}-{description.key}" ) + self._update_native_value() @property def available(self) -> bool: @@ -289,13 +294,18 @@ class GlancesSensor(CoordinatorEntity[GlancesDataUpdateCoordinator], SensorEntit ) return False - @property - def native_value(self) -> StateType: - """Return the state of the resources.""" - value = self.coordinator.data[self.entity_description.type] + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._update_native_value() + super()._handle_coordinator_update() - if isinstance(value.get(self._sensor_label), dict): - return cast( - StateType, value[self._sensor_label][self.entity_description.key] - ) - return cast(StateType, value[self.entity_description.key]) + def _update_native_value(self) -> None: + """Update sensor native value from coordinator data.""" + data = self.coordinator.data[self.entity_description.type] + if dict_val := data.get(self._sensor_label): + self._attr_native_value = dict_val.get(self.entity_description.key) + elif self.entity_description.key in data: + self._attr_native_value = data.get(self.entity_description.key) + else: + self._attr_native_value = None diff --git a/homeassistant/components/glances/strings.json b/homeassistant/components/glances/strings.json index b0b535ce8ed..10a4cb7ed00 100644 --- a/homeassistant/components/glances/strings.json +++ b/homeassistant/components/glances/strings.json @@ -100,6 +100,9 @@ }, "raid_used": { "name": "{sensor_label} used" + }, + "uptime": { + "name": "Uptime" } } }, diff --git a/tests/components/glances/__init__.py b/tests/components/glances/__init__.py index f0f1fe01796..fd0df3be3a9 100644 --- a/tests/components/glances/__init__.py +++ b/tests/components/glances/__init__.py @@ -1,5 +1,6 @@ """Tests for Glances.""" +from datetime import datetime from typing import Any MOCK_USER_INPUT: dict[str, Any] = { @@ -173,6 +174,8 @@ MOCK_DATA = { "uptime": "3 days, 10:25:20", } +MOCK_REFERENCE_DATE: datetime = datetime.fromisoformat("2024-02-13T14:13:12") + HA_SENSOR_DATA: dict[str, Any] = { "fs": { "/ssl": {"disk_use": 30.7, "disk_use_percent": 6.7, "disk_free": 426.5}, @@ -207,4 +210,5 @@ HA_SENSOR_DATA: dict[str, Any] = { "config": "UU", }, }, + "uptime": "3 days, 10:25:20", } diff --git a/tests/components/glances/snapshots/test_sensor.ambr b/tests/components/glances/snapshots/test_sensor.ambr index 23242f66071..cf74e91f613 100644 --- a/tests/components/glances/snapshots/test_sensor.ambr +++ b/tests/components/glances/snapshots/test_sensor.ambr @@ -954,3 +954,50 @@ 'state': '30.7', }) # --- +# name: test_sensor_states[sensor.0_0_0_0_uptime-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.0_0_0_0_uptime', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Uptime', + 'platform': 'glances', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'uptime', + 'unique_id': 'test--uptime', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_uptime-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': '0.0.0.0 Uptime', + }), + 'context': , + 'entity_id': 'sensor.0_0_0_0_uptime', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-02-10T03:47:52+00:00', + }) +# --- diff --git a/tests/components/glances/test_sensor.py b/tests/components/glances/test_sensor.py index ebe8b75b618..7dee47680ed 100644 --- a/tests/components/glances/test_sensor.py +++ b/tests/components/glances/test_sensor.py @@ -1,25 +1,36 @@ """Tests for glances sensors.""" +from datetime import timedelta +from unittest.mock import AsyncMock + +from freezegun.api import FrozenDateTimeFactory from syrupy import SnapshotAssertion from homeassistant.components.glances.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import MOCK_USER_INPUT +from . import HA_SENSOR_DATA, MOCK_REFERENCE_DATE, MOCK_USER_INPUT -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed async def test_sensor_states( - hass: HomeAssistant, snapshot: SnapshotAssertion, entity_registry: er.EntityRegistry + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + freezer: FrozenDateTimeFactory, ) -> None: """Test sensor states are correctly collected from library.""" + freezer.move_to(MOCK_REFERENCE_DATE) + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT, entry_id="test") entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + entity_entries = er.async_entries_for_config_entry(entity_registry, entry.entry_id) assert entity_entries @@ -28,3 +39,35 @@ async def test_sensor_states( assert hass.states.get(entity_entry.entity_id) == snapshot( name=f"{entity_entry.entity_id}-state" ) + + +async def test_uptime_variation( + hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_api: AsyncMock +) -> None: + """Test uptime small variation update.""" + + # Init with reference time + freezer.move_to(MOCK_REFERENCE_DATE) + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT, entry_id="test") + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + uptime_state = hass.states.get("sensor.0_0_0_0_uptime").state + + # Time change should not change uptime (absolute date) + freezer.tick(delta=timedelta(seconds=120)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + uptime_state2 = hass.states.get("sensor.0_0_0_0_uptime").state + assert uptime_state2 == uptime_state + + mock_data = HA_SENSOR_DATA.copy() + mock_data["uptime"] = "1:25:20" + mock_api.return_value.get_ha_sensor_data = AsyncMock(return_value=mock_data) + + # Server has been restarted so therefore we should have a new state + freezer.move_to(MOCK_REFERENCE_DATE + timedelta(days=2)) + freezer.tick(delta=timedelta(seconds=120)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert hass.states.get("sensor.0_0_0_0_uptime").state == "2024-02-15T12:49:52+00:00" From 5eb4cf6a0569cde13888c11f79f372f88cb6358f Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Sun, 31 Mar 2024 20:06:30 +0200 Subject: [PATCH 111/967] Add error sensor for Husqvarna Automower (#113165) * Add error sensor for Husqvarna Automower * Apply suggestions from code review Co-authored-by: Jan-Philipp Benecke * address review * Add restricted reason sensor * ruff * pin options --------- Co-authored-by: Jan-Philipp Benecke --- .../components/husqvarna_automower/icons.json | 6 + .../components/husqvarna_automower/sensor.py | 177 +++++++- .../husqvarna_automower/strings.json | 143 ++++++ .../snapshots/test_sensor.ambr | 410 ++++++++++++++++++ .../husqvarna_automower/test_sensor.py | 25 ++ 5 files changed, 760 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/husqvarna_automower/icons.json b/homeassistant/components/husqvarna_automower/icons.json index 65cc85bd09b..ec11ef92d08 100644 --- a/homeassistant/components/husqvarna_automower/icons.json +++ b/homeassistant/components/husqvarna_automower/icons.json @@ -14,11 +14,17 @@ } }, "sensor": { + "error": { + "default": "mdi:alert-circle-outline" + }, "number_of_charging_cycles": { "default": "mdi:battery-sync-outline" }, "number_of_collisions": { "default": "mdi:counter" + }, + "restricted_reason": { + "default": "mdi:tooltip-question" } } } diff --git a/homeassistant/components/husqvarna_automower/sensor.py b/homeassistant/components/husqvarna_automower/sensor.py index e054d02e3ba..10aec9b1536 100644 --- a/homeassistant/components/husqvarna_automower/sensor.py +++ b/homeassistant/components/husqvarna_automower/sensor.py @@ -5,7 +5,7 @@ from dataclasses import dataclass from datetime import datetime import logging -from aioautomower.model import MowerAttributes, MowerModes +from aioautomower.model import MowerAttributes, MowerModes, RestrictedReasons from homeassistant.components.sensor import ( SensorDeviceClass, @@ -26,6 +26,165 @@ from .entity import AutomowerBaseEntity _LOGGER = logging.getLogger(__name__) +ERROR_KEY_LIST = [ + "no_error", + "alarm_mower_in_motion", + "alarm_mower_lifted", + "alarm_mower_stopped", + "alarm_mower_switched_off", + "alarm_mower_tilted", + "alarm_outside_geofence", + "angular_sensor_problem", + "battery_problem", + "battery_problem", + "battery_restriction_due_to_ambient_temperature", + "can_error", + "charging_current_too_high", + "charging_station_blocked", + "charging_system_problem", + "charging_system_problem", + "collision_sensor_defect", + "collision_sensor_error", + "collision_sensor_problem_front", + "collision_sensor_problem_rear", + "com_board_not_available", + "communication_circuit_board_sw_must_be_updated", + "complex_working_area", + "connection_changed", + "connection_not_changed", + "connectivity_problem", + "connectivity_problem", + "connectivity_problem", + "connectivity_problem", + "connectivity_problem", + "connectivity_problem", + "connectivity_settings_restored", + "cutting_drive_motor_1_defect", + "cutting_drive_motor_2_defect", + "cutting_drive_motor_3_defect", + "cutting_height_blocked", + "cutting_height_problem", + "cutting_height_problem_curr", + "cutting_height_problem_dir", + "cutting_height_problem_drive", + "cutting_motor_problem", + "cutting_stopped_slope_too_steep", + "cutting_system_blocked", + "cutting_system_blocked", + "cutting_system_imbalance_warning", + "cutting_system_major_imbalance", + "destination_not_reachable", + "difficult_finding_home", + "docking_sensor_defect", + "electronic_problem", + "empty_battery", + "folding_cutting_deck_sensor_defect", + "folding_sensor_activated", + "geofence_problem", + "geofence_problem", + "gps_navigation_problem", + "guide_1_not_found", + "guide_2_not_found", + "guide_3_not_found", + "guide_calibration_accomplished", + "guide_calibration_failed", + "high_charging_power_loss", + "high_internal_power_loss", + "high_internal_temperature", + "internal_voltage_error", + "invalid_battery_combination_invalid_combination_of_different_battery_types", + "invalid_sub_device_combination", + "invalid_system_configuration", + "left_brush_motor_overloaded", + "lift_sensor_defect", + "lifted", + "limited_cutting_height_range", + "limited_cutting_height_range", + "loop_sensor_defect", + "loop_sensor_problem_front", + "loop_sensor_problem_left", + "loop_sensor_problem_rear", + "loop_sensor_problem_right", + "low_battery", + "memory_circuit_problem", + "mower_lifted", + "mower_tilted", + "no_accurate_position_from_satellites", + "no_confirmed_position", + "no_drive", + "no_loop_signal", + "no_power_in_charging_station", + "no_response_from_charger", + "outside_working_area", + "poor_signal_quality", + "reference_station_communication_problem", + "right_brush_motor_overloaded", + "safety_function_faulty", + "settings_restored", + "sim_card_locked", + "sim_card_locked", + "sim_card_locked", + "sim_card_locked", + "sim_card_not_found", + "sim_card_requires_pin", + "slipped_mower_has_slipped_situation_not_solved_with_moving_pattern", + "slope_too_steep", + "sms_could_not_be_sent", + "stop_button_problem", + "stuck_in_charging_station", + "switch_cord_problem", + "temporary_battery_problem", + "temporary_battery_problem", + "temporary_battery_problem", + "temporary_battery_problem", + "temporary_battery_problem", + "temporary_battery_problem", + "temporary_battery_problem", + "temporary_battery_problem", + "tilt_sensor_problem", + "too_high_discharge_current", + "too_high_internal_current", + "trapped", + "ultrasonic_problem", + "ultrasonic_sensor_1_defect", + "ultrasonic_sensor_2_defect", + "ultrasonic_sensor_3_defect", + "ultrasonic_sensor_4_defect", + "unexpected_cutting_height_adj", + "unexpected_error", + "upside_down", + "weak_gps_signal", + "wheel_drive_problem_left", + "wheel_drive_problem_rear_left", + "wheel_drive_problem_rear_right", + "wheel_drive_problem_right", + "wheel_motor_blocked_left", + "wheel_motor_blocked_rear_left", + "wheel_motor_blocked_rear_right", + "wheel_motor_blocked_right", + "wheel_motor_overloaded_left", + "wheel_motor_overloaded_rear_left", + "wheel_motor_overloaded_rear_right", + "wheel_motor_overloaded_right", + "work_area_not_valid", + "wrong_loop_signal", + "wrong_pin_code", + "zone_generator_problem", +] + +RESTRICTED_REASONS: list = [ + RestrictedReasons.ALL_WORK_AREAS_COMPLETED.lower(), + RestrictedReasons.DAILY_LIMIT.lower(), + RestrictedReasons.EXTERNAL.lower(), + RestrictedReasons.FOTA.lower(), + RestrictedReasons.FROST.lower(), + RestrictedReasons.NONE.lower(), + RestrictedReasons.NOT_APPLICABLE.lower(), + RestrictedReasons.PARK_OVERRIDE.lower(), + RestrictedReasons.SENSOR.lower(), + RestrictedReasons.WEEK_SCHEDULE.lower(), +] + @dataclass(frozen=True, kw_only=True) class AutomowerSensorEntityDescription(SensorEntityDescription): @@ -141,6 +300,22 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda data: dt_util.as_local(data.planner.next_start_datetime), ), + AutomowerSensorEntityDescription( + key="error", + translation_key="error", + device_class=SensorDeviceClass.ENUM, + 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, + value_fn=lambda data: data.planner.restricted_reason.lower(), + ), ) diff --git a/homeassistant/components/husqvarna_automower/strings.json b/homeassistant/components/husqvarna_automower/strings.json index 8032c670404..0a2d3685c6e 100644 --- a/homeassistant/components/husqvarna_automower/strings.json +++ b/homeassistant/components/husqvarna_automower/strings.json @@ -49,6 +49,134 @@ } }, "sensor": { + "error": { + "name": "Error", + "state": { + "alarm_mower_in_motion": "Alarm! Mower in motion", + "alarm_mower_lifted": "Alarm! Mower lifted", + "alarm_mower_stopped": "Alarm! Mower stopped", + "alarm_mower_switched_off": "Alarm! Mower switched off", + "alarm_mower_tilted": "Alarm! Mower tilted", + "alarm_outside_geofence": "Alarm! Outside geofence", + "angular_sensor_problem": "Angular sensor problem", + "battery_problem": "Battery problem", + "battery_restriction_due_to_ambient_temperature": "Battery restriction due to ambient temperature", + "can_error": "CAN error", + "charging_current_too_high": "Charging current too high", + "charging_station_blocked": "Charging station blocked", + "charging_system_problem": "Charging system problem", + "collision_sensor_defect": "Collision sensor defect", + "collision_sensor_error": "Collision sensor error", + "collision_sensor_problem_front": "Front collision sensor problem", + "collision_sensor_problem_rear": "Rear collision sensor problem", + "com_board_not_available": "Com board not available", + "communication_circuit_board_sw_must_be_updated": "Communication circuit board software must be updated", + "complex_working_area": "Complex working area", + "connection_changed": "Connection changed", + "connection_not_changed": "Connection NOT changed", + "connectivity_problem": "Connectivity problem", + "connectivity_settings_restored": "Connectivity settings restored", + "cutting_drive_motor_1_defect": "Cutting drive motor 1 defect", + "cutting_drive_motor_2_defect": "Cutting drive motor 2 defect", + "cutting_drive_motor_3_defect": "Cutting drive motor 3 defect", + "cutting_height_blocked": "Cutting height blocked", + "cutting_height_problem": "Cutting height problem", + "cutting_height_problem_curr": "Cutting height problem, curr", + "cutting_height_problem_dir": "Cutting height problem, dir", + "cutting_height_problem_drive": "Cutting height problem, drive", + "cutting_motor_problem": "Cutting motor problem", + "cutting_stopped_slope_too_steep": "Cutting stopped - slope too steep", + "cutting_system_blocked": "Cutting system blocked", + "cutting_system_imbalance_warning": "Cutting system imbalance", + "cutting_system_major_imbalance": "Cutting system major imbalance", + "destination_not_reachable": "Destination not reachable", + "difficult_finding_home": "Difficult finding home", + "docking_sensor_defect": "Docking sensor defect", + "electronic_problem": "Electronic problem", + "empty_battery": "Empty battery", + "folding_cutting_deck_sensor_defect": "Folding cutting deck sensor defect", + "folding_sensor_activated": "Folding sensor activated", + "geofence_problem": "Geofence problem", + "gps_navigation_problem": "GPS navigation problem", + "guide_1_not_found": "Guide 1 not found", + "guide_2_not_found": "Guide 2 not found", + "guide_3_not_found": "Guide 3 not found", + "guide_calibration_accomplished": "Guide calibration accomplished", + "guide_calibration_failed": "Guide calibration failed", + "high_charging_power_loss": "High charging power loss", + "high_internal_power_loss": "High internal power loss", + "high_internal_temperature": "High internal temperature", + "internal_voltage_error": "Internal voltage error", + "invalid_battery_combination_invalid_combination_of_different_battery_types": "Invalid battery combination - Invalid combination of different battery types.", + "invalid_sub_device_combination": "Invalid sub-device combination", + "invalid_system_configuration": "Invalid system configuration", + "left_brush_motor_overloaded": "Left brush motor overloaded", + "lift_sensor_defect": "Lift Sensor defect", + "lifted": "Lifted", + "limited_cutting_height_range": "Limited cutting height range", + "loop_sensor_defect": "Loop sensor defect", + "loop_sensor_problem_front": "Front loop sensor problem", + "loop_sensor_problem_left": "Left loop sensor problem", + "loop_sensor_problem_rear": "Rear loop sensor problem", + "loop_sensor_problem_right": "Right loop sensor problem", + "low_battery": "Low battery", + "memory_circuit_problem": "Memory circuit problem", + "mower_lifted": "Mower lifted", + "mower_tilted": "Mower tilted", + "no_accurate_position_from_satellites": "No accurate position from satellites", + "no_confirmed_position": "No confirmed position", + "no_drive": "No drive", + "no_error": "No error", + "no_loop_signal": "No loop signal", + "no_power_in_charging_station": "No power in charging station", + "no_response_from_charger": "No response from charger", + "outside_working_area": "Outside working area", + "poor_signal_quality": "Poor signal quality", + "reference_station_communication_problem": "Reference station communication problem", + "right_brush_motor_overloaded": "Right brush motor overloaded", + "safety_function_faulty": "Safety function faulty", + "settings_restored": "Settings restored", + "sim_card_locked": "SIM card locked", + "sim_card_not_found": "SIM card not found", + "sim_card_requires_pin": "SIM card requires PIN", + "slipped_mower_has_slipped_situation_not_solved_with_moving_pattern": "Slipped - Mower has Slipped. Situation not solved with moving pattern", + "slope_too_steep": "Slope too steep", + "sms_could_not_be_sent": "SMS could not be sent", + "stop_button_problem": "STOP button problem", + "stuck_in_charging_station": "Stuck in charging station", + "switch_cord_problem": "Switch cord problem", + "temporary_battery_problem": "Temporary battery problem", + "tilt_sensor_problem": "Tilt sensor problem", + "too_high_discharge_current": "Discharge current too high", + "too_high_internal_current": "Internal current too high", + "trapped": "Trapped", + "ultrasonic_problem": "Ultrasonic problem", + "ultrasonic_sensor_1_defect": "Ultrasonic Sensor 1 defect", + "ultrasonic_sensor_2_defect": "Ultrasonic Sensor 2 defect", + "ultrasonic_sensor_3_defect": "Ultrasonic Sensor 3 defect", + "ultrasonic_sensor_4_defect": "Ultrasonic Sensor 4 defect", + "unexpected_cutting_height_adj": "Unexpected cutting height adjustment", + "unexpected_error": "Unexpected error", + "upside_down": "Upside down", + "weak_gps_signal": "Weak GPS signal", + "wheel_drive_problem_left": "Left wheel drive problem", + "wheel_drive_problem_rear_left": "Rear left wheel drive problem", + "wheel_drive_problem_rear_right": "Rear right wheel drive problem", + "wheel_drive_problem_right": "Right wheel drive problem", + "wheel_motor_blocked_left": "Left wheel motor blocked", + "wheel_motor_blocked_rear_left": "Rear left wheel motor blocked", + "wheel_motor_blocked_rear_right": "Rear right wheel motor blocked", + "wheel_motor_blocked_right": "Right wheel motor blocked", + "wheel_motor_overloaded_left": "Left wheel motor overloaded", + "wheel_motor_overloaded_rear_left": "Rear left wheel motor overloaded", + "wheel_motor_overloaded_rear_right": "Rear right wheel motor overloaded", + "wheel_motor_overloaded_right": "Right wheel motor overloaded", + "work_area_not_valid": "Work area not valid", + "wrong_loop_signal": "Wrong loop signal", + "wrong_pin_code": "Wrong PIN code", + "zone_generator_problem": "Zone generator problem" + } + }, "number_of_charging_cycles": { "name": "Number of charging cycles" }, @@ -58,6 +186,21 @@ "cutting_blade_usage_time": { "name": "Cutting blade usage time" }, + "restricted_reason": { + "name": "Restricted reason", + "state": { + "none": "No restrictions", + "week_schedule": "Week schedule", + "park_override": "Park override", + "sensor": "Weather timer", + "daily_limit": "Daily limit", + "fota": "Firmware Over-the-Air update running", + "frost": "Frost", + "all_work_areas_completed": "All work areas completed", + "external": "External", + "not_applicable": "Not applicable" + } + }, "total_charging_time": { "name": "Total charging time" }, diff --git a/tests/components/husqvarna_automower/snapshots/test_sensor.ambr b/tests/components/husqvarna_automower/snapshots/test_sensor.ambr index ce81098f753..fda9c900240 100644 --- a/tests/components/husqvarna_automower/snapshots/test_sensor.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_sensor.ambr @@ -104,6 +104,344 @@ 'state': '0.034', }) # --- +# name: test_sensor[sensor.test_mower_1_error-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'no_error', + 'alarm_mower_in_motion', + 'alarm_mower_lifted', + 'alarm_mower_stopped', + 'alarm_mower_switched_off', + 'alarm_mower_tilted', + 'alarm_outside_geofence', + 'angular_sensor_problem', + 'battery_problem', + 'battery_problem', + 'battery_restriction_due_to_ambient_temperature', + 'can_error', + 'charging_current_too_high', + 'charging_station_blocked', + 'charging_system_problem', + 'charging_system_problem', + 'collision_sensor_defect', + 'collision_sensor_error', + 'collision_sensor_problem_front', + 'collision_sensor_problem_rear', + 'com_board_not_available', + 'communication_circuit_board_sw_must_be_updated', + 'complex_working_area', + 'connection_changed', + 'connection_not_changed', + 'connectivity_problem', + 'connectivity_problem', + 'connectivity_problem', + 'connectivity_problem', + 'connectivity_problem', + 'connectivity_problem', + 'connectivity_settings_restored', + 'cutting_drive_motor_1_defect', + 'cutting_drive_motor_2_defect', + 'cutting_drive_motor_3_defect', + 'cutting_height_blocked', + 'cutting_height_problem', + 'cutting_height_problem_curr', + 'cutting_height_problem_dir', + 'cutting_height_problem_drive', + 'cutting_motor_problem', + 'cutting_stopped_slope_too_steep', + 'cutting_system_blocked', + 'cutting_system_blocked', + 'cutting_system_imbalance_warning', + 'cutting_system_major_imbalance', + 'destination_not_reachable', + 'difficult_finding_home', + 'docking_sensor_defect', + 'electronic_problem', + 'empty_battery', + 'folding_cutting_deck_sensor_defect', + 'folding_sensor_activated', + 'geofence_problem', + 'geofence_problem', + 'gps_navigation_problem', + 'guide_1_not_found', + 'guide_2_not_found', + 'guide_3_not_found', + 'guide_calibration_accomplished', + 'guide_calibration_failed', + 'high_charging_power_loss', + 'high_internal_power_loss', + 'high_internal_temperature', + 'internal_voltage_error', + 'invalid_battery_combination_invalid_combination_of_different_battery_types', + 'invalid_sub_device_combination', + 'invalid_system_configuration', + 'left_brush_motor_overloaded', + 'lift_sensor_defect', + 'lifted', + 'limited_cutting_height_range', + 'limited_cutting_height_range', + 'loop_sensor_defect', + 'loop_sensor_problem_front', + 'loop_sensor_problem_left', + 'loop_sensor_problem_rear', + 'loop_sensor_problem_right', + 'low_battery', + 'memory_circuit_problem', + 'mower_lifted', + 'mower_tilted', + 'no_accurate_position_from_satellites', + 'no_confirmed_position', + 'no_drive', + 'no_loop_signal', + 'no_power_in_charging_station', + 'no_response_from_charger', + 'outside_working_area', + 'poor_signal_quality', + 'reference_station_communication_problem', + 'right_brush_motor_overloaded', + 'safety_function_faulty', + 'settings_restored', + 'sim_card_locked', + 'sim_card_locked', + 'sim_card_locked', + 'sim_card_locked', + 'sim_card_not_found', + 'sim_card_requires_pin', + 'slipped_mower_has_slipped_situation_not_solved_with_moving_pattern', + 'slope_too_steep', + 'sms_could_not_be_sent', + 'stop_button_problem', + 'stuck_in_charging_station', + 'switch_cord_problem', + 'temporary_battery_problem', + 'temporary_battery_problem', + 'temporary_battery_problem', + 'temporary_battery_problem', + 'temporary_battery_problem', + 'temporary_battery_problem', + 'temporary_battery_problem', + 'temporary_battery_problem', + 'tilt_sensor_problem', + 'too_high_discharge_current', + 'too_high_internal_current', + 'trapped', + 'ultrasonic_problem', + 'ultrasonic_sensor_1_defect', + 'ultrasonic_sensor_2_defect', + 'ultrasonic_sensor_3_defect', + 'ultrasonic_sensor_4_defect', + 'unexpected_cutting_height_adj', + 'unexpected_error', + 'upside_down', + 'weak_gps_signal', + 'wheel_drive_problem_left', + 'wheel_drive_problem_rear_left', + 'wheel_drive_problem_rear_right', + 'wheel_drive_problem_right', + 'wheel_motor_blocked_left', + 'wheel_motor_blocked_rear_left', + 'wheel_motor_blocked_rear_right', + 'wheel_motor_blocked_right', + 'wheel_motor_overloaded_left', + 'wheel_motor_overloaded_rear_left', + 'wheel_motor_overloaded_rear_right', + 'wheel_motor_overloaded_right', + 'work_area_not_valid', + 'wrong_loop_signal', + 'wrong_pin_code', + 'zone_generator_problem', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_mower_1_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': 'Error', + 'platform': 'husqvarna_automower', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'error', + 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_error', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.test_mower_1_error-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Test Mower 1 Error', + 'options': list([ + 'no_error', + 'alarm_mower_in_motion', + 'alarm_mower_lifted', + 'alarm_mower_stopped', + 'alarm_mower_switched_off', + 'alarm_mower_tilted', + 'alarm_outside_geofence', + 'angular_sensor_problem', + 'battery_problem', + 'battery_problem', + 'battery_restriction_due_to_ambient_temperature', + 'can_error', + 'charging_current_too_high', + 'charging_station_blocked', + 'charging_system_problem', + 'charging_system_problem', + 'collision_sensor_defect', + 'collision_sensor_error', + 'collision_sensor_problem_front', + 'collision_sensor_problem_rear', + 'com_board_not_available', + 'communication_circuit_board_sw_must_be_updated', + 'complex_working_area', + 'connection_changed', + 'connection_not_changed', + 'connectivity_problem', + 'connectivity_problem', + 'connectivity_problem', + 'connectivity_problem', + 'connectivity_problem', + 'connectivity_problem', + 'connectivity_settings_restored', + 'cutting_drive_motor_1_defect', + 'cutting_drive_motor_2_defect', + 'cutting_drive_motor_3_defect', + 'cutting_height_blocked', + 'cutting_height_problem', + 'cutting_height_problem_curr', + 'cutting_height_problem_dir', + 'cutting_height_problem_drive', + 'cutting_motor_problem', + 'cutting_stopped_slope_too_steep', + 'cutting_system_blocked', + 'cutting_system_blocked', + 'cutting_system_imbalance_warning', + 'cutting_system_major_imbalance', + 'destination_not_reachable', + 'difficult_finding_home', + 'docking_sensor_defect', + 'electronic_problem', + 'empty_battery', + 'folding_cutting_deck_sensor_defect', + 'folding_sensor_activated', + 'geofence_problem', + 'geofence_problem', + 'gps_navigation_problem', + 'guide_1_not_found', + 'guide_2_not_found', + 'guide_3_not_found', + 'guide_calibration_accomplished', + 'guide_calibration_failed', + 'high_charging_power_loss', + 'high_internal_power_loss', + 'high_internal_temperature', + 'internal_voltage_error', + 'invalid_battery_combination_invalid_combination_of_different_battery_types', + 'invalid_sub_device_combination', + 'invalid_system_configuration', + 'left_brush_motor_overloaded', + 'lift_sensor_defect', + 'lifted', + 'limited_cutting_height_range', + 'limited_cutting_height_range', + 'loop_sensor_defect', + 'loop_sensor_problem_front', + 'loop_sensor_problem_left', + 'loop_sensor_problem_rear', + 'loop_sensor_problem_right', + 'low_battery', + 'memory_circuit_problem', + 'mower_lifted', + 'mower_tilted', + 'no_accurate_position_from_satellites', + 'no_confirmed_position', + 'no_drive', + 'no_loop_signal', + 'no_power_in_charging_station', + 'no_response_from_charger', + 'outside_working_area', + 'poor_signal_quality', + 'reference_station_communication_problem', + 'right_brush_motor_overloaded', + 'safety_function_faulty', + 'settings_restored', + 'sim_card_locked', + 'sim_card_locked', + 'sim_card_locked', + 'sim_card_locked', + 'sim_card_not_found', + 'sim_card_requires_pin', + 'slipped_mower_has_slipped_situation_not_solved_with_moving_pattern', + 'slope_too_steep', + 'sms_could_not_be_sent', + 'stop_button_problem', + 'stuck_in_charging_station', + 'switch_cord_problem', + 'temporary_battery_problem', + 'temporary_battery_problem', + 'temporary_battery_problem', + 'temporary_battery_problem', + 'temporary_battery_problem', + 'temporary_battery_problem', + 'temporary_battery_problem', + 'temporary_battery_problem', + 'tilt_sensor_problem', + 'too_high_discharge_current', + 'too_high_internal_current', + 'trapped', + 'ultrasonic_problem', + 'ultrasonic_sensor_1_defect', + 'ultrasonic_sensor_2_defect', + 'ultrasonic_sensor_3_defect', + 'ultrasonic_sensor_4_defect', + 'unexpected_cutting_height_adj', + 'unexpected_error', + 'upside_down', + 'weak_gps_signal', + 'wheel_drive_problem_left', + 'wheel_drive_problem_rear_left', + 'wheel_drive_problem_rear_right', + 'wheel_drive_problem_right', + 'wheel_motor_blocked_left', + 'wheel_motor_blocked_rear_left', + 'wheel_motor_blocked_rear_right', + 'wheel_motor_blocked_right', + 'wheel_motor_overloaded_left', + 'wheel_motor_overloaded_rear_left', + 'wheel_motor_overloaded_rear_right', + 'wheel_motor_overloaded_right', + 'work_area_not_valid', + 'wrong_loop_signal', + 'wrong_pin_code', + 'zone_generator_problem', + ]), + }), + 'context': , + 'entity_id': 'sensor.test_mower_1_error', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'no_error', + }) +# --- # name: test_sensor[sensor.test_mower_1_mode-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -311,6 +649,78 @@ 'state': '11396', }) # --- +# name: test_sensor[sensor.test_mower_1_restricted_reason-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'all_work_areas_completed', + 'daily_limit', + 'external', + 'fota', + 'frost', + 'none', + 'not_applicable', + 'park_override', + 'sensor', + 'week_schedule', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_mower_1_restricted_reason', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Restricted reason', + 'platform': 'husqvarna_automower', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'restricted_reason', + 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_restricted_reason', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.test_mower_1_restricted_reason-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Test Mower 1 Restricted reason', + 'options': list([ + 'all_work_areas_completed', + 'daily_limit', + 'external', + 'fota', + 'frost', + 'none', + 'not_applicable', + 'park_override', + 'sensor', + 'week_schedule', + ]), + }), + 'context': , + 'entity_id': 'sensor.test_mower_1_restricted_reason', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'week_schedule', + }) +# --- # name: test_sensor[sensor.test_mower_1_total_charging_time-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/husqvarna_automower/test_sensor.py b/tests/components/husqvarna_automower/test_sensor.py index bc464b2ce78..6d4e8412ad3 100644 --- a/tests/components/husqvarna_automower/test_sensor.py +++ b/tests/components/husqvarna_automower/test_sensor.py @@ -94,6 +94,31 @@ async def test_statistics_not_available( assert state is None +async def test_error_sensor( + hass: HomeAssistant, + mock_automower_client: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test error sensor.""" + values = mower_list_to_dictionary_dataclass( + load_json_value_fixture("mower.json", DOMAIN) + ) + await setup_integration(hass, mock_config_entry) + + 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(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + state = hass.states.get("sensor.test_mower_1_error") + assert state.state == expected_state + + async def test_sensor( hass: HomeAssistant, entity_registry: er.EntityRegistry, From 3bb9858dfbeb0ae9d9d4552e9447939eba30b027 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 31 Mar 2024 20:08:43 +0200 Subject: [PATCH 112/967] Fix server update from breaking setup in Speedtest.NET (#114524) --- homeassistant/components/speedtestdotnet/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/speedtestdotnet/__init__.py b/homeassistant/components/speedtestdotnet/__init__.py index 831e66d1c4e..3c15f2fb820 100644 --- a/homeassistant/components/speedtestdotnet/__init__.py +++ b/homeassistant/components/speedtestdotnet/__init__.py @@ -25,10 +25,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b partial(speedtest.Speedtest, secure=True) ) coordinator = SpeedTestDataCoordinator(hass, config_entry, api) - await hass.async_add_executor_job(coordinator.update_servers) except speedtest.SpeedtestException as err: raise ConfigEntryNotReady from err + hass.data[DOMAIN] = coordinator + async def _async_finish_startup(hass: HomeAssistant) -> None: """Run this only when HA has finished its startup.""" await coordinator.async_config_entry_first_refresh() @@ -36,8 +37,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b # Don't start a speedtest during startup async_at_started(hass, _async_finish_startup) - hass.data[DOMAIN] = coordinator - await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) config_entry.async_on_unload(config_entry.add_update_listener(update_listener)) From f08af5dc6d344d3d5cc07b5ed57c1a62977e87df Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Sun, 31 Mar 2024 23:33:59 +0200 Subject: [PATCH 113/967] Avoid use of `hass.helpers` in _mqtt_mock_entry fixture (#114536) --- tests/conftest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 157e0f2ba59..a38da17f44b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -62,6 +62,7 @@ from homeassistant.helpers import ( label_registry as lr, recorder as recorder_helper, ) +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import ConfigType from homeassistant.setup import BASE_PLATFORMS, async_setup_component from homeassistant.util import location @@ -985,7 +986,7 @@ async def _mqtt_mock_entry( # connected set to True to get a more realistic behavior when subscribing mock_mqtt_instance.connected = True - hass.helpers.dispatcher.async_dispatcher_send(mqtt.MQTT_CONNECTED) + async_dispatcher_send(hass, mqtt.MQTT_CONNECTED) await hass.async_block_till_done() return mock_mqtt_instance From 05d40fbc4c6b8363ef46e5a6ddea05ec9e7cbd13 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 1 Apr 2024 00:24:41 +0200 Subject: [PATCH 114/967] Bump axis to v60 (#114544) * Improve Axis MQTT support * Bump axis to v60 --- homeassistant/components/axis/hub/hub.py | 5 +++-- homeassistant/components/axis/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/axis/const.py | 1 + tests/components/axis/test_hub.py | 4 ++-- 6 files changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/axis/hub/hub.py b/homeassistant/components/axis/hub/hub.py index 08eb816f6ab..4abd1358417 100644 --- a/homeassistant/components/axis/hub/hub.py +++ b/homeassistant/components/axis/hub/hub.py @@ -116,7 +116,7 @@ class AxisHub: if status.status.state == ClientState.ACTIVE: self.config.entry.async_on_unload( await mqtt.async_subscribe( - hass, f"{self.api.vapix.serial_number}/#", self.mqtt_message + hass, f"{status.config.device_topic_prefix}/#", self.mqtt_message ) ) @@ -124,7 +124,8 @@ class AxisHub: def mqtt_message(self, message: ReceiveMessage) -> None: """Receive Axis MQTT message.""" self.disconnect_from_stream() - + if message.topic.endswith("event/connection"): + return event = mqtt_json_to_event(message.payload) self.api.event.handler(event) diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index f47d10df484..1065783d957 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -26,7 +26,7 @@ "iot_class": "local_push", "loggers": ["axis"], "quality_scale": "platinum", - "requirements": ["axis==59"], + "requirements": ["axis==60"], "ssdp": [ { "manufacturer": "AXIS" diff --git a/requirements_all.txt b/requirements_all.txt index bc0b881b4c3..3f8b85d8e5a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -514,7 +514,7 @@ aurorapy==0.2.7 # avion==0.10 # homeassistant.components.axis -axis==59 +axis==60 # homeassistant.components.azure_event_hub azure-eventhub==5.11.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b914a8f911f..a152358ac64 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -454,7 +454,7 @@ auroranoaa==0.0.3 aurorapy==0.2.7 # homeassistant.components.axis -axis==59 +axis==60 # homeassistant.components.azure_event_hub azure-eventhub==5.11.1 diff --git a/tests/components/axis/const.py b/tests/components/axis/const.py index 7b881ea55e5..16b9d17f99e 100644 --- a/tests/components/axis/const.py +++ b/tests/components/axis/const.py @@ -74,6 +74,7 @@ MQTT_CLIENT_RESPONSE = { "status": {"state": "active", "connectionStatus": "Connected"}, "config": { "server": {"protocol": "tcp", "host": "192.168.0.90", "port": 1883}, + "deviceTopicPrefix": f"axis/{MAC}", }, }, } diff --git a/tests/components/axis/test_hub.py b/tests/components/axis/test_hub.py index 3291f88d90a..1ae6db05427 100644 --- a/tests/components/axis/test_hub.py +++ b/tests/components/axis/test_hub.py @@ -91,9 +91,9 @@ async def test_device_support_mqtt( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_config_entry ) -> None: """Successful setup.""" - mqtt_mock.async_subscribe.assert_called_with(f"{MAC}/#", mock.ANY, 0, "utf-8") + mqtt_mock.async_subscribe.assert_called_with(f"axis/{MAC}/#", mock.ANY, 0, "utf-8") - topic = f"{MAC}/event/tns:onvif/Device/tns:axis/Sensor/PIR/$source/sensor/0" + topic = f"axis/{MAC}/event/tns:onvif/Device/tns:axis/Sensor/PIR/$source/sensor/0" message = ( b'{"timestamp": 1590258472044, "topic": "onvif:Device/axis:Sensor/PIR",' b' "message": {"source": {"sensor": "0"}, "key": {}, "data": {"state": "1"}}}' From 0238c2ea9e6460fe306ab2003cc5c3541b310138 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Mon, 1 Apr 2024 00:41:06 +0200 Subject: [PATCH 115/967] Use device registry mock instead of `hass.helpers` in dsmr tests (#114535) --- tests/components/dsmr/test_mbus_migration.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/components/dsmr/test_mbus_migration.py b/tests/components/dsmr/test_mbus_migration.py index 95def2f66cf..429128c48bb 100644 --- a/tests/components/dsmr/test_mbus_migration.py +++ b/tests/components/dsmr/test_mbus_migration.py @@ -6,13 +6,16 @@ from decimal import Decimal from homeassistant.components.dsmr.const import DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_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 async def test_migrate_gas_to_mbus( - hass: HomeAssistant, entity_registry: er.EntityRegistry, dsmr_connection_fixture + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + dsmr_connection_fixture, ) -> None: """Test migration of unique_id.""" (connection_factory, transport, protocol) = dsmr_connection_fixture @@ -42,7 +45,6 @@ async def test_migrate_gas_to_mbus( old_unique_id = "37464C4F32313139303333373331_belgium_5min_gas_meter_reading" - device_registry = hass.helpers.device_registry.async_get(hass) device = device_registry.async_get_or_create( config_entry_id=mock_entry.entry_id, identifiers={(DOMAIN, mock_entry.entry_id)}, @@ -108,7 +110,10 @@ async def test_migrate_gas_to_mbus( async def test_migrate_gas_to_mbus_exists( - hass: HomeAssistant, entity_registry: er.EntityRegistry, dsmr_connection_fixture + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + dsmr_connection_fixture, ) -> None: """Test migration of unique_id.""" (connection_factory, transport, protocol) = dsmr_connection_fixture @@ -138,7 +143,6 @@ async def test_migrate_gas_to_mbus_exists( old_unique_id = "37464C4F32313139303333373331_belgium_5min_gas_meter_reading" - device_registry = hass.helpers.device_registry.async_get(hass) device = device_registry.async_get_or_create( config_entry_id=mock_entry.entry_id, identifiers={(DOMAIN, mock_entry.entry_id)}, From ecf286cd81fc94ca405c1204f0ad0bc7f3dd80c4 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Mon, 1 Apr 2024 00:41:47 +0200 Subject: [PATCH 116/967] Avoid use of `hass.helpers` in plugwise test (#114534) --- tests/components/plugwise/test_binary_sensor.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/components/plugwise/test_binary_sensor.py b/tests/components/plugwise/test_binary_sensor.py index aec20bc4a0b..878300bddb4 100644 --- a/tests/components/plugwise/test_binary_sensor.py +++ b/tests/components/plugwise/test_binary_sensor.py @@ -4,6 +4,7 @@ from unittest.mock import MagicMock from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_component import async_update_entity from tests.common import MockConfigEntry @@ -45,9 +46,7 @@ async def test_anna_climate_binary_sensor_change( assert state assert state.state == STATE_ON - await hass.helpers.entity_component.async_update_entity( - "binary_sensor.opentherm_dhw_state" - ) + await async_update_entity(hass, "binary_sensor.opentherm_dhw_state") state = hass.states.get("binary_sensor.opentherm_dhw_state") assert state From 04786e019aa8094634148227f5f07b1ba7b9439f Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Mon, 1 Apr 2024 00:42:46 +0200 Subject: [PATCH 117/967] Use device registry mock instead of `hass.helpers` in traccar_server tests (#114532) --- tests/components/traccar_server/test_diagnostics.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/components/traccar_server/test_diagnostics.py b/tests/components/traccar_server/test_diagnostics.py index 3d112057315..faf1b628fcd 100644 --- a/tests/components/traccar_server/test_diagnostics.py +++ b/tests/components/traccar_server/test_diagnostics.py @@ -49,14 +49,14 @@ async def test_device_diagnostics( await setup_integration(hass, mock_config_entry) devices = dr.async_entries_for_config_entry( - hass.helpers.device_registry.async_get(hass), + device_registry, mock_config_entry.entry_id, ) assert len(devices) == 1 for device in dr.async_entries_for_config_entry( - hass.helpers.device_registry.async_get(hass), mock_config_entry.entry_id + device_registry, mock_config_entry.entry_id ): result = await get_diagnostics_for_device( hass, hass_client, mock_config_entry, device=device @@ -78,14 +78,14 @@ async def test_device_diagnostics_with_disabled_entity( await setup_integration(hass, mock_config_entry) devices = dr.async_entries_for_config_entry( - hass.helpers.device_registry.async_get(hass), + device_registry, mock_config_entry.entry_id, ) assert len(devices) == 1 for device in dr.async_entries_for_config_entry( - hass.helpers.device_registry.async_get(hass), mock_config_entry.entry_id + device_registry, mock_config_entry.entry_id ): for entry in er.async_entries_for_device( entity_registry, From be398e0a3fa5f9a6be6eb7d17cc7a718bfff56e6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 31 Mar 2024 13:49:24 -1000 Subject: [PATCH 118/967] Fix flakey sonos test test_creating_entry_sets_up_media_player (#114539) --- tests/components/sonos/test_init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/sonos/test_init.py b/tests/components/sonos/test_init.py index 77bf9a5d12b..97fdc27d461 100644 --- a/tests/components/sonos/test_init.py +++ b/tests/components/sonos/test_init.py @@ -51,7 +51,7 @@ async def test_creating_entry_sets_up_media_player( result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert len(mock_setup.mock_calls) == 1 From 55657dcb4079c8384878ddf753864f4072a8f3b8 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Mon, 1 Apr 2024 02:08:05 +0200 Subject: [PATCH 119/967] Bump python-songpal to 0.16.2 (#114525) --- homeassistant/components/songpal/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/songpal/manifest.json b/homeassistant/components/songpal/manifest.json index d4d33a77d43..c4dec6b938d 100644 --- a/homeassistant/components/songpal/manifest.json +++ b/homeassistant/components/songpal/manifest.json @@ -7,7 +7,7 @@ "iot_class": "local_push", "loggers": ["songpal"], "quality_scale": "gold", - "requirements": ["python-songpal==0.16.1"], + "requirements": ["python-songpal==0.16.2"], "ssdp": [ { "st": "urn:schemas-sony-com:service:ScalarWebAPI:1", diff --git a/requirements_all.txt b/requirements_all.txt index 3f8b85d8e5a..2c47ab0828f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2300,7 +2300,7 @@ python-roborock==0.40.0 python-smarttub==0.0.36 # homeassistant.components.songpal -python-songpal==0.16.1 +python-songpal==0.16.2 # homeassistant.components.tado python-tado==0.17.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a152358ac64..15985c3a400 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1776,7 +1776,7 @@ python-roborock==0.40.0 python-smarttub==0.0.36 # homeassistant.components.songpal -python-songpal==0.16.1 +python-songpal==0.16.2 # homeassistant.components.tado python-tado==0.17.4 From dc59073f25dcaacfb23986a19008cdb92fde7d53 Mon Sep 17 00:00:00 2001 From: Jonny Rimkus Date: Mon, 1 Apr 2024 10:50:21 +0200 Subject: [PATCH 120/967] Bump slixmpp version to 1.8.5 (#114448) * Update slixmpp to 1.8.5, hopefully fixes #113990 * Bump slixmpp version to 1.8.5 #114448 --- homeassistant/components/xmpp/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xmpp/manifest.json b/homeassistant/components/xmpp/manifest.json index 30dee6c842b..308c3d70978 100644 --- a/homeassistant/components/xmpp/manifest.json +++ b/homeassistant/components/xmpp/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/xmpp", "iot_class": "cloud_push", "loggers": ["pyasn1", "slixmpp"], - "requirements": ["slixmpp==1.8.4", "emoji==2.8.0"] + "requirements": ["slixmpp==1.8.5", "emoji==2.8.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2c47ab0828f..32c12ff7c1a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2560,7 +2560,7 @@ sisyphus-control==3.1.3 slackclient==2.5.0 # homeassistant.components.xmpp -slixmpp==1.8.4 +slixmpp==1.8.5 # homeassistant.components.smart_meter_texas smart-meter-texas==0.4.7 From d5f883fbf06e41b920258c1b7b6f9a07eced04cf Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Mon, 1 Apr 2024 11:11:59 +0200 Subject: [PATCH 121/967] Unignore Ruff PLR in tests (#114470) * Unignore Ruff PLR in tests * Address review comments * review comments * fix import * Update test_api.py * Update test_api.py * Update test_api.py --- .../alarm_control_panel/test_device_action.py | 2 +- .../test_device_condition.py | 2 +- .../alarm_control_panel/test_device_trigger.py | 2 +- tests/components/alert/test_init.py | 3 +-- tests/components/alexa/test_smart_home.py | 2 +- tests/components/androidtv/patchers.py | 3 ++- tests/components/apache_kafka/test_init.py | 2 +- tests/components/aprs/test_device_tracker.py | 2 +- .../components/arcam_fmj/test_device_trigger.py | 2 +- tests/components/assist_pipeline/test_init.py | 2 +- tests/components/automation/test_init.py | 2 +- tests/components/baf/__init__.py | 1 - .../binary_sensor/test_device_condition.py | 2 +- .../binary_sensor/test_device_trigger.py | 2 +- tests/components/caldav/test_todo.py | 2 +- tests/components/calendar/test_trigger.py | 3 +-- tests/components/climate/test_device_action.py | 2 +- .../components/climate/test_device_condition.py | 2 +- tests/components/climate/test_device_trigger.py | 2 +- tests/components/configurator/test_init.py | 2 +- tests/components/cover/test_device_action.py | 2 +- tests/components/cover/test_device_condition.py | 2 +- tests/components/cover/test_device_trigger.py | 2 +- tests/components/cover/test_init.py | 2 +- tests/components/datadog/test_init.py | 2 +- tests/components/demo/test_notify.py | 2 +- tests/components/demo/test_remote.py | 2 +- tests/components/device_automation/test_init.py | 3 +-- .../device_automation/test_toggle_entity.py | 2 +- .../device_tracker/test_device_condition.py | 2 +- .../device_tracker/test_device_trigger.py | 3 +-- tests/components/device_tracker/test_init.py | 3 +-- tests/components/ecobee/test_climate.py | 2 +- tests/components/fan/test_device_action.py | 2 +- tests/components/fan/test_device_condition.py | 2 +- tests/components/fan/test_device_trigger.py | 2 +- tests/components/foobot/test_sensor.py | 2 +- tests/components/fully_kiosk/test_button.py | 2 +- .../components/fully_kiosk/test_media_player.py | 2 +- tests/components/fully_kiosk/test_number.py | 2 +- tests/components/fully_kiosk/test_switch.py | 2 +- tests/components/google/conftest.py | 5 ----- tests/components/google_pubsub/test_init.py | 4 ++-- tests/components/group/test_init.py | 2 +- tests/components/group/test_notify.py | 2 +- tests/components/hassio/test_ingress.py | 4 ++-- tests/components/history/test_init.py | 12 +++++------- .../history/test_init_db_schema_30.py | 5 ++--- .../homeassistant/triggers/test_event.py | 2 +- .../triggers/test_homeassistant.py | 2 +- .../triggers/test_numeric_state.py | 2 +- .../homeassistant/triggers/test_state.py | 2 +- .../homeassistant/triggers/test_time_pattern.py | 4 ++-- tests/components/homekit/test_util.py | 1 - .../homekit_controller/test_device_trigger.py | 2 +- tests/components/http/test_ban.py | 2 +- tests/components/http/test_init.py | 2 +- .../components/humidifier/test_device_action.py | 2 +- .../humidifier/test_device_condition.py | 2 +- .../humidifier/test_device_trigger.py | 2 +- tests/components/image_processing/test_init.py | 2 +- tests/components/influxdb/test_init.py | 2 +- tests/components/influxdb/test_sensor.py | 2 +- tests/components/kira/test_init.py | 2 +- tests/components/knx/conftest.py | 1 - tests/components/kodi/test_device_trigger.py | 2 +- tests/components/light/test_device_action.py | 4 ++-- tests/components/light/test_device_condition.py | 2 +- tests/components/light/test_device_trigger.py | 2 +- tests/components/litejet/test_trigger.py | 2 +- tests/components/lock/test_device_action.py | 2 +- tests/components/lock/test_device_condition.py | 2 +- tests/components/lock/test_device_trigger.py | 2 +- tests/components/logentries/test_init.py | 2 +- tests/components/mailbox/test_init.py | 2 +- .../media_player/test_device_condition.py | 2 +- .../media_player/test_device_trigger.py | 2 +- tests/components/meraki/test_device_tracker.py | 2 +- tests/components/mfi/test_sensor.py | 2 +- tests/components/mfi/test_switch.py | 2 +- .../components/mikrotik/test_device_tracker.py | 3 +-- tests/components/mochad/test_light.py | 2 +- tests/components/mochad/test_switch.py | 2 +- tests/components/mold_indicator/test_sensor.py | 2 +- tests/components/mqtt/test_device_trigger.py | 2 +- tests/components/mqtt/test_trigger.py | 2 +- tests/components/mqtt_room/test_sensor.py | 2 +- tests/components/nest/test_api.py | 5 +++-- tests/components/nest/test_device_trigger.py | 2 +- tests/components/netatmo/test_device_trigger.py | 2 +- tests/components/number/test_device_action.py | 2 +- tests/components/opengarage/test_button.py | 2 +- .../philips_js/test_device_trigger.py | 2 +- tests/components/pilight/test_sensor.py | 3 +-- tests/components/pjlink/test_media_player.py | 2 +- tests/components/plant/test_init.py | 2 +- tests/components/plex/test_init.py | 2 +- tests/components/python_script/test_init.py | 4 ++-- tests/components/recorder/test_history.py | 12 +++++------- .../recorder/test_history_db_schema_30.py | 17 ++++++----------- .../recorder/test_history_db_schema_32.py | 12 +++++------- .../recorder/test_history_db_schema_42.py | 12 +++++------- tests/components/remote/test_device_action.py | 2 +- .../components/remote/test_device_condition.py | 2 +- tests/components/remote/test_device_trigger.py | 2 +- tests/components/remote/test_init.py | 2 +- tests/components/rest/test_notify.py | 2 +- tests/components/rfxtrx/test_device_action.py | 2 +- tests/components/rfxtrx/test_device_trigger.py | 2 +- tests/components/ring/test_init.py | 2 +- .../components/sensor/test_device_condition.py | 2 +- tests/components/sensor/test_device_trigger.py | 2 +- tests/components/smtp/test_notify.py | 2 +- tests/components/snips/test_init.py | 2 +- tests/components/statsd/test_init.py | 2 +- tests/components/sun/test_trigger.py | 3 +-- tests/components/switch/test_device_action.py | 2 +- .../components/switch/test_device_condition.py | 2 +- tests/components/switch/test_device_trigger.py | 2 +- tests/components/tag/test_trigger.py | 2 +- tests/components/tasmota/test_device_trigger.py | 2 +- tests/components/telegram/test_notify.py | 2 +- tests/components/template/test_light.py | 2 +- tests/components/template/test_trigger.py | 2 +- tests/components/text/test_device_action.py | 2 +- tests/components/universal/test_media_player.py | 5 +---- tests/components/update/test_device_trigger.py | 2 +- tests/components/vacuum/test_device_action.py | 2 +- .../components/vacuum/test_device_condition.py | 2 +- tests/components/vacuum/test_device_trigger.py | 2 +- .../water_heater/test_device_action.py | 2 +- .../components/yandex_transport/test_sensor.py | 2 +- tests/components/zha/conftest.py | 1 - .../components/zha/test_alarm_control_panel.py | 4 ++-- tests/components/zha/test_binary_sensor.py | 4 +--- tests/components/zha/test_button.py | 5 ++--- tests/components/zha/test_cluster_handlers.py | 3 +-- tests/components/zha/test_cover.py | 3 +-- tests/components/zha/test_device.py | 2 +- tests/components/zha/test_device_action.py | 5 ++--- tests/components/zha/test_device_tracker.py | 2 +- tests/components/zha/test_device_trigger.py | 4 ++-- tests/components/zha/test_diagnostics.py | 4 ++-- tests/components/zha/test_discover.py | 2 +- tests/components/zha/test_gateway.py | 5 ++--- tests/components/zha/test_helpers.py | 5 ++--- tests/components/zha/test_light.py | 10 +++------- tests/components/zha/test_lock.py | 3 +-- tests/components/zha/test_logbook.py | 2 +- tests/components/zha/test_number.py | 3 +-- tests/components/zha/test_registries.py | 2 +- tests/components/zha/test_select.py | 5 ++--- tests/components/zha/test_sensor.py | 1 - tests/components/zha/test_siren.py | 5 ++--- tests/components/zha/test_switch.py | 5 ++--- tests/components/zha/test_update.py | 6 +++--- tests/components/zha/test_websocket_api.py | 3 +-- tests/helpers/test_condition.py | 2 +- tests/helpers/test_registry.py | 4 ++-- tests/helpers/test_script.py | 2 +- tests/helpers/test_sun.py | 2 +- tests/ruff.toml | 1 - tests/scripts/test_check_config.py | 2 +- tests/scripts/test_init.py | 2 +- tests/test_config.py | 7 +++++-- tests/test_loader.py | 4 ++-- tests/util/test_dt.py | 6 ------ tests/util/test_package.py | 2 +- tests/util/yaml/test_init.py | 2 +- 169 files changed, 211 insertions(+), 266 deletions(-) diff --git a/tests/components/alarm_control_panel/test_device_action.py b/tests/components/alarm_control_panel/test_device_action.py index afcfa0a7a12..5d142ab277b 100644 --- a/tests/components/alarm_control_panel/test_device_action.py +++ b/tests/components/alarm_control_panel/test_device_action.py @@ -3,11 +3,11 @@ import pytest from pytest_unordered import unordered +from homeassistant.components import automation from homeassistant.components.alarm_control_panel import ( DOMAIN, AlarmControlPanelEntityFeature, ) -import homeassistant.components.automation as automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.const import ( CONF_PLATFORM, diff --git a/tests/components/alarm_control_panel/test_device_condition.py b/tests/components/alarm_control_panel/test_device_condition.py index d95574b7c9f..b6ee6b2faaa 100644 --- a/tests/components/alarm_control_panel/test_device_condition.py +++ b/tests/components/alarm_control_panel/test_device_condition.py @@ -3,11 +3,11 @@ import pytest from pytest_unordered import unordered +from homeassistant.components import automation from homeassistant.components.alarm_control_panel import ( DOMAIN, AlarmControlPanelEntityFeature, ) -import homeassistant.components.automation as automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, diff --git a/tests/components/alarm_control_panel/test_device_trigger.py b/tests/components/alarm_control_panel/test_device_trigger.py index be241ef241e..00cdc5ddbee 100644 --- a/tests/components/alarm_control_panel/test_device_trigger.py +++ b/tests/components/alarm_control_panel/test_device_trigger.py @@ -5,11 +5,11 @@ from datetime import timedelta import pytest from pytest_unordered import unordered +from homeassistant.components import automation from homeassistant.components.alarm_control_panel import ( DOMAIN, AlarmControlPanelEntityFeature, ) -import homeassistant.components.automation as automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, diff --git a/tests/components/alert/test_init.py b/tests/components/alert/test_init.py index 7c4030b56da..31236c84f34 100644 --- a/tests/components/alert/test_init.py +++ b/tests/components/alert/test_init.py @@ -4,7 +4,7 @@ from copy import deepcopy import pytest -import homeassistant.components.alert as alert +from homeassistant.components import alert, notify from homeassistant.components.alert.const import ( CONF_ALERT_MESSAGE, CONF_DATA, @@ -14,7 +14,6 @@ from homeassistant.components.alert.const import ( CONF_TITLE, DOMAIN, ) -import homeassistant.components.notify as notify from homeassistant.const import ( ATTR_ENTITY_ID, CONF_ENTITY_ID, diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index b7e6a5e53ac..fa8d7a2c9fb 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -5,8 +5,8 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest +from homeassistant.components import camera from homeassistant.components.alexa import smart_home, state_report -import homeassistant.components.camera as camera from homeassistant.components.climate import ClimateEntityFeature from homeassistant.components.cover import CoverDeviceClass, CoverEntityFeature from homeassistant.components.media_player import MediaPlayerEntityFeature diff --git a/tests/components/androidtv/patchers.py b/tests/components/androidtv/patchers.py index 67393a21f41..90a13523ebe 100644 --- a/tests/components/androidtv/patchers.py +++ b/tests/components/androidtv/patchers.py @@ -2,6 +2,7 @@ from unittest.mock import patch +from androidtv.adb_manager.adb_manager_async import DeviceAsync from androidtv.constants import CMD_DEVICE_PROPERTIES, CMD_MAC_ETH0, CMD_MAC_WLAN0 from homeassistant.components.androidtv.const import ( @@ -62,7 +63,7 @@ class ClientAsyncFakeFail: """Initialize a `ClientAsyncFakeFail` instance.""" self._devices = [] - async def device(self, serial): + async def device(self, serial) -> DeviceAsync | None: """Mock the `ClientAsync.device` method when the device is not connected via ADB.""" self._devices = [] return None diff --git a/tests/components/apache_kafka/test_init.py b/tests/components/apache_kafka/test_init.py index 93d9619f0c3..2b702046054 100644 --- a/tests/components/apache_kafka/test_init.py +++ b/tests/components/apache_kafka/test_init.py @@ -9,7 +9,7 @@ from unittest.mock import patch import pytest -import homeassistant.components.apache_kafka as apache_kafka +from homeassistant.components import apache_kafka from homeassistant.const import STATE_ON from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/aprs/test_device_tracker.py b/tests/components/aprs/test_device_tracker.py index ca2a5ce1833..92081111c8b 100644 --- a/tests/components/aprs/test_device_tracker.py +++ b/tests/components/aprs/test_device_tracker.py @@ -7,7 +7,7 @@ import aprslib from aprslib import IS import pytest -import homeassistant.components.aprs.device_tracker as device_tracker +from homeassistant.components.aprs import device_tracker from homeassistant.core import HomeAssistant DEFAULT_PORT = 14580 diff --git a/tests/components/arcam_fmj/test_device_trigger.py b/tests/components/arcam_fmj/test_device_trigger.py index a8510628b26..1b43d27281c 100644 --- a/tests/components/arcam_fmj/test_device_trigger.py +++ b/tests/components/arcam_fmj/test_device_trigger.py @@ -2,8 +2,8 @@ import pytest +from homeassistant.components import automation from homeassistant.components.arcam_fmj.const import DOMAIN -import homeassistant.components.automation as automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er diff --git a/tests/components/assist_pipeline/test_init.py b/tests/components/assist_pipeline/test_init.py index c6f45044cb3..f9b91af3bf1 100644 --- a/tests/components/assist_pipeline/test_init.py +++ b/tests/components/assist_pipeline/test_init.py @@ -727,7 +727,7 @@ def test_pipeline_run_equality(hass: HomeAssistant, init_components) -> None: event_callback=event_callback, ) - assert run_1 == run_1 + assert run_1 == run_1 # noqa: PLR0124 assert run_1 != run_2 assert run_1 != 1234 diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 00a7e6980d7..3e569586a2a 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -9,7 +9,7 @@ from unittest.mock import Mock, patch import pytest from homeassistant import config_entries -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.automation import ( ATTR_SOURCE, DOMAIN, diff --git a/tests/components/baf/__init__.py b/tests/components/baf/__init__.py index 648f235349d..09288c4a874 100644 --- a/tests/components/baf/__init__.py +++ b/tests/components/baf/__init__.py @@ -29,7 +29,6 @@ class MockBAFDevice(Device): """Mock async_wait_available.""" if self._async_wait_available_side_effect: raise self._async_wait_available_side_effect - return def async_run(self): """Mock async_run.""" diff --git a/tests/components/binary_sensor/test_device_condition.py b/tests/components/binary_sensor/test_device_condition.py index 83451313bad..93689b4f233 100644 --- a/tests/components/binary_sensor/test_device_condition.py +++ b/tests/components/binary_sensor/test_device_condition.py @@ -6,7 +6,7 @@ from freezegun import freeze_time import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDeviceClass from homeassistant.components.binary_sensor.device_condition import ENTITY_CONDITIONS from homeassistant.components.device_automation import DeviceAutomationType diff --git a/tests/components/binary_sensor/test_device_trigger.py b/tests/components/binary_sensor/test_device_trigger.py index ad7bd9c3528..76dcdb33993 100644 --- a/tests/components/binary_sensor/test_device_trigger.py +++ b/tests/components/binary_sensor/test_device_trigger.py @@ -5,7 +5,7 @@ from datetime import timedelta import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDeviceClass from homeassistant.components.binary_sensor.device_trigger import ENTITY_TRIGGERS from homeassistant.components.device_automation import DeviceAutomationType diff --git a/tests/components/caldav/test_todo.py b/tests/components/caldav/test_todo.py index 67fc5f7f443..bea4725856e 100644 --- a/tests/components/caldav/test_todo.py +++ b/tests/components/caldav/test_todo.py @@ -624,7 +624,7 @@ async def test_remove_item( assert state.state == "1" def lookup(uid: str) -> Mock: - assert uid == "2" or uid == "3" + assert uid in ("2", "3") if uid == "2": return item1 return item2 diff --git a/tests/components/calendar/test_trigger.py b/tests/components/calendar/test_trigger.py index 050329cd855..54cfd353618 100644 --- a/tests/components/calendar/test_trigger.py +++ b/tests/components/calendar/test_trigger.py @@ -20,8 +20,7 @@ import zoneinfo from freezegun.api import FrozenDateTimeFactory import pytest -from homeassistant.components import calendar -import homeassistant.components.automation as automation +from homeassistant.components import automation, calendar from homeassistant.components.calendar.trigger import EVENT_END, EVENT_START from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF from homeassistant.core import HomeAssistant diff --git a/tests/components/climate/test_device_action.py b/tests/components/climate/test_device_action.py index 3ee5a9b8edd..850f8b6c843 100644 --- a/tests/components/climate/test_device_action.py +++ b/tests/components/climate/test_device_action.py @@ -4,7 +4,7 @@ import pytest from pytest_unordered import unordered import voluptuous_serialize -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.climate import DOMAIN, HVACMode, const, device_action from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.const import EntityCategory diff --git a/tests/components/climate/test_device_condition.py b/tests/components/climate/test_device_condition.py index d9345a0516c..e44802f7d4d 100644 --- a/tests/components/climate/test_device_condition.py +++ b/tests/components/climate/test_device_condition.py @@ -4,7 +4,7 @@ import pytest from pytest_unordered import unordered import voluptuous_serialize -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.climate import DOMAIN, HVACMode, const, device_condition from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.const import EntityCategory diff --git a/tests/components/climate/test_device_trigger.py b/tests/components/climate/test_device_trigger.py index 7dbe106bd4f..af14c42c086 100644 --- a/tests/components/climate/test_device_trigger.py +++ b/tests/components/climate/test_device_trigger.py @@ -4,7 +4,7 @@ import pytest from pytest_unordered import unordered import voluptuous_serialize -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.climate import ( DOMAIN, HVACAction, diff --git a/tests/components/configurator/test_init.py b/tests/components/configurator/test_init.py index 44813f01a18..6c937473ddc 100644 --- a/tests/components/configurator/test_init.py +++ b/tests/components/configurator/test_init.py @@ -2,7 +2,7 @@ from datetime import timedelta -import homeassistant.components.configurator as configurator +from homeassistant.components import configurator from homeassistant.const import ATTR_FRIENDLY_NAME from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util diff --git a/tests/components/cover/test_device_action.py b/tests/components/cover/test_device_action.py index 43bf7431626..e70e8d3a70f 100644 --- a/tests/components/cover/test_device_action.py +++ b/tests/components/cover/test_device_action.py @@ -3,7 +3,7 @@ import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.cover import DOMAIN, CoverEntityFeature from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.const import CONF_PLATFORM, EntityCategory diff --git a/tests/components/cover/test_device_condition.py b/tests/components/cover/test_device_condition.py index a58f94f44f3..d1a542e6608 100644 --- a/tests/components/cover/test_device_condition.py +++ b/tests/components/cover/test_device_condition.py @@ -3,7 +3,7 @@ import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.cover import DOMAIN, CoverEntityFeature from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.const import ( diff --git a/tests/components/cover/test_device_trigger.py b/tests/components/cover/test_device_trigger.py index 5db52b6d618..afd39fe6d8e 100644 --- a/tests/components/cover/test_device_trigger.py +++ b/tests/components/cover/test_device_trigger.py @@ -5,7 +5,7 @@ from datetime import timedelta import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.cover import DOMAIN, CoverEntityFeature from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.const import ( diff --git a/tests/components/cover/test_init.py b/tests/components/cover/test_init.py index ec090b878f2..0052093298e 100644 --- a/tests/components/cover/test_init.py +++ b/tests/components/cover/test_init.py @@ -4,7 +4,7 @@ from enum import Enum import pytest -import homeassistant.components.cover as cover +from homeassistant.components import cover from homeassistant.const import ( ATTR_ENTITY_ID, CONF_PLATFORM, diff --git a/tests/components/datadog/test_init.py b/tests/components/datadog/test_init.py index 51d698186b7..36c1d951078 100644 --- a/tests/components/datadog/test_init.py +++ b/tests/components/datadog/test_init.py @@ -3,7 +3,7 @@ from unittest import mock from unittest.mock import patch -import homeassistant.components.datadog as datadog +from homeassistant.components import datadog from homeassistant.const import EVENT_LOGBOOK_ENTRY, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/demo/test_notify.py b/tests/components/demo/test_notify.py index 54eadc3bd91..0bc7a8bc1d8 100644 --- a/tests/components/demo/test_notify.py +++ b/tests/components/demo/test_notify.py @@ -6,8 +6,8 @@ from unittest.mock import patch import pytest import voluptuous as vol +from homeassistant.components import notify import homeassistant.components.demo.notify as demo -import homeassistant.components.notify as notify from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import discovery from homeassistant.setup import async_setup_component diff --git a/tests/components/demo/test_remote.py b/tests/components/demo/test_remote.py index e2a82248fdf..bcab2fb3de0 100644 --- a/tests/components/demo/test_remote.py +++ b/tests/components/demo/test_remote.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest -import homeassistant.components.remote as remote +from homeassistant.components import remote from homeassistant.components.remote import ATTR_COMMAND from homeassistant.const import ( ATTR_ENTITY_ID, diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index 1a4488e43cd..32e624f1c8c 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -8,8 +8,7 @@ from pytest_unordered import unordered import voluptuous as vol from homeassistant import config_entries, loader -from homeassistant.components import device_automation -import homeassistant.components.automation as automation +from homeassistant.components import automation, device_automation from homeassistant.components.device_automation import ( InvalidDeviceAutomationConfig, toggle_entity, diff --git a/tests/components/device_automation/test_toggle_entity.py b/tests/components/device_automation/test_toggle_entity.py index 59d316545fa..44a29d4a9ba 100644 --- a/tests/components/device_automation/test_toggle_entity.py +++ b/tests/components/device_automation/test_toggle_entity.py @@ -4,7 +4,7 @@ from datetime import timedelta import pytest -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er diff --git a/tests/components/device_tracker/test_device_condition.py b/tests/components/device_tracker/test_device_condition.py index 431840d2f57..18f3d64ec0e 100644 --- a/tests/components/device_tracker/test_device_condition.py +++ b/tests/components/device_tracker/test_device_condition.py @@ -3,7 +3,7 @@ import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.device_tracker import DOMAIN from homeassistant.const import STATE_HOME, EntityCategory diff --git a/tests/components/device_tracker/test_device_trigger.py b/tests/components/device_tracker/test_device_trigger.py index 1bbe2394d8e..67c41b85752 100644 --- a/tests/components/device_tracker/test_device_trigger.py +++ b/tests/components/device_tracker/test_device_trigger.py @@ -4,10 +4,9 @@ import pytest from pytest_unordered import unordered import voluptuous_serialize -import homeassistant.components.automation as automation +from homeassistant.components import automation, zone from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.device_tracker import DOMAIN, device_trigger -import homeassistant.components.zone as zone from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers import ( diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index b36ffdf14f6..cc6cf5c1c1e 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -9,8 +9,7 @@ from unittest.mock import Mock, call, patch import pytest -from homeassistant.components import zone -import homeassistant.components.device_tracker as device_tracker +from homeassistant.components import device_tracker, zone from homeassistant.components.device_tracker import SourceType, const, legacy from homeassistant.const import ( ATTR_ENTITY_PICTURE, diff --git a/tests/components/ecobee/test_climate.py b/tests/components/ecobee/test_climate.py index 0ec4f9cee68..7ea9950e2d4 100644 --- a/tests/components/ecobee/test_climate.py +++ b/tests/components/ecobee/test_climate.py @@ -7,6 +7,7 @@ from unittest.mock import MagicMock import pytest +from homeassistant import const from homeassistant.components import climate from homeassistant.components.climate import ClimateEntityFeature from homeassistant.components.ecobee.climate import ( @@ -14,7 +15,6 @@ from homeassistant.components.ecobee.climate import ( PRESET_AWAY_INDEFINITELY, Thermostat, ) -import homeassistant.const as const from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, STATE_OFF from homeassistant.core import HomeAssistant diff --git a/tests/components/fan/test_device_action.py b/tests/components/fan/test_device_action.py index c08e0617700..96e02ab5592 100644 --- a/tests/components/fan/test_device_action.py +++ b/tests/components/fan/test_device_action.py @@ -3,7 +3,7 @@ import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.fan import DOMAIN from homeassistant.const import EntityCategory diff --git a/tests/components/fan/test_device_condition.py b/tests/components/fan/test_device_condition.py index afd237d1974..72e1dfb4ca2 100644 --- a/tests/components/fan/test_device_condition.py +++ b/tests/components/fan/test_device_condition.py @@ -3,7 +3,7 @@ import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.fan import DOMAIN from homeassistant.const import STATE_OFF, STATE_ON, EntityCategory diff --git a/tests/components/fan/test_device_trigger.py b/tests/components/fan/test_device_trigger.py index 92b6443f241..c121569184f 100644 --- a/tests/components/fan/test_device_trigger.py +++ b/tests/components/fan/test_device_trigger.py @@ -5,7 +5,7 @@ from datetime import timedelta import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.fan import DOMAIN from homeassistant.const import STATE_OFF, STATE_ON, EntityCategory diff --git a/tests/components/foobot/test_sensor.py b/tests/components/foobot/test_sensor.py index d8beae3b77b..d5461ae71c7 100644 --- a/tests/components/foobot/test_sensor.py +++ b/tests/components/foobot/test_sensor.py @@ -6,8 +6,8 @@ from unittest.mock import MagicMock import pytest +from homeassistant.components import sensor from homeassistant.components.foobot import sensor as foobot -import homeassistant.components.sensor as sensor from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, diff --git a/tests/components/fully_kiosk/test_button.py b/tests/components/fully_kiosk/test_button.py index 9bd4c3a897c..4652ee96047 100644 --- a/tests/components/fully_kiosk/test_button.py +++ b/tests/components/fully_kiosk/test_button.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock -import homeassistant.components.button as button +from homeassistant.components import button from homeassistant.components.fully_kiosk.const import DOMAIN from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant diff --git a/tests/components/fully_kiosk/test_media_player.py b/tests/components/fully_kiosk/test_media_player.py index b8719a578aa..b6eff4cfa2c 100644 --- a/tests/components/fully_kiosk/test_media_player.py +++ b/tests/components/fully_kiosk/test_media_player.py @@ -2,8 +2,8 @@ from unittest.mock import MagicMock, Mock, patch +from homeassistant.components import media_player from homeassistant.components.fully_kiosk.const import DOMAIN, MEDIA_SUPPORT_FULLYKIOSK -import homeassistant.components.media_player as media_player from homeassistant.components.media_source import DOMAIN as MS_DOMAIN from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant diff --git a/tests/components/fully_kiosk/test_number.py b/tests/components/fully_kiosk/test_number.py index b4ac50cb076..2fbbf751725 100644 --- a/tests/components/fully_kiosk/test_number.py +++ b/tests/components/fully_kiosk/test_number.py @@ -2,8 +2,8 @@ from unittest.mock import MagicMock +from homeassistant.components import number from homeassistant.components.fully_kiosk.const import DOMAIN, UPDATE_INTERVAL -import homeassistant.components.number as number from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er diff --git a/tests/components/fully_kiosk/test_switch.py b/tests/components/fully_kiosk/test_switch.py index 03ac00ef677..5b3b5e651b0 100644 --- a/tests/components/fully_kiosk/test_switch.py +++ b/tests/components/fully_kiosk/test_switch.py @@ -2,8 +2,8 @@ from unittest.mock import MagicMock +from homeassistant.components import switch from homeassistant.components.fully_kiosk.const import DOMAIN -import homeassistant.components.switch as switch from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index 989e6690630..37a652e3752 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -255,7 +255,6 @@ def mock_events_list( json=resp, exc=exc, ) - return return _put_result @@ -268,7 +267,6 @@ def mock_events_list_items( def _put_items(items: list[dict[str, Any]]) -> None: mock_events_list({"items": items}) - return return _put_items @@ -289,7 +287,6 @@ def mock_calendars_list( json=resp, exc=exc, ) - return return _result @@ -312,7 +309,6 @@ def mock_calendar_get( exc=exc, status=status, ) - return return _result @@ -330,7 +326,6 @@ def mock_insert_event( f"{API_BASE_URL}/calendars/{calendar_id}/events", exc=exc, ) - return return _expect_result diff --git a/tests/components/google_pubsub/test_init.py b/tests/components/google_pubsub/test_init.py index e397ab2c403..a793ade5312 100644 --- a/tests/components/google_pubsub/test_init.py +++ b/tests/components/google_pubsub/test_init.py @@ -3,11 +3,11 @@ from dataclasses import dataclass from datetime import datetime import os -import unittest.mock as mock +from unittest import mock import pytest -import homeassistant.components.google_pubsub as google_pubsub +from homeassistant.components import google_pubsub from homeassistant.components.google_pubsub import DateTimeJSONEncoder as victim from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py index 45846123a80..0f8d487b340 100644 --- a/tests/components/group/test_init.py +++ b/tests/components/group/test_init.py @@ -8,7 +8,7 @@ from unittest.mock import patch import pytest -import homeassistant.components.group as group +from homeassistant.components import group from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_FRIENDLY_NAME, diff --git a/tests/components/group/test_notify.py b/tests/components/group/test_notify.py index 52d049431d8..5709e648508 100644 --- a/tests/components/group/test_notify.py +++ b/tests/components/group/test_notify.py @@ -3,10 +3,10 @@ from unittest.mock import MagicMock, patch from homeassistant import config as hass_config +from homeassistant.components import notify import homeassistant.components.demo.notify as demo from homeassistant.components.group import SERVICE_RELOAD import homeassistant.components.group.notify as group -import homeassistant.components.notify as notify from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/hassio/test_ingress.py b/tests/components/hassio/test_ingress.py index 27e99f7f596..805b5292edb 100644 --- a/tests/components/hassio/test_ingress.py +++ b/tests/components/hassio/test_ingress.py @@ -379,8 +379,8 @@ async def test_ingress_request_get_compressed( # Check we got right response assert resp.status == HTTPStatus.OK - body = await resp.text() - assert body == body + resp_body = await resp.text() + assert resp_body == body assert resp.headers["Content-Encoding"] == "deflate" # Check we forwarded command diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 5d9cb86f9b6..d0712b968bc 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -162,13 +162,11 @@ def test_get_significant_states_without_initial(hass_history) -> None: one_with_microsecond = zero + timedelta(seconds=1, microseconds=1) one_and_half = zero + timedelta(seconds=1.5) for entity_id in states: - states[entity_id] = list( - filter( - lambda s: s.last_changed != one - and s.last_changed != one_with_microsecond, - states[entity_id], - ) - ) + states[entity_id] = [ + s + for s in states[entity_id] + if s.last_changed not in (one, one_with_microsecond) + ] del states["media_player.test2"] hist = get_significant_states( diff --git a/tests/components/history/test_init_db_schema_30.py b/tests/components/history/test_init_db_schema_30.py index ce5c5a4b6c6..2e26256da90 100644 --- a/tests/components/history/test_init_db_schema_30.py +++ b/tests/components/history/test_init_db_schema_30.py @@ -148,7 +148,7 @@ def test_get_significant_states_with_initial(legacy_hass_history) -> None: if entity_id == "media_player.test": states[entity_id] = states[entity_id][1:] for state in states[entity_id]: - if state.last_changed == one or state.last_changed == one_with_microsecond: + if state.last_changed in (one, one_with_microsecond): state.last_changed = one_and_half state.last_updated = one_and_half @@ -177,8 +177,7 @@ def test_get_significant_states_without_initial(legacy_hass_history) -> None: for entity_id in states: states[entity_id] = list( filter( - lambda s: s.last_changed != one - and s.last_changed != one_with_microsecond, + lambda s: s.last_changed not in (one, one_with_microsecond), states[entity_id], ) ) diff --git a/tests/components/homeassistant/triggers/test_event.py b/tests/components/homeassistant/triggers/test_event.py index a0c1f6cb45d..451f35f66fe 100644 --- a/tests/components/homeassistant/triggers/test_event.py +++ b/tests/components/homeassistant/triggers/test_event.py @@ -2,7 +2,7 @@ import pytest -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_OFF from homeassistant.core import Context, HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/homeassistant/triggers/test_homeassistant.py b/tests/components/homeassistant/triggers/test_homeassistant.py index ebe90415018..2afb533cdc0 100644 --- a/tests/components/homeassistant/triggers/test_homeassistant.py +++ b/tests/components/homeassistant/triggers/test_homeassistant.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.core import CoreState, HomeAssistant from homeassistant.helpers.typing import ConfigType from homeassistant.setup import async_setup_component diff --git a/tests/components/homeassistant/triggers/test_numeric_state.py b/tests/components/homeassistant/triggers/test_numeric_state.py index 65c2863d0d7..cf2e1938228 100644 --- a/tests/components/homeassistant/triggers/test_numeric_state.py +++ b/tests/components/homeassistant/triggers/test_numeric_state.py @@ -8,7 +8,7 @@ from freezegun.api import FrozenDateTimeFactory import pytest import voluptuous as vol -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.homeassistant.triggers import ( numeric_state as numeric_state_trigger, ) diff --git a/tests/components/homeassistant/triggers/test_state.py b/tests/components/homeassistant/triggers/test_state.py index 9d1d60031e0..aaf228c06f8 100644 --- a/tests/components/homeassistant/triggers/test_state.py +++ b/tests/components/homeassistant/triggers/test_state.py @@ -6,7 +6,7 @@ from unittest.mock import patch from freezegun.api import FrozenDateTimeFactory import pytest -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.homeassistant.triggers import state as state_trigger from homeassistant.const import ( ATTR_ENTITY_ID, diff --git a/tests/components/homeassistant/triggers/test_time_pattern.py b/tests/components/homeassistant/triggers/test_time_pattern.py index 2d814813ed4..2324599c3c6 100644 --- a/tests/components/homeassistant/triggers/test_time_pattern.py +++ b/tests/components/homeassistant/triggers/test_time_pattern.py @@ -6,8 +6,8 @@ from freezegun.api import FrozenDateTimeFactory import pytest import voluptuous as vol -import homeassistant.components.automation as automation -import homeassistant.components.homeassistant.triggers.time_pattern as time_pattern +from homeassistant.components import automation +from homeassistant.components.homeassistant.triggers import time_pattern from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_OFF from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index c419f7c19e7..17e38a0a145 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -67,7 +67,6 @@ def _mock_socket(failure_attempts: int = 0) -> MagicMock: attempts += 1 if attempts <= failure_attempts: raise OSError - return mock_socket.bind = Mock(side_effect=_simulate_bind) return mock_socket diff --git a/tests/components/homekit_controller/test_device_trigger.py b/tests/components/homekit_controller/test_device_trigger.py index 40f565ec88b..239d170a84f 100644 --- a/tests/components/homekit_controller/test_device_trigger.py +++ b/tests/components/homekit_controller/test_device_trigger.py @@ -5,7 +5,7 @@ from aiohomekit.model.services import ServicesTypes import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.homekit_controller.const import DOMAIN from homeassistant.config_entries import ConfigEntryState diff --git a/tests/components/http/test_ban.py b/tests/components/http/test_ban.py index a10aa740268..91476bf4698 100644 --- a/tests/components/http/test_ban.py +++ b/tests/components/http/test_ban.py @@ -10,7 +10,7 @@ from aiohttp.web_exceptions import HTTPUnauthorized from aiohttp.web_middlewares import middleware import pytest -import homeassistant.components.http as http +from homeassistant.components import http from homeassistant.components.http import KEY_AUTHENTICATED, KEY_HASS from homeassistant.components.http.ban import ( IP_BANS_FILE, diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index 98e97d0fe57..9e892e2ee43 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -13,7 +13,7 @@ import pytest from homeassistant.auth.providers.legacy_api_password import ( LegacyApiPasswordAuthProvider, ) -import homeassistant.components.http as http +from homeassistant.components import http from homeassistant.core import HomeAssistant from homeassistant.helpers.http import KEY_HASS from homeassistant.helpers.network import NoURLAvailableError diff --git a/tests/components/humidifier/test_device_action.py b/tests/components/humidifier/test_device_action.py index 13c41fd8369..567be27721f 100644 --- a/tests/components/humidifier/test_device_action.py +++ b/tests/components/humidifier/test_device_action.py @@ -4,7 +4,7 @@ import pytest from pytest_unordered import unordered import voluptuous_serialize -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.humidifier import DOMAIN, const, device_action from homeassistant.const import STATE_ON, EntityCategory diff --git a/tests/components/humidifier/test_device_condition.py b/tests/components/humidifier/test_device_condition.py index fa17d1bb732..ad4ac78d064 100644 --- a/tests/components/humidifier/test_device_condition.py +++ b/tests/components/humidifier/test_device_condition.py @@ -4,7 +4,7 @@ import pytest from pytest_unordered import unordered import voluptuous_serialize -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.humidifier import DOMAIN, const, device_condition from homeassistant.const import ATTR_MODE, STATE_OFF, STATE_ON, EntityCategory diff --git a/tests/components/humidifier/test_device_trigger.py b/tests/components/humidifier/test_device_trigger.py index 3e05f6b02d1..e064e82a385 100644 --- a/tests/components/humidifier/test_device_trigger.py +++ b/tests/components/humidifier/test_device_trigger.py @@ -6,7 +6,7 @@ import pytest from pytest_unordered import unordered import voluptuous_serialize -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.humidifier import DOMAIN, const, device_trigger from homeassistant.const import ( diff --git a/tests/components/image_processing/test_init.py b/tests/components/image_processing/test_init.py index 6415e4e2a4e..62027552fb0 100644 --- a/tests/components/image_processing/test_init.py +++ b/tests/components/image_processing/test_init.py @@ -4,7 +4,7 @@ from unittest.mock import PropertyMock, patch import pytest -import homeassistant.components.http as http +from homeassistant.components import http import homeassistant.components.image_processing as ip from homeassistant.const import ATTR_ENTITY_PICTURE from homeassistant.core import HomeAssistant diff --git a/tests/components/influxdb/test_init.py b/tests/components/influxdb/test_init.py index 41f14aa78e3..cd95248eb33 100644 --- a/tests/components/influxdb/test_init.py +++ b/tests/components/influxdb/test_init.py @@ -7,7 +7,7 @@ from unittest.mock import ANY, MagicMock, Mock, call, patch import pytest -import homeassistant.components.influxdb as influxdb +from homeassistant.components import influxdb from homeassistant.components.influxdb.const import DEFAULT_BUCKET from homeassistant.const import PERCENTAGE, STATE_OFF, STATE_ON, STATE_STANDBY from homeassistant.core import HomeAssistant, split_entity_id diff --git a/tests/components/influxdb/test_sensor.py b/tests/components/influxdb/test_sensor.py index 395d33004a7..d3464c7e417 100644 --- a/tests/components/influxdb/test_sensor.py +++ b/tests/components/influxdb/test_sensor.py @@ -12,6 +12,7 @@ from influxdb_client.rest import ApiException import pytest from voluptuous import Invalid +from homeassistant.components import sensor from homeassistant.components.influxdb.const import ( API_VERSION_2, DEFAULT_API_VERSION, @@ -22,7 +23,6 @@ from homeassistant.components.influxdb.const import ( TEST_QUERY_V2, ) from homeassistant.components.influxdb.sensor import PLATFORM_SCHEMA -import homeassistant.components.sensor as sensor from homeassistant.const import STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import PLATFORM_NOT_READY_BASE_WAIT_TIME diff --git a/tests/components/kira/test_init.py b/tests/components/kira/test_init.py index 6b6ad4c1fcf..a200c25d2a3 100644 --- a/tests/components/kira/test_init.py +++ b/tests/components/kira/test_init.py @@ -7,7 +7,7 @@ from unittest.mock import patch import pytest -import homeassistant.components.kira as kira +from homeassistant.components import kira from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/knx/conftest.py b/tests/components/knx/conftest.py index bd724029516..92a9e3594ee 100644 --- a/tests/components/knx/conftest.py +++ b/tests/components/knx/conftest.py @@ -279,4 +279,3 @@ def load_knxproj(hass_storage): "version": 1, "data": FIXTURE_PROJECT_DATA, } - return diff --git a/tests/components/kodi/test_device_trigger.py b/tests/components/kodi/test_device_trigger.py index 1737fe5d7c9..2a3c1f7544f 100644 --- a/tests/components/kodi/test_device_trigger.py +++ b/tests/components/kodi/test_device_trigger.py @@ -2,7 +2,7 @@ import pytest -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.kodi import DOMAIN from homeassistant.components.media_player import DOMAIN as MP_DOMAIN diff --git a/tests/components/light/test_device_action.py b/tests/components/light/test_device_action.py index 3f8ed8adbb6..d2a13f22253 100644 --- a/tests/components/light/test_device_action.py +++ b/tests/components/light/test_device_action.py @@ -3,7 +3,7 @@ import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.light import ( ATTR_SUPPORTED_COLOR_MODES, @@ -168,7 +168,7 @@ async def test_get_action_capabilities( capabilities = await async_get_device_automation_capabilities( hass, DeviceAutomationType.ACTION, action ) - assert capabilities == {"extra_fields": []} or capabilities == {} + assert capabilities in ({"extra_fields": []}, {}) @pytest.mark.parametrize( diff --git a/tests/components/light/test_device_condition.py b/tests/components/light/test_device_condition.py index 16237547bc9..caaa51e86fa 100644 --- a/tests/components/light/test_device_condition.py +++ b/tests/components/light/test_device_condition.py @@ -6,7 +6,7 @@ from freezegun import freeze_time import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.light import DOMAIN from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON, EntityCategory diff --git a/tests/components/light/test_device_trigger.py b/tests/components/light/test_device_trigger.py index ff692432d31..ea1c1c66b21 100644 --- a/tests/components/light/test_device_trigger.py +++ b/tests/components/light/test_device_trigger.py @@ -5,7 +5,7 @@ from datetime import timedelta import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.light import DOMAIN from homeassistant.const import STATE_OFF, STATE_ON, EntityCategory diff --git a/tests/components/litejet/test_trigger.py b/tests/components/litejet/test_trigger.py index b9379efdad4..9746ab92cad 100644 --- a/tests/components/litejet/test_trigger.py +++ b/tests/components/litejet/test_trigger.py @@ -8,7 +8,7 @@ from unittest.mock import patch import pytest from homeassistant import setup -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util diff --git a/tests/components/lock/test_device_action.py b/tests/components/lock/test_device_action.py index 3396324284b..3b46117ccd2 100644 --- a/tests/components/lock/test_device_action.py +++ b/tests/components/lock/test_device_action.py @@ -3,7 +3,7 @@ import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.lock import DOMAIN, LockEntityFeature from homeassistant.const import EntityCategory diff --git a/tests/components/lock/test_device_condition.py b/tests/components/lock/test_device_condition.py index 71e1b6ac48e..749e1037662 100644 --- a/tests/components/lock/test_device_condition.py +++ b/tests/components/lock/test_device_condition.py @@ -3,7 +3,7 @@ import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.lock import DOMAIN from homeassistant.const import ( diff --git a/tests/components/lock/test_device_trigger.py b/tests/components/lock/test_device_trigger.py index a45fd7527b5..3f518143285 100644 --- a/tests/components/lock/test_device_trigger.py +++ b/tests/components/lock/test_device_trigger.py @@ -5,7 +5,7 @@ from datetime import timedelta import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.lock import DOMAIN from homeassistant.const import ( diff --git a/tests/components/logentries/test_init.py b/tests/components/logentries/test_init.py index 40e73a86c05..a75d83660a8 100644 --- a/tests/components/logentries/test_init.py +++ b/tests/components/logentries/test_init.py @@ -4,7 +4,7 @@ from unittest.mock import ANY, call, patch import pytest -import homeassistant.components.logentries as logentries +from homeassistant.components import logentries from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/mailbox/test_init.py b/tests/components/mailbox/test_init.py index 8b83f2b0ec7..296a4fbfa6b 100644 --- a/tests/components/mailbox/test_init.py +++ b/tests/components/mailbox/test_init.py @@ -9,7 +9,7 @@ from aiohttp.test_utils import TestClient import pytest from homeassistant.bootstrap import async_setup_component -import homeassistant.components.mailbox as mailbox +from homeassistant.components import mailbox from homeassistant.core import HomeAssistant from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType diff --git a/tests/components/media_player/test_device_condition.py b/tests/components/media_player/test_device_condition.py index 3d020b01c3d..d64161b8409 100644 --- a/tests/components/media_player/test_device_condition.py +++ b/tests/components/media_player/test_device_condition.py @@ -3,7 +3,7 @@ import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.media_player import DOMAIN from homeassistant.const import ( diff --git a/tests/components/media_player/test_device_trigger.py b/tests/components/media_player/test_device_trigger.py index 3f347918f3d..ab11683889d 100644 --- a/tests/components/media_player/test_device_trigger.py +++ b/tests/components/media_player/test_device_trigger.py @@ -5,7 +5,7 @@ from datetime import timedelta import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.media_player import DOMAIN from homeassistant.const import ( diff --git a/tests/components/meraki/test_device_tracker.py b/tests/components/meraki/test_device_tracker.py index d0cd2cf8c5a..d5d61516c08 100644 --- a/tests/components/meraki/test_device_tracker.py +++ b/tests/components/meraki/test_device_tracker.py @@ -5,7 +5,7 @@ import json import pytest -import homeassistant.components.device_tracker as device_tracker +from homeassistant.components import device_tracker from homeassistant.components.device_tracker import legacy from homeassistant.components.meraki.device_tracker import ( CONF_SECRET, diff --git a/tests/components/mfi/test_sensor.py b/tests/components/mfi/test_sensor.py index 24f6a52fa5c..49efdd5dc71 100644 --- a/tests/components/mfi/test_sensor.py +++ b/tests/components/mfi/test_sensor.py @@ -1,7 +1,7 @@ """The tests for the mFi sensor platform.""" from copy import deepcopy -import unittest.mock as mock +from unittest import mock from mficlient.client import FailedToLogin import pytest diff --git a/tests/components/mfi/test_switch.py b/tests/components/mfi/test_switch.py index 6c69787beef..03b5d5f2c0a 100644 --- a/tests/components/mfi/test_switch.py +++ b/tests/components/mfi/test_switch.py @@ -1,6 +1,6 @@ """The tests for the mFi switch platform.""" -import unittest.mock as mock +from unittest import mock import pytest diff --git a/tests/components/mikrotik/test_device_tracker.py b/tests/components/mikrotik/test_device_tracker.py index 89dc37fd781..1eec2132a91 100644 --- a/tests/components/mikrotik/test_device_tracker.py +++ b/tests/components/mikrotik/test_device_tracker.py @@ -8,8 +8,7 @@ from typing import Any from freezegun import freeze_time import pytest -from homeassistant.components import mikrotik -import homeassistant.components.device_tracker as device_tracker +from homeassistant.components import device_tracker, mikrotik from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er diff --git a/tests/components/mochad/test_light.py b/tests/components/mochad/test_light.py index b04f9a13933..872bd3a9d61 100644 --- a/tests/components/mochad/test_light.py +++ b/tests/components/mochad/test_light.py @@ -1,6 +1,6 @@ """The tests for the mochad light platform.""" -import unittest.mock as mock +from unittest import mock import pytest diff --git a/tests/components/mochad/test_switch.py b/tests/components/mochad/test_switch.py index 96c3ba60b65..750dd48296e 100644 --- a/tests/components/mochad/test_switch.py +++ b/tests/components/mochad/test_switch.py @@ -1,6 +1,6 @@ """The tests for the mochad switch platform.""" -import unittest.mock as mock +from unittest import mock import pytest diff --git a/tests/components/mold_indicator/test_sensor.py b/tests/components/mold_indicator/test_sensor.py index 0acea3d03e6..760d82dfedc 100644 --- a/tests/components/mold_indicator/test_sensor.py +++ b/tests/components/mold_indicator/test_sensor.py @@ -2,11 +2,11 @@ import pytest +from homeassistant.components import sensor from homeassistant.components.mold_indicator.sensor import ( ATTR_CRITICAL_TEMP, ATTR_DEWPOINT, ) -import homeassistant.components.sensor as sensor from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, diff --git a/tests/components/mqtt/test_device_trigger.py b/tests/components/mqtt/test_device_trigger.py index 4f4c9a18bd9..465e87205fa 100644 --- a/tests/components/mqtt/test_device_trigger.py +++ b/tests/components/mqtt/test_device_trigger.py @@ -5,7 +5,7 @@ import json import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.mqtt import _LOGGER, DOMAIN, debug_info from homeassistant.core import HomeAssistant, ServiceCall diff --git a/tests/components/mqtt/test_trigger.py b/tests/components/mqtt/test_trigger.py index 90a39bfd4fb..ceb9207e0c2 100644 --- a/tests/components/mqtt/test_trigger.py +++ b/tests/components/mqtt/test_trigger.py @@ -4,7 +4,7 @@ from unittest.mock import ANY import pytest -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_OFF from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/mqtt_room/test_sensor.py b/tests/components/mqtt_room/test_sensor.py index bc1890f08fa..e6fe7db3b8e 100644 --- a/tests/components/mqtt_room/test_sensor.py +++ b/tests/components/mqtt_room/test_sensor.py @@ -6,8 +6,8 @@ from unittest.mock import patch import pytest +from homeassistant.components import sensor from homeassistant.components.mqtt import CONF_QOS, CONF_STATE_TOPIC, DEFAULT_QOS -import homeassistant.components.sensor as sensor from homeassistant.const import ( CONF_DEVICE_ID, CONF_NAME, diff --git a/tests/components/nest/test_api.py b/tests/components/nest/test_api.py index 3e0932c607d..fd07233fa8c 100644 --- a/tests/components/nest/test_api.py +++ b/tests/components/nest/test_api.py @@ -11,6 +11,7 @@ The tests below exercise both cases during integration setup. import time from unittest.mock import patch +from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber import pytest from homeassistant.components.nest.const import API_URL, OAUTH2_TOKEN, SDM_SCOPES @@ -55,7 +56,7 @@ async def test_auth( async def async_new_subscriber( creds, subscription_name, event_loop, async_callback - ): + ) -> GoogleNestSubscriber | None: """Capture credentials for tests.""" nonlocal captured_creds captured_creds = creds @@ -123,7 +124,7 @@ async def test_auth_expired_token( async def async_new_subscriber( creds, subscription_name, event_loop, async_callback - ): + ) -> GoogleNestSubscriber | None: """Capture credentials for tests.""" nonlocal captured_creds captured_creds = creds diff --git a/tests/components/nest/test_device_trigger.py b/tests/components/nest/test_device_trigger.py index 51cf4254614..44fb6bcf701 100644 --- a/tests/components/nest/test_device_trigger.py +++ b/tests/components/nest/test_device_trigger.py @@ -4,7 +4,7 @@ from google_nest_sdm.event import EventMessage import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, diff --git a/tests/components/netatmo/test_device_trigger.py b/tests/components/netatmo/test_device_trigger.py index f7c31d7681c..566bc72426b 100644 --- a/tests/components/netatmo/test_device_trigger.py +++ b/tests/components/netatmo/test_device_trigger.py @@ -3,7 +3,7 @@ import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.netatmo import DOMAIN as NETATMO_DOMAIN from homeassistant.components.netatmo.const import ( diff --git a/tests/components/number/test_device_action.py b/tests/components/number/test_device_action.py index 0c34f1bf53c..92a7cefd467 100644 --- a/tests/components/number/test_device_action.py +++ b/tests/components/number/test_device_action.py @@ -4,7 +4,7 @@ import pytest from pytest_unordered import unordered import voluptuous_serialize -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.number import DOMAIN, device_action from homeassistant.const import EntityCategory diff --git a/tests/components/opengarage/test_button.py b/tests/components/opengarage/test_button.py index 3742b7c8aec..4ace809f564 100644 --- a/tests/components/opengarage/test_button.py +++ b/tests/components/opengarage/test_button.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock -import homeassistant.components.button as button +from homeassistant.components import button from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er diff --git a/tests/components/philips_js/test_device_trigger.py b/tests/components/philips_js/test_device_trigger.py index 3a59e3c6662..3fbac81acbf 100644 --- a/tests/components/philips_js/test_device_trigger.py +++ b/tests/components/philips_js/test_device_trigger.py @@ -3,7 +3,7 @@ import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.philips_js.const import DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/pilight/test_sensor.py b/tests/components/pilight/test_sensor.py index 629f0f13de4..97e031736e5 100644 --- a/tests/components/pilight/test_sensor.py +++ b/tests/components/pilight/test_sensor.py @@ -4,8 +4,7 @@ import logging import pytest -from homeassistant.components import pilight -import homeassistant.components.sensor as sensor +from homeassistant.components import pilight, sensor from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/pjlink/test_media_player.py b/tests/components/pjlink/test_media_player.py index d44bc942290..b2f250103ad 100644 --- a/tests/components/pjlink/test_media_player.py +++ b/tests/components/pjlink/test_media_player.py @@ -9,7 +9,7 @@ from pypjlink import MUTE_AUDIO from pypjlink.projector import ProjectorError import pytest -import homeassistant.components.media_player as media_player +from homeassistant.components import media_player from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/plant/test_init.py b/tests/components/plant/test_init.py index d173544284d..0f79ade2df5 100644 --- a/tests/components/plant/test_init.py +++ b/tests/components/plant/test_init.py @@ -2,7 +2,7 @@ from datetime import datetime, timedelta -import homeassistant.components.plant as plant +from homeassistant.components import plant from homeassistant.components.recorder import Recorder from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, diff --git a/tests/components/plex/test_init.py b/tests/components/plex/test_init.py index a1a05db9d9a..b8dc42d5472 100644 --- a/tests/components/plex/test_init.py +++ b/tests/components/plex/test_init.py @@ -10,7 +10,7 @@ import plexapi import requests import requests_mock -import homeassistant.components.plex.const as const +from homeassistant.components.plex import const from homeassistant.components.plex.models import ( LIVE_TV_SECTION, TRANSIENT_SECTION, diff --git a/tests/components/python_script/test_init.py b/tests/components/python_script/test_init.py index 504d61a0d8d..463d69975b4 100644 --- a/tests/components/python_script/test_init.py +++ b/tests/components/python_script/test_init.py @@ -47,9 +47,9 @@ async def test_setup(hass: HomeAssistant) -> None: ) assert len(mock_ex.mock_calls) == 1 - hass, script, source, data = mock_ex.mock_calls[0][1] + test_hass, script, source, data = mock_ex.mock_calls[0][1] - assert hass is hass + assert test_hass is hass assert script == "hello.py" assert source == "fake source" assert data == {"some": "data"} diff --git a/tests/components/recorder/test_history.py b/tests/components/recorder/test_history.py index 04204cf84a6..ebcb0522e72 100644 --- a/tests/components/recorder/test_history.py +++ b/tests/components/recorder/test_history.py @@ -636,13 +636,11 @@ def test_get_significant_states_without_initial( one_with_microsecond = zero + timedelta(seconds=1, microseconds=1) one_and_half = zero + timedelta(seconds=1.5) for entity_id in states: - states[entity_id] = list( - filter( - lambda s: s.last_changed != one - and s.last_changed != one_with_microsecond, - states[entity_id], - ) - ) + states[entity_id] = [ + s + for s in states[entity_id] + if s.last_changed not in (one, one_with_microsecond) + ] del states["media_player.test2"] del states["thermostat.test3"] diff --git a/tests/components/recorder/test_history_db_schema_30.py b/tests/components/recorder/test_history_db_schema_30.py index 0aaf1ebb094..2d0b3398a87 100644 --- a/tests/components/recorder/test_history_db_schema_30.py +++ b/tests/components/recorder/test_history_db_schema_30.py @@ -384,10 +384,7 @@ def test_get_significant_states_with_initial( if entity_id == "media_player.test": states[entity_id] = states[entity_id][1:] for state in states[entity_id]: - if ( - state.last_changed == one - or state.last_changed == one_with_microsecond - ): + if state.last_changed in (one, one_with_microsecond): state.last_changed = one_and_half state.last_updated = one_and_half @@ -418,13 +415,11 @@ def test_get_significant_states_without_initial( one_with_microsecond = zero + timedelta(seconds=1, microseconds=1) one_and_half = zero + timedelta(seconds=1.5) for entity_id in states: - states[entity_id] = list( - filter( - lambda s: s.last_changed != one - and s.last_changed != one_with_microsecond, - states[entity_id], - ) - ) + states[entity_id] = [ + s + for s in states[entity_id] + if s.last_changed not in (one, one_with_microsecond) + ] del states["media_player.test2"] hist = history.get_significant_states( diff --git a/tests/components/recorder/test_history_db_schema_32.py b/tests/components/recorder/test_history_db_schema_32.py index 9bb6d70b125..5acf07b0604 100644 --- a/tests/components/recorder/test_history_db_schema_32.py +++ b/tests/components/recorder/test_history_db_schema_32.py @@ -408,13 +408,11 @@ def test_get_significant_states_without_initial( one_with_microsecond = zero + timedelta(seconds=1, microseconds=1) one_and_half = zero + timedelta(seconds=1.5) for entity_id in states: - states[entity_id] = list( - filter( - lambda s: s.last_changed != one - and s.last_changed != one_with_microsecond, - states[entity_id], - ) - ) + states[entity_id] = [ + s + for s in states[entity_id] + if s.last_changed not in (one, one_with_microsecond) + ] del states["media_player.test2"] hist = history.get_significant_states( diff --git a/tests/components/recorder/test_history_db_schema_42.py b/tests/components/recorder/test_history_db_schema_42.py index a72345e71bd..e342799c3a8 100644 --- a/tests/components/recorder/test_history_db_schema_42.py +++ b/tests/components/recorder/test_history_db_schema_42.py @@ -638,13 +638,11 @@ def test_get_significant_states_without_initial( one_with_microsecond = zero + timedelta(seconds=1, microseconds=1) one_and_half = zero + timedelta(seconds=1.5) for entity_id in states: - states[entity_id] = list( - filter( - lambda s: s.last_changed != one - and s.last_changed != one_with_microsecond, - states[entity_id], - ) - ) + states[entity_id] = [ + s + for s in states[entity_id] + if s.last_changed not in (one, one_with_microsecond) + ] del states["media_player.test2"] del states["thermostat.test3"] diff --git a/tests/components/remote/test_device_action.py b/tests/components/remote/test_device_action.py index 09c68843872..50a859af446 100644 --- a/tests/components/remote/test_device_action.py +++ b/tests/components/remote/test_device_action.py @@ -3,7 +3,7 @@ import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.remote import DOMAIN from homeassistant.const import EntityCategory diff --git a/tests/components/remote/test_device_condition.py b/tests/components/remote/test_device_condition.py index e7826f4952c..edfa7c5adf9 100644 --- a/tests/components/remote/test_device_condition.py +++ b/tests/components/remote/test_device_condition.py @@ -6,7 +6,7 @@ from freezegun import freeze_time import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.remote import DOMAIN from homeassistant.const import STATE_OFF, STATE_ON, EntityCategory diff --git a/tests/components/remote/test_device_trigger.py b/tests/components/remote/test_device_trigger.py index b77d971e9a6..1f80843be9a 100644 --- a/tests/components/remote/test_device_trigger.py +++ b/tests/components/remote/test_device_trigger.py @@ -5,7 +5,7 @@ from datetime import timedelta import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.remote import DOMAIN from homeassistant.const import STATE_OFF, STATE_ON, EntityCategory diff --git a/tests/components/remote/test_init.py b/tests/components/remote/test_init.py index 15fbb1174c6..575e69015fe 100644 --- a/tests/components/remote/test_init.py +++ b/tests/components/remote/test_init.py @@ -2,7 +2,7 @@ import pytest -import homeassistant.components.remote as remote +from homeassistant.components import remote from homeassistant.components.remote import ( ATTR_ALTERNATIVE, ATTR_COMMAND, diff --git a/tests/components/rest/test_notify.py b/tests/components/rest/test_notify.py index 9f47e74c535..9731388a26e 100644 --- a/tests/components/rest/test_notify.py +++ b/tests/components/rest/test_notify.py @@ -5,7 +5,7 @@ from unittest.mock import patch import respx from homeassistant import config as hass_config -import homeassistant.components.notify as notify +from homeassistant.components import notify from homeassistant.components.rest import DOMAIN from homeassistant.const import SERVICE_RELOAD from homeassistant.core import HomeAssistant diff --git a/tests/components/rfxtrx/test_device_action.py b/tests/components/rfxtrx/test_device_action.py index a717fcf35d6..c678f2dfc62 100644 --- a/tests/components/rfxtrx/test_device_action.py +++ b/tests/components/rfxtrx/test_device_action.py @@ -8,7 +8,7 @@ import pytest from pytest_unordered import unordered import RFXtrx -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.rfxtrx import DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/rfxtrx/test_device_trigger.py b/tests/components/rfxtrx/test_device_trigger.py index 7d24ec3ff6a..629ff897eb7 100644 --- a/tests/components/rfxtrx/test_device_trigger.py +++ b/tests/components/rfxtrx/test_device_trigger.py @@ -7,7 +7,7 @@ from typing import Any, NamedTuple import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.rfxtrx import DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/ring/test_init.py b/tests/components/ring/test_init.py index ba5dd03ba9c..3ca686c37a4 100644 --- a/tests/components/ring/test_init.py +++ b/tests/components/ring/test_init.py @@ -7,7 +7,7 @@ import pytest import requests_mock from ring_doorbell import AuthenticationError, RingError, RingTimeout -import homeassistant.components.ring as ring +from homeassistant.components import ring from homeassistant.components.ring import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.core import HomeAssistant diff --git a/tests/components/sensor/test_device_condition.py b/tests/components/sensor/test_device_condition.py index b633c744205..08de630f025 100644 --- a/tests/components/sensor/test_device_condition.py +++ b/tests/components/sensor/test_device_condition.py @@ -3,7 +3,7 @@ import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.sensor import ( ATTR_STATE_CLASS, diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index 98bea960fcc..bb7337c0144 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -5,7 +5,7 @@ from datetime import timedelta import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.sensor import ( ATTR_STATE_CLASS, diff --git a/tests/components/smtp/test_notify.py b/tests/components/smtp/test_notify.py index b27a7c2d863..dd51cf15992 100644 --- a/tests/components/smtp/test_notify.py +++ b/tests/components/smtp/test_notify.py @@ -7,7 +7,7 @@ from unittest.mock import patch import pytest from homeassistant import config as hass_config -import homeassistant.components.notify as notify +from homeassistant.components import notify from homeassistant.components.smtp.const import DOMAIN from homeassistant.components.smtp.notify import MailNotificationService from homeassistant.const import SERVICE_RELOAD diff --git a/tests/components/snips/test_init.py b/tests/components/snips/test_init.py index 9590473f218..89ee211b38f 100644 --- a/tests/components/snips/test_init.py +++ b/tests/components/snips/test_init.py @@ -7,7 +7,7 @@ import pytest import voluptuous as vol from homeassistant.bootstrap import async_setup_component -import homeassistant.components.snips as snips +from homeassistant.components import snips from homeassistant.core import HomeAssistant from homeassistant.helpers.intent import ServiceIntentHandler, async_register diff --git a/tests/components/statsd/test_init.py b/tests/components/statsd/test_init.py index e24909e3f53..f9222e4bacf 100644 --- a/tests/components/statsd/test_init.py +++ b/tests/components/statsd/test_init.py @@ -6,7 +6,7 @@ from unittest.mock import patch import pytest import voluptuous as vol -import homeassistant.components.statsd as statsd +from homeassistant.components import statsd from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/sun/test_trigger.py b/tests/components/sun/test_trigger.py index f7bdb5eb17b..50e070a4f68 100644 --- a/tests/components/sun/test_trigger.py +++ b/tests/components/sun/test_trigger.py @@ -5,8 +5,7 @@ from datetime import datetime from freezegun import freeze_time import pytest -from homeassistant.components import sun -import homeassistant.components.automation as automation +from homeassistant.components import automation, sun from homeassistant.const import ( ATTR_ENTITY_ID, ENTITY_MATCH_ALL, diff --git a/tests/components/switch/test_device_action.py b/tests/components/switch/test_device_action.py index c35f7261afc..9ad656bcc2b 100644 --- a/tests/components/switch/test_device_action.py +++ b/tests/components/switch/test_device_action.py @@ -3,7 +3,7 @@ import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.switch import DOMAIN from homeassistant.const import EntityCategory diff --git a/tests/components/switch/test_device_condition.py b/tests/components/switch/test_device_condition.py index d69d8a547aa..e351daf2a5b 100644 --- a/tests/components/switch/test_device_condition.py +++ b/tests/components/switch/test_device_condition.py @@ -6,7 +6,7 @@ from freezegun import freeze_time import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.switch import DOMAIN from homeassistant.const import STATE_OFF, STATE_ON, EntityCategory diff --git a/tests/components/switch/test_device_trigger.py b/tests/components/switch/test_device_trigger.py index 874210a32bc..58803b0c6ac 100644 --- a/tests/components/switch/test_device_trigger.py +++ b/tests/components/switch/test_device_trigger.py @@ -5,7 +5,7 @@ from datetime import timedelta import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.switch import DOMAIN from homeassistant.const import STATE_OFF, STATE_ON, EntityCategory diff --git a/tests/components/tag/test_trigger.py b/tests/components/tag/test_trigger.py index 53c511b8594..a034334508f 100644 --- a/tests/components/tag/test_trigger.py +++ b/tests/components/tag/test_trigger.py @@ -2,7 +2,7 @@ import pytest -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.tag import async_scan_tag from homeassistant.components.tag.const import DEVICE_ID, DOMAIN, TAG_ID from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF diff --git a/tests/components/tasmota/test_device_trigger.py b/tests/components/tasmota/test_device_trigger.py index a5d30814b38..8d299a272f7 100644 --- a/tests/components/tasmota/test_device_trigger.py +++ b/tests/components/tasmota/test_device_trigger.py @@ -8,7 +8,7 @@ from hatasmota.switch import TasmotaSwitchTriggerConfig import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.tasmota import _LOGGER from homeassistant.components.tasmota.const import DEFAULT_PREFIX, DOMAIN diff --git a/tests/components/telegram/test_notify.py b/tests/components/telegram/test_notify.py index ee13d8dc47c..e1daf4da074 100644 --- a/tests/components/telegram/test_notify.py +++ b/tests/components/telegram/test_notify.py @@ -3,7 +3,7 @@ from unittest.mock import patch from homeassistant import config as hass_config -import homeassistant.components.notify as notify +from homeassistant.components import notify from homeassistant.components.telegram import DOMAIN from homeassistant.const import SERVICE_RELOAD from homeassistant.core import HomeAssistant diff --git a/tests/components/template/test_light.py b/tests/components/template/test_light.py index a40f093a573..0dfbc0f833d 100644 --- a/tests/components/template/test_light.py +++ b/tests/components/template/test_light.py @@ -2,7 +2,7 @@ import pytest -import homeassistant.components.light as light +from homeassistant.components import light from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, diff --git a/tests/components/template/test_trigger.py b/tests/components/template/test_trigger.py index db7cd3a2471..0d7d765b988 100644 --- a/tests/components/template/test_trigger.py +++ b/tests/components/template/test_trigger.py @@ -6,7 +6,7 @@ from unittest import mock from freezegun.api import FrozenDateTimeFactory import pytest -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.template import trigger as template_trigger from homeassistant.const import ( ATTR_ENTITY_ID, diff --git a/tests/components/text/test_device_action.py b/tests/components/text/test_device_action.py index 6a0e0958558..29e030b034e 100644 --- a/tests/components/text/test_device_action.py +++ b/tests/components/text/test_device_action.py @@ -4,7 +4,7 @@ import pytest from pytest_unordered import unordered import voluptuous_serialize -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.text import DOMAIN, device_action from homeassistant.const import EntityCategory diff --git a/tests/components/universal/test_media_player.py b/tests/components/universal/test_media_player.py index 9df9247900f..008f7aa5162 100644 --- a/tests/components/universal/test_media_player.py +++ b/tests/components/universal/test_media_player.py @@ -7,12 +7,9 @@ import pytest from voluptuous.error import MultipleInvalid from homeassistant import config as hass_config -import homeassistant.components.input_number as input_number -import homeassistant.components.input_select as input_select -import homeassistant.components.media_player as media_player +from homeassistant.components import input_number, input_select, media_player, switch from homeassistant.components.media_player import MediaClass, MediaPlayerEntityFeature from homeassistant.components.media_player.browse_media import BrowseMedia -import homeassistant.components.switch as switch import homeassistant.components.universal.media_player as universal from homeassistant.const import ( SERVICE_RELOAD, diff --git a/tests/components/update/test_device_trigger.py b/tests/components/update/test_device_trigger.py index 31a9ee7b36e..1ffd295bbc9 100644 --- a/tests/components/update/test_device_trigger.py +++ b/tests/components/update/test_device_trigger.py @@ -5,7 +5,7 @@ from datetime import timedelta import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.update import DOMAIN from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON, EntityCategory diff --git a/tests/components/vacuum/test_device_action.py b/tests/components/vacuum/test_device_action.py index fe5b2814a33..fec2ca1bf12 100644 --- a/tests/components/vacuum/test_device_action.py +++ b/tests/components/vacuum/test_device_action.py @@ -3,7 +3,7 @@ import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.vacuum import DOMAIN from homeassistant.const import EntityCategory diff --git a/tests/components/vacuum/test_device_condition.py b/tests/components/vacuum/test_device_condition.py index f8d1368a163..850c69c1757 100644 --- a/tests/components/vacuum/test_device_condition.py +++ b/tests/components/vacuum/test_device_condition.py @@ -3,7 +3,7 @@ import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.vacuum import ( DOMAIN, diff --git a/tests/components/vacuum/test_device_trigger.py b/tests/components/vacuum/test_device_trigger.py index 831d6807b8c..b2273d905c1 100644 --- a/tests/components/vacuum/test_device_trigger.py +++ b/tests/components/vacuum/test_device_trigger.py @@ -5,7 +5,7 @@ from datetime import timedelta import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.vacuum import DOMAIN, STATE_CLEANING, STATE_DOCKED from homeassistant.const import EntityCategory diff --git a/tests/components/water_heater/test_device_action.py b/tests/components/water_heater/test_device_action.py index d456fa7be71..e08721d3e10 100644 --- a/tests/components/water_heater/test_device_action.py +++ b/tests/components/water_heater/test_device_action.py @@ -3,7 +3,7 @@ import pytest from pytest_unordered import unordered -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.water_heater import DOMAIN from homeassistant.const import EntityCategory diff --git a/tests/components/yandex_transport/test_sensor.py b/tests/components/yandex_transport/test_sensor.py index d302ce17a26..5ad9fa92c39 100644 --- a/tests/components/yandex_transport/test_sensor.py +++ b/tests/components/yandex_transport/test_sensor.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, patch import pytest -import homeassistant.components.sensor as sensor +from homeassistant.components import sensor from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index b1ac22d544d..254cf13c556 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -546,6 +546,5 @@ def core_rs(hass_storage): } ], } - return return _storage diff --git a/tests/components/zha/test_alarm_control_panel.py b/tests/components/zha/test_alarm_control_panel.py index 18065420e58..8d3bd76ef61 100644 --- a/tests/components/zha/test_alarm_control_panel.py +++ b/tests/components/zha/test_alarm_control_panel.py @@ -3,8 +3,8 @@ from unittest.mock import AsyncMock, call, patch, sentinel import pytest -import zigpy.profiles.zha as zha -import zigpy.zcl.clusters.security as security +from zigpy.profiles import zha +from zigpy.zcl.clusters import security import zigpy.zcl.foundation as zcl_f from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN diff --git a/tests/components/zha/test_binary_sensor.py b/tests/components/zha/test_binary_sensor.py index 18e78ae7e57..bd9262a41ce 100644 --- a/tests/components/zha/test_binary_sensor.py +++ b/tests/components/zha/test_binary_sensor.py @@ -4,9 +4,7 @@ from unittest.mock import patch import pytest import zigpy.profiles.zha -import zigpy.zcl.clusters.general as general -import zigpy.zcl.clusters.measurement as measurement -import zigpy.zcl.clusters.security as security +from zigpy.zcl.clusters import general, measurement, security from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/zha/test_button.py b/tests/components/zha/test_button.py index 4c0c6845885..97aaf2bd871 100644 --- a/tests/components/zha/test_button.py +++ b/tests/components/zha/test_button.py @@ -15,13 +15,12 @@ from zhaquirks.const import ( from zhaquirks.tuya.ts0601_valve import ParksideTuyaValveManufCluster from zigpy.const import SIG_EP_PROFILE from zigpy.exceptions import ZigbeeException -import zigpy.profiles.zha as zha +from zigpy.profiles import zha from zigpy.quirks import CustomCluster, CustomDevice from zigpy.quirks.v2 import add_to_registry_v2 import zigpy.types as t -import zigpy.zcl.clusters.general as general +from zigpy.zcl.clusters import general, security from zigpy.zcl.clusters.manufacturer_specific import ManufacturerSpecificCluster -import zigpy.zcl.clusters.security as security import zigpy.zcl.foundation as zcl_f from homeassistant.components.button import DOMAIN, SERVICE_PRESS, ButtonDeviceClass diff --git a/tests/components/zha/test_cluster_handlers.py b/tests/components/zha/test_cluster_handlers.py index 60c958f20fe..b8fbd071a6d 100644 --- a/tests/components/zha/test_cluster_handlers.py +++ b/tests/components/zha/test_cluster_handlers.py @@ -19,7 +19,7 @@ import zigpy.zcl.clusters from zigpy.zcl.clusters import CLUSTERS_BY_ID import zigpy.zdo.types as zdo_t -import homeassistant.components.zha.core.cluster_handlers as cluster_handlers +from homeassistant.components.zha.core import cluster_handlers, registries from homeassistant.components.zha.core.cluster_handlers.lighting import ( ColorClusterHandler, ) @@ -27,7 +27,6 @@ import homeassistant.components.zha.core.const as zha_const from homeassistant.components.zha.core.device import ZHADevice from homeassistant.components.zha.core.endpoint import Endpoint from homeassistant.components.zha.core.helpers import get_zha_gateway -import homeassistant.components.zha.core.registries as registries from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError diff --git a/tests/components/zha/test_cover.py b/tests/components/zha/test_cover.py index a1b320097e8..5f6dac885f2 100644 --- a/tests/components/zha/test_cover.py +++ b/tests/components/zha/test_cover.py @@ -6,8 +6,7 @@ from unittest.mock import patch import pytest import zigpy.profiles.zha import zigpy.types -import zigpy.zcl.clusters.closures as closures -import zigpy.zcl.clusters.general as general +from zigpy.zcl.clusters import closures, general import zigpy.zcl.foundation as zcl_f from homeassistant.components.cover import ( diff --git a/tests/components/zha/test_device.py b/tests/components/zha/test_device.py index 48eecdd87d4..289442b3466 100644 --- a/tests/components/zha/test_device.py +++ b/tests/components/zha/test_device.py @@ -9,7 +9,7 @@ from unittest.mock import patch import pytest import zigpy.profiles.zha import zigpy.types -import zigpy.zcl.clusters.general as general +from zigpy.zcl.clusters import general import zigpy.zdo.types as zdo_t from homeassistant.components.zha.core.const import ( diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py index a7b66dea8d7..bc478532859 100644 --- a/tests/components/zha/test_device_action.py +++ b/tests/components/zha/test_device_action.py @@ -6,11 +6,10 @@ import pytest from pytest_unordered import unordered from zhaquirks.inovelli.VZM31SN import InovelliVZM31SNv11 import zigpy.profiles.zha -import zigpy.zcl.clusters.general as general -import zigpy.zcl.clusters.security as security +from zigpy.zcl.clusters import general, security import zigpy.zcl.foundation as zcl_f -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.zha import DOMAIN from homeassistant.const import Platform diff --git a/tests/components/zha/test_device_tracker.py b/tests/components/zha/test_device_tracker.py index 89ea788e5ef..64360c8b2ff 100644 --- a/tests/components/zha/test_device_tracker.py +++ b/tests/components/zha/test_device_tracker.py @@ -6,7 +6,7 @@ from unittest.mock import patch import pytest import zigpy.profiles.zha -import zigpy.zcl.clusters.general as general +from zigpy.zcl.clusters import general from homeassistant.components.device_tracker import SourceType from homeassistant.components.zha.core.registries import ( diff --git a/tests/components/zha/test_device_trigger.py b/tests/components/zha/test_device_trigger.py index f9141795ef1..2cb7c8c94e7 100644 --- a/tests/components/zha/test_device_trigger.py +++ b/tests/components/zha/test_device_trigger.py @@ -7,9 +7,9 @@ from unittest.mock import patch import pytest from zigpy.application import ControllerApplication import zigpy.profiles.zha -import zigpy.zcl.clusters.general as general +from zigpy.zcl.clusters import general -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, diff --git a/tests/components/zha/test_diagnostics.py b/tests/components/zha/test_diagnostics.py index 3493d772a6f..50b07b70e8d 100644 --- a/tests/components/zha/test_diagnostics.py +++ b/tests/components/zha/test_diagnostics.py @@ -3,8 +3,8 @@ from unittest.mock import patch import pytest -import zigpy.profiles.zha as zha -import zigpy.zcl.clusters.security as security +from zigpy.profiles import zha +from zigpy.zcl.clusters import security from homeassistant.components.diagnostics import REDACTED from homeassistant.components.zha.core.device import ZHADevice diff --git a/tests/components/zha/test_discover.py b/tests/components/zha/test_discover.py index c32b9252f4d..16733d69109 100644 --- a/tests/components/zha/test_discover.py +++ b/tests/components/zha/test_discover.py @@ -40,7 +40,7 @@ import zigpy.zcl.clusters.general import zigpy.zcl.clusters.security import zigpy.zcl.foundation as zcl_f -import homeassistant.components.zha.core.cluster_handlers as cluster_handlers +from homeassistant.components.zha.core import cluster_handlers import homeassistant.components.zha.core.const as zha_const from homeassistant.components.zha.core.device import ZHADevice import homeassistant.components.zha.core.discovery as disc diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py index 182cc2c4752..d090ac8aba0 100644 --- a/tests/components/zha/test_gateway.py +++ b/tests/components/zha/test_gateway.py @@ -5,10 +5,9 @@ from unittest.mock import MagicMock, PropertyMock, patch import pytest from zigpy.application import ControllerApplication -import zigpy.profiles.zha as zha +from zigpy.profiles import zha import zigpy.types -import zigpy.zcl.clusters.general as general -import zigpy.zcl.clusters.lighting as lighting +from zigpy.zcl.clusters import general, lighting import zigpy.zdo.types from homeassistant.components.zha.core.gateway import ZHAGateway diff --git a/tests/components/zha/test_helpers.py b/tests/components/zha/test_helpers.py index fed8fe5bb91..0615fefd644 100644 --- a/tests/components/zha/test_helpers.py +++ b/tests/components/zha/test_helpers.py @@ -6,11 +6,10 @@ from unittest.mock import patch import pytest import voluptuous_serialize -import zigpy.profiles.zha as zha +from zigpy.profiles import zha from zigpy.quirks.v2.homeassistant import UnitOfPower as QuirksUnitOfPower from zigpy.types.basic import uint16_t -import zigpy.zcl.clusters.general as general -import zigpy.zcl.clusters.lighting as lighting +from zigpy.zcl.clusters import general, lighting from homeassistant.components.zha.core.helpers import ( cluster_command_schema_to_vol_schema, diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index a6473c6007c..762ab14cbaa 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -4,9 +4,8 @@ from datetime import timedelta from unittest.mock import AsyncMock, call, patch, sentinel import pytest -import zigpy.profiles.zha as zha -import zigpy.zcl.clusters.general as general -import zigpy.zcl.clusters.lighting as lighting +from zigpy.profiles import zha +from zigpy.zcl.clusters import general, lighting import zigpy.zcl.foundation as zcl_f from homeassistant.components.light import ( @@ -1631,10 +1630,7 @@ async def test_zha_group_light_entity( device_2_entity_id = find_entity_id(Platform.LIGHT, device_light_2, hass) device_3_entity_id = find_entity_id(Platform.LIGHT, device_light_3, hass) - assert ( - device_1_entity_id != device_2_entity_id - and device_1_entity_id != device_3_entity_id - ) + assert device_1_entity_id not in (device_2_entity_id, device_3_entity_id) assert device_2_entity_id != device_3_entity_id group_entity_id = async_find_group_entity_id(hass, Platform.LIGHT, zha_group) diff --git a/tests/components/zha/test_lock.py b/tests/components/zha/test_lock.py index 52b1d891dfd..b16d7a31828 100644 --- a/tests/components/zha/test_lock.py +++ b/tests/components/zha/test_lock.py @@ -4,8 +4,7 @@ from unittest.mock import patch import pytest import zigpy.profiles.zha -import zigpy.zcl.clusters.closures as closures -import zigpy.zcl.clusters.general as general +from zigpy.zcl.clusters import closures, general import zigpy.zcl.foundation as zcl_f from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN diff --git a/tests/components/zha/test_logbook.py b/tests/components/zha/test_logbook.py index 889c73362ae..317e10346f0 100644 --- a/tests/components/zha/test_logbook.py +++ b/tests/components/zha/test_logbook.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest import zigpy.profiles.zha -import zigpy.zcl.clusters.general as general +from zigpy.zcl.clusters import general from homeassistant.components.zha.core.const import ZHA_EVENT from homeassistant.const import CONF_DEVICE_ID, CONF_UNIQUE_ID, Platform diff --git a/tests/components/zha/test_number.py b/tests/components/zha/test_number.py index a9fb3dd9509..6bb1703a229 100644 --- a/tests/components/zha/test_number.py +++ b/tests/components/zha/test_number.py @@ -5,8 +5,7 @@ from unittest.mock import call, patch import pytest from zigpy.exceptions import ZigbeeException from zigpy.profiles import zha -import zigpy.zcl.clusters.general as general -import zigpy.zcl.clusters.lighting as lighting +from zigpy.zcl.clusters import general, lighting import zigpy.zcl.foundation as zcl_f from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN diff --git a/tests/components/zha/test_registries.py b/tests/components/zha/test_registries.py index 29020aa4313..279975a260f 100644 --- a/tests/components/zha/test_registries.py +++ b/tests/components/zha/test_registries.py @@ -9,8 +9,8 @@ import pytest 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 -import homeassistant.components.zha.core.registries as registries from homeassistant.helpers import entity_registry as er if typing.TYPE_CHECKING: diff --git a/tests/components/zha/test_select.py b/tests/components/zha/test_select.py index bb1c5ca270a..97aed05dcd3 100644 --- a/tests/components/zha/test_select.py +++ b/tests/components/zha/test_select.py @@ -11,13 +11,12 @@ from zhaquirks import ( PROFILE_ID, ) from zigpy.const import SIG_EP_PROFILE -import zigpy.profiles.zha as zha +from zigpy.profiles import zha from zigpy.quirks import CustomCluster, CustomDevice from zigpy.quirks.v2 import CustomDeviceV2, add_to_registry_v2 import zigpy.types as t -import zigpy.zcl.clusters.general as general +from zigpy.zcl.clusters import general, security from zigpy.zcl.clusters.manufacturer_specific import ManufacturerSpecificCluster -import zigpy.zcl.clusters.security as security from homeassistant.components.zha.select import AqaraMotionSensitivities from homeassistant.const import STATE_UNKNOWN, EntityCategory, Platform diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index 8d0ef8107e3..59da8332b27 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -672,7 +672,6 @@ def core_rs(hass_storage): } ], } - return return _storage diff --git a/tests/components/zha/test_siren.py b/tests/components/zha/test_siren.py index f5486d91c0f..652955ef98d 100644 --- a/tests/components/zha/test_siren.py +++ b/tests/components/zha/test_siren.py @@ -5,10 +5,9 @@ from unittest.mock import ANY, call, patch import pytest from zigpy.const import SIG_EP_PROFILE -import zigpy.profiles.zha as zha +from zigpy.profiles import zha import zigpy.zcl -import zigpy.zcl.clusters.general as general -import zigpy.zcl.clusters.security as security +from zigpy.zcl.clusters import general, security import zigpy.zcl.foundation as zcl_f from homeassistant.components.siren import ( diff --git a/tests/components/zha/test_switch.py b/tests/components/zha/test_switch.py index 644062198f9..c8c2842c400 100644 --- a/tests/components/zha/test_switch.py +++ b/tests/components/zha/test_switch.py @@ -11,12 +11,11 @@ from zhaquirks.const import ( PROFILE_ID, ) from zigpy.exceptions import ZigbeeException -import zigpy.profiles.zha as zha +from zigpy.profiles import zha from zigpy.quirks import _DEVICE_REGISTRY, CustomCluster, CustomDevice from zigpy.quirks.v2 import CustomDeviceV2, add_to_registry_v2 import zigpy.types as t -import zigpy.zcl.clusters.closures as closures -import zigpy.zcl.clusters.general as general +from zigpy.zcl.clusters import closures, general from zigpy.zcl.clusters.manufacturer_specific import ManufacturerSpecificCluster import zigpy.zcl.foundation as zcl_f diff --git a/tests/components/zha/test_update.py b/tests/components/zha/test_update.py index 854c08985ac..60cd5bf9ff9 100644 --- a/tests/components/zha/test_update.py +++ b/tests/components/zha/test_update.py @@ -7,10 +7,10 @@ from zigpy.exceptions import DeliveryError from zigpy.ota import OtaImageWithMetadata import zigpy.ota.image as firmware from zigpy.ota.providers import BaseOtaImageMetadata -import zigpy.profiles.zha as zha +from zigpy.profiles import zha import zigpy.types as t -import zigpy.zcl.clusters.general as general -import zigpy.zcl.foundation as foundation +from zigpy.zcl import foundation +from zigpy.zcl.clusters import general from homeassistant.components.homeassistant import ( DOMAIN as HA_DOMAIN, diff --git a/tests/components/zha/test_websocket_api.py b/tests/components/zha/test_websocket_api.py index 623d7acf602..927da4ed2c0 100644 --- a/tests/components/zha/test_websocket_api.py +++ b/tests/components/zha/test_websocket_api.py @@ -15,9 +15,8 @@ import zigpy.profiles.zha import zigpy.types from zigpy.types.named import EUI64 import zigpy.util -import zigpy.zcl.clusters.general as general +from zigpy.zcl.clusters import general, security from zigpy.zcl.clusters.general import Groups -import zigpy.zcl.clusters.security as security import zigpy.zdo.types as zdo_types from homeassistant.components.websocket_api import const diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index 1b279fd0f51..701bc342760 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -8,7 +8,7 @@ from freezegun import freeze_time import pytest import voluptuous as vol -import homeassistant.components.automation as automation +from homeassistant.components import automation from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ( ATTR_DEVICE_CLASS, diff --git a/tests/helpers/test_registry.py b/tests/helpers/test_registry.py index 46b04b05fe3..0218267452a 100644 --- a/tests/helpers/test_registry.py +++ b/tests/helpers/test_registry.py @@ -21,10 +21,10 @@ class SampleRegistry(BaseRegistry): self._store = storage.Store(hass, 1, "test") self.save_calls = 0 - def _data_to_save(self) -> None: + def _data_to_save(self) -> dict[str, Any]: """Return data of registry to save.""" self.save_calls += 1 - return None + return {} @pytest.mark.parametrize( diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 86fb84eb582..53499c4f88c 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -16,7 +16,7 @@ import voluptuous as vol # Otherwise can't test just this file (import order issue) from homeassistant import config_entries, exceptions -import homeassistant.components.scene as scene +from homeassistant.components import scene from homeassistant.const import ( ATTR_ENTITY_ID, CONF_DEVICE_ID, diff --git a/tests/helpers/test_sun.py b/tests/helpers/test_sun.py index b6dc1616a48..da436d799aa 100644 --- a/tests/helpers/test_sun.py +++ b/tests/helpers/test_sun.py @@ -7,7 +7,7 @@ import pytest from homeassistant.const import SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET from homeassistant.core import HomeAssistant -import homeassistant.helpers.sun as sun +from homeassistant.helpers import sun import homeassistant.util.dt as dt_util diff --git a/tests/ruff.toml b/tests/ruff.toml index 1a8876b9171..b88a5f1689e 100644 --- a/tests/ruff.toml +++ b/tests/ruff.toml @@ -5,7 +5,6 @@ extend = "../pyproject.toml" extend-ignore = [ "PLC", # pylint - "PLR", # pylint "B904", # Use raise from to specify exception cause "N815", # Variable {name} in class scope should not be mixedCase ] diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index a77e5bf504a..96d63206cfc 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -6,7 +6,7 @@ from unittest.mock import patch import pytest from homeassistant.config import YAML_CONFIG_FILE -import homeassistant.scripts.check_config as check_config +from homeassistant.scripts import check_config from tests.common import get_test_config_dir diff --git a/tests/scripts/test_init.py b/tests/scripts/test_init.py index 44edece1812..002ade2baef 100644 --- a/tests/scripts/test_init.py +++ b/tests/scripts/test_init.py @@ -2,7 +2,7 @@ from unittest.mock import patch -import homeassistant.scripts as scripts +from homeassistant import scripts @patch("homeassistant.scripts.get_default_config_dir", return_value="/default") diff --git a/tests/test_config.py b/tests/test_config.py index c20e2822592..49cc2bb573f 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -39,8 +39,11 @@ from homeassistant.core import ( HomeAssistantError, ) from homeassistant.exceptions import ConfigValidationError -from homeassistant.helpers import config_validation as cv, issue_registry as ir -import homeassistant.helpers.check_config as check_config +from homeassistant.helpers import ( + check_config, + config_validation as cv, + issue_registry as ir, +) from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType from homeassistant.loader import Integration, async_get_integration diff --git a/tests/test_loader.py b/tests/test_loader.py index 4442fe5fd82..e73029e14e2 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -1349,7 +1349,7 @@ async def test_async_get_component_concurrent_loads( modules_without_integration = { k: v for k, v in sys.modules.items() - if k != config_flow_module_name and k != integration.pkg_path + if k not in (config_flow_module_name, integration.pkg_path) } with ( patch.dict( @@ -1737,7 +1737,7 @@ async def test_async_get_platforms_concurrent_loads( modules_without_button = { k: v for k, v in sys.modules.items() - if k != button_module_name and k != integration.pkg_path + if k not in (button_module_name, integration.pkg_path) } with ( patch.dict( diff --git a/tests/util/test_dt.py b/tests/util/test_dt.py index 5716e4e524c..7ed8154f033 100644 --- a/tests/util/test_dt.py +++ b/tests/util/test_dt.py @@ -88,12 +88,6 @@ def test_as_local_with_naive_object() -> None: ) < timedelta(seconds=1) -def test_as_local_with_local_object() -> None: - """Test local with local object.""" - now = dt_util.now() - assert now == now - - def test_as_local_with_utc_object() -> None: """Test local time with UTC object.""" dt_util.set_default_time_zone(dt_util.get_time_zone(TEST_TIME_ZONE)) diff --git a/tests/util/test_package.py b/tests/util/test_package.py index 0e2e9278676..2ead327bf10 100644 --- a/tests/util/test_package.py +++ b/tests/util/test_package.py @@ -10,7 +10,7 @@ from unittest.mock import MagicMock, call, patch import pytest -import homeassistant.util.package as package +from homeassistant.util import package RESOURCE_DIR = os.path.abspath( os.path.join(os.path.dirname(__file__), "..", "resources") diff --git a/tests/util/yaml/test_init.py b/tests/util/yaml/test_init.py index dba8e9b8017..113a348c1d1 100644 --- a/tests/util/yaml/test_init.py +++ b/tests/util/yaml/test_init.py @@ -16,7 +16,7 @@ import yaml as pyyaml from homeassistant.config import YAML_CONFIG_FILE, load_yaml_config_file from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -import homeassistant.util.yaml as yaml +from homeassistant.util import yaml from homeassistant.util.yaml import loader as yaml_loader from tests.common import extract_stack_to_frame, get_test_config_dir, patch_yaml_files From ea7f2af966a3a46c1670f14d8cc5315537f3aca0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 31 Mar 2024 23:23:38 -1000 Subject: [PATCH 122/967] Fix missing mocking in blink tests (#114540) extracted from #114539 --- tests/components/blink/conftest.py | 1 + tests/components/blink/test_init.py | 12 ++++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/components/blink/conftest.py b/tests/components/blink/conftest.py index b18fdf7615e..c6e3ee0960d 100644 --- a/tests/components/blink/conftest.py +++ b/tests/components/blink/conftest.py @@ -60,6 +60,7 @@ def blink_api_fixture(camera) -> MagicMock: mock_blink_api.refresh = AsyncMock(return_value=True) mock_blink_api.sync = MagicMock(return_value=True) mock_blink_api.cameras = {camera.name: camera} + mock_blink_api.request_homescreen = AsyncMock(return_value=True) with patch("homeassistant.components.blink.Blink") as class_mock: class_mock.return_value = mock_blink_api diff --git a/tests/components/blink/test_init.py b/tests/components/blink/test_init.py index 1f3a4c956c4..46806ef3349 100644 --- a/tests/components/blink/test_init.py +++ b/tests/components/blink/test_init.py @@ -1,8 +1,9 @@ """Test the Blink init.""" -from unittest.mock import AsyncMock, MagicMock +from unittest.mock import AsyncMock, MagicMock, patch from aiohttp import ClientError +from blinkpy.auth import LoginError import pytest from homeassistant.components.blink.const import ( @@ -53,9 +54,16 @@ async def test_setup_not_ready_authkey_required( """Test setup failed because 2FA is needed to connect to the Blink system.""" mock_blink_auth_api.check_key_required = MagicMock(return_value=True) + mock_blink_auth_api.send_auth_key = AsyncMock(return_value=False) mock_config_entry.add_to_hass(hass) - assert not await hass.config_entries.async_setup(mock_config_entry.entry_id) + with patch( + "homeassistant.components.blink.config_flow.Auth.startup", + side_effect=LoginError, + ): + assert not 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 From ad3577804bc3bd6f6c90a449780afd4c756f4f78 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 1 Apr 2024 14:12:24 +0200 Subject: [PATCH 123/967] Ensure coverage entries are sorted (#114424) * Ensure coverage entries are sorted * Use autofix * Adjust * Add comment to coverage file * test CI * revert CI test --------- Co-authored-by: Martin Hjelmare --- .coveragerc | 112 +++++++++++++++++++----------------- script/hassfest/coverage.py | 78 ++++++++++++++++++++++++- 2 files changed, 135 insertions(+), 55 deletions(-) diff --git a/.coveragerc b/.coveragerc index d51cc28c7fc..e32db823542 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,3 +1,7 @@ +# Sorted by hassfest. +# +# To sort, run python3 -m script.hassfest -p coverage + [run] source = homeassistant omit = @@ -103,10 +107,10 @@ omit = homeassistant/components/aurora/sensor.py homeassistant/components/avea/light.py homeassistant/components/avion/light.py + homeassistant/components/awair/coordinator.py homeassistant/components/azure_devops/__init__.py homeassistant/components/azure_devops/sensor.py homeassistant/components/azure_service_bus/* - homeassistant/components/awair/coordinator.py homeassistant/components/baf/__init__.py homeassistant/components/baf/climate.py homeassistant/components/baf/entity.py @@ -190,8 +194,8 @@ omit = homeassistant/components/comelit/alarm_control_panel.py homeassistant/components/comelit/climate.py homeassistant/components/comelit/const.py - homeassistant/components/comelit/cover.py homeassistant/components/comelit/coordinator.py + homeassistant/components/comelit/cover.py homeassistant/components/comelit/humidifier.py homeassistant/components/comelit/light.py homeassistant/components/comelit/sensor.py @@ -239,8 +243,8 @@ omit = homeassistant/components/dominos/* homeassistant/components/doods/* homeassistant/components/doorbird/__init__.py - homeassistant/components/doorbird/camera.py homeassistant/components/doorbird/button.py + homeassistant/components/doorbird/camera.py homeassistant/components/doorbird/device.py homeassistant/components/doorbird/entity.py homeassistant/components/doorbird/util.py @@ -260,12 +264,12 @@ omit = homeassistant/components/dunehd/__init__.py homeassistant/components/dunehd/media_player.py homeassistant/components/duotecno/__init__.py - homeassistant/components/duotecno/entity.py - homeassistant/components/duotecno/switch.py - homeassistant/components/duotecno/cover.py - homeassistant/components/duotecno/light.py - homeassistant/components/duotecno/climate.py homeassistant/components/duotecno/binary_sensor.py + homeassistant/components/duotecno/climate.py + homeassistant/components/duotecno/cover.py + homeassistant/components/duotecno/entity.py + homeassistant/components/duotecno/light.py + homeassistant/components/duotecno/switch.py homeassistant/components/dwd_weather_warnings/const.py homeassistant/components/dwd_weather_warnings/coordinator.py homeassistant/components/dwd_weather_warnings/sensor.py @@ -305,10 +309,12 @@ omit = homeassistant/components/edl21/__init__.py homeassistant/components/edl21/sensor.py homeassistant/components/egardia/* + homeassistant/components/electrasmart/__init__.py + homeassistant/components/electrasmart/climate.py homeassistant/components/electric_kiwi/__init__.py homeassistant/components/electric_kiwi/api.py - homeassistant/components/electric_kiwi/oauth2.py homeassistant/components/electric_kiwi/coordinator.py + homeassistant/components/electric_kiwi/oauth2.py homeassistant/components/electric_kiwi/select.py homeassistant/components/eliqonline/sensor.py homeassistant/components/elkm1/__init__.py @@ -381,11 +387,11 @@ omit = homeassistant/components/ezviz/binary_sensor.py homeassistant/components/ezviz/button.py homeassistant/components/ezviz/camera.py + homeassistant/components/ezviz/coordinator.py + homeassistant/components/ezviz/entity.py homeassistant/components/ezviz/image.py homeassistant/components/ezviz/light.py - homeassistant/components/ezviz/coordinator.py homeassistant/components/ezviz/number.py - homeassistant/components/ezviz/entity.py homeassistant/components/ezviz/select.py homeassistant/components/ezviz/sensor.py homeassistant/components/ezviz/siren.py @@ -534,8 +540,8 @@ omit = homeassistant/components/hive/switch.py homeassistant/components/hive/water_heater.py homeassistant/components/hko/__init__.py - homeassistant/components/hko/weather.py homeassistant/components/hko/coordinator.py + homeassistant/components/hko/weather.py homeassistant/components/hlk_sw16/__init__.py homeassistant/components/hlk_sw16/switch.py homeassistant/components/home_connect/__init__.py @@ -573,9 +579,9 @@ omit = homeassistant/components/hunterdouglas_powerview/sensor.py homeassistant/components/hunterdouglas_powerview/shade_data.py homeassistant/components/hunterdouglas_powerview/util.py - homeassistant/components/hvv_departures/__init__.py homeassistant/components/huum/__init__.py homeassistant/components/huum/climate.py + homeassistant/components/hvv_departures/__init__.py homeassistant/components/hvv_departures/binary_sensor.py homeassistant/components/hvv_departures/sensor.py homeassistant/components/ialarm/alarm_control_panel.py @@ -668,9 +674,9 @@ omit = homeassistant/components/keyboard/* homeassistant/components/keyboard_remote/* homeassistant/components/keymitt_ble/__init__.py + homeassistant/components/keymitt_ble/coordinator.py homeassistant/components/keymitt_ble/entity.py homeassistant/components/keymitt_ble/switch.py - homeassistant/components/keymitt_ble/coordinator.py homeassistant/components/kitchen_sink/weather.py homeassistant/components/kiwi/lock.py homeassistant/components/kodi/__init__.py @@ -841,8 +847,15 @@ omit = homeassistant/components/mysensors/switch.py homeassistant/components/mystrom/binary_sensor.py homeassistant/components/mystrom/light.py - homeassistant/components/mystrom/switch.py homeassistant/components/mystrom/sensor.py + homeassistant/components/mystrom/switch.py + homeassistant/components/myuplink/__init__.py + homeassistant/components/myuplink/api.py + homeassistant/components/myuplink/application_credentials.py + homeassistant/components/myuplink/coordinator.py + homeassistant/components/myuplink/entity.py + homeassistant/components/myuplink/helpers.py + homeassistant/components/myuplink/sensor.py homeassistant/components/nad/media_player.py homeassistant/components/nanoleaf/__init__.py homeassistant/components/nanoleaf/button.py @@ -850,13 +863,13 @@ omit = homeassistant/components/nanoleaf/light.py homeassistant/components/neato/__init__.py homeassistant/components/neato/api.py + homeassistant/components/neato/button.py homeassistant/components/neato/camera.py homeassistant/components/neato/entity.py homeassistant/components/neato/hub.py homeassistant/components/neato/sensor.py homeassistant/components/neato/switch.py homeassistant/components/neato/vacuum.py - homeassistant/components/neato/button.py homeassistant/components/nederlandse_spoorwegen/sensor.py homeassistant/components/netdata/sensor.py homeassistant/components/netgear/__init__.py @@ -966,10 +979,10 @@ omit = homeassistant/components/openweathermap/weather.py homeassistant/components/openweathermap/weather_update_coordinator.py homeassistant/components/opnsense/__init__.py + homeassistant/components/opnsense/device_tracker.py homeassistant/components/opower/__init__.py homeassistant/components/opower/coordinator.py homeassistant/components/opower/sensor.py - homeassistant/components/opnsense/device_tracker.py homeassistant/components/opple/light.py homeassistant/components/oru/* homeassistant/components/orvibo/switch.py @@ -1096,17 +1109,6 @@ omit = homeassistant/components/rainmachine/switch.py homeassistant/components/rainmachine/update.py homeassistant/components/rainmachine/util.py - homeassistant/components/renson/__init__.py - homeassistant/components/renson/const.py - homeassistant/components/renson/coordinator.py - homeassistant/components/renson/entity.py - homeassistant/components/renson/sensor.py - homeassistant/components/renson/button.py - homeassistant/components/renson/fan.py - homeassistant/components/renson/switch.py - homeassistant/components/renson/binary_sensor.py - homeassistant/components/renson/number.py - homeassistant/components/renson/time.py homeassistant/components/raspyrfm/* homeassistant/components/recollect_waste/sensor.py homeassistant/components/recorder/repack.py @@ -1121,6 +1123,17 @@ omit = homeassistant/components/rejseplanen/sensor.py homeassistant/components/remember_the_milk/__init__.py homeassistant/components/remote_rpi_gpio/* + homeassistant/components/renson/__init__.py + homeassistant/components/renson/binary_sensor.py + homeassistant/components/renson/button.py + homeassistant/components/renson/const.py + homeassistant/components/renson/coordinator.py + homeassistant/components/renson/entity.py + homeassistant/components/renson/fan.py + homeassistant/components/renson/number.py + homeassistant/components/renson/sensor.py + homeassistant/components/renson/switch.py + homeassistant/components/renson/time.py homeassistant/components/reolink/binary_sensor.py homeassistant/components/reolink/button.py homeassistant/components/reolink/camera.py @@ -1164,11 +1177,11 @@ omit = homeassistant/components/route53/* homeassistant/components/rpi_camera/* homeassistant/components/rtorrent/sensor.py + homeassistant/components/russound_rio/media_player.py + homeassistant/components/russound_rnet/media_player.py homeassistant/components/ruuvi_gateway/__init__.py homeassistant/components/ruuvi_gateway/bluetooth.py homeassistant/components/ruuvi_gateway/coordinator.py - homeassistant/components/russound_rio/media_player.py - homeassistant/components/russound_rnet/media_player.py homeassistant/components/rympro/__init__.py homeassistant/components/rympro/coordinator.py homeassistant/components/rympro/sensor.py @@ -1179,8 +1192,8 @@ omit = homeassistant/components/schluter/* homeassistant/components/screenlogic/binary_sensor.py homeassistant/components/screenlogic/climate.py - homeassistant/components/screenlogic/coordinator.py homeassistant/components/screenlogic/const.py + homeassistant/components/screenlogic/coordinator.py homeassistant/components/screenlogic/entity.py homeassistant/components/screenlogic/light.py homeassistant/components/screenlogic/number.py @@ -1255,8 +1268,8 @@ omit = homeassistant/components/solaredge/coordinator.py homeassistant/components/solaredge_local/sensor.py homeassistant/components/solarlog/__init__.py - homeassistant/components/solarlog/sensor.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 @@ -1293,14 +1306,6 @@ omit = homeassistant/components/squeezebox/__init__.py homeassistant/components/squeezebox/browse_media.py homeassistant/components/squeezebox/media_player.py - homeassistant/components/starlink/__init__.py - homeassistant/components/starlink/binary_sensor.py - homeassistant/components/starlink/button.py - homeassistant/components/starlink/coordinator.py - homeassistant/components/starlink/device_tracker.py - homeassistant/components/starlink/sensor.py - homeassistant/components/starlink/switch.py - homeassistant/components/starlink/time.py homeassistant/components/starline/__init__.py homeassistant/components/starline/account.py homeassistant/components/starline/binary_sensor.py @@ -1311,6 +1316,14 @@ omit = homeassistant/components/starline/sensor.py homeassistant/components/starline/switch.py homeassistant/components/starlingbank/sensor.py + homeassistant/components/starlink/__init__.py + homeassistant/components/starlink/binary_sensor.py + homeassistant/components/starlink/button.py + homeassistant/components/starlink/coordinator.py + homeassistant/components/starlink/device_tracker.py + homeassistant/components/starlink/sensor.py + homeassistant/components/starlink/switch.py + homeassistant/components/starlink/time.py homeassistant/components/steam_online/sensor.py homeassistant/components/stiebel_eltron/* homeassistant/components/stookalert/__init__.py @@ -1354,9 +1367,9 @@ omit = homeassistant/components/switchbot/entity.py homeassistant/components/switchbot/humidifier.py homeassistant/components/switchbot/light.py + homeassistant/components/switchbot/lock.py homeassistant/components/switchbot/sensor.py homeassistant/components/switchbot/switch.py - homeassistant/components/switchbot/lock.py homeassistant/components/switchbot_cloud/climate.py homeassistant/components/switchbot_cloud/coordinator.py homeassistant/components/switchbot_cloud/entity.py @@ -1516,9 +1529,9 @@ omit = homeassistant/components/ue_smart_radio/media_player.py homeassistant/components/ukraine_alarm/__init__.py homeassistant/components/ukraine_alarm/binary_sensor.py - homeassistant/components/unifiled/* homeassistant/components/unifi_direct/__init__.py homeassistant/components/unifi_direct/device_tracker.py + homeassistant/components/unifiled/* homeassistant/components/upb/__init__.py homeassistant/components/upb/light.py homeassistant/components/upc_connect/* @@ -1528,7 +1541,6 @@ omit = homeassistant/components/upnp/__init__.py homeassistant/components/upnp/device.py homeassistant/components/upnp/sensor.py - homeassistant/components/vasttrafik/sensor.py homeassistant/components/v2c/__init__.py homeassistant/components/v2c/binary_sensor.py homeassistant/components/v2c/coordinator.py @@ -1536,6 +1548,7 @@ omit = homeassistant/components/v2c/number.py homeassistant/components/v2c/sensor.py homeassistant/components/v2c/switch.py + homeassistant/components/vasttrafik/sensor.py homeassistant/components/velbus/__init__.py homeassistant/components/velbus/binary_sensor.py homeassistant/components/velbus/button.py @@ -1543,8 +1556,8 @@ omit = homeassistant/components/velbus/cover.py homeassistant/components/velbus/entity.py homeassistant/components/velbus/light.py - homeassistant/components/velbus/sensor.py homeassistant/components/velbus/select.py + homeassistant/components/velbus/sensor.py homeassistant/components/velbus/switch.py homeassistant/components/velux/__init__.py homeassistant/components/velux/cover.py @@ -1710,12 +1723,12 @@ omit = homeassistant/components/zeversolar/coordinator.py homeassistant/components/zeversolar/entity.py homeassistant/components/zeversolar/sensor.py - homeassistant/components/zha/websocket_api.py homeassistant/components/zha/core/cluster_handlers/* homeassistant/components/zha/core/device.py homeassistant/components/zha/core/gateway.py homeassistant/components/zha/core/helpers.py homeassistant/components/zha/light.py + homeassistant/components/zha/websocket_api.py homeassistant/components/zhong_hong/climate.py homeassistant/components/ziggo_mediabox_xl/media_player.py homeassistant/components/zoneminder/* @@ -1732,15 +1745,6 @@ omit = homeassistant/components/zwave_me/sensor.py homeassistant/components/zwave_me/siren.py homeassistant/components/zwave_me/switch.py - homeassistant/components/electrasmart/climate.py - homeassistant/components/electrasmart/__init__.py - homeassistant/components/myuplink/__init__.py - homeassistant/components/myuplink/api.py - homeassistant/components/myuplink/application_credentials.py - homeassistant/components/myuplink/coordinator.py - homeassistant/components/myuplink/entity.py - homeassistant/components/myuplink/helpers.py - homeassistant/components/myuplink/sensor.py [report] diff --git a/script/hassfest/coverage.py b/script/hassfest/coverage.py index 64951fb0288..264960a42e1 100644 --- a/script/hassfest/coverage.py +++ b/script/hassfest/coverage.py @@ -20,6 +20,42 @@ DONT_IGNORE = ( "scene.py", ) +PREFIX = """# Sorted by hassfest. +# +# To sort, run python3 -m script.hassfest -p coverage + +[run] +source = homeassistant +omit = + homeassistant/__main__.py + homeassistant/helpers/signal.py + homeassistant/scripts/__init__.py + homeassistant/scripts/check_config.py + homeassistant/scripts/ensure_config.py + homeassistant/scripts/benchmark/__init__.py + homeassistant/scripts/macos/__init__.py + + # omit pieces of code that rely on external devices being present +""" + +SUFFIX = """[report] +# Regexes for lines to exclude from consideration +exclude_lines = + # Have to re-enable the standard pragma + pragma: no cover + + # Don't complain about missing debug-only code: + def __repr__ + + # Don't complain if tests don't hit defensive assertion code: + raise AssertionError + raise NotImplementedError + + # TYPE_CHECKING and @overload blocks are never executed during pytest run + if TYPE_CHECKING: + @overload +""" + def validate(integrations: dict[str, Integration], config: Config) -> None: """Validate coverage.""" @@ -28,6 +64,7 @@ def validate(integrations: dict[str, Integration], config: Config) -> None: not_found: list[str] = [] checking = False + previous_line = "" with coverage_path.open("rt") as fp: for line in fp: line = line.strip() @@ -55,13 +92,27 @@ def validate(integrations: dict[str, Integration], config: Config) -> None: not_found.append(line) continue - if not line.startswith("homeassistant/components/") or len(path.parts) != 4: + if not line.startswith("homeassistant/components/"): continue integration_path = path.parent + while len(integration_path.parts) > 3: + integration_path = integration_path.parent integration = integrations[integration_path.name] + # Ensure sorted + if line < previous_line: + integration.add_error( + "coverage", + f"{line} is unsorted in .coveragerc file", + ) + previous_line = line + + # Ignore sub-directories for further checks + if len(path.parts) > 4: + continue + if ( path.parts[-1] == "*" and Path(f"tests/components/{integration.domain}/__init__.py").exists() @@ -85,3 +136,28 @@ def validate(integrations: dict[str, Integration], config: Config) -> None: raise RuntimeError( f".coveragerc references files that don't exist: {', '.join(not_found)}." ) + + +def generate(integrations: dict[str, Integration], config: Config) -> None: + """Sort coverage.""" + coverage_path = config.root / ".coveragerc" + lines = [] + start = False + + with coverage_path.open("rt") as fp: + for line in fp: + if ( + not start + and line + == " # omit pieces of code that rely on external devices being present\n" + ): + start = True + elif line == "[report]\n": + break + elif start and line != "\n": + lines.append(line) + + content = f"{PREFIX}{"".join(sorted(lines))}\n\n{SUFFIX}" + + with coverage_path.open("w") as fp: + fp.write(content) From 72447a0717f2b2c29adb11341de7c76421d105ba Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Mon, 1 Apr 2024 14:49:14 +0200 Subject: [PATCH 124/967] Bump velbusaio to 2024.4.0 (#114569) Bump valbusaio to 2024.4.0 --- homeassistant/components/velbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index c5f9ccd3563..1c51c58d238 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -13,7 +13,7 @@ "velbus-packet", "velbus-protocol" ], - "requirements": ["velbus-aio==2023.12.0"], + "requirements": ["velbus-aio==2024.4.0"], "usb": [ { "vid": "10CF", diff --git a/requirements_all.txt b/requirements_all.txt index 32c12ff7c1a..77c2aa07434 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2802,7 +2802,7 @@ vallox-websocket-api==5.1.1 vehicle==2.2.1 # homeassistant.components.velbus -velbus-aio==2023.12.0 +velbus-aio==2024.4.0 # homeassistant.components.venstar venstarcolortouch==0.19 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 15985c3a400..f3ebbc7a972 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2158,7 +2158,7 @@ vallox-websocket-api==5.1.1 vehicle==2.2.1 # homeassistant.components.velbus -velbus-aio==2023.12.0 +velbus-aio==2024.4.0 # homeassistant.components.venstar venstarcolortouch==0.19 From 7f9ad140f9a19f33285d2f2b059f567bc49e9668 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 1 Apr 2024 15:53:14 +0200 Subject: [PATCH 125/967] Fix wrong icons (#114567) * Fix wrong icons * Fix wrong icons --- homeassistant/components/ffmpeg/icons.json | 2 +- homeassistant/components/input_select/icons.json | 2 +- homeassistant/components/media_player/icons.json | 2 +- homeassistant/components/synology_dsm/icons.json | 2 +- homeassistant/components/timer/icons.json | 2 +- homeassistant/components/xiaomi_miio/icons.json | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/ffmpeg/icons.json b/homeassistant/components/ffmpeg/icons.json index 3017b7dc0da..a23f024599c 100644 --- a/homeassistant/components/ffmpeg/icons.json +++ b/homeassistant/components/ffmpeg/icons.json @@ -1,7 +1,7 @@ { "services": { "restart": "mdi:restart", - "start": "mdi:start", + "start": "mdi:play", "stop": "mdi:stop" } } diff --git a/homeassistant/components/input_select/icons.json b/homeassistant/components/input_select/icons.json index 894b6be60dd..03b477ddb36 100644 --- a/homeassistant/components/input_select/icons.json +++ b/homeassistant/components/input_select/icons.json @@ -1,6 +1,6 @@ { "services": { - "select_next": "mdi:skip", + "select_next": "mdi:skip-next", "select_option": "mdi:check", "select_previous": "mdi:skip-previous", "select_first": "mdi:skip-backward", diff --git a/homeassistant/components/media_player/icons.json b/homeassistant/components/media_player/icons.json index e2769085833..847ce5989d6 100644 --- a/homeassistant/components/media_player/icons.json +++ b/homeassistant/components/media_player/icons.json @@ -52,7 +52,7 @@ "unjoin": "mdi:ungroup", "volume_down": "mdi:volume-minus", "volume_mute": "mdi:volume-mute", - "volume_set": "mdi:volume", + "volume_set": "mdi:volume-medium", "volume_up": "mdi:volume-plus" } } diff --git a/homeassistant/components/synology_dsm/icons.json b/homeassistant/components/synology_dsm/icons.json index bbdbc9d2c96..8b4fad457d5 100644 --- a/homeassistant/components/synology_dsm/icons.json +++ b/homeassistant/components/synology_dsm/icons.json @@ -75,7 +75,7 @@ } }, "services": { - "reboot": "mdi:reboot", + "reboot": "mdi:restart", "shutdown": "mdi:power" } } diff --git a/homeassistant/components/timer/icons.json b/homeassistant/components/timer/icons.json index 4cad5c119bd..1e352f7280b 100644 --- a/homeassistant/components/timer/icons.json +++ b/homeassistant/components/timer/icons.json @@ -1,6 +1,6 @@ { "services": { - "start": "mdi:start", + "start": "mdi:play", "pause": "mdi:pause", "cancel": "mdi:cancel", "finish": "mdi:check", diff --git a/homeassistant/components/xiaomi_miio/icons.json b/homeassistant/components/xiaomi_miio/icons.json index a9daaf9a61c..bbd3f6607d7 100644 --- a/homeassistant/components/xiaomi_miio/icons.json +++ b/homeassistant/components/xiaomi_miio/icons.json @@ -17,7 +17,7 @@ "switch_set_wifi_led_off": "mdi:wifi-off", "switch_set_power_price": "mdi:currency-usd", "switch_set_power_mode": "mdi:power", - "vacuum_remote_control_start": "mdi:start", + "vacuum_remote_control_start": "mdi:play", "vacuum_remote_control_stop": "mdi:stop", "vacuum_remote_control_move": "mdi:remote", "vacuum_remote_control_move_step": "mdi:remote", From 2e11a61726a29ee0720238d8b081755aa4d94c4e Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 1 Apr 2024 16:04:18 +0200 Subject: [PATCH 126/967] Automatic cleanup of entity and device registry in Tankerkoenig (#114573) --- .../components/tankerkoenig/__init__.py | 1 - .../components/tankerkoenig/coordinator.py | 34 +++++++-- .../tankerkoenig/test_coordinator.py | 71 ++++++++++++++++++- 3 files changed, 98 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/tankerkoenig/__init__.py b/homeassistant/components/tankerkoenig/__init__.py index 7443fa72b5b..ac009b7a274 100644 --- a/homeassistant/components/tankerkoenig/__init__.py +++ b/homeassistant/components/tankerkoenig/__init__.py @@ -21,7 +21,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = TankerkoenigDataUpdateCoordinator( hass, - entry, name=entry.unique_id or DOMAIN, update_interval=DEFAULT_SCAN_INTERVAL, ) diff --git a/homeassistant/components/tankerkoenig/coordinator.py b/homeassistant/components/tankerkoenig/coordinator.py index 447099d2dca..b7a45b65d0a 100644 --- a/homeassistant/components/tankerkoenig/coordinator.py +++ b/homeassistant/components/tankerkoenig/coordinator.py @@ -17,9 +17,10 @@ from aiotankerkoenig import ( ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_SHOW_ON_MAP +from homeassistant.const import ATTR_ID, CONF_API_KEY, CONF_SHOW_ON_MAP 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.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -31,10 +32,11 @@ _LOGGER = logging.getLogger(__name__) class TankerkoenigDataUpdateCoordinator(DataUpdateCoordinator): """Get the latest data from the API.""" + config_entry: ConfigEntry + def __init__( self, hass: HomeAssistant, - entry: ConfigEntry, name: str, update_interval: int, ) -> None: @@ -47,13 +49,14 @@ class TankerkoenigDataUpdateCoordinator(DataUpdateCoordinator): update_interval=timedelta(minutes=update_interval), ) - self._selected_stations: list[str] = entry.data[CONF_STATIONS] + self._selected_stations: list[str] = self.config_entry.data[CONF_STATIONS] self.stations: dict[str, Station] = {} - self.fuel_types: list[str] = entry.data[CONF_FUEL_TYPES] - self.show_on_map: bool = entry.options[CONF_SHOW_ON_MAP] + self.fuel_types: list[str] = self.config_entry.data[CONF_FUEL_TYPES] + self.show_on_map: bool = self.config_entry.options[CONF_SHOW_ON_MAP] self._tankerkoenig = Tankerkoenig( - api_key=entry.data[CONF_API_KEY], session=async_get_clientsession(hass) + api_key=self.config_entry.data[CONF_API_KEY], + session=async_get_clientsession(hass), ) async def async_setup(self) -> None: @@ -81,6 +84,25 @@ class TankerkoenigDataUpdateCoordinator(DataUpdateCoordinator): self.stations[station_id] = station + entity_reg = er.async_get(self.hass) + for entity in er.async_entries_for_config_entry( + entity_reg, self.config_entry.entry_id + ): + if entity.unique_id.split("_")[0] not in self._selected_stations: + _LOGGER.debug("Removing obsolete entity entry %s", entity.entity_id) + entity_reg.async_remove(entity.entity_id) + + device_reg = dr.async_get(self.hass) + for device in dr.async_entries_for_config_entry( + device_reg, self.config_entry.entry_id + ): + if not any( + (ATTR_ID, station_id) in device.identifiers + for station_id in self._selected_stations + ): + _LOGGER.debug("Removing obsolete device entry %s", device.name) + device_reg.async_remove_device(device.id) + if len(self.stations) > 10: _LOGGER.warning( "Found more than 10 stations to check. " diff --git a/tests/components/tankerkoenig/test_coordinator.py b/tests/components/tankerkoenig/test_coordinator.py index 5a33cb95dd9..de65cd921be 100644 --- a/tests/components/tankerkoenig/test_coordinator.py +++ b/tests/components/tankerkoenig/test_coordinator.py @@ -13,10 +13,13 @@ from aiotankerkoenig.exceptions import ( ) import pytest +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.tankerkoenig.const import DEFAULT_SCAN_INTERVAL, DOMAIN from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.const import ATTR_ID, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant +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 @@ -121,3 +124,69 @@ async def test_setup_exception_logging( await hass.async_block_till_done() assert expected_log in caplog.text + + +async def test_automatic_registry_cleanup( + hass: HomeAssistant, + config_entry: MockConfigEntry, + tankerkoenig: AsyncMock, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test automatic registry cleanup for obsolete entity and devices entries.""" + # setup normal + config_entry.add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + assert ( + len(er.async_entries_for_config_entry(entity_registry, config_entry.entry_id)) + == 4 + ) + assert ( + len(dr.async_entries_for_config_entry(device_registry, config_entry.entry_id)) + == 1 + ) + + # add obsolete entity and device entries + obsolete_station_id = "aabbccddee-xxxx-xxxx-xxxx-ff11223344" + + entity_registry.async_get_or_create( + DOMAIN, + BINARY_SENSOR_DOMAIN, + f"{obsolete_station_id}_status", + config_entry=config_entry, + ) + entity_registry.async_get_or_create( + DOMAIN, + SENSOR_DOMAIN, + f"{obsolete_station_id}_e10", + config_entry=config_entry, + ) + device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={(ATTR_ID, obsolete_station_id)}, + name="Obsolete Station", + ) + + assert ( + len(er.async_entries_for_config_entry(entity_registry, config_entry.entry_id)) + == 6 + ) + assert ( + len(dr.async_entries_for_config_entry(device_registry, config_entry.entry_id)) + == 2 + ) + + # reload config entry to trigger automatic cleanup + await hass.config_entries.async_reload(config_entry.entry_id) + await hass.async_block_till_done() + + assert ( + len(er.async_entries_for_config_entry(entity_registry, config_entry.entry_id)) + == 4 + ) + assert ( + len(dr.async_entries_for_config_entry(device_registry, config_entry.entry_id)) + == 1 + ) From 429b5d22cf92a3f1c615ac6593ed2a309b086477 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Mon, 1 Apr 2024 15:51:27 +0100 Subject: [PATCH 127/967] Upgrade aioazuredevops to 2.0.0 (#114537) * Upgrade aioazuredevops to 2.0.0 * Refactor Azure DevOps config flow to use async_get_clientsession * Wrap conditional in parentesis --- homeassistant/components/azure_devops/__init__.py | 11 +++++++++-- .../components/azure_devops/config_flow.py | 4 +++- .../components/azure_devops/manifest.json | 2 +- homeassistant/components/azure_devops/sensor.py | 14 +++++++++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 24 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/azure_devops/__init__.py b/homeassistant/components/azure_devops/__init__.py index e2b761708a5..deda8f466a6 100644 --- a/homeassistant/components/azure_devops/__init__.py +++ b/homeassistant/components/azure_devops/__init__.py @@ -16,6 +16,7 @@ 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.entity import EntityDescription from homeassistant.helpers.update_coordinator import ( @@ -43,7 +44,8 @@ class AzureDevOpsEntityDescription(EntityDescription): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Azure DevOps from a config entry.""" - client = DevOpsClient() + 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]) @@ -62,7 +64,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Fetch data from Azure DevOps.""" try: - return await client.get_builds( + builds = await client.get_builds( entry.data[CONF_ORG], entry.data[CONF_PROJECT], BUILDS_QUERY, @@ -70,6 +72,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except aiohttp.ClientError as exception: raise UpdateFailed from exception + if builds is None: + raise UpdateFailed("No builds found") + + return builds + coordinator = DataUpdateCoordinator( hass, _LOGGER, diff --git a/homeassistant/components/azure_devops/config_flow.py b/homeassistant/components/azure_devops/config_flow.py index 336fd2ca8df..ffb0abf609a 100644 --- a/homeassistant/components/azure_devops/config_flow.py +++ b/homeassistant/components/azure_devops/config_flow.py @@ -10,6 +10,7 @@ import aiohttp import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_ORG, CONF_PAT, CONF_PROJECT, DOMAIN @@ -56,7 +57,8 @@ class AzureDevOpsFlowHandler(ConfigFlow, domain=DOMAIN): """Check the setup of the flow.""" errors: dict[str, str] = {} - client = DevOpsClient() + aiohttp_session = async_get_clientsession(self.hass) + client = DevOpsClient(session=aiohttp_session) try: if self._pat is not None: diff --git a/homeassistant/components/azure_devops/manifest.json b/homeassistant/components/azure_devops/manifest.json index 391cad570f2..0d5e5a1c685 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==1.4.3"] + "requirements": ["aioazuredevops==2.0.0"] } diff --git a/homeassistant/components/azure_devops/sensor.py b/homeassistant/components/azure_devops/sensor.py index a6e4ee95cad..514db5462e9 100644 --- a/homeassistant/components/azure_devops/sensor.py +++ b/homeassistant/components/azure_devops/sensor.py @@ -39,19 +39,23 @@ async def async_setup_entry( AzureDevOpsSensor( coordinator, AzureDevOpsSensorEntityDescription( - key=f"{build.project.id}_{build.definition.id}_latest_build", + 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.id, - "definition_name": build.definition.name, - "id": build.id, + "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, + "url": build.links.web if build.links else None, "queue_time": build.queue_time, "start_time": build.start_time, "finish_time": build.finish_time, diff --git a/requirements_all.txt b/requirements_all.txt index 77c2aa07434..e2181cec025 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -206,7 +206,7 @@ aioasuswrt==1.4.0 aioautomower==2024.3.4 # homeassistant.components.azure_devops -aioazuredevops==1.4.3 +aioazuredevops==2.0.0 # homeassistant.components.baf aiobafi6==0.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f3ebbc7a972..703315b5b3f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -185,7 +185,7 @@ aioasuswrt==1.4.0 aioautomower==2024.3.4 # homeassistant.components.azure_devops -aioazuredevops==1.4.3 +aioazuredevops==2.0.0 # homeassistant.components.baf aiobafi6==0.9.0 From 96120b64e0e7cb208ceaa16f0329f3c1be4f5c73 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 1 Apr 2024 06:53:49 -1000 Subject: [PATCH 128/967] Fix missing mocking in nextdns tests (#114541) --- tests/components/nextdns/__init__.py | 31 ++++++++++++------- .../components/nextdns/test_binary_sensor.py | 11 +++---- tests/components/nextdns/test_sensor.py | 29 +++-------------- tests/components/nextdns/test_switch.py | 11 +++---- 4 files changed, 32 insertions(+), 50 deletions(-) diff --git a/tests/components/nextdns/__init__.py b/tests/components/nextdns/__init__.py index e4948a9358f..4cf74d72e63 100644 --- a/tests/components/nextdns/__init__.py +++ b/tests/components/nextdns/__init__.py @@ -1,5 +1,6 @@ """Tests for the NextDNS integration.""" +from contextlib import contextmanager from unittest.mock import patch from nextdns import ( @@ -113,16 +114,9 @@ SETTINGS = Settings( ) -async def init_integration(hass: HomeAssistant) -> MockConfigEntry: - """Set up the NextDNS integration in Home Assistant.""" - entry = MockConfigEntry( - domain=DOMAIN, - title="Fake Profile", - unique_id="xyz12", - data={CONF_API_KEY: "fake_api_key", CONF_PROFILE_ID: "xyz12"}, - entry_id="d9aa37407ddac7b964a99e86312288d6", - ) - +@contextmanager +def mock_nextdns(): + """Mock the NextDNS class.""" with ( patch( "homeassistant.components.nextdns.NextDns.get_profiles", @@ -157,7 +151,22 @@ async def init_integration(hass: HomeAssistant) -> MockConfigEntry: return_value=CONNECTION_STATUS, ), ): - entry.add_to_hass(hass) + yield + + +async def init_integration(hass: HomeAssistant) -> MockConfigEntry: + """Set up the NextDNS integration in Home Assistant.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="Fake Profile", + unique_id="xyz12", + data={CONF_API_KEY: "fake_api_key", CONF_PROFILE_ID: "xyz12"}, + entry_id="d9aa37407ddac7b964a99e86312288d6", + ) + + entry.add_to_hass(hass) + + with mock_nextdns(): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/nextdns/test_binary_sensor.py b/tests/components/nextdns/test_binary_sensor.py index 484b4e99aad..b69db4798d3 100644 --- a/tests/components/nextdns/test_binary_sensor.py +++ b/tests/components/nextdns/test_binary_sensor.py @@ -10,7 +10,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.util.dt import utcnow -from . import CONNECTION_STATUS, init_integration +from . import init_integration, mock_nextdns from tests.common import async_fire_time_changed @@ -57,19 +57,16 @@ async def test_availability(hass: HomeAssistant) -> None: side_effect=ApiError("API Error"), ): async_fire_time_changed(hass, future) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("binary_sensor.fake_profile_device_connection_status") assert state assert state.state == STATE_UNAVAILABLE future = utcnow() + timedelta(minutes=20) - with patch( - "homeassistant.components.nextdns.NextDns.connection_status", - return_value=CONNECTION_STATUS, - ): + with mock_nextdns(): async_fire_time_changed(hass, future) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("binary_sensor.fake_profile_device_connection_status") assert state diff --git a/tests/components/nextdns/test_sensor.py b/tests/components/nextdns/test_sensor.py index a6d9b4c545f..951d220eccb 100644 --- a/tests/components/nextdns/test_sensor.py +++ b/tests/components/nextdns/test_sensor.py @@ -11,7 +11,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.util.dt import utcnow -from . import DNSSEC, ENCRYPTION, IP_VERSIONS, PROTOCOLS, STATUS, init_integration +from . import init_integration, mock_nextdns from tests.common import async_fire_time_changed @@ -332,7 +332,7 @@ async def test_availability( ), ): async_fire_time_changed(hass, future) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.fake_profile_dns_queries") assert state @@ -355,30 +355,9 @@ async def test_availability( assert state.state == STATE_UNAVAILABLE future = utcnow() + timedelta(minutes=20) - with ( - patch( - "homeassistant.components.nextdns.NextDns.get_analytics_status", - return_value=STATUS, - ), - patch( - "homeassistant.components.nextdns.NextDns.get_analytics_encryption", - return_value=ENCRYPTION, - ), - patch( - "homeassistant.components.nextdns.NextDns.get_analytics_dnssec", - return_value=DNSSEC, - ), - patch( - "homeassistant.components.nextdns.NextDns.get_analytics_ip_versions", - return_value=IP_VERSIONS, - ), - patch( - "homeassistant.components.nextdns.NextDns.get_analytics_protocols", - return_value=PROTOCOLS, - ), - ): + with mock_nextdns(): async_fire_time_changed(hass, future) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.fake_profile_dns_queries") assert state diff --git a/tests/components/nextdns/test_switch.py b/tests/components/nextdns/test_switch.py index f51ee32fd10..a9dd0ba5cbd 100644 --- a/tests/components/nextdns/test_switch.py +++ b/tests/components/nextdns/test_switch.py @@ -22,7 +22,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from homeassistant.util.dt import utcnow -from . import SETTINGS, init_integration +from . import init_integration, mock_nextdns from tests.common import async_fire_time_changed @@ -693,19 +693,16 @@ async def test_availability(hass: HomeAssistant) -> None: side_effect=ApiError("API Error"), ): async_fire_time_changed(hass, future) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("switch.fake_profile_web3") assert state assert state.state == STATE_UNAVAILABLE future = utcnow() + timedelta(minutes=20) - with patch( - "homeassistant.components.nextdns.NextDns.get_settings", - return_value=SETTINGS, - ): + with mock_nextdns(): async_fire_time_changed(hass, future) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("switch.fake_profile_web3") assert state From 94060b156652ad14c7ed5d60d4d9191335b088b6 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 1 Apr 2024 13:28:39 -0400 Subject: [PATCH 129/967] Bump plexapi to 4.15.11 (#114581) --- homeassistant/components/plex/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index e33cbc2e0c1..85362371715 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_push", "loggers": ["plexapi", "plexwebsocket"], "requirements": [ - "PlexAPI==4.15.10", + "PlexAPI==4.15.11", "plexauth==0.0.6", "plexwebsocket==0.0.14" ], diff --git a/requirements_all.txt b/requirements_all.txt index e2181cec025..7be67deda31 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -45,7 +45,7 @@ Mastodon.py==1.8.1 Pillow==10.2.0 # homeassistant.components.plex -PlexAPI==4.15.10 +PlexAPI==4.15.11 # homeassistant.components.progettihwsw ProgettiHWSW==0.1.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 703315b5b3f..a1f23914038 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -39,7 +39,7 @@ HATasmota==0.8.0 Pillow==10.2.0 # homeassistant.components.plex -PlexAPI==4.15.10 +PlexAPI==4.15.11 # homeassistant.components.progettihwsw ProgettiHWSW==0.1.3 From 886f03dd71ea0ec2f5169848b47a1d2ff1b6b716 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 1 Apr 2024 19:36:57 +0200 Subject: [PATCH 130/967] Apply late review of tankerkoenig (#114582) remove config entry from device, not device itself --- homeassistant/components/tankerkoenig/coordinator.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tankerkoenig/coordinator.py b/homeassistant/components/tankerkoenig/coordinator.py index b7a45b65d0a..458c629f422 100644 --- a/homeassistant/components/tankerkoenig/coordinator.py +++ b/homeassistant/components/tankerkoenig/coordinator.py @@ -101,7 +101,9 @@ class TankerkoenigDataUpdateCoordinator(DataUpdateCoordinator): for station_id in self._selected_stations ): _LOGGER.debug("Removing obsolete device entry %s", device.name) - device_reg.async_remove_device(device.id) + device_reg.async_update_device( + device.id, remove_config_entry_id=self.config_entry.entry_id + ) if len(self.stations) > 10: _LOGGER.warning( From ae640b6e1ac00be366013df5759c256b54ab20fe Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 1 Apr 2024 09:03:59 -1000 Subject: [PATCH 131/967] Small cleanups to zone to reduce startup time (#114587) --- homeassistant/components/zone/__init__.py | 58 +++++++++++------------ 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index 2fda501c447..ee85bda5a6d 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -315,7 +315,9 @@ async def async_setup_entry( await storage_collection.async_create_item(data) - hass.async_create_task(hass.config_entries.async_remove(config_entry.entry_id)) + hass.async_create_task( + hass.config_entries.async_remove(config_entry.entry_id), eager_start=True + ) return True @@ -331,6 +333,7 @@ class Zone(collection.CollectionEntity): """Representation of a Zone.""" editable: bool + _attr_should_poll = False def __init__(self, config: ConfigType) -> None: """Initialize the zone.""" @@ -339,6 +342,16 @@ class Zone(collection.CollectionEntity): self._attrs: dict | None = None self._remove_listener: Callable[[], None] | None = None self._persons_in_zone: set[str] = set() + self._set_attrs_from_config() + + def _set_attrs_from_config(self) -> None: + """Set the attributes from the config.""" + config = self._config + name: str = config[CONF_NAME] + self._attr_name = name + self._case_folded_name = name.casefold() + self._attr_unique_id = config.get(CONF_ID) + self._attr_icon = config.get(CONF_ICON) @classmethod def from_storage(cls, config: ConfigType) -> Self: @@ -361,31 +374,12 @@ class Zone(collection.CollectionEntity): """Return the state property really does nothing for a zone.""" return len(self._persons_in_zone) - @property - def name(self) -> str: - """Return name.""" - return cast(str, self._config[CONF_NAME]) - - @property - def unique_id(self) -> str | None: - """Return unique ID.""" - return self._config.get(CONF_ID) - - @property - def icon(self) -> str | None: - """Return the icon if any.""" - return self._config.get(CONF_ICON) - - @property - def should_poll(self) -> bool: - """Zone does not poll.""" - return False - async def async_update_config(self, config: ConfigType) -> None: """Handle when the config is updated.""" if self._config == config: return self._config = config + self._set_attrs_from_config() self._generate_attrs() self.async_write_ha_state() @@ -394,13 +388,14 @@ class Zone(collection.CollectionEntity): self, evt: Event[event.EventStateChangedData] ) -> None: person_entity_id = evt.data["entity_id"] - cur_count = len(self._persons_in_zone) + persons_in_zone = self._persons_in_zone + cur_count = len(persons_in_zone) if self._state_is_in_zone(evt.data["new_state"]): - self._persons_in_zone.add(person_entity_id) - elif person_entity_id in self._persons_in_zone: - self._persons_in_zone.remove(person_entity_id) + persons_in_zone.add(person_entity_id) + elif person_entity_id in persons_in_zone: + persons_in_zone.remove(person_entity_id) - if len(self._persons_in_zone) != cur_count: + if len(persons_in_zone) != cur_count: self._generate_attrs() self.async_write_ha_state() @@ -408,10 +403,11 @@ class Zone(collection.CollectionEntity): """Run when entity about to be added to hass.""" await super().async_added_to_hass() person_domain = "person" # avoid circular import - persons = self.hass.states.async_entity_ids(person_domain) - for person in persons: - if self._state_is_in_zone(self.hass.states.get(person)): - self._persons_in_zone.add(person) + self._persons_in_zone = { + state.entity_id + for state in self.hass.states.async_all(person_domain) + if self._state_is_in_zone(state) + } self._generate_attrs() self.async_on_remove( @@ -446,7 +442,7 @@ class Zone(collection.CollectionEntity): STATE_UNAVAILABLE, ) and ( - state.state.casefold() == self.name.casefold() + state.state.casefold() == self._case_folded_name or (state.state == STATE_HOME and self.entity_id == ENTITY_ID_HOME) ) ) From 304ed8bf3d70815d97d4432badbf83f126d320f1 Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Mon, 1 Apr 2024 21:28:54 +0200 Subject: [PATCH 132/967] Unignore Ruff PLC in tests (#114572) --- tests/components/deconz/test_alarm_control_panel.py | 8 ++++---- tests/components/rainbird/test_calendar.py | 2 +- tests/ruff.toml | 1 - tests/test_config.py | 4 ++-- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/tests/components/deconz/test_alarm_control_panel.py b/tests/components/deconz/test_alarm_control_panel.py index ec926491724..c855076de2f 100644 --- a/tests/components/deconz/test_alarm_control_panel.py +++ b/tests/components/deconz/test_alarm_control_panel.py @@ -171,11 +171,11 @@ async def test_alarm_control_panel( # Event signals alarm control panel arming - for arming_event in { + for arming_event in ( AncillaryControlPanel.ARMING_AWAY, AncillaryControlPanel.ARMING_NIGHT, AncillaryControlPanel.ARMING_STAY, - }: + ): event_changed_sensor = { "t": "event", "e": "changed", @@ -190,10 +190,10 @@ async def test_alarm_control_panel( # Event signals alarm control panel pending - for pending_event in { + for pending_event in ( AncillaryControlPanel.ENTRY_DELAY, AncillaryControlPanel.EXIT_DELAY, - }: + ): event_changed_sensor = { "t": "event", "e": "changed", diff --git a/tests/components/rainbird/test_calendar.py b/tests/components/rainbird/test_calendar.py index 6258ac56249..9f6dfd9213d 100644 --- a/tests/components/rainbird/test_calendar.py +++ b/tests/components/rainbird/test_calendar.py @@ -125,7 +125,7 @@ def get_events_fixture( ) assert response.status == HTTPStatus.OK results = await response.json() - return [{k: event[k] for k in {"summary", "start", "end"}} for event in results] + return [{k: event[k] for k in ("summary", "start", "end")} for event in results] return _fetch diff --git a/tests/ruff.toml b/tests/ruff.toml index b88a5f1689e..5455e211762 100644 --- a/tests/ruff.toml +++ b/tests/ruff.toml @@ -4,7 +4,6 @@ extend = "../pyproject.toml" [lint] extend-ignore = [ - "PLC", # pylint "B904", # Use raise from to specify exception cause "N815", # Variable {name} in class scope should not be mixedCase ] diff --git a/tests/test_config.py b/tests/test_config.py index 49cc2bb573f..89a9b7b7082 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -2042,12 +2042,12 @@ async def test_core_config_schema_legacy_template( await config_util.async_process_ha_core_config(hass, config) issue_registry = ir.async_get(hass) - for issue_id in {"legacy_templates_true", "legacy_templates_false"}: + 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"}: + for issue_id in ("legacy_templates_true", "legacy_templates_false"): assert not issue_registry.async_get_issue("homeassistant", issue_id) From 0732952b325d3b4b7b072c076289f5a6c940777a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 1 Apr 2024 09:58:06 -1000 Subject: [PATCH 133/967] Reduce hassio startup time (#114588) --- homeassistant/components/hassio/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 90b155aff15..8f648bd006b 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -368,7 +368,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: require_admin=True, ) - await hassio.update_hass_api(config.get("http", {}), refresh_token) + update_hass_api_task = hass.async_create_task( + hassio.update_hass_api(config.get("http", {}), refresh_token), eager_start=True + ) last_timezone = None @@ -481,6 +483,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: # _async_setup_hardware_integration is called # so the hardware integration can be set up # and does not fallback to calling later + await update_hass_api_task await panels_task await update_info_task await push_config_task From 98a160860471bed46002a02f9bff45d74d794045 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 1 Apr 2024 21:59:06 +0200 Subject: [PATCH 134/967] Reduce usage of executer threads in AVM Fritz!Tools (#114570) * call entity state update calls in one executer task * remove not needed wrapping * mark as "non-public" method * add guard against changes on _entity_update_functions --- homeassistant/components/fritz/common.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index b9d77220d7c..9d8bcd1ab3e 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -311,6 +311,17 @@ class FritzBoxTools( ) return unregister_entity_updates + def _entity_states_update(self) -> dict: + """Run registered entity update calls.""" + entity_states = {} + for key in list(self._entity_update_functions): + if (update_fn := self._entity_update_functions.get(key)) is not None: + _LOGGER.debug("update entity %s", key) + entity_states[key] = update_fn( + self.fritz_status, self.data["entity_states"].get(key) + ) + return entity_states + async def _async_update_data(self) -> UpdateCoordinatorDataType: """Update FritzboxTools data.""" entity_data: UpdateCoordinatorDataType = { @@ -319,15 +330,9 @@ class FritzBoxTools( } try: await self.async_scan_devices() - for key in list(self._entity_update_functions): - _LOGGER.debug("update entity %s", key) - entity_data["entity_states"][ - key - ] = await self.hass.async_add_executor_job( - self._entity_update_functions[key], - self.fritz_status, - self.data["entity_states"].get(key), - ) + entity_data["entity_states"] = await self.hass.async_add_executor_job( + self._entity_states_update + ) if self.has_call_deflections: entity_data[ "call_deflections" From f2b9e6b3897ba501b58cd8575475a4db75ce9d07 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 1 Apr 2024 11:18:26 -1000 Subject: [PATCH 135/967] Bump zeroconf to 0.132.0 (#114596) changelog: https://github.com/python-zeroconf/python-zeroconf/compare/0.131.0...0.132.0 --- homeassistant/components/zeroconf/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/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index aecc88968f3..7c489517dd7 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["zeroconf"], "quality_scale": "internal", - "requirements": ["zeroconf==0.131.0"] + "requirements": ["zeroconf==0.132.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 72cd71f889f..0b961013b93 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -60,7 +60,7 @@ voluptuous-serialize==2.6.0 voluptuous==0.13.1 webrtc-noise-gain==1.2.3 yarl==1.9.4 -zeroconf==0.131.0 +zeroconf==0.132.0 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index 7be67deda31..cacbe912fd8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2932,7 +2932,7 @@ zamg==0.3.6 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.131.0 +zeroconf==0.132.0 # homeassistant.components.zeversolar zeversolar==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a1f23914038..a093b9f0160 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2267,7 +2267,7 @@ yt-dlp==2024.03.10 zamg==0.3.6 # homeassistant.components.zeroconf -zeroconf==0.131.0 +zeroconf==0.132.0 # homeassistant.components.zeversolar zeversolar==0.3.1 From e14573a465429147f5bcd3a56416de5c07cb5110 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Mon, 1 Apr 2024 23:33:29 +0200 Subject: [PATCH 136/967] Migrate uptime to use single_config_entry (#114586) --- homeassistant/components/uptime/config_flow.py | 3 --- homeassistant/components/uptime/manifest.json | 3 ++- homeassistant/components/uptime/strings.json | 3 --- homeassistant/generated/integrations.json | 3 ++- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/uptime/config_flow.py b/homeassistant/components/uptime/config_flow.py index 2cfec38d200..6dd68bae148 100644 --- a/homeassistant/components/uptime/config_flow.py +++ b/homeassistant/components/uptime/config_flow.py @@ -20,9 +20,6 @@ class UptimeConfigFlow(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - if user_input is not None: return self.async_create_entry( title="Uptime", diff --git a/homeassistant/components/uptime/manifest.json b/homeassistant/components/uptime/manifest.json index 2b4dbcd0fec..37e61976b9c 100644 --- a/homeassistant/components/uptime/manifest.json +++ b/homeassistant/components/uptime/manifest.json @@ -6,5 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/uptime", "integration_type": "service", "iot_class": "local_push", - "quality_scale": "internal" + "quality_scale": "internal", + "single_config_entry": true } diff --git a/homeassistant/components/uptime/strings.json b/homeassistant/components/uptime/strings.json index 9ceb91de9ba..868c2a51588 100644 --- a/homeassistant/components/uptime/strings.json +++ b/homeassistant/components/uptime/strings.json @@ -5,9 +5,6 @@ "user": { "description": "[%key:common::config_flow::description::confirm_setup%]" } - }, - "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" } } } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index b4eff321e6e..feb5a373505 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -6472,7 +6472,8 @@ "uptime": { "integration_type": "service", "config_flow": true, - "iot_class": "local_push" + "iot_class": "local_push", + "single_config_entry": true }, "uptimerobot": { "name": "UptimeRobot", From 8e384ab5984c4d2b95f1b5b407aad684763a0553 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Mon, 1 Apr 2024 23:55:14 +0200 Subject: [PATCH 137/967] Use dict comprehension in honeywell diagnostics (#114598) --- .../components/honeywell/diagnostics.py | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/honeywell/diagnostics.py b/homeassistant/components/honeywell/diagnostics.py index b489eb4a596..35624c8fc39 100644 --- a/homeassistant/components/honeywell/diagnostics.py +++ b/homeassistant/components/honeywell/diagnostics.py @@ -16,19 +16,13 @@ async def async_get_config_entry_diagnostics( config_entry: ConfigEntry, ) -> dict[str, Any]: """Return diagnostics for a config entry.""" + honeywell: HoneywellData = hass.data[DOMAIN][config_entry.entry_id] - Honeywell: HoneywellData = hass.data[DOMAIN][config_entry.entry_id] - diagnostics_data = {} - - for device, module in Honeywell.devices.items(): - diagnostics_data.update( - { - f"Device {device}": { - "UI Data": module.raw_ui_data, - "Fan Data": module.raw_fan_data, - "DR Data": module.raw_dr_data, - } - } - ) - - return diagnostics_data + return { + f"Device {device}": { + "UI Data": module.raw_ui_data, + "Fan Data": module.raw_fan_data, + "DR Data": module.raw_dr_data, + } + for device, module in honeywell.devices.items() + } From acdb3cc7a23601ad3e063337ec17443c8d754d7d Mon Sep 17 00:00:00 2001 From: IngoK1 <45150614+IngoK1@users.noreply.github.com> Date: Tue, 2 Apr 2024 00:07:02 +0200 Subject: [PATCH 138/967] Fix for Sonos URL encoding problem #102557 (#109518) * Fix for URL encoding problem #102557 Fixes the problem "Cannot play media with spaces in folder names to Sonos #102557" removing the encoding of the strings in the music library. * Fix type casting problem * Update media_browser.py to fix pr check findings Added required casting for all unquote statements to avoid further casting findings in the pr checks * Update media_browser.py Checked on linting, lets give it another try * Update media_browser.py Updated ruff run * Update media_browser.py - added version run through ruff * Update media_browser.py - ruff changes * Apply ruff formatting * Update homeassistant/components/sonos/media_browser.py Co-authored-by: jjlawren * Update homeassistant/components/sonos/media_browser.py Co-authored-by: jjlawren * Update homeassistant/components/sonos/media_browser.py Co-authored-by: jjlawren * Update homeassistant/components/sonos/media_browser.py Co-authored-by: jjlawren --------- Co-authored-by: computeq-admin <51021172+computeq-admin@users.noreply.github.com> Co-authored-by: Jason Lawrence --- homeassistant/components/sonos/media_browser.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/sonos/media_browser.py b/homeassistant/components/sonos/media_browser.py index 17327bf4be1..bd57e57e468 100644 --- a/homeassistant/components/sonos/media_browser.py +++ b/homeassistant/components/sonos/media_browser.py @@ -7,6 +7,7 @@ from contextlib import suppress from functools import partial import logging from typing import cast +import urllib.parse from soco.data_structures import DidlObject from soco.ms_data_structures import MusicServiceItem @@ -60,12 +61,14 @@ def get_thumbnail_url_full( media_content_id, media_content_type, ) - return getattr(item, "album_art_uri", None) + return urllib.parse.unquote(getattr(item, "album_art_uri", "")) - return get_browse_image_url( - media_content_type, - media_content_id, - media_image_id, + return urllib.parse.unquote( + get_browse_image_url( + media_content_type, + media_content_id, + media_image_id, + ) ) @@ -166,6 +169,7 @@ def build_item_response( payload["idstring"] = "A:ALBUMARTIST/" + "/".join( payload["idstring"].split("/")[2:] ) + payload["idstring"] = urllib.parse.unquote(payload["idstring"]) try: search_type = MEDIA_TYPES_TO_SONOS[payload["search_type"]] @@ -496,7 +500,7 @@ def get_media( if not item_id.startswith("A:ALBUM") and search_type == SONOS_ALBUM: item_id = "A:ALBUMARTIST/" + "/".join(item_id.split("/")[2:]) - search_term = item_id.split("/")[-1] + search_term = urllib.parse.unquote(item_id.split("/")[-1]) matches = media_library.get_music_library_information( search_type, search_term=search_term, full_album_art_uri=True ) From b1af590eed7506730464cdf40c7ead29946ebc96 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 2 Apr 2024 00:14:13 +0200 Subject: [PATCH 139/967] Fix reolink media source data access (#114593) * Add test * Fix reolink media source data access --- homeassistant/components/reolink/media_source.py | 16 ++++++++++------ tests/components/reolink/test_media_source.py | 11 +++++++++++ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/reolink/media_source.py b/homeassistant/components/reolink/media_source.py index 84c844a0f92..c22a0fc28e7 100644 --- a/homeassistant/components/reolink/media_source.py +++ b/homeassistant/components/reolink/media_source.py @@ -46,7 +46,6 @@ class ReolinkVODMediaSource(MediaSource): """Initialize ReolinkVODMediaSource.""" super().__init__(DOMAIN) self.hass = hass - self.data: dict[str, ReolinkData] = hass.data[DOMAIN] async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: """Resolve media to a url.""" @@ -57,7 +56,8 @@ class ReolinkVODMediaSource(MediaSource): _, config_entry_id, channel_str, stream_res, filename = identifier channel = int(channel_str) - host = self.data[config_entry_id].host + data: dict[str, ReolinkData] = self.hass.data[DOMAIN] + host = data[config_entry_id].host vod_type = VodRequestType.RTMP if host.api.is_nvr: @@ -130,7 +130,8 @@ class ReolinkVODMediaSource(MediaSource): if config_entry.state != ConfigEntryState.LOADED: continue channels: list[str] = [] - host = self.data[config_entry.entry_id].host + data: dict[str, ReolinkData] = self.hass.data[DOMAIN] + host = data[config_entry.entry_id].host entities = er.async_entries_for_config_entry( entity_reg, config_entry.entry_id ) @@ -187,7 +188,8 @@ class ReolinkVODMediaSource(MediaSource): self, config_entry_id: str, channel: int ) -> BrowseMediaSource: """Allow the user to select the high or low playback resolution, (low loads faster).""" - host = self.data[config_entry_id].host + data: dict[str, ReolinkData] = self.hass.data[DOMAIN] + host = data[config_entry_id].host main_enc = await host.api.get_encoding(channel, "main") if main_enc == "h265": @@ -236,7 +238,8 @@ class ReolinkVODMediaSource(MediaSource): self, config_entry_id: str, channel: int, stream: str ) -> BrowseMediaSource: """Return all days on which recordings are available for a reolink camera.""" - host = self.data[config_entry_id].host + data: dict[str, ReolinkData] = self.hass.data[DOMAIN] + host = data[config_entry_id].host # We want today of the camera, not necessarily today of the server now = host.api.time() or await host.api.async_get_time() @@ -288,7 +291,8 @@ class ReolinkVODMediaSource(MediaSource): day: int, ) -> BrowseMediaSource: """Return all recording files on a specific day of a Reolink camera.""" - host = self.data[config_entry_id].host + data: dict[str, ReolinkData] = self.hass.data[DOMAIN] + host = data[config_entry_id].host start = dt.datetime(year, month, day, hour=0, minute=0, second=0) end = dt.datetime(year, month, day, hour=23, minute=59, second=59) diff --git a/tests/components/reolink/test_media_source.py b/tests/components/reolink/test_media_source.py index 9c5aebed222..1eb45945eee 100644 --- a/tests/components/reolink/test_media_source.py +++ b/tests/components/reolink/test_media_source.py @@ -65,6 +65,17 @@ async def setup_component(hass: HomeAssistant) -> None: assert await async_setup_component(hass, MEDIA_STREAM_DOMAIN, {}) +async def test_platform_loads_before_config_entry( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, +) -> None: + """Test that the platform can be loaded before the config entry.""" + # Fake that the config entry is not loaded before the media_source platform + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + assert mock_setup_entry.call_count == 0 + + async def test_resolve( hass: HomeAssistant, reolink_connect: MagicMock, From d2e4f5f36e8e46cd0a8516ef92103de7ee2c2a59 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 1 Apr 2024 21:34:25 -0400 Subject: [PATCH 140/967] Add conversation entity (#114518) * Default agent as entity * Migrate constant to point at new value * Fix tests * Fix more tests * Move assist pipeline back to cloud after dependenceis --- .../components/assist_pipeline/pipeline.py | 4 + homeassistant/components/cloud/http_api.py | 5 +- homeassistant/components/cloud/stt.py | 16 +- homeassistant/components/cloud/tts.py | 17 +- .../components/conversation/__init__.py | 53 ++++-- .../components/conversation/agent_manager.py | 103 +++++------ .../components/conversation/const.py | 4 +- .../components/conversation/default_agent.py | 44 +++-- .../components/conversation/entity.py | 57 ++++++ homeassistant/components/conversation/http.py | 46 ++++- .../components/conversation/manifest.json | 2 +- .../components/conversation/models.py | 8 + .../components/conversation/trigger.py | 7 +- .../assist_pipeline/snapshots/test_init.ambr | 8 +- .../snapshots/test_websocket.ambr | 16 +- .../assist_pipeline/test_pipeline.py | 25 +-- .../assist_pipeline/test_websocket.py | 8 +- .../components/cloud/test_assist_pipeline.py | 2 + tests/components/cloud/test_http_api.py | 2 + tests/components/conversation/conftest.py | 13 +- .../conversation/snapshots/test_init.ambr | 166 +++++++++++++++++- .../conversation/test_default_agent.py | 12 +- tests/components/conversation/test_entity.py | 47 +++++ tests/components/conversation/test_init.py | 31 ++-- tests/components/conversation/test_trigger.py | 4 +- .../google_assistant_sdk/test_init.py | 5 +- .../conftest.py | 11 +- .../test_init.py | 4 +- tests/components/mobile_app/test_webhook.py | 2 +- tests/components/ollama/conftest.py | 6 + tests/components/ollama/test_init.py | 6 +- .../openai_conversation/conftest.py | 7 + .../openai_conversation/test_init.py | 2 +- 33 files changed, 566 insertions(+), 177 deletions(-) create mode 100644 homeassistant/components/conversation/entity.py create mode 100644 tests/components/conversation/test_entity.py diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py index 01a12b3635b..33e1b8c2f76 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -376,6 +376,10 @@ class Pipeline: This function was added in HA Core 2023.10, previous versions will raise if there are unexpected items in the serialized data. """ + # Migrate to new value for conversation agent + if data["conversation_engine"] == conversation.OLD_HOME_ASSISTANT_AGENT: + data["conversation_engine"] = conversation.HOME_ASSISTANT_AGENT + return cls( conversation_engine=data["conversation_engine"], conversation_language=data["conversation_language"], diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 8ca55876b28..b577e9de0d4 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -223,7 +223,10 @@ class CloudLoginView(HomeAssistantView): cloud: Cloud[CloudClient] = hass.data[DOMAIN] await cloud.login(data["email"], data["password"]) - new_cloud_pipeline_id = await async_create_cloud_pipeline(hass) + if "assist_pipeline" in hass.config.components: + new_cloud_pipeline_id = await async_create_cloud_pipeline(hass) + else: + new_cloud_pipeline_id = None return self.json({"success": True, "cloud_pipeline": new_cloud_pipeline_id}) diff --git a/homeassistant/components/cloud/stt.py b/homeassistant/components/cloud/stt.py index d718cc5201e..c68e9f245ee 100644 --- a/homeassistant/components/cloud/stt.py +++ b/homeassistant/components/cloud/stt.py @@ -24,6 +24,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.setup import async_when_setup from .assist_pipeline import async_migrate_cloud_pipeline_engine from .client import CloudClient @@ -86,9 +87,18 @@ class CloudProviderEntity(SpeechToTextEntity): async def async_added_to_hass(self) -> None: """Run when entity is about to be added to hass.""" - await async_migrate_cloud_pipeline_engine( - self.hass, platform=Platform.STT, engine_id=self.entity_id - ) + + async def pipeline_setup(hass: HomeAssistant, _comp: str) -> None: + """When assist_pipeline is set up.""" + assert self.platform.config_entry + self.platform.config_entry.async_create_task( + hass, + async_migrate_cloud_pipeline_engine( + self.hass, platform=Platform.STT, engine_id=self.entity_id + ), + ) + + async_when_setup(self.hass, "assist_pipeline", pipeline_setup) async def async_process_audio_stream( self, metadata: SpeechMetadata, stream: AsyncIterable[bytes] diff --git a/homeassistant/components/cloud/tts.py b/homeassistant/components/cloud/tts.py index 42e4b94a189..53cec74d133 100644 --- a/homeassistant/components/cloud/tts.py +++ b/homeassistant/components/cloud/tts.py @@ -27,6 +27,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.typing import ConfigType, DiscoveryInfoType +from homeassistant.setup import async_when_setup from .assist_pipeline import async_migrate_cloud_pipeline_engine from .client import CloudClient @@ -156,9 +157,19 @@ class CloudTTSEntity(TextToSpeechEntity): async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" await super().async_added_to_hass() - await async_migrate_cloud_pipeline_engine( - self.hass, platform=Platform.TTS, engine_id=self.entity_id - ) + + async def pipeline_setup(hass: HomeAssistant, _comp: str) -> None: + """When assist_pipeline is set up.""" + assert self.platform.config_entry + self.platform.config_entry.async_create_task( + hass, + async_migrate_cloud_pipeline_engine( + self.hass, platform=Platform.TTS, engine_id=self.entity_id + ), + ) + + async_when_setup(self.hass, "assist_pipeline", pipeline_setup) + self.async_on_remove( self.cloud.client.prefs.async_listen_updates(self._sync_prefs) ) diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index a0717ddaa58..63e0e9bff59 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations -from collections.abc import Iterable import logging import re from typing import Literal @@ -20,6 +19,7 @@ from homeassistant.core import ( ) from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, intent +from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass @@ -27,15 +27,19 @@ from .agent_manager import ( AgentInfo, agent_id_validator, async_converse, + async_get_agent, get_agent_manager, ) -from .const import DATA_CONFIG, HOME_ASSISTANT_AGENT +from .const import HOME_ASSISTANT_AGENT, OLD_HOME_ASSISTANT_AGENT +from .default_agent import async_get_default_agent, async_setup_default_agent +from .entity import ConversationEntity from .http import async_setup as async_setup_conversation_http from .models import AbstractConversationAgent, ConversationInput, ConversationResult __all__ = [ "DOMAIN", "HOME_ASSISTANT_AGENT", + "OLD_HOME_ASSISTANT_AGENT", "async_converse", "async_get_agent_info", "async_set_agent", @@ -122,16 +126,26 @@ async def async_get_conversation_languages( all conversation agents. """ agent_manager = get_agent_manager(hass) + entity_component: EntityComponent[ConversationEntity] = hass.data[DOMAIN] languages: set[str] = set() + agents: list[ConversationEntity | AbstractConversationAgent] + + if agent_id: + agent = async_get_agent(hass, agent_id) + + if agent is None: + raise ValueError(f"Agent {agent_id} not found") + + agents = [agent] - agent_ids: Iterable[str] - if agent_id is None: - agent_ids = iter(info.id for info in agent_manager.async_get_agent_info()) else: - agent_ids = (agent_id,) + agents = list(entity_component.entities) + for info in agent_manager.async_get_agent_info(): + agent = agent_manager.async_get_agent(info.id) + assert agent is not None + agents.append(agent) - for _agent_id in agent_ids: - agent = await agent_manager.async_get_agent(_agent_id) + for agent in agents: if agent.supported_languages == MATCH_ALL: return MATCH_ALL for language_tag in agent.supported_languages: @@ -146,10 +160,18 @@ def async_get_agent_info( agent_id: str | None = None, ) -> AgentInfo | None: """Get information on the agent or None if not found.""" - manager = get_agent_manager(hass) + agent = async_get_agent(hass, agent_id) - if agent_id is None: - agent_id = manager.default_agent + if agent is None: + return None + + if isinstance(agent, ConversationEntity): + name = agent.name + if not isinstance(name, str): + name = agent.entity_id + return AgentInfo(id=agent.entity_id, name=name) + + manager = get_agent_manager(hass) for agent_info in manager.async_get_agent_info(): if agent_info.id == agent_id: @@ -160,10 +182,11 @@ def async_get_agent_info( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Register the process service.""" - agent_manager = get_agent_manager(hass) + entity_component = hass.data[DOMAIN] = EntityComponent(_LOGGER, DOMAIN, hass) - if config_intents := config.get(DOMAIN, {}).get("intents"): - hass.data[DATA_CONFIG] = config_intents + await async_setup_default_agent( + hass, entity_component, config.get(DOMAIN, {}).get("intents", {}) + ) async def handle_process(service: ServiceCall) -> ServiceResponse: """Parse text into commands.""" @@ -188,7 +211,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def handle_reload(service: ServiceCall) -> None: """Reload intents.""" - agent = await agent_manager.async_get_agent() + agent = async_get_default_agent(hass) await agent.async_reload(language=service.data.get(ATTR_LANGUAGE)) hass.services.async_register( diff --git a/homeassistant/components/conversation/agent_manager.py b/homeassistant/components/conversation/agent_manager.py index f34ecfaecc9..838539b4992 100644 --- a/homeassistant/components/conversation/agent_manager.py +++ b/homeassistant/components/conversation/agent_manager.py @@ -2,8 +2,6 @@ from __future__ import annotations -import asyncio -from dataclasses import dataclass import logging from typing import Any @@ -11,10 +9,17 @@ import voluptuous as vol from homeassistant.core import Context, HomeAssistant, async_get_hass, callback from homeassistant.helpers import config_validation as cv, singleton +from homeassistant.helpers.entity_component import EntityComponent -from .const import DATA_CONFIG, HOME_ASSISTANT_AGENT -from .default_agent import DefaultAgent, async_setup as async_setup_default_agent -from .models import AbstractConversationAgent, ConversationInput, ConversationResult +from .const import DOMAIN, HOME_ASSISTANT_AGENT, OLD_HOME_ASSISTANT_AGENT +from .default_agent import async_get_default_agent +from .entity import ConversationEntity +from .models import ( + AbstractConversationAgent, + AgentInfo, + ConversationInput, + ConversationResult, +) _LOGGER = logging.getLogger(__name__) @@ -23,20 +28,37 @@ _LOGGER = logging.getLogger(__name__) @callback def get_agent_manager(hass: HomeAssistant) -> AgentManager: """Get the active agent.""" - manager = AgentManager(hass) - manager.async_setup() - return manager + return AgentManager(hass) def agent_id_validator(value: Any) -> str: """Validate agent ID.""" hass = async_get_hass() - manager = get_agent_manager(hass) - if not manager.async_is_valid_agent_id(cv.string(value)): + if async_get_agent(hass, cv.string(value)) is None: raise vol.Invalid("invalid agent ID") return value +@callback +def async_get_agent( + hass: HomeAssistant, agent_id: str | None = None +) -> AbstractConversationAgent | ConversationEntity | None: + """Get specified agent.""" + if agent_id is None or agent_id in (HOME_ASSISTANT_AGENT, OLD_HOME_ASSISTANT_AGENT): + return async_get_default_agent(hass) + + if "." in agent_id: + entity_component: EntityComponent[ConversationEntity] = hass.data[DOMAIN] + return entity_component.get_entity(agent_id) + + manager = get_agent_manager(hass) + + if not manager.async_is_valid_agent_id(agent_id): + return None + + return manager.async_get_agent(agent_id) + + async def async_converse( hass: HomeAssistant, text: str, @@ -47,13 +69,22 @@ async def async_converse( device_id: str | None = None, ) -> ConversationResult: """Process text and get intent.""" - agent = await get_agent_manager(hass).async_get_agent(agent_id) + agent = async_get_agent(hass, agent_id) + + if agent is None: + raise ValueError(f"Agent {agent_id} not found") + + if isinstance(agent, ConversationEntity): + agent.async_set_context(context) + method = agent.internal_async_process + else: + method = agent.async_process if language is None: language = hass.config.language _LOGGER.debug("Processing in %s: %s", language, text) - result = await agent.async_process( + result = await method( ConversationInput( text=text, context=context, @@ -65,52 +96,17 @@ async def async_converse( return result -@dataclass(frozen=True) -class AgentInfo: - """Container for conversation agent info.""" - - id: str - name: str - - class AgentManager: """Class to manage conversation agents.""" - default_agent: str = HOME_ASSISTANT_AGENT - _builtin_agent: AbstractConversationAgent | None = None - def __init__(self, hass: HomeAssistant) -> None: """Initialize the conversation agents.""" self.hass = hass self._agents: dict[str, AbstractConversationAgent] = {} - self._builtin_agent_init_lock = asyncio.Lock() - def async_setup(self) -> None: - """Set up the conversation agents.""" - async_setup_default_agent(self.hass) - - async def async_get_agent( - self, agent_id: str | None = None - ) -> AbstractConversationAgent: + @callback + def async_get_agent(self, agent_id: str) -> AbstractConversationAgent | None: """Get the agent.""" - if agent_id is None: - agent_id = self.default_agent - - if agent_id == HOME_ASSISTANT_AGENT: - if self._builtin_agent is not None: - return self._builtin_agent - - async with self._builtin_agent_init_lock: - if self._builtin_agent is not None: - return self._builtin_agent - - self._builtin_agent = DefaultAgent(self.hass) - await self._builtin_agent.async_initialize( - self.hass.data.get(DATA_CONFIG) - ) - - return self._builtin_agent - if agent_id not in self._agents: raise ValueError(f"Agent {agent_id} not found") @@ -119,12 +115,7 @@ class AgentManager: @callback def async_get_agent_info(self) -> list[AgentInfo]: """List all agents.""" - agents: list[AgentInfo] = [ - AgentInfo( - id=HOME_ASSISTANT_AGENT, - name="Home Assistant", - ) - ] + agents: list[AgentInfo] = [] for agent_id, agent in self._agents.items(): config_entry = self.hass.config_entries.async_get_entry(agent_id) @@ -148,7 +139,7 @@ class AgentManager: @callback def async_is_valid_agent_id(self, agent_id: str) -> bool: """Check if the agent id is valid.""" - return agent_id in self._agents or agent_id == HOME_ASSISTANT_AGENT + return agent_id in self._agents @callback def async_set_agent(self, agent_id: str, agent: AbstractConversationAgent) -> None: diff --git a/homeassistant/components/conversation/const.py b/homeassistant/components/conversation/const.py index 5cb5ca3bdea..d20b6d96aa2 100644 --- a/homeassistant/components/conversation/const.py +++ b/homeassistant/components/conversation/const.py @@ -2,5 +2,5 @@ DOMAIN = "conversation" DEFAULT_EXPOSED_ATTRIBUTES = {"device_class"} -HOME_ASSISTANT_AGENT = "homeassistant" -DATA_CONFIG = "conversation_config" +HOME_ASSISTANT_AGENT = "conversation.home_assistant" +OLD_HOME_ASSISTANT_AGENT = "homeassistant" diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 5a8d7b64eec..32ab7924916 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -24,7 +24,7 @@ from hassil.util import merge_dict from home_assistant_intents import ErrorKey, get_intents, get_languages import yaml -from homeassistant import core, setup +from homeassistant import core from homeassistant.components.homeassistant.exposed_entities import ( async_listen_entity_updates, async_should_expose, @@ -40,6 +40,7 @@ from homeassistant.helpers import ( template, translation, ) +from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import ( EventStateChangedData, async_track_state_added_domain, @@ -47,7 +48,8 @@ from homeassistant.helpers.event import ( from homeassistant.util.json import JsonObjectType, json_loads_object from .const import DEFAULT_EXPOSED_ATTRIBUTES, DOMAIN -from .models import AbstractConversationAgent, ConversationInput, ConversationResult +from .entity import ConversationEntity +from .models import ConversationInput, ConversationResult _LOGGER = logging.getLogger(__name__) _DEFAULT_ERROR_TEXT = "Sorry, I couldn't understand that" @@ -60,6 +62,14 @@ TRIGGER_CALLBACK_TYPE = Callable[ METADATA_CUSTOM_SENTENCE = "hass_custom_sentence" METADATA_CUSTOM_FILE = "hass_custom_file" +DATA_DEFAULT_ENTITY = "conversation_default_entity" + + +@core.callback +def async_get_default_agent(hass: core.HomeAssistant) -> DefaultAgent: + """Get the default agent.""" + return hass.data[DATA_DEFAULT_ENTITY] + def json_load(fp: IO[str]) -> JsonObjectType: """Wrap json_loads for get_intents.""" @@ -109,9 +119,16 @@ def _get_language_variations(language: str) -> Iterable[str]: yield lang -@core.callback -def async_setup(hass: core.HomeAssistant) -> None: +async def async_setup_default_agent( + hass: core.HomeAssistant, + entity_component: EntityComponent[ConversationEntity], + config_intents: dict[str, Any], +) -> None: """Set up entity registry listener for the default agent.""" + entity = DefaultAgent(hass, config_intents) + await entity_component.async_add_entities([entity]) + hass.data[DATA_DEFAULT_ENTITY] = entity + entity_registry = er.async_get(hass) for entity_id in entity_registry.entities: async_should_expose(hass, DOMAIN, entity_id) @@ -131,17 +148,21 @@ def async_setup(hass: core.HomeAssistant) -> None: start.async_at_started(hass, async_hass_started) -class DefaultAgent(AbstractConversationAgent): +class DefaultAgent(ConversationEntity): """Default agent for conversation agent.""" - def __init__(self, hass: core.HomeAssistant) -> None: + _attr_name = "Home Assistant" + + def __init__( + self, hass: core.HomeAssistant, config_intents: dict[str, Any] + ) -> None: """Initialize the default agent.""" self.hass = hass self._lang_intents: dict[str, LanguageIntents] = {} self._lang_lock: dict[str, asyncio.Lock] = defaultdict(asyncio.Lock) # intent -> [sentences] - self._config_intents: dict[str, Any] = {} + self._config_intents: dict[str, Any] = config_intents self._slot_lists: dict[str, SlotList] | None = None # Sentences that will trigger a callback (skipping intent recognition) @@ -154,15 +175,6 @@ class DefaultAgent(AbstractConversationAgent): """Return a list of supported languages.""" return get_languages() - async def async_initialize(self, config_intents: dict[str, Any] | None) -> None: - """Initialize the default agent.""" - if "intent" not in self.hass.config.components: - await setup.async_setup_component(self.hass, "intent", {}) - - # Intents from config may only contains sentences for HA config's language - if config_intents: - self._config_intents = config_intents - @core.callback def _filter_entity_registry_changes(self, event_data: dict[str, Any]) -> bool: """Filter entity registry changed events.""" diff --git a/homeassistant/components/conversation/entity.py b/homeassistant/components/conversation/entity.py new file mode 100644 index 00000000000..12dbea41344 --- /dev/null +++ b/homeassistant/components/conversation/entity.py @@ -0,0 +1,57 @@ +"""Entity for conversation integration.""" + +from abc import abstractmethod +from typing import Literal, final + +from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.util import dt as dt_util + +from .models import ConversationInput, ConversationResult + + +class ConversationEntity(RestoreEntity): + """Entity that supports conversations.""" + + _attr_should_poll = False + __last_activity: str | None = None + + @property + @final + def state(self) -> str | None: + """Return the state of the entity.""" + if self.__last_activity is None: + return None + return self.__last_activity + + async def async_internal_added_to_hass(self) -> None: + """Call when the entity is added to hass.""" + await super().async_internal_added_to_hass() + state = await self.async_get_last_state() + if ( + state is not None + and state.state is not None + and state.state not in (STATE_UNAVAILABLE, STATE_UNKNOWN) + ): + self.__last_activity = state.state + + @final + async def internal_async_process( + self, user_input: ConversationInput + ) -> ConversationResult: + """Process a sentence.""" + self.__last_activity = dt_util.utcnow().isoformat() + self.async_write_ha_state() + return await self.async_process(user_input) + + @property + @abstractmethod + def supported_languages(self) -> list[str] | Literal["*"]: + """Return a list of supported languages.""" + + @abstractmethod + async def async_process(self, user_input: ConversationInput) -> ConversationResult: + """Process a sentence.""" + + async def async_prepare(self, language: str | None = None) -> None: + """Load intents for a language.""" diff --git a/homeassistant/components/conversation/http.py b/homeassistant/components/conversation/http.py index fb67d686b23..beda7ba1550 100644 --- a/homeassistant/components/conversation/http.py +++ b/homeassistant/components/conversation/http.py @@ -19,16 +19,24 @@ from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.const import MATCH_ALL from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers import config_validation as cv, intent +from homeassistant.helpers.entity_component import EntityComponent from homeassistant.util import language as language_util -from .agent_manager import agent_id_validator, async_converse, get_agent_manager -from .const import HOME_ASSISTANT_AGENT +from .agent_manager import ( + agent_id_validator, + async_converse, + async_get_agent, + get_agent_manager, +) +from .const import DOMAIN from .default_agent import ( METADATA_CUSTOM_FILE, METADATA_CUSTOM_SENTENCE, DefaultAgent, SentenceTriggerResult, + async_get_default_agent, ) +from .entity import ConversationEntity from .models import ConversationInput @@ -83,8 +91,14 @@ async def websocket_prepare( msg: dict[str, Any], ) -> None: """Reload intents.""" - manager = get_agent_manager(hass) - agent = await manager.async_get_agent(msg.get("agent_id")) + 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" + ) + return + await agent.async_prepare(msg.get("language")) connection.send_result(msg["id"]) @@ -101,14 +115,32 @@ async def websocket_list_agents( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict ) -> None: """List conversation agents and, optionally, if they support a given language.""" - manager = get_agent_manager(hass) + entity_component: EntityComponent[ConversationEntity] = hass.data[DOMAIN] country = msg.get("country") language = msg.get("language") agents = [] + for entity in entity_component.entities: + supported_languages = entity.supported_languages + if language and supported_languages != MATCH_ALL: + supported_languages = language_util.matches( + language, supported_languages, country + ) + + agents.append( + { + "id": entity.entity_id, + "name": entity.name or entity.entity_id, + "supported_languages": supported_languages, + } + ) + + manager = get_agent_manager(hass) + for agent_info in manager.async_get_agent_info(): - agent = await manager.async_get_agent(agent_info.id) + agent = manager.async_get_agent(agent_info.id) + assert agent is not None supported_languages = agent.supported_languages if language and supported_languages != MATCH_ALL: @@ -139,7 +171,7 @@ async def websocket_hass_agent_debug( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict ) -> None: """Return intents that would be matched by the default agent for a list of sentences.""" - agent = await get_agent_manager(hass).async_get_agent(HOME_ASSISTANT_AGENT) + agent = async_get_default_agent(hass) assert isinstance(agent, DefaultAgent) results = [ await agent.async_recognize( diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 7f463483bf9..07fc86313ba 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -2,7 +2,7 @@ "domain": "conversation", "name": "Conversation", "codeowners": ["@home-assistant/core", "@synesthesiam"], - "dependencies": ["http"], + "dependencies": ["http", "intent"], "documentation": "https://www.home-assistant.io/integrations/conversation", "integration_type": "system", "iot_class": "local_push", diff --git a/homeassistant/components/conversation/models.py b/homeassistant/components/conversation/models.py index 22b3437907c..3fd24152698 100644 --- a/homeassistant/components/conversation/models.py +++ b/homeassistant/components/conversation/models.py @@ -10,6 +10,14 @@ from homeassistant.core import Context from homeassistant.helpers import intent +@dataclass(frozen=True) +class AgentInfo: + """Container for conversation agent info.""" + + id: str + name: str + + @dataclass(slots=True) class ConversationInput: """User input to be processed.""" diff --git a/homeassistant/components/conversation/trigger.py b/homeassistant/components/conversation/trigger.py index 05fea054bca..0a4cbfcb7e5 100644 --- a/homeassistant/components/conversation/trigger.py +++ b/homeassistant/components/conversation/trigger.py @@ -14,9 +14,8 @@ from homeassistant.helpers.script import ScriptRunResult from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import UNDEFINED, ConfigType -from .agent_manager import get_agent_manager -from .const import DOMAIN, HOME_ASSISTANT_AGENT -from .default_agent import DefaultAgent +from .const import DOMAIN +from .default_agent import DefaultAgent, async_get_default_agent def has_no_punctuation(value: list[str]) -> list[str]: @@ -111,7 +110,7 @@ async def async_attach_trigger( # two trigger copies for who will provide a response. return None - default_agent = await get_agent_manager(hass).async_get_agent(HOME_ASSISTANT_AGENT) + default_agent = async_get_default_agent(hass) assert isinstance(default_agent, DefaultAgent) return default_agent.register_trigger(sentences, call_action) diff --git a/tests/components/assist_pipeline/snapshots/test_init.ambr b/tests/components/assist_pipeline/snapshots/test_init.ambr index bbd0c9d333a..8124ed4ab85 100644 --- a/tests/components/assist_pipeline/snapshots/test_init.ambr +++ b/tests/components/assist_pipeline/snapshots/test_init.ambr @@ -34,7 +34,7 @@ 'data': dict({ 'conversation_id': None, 'device_id': None, - 'engine': 'homeassistant', + 'engine': 'conversation.home_assistant', 'intent_input': 'test transcript', 'language': 'en', }), @@ -123,7 +123,7 @@ 'data': dict({ 'conversation_id': None, 'device_id': None, - 'engine': 'homeassistant', + 'engine': 'conversation.home_assistant', 'intent_input': 'test transcript', 'language': 'en-US', }), @@ -212,7 +212,7 @@ 'data': dict({ 'conversation_id': None, 'device_id': None, - 'engine': 'homeassistant', + 'engine': 'conversation.home_assistant', 'intent_input': 'test transcript', 'language': 'en-US', }), @@ -325,7 +325,7 @@ 'data': dict({ 'conversation_id': None, 'device_id': None, - 'engine': 'homeassistant', + 'engine': 'conversation.home_assistant', 'intent_input': 'test transcript', 'language': 'en', }), diff --git a/tests/components/assist_pipeline/snapshots/test_websocket.ambr b/tests/components/assist_pipeline/snapshots/test_websocket.ambr index 10a76bc9344..f952e3b7286 100644 --- a/tests/components/assist_pipeline/snapshots/test_websocket.ambr +++ b/tests/components/assist_pipeline/snapshots/test_websocket.ambr @@ -33,7 +33,7 @@ dict({ 'conversation_id': None, 'device_id': None, - 'engine': 'homeassistant', + 'engine': 'conversation.home_assistant', 'intent_input': 'test transcript', 'language': 'en', }) @@ -114,7 +114,7 @@ dict({ 'conversation_id': None, 'device_id': None, - 'engine': 'homeassistant', + 'engine': 'conversation.home_assistant', 'intent_input': 'test transcript', 'language': 'en', }) @@ -207,7 +207,7 @@ dict({ 'conversation_id': None, 'device_id': None, - 'engine': 'homeassistant', + 'engine': 'conversation.home_assistant', 'intent_input': 'test transcript', 'language': 'en', }) @@ -409,7 +409,7 @@ dict({ 'conversation_id': None, 'device_id': None, - 'engine': 'homeassistant', + 'engine': 'conversation.home_assistant', 'intent_input': 'test transcript', 'language': 'en', }) @@ -615,7 +615,7 @@ dict({ 'conversation_id': None, 'device_id': None, - 'engine': 'homeassistant', + 'engine': 'conversation.home_assistant', 'intent_input': 'Are the lights on?', 'language': 'en', }) @@ -637,7 +637,7 @@ dict({ 'conversation_id': None, 'device_id': None, - 'engine': 'homeassistant', + 'engine': 'conversation.home_assistant', 'intent_input': 'Are the lights on?', 'language': 'en', }) @@ -665,7 +665,7 @@ dict({ 'conversation_id': None, 'device_id': None, - 'engine': 'homeassistant', + 'engine': 'conversation.home_assistant', 'intent_input': 'never mind', 'language': 'en', }) @@ -799,7 +799,7 @@ dict({ 'conversation_id': 'mock-conversation-id', 'device_id': 'mock-device-id', - 'engine': 'homeassistant', + 'engine': 'conversation.home_assistant', 'intent_input': 'Are the lights on?', 'language': 'en', }) diff --git a/tests/components/assist_pipeline/test_pipeline.py b/tests/components/assist_pipeline/test_pipeline.py index 3bfe6605839..3588bba6416 100644 --- a/tests/components/assist_pipeline/test_pipeline.py +++ b/tests/components/assist_pipeline/test_pipeline.py @@ -6,6 +6,7 @@ from unittest.mock import ANY, patch import pytest +from homeassistant.components import conversation from homeassistant.components.assist_pipeline.const import DOMAIN from homeassistant.components.assist_pipeline.pipeline import ( STORAGE_KEY, @@ -117,6 +118,7 @@ async def test_loading_pipelines_from_storage( hass: HomeAssistant, hass_storage: dict[str, Any] ) -> None: """Test loading stored pipelines on start.""" + id_1 = "01GX8ZWBAQYWNB1XV3EXEZ75DY" hass_storage[STORAGE_KEY] = { "version": STORAGE_VERSION, "minor_version": STORAGE_VERSION_MINOR, @@ -124,9 +126,9 @@ async def test_loading_pipelines_from_storage( "data": { "items": [ { - "conversation_engine": "conversation_engine_1", + "conversation_engine": conversation.OLD_HOME_ASSISTANT_AGENT, "conversation_language": "language_1", - "id": "01GX8ZWBAQYWNB1XV3EXEZ75DY", + "id": id_1, "language": "language_1", "name": "name_1", "stt_engine": "stt_engine_1", @@ -166,7 +168,7 @@ async def test_loading_pipelines_from_storage( "wake_word_id": "wakeword_id_3", }, ], - "preferred_item": "01GX8ZWBAQYWNB1XV3EXEZ75DY", + "preferred_item": id_1, }, } @@ -175,7 +177,8 @@ async def test_loading_pipelines_from_storage( pipeline_data: PipelineData = hass.data[DOMAIN] store = pipeline_data.pipeline_store assert len(store.data) == 3 - assert store.async_get_preferred_item() == "01GX8ZWBAQYWNB1XV3EXEZ75DY" + assert store.async_get_preferred_item() == id_1 + assert store.data[id_1].conversation_engine == conversation.HOME_ASSISTANT_AGENT async def test_migrate_pipeline_store( @@ -262,7 +265,7 @@ async def test_create_default_pipeline( tts_engine_id="test", pipeline_name="Test pipeline", ) == Pipeline( - conversation_engine="homeassistant", + conversation_engine="conversation.home_assistant", conversation_language="en", id=ANY, language="en", @@ -304,7 +307,7 @@ async def test_get_pipelines(hass: HomeAssistant) -> None: pipelines = async_get_pipelines(hass) assert list(pipelines) == [ Pipeline( - conversation_engine="homeassistant", + conversation_engine="conversation.home_assistant", conversation_language="en", id=ANY, language="en", @@ -351,7 +354,7 @@ async def test_default_pipeline_no_stt_tts( # Check the default pipeline pipeline = async_get_pipeline(hass, None) assert pipeline == Pipeline( - conversation_engine="homeassistant", + conversation_engine="conversation.home_assistant", conversation_language=conv_language, id=pipeline.id, language=pipeline_language, @@ -414,7 +417,7 @@ async def test_default_pipeline( # Check the default pipeline pipeline = async_get_pipeline(hass, None) assert pipeline == Pipeline( - conversation_engine="homeassistant", + conversation_engine="conversation.home_assistant", conversation_language=conv_language, id=pipeline.id, language=pipeline_language, @@ -445,7 +448,7 @@ async def test_default_pipeline_unsupported_stt_language( # Check the default pipeline pipeline = async_get_pipeline(hass, None) assert pipeline == Pipeline( - conversation_engine="homeassistant", + conversation_engine="conversation.home_assistant", conversation_language="en", id=pipeline.id, language="en", @@ -476,7 +479,7 @@ async def test_default_pipeline_unsupported_tts_language( # Check the default pipeline pipeline = async_get_pipeline(hass, None) assert pipeline == Pipeline( - conversation_engine="homeassistant", + conversation_engine="conversation.home_assistant", conversation_language="en", id=pipeline.id, language="en", @@ -502,7 +505,7 @@ async def test_update_pipeline( pipelines = list(pipelines) assert pipelines == [ Pipeline( - conversation_engine="homeassistant", + conversation_engine="conversation.home_assistant", conversation_language="en", id=ANY, language="en", diff --git a/tests/components/assist_pipeline/test_websocket.py b/tests/components/assist_pipeline/test_websocket.py index 0883046f3a1..e08dd9685ea 100644 --- a/tests/components/assist_pipeline/test_websocket.py +++ b/tests/components/assist_pipeline/test_websocket.py @@ -1166,7 +1166,7 @@ async def test_get_pipeline( msg = await client.receive_json() assert msg["success"] assert msg["result"] == { - "conversation_engine": "homeassistant", + "conversation_engine": "conversation.home_assistant", "conversation_language": "en", "id": ANY, "language": "en", @@ -1250,7 +1250,7 @@ async def test_list_pipelines( assert msg["result"] == { "pipelines": [ { - "conversation_engine": "homeassistant", + "conversation_engine": "conversation.home_assistant", "conversation_language": "en", "id": ANY, "language": "en", @@ -2012,7 +2012,7 @@ async def test_wake_word_cooldown_different_entities( await client_pipeline.send_json_auto_id( { "type": "assist_pipeline/pipeline/create", - "conversation_engine": "homeassistant", + "conversation_engine": "conversation.home_assistant", "conversation_language": "en-US", "language": "en", "name": "pipeline_with_wake_word_1", @@ -2032,7 +2032,7 @@ async def test_wake_word_cooldown_different_entities( await client_pipeline.send_json_auto_id( { "type": "assist_pipeline/pipeline/create", - "conversation_engine": "homeassistant", + "conversation_engine": "conversation.home_assistant", "conversation_language": "en-US", "language": "en", "name": "pipeline_with_wake_word_2", diff --git a/tests/components/cloud/test_assist_pipeline.py b/tests/components/cloud/test_assist_pipeline.py index 5c2fc074898..de30212c040 100644 --- a/tests/components/cloud/test_assist_pipeline.py +++ b/tests/components/cloud/test_assist_pipeline.py @@ -7,10 +7,12 @@ from homeassistant.components.cloud.assist_pipeline import ( ) from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component async def test_migrate_pipeline_invalid_platform(hass: HomeAssistant) -> None: """Test migrate pipeline with invalid platform.""" + await async_setup_component(hass, "assist_pipeline", {}) with pytest.raises(ValueError): await async_migrate_cloud_pipeline_engine( hass, Platform.BINARY_SENSOR, "test-engine-id" diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 0dad7cfa882..5ee9af88681 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -231,6 +231,7 @@ async def test_login_view_create_pipeline( } assert await async_setup_component(hass, "homeassistant", {}) + assert await async_setup_component(hass, "assist_pipeline", {}) assert await async_setup_component(hass, DOMAIN, {"cloud": {}}) await hass.async_block_till_done() @@ -270,6 +271,7 @@ async def test_login_view_create_pipeline_fail( } assert await async_setup_component(hass, "homeassistant", {}) + assert await async_setup_component(hass, "assist_pipeline", {}) assert await async_setup_component(hass, DOMAIN, {"cloud": {}}) await hass.async_block_till_done() diff --git a/tests/components/conversation/conftest.py b/tests/components/conversation/conftest.py index d6c2d9e2e5e..4801e506460 100644 --- a/tests/components/conversation/conftest.py +++ b/tests/components/conversation/conftest.py @@ -7,6 +7,8 @@ import pytest from homeassistant.components import conversation from homeassistant.components.shopping_list import intent as sl_intent from homeassistant.const import MATCH_ALL +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component from . import MockAgent @@ -14,7 +16,7 @@ from tests.common import MockConfigEntry @pytest.fixture -def mock_agent_support_all(hass): +def mock_agent_support_all(hass: HomeAssistant): """Mock agent that supports all languages.""" entry = MockConfigEntry(entry_id="mock-entry-support-all") entry.add_to_hass(hass) @@ -34,7 +36,7 @@ def mock_shopping_list_io(): @pytest.fixture -async def sl_setup(hass): +async def sl_setup(hass: HomeAssistant): """Set up the shopping list.""" entry = MockConfigEntry(domain="shopping_list") @@ -43,3 +45,10 @@ async def sl_setup(hass): assert await hass.config_entries.async_setup(entry.entry_id) await sl_intent.async_setup_intents(hass) + + +@pytest.fixture +async def init_components(hass: HomeAssistant): + """Initialize relevant components with empty configs.""" + assert await async_setup_component(hass, "homeassistant", {}) + assert await async_setup_component(hass, "conversation", {}) diff --git a/tests/components/conversation/snapshots/test_init.ambr b/tests/components/conversation/snapshots/test_init.ambr index 6af9d197e01..d514d145477 100644 --- a/tests/components/conversation/snapshots/test_init.ambr +++ b/tests/components/conversation/snapshots/test_init.ambr @@ -101,7 +101,7 @@ # --- # name: test_get_agent_info dict({ - 'id': 'homeassistant', + 'id': 'conversation.home_assistant', 'name': 'Home Assistant', }) # --- @@ -113,7 +113,7 @@ # --- # name: test_get_agent_info.2 dict({ - 'id': 'homeassistant', + 'id': 'conversation.home_assistant', 'name': 'Home Assistant', }) # --- @@ -127,7 +127,7 @@ dict({ 'agents': list([ dict({ - 'id': 'homeassistant', + 'id': 'conversation.home_assistant', 'name': 'Home Assistant', 'supported_languages': list([ 'af', @@ -207,7 +207,7 @@ dict({ 'agents': list([ dict({ - 'id': 'homeassistant', + 'id': 'conversation.home_assistant', 'name': 'Home Assistant', 'supported_languages': list([ ]), @@ -231,7 +231,7 @@ dict({ 'agents': list([ dict({ - 'id': 'homeassistant', + 'id': 'conversation.home_assistant', 'name': 'Home Assistant', 'supported_languages': list([ 'en', @@ -255,7 +255,7 @@ dict({ 'agents': list([ dict({ - 'id': 'homeassistant', + 'id': 'conversation.home_assistant', 'name': 'Home Assistant', 'supported_languages': list([ 'en', @@ -279,7 +279,7 @@ dict({ 'agents': list([ dict({ - 'id': 'homeassistant', + 'id': 'conversation.home_assistant', 'name': 'Home Assistant', 'supported_languages': list([ 'de', @@ -304,7 +304,7 @@ dict({ 'agents': list([ dict({ - 'id': 'homeassistant', + 'id': 'conversation.home_assistant', 'name': 'Home Assistant', 'supported_languages': list([ 'de-CH', @@ -415,6 +415,36 @@ }), }) # --- +# name: test_http_processing_intent[conversation.home_assistant] + dict({ + 'conversation_id': None, + 'response': dict({ + 'card': dict({ + }), + 'data': dict({ + 'failed': list([ + ]), + 'success': list([ + dict({ + 'id': 'light.kitchen', + 'name': 'kitchen', + 'type': 'entity', + }), + ]), + 'targets': list([ + ]), + }), + 'language': 'en', + 'response_type': 'action_done', + 'speech': dict({ + 'plain': dict({ + 'extra_data': None, + 'speech': 'Turned on the light', + }), + }), + }), + }) +# --- # name: test_http_processing_intent[homeassistant] dict({ 'conversation_id': None, @@ -1035,6 +1065,36 @@ }), }) # --- +# name: test_turn_on_intent[None-turn kitchen on-conversation.home_assistant] + dict({ + 'conversation_id': None, + 'response': dict({ + 'card': dict({ + }), + 'data': dict({ + 'failed': list([ + ]), + 'success': list([ + dict({ + 'id': 'light.kitchen', + 'name': 'kitchen', + 'type': , + }), + ]), + 'targets': list([ + ]), + }), + 'language': 'en', + 'response_type': 'action_done', + 'speech': dict({ + 'plain': dict({ + 'extra_data': None, + 'speech': 'Turned on the light', + }), + }), + }), + }) +# --- # name: test_turn_on_intent[None-turn kitchen on-homeassistant] dict({ 'conversation_id': None, @@ -1095,6 +1155,36 @@ }), }) # --- +# name: test_turn_on_intent[None-turn on kitchen-conversation.home_assistant] + dict({ + 'conversation_id': None, + 'response': dict({ + 'card': dict({ + }), + 'data': dict({ + 'failed': list([ + ]), + 'success': list([ + dict({ + 'id': 'light.kitchen', + 'name': 'kitchen', + 'type': , + }), + ]), + 'targets': list([ + ]), + }), + 'language': 'en', + 'response_type': 'action_done', + 'speech': dict({ + 'plain': dict({ + 'extra_data': None, + 'speech': 'Turned on the light', + }), + }), + }), + }) +# --- # name: test_turn_on_intent[None-turn on kitchen-homeassistant] dict({ 'conversation_id': None, @@ -1155,6 +1245,36 @@ }), }) # --- +# name: test_turn_on_intent[my_new_conversation-turn kitchen on-conversation.home_assistant] + dict({ + 'conversation_id': None, + 'response': dict({ + 'card': dict({ + }), + 'data': dict({ + 'failed': list([ + ]), + 'success': list([ + dict({ + 'id': 'light.kitchen', + 'name': 'kitchen', + 'type': , + }), + ]), + 'targets': list([ + ]), + }), + 'language': 'en', + 'response_type': 'action_done', + 'speech': dict({ + 'plain': dict({ + 'extra_data': None, + 'speech': 'Turned on the light', + }), + }), + }), + }) +# --- # name: test_turn_on_intent[my_new_conversation-turn kitchen on-homeassistant] dict({ 'conversation_id': None, @@ -1215,6 +1335,36 @@ }), }) # --- +# name: test_turn_on_intent[my_new_conversation-turn on kitchen-conversation.home_assistant] + dict({ + 'conversation_id': None, + 'response': dict({ + 'card': dict({ + }), + 'data': dict({ + 'failed': list([ + ]), + 'success': list([ + dict({ + 'id': 'light.kitchen', + 'name': 'kitchen', + 'type': , + }), + ]), + 'targets': list([ + ]), + }), + 'language': 'en', + 'response_type': 'action_done', + 'speech': dict({ + 'plain': dict({ + 'extra_data': None, + 'speech': 'Turned on the light', + }), + }), + }), + }) +# --- # name: test_turn_on_intent[my_new_conversation-turn on kitchen-homeassistant] dict({ 'conversation_id': None, diff --git a/tests/components/conversation/test_default_agent.py b/tests/components/conversation/test_default_agent.py index c600c71711e..474198cb8a3 100644 --- a/tests/components/conversation/test_default_agent.py +++ b/tests/components/conversation/test_default_agent.py @@ -7,7 +7,7 @@ from hassil.recognize import Intent, IntentData, MatchEntity, RecognizeResult import pytest from homeassistant.components import conversation -from homeassistant.components.conversation import agent_manager, default_agent +from homeassistant.components.conversation import default_agent from homeassistant.components.homeassistant.exposed_entities import ( async_get_assistant_settings, ) @@ -152,9 +152,7 @@ async def test_conversation_agent( init_components, ) -> None: """Test DefaultAgent.""" - agent = await agent_manager.get_agent_manager(hass).async_get_agent( - conversation.HOME_ASSISTANT_AGENT - ) + agent = default_agent.async_get_default_agent(hass) with patch( "homeassistant.components.conversation.default_agent.get_languages", return_value=["dwarvish", "elvish", "entish"], @@ -181,6 +179,7 @@ async def test_expose_flag_automatically_set( # After setting up conversation, the expose flag should now be set on all entities assert async_get_assistant_settings(hass, conversation.DOMAIN) == { + "conversation.home_assistant": {"should_expose": False}, light.entity_id: {"should_expose": True}, test.entity_id: {"should_expose": False}, } @@ -190,6 +189,7 @@ async def test_expose_flag_automatically_set( hass.states.async_set(new_light, "test") await hass.async_block_till_done() assert async_get_assistant_settings(hass, conversation.DOMAIN) == { + "conversation.home_assistant": {"should_expose": False}, light.entity_id: {"should_expose": True}, new_light: {"should_expose": True}, test.entity_id: {"should_expose": False}, @@ -254,9 +254,7 @@ async def test_trigger_sentences(hass: HomeAssistant, init_components) -> None: trigger_sentences = ["It's party time", "It is time to party"] trigger_response = "Cowabunga!" - agent = await agent_manager.get_agent_manager(hass).async_get_agent( - conversation.HOME_ASSISTANT_AGENT - ) + agent = default_agent.async_get_default_agent(hass) assert isinstance(agent, default_agent.DefaultAgent) callback = AsyncMock(return_value=trigger_response) diff --git a/tests/components/conversation/test_entity.py b/tests/components/conversation/test_entity.py new file mode 100644 index 00000000000..c84f94c4aa4 --- /dev/null +++ b/tests/components/conversation/test_entity.py @@ -0,0 +1,47 @@ +"""Tests for conversation entity.""" + +from unittest.mock import patch + +from homeassistant.core import Context, HomeAssistant, State +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util + +from tests.common import mock_restore_cache + + +async def test_state_set_and_restore(hass: HomeAssistant) -> None: + """Test we set and restore state in the integration.""" + entity_id = "conversation.home_assistant" + timestamp = "2023-01-01T23:59:59+00:00" + mock_restore_cache(hass, (State(entity_id, timestamp),)) + + await async_setup_component(hass, "homeassistant", {}) + await async_setup_component(hass, "conversation", {}) + + state = hass.states.get(entity_id) + assert state + assert state.state == timestamp + + now = dt_util.utcnow() + context = Context() + + with ( + patch( + "homeassistant.components.conversation.default_agent.DefaultAgent.async_process" + ) as mock_process, + patch("homeassistant.util.dt.utcnow", return_value=now), + ): + await hass.services.async_call( + "conversation", + "process", + {"text": "Hello"}, + context=context, + blocking=True, + ) + + assert len(mock_process.mock_calls) == 1 + + state = hass.states.get(entity_id) + assert state + assert state.state == now.isoformat() + assert state.context is context diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index 62f67548ece..5b117c1ac70 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -9,7 +9,7 @@ from syrupy.assertion import SnapshotAssertion import voluptuous as vol from homeassistant.components import conversation -from homeassistant.components.conversation import agent_manager, default_agent +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 @@ -35,7 +35,13 @@ from tests.common import ( from tests.components.light.common import MockLight from tests.typing import ClientSessionGenerator, WebSocketGenerator -AGENT_ID_OPTIONS = [None, conversation.HOME_ASSISTANT_AGENT] +AGENT_ID_OPTIONS = [ + None, + # Old value of conversation.HOME_ASSISTANT_AGENT, + "homeassistant", + # Current value of conversation.HOME_ASSISTANT_AGENT, + "conversation.home_assistant", +] class OrderBeerIntentHandler(intent.IntentHandler): @@ -51,14 +57,6 @@ class OrderBeerIntentHandler(intent.IntentHandler): return response -@pytest.fixture -async def init_components(hass): - """Initialize relevant components with empty configs.""" - assert await async_setup_component(hass, "homeassistant", {}) - assert await async_setup_component(hass, "conversation", {}) - assert await async_setup_component(hass, "intent", {}) - - @pytest.mark.parametrize("agent_id", AGENT_ID_OPTIONS) async def test_http_processing_intent( hass: HomeAssistant, @@ -752,7 +750,7 @@ async def test_ws_prepare( """Test the Websocket prepare conversation API.""" assert await async_setup_component(hass, "homeassistant", {}) assert await async_setup_component(hass, "conversation", {}) - agent = await agent_manager.get_agent_manager(hass).async_get_agent() + agent = default_agent.async_get_default_agent(hass) assert isinstance(agent, default_agent.DefaultAgent) # No intents should be loaded yet @@ -854,7 +852,7 @@ async def test_prepare_reload(hass: HomeAssistant) -> None: assert await async_setup_component(hass, "conversation", {}) # Load intents - agent = await agent_manager.get_agent_manager(hass).async_get_agent() + agent = default_agent.async_get_default_agent(hass) assert isinstance(agent, default_agent.DefaultAgent) await agent.async_prepare(language) @@ -882,7 +880,7 @@ async def test_prepare_fail(hass: HomeAssistant) -> None: assert await async_setup_component(hass, "conversation", {}) # Load intents - agent = await agent_manager.get_agent_manager(hass).async_get_agent() + agent = default_agent.async_get_default_agent(hass) assert isinstance(agent, default_agent.DefaultAgent) await agent.async_prepare("not-a-language") @@ -919,7 +917,7 @@ async def test_non_default_response(hass: HomeAssistant, init_components) -> Non hass.states.async_set("cover.front_door", "closed") calls = async_mock_service(hass, "cover", SERVICE_OPEN_COVER) - agent = await agent_manager.get_agent_manager(hass).async_get_agent() + agent = default_agent.async_get_default_agent(hass) assert isinstance(agent, default_agent.DefaultAgent) result = await agent.async_process( @@ -1063,12 +1061,15 @@ async def test_light_area_same_name( assert call.data == {"entity_id": [kitchen_light.entity_id]} -async def test_agent_id_validator_invalid_agent(hass: HomeAssistant) -> None: +async def test_agent_id_validator_invalid_agent( + hass: HomeAssistant, init_components +) -> None: """Test validating agent id.""" with pytest.raises(vol.Invalid): conversation.agent_id_validator("invalid_agent") conversation.agent_id_validator(conversation.HOME_ASSISTANT_AGENT) + conversation.agent_id_validator("conversation.home_assistant") async def test_get_agent_list( diff --git a/tests/components/conversation/test_trigger.py b/tests/components/conversation/test_trigger.py index 33ad8efdd2e..9e78b9b6180 100644 --- a/tests/components/conversation/test_trigger.py +++ b/tests/components/conversation/test_trigger.py @@ -5,7 +5,7 @@ import logging import pytest import voluptuous as vol -from homeassistant.components.conversation import agent_manager, default_agent +from homeassistant.components.conversation import default_agent from homeassistant.components.conversation.models import ConversationInput from homeassistant.core import Context, HomeAssistant from homeassistant.helpers import trigger @@ -515,7 +515,7 @@ async def test_trigger_with_device_id(hass: HomeAssistant) -> None: }, ) - agent = await agent_manager.get_agent_manager(hass).async_get_agent() + agent = default_agent.async_get_default_agent(hass) assert isinstance(agent, default_agent.DefaultAgent) result = await agent.async_process( diff --git a/tests/components/google_assistant_sdk/test_init.py b/tests/components/google_assistant_sdk/test_init.py index 7c2fc8291d4..11b3fbaa03f 100644 --- a/tests/components/google_assistant_sdk/test_init.py +++ b/tests/components/google_assistant_sdk/test_init.py @@ -327,6 +327,7 @@ async def test_conversation_agent( """Test GoogleAssistantConversationAgent.""" await setup_integration() + assert await async_setup_component(hass, "homeassistant", {}) assert await async_setup_component(hass, "conversation", {}) entries = hass.config_entries.async_entries(DOMAIN) @@ -334,7 +335,7 @@ async def test_conversation_agent( entry = entries[0] assert entry.state is ConfigEntryState.LOADED - agent = await conversation.get_agent_manager(hass).async_get_agent(entry.entry_id) + agent = conversation.get_agent_manager(hass).async_get_agent(entry.entry_id) assert agent.supported_languages == SUPPORTED_LANGUAGE_CODES text1 = "tell me a joke" @@ -365,6 +366,7 @@ async def test_conversation_agent_refresh_token( """Test GoogleAssistantConversationAgent when token is expired.""" await setup_integration() + assert await async_setup_component(hass, "homeassistant", {}) assert await async_setup_component(hass, "conversation", {}) entries = hass.config_entries.async_entries(DOMAIN) @@ -416,6 +418,7 @@ async def test_conversation_agent_language_changed( """Test GoogleAssistantConversationAgent when language is changed.""" await setup_integration() + assert await async_setup_component(hass, "homeassistant", {}) assert await async_setup_component(hass, "conversation", {}) entries = hass.config_entries.async_entries(DOMAIN) diff --git a/tests/components/google_generative_ai_conversation/conftest.py b/tests/components/google_generative_ai_conversation/conftest.py index 66dfd980cf3..5c979d3bc47 100644 --- a/tests/components/google_generative_ai_conversation/conftest.py +++ b/tests/components/google_generative_ai_conversation/conftest.py @@ -4,6 +4,8 @@ from unittest.mock import patch import pytest +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -23,10 +25,17 @@ def mock_config_entry(hass): @pytest.fixture -async def mock_init_component(hass, mock_config_entry): +async def mock_init_component(hass: HomeAssistant, mock_config_entry: ConfigEntry): """Initialize integration.""" + assert await async_setup_component(hass, "homeassistant", {}) with patch("google.generativeai.get_model"): assert await async_setup_component( hass, "google_generative_ai_conversation", {} ) await hass.async_block_till_done() + + +@pytest.fixture(autouse=True) +async def setup_ha(hass: HomeAssistant) -> None: + """Set up Home Assistant.""" + assert await async_setup_component(hass, "homeassistant", {}) diff --git a/tests/components/google_generative_ai_conversation/test_init.py b/tests/components/google_generative_ai_conversation/test_init.py index 92e84b1fd39..befe3b93d12 100644 --- a/tests/components/google_generative_ai_conversation/test_init.py +++ b/tests/components/google_generative_ai_conversation/test_init.py @@ -10,6 +10,7 @@ from homeassistant.components import conversation from homeassistant.core import Context, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import area_registry as ar, device_registry as dr, intent +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -124,6 +125,7 @@ async def test_template_error( hass: HomeAssistant, mock_config_entry: MockConfigEntry ) -> None: """Test that template error handling works.""" + assert await async_setup_component(hass, "homeassistant", {}) hass.config_entries.async_update_entry( mock_config_entry, options={ @@ -152,7 +154,7 @@ async def test_conversation_agent( mock_init_component, ) -> None: """Test GoogleGenerativeAIAgent.""" - agent = await conversation.get_agent_manager(hass).async_get_agent( + agent = conversation.get_agent_manager(hass).async_get_agent( mock_config_entry.entry_id ) assert agent.supported_languages == "*" diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index 9d941685c09..c67312939b1 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -1033,7 +1033,7 @@ async def test_webhook_handle_conversation_process( webhook_client.server.app.router._frozen = False with patch( - "homeassistant.components.conversation.agent_manager.AgentManager.async_get_agent", + "homeassistant.components.conversation.agent_manager.async_get_agent", return_value=mock_conversation_agent, ): resp = await webhook_client.post( diff --git a/tests/components/ollama/conftest.py b/tests/components/ollama/conftest.py index 78ecf0766d7..db1689bd416 100644 --- a/tests/components/ollama/conftest.py +++ b/tests/components/ollama/conftest.py @@ -35,3 +35,9 @@ async def mock_init_component(hass: HomeAssistant, mock_config_entry: MockConfig ): assert await async_setup_component(hass, ollama.DOMAIN, {}) await hass.async_block_till_done() + + +@pytest.fixture(autouse=True) +async def setup_ha(hass: HomeAssistant) -> None: + """Set up Home Assistant.""" + assert await async_setup_component(hass, "homeassistant", {}) diff --git a/tests/components/ollama/test_init.py b/tests/components/ollama/test_init.py index 6dd9dc73973..5326a8ed609 100644 --- a/tests/components/ollama/test_init.py +++ b/tests/components/ollama/test_init.py @@ -229,7 +229,7 @@ async def test_message_history_pruning( assert isinstance(result.conversation_id, str) conversation_ids.append(result.conversation_id) - agent = await conversation.get_agent_manager(hass).async_get_agent( + agent = conversation.get_agent_manager(hass).async_get_agent( mock_config_entry.entry_id ) assert isinstance(agent, ollama.OllamaAgent) @@ -284,7 +284,7 @@ async def test_message_history_unlimited( result.response.response_type == intent.IntentResponseType.ACTION_DONE ), result - agent = await conversation.get_agent_manager(hass).async_get_agent( + agent = conversation.get_agent_manager(hass).async_get_agent( mock_config_entry.entry_id ) assert isinstance(agent, ollama.OllamaAgent) @@ -340,7 +340,7 @@ async def test_conversation_agent( mock_init_component, ) -> None: """Test OllamaAgent.""" - agent = await conversation.get_agent_manager(hass).async_get_agent( + agent = conversation.get_agent_manager(hass).async_get_agent( mock_config_entry.entry_id ) assert agent.supported_languages == MATCH_ALL diff --git a/tests/components/openai_conversation/conftest.py b/tests/components/openai_conversation/conftest.py index a8081c01c32..1597fa79d0a 100644 --- a/tests/components/openai_conversation/conftest.py +++ b/tests/components/openai_conversation/conftest.py @@ -4,6 +4,7 @@ from unittest.mock import patch import pytest +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -30,3 +31,9 @@ async def mock_init_component(hass, mock_config_entry): ): assert await async_setup_component(hass, "openai_conversation", {}) await hass.async_block_till_done() + + +@pytest.fixture(autouse=True) +async def setup_ha(hass: HomeAssistant) -> None: + """Set up Home Assistant.""" + assert await async_setup_component(hass, "homeassistant", {}) diff --git a/tests/components/openai_conversation/test_init.py b/tests/components/openai_conversation/test_init.py index c94fdcebcde..2702b749a64 100644 --- a/tests/components/openai_conversation/test_init.py +++ b/tests/components/openai_conversation/test_init.py @@ -194,7 +194,7 @@ async def test_conversation_agent( mock_init_component, ) -> None: """Test OpenAIAgent.""" - agent = await conversation.get_agent_manager(hass).async_get_agent( + agent = conversation.get_agent_manager(hass).async_get_agent( mock_config_entry.entry_id ) assert agent.supported_languages == "*" From ce9d4c868337fefb7abe8acc44f0d44794452958 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 1 Apr 2024 15:34:54 -1000 Subject: [PATCH 141/967] Fix flakey cast discovery stop test (#114605) https://github.com/home-assistant/core/actions/runs/8515215623/job/23322322954?pr=114602 --- tests/components/cast/test_media_player.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index d75aebe4ded..72eea4c5ff4 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -453,13 +453,13 @@ async def test_stop_discovery_called_on_stop( """Test pychromecast.stop_discovery called on shutdown.""" # start_discovery should be called with empty config await async_setup_cast(hass, {}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert castbrowser_mock.return_value.start_discovery.call_count == 1 # stop discovery should be called on shutdown hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - await hass.async_block_till_done() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) + await hass.async_block_till_done(wait_background_tasks=True) assert castbrowser_mock.return_value.stop_discovery.call_count == 1 From 0963f5e64223cb67caeb0331ada355e0fcc11595 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 1 Apr 2024 15:35:38 -1000 Subject: [PATCH 142/967] Avoid storing raw extracted traceback in system_log (#114603) This is never actually used and takes up quite a bit of ram --- homeassistant/components/system_log/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index 77f0b095a30..423f5c6f5d8 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -166,7 +166,6 @@ class LogEntry: "level", "message", "exception", - "extracted_tb", "root_cause", "source", "count", @@ -200,7 +199,6 @@ class LogEntry: else: self.source = (record.pathname, record.lineno) self.count = 1 - self.extracted_tb = extracted_tb self.key = (self.name, self.source, self.root_cause) def to_dict(self) -> dict[str, Any]: From b12c69accbe42653e4e00d41e1c026c953b82a93 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 1 Apr 2024 15:36:48 -1000 Subject: [PATCH 143/967] Fix memory leak when importing a platform fails (#114602) * Fix memory leak when importing a platform fails re-raising ImportError would trigger a memory leak * fixes, coverage * Apply suggestions from code review --- homeassistant/loader.py | 31 ++++++------ tests/test_loader.py | 107 +++++++++++++++++++++++++++++++--------- 2 files changed, 98 insertions(+), 40 deletions(-) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 722f3fd83c7..eb70f0b83af 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -750,9 +750,7 @@ class Integration: self._import_futures: dict[str, asyncio.Future[ModuleType]] = {} cache: dict[str, ModuleType | ComponentProtocol] = hass.data[DATA_COMPONENTS] self._cache = cache - missing_platforms_cache: dict[str, ImportError] = hass.data[ - DATA_MISSING_PLATFORMS - ] + missing_platforms_cache: dict[str, bool] = hass.data[DATA_MISSING_PLATFORMS] self._missing_platforms_cache = missing_platforms_cache self._top_level_files = top_level_files or set() _LOGGER.info("Loaded %s from %s", self.domain, pkg_path) @@ -1085,8 +1083,7 @@ class Integration: import_futures: list[tuple[str, asyncio.Future[ModuleType]]] = [] for platform_name in platform_names: - full_name = f"{domain}.{platform_name}" - if platform := self._get_platform_cached_or_raise(full_name): + if platform := self._get_platform_cached_or_raise(platform_name): platforms[platform_name] = platform continue @@ -1095,6 +1092,7 @@ class Integration: in_progress_imports[platform_name] = future continue + full_name = f"{domain}.{platform_name}" if ( self.import_executor and full_name not in self.hass.config.components @@ -1166,14 +1164,18 @@ class Integration: return platforms - def _get_platform_cached_or_raise(self, full_name: str) -> ModuleType | None: + def _get_platform_cached_or_raise(self, platform_name: str) -> ModuleType | None: """Return a platform for an integration from cache.""" + full_name = f"{self.domain}.{platform_name}" if full_name in self._cache: # the cache is either a ModuleType or a ComponentProtocol # but we only care about the ModuleType here return self._cache[full_name] # type: ignore[return-value] if full_name in self._missing_platforms_cache: - raise self._missing_platforms_cache[full_name] + raise ModuleNotFoundError( + f"Platform {full_name} not found", + name=f"{self.pkg_path}.{platform_name}", + ) return None def platforms_are_loaded(self, platform_names: Iterable[str]) -> bool: @@ -1189,9 +1191,7 @@ class Integration: def get_platform(self, platform_name: str) -> ModuleType: """Return a platform for an integration.""" - if platform := self._get_platform_cached_or_raise( - f"{self.domain}.{platform_name}" - ): + if platform := self._get_platform_cached_or_raise(platform_name): return platform return self._load_platform(platform_name) @@ -1212,10 +1212,7 @@ class Integration: ): existing_platforms.append(platform_name) continue - missing_platforms[full_name] = ModuleNotFoundError( - f"Platform {full_name} not found", - name=f"{self.pkg_path}.{platform_name}", - ) + missing_platforms[full_name] = True return existing_platforms @@ -1233,11 +1230,13 @@ class Integration: cache: dict[str, ModuleType] = self.hass.data[DATA_COMPONENTS] try: cache[full_name] = self._import_platform(platform_name) - except ImportError as ex: + except ModuleNotFoundError: if self.domain in cache: # If the domain is loaded, cache that the platform # does not exist so we do not try to load it again - self._missing_platforms_cache[full_name] = ex + self._missing_platforms_cache[full_name] = True + raise + except ImportError: raise except RuntimeError as err: # _DeadlockError inherits from RuntimeError diff --git a/tests/test_loader.py b/tests/test_loader.py index e73029e14e2..41796f2f7d2 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -274,7 +274,61 @@ async def test_get_integration_exceptions(hass: HomeAssistant) -> None: async def test_get_platform_caches_failures_when_component_loaded( hass: HomeAssistant, ) -> None: - """Test get_platform cache failures only when the component is loaded.""" + """Test get_platform caches failures only when the component is loaded. + + Only ModuleNotFoundError is cached, ImportError is not cached. + """ + integration = await loader.async_get_integration(hass, "hue") + + with ( + pytest.raises(ModuleNotFoundError), + patch( + "homeassistant.loader.importlib.import_module", + side_effect=ModuleNotFoundError("Boom"), + ), + ): + assert integration.get_component() == hue + + with ( + pytest.raises(ModuleNotFoundError), + patch( + "homeassistant.loader.importlib.import_module", + side_effect=ModuleNotFoundError("Boom"), + ), + ): + assert integration.get_platform("light") == hue_light + + # Hue is not loaded so we should still hit the import_module path + with ( + pytest.raises(ModuleNotFoundError), + patch( + "homeassistant.loader.importlib.import_module", + side_effect=ModuleNotFoundError("Boom"), + ), + ): + assert integration.get_platform("light") == hue_light + + assert integration.get_component() == hue + + # Hue is loaded so we should cache the import_module failure now + with ( + pytest.raises(ModuleNotFoundError), + patch( + "homeassistant.loader.importlib.import_module", + side_effect=ModuleNotFoundError("Boom"), + ), + ): + assert integration.get_platform("light") == hue_light + + # Hue is loaded and the last call should have cached the import_module failure + with pytest.raises(ModuleNotFoundError): + assert integration.get_platform("light") == hue_light + + +async def test_get_platform_only_cached_module_not_found_when_component_loaded( + hass: HomeAssistant, +) -> None: + """Test get_platform cache only cache module not found when the component is loaded.""" integration = await loader.async_get_integration(hass, "hue") with ( @@ -317,41 +371,43 @@ async def test_get_platform_caches_failures_when_component_loaded( ): assert integration.get_platform("light") == hue_light - # Hue is loaded and the last call should have cached the import_module failure - with pytest.raises(ImportError): - assert integration.get_platform("light") == hue_light + # ImportError is not cached because we only cache ModuleNotFoundError + assert integration.get_platform("light") == hue_light async def test_async_get_platform_caches_failures_when_component_loaded( hass: HomeAssistant, ) -> None: - """Test async_get_platform cache failures only when the component is loaded.""" + """Test async_get_platform caches failures only when the component is loaded. + + Only ModuleNotFoundError is cached, ImportError is not cached. + """ integration = await loader.async_get_integration(hass, "hue") with ( - pytest.raises(ImportError), + pytest.raises(ModuleNotFoundError), patch( "homeassistant.loader.importlib.import_module", - side_effect=ImportError("Boom"), + side_effect=ModuleNotFoundError("Boom"), ), ): assert integration.get_component() == hue with ( - pytest.raises(ImportError), + pytest.raises(ModuleNotFoundError), patch( "homeassistant.loader.importlib.import_module", - side_effect=ImportError("Boom"), + side_effect=ModuleNotFoundError("Boom"), ), ): assert await integration.async_get_platform("light") == hue_light # Hue is not loaded so we should still hit the import_module path with ( - pytest.raises(ImportError), + pytest.raises(ModuleNotFoundError), patch( "homeassistant.loader.importlib.import_module", - side_effect=ImportError("Boom"), + side_effect=ModuleNotFoundError("Boom"), ), ): assert await integration.async_get_platform("light") == hue_light @@ -360,16 +416,16 @@ async def test_async_get_platform_caches_failures_when_component_loaded( # Hue is loaded so we should cache the import_module failure now with ( - pytest.raises(ImportError), + pytest.raises(ModuleNotFoundError), patch( "homeassistant.loader.importlib.import_module", - side_effect=ImportError("Boom"), + side_effect=ModuleNotFoundError("Boom"), ), ): assert await integration.async_get_platform("light") == hue_light # Hue is loaded and the last call should have cached the import_module failure - with pytest.raises(ImportError): + with pytest.raises(ModuleNotFoundError): assert await integration.async_get_platform("light") == hue_light # The cache should never be filled because the import error is remembered @@ -379,33 +435,36 @@ async def test_async_get_platform_caches_failures_when_component_loaded( async def test_async_get_platforms_caches_failures_when_component_loaded( hass: HomeAssistant, ) -> None: - """Test async_get_platforms cache failures only when the component is loaded.""" + """Test async_get_platforms cache failures only when the component is loaded. + + Only ModuleNotFoundError is cached, ImportError is not cached. + """ integration = await loader.async_get_integration(hass, "hue") with ( - pytest.raises(ImportError), + pytest.raises(ModuleNotFoundError), patch( "homeassistant.loader.importlib.import_module", - side_effect=ImportError("Boom"), + side_effect=ModuleNotFoundError("Boom"), ), ): assert integration.get_component() == hue with ( - pytest.raises(ImportError), + pytest.raises(ModuleNotFoundError), patch( "homeassistant.loader.importlib.import_module", - side_effect=ImportError("Boom"), + side_effect=ModuleNotFoundError("Boom"), ), ): assert await integration.async_get_platforms(["light"]) == {"light": hue_light} # Hue is not loaded so we should still hit the import_module path with ( - pytest.raises(ImportError), + pytest.raises(ModuleNotFoundError), patch( "homeassistant.loader.importlib.import_module", - side_effect=ImportError("Boom"), + side_effect=ModuleNotFoundError("Boom"), ), ): assert await integration.async_get_platforms(["light"]) == {"light": hue_light} @@ -414,16 +473,16 @@ async def test_async_get_platforms_caches_failures_when_component_loaded( # Hue is loaded so we should cache the import_module failure now with ( - pytest.raises(ImportError), + pytest.raises(ModuleNotFoundError), patch( "homeassistant.loader.importlib.import_module", - side_effect=ImportError("Boom"), + side_effect=ModuleNotFoundError("Boom"), ), ): assert await integration.async_get_platforms(["light"]) == {"light": hue_light} # Hue is loaded and the last call should have cached the import_module failure - with pytest.raises(ImportError): + with pytest.raises(ModuleNotFoundError): assert await integration.async_get_platforms(["light"]) == {"light": hue_light} # The cache should never be filled because the import error is remembered From 5856bbc07b4cbd1f7a3fb5f588fca9b0fb1a5769 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 1 Apr 2024 15:37:30 -1000 Subject: [PATCH 144/967] Add missing platforms_exist guard to check_config (#114600) * Add missing platforms_exist guard to check_config related issue #112811 When the exception hits, the config will end up being saved in the traceback so the memory is never released. This matches the check_config code to homeassistant.config to avoid having the exception thrown. * patch * merge branch --- homeassistant/helpers/check_config.py | 19 ++++++++++--------- tests/helpers/test_check_config.py | 2 ++ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/homeassistant/helpers/check_config.py b/homeassistant/helpers/check_config.py index 8537f442595..78dddb12381 100644 --- a/homeassistant/helpers/check_config.py +++ b/homeassistant/helpers/check_config.py @@ -198,15 +198,16 @@ async def async_check_ha_config_file( # noqa: C901 # Check if the integration has a custom config validator config_validator = None - try: - config_validator = await integration.async_get_platform("config") - except ImportError as err: - # Filter out import error of the config platform. - # If the config platform contains bad imports, make sure - # that still fails. - if err.name != f"{integration.pkg_path}.config": - result.add_error(f"Error importing config platform {domain}: {err}") - continue + if integration.platforms_exists(("config",)): + try: + config_validator = await integration.async_get_platform("config") + except ImportError as err: + # Filter out import error of the config platform. + # If the config platform contains bad imports, make sure + # that still fails. + if err.name != f"{integration.pkg_path}.config": + result.add_error(f"Error importing config platform {domain}: {err}") + continue if config_validator is not None and hasattr( config_validator, "async_validate_config" diff --git a/tests/helpers/test_check_config.py b/tests/helpers/test_check_config.py index fd94c453e51..de7edf42dc2 100644 --- a/tests/helpers/test_check_config.py +++ b/tests/helpers/test_check_config.py @@ -350,6 +350,7 @@ async def test_config_platform_import_error(hass: HomeAssistant) -> None: side_effect=ImportError("blablabla"), ), patch("os.path.isfile", return_value=True), + patch("homeassistant.loader.Integration.platforms_exists", return_value=True), patch_yaml_files(files), ): res = await async_check_ha_config_file(hass) @@ -373,6 +374,7 @@ async def test_platform_import_error(hass: HomeAssistant) -> None: "homeassistant.loader.Integration.async_get_platform", side_effect=[None, ImportError("blablabla")], ), + patch("homeassistant.loader.Integration.platforms_exists", return_value=True), patch("os.path.isfile", return_value=True), patch_yaml_files(files), ): From 3513bd0cc51688d029e240db854f07c97dd961f2 Mon Sep 17 00:00:00 2001 From: mkmer Date: Mon, 1 Apr 2024 21:47:30 -0400 Subject: [PATCH 145/967] Bump whirlpool-sixth-sense to 0.18.7 (#114606) Bump sixth-sense to 0.18.7 --- homeassistant/components/whirlpool/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/whirlpool/manifest.json b/homeassistant/components/whirlpool/manifest.json index 0c46580ceeb..ee7861588ed 100644 --- a/homeassistant/components/whirlpool/manifest.json +++ b/homeassistant/components/whirlpool/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "cloud_push", "loggers": ["whirlpool"], - "requirements": ["whirlpool-sixth-sense==0.18.6"] + "requirements": ["whirlpool-sixth-sense==0.18.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index cacbe912fd8..3f15b0726a3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2854,7 +2854,7 @@ webmin-xmlrpc==0.0.2 webrtc-noise-gain==1.2.3 # homeassistant.components.whirlpool -whirlpool-sixth-sense==0.18.6 +whirlpool-sixth-sense==0.18.7 # homeassistant.components.whois whois==0.9.27 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a093b9f0160..2a55d30abab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2201,7 +2201,7 @@ webmin-xmlrpc==0.0.2 webrtc-noise-gain==1.2.3 # homeassistant.components.whirlpool -whirlpool-sixth-sense==0.18.6 +whirlpool-sixth-sense==0.18.7 # homeassistant.components.whois whois==0.9.27 From ab2c88353b82f149b215495ca88caa7f868638a7 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 2 Apr 2024 03:48:26 +0200 Subject: [PATCH 146/967] Filter out ignored entries in ssdp step of AVM Fritz!SmartHome (#114574) filter out ignored entries in ssdp step --- homeassistant/components/fritzbox/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fritzbox/config_flow.py b/homeassistant/components/fritzbox/config_flow.py index fe4cf82b29b..c89415fa7ee 100644 --- a/homeassistant/components/fritzbox/config_flow.py +++ b/homeassistant/components/fritzbox/config_flow.py @@ -141,7 +141,7 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_abort(reason="already_in_progress") # update old and user-configured config entries - for entry in self._async_current_entries(): + for entry in self._async_current_entries(include_ignore=False): if entry.data[CONF_HOST] == host: if uuid and not entry.unique_id: self.hass.config_entries.async_update_entry(entry, unique_id=uuid) From c2ffed9b2defdc9861b9f0e51987ae7aadd12659 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Tue, 2 Apr 2024 08:25:28 +0200 Subject: [PATCH 147/967] Use switch entities instead of toggle entities in tests (#114585) --- tests/components/conftest.py | 8 ++--- .../generic_hygrostat/test_humidifier.py | 8 ++--- .../generic_thermostat/test_climate.py | 8 ++--- tests/components/switch/common.py | 35 ++++++++++++++----- tests/components/switch/test_init.py | 8 ++--- 5 files changed, 43 insertions(+), 24 deletions(-) diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 958c7fe3c86..bde8cad5ea4 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -9,13 +9,13 @@ import pytest from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant -from tests.common import MockToggleEntity from tests.components.conversation import MockAgent if TYPE_CHECKING: from tests.components.device_tracker.common import MockScanner from tests.components.light.common import MockLight from tests.components.sensor.common import MockSensor + from tests.components.switch.common import MockSwitch @pytest.fixture(scope="session", autouse=True) @@ -145,11 +145,11 @@ def mock_sensor_entities() -> dict[str, "MockSensor"]: @pytest.fixture -def mock_toggle_entities() -> list[MockToggleEntity]: +def mock_switch_entities() -> list["MockSwitch"]: """Return mocked toggle entities.""" - from tests.components.switch.common import get_mock_toggle_entities + from tests.components.switch.common import get_mock_switch_entities - return get_mock_toggle_entities() + return get_mock_switch_entities() @pytest.fixture diff --git a/tests/components/generic_hygrostat/test_humidifier.py b/tests/components/generic_hygrostat/test_humidifier.py index 528418b9974..ef7a2c90aa9 100644 --- a/tests/components/generic_hygrostat/test_humidifier.py +++ b/tests/components/generic_hygrostat/test_humidifier.py @@ -37,12 +37,12 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.common import ( - MockToggleEntity, assert_setup_component, async_fire_time_changed, mock_restore_cache, setup_test_component_platform, ) +from tests.components.switch.common import MockSwitch ENTITY = "humidifier.test" ENT_SENSOR = "sensor.test" @@ -129,11 +129,11 @@ async def test_humidifier_input_boolean(hass: HomeAssistant, setup_comp_1) -> No async def test_humidifier_switch( - hass: HomeAssistant, setup_comp_1, mock_toggle_entities: list[MockToggleEntity] + hass: HomeAssistant, setup_comp_1, mock_switch_entities: list[MockSwitch] ) -> None: """Test humidifier switching test switch.""" - setup_test_component_platform(hass, switch.DOMAIN, mock_toggle_entities) - switch_1 = mock_toggle_entities[1] + setup_test_component_platform(hass, switch.DOMAIN, mock_switch_entities) + switch_1 = mock_switch_entities[1] assert await async_setup_component( hass, switch.DOMAIN, {"switch": {"platform": "test"}} ) diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py index f6424f894cf..8903f4b6606 100644 --- a/tests/components/generic_thermostat/test_climate.py +++ b/tests/components/generic_thermostat/test_climate.py @@ -50,7 +50,6 @@ from homeassistant.util import dt as dt_util from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM from tests.common import ( - MockToggleEntity, assert_setup_component, async_fire_time_changed, async_mock_service, @@ -59,6 +58,7 @@ from tests.common import ( setup_test_component_platform, ) from tests.components.climate import common +from tests.components.switch.common import MockSwitch ENTITY = "climate.test" ENT_SENSOR = "sensor.test" @@ -142,11 +142,11 @@ async def test_heater_input_boolean(hass: HomeAssistant, setup_comp_1) -> None: async def test_heater_switch( - hass: HomeAssistant, setup_comp_1, mock_toggle_entities: list[MockToggleEntity] + hass: HomeAssistant, setup_comp_1, mock_switch_entities: list[MockSwitch] ) -> None: """Test heater switching test switch.""" - setup_test_component_platform(hass, switch.DOMAIN, mock_toggle_entities) - switch_1 = mock_toggle_entities[1] + setup_test_component_platform(hass, switch.DOMAIN, mock_switch_entities) + switch_1 = mock_switch_entities[1] assert await async_setup_component( hass, switch.DOMAIN, {"switch": {"platform": "test"}} ) diff --git a/tests/components/switch/common.py b/tests/components/switch/common.py index cb30efe47d0..e9764d59d7c 100644 --- a/tests/components/switch/common.py +++ b/tests/components/switch/common.py @@ -4,7 +4,9 @@ All containing methods are legacy helpers that should not be used by new components. Instead call the service directly. """ -from homeassistant.components.switch import DOMAIN +from typing import Any + +from homeassistant.components.switch import DOMAIN, SwitchDeviceClass, SwitchEntity from homeassistant.const import ( ATTR_ENTITY_ID, ENTITY_MATCH_ALL, @@ -15,8 +17,6 @@ from homeassistant.const import ( ) from homeassistant.loader import bind_hass -from tests.common import MockToggleEntity - @bind_hass def turn_on(hass, entity_id=ENTITY_MATCH_ALL): @@ -42,10 +42,29 @@ async def async_turn_off(hass, entity_id=ENTITY_MATCH_ALL): await hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data, blocking=True) -def get_mock_toggle_entities() -> list[MockToggleEntity]: - """Return a list of mock toggle entities.""" +class MockSwitch(SwitchEntity): + """Mocked switch entity.""" + + _attr_device_class = SwitchDeviceClass.SWITCH + + def __init__(self, name: str | None, state: str) -> None: + """Initialize the mock switch entity.""" + self._attr_name = name + self._attr_is_on = state == STATE_ON + + def turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + self._attr_is_on = True + + def turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + self._attr_is_on = False + + +def get_mock_switch_entities() -> list[MockSwitch]: + """Return a list of mock switch entities.""" return [ - MockToggleEntity("AC", STATE_ON), - MockToggleEntity("AC", STATE_OFF), - MockToggleEntity(None, STATE_OFF), + MockSwitch("AC", STATE_ON), + MockSwitch("AC", STATE_OFF), + MockSwitch(None, STATE_OFF), ] diff --git a/tests/components/switch/test_init.py b/tests/components/switch/test_init.py index 28e9d273570..aa3e4ccce58 100644 --- a/tests/components/switch/test_init.py +++ b/tests/components/switch/test_init.py @@ -9,9 +9,9 @@ from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from . import common +from .common import MockSwitch from tests.common import ( - MockToggleEntity, MockUser, help_test_all, import_and_test_deprecated_constant_enum, @@ -20,10 +20,10 @@ from tests.common import ( @pytest.fixture(autouse=True) -def entities(hass: HomeAssistant, mock_toggle_entities: list[MockToggleEntity]): +def entities(hass: HomeAssistant, mock_switch_entities: list[MockSwitch]): """Initialize the test switch.""" - setup_test_component_platform(hass, switch.DOMAIN, mock_toggle_entities) - return mock_toggle_entities + setup_test_component_platform(hass, switch.DOMAIN, mock_switch_entities) + return mock_switch_entities async def test_methods( From 09fbd8bb52441028b5e24463d3164e7417d893e0 Mon Sep 17 00:00:00 2001 From: max2697 <143563471+max2697@users.noreply.github.com> Date: Tue, 2 Apr 2024 02:04:28 -0500 Subject: [PATCH 148/967] Bump opower to 0.4.2 (#114608) --- 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 bc6f8796d50..879aeb0327b 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.1"] + "requirements": ["opower==0.4.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3f15b0726a3..a203e4663e4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1486,7 +1486,7 @@ openwrt-luci-rpc==1.1.17 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.4.1 +opower==0.4.2 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2a55d30abab..0d8e0cea3b3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1180,7 +1180,7 @@ openerz-api==0.3.0 openhomedevice==2.2.0 # homeassistant.components.opower -opower==0.4.1 +opower==0.4.2 # homeassistant.components.oralb oralb-ble==0.17.6 From 667e119d32265ec61442880d215d8b33a7da4935 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Apr 2024 09:07:47 +0200 Subject: [PATCH 149/967] Bump Wandalen/wretry.action from 2.1.0 to 3.0.0 (#114554) 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 ab81b8f356d..66965bf5363 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1070,7 +1070,7 @@ jobs: pattern: coverage-* - name: Upload coverage to Codecov (full coverage) if: needs.info.outputs.test_full_suite == 'true' - uses: Wandalen/wretry.action@v2.1.0 + uses: Wandalen/wretry.action@v3.0.0 with: action: codecov/codecov-action@v3.1.3 with: | @@ -1081,7 +1081,7 @@ jobs: attempt_delay: 30000 - name: Upload coverage to Codecov (partial coverage) if: needs.info.outputs.test_full_suite == 'false' - uses: Wandalen/wretry.action@v2.1.0 + uses: Wandalen/wretry.action@v3.0.0 with: action: codecov/codecov-action@v3.1.3 with: | From 0030c97f59afadf8e32bae68602224d7903429fd Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 2 Apr 2024 09:19:25 +0200 Subject: [PATCH 150/967] Tweak integration sensor (#114384) * Tweak integration sensor * Improve tests --- .../components/integration/sensor.py | 68 +++++++++++-------- tests/components/integration/test_sensor.py | 30 ++++++-- 2 files changed, 65 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index ef587e405e6..ed017a21527 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -30,7 +30,6 @@ from homeassistant.const import ( ) from homeassistant.core import Event, HomeAssistant, State, callback from homeassistant.helpers import ( - condition, config_validation as cv, device_registry as dr, entity_registry as er, @@ -97,57 +96,72 @@ class _IntegrationMethod(ABC): return _NAME_TO_INTEGRATION_METHOD[method_name]() @abstractmethod - def validate_states(self, left: State, right: State) -> bool: + def validate_states( + self, left: State, right: State + ) -> tuple[Decimal, Decimal] | None: """Check state requirements for integration.""" @abstractmethod def calculate_area_with_two_states( - self, elapsed_time: float, left: State, right: State + self, elapsed_time: Decimal, left: Decimal, right: Decimal ) -> Decimal: """Calculate area given two states.""" def calculate_area_with_one_state( - self, elapsed_time: float, constant_state: State + self, elapsed_time: Decimal, constant_state: Decimal ) -> Decimal: - return Decimal(constant_state.state) * Decimal(elapsed_time) + return constant_state * elapsed_time class _Trapezoidal(_IntegrationMethod): def calculate_area_with_two_states( - self, elapsed_time: float, left: State, right: State + self, elapsed_time: Decimal, left: Decimal, right: Decimal ) -> Decimal: - return Decimal(elapsed_time) * (Decimal(left.state) + Decimal(right.state)) / 2 + return elapsed_time * (left + right) / 2 - def validate_states(self, left: State, right: State) -> bool: - return _is_numeric_state(left) and _is_numeric_state(right) + 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) + ) is None: + return None + return (left_dec, right_dec) class _Left(_IntegrationMethod): def calculate_area_with_two_states( - self, elapsed_time: float, left: State, right: State + self, elapsed_time: Decimal, left: Decimal, right: Decimal ) -> Decimal: return self.calculate_area_with_one_state(elapsed_time, left) - def validate_states(self, left: State, right: State) -> bool: - return _is_numeric_state(left) + def validate_states( + self, left: State, right: State + ) -> tuple[Decimal, Decimal] | None: + if (left_dec := _decimal_state(left.state)) is None: + return None + return (left_dec, left_dec) class _Right(_IntegrationMethod): def calculate_area_with_two_states( - self, elapsed_time: float, left: State, right: State + self, elapsed_time: Decimal, left: Decimal, right: Decimal ) -> Decimal: return self.calculate_area_with_one_state(elapsed_time, right) - def validate_states(self, left: State, right: State) -> bool: - return _is_numeric_state(right) + def validate_states( + self, left: State, right: State + ) -> tuple[Decimal, Decimal] | None: + if (right_dec := _decimal_state(right.state)) is None: + return None + return (right_dec, right_dec) -def _is_numeric_state(state: State) -> bool: +def _decimal_state(state: str) -> Decimal | None: try: - float(state.state) - except (ValueError, TypeError): - return False - return True + return Decimal(state) + except (InvalidOperation, TypeError): + return None _NAME_TO_INTEGRATION_METHOD: dict[str, type[_IntegrationMethod]] = { @@ -413,7 +427,7 @@ class IntegrationSensor(RestoreSensor): if old_state is None or new_state is None: return - if condition.state(self.hass, new_state, [STATE_UNAVAILABLE]): + if new_state.state == STATE_UNAVAILABLE: self._attr_available = False self.async_write_ha_state() return @@ -421,18 +435,16 @@ class IntegrationSensor(RestoreSensor): self._attr_available = True self._derive_and_set_attributes_from_state(new_state) - if not self._method.validate_states(old_state, new_state): + if not (states := self._method.validate_states(old_state, new_state)): self.async_write_ha_state() return - elapsed_seconds = ( - new_state.last_updated - old_state.last_updated - ).total_seconds() - - area = self._method.calculate_area_with_two_states( - elapsed_seconds, old_state, new_state + elapsed_seconds = Decimal( + (new_state.last_updated - old_state.last_updated).total_seconds() ) + area = self._method.calculate_area_with_two_states(elapsed_seconds, *states) + self._update_integral(area) self.async_write_ha_state() diff --git a/tests/components/integration/test_sensor.py b/tests/components/integration/test_sensor.py index 53763247bdf..555cb44caf5 100644 --- a/tests/components/integration/test_sensor.py +++ b/tests/components/integration/test_sensor.py @@ -629,8 +629,17 @@ async def test_device_class(hass: HomeAssistant, method) -> None: assert state.attributes.get("device_class") == SensorDeviceClass.ENERGY -@pytest.mark.parametrize("method", ["trapezoidal", "left", "right"]) -async def test_calc_errors(hass: HomeAssistant, method) -> None: +@pytest.mark.parametrize( + ("method", "expected_states"), + [ + ("trapezoidal", [STATE_UNKNOWN, "0.500", "0.500"]), + ("left", [STATE_UNKNOWN, "0.000", "1.000"]), + ("right", ["0.000", "1.000", "1.000"]), + ], +) +async def test_calc_errors( + hass: HomeAssistant, method: str, expected_states: list[str] +) -> None: """Test integration sensor units using a power source.""" config = { "sensor": { @@ -649,9 +658,9 @@ async def test_calc_errors(hass: HomeAssistant, method) -> None: hass.states.async_set(entity_id, None, {}) await hass.async_block_till_done() - state = hass.states.get("sensor.integration") # With the source sensor in a None state, the Reimann sensor should be # unknown + state = hass.states.get("sensor.integration") assert state is not None assert state.state == STATE_UNKNOWN @@ -665,7 +674,7 @@ async def test_calc_errors(hass: HomeAssistant, method) -> None: state = hass.states.get("sensor.integration") assert state is not None - assert state.state == STATE_UNKNOWN if method != "right" else "0.000" + assert state.state == expected_states[0] # With the source sensor updated successfully, the Reimann sensor # should have a zero (known) value. @@ -677,7 +686,18 @@ async def test_calc_errors(hass: HomeAssistant, method) -> None: state = hass.states.get("sensor.integration") assert state is not None - assert round(float(state.state)) == 0 if method != "right" else 1 + assert state.state == expected_states[1] + + # Set the source sensor back to a non numeric state + now += timedelta(seconds=3600) + with freeze_time(now): + hass.states.async_set(entity_id, "unexpected", {"device_class": None}) + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get("sensor.integration") + assert state is not None + assert state.state == expected_states[2] async def test_device_id( From 078535e1d6448d1578ebb637d8cfbc0b4711f398 Mon Sep 17 00:00:00 2001 From: mkmer Date: Tue, 2 Apr 2024 03:41:40 -0400 Subject: [PATCH 151/967] Add diagnostic platform to Whirlpool (#114578) * Add diagnostic platform and tests * lowercase variable * Correc doc string --- .../components/whirlpool/diagnostics.py | 49 +++++++++++++++++++ .../whirlpool/snapshots/test_diagnostics.ambr | 44 +++++++++++++++++ .../components/whirlpool/test_diagnostics.py | 32 ++++++++++++ 3 files changed, 125 insertions(+) create mode 100644 homeassistant/components/whirlpool/diagnostics.py create mode 100644 tests/components/whirlpool/snapshots/test_diagnostics.ambr create mode 100644 tests/components/whirlpool/test_diagnostics.py diff --git a/homeassistant/components/whirlpool/diagnostics.py b/homeassistant/components/whirlpool/diagnostics.py new file mode 100644 index 00000000000..9b1dd00e7bd --- /dev/null +++ b/homeassistant/components/whirlpool/diagnostics.py @@ -0,0 +1,49 @@ +"""Diagnostics support for Whirlpool.""" + +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 . import WhirlpoolData +from .const import DOMAIN + +TO_REDACT = { + "SERIAL_NUMBER", + "macaddress", + "username", + "password", + "token", + "unique_id", + "SAID", +} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, + config_entry: ConfigEntry, +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + + whirlpool: WhirlpoolData = hass.data[DOMAIN][config_entry.entry_id] + diagnostics_data = { + "Washer_dryers": { + wd["NAME"]: dict(wd.items()) + for wd in whirlpool.appliances_manager.washer_dryers + }, + "aircons": { + ac["NAME"]: dict(ac.items()) for ac in whirlpool.appliances_manager.aircons + }, + "ovens": { + oven["NAME"]: dict(oven.items()) + for oven in whirlpool.appliances_manager.ovens + }, + } + + return { + "config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT), + "appliances": async_redact_data(diagnostics_data, TO_REDACT), + } diff --git a/tests/components/whirlpool/snapshots/test_diagnostics.ambr b/tests/components/whirlpool/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..5a0beb112e6 --- /dev/null +++ b/tests/components/whirlpool/snapshots/test_diagnostics.ambr @@ -0,0 +1,44 @@ +# serializer version: 1 +# name: test_entry_diagnostics + dict({ + 'appliances': dict({ + 'Washer_dryers': dict({ + 'dryer': dict({ + 'NAME': 'dryer', + 'SAID': '**REDACTED**', + }), + 'washer': dict({ + 'NAME': 'washer', + 'SAID': '**REDACTED**', + }), + }), + 'aircons': dict({ + 'TestZone': dict({ + 'NAME': 'TestZone', + 'SAID': '**REDACTED**', + }), + }), + 'ovens': dict({ + }), + }), + 'config_entry': dict({ + 'data': dict({ + 'brand': 'Whirlpool', + 'password': '**REDACTED**', + 'region': 'EU', + 'username': '**REDACTED**', + }), + 'disabled_by': None, + 'domain': 'whirlpool', + 'minor_version': 1, + 'options': dict({ + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': 'Mock Title', + 'unique_id': None, + 'version': 1, + }), + }) +# --- diff --git a/tests/components/whirlpool/test_diagnostics.py b/tests/components/whirlpool/test_diagnostics.py new file mode 100644 index 00000000000..6cfc1b76e38 --- /dev/null +++ b/tests/components/whirlpool/test_diagnostics.py @@ -0,0 +1,32 @@ +"""Test Blink diagnostics.""" + +from unittest.mock import MagicMock + +from syrupy import SnapshotAssertion +from syrupy.filters import props + +from homeassistant.core import HomeAssistant + +from . import init_integration + +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_appliances_manager_api: MagicMock, + mock_aircon1_api: MagicMock, + mock_aircon_api_instances: MagicMock, +) -> None: + """Test config entry diagnostics.""" + + mock_entry = await init_integration(hass) + + result = await get_diagnostics_for_config_entry(hass, hass_client, mock_entry) + + assert result == snapshot(exclude=props("entry_id")) From 31cd41adb8a68017dc9c2c26d2b15d937a3da736 Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Tue, 2 Apr 2024 04:11:45 -0400 Subject: [PATCH 152/967] Display sonos album title with URL encoding (#113693) * unescape the title When extracting the title from the item_id, it needs to be unescaped. * sort imports --- .../components/sonos/media_browser.py | 2 +- tests/components/sonos/test_media_browser.py | 96 +++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 tests/components/sonos/test_media_browser.py diff --git a/homeassistant/components/sonos/media_browser.py b/homeassistant/components/sonos/media_browser.py index bd57e57e468..967e81061ed 100644 --- a/homeassistant/components/sonos/media_browser.py +++ b/homeassistant/components/sonos/media_browser.py @@ -205,7 +205,7 @@ def build_item_response( if not title: try: - title = payload["idstring"].split("/")[1] + title = urllib.parse.unquote(payload["idstring"].split("/")[1]) except IndexError: title = LIBRARY_TITLES_MAPPING[payload["idstring"]] diff --git a/tests/components/sonos/test_media_browser.py b/tests/components/sonos/test_media_browser.py new file mode 100644 index 00000000000..cb6303c800d --- /dev/null +++ b/tests/components/sonos/test_media_browser.py @@ -0,0 +1,96 @@ +"""Tests for the Sonos Media Browser.""" + +from functools import partial + +from homeassistant.components.media_player.browse_media import BrowseMedia +from homeassistant.components.media_player.const import MediaClass, MediaType +from homeassistant.components.sonos.media_browser import ( + build_item_response, + get_thumbnail_url_full, +) +from homeassistant.core import HomeAssistant + +from .conftest import SoCoMockFactory + + +class MockMusicServiceItem: + """Mocks a Soco MusicServiceItem.""" + + def __init__( + self, + title: str, + item_id: str, + parent_id: str, + item_class: str, + ) -> None: + """Initialize the mock item.""" + self.title = title + self.item_id = item_id + self.item_class = item_class + self.parent_id = parent_id + + def get_uri(self) -> str: + """Return URI.""" + return self.item_id.replace("S://", "x-file-cifs://") + + +def mock_browse_by_idstring( + search_type: str, idstring: str, start=0, max_items=100, full_album_art_uri=False +) -> list[MockMusicServiceItem]: + """Mock the call to browse_by_id_string.""" + if search_type == "albums" and ( + idstring == "A:ALBUM/Abbey%20Road" or idstring == "A:ALBUM/Abbey Road" + ): + return [ + MockMusicServiceItem( + "Come Together", + "S://192.168.42.10/music/The%20Beatles/Abbey%20Road/01%20Come%20Together.mp3", + "A:ALBUM/Abbey%20Road", + "object.item.audioItem.musicTrack", + ), + MockMusicServiceItem( + "Something", + "S://192.168.42.10/music/The%20Beatles/Abbey%20Road/03%20Something.mp3", + "A:ALBUM/Abbey%20Road", + "object.item.audioItem.musicTrack", + ), + ] + return None + + +async def test_build_item_response( + hass: HomeAssistant, + soco_factory: SoCoMockFactory, + async_autosetup_sonos, + soco, + discover, +) -> None: + """Test building a browse item response.""" + soco_mock = soco_factory.mock_list.get("192.168.42.2") + soco_mock.music_library.browse_by_idstring = mock_browse_by_idstring + browse_item: BrowseMedia = build_item_response( + soco_mock.music_library, + {"search_type": MediaType.ALBUM, "idstring": "A:ALBUM/Abbey%20Road"}, + partial( + get_thumbnail_url_full, + soco_mock.music_library, + True, + None, + ), + ) + assert browse_item.title == "Abbey Road" + assert browse_item.media_class == MediaClass.ALBUM + assert browse_item.media_content_id == "A:ALBUM/Abbey%20Road" + assert len(browse_item.children) == 2 + assert browse_item.children[0].media_class == MediaClass.TRACK + assert browse_item.children[0].title == "Come Together" + assert ( + browse_item.children[0].media_content_id + == "x-file-cifs://192.168.42.10/music/The%20Beatles/Abbey%20Road/01%20Come%20Together.mp3" + ) + assert browse_item.children[1].media_class == MediaClass.TRACK + assert browse_item.children[1].title == "Something" + assert ( + browse_item.children[1].media_content_id + == "x-file-cifs://192.168.42.10/music/The%20Beatles/Abbey%20Road/03%20Something.mp3" + ) From c8f282c8bc8f63bf4b5d3a2adb2dd1c43c86016b Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 2 Apr 2024 10:15:58 +0200 Subject: [PATCH 153/967] Improve Shelly RPC device update progress (#114566) Co-authored-by: Shay Levy Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/shelly/update.py | 16 ++++++++++------ tests/components/shelly/test_update.py | 19 +++++++++++-------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/shelly/update.py b/homeassistant/components/shelly/update.py index f6a89c5381b..56ad1f2ef67 100644 --- a/homeassistant/components/shelly/update.py +++ b/homeassistant/components/shelly/update.py @@ -222,7 +222,7 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity): ) -> None: """Initialize update entity.""" super().__init__(coordinator, key, attribute, description) - self._ota_in_progress: bool = False + self._ota_in_progress: bool | int = False self._attr_release_url = get_release_url( coordinator.device.gen, coordinator.model, description.beta ) @@ -237,14 +237,13 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity): @callback def _ota_progress_callback(self, event: dict[str, Any]) -> None: """Handle device OTA progress.""" - if self._ota_in_progress: + if self.in_progress is not False: event_type = event["event"] if event_type == OTA_BEGIN: - self._attr_in_progress = 0 + self._ota_in_progress = 0 elif event_type == OTA_PROGRESS: - self._attr_in_progress = event["progress_percent"] + self._ota_in_progress = event["progress_percent"] elif event_type in (OTA_ERROR, OTA_SUCCESS): - self._attr_in_progress = False self._ota_in_progress = False self.async_write_ha_state() @@ -262,6 +261,11 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity): return self.installed_version + @property + def in_progress(self) -> bool | int: + """Update installation in progress.""" + return self._ota_in_progress + async def async_install( self, version: str | None, backup: bool, **kwargs: Any ) -> None: @@ -292,7 +296,7 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity): await self.coordinator.async_shutdown_device_and_start_reauth() else: self._ota_in_progress = True - LOGGER.debug("OTA update call successful") + LOGGER.info("OTA update call for %s successful", self.coordinator.name) class RpcSleepingUpdateEntity( diff --git a/tests/components/shelly/test_update.py b/tests/components/shelly/test_update.py index 387dc93e33e..f3960620a21 100644 --- a/tests/components/shelly/test_update.py +++ b/tests/components/shelly/test_update.py @@ -255,6 +255,16 @@ async def test_rpc_update( {ATTR_ENTITY_ID: entity_id}, blocking=True, ) + + assert mock_rpc_device.trigger_ota_update.call_count == 1 + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + assert state.attributes[ATTR_INSTALLED_VERSION] == "1" + assert state.attributes[ATTR_LATEST_VERSION] == "2" + assert state.attributes[ATTR_IN_PROGRESS] is True + assert state.attributes[ATTR_RELEASE_URL] == GEN2_RELEASE_URL + inject_rpc_device_event( monkeypatch, mock_rpc_device, @@ -270,14 +280,7 @@ async def test_rpc_update( }, ) - assert mock_rpc_device.trigger_ota_update.call_count == 1 - - state = hass.states.get(entity_id) - assert state.state == STATE_ON - assert state.attributes[ATTR_INSTALLED_VERSION] == "1" - assert state.attributes[ATTR_LATEST_VERSION] == "2" - assert state.attributes[ATTR_IN_PROGRESS] == 0 - assert state.attributes[ATTR_RELEASE_URL] == GEN2_RELEASE_URL + assert hass.states.get(entity_id).attributes[ATTR_IN_PROGRESS] == 0 inject_rpc_device_event( monkeypatch, From 67c334f8424978419ff5c6cfc76929e622a38859 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Tue, 2 Apr 2024 10:30:01 +0200 Subject: [PATCH 154/967] Fix ruff issue in sonos (#114616) --- tests/components/sonos/test_media_browser.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/components/sonos/test_media_browser.py b/tests/components/sonos/test_media_browser.py index cb6303c800d..d8d0e1c3a07 100644 --- a/tests/components/sonos/test_media_browser.py +++ b/tests/components/sonos/test_media_browser.py @@ -38,8 +38,9 @@ def mock_browse_by_idstring( search_type: str, idstring: str, start=0, max_items=100, full_album_art_uri=False ) -> list[MockMusicServiceItem]: """Mock the call to browse_by_id_string.""" - if search_type == "albums" and ( - idstring == "A:ALBUM/Abbey%20Road" or idstring == "A:ALBUM/Abbey Road" + if search_type == "albums" and idstring in ( + "A:ALBUM/Abbey%20Road", + "A:ALBUM/Abbey Road", ): return [ MockMusicServiceItem( From 4a93b4a4b4adecbf586cedacaab76fef8f2510d0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 2 Apr 2024 10:43:14 +0200 Subject: [PATCH 155/967] Add floor selector (#114614) --- homeassistant/helpers/selector.py | 42 +++++++++++++++++++ tests/helpers/test_selector.py | 67 +++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index 938cc6a9246..c4db601fac6 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -844,6 +844,48 @@ class EntitySelector(Selector[EntitySelectorConfig]): return cast(list, vol.Schema([validate])(data)) # Output is a list +class FloorSelectorConfig(TypedDict, total=False): + """Class to represent an floor selector config.""" + + entity: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig] + device: DeviceFilterSelectorConfig | list[DeviceFilterSelectorConfig] + multiple: bool + + +@SELECTORS.register("floor") +class FloorSelector(Selector[AreaSelectorConfig]): + """Selector of a single or list of floors.""" + + selector_type = "floor" + + CONFIG_SCHEMA = vol.Schema( + { + vol.Optional("entity"): vol.All( + cv.ensure_list, + [ENTITY_FILTER_SELECTOR_CONFIG_SCHEMA], + ), + vol.Optional("device"): vol.All( + cv.ensure_list, + [DEVICE_FILTER_SELECTOR_CONFIG_SCHEMA], + ), + vol.Optional("multiple", default=False): cv.boolean, + } + ) + + def __init__(self, config: FloorSelectorConfig | None = None) -> None: + """Instantiate a selector.""" + super().__init__(config) + + def __call__(self, data: Any) -> str | list[str]: + """Validate the passed selection.""" + if not self.config["multiple"]: + floor_id: str = vol.Schema(str)(data) + return floor_id + if not isinstance(data, list): + raise vol.Invalid("Value should be a list") + return [vol.Schema(str)(val) for val in data] + + class IconSelectorConfig(TypedDict, total=False): """Class to represent an icon selector config.""" diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index 0dc7e570fc5..8864edc7386 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -1158,3 +1158,70 @@ def test_qr_code_selector_schema(schema, valid_selections, invalid_selections) - def test_label_selector_schema(schema, valid_selections, invalid_selections) -> None: """Test label selector.""" _test_selector("label", schema, valid_selections, invalid_selections) + + +@pytest.mark.parametrize( + ("schema", "valid_selections", "invalid_selections"), + [ + ({}, ("abc123",), (None,)), + ({"entity": {}}, ("abc123",), (None,)), + ({"entity": {"domain": "light"}}, ("abc123",), (None,)), + ( + {"entity": {"domain": "binary_sensor", "device_class": "motion"}}, + ("abc123",), + (None,), + ), + ( + { + "entity": { + "domain": "binary_sensor", + "device_class": "motion", + "integration": "demo", + } + }, + ("abc123",), + (None,), + ), + ( + { + "entity": [ + {"domain": "light"}, + {"domain": "binary_sensor", "device_class": "motion"}, + ] + }, + ("abc123",), + (None,), + ), + ( + {"device": {"integration": "demo", "model": "mock-model"}}, + ("abc123",), + (None,), + ), + ( + { + "device": [ + {"integration": "demo", "model": "mock-model"}, + {"integration": "other-demo", "model": "other-mock-model"}, + ] + }, + ("abc123",), + (None,), + ), + ( + { + "entity": {"domain": "binary_sensor", "device_class": "motion"}, + "device": {"integration": "demo", "model": "mock-model"}, + }, + ("abc123",), + (None,), + ), + ( + {"multiple": True}, + ((["abc123", "def456"],)), + (None, "abc123", ["abc123", None]), + ), + ], +) +def test_floor_selector_schema(schema, valid_selections, invalid_selections) -> None: + """Test floor selector.""" + _test_selector("floor", schema, valid_selections, invalid_selections) From 385da75963fe5bda5ef361abd0eb8e1e0a7f173c Mon Sep 17 00:00:00 2001 From: Jack Boswell Date: Tue, 2 Apr 2024 21:45:46 +1300 Subject: [PATCH 156/967] Catch potential ValueError when getting or setting Starlink sleep values (#114607) --- homeassistant/components/starlink/time.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/starlink/time.py b/homeassistant/components/starlink/time.py index 4d9e2d06675..6475610564d 100644 --- a/homeassistant/components/starlink/time.py +++ b/homeassistant/components/starlink/time.py @@ -10,6 +10,7 @@ import math from homeassistant.components.time import TimeEntity, TimeEntityDescription 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 @@ -62,14 +63,22 @@ class StarlinkTimeEntity(StarlinkEntity, TimeEntity): def _utc_minutes_to_time(utc_minutes: int, timezone: tzinfo) -> time: hour = math.floor(utc_minutes / 60) minute = utc_minutes % 60 - utc = datetime.now(UTC).replace(hour=hour, minute=minute, second=0, microsecond=0) + try: + utc = datetime.now(UTC).replace( + hour=hour, minute=minute, second=0, microsecond=0 + ) + except ValueError as exc: + raise HomeAssistantError from exc return utc.astimezone(timezone).time() def _time_to_utc_minutes(t: time, timezone: tzinfo) -> int: - zoned_time = datetime.now(timezone).replace( - hour=t.hour, minute=t.minute, second=0, microsecond=0 - ) + try: + zoned_time = datetime.now(timezone).replace( + hour=t.hour, minute=t.minute, second=0, microsecond=0 + ) + except ValueError as exc: + raise HomeAssistantError from exc utc_time = zoned_time.astimezone(UTC).time() return (utc_time.hour * 60) + utc_time.minute From 51e716bef35e120e998d90092242f3099e0e6506 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Tue, 2 Apr 2024 09:47:30 +0100 Subject: [PATCH 157/967] Update ring quality scale to silver (#113146) --- homeassistant/components/ring/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index 764557a3a1d..e682dc053b3 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -13,5 +13,6 @@ "documentation": "https://www.home-assistant.io/integrations/ring", "iot_class": "cloud_polling", "loggers": ["ring_doorbell"], + "quality_scale": "silver", "requirements": ["ring-doorbell[listen]==0.8.8"] } From 21c7cc3250e6610244d12eae0ca0f8726e7b031b Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 2 Apr 2024 11:15:52 +0200 Subject: [PATCH 158/967] Bump roombapy to 1.8.1 (#114478) * Bump roombapy to 1.7.0 * Bump * Bump * Fix --- homeassistant/components/roomba/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/roomba/test_config_flow.py | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/roomba/manifest.json b/homeassistant/components/roomba/manifest.json index ae08d8f6a1f..a697680b379 100644 --- a/homeassistant/components/roomba/manifest.json +++ b/homeassistant/components/roomba/manifest.json @@ -24,7 +24,7 @@ "documentation": "https://www.home-assistant.io/integrations/roomba", "iot_class": "local_push", "loggers": ["paho_mqtt", "roombapy"], - "requirements": ["roombapy==1.6.13"], + "requirements": ["roombapy==1.8.1"], "zeroconf": [ { "type": "_amzn-alexa._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index a203e4663e4..dfcff9119b6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2469,7 +2469,7 @@ rokuecp==0.19.2 romy==0.0.9 # homeassistant.components.roomba -roombapy==1.6.13 +roombapy==1.8.1 # homeassistant.components.roon roonapi==0.1.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0d8e0cea3b3..794a4c40db6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1903,7 +1903,7 @@ rokuecp==0.19.2 romy==0.0.9 # homeassistant.components.roomba -roombapy==1.6.13 +roombapy==1.8.1 # homeassistant.components.roon roonapi==0.1.6 diff --git a/tests/components/roomba/test_config_flow.py b/tests/components/roomba/test_config_flow.py index 2eaf3b14e38..282884c0be3 100644 --- a/tests/components/roomba/test_config_flow.py +++ b/tests/components/roomba/test_config_flow.py @@ -99,12 +99,12 @@ def _mocked_discovery(*_): roomba = RoombaInfo( hostname="irobot-BLID", - robotname="robot_name", + robot_name="robot_name", ip=MOCK_IP, mac="mac", - sw="firmware", + firmware="firmware", sku="sku", - cap={"cap": 1}, + capabilities={"cap": 1}, ) roomba_discovery.get_all = MagicMock(return_value=[roomba]) From e473914407e7dd3fbb2e109838c631edf477135b Mon Sep 17 00:00:00 2001 From: Fexiven <48439988+Fexiven@users.noreply.github.com> Date: Tue, 2 Apr 2024 12:04:07 +0200 Subject: [PATCH 159/967] Fix Starlink integration startup issue (#114615) --- homeassistant/components/starlink/coordinator.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/starlink/coordinator.py b/homeassistant/components/starlink/coordinator.py index 9c597fbb033..ff33b3ecc41 100644 --- a/homeassistant/components/starlink/coordinator.py +++ b/homeassistant/components/starlink/coordinator.py @@ -58,14 +58,14 @@ class StarlinkUpdateCoordinator(DataUpdateCoordinator[StarlinkData]): async def _async_update_data(self) -> StarlinkData: async with asyncio.timeout(4): try: - status, location, sleep = await asyncio.gather( - self.hass.async_add_executor_job(status_data, self.channel_context), - self.hass.async_add_executor_job( - location_data, self.channel_context - ), - self.hass.async_add_executor_job( - get_sleep_config, self.channel_context - ), + status = await self.hass.async_add_executor_job( + status_data, self.channel_context + ) + location = await self.hass.async_add_executor_job( + location_data, self.channel_context + ) + sleep = await self.hass.async_add_executor_job( + get_sleep_config, self.channel_context ) return StarlinkData(location, sleep, *status) except GrpcError as exc: From a1ae4ec23d1f5c73eb09c72a867f3dbd6eb9ba42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 2 Apr 2024 12:11:28 +0200 Subject: [PATCH 160/967] Add sensor entities to Traccar Server (#111374) --- .coveragerc | 1 + .../components/traccar_server/__init__.py | 2 +- .../traccar_server/device_tracker.py | 14 -- .../components/traccar_server/diagnostics.py | 21 ++- .../components/traccar_server/icons.json | 15 ++ .../components/traccar_server/sensor.py | 125 +++++++++++++++ .../components/traccar_server/strings.json | 13 ++ .../traccar_server/fixtures/devices.json | 2 +- .../traccar_server/fixtures/positions.json | 3 +- .../traccar_server/fixtures/server.json | 4 +- .../snapshots/test_diagnostics.ambr | 149 ++++++++++++++++-- .../traccar_server/test_diagnostics.py | 10 ++ 12 files changed, 318 insertions(+), 41 deletions(-) create mode 100644 homeassistant/components/traccar_server/icons.json create mode 100644 homeassistant/components/traccar_server/sensor.py diff --git a/.coveragerc b/.coveragerc index e32db823542..54cc470ac63 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1482,6 +1482,7 @@ omit = homeassistant/components/traccar_server/device_tracker.py 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 diff --git a/homeassistant/components/traccar_server/__init__.py b/homeassistant/components/traccar_server/__init__.py index fc513136681..703df6cbfa4 100644 --- a/homeassistant/components/traccar_server/__init__.py +++ b/homeassistant/components/traccar_server/__init__.py @@ -30,7 +30,7 @@ from .const import ( ) from .coordinator import TraccarServerCoordinator -PLATFORMS: list[Platform] = [Platform.DEVICE_TRACKER] +PLATFORMS: list[Platform] = [Platform.DEVICE_TRACKER, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/traccar_server/device_tracker.py b/homeassistant/components/traccar_server/device_tracker.py index e459cdacf14..d15ba084dad 100644 --- a/homeassistant/components/traccar_server/device_tracker.py +++ b/homeassistant/components/traccar_server/device_tracker.py @@ -10,12 +10,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( - ATTR_ADDRESS, - ATTR_ALTITUDE, ATTR_CATEGORY, - ATTR_GEOFENCE, ATTR_MOTION, - ATTR_SPEED, ATTR_STATUS, ATTR_TRACCAR_ID, ATTR_TRACKER, @@ -44,23 +40,13 @@ class TraccarServerDeviceTracker(TraccarServerEntity, TrackerEntity): _attr_has_entity_name = True _attr_name = None - @property - def battery_level(self) -> int: - """Return battery value of the device.""" - return self.traccar_position["attributes"].get("batteryLevel", -1) - @property def extra_state_attributes(self) -> dict[str, Any]: """Return device specific attributes.""" - geofence_name = self.traccar_geofence["name"] if self.traccar_geofence else None return { **self.traccar_attributes, - ATTR_ADDRESS: self.traccar_position["address"], - ATTR_ALTITUDE: self.traccar_position["altitude"], ATTR_CATEGORY: self.traccar_device["category"], - ATTR_GEOFENCE: geofence_name, ATTR_MOTION: self.traccar_position["attributes"].get("motion", False), - ATTR_SPEED: self.traccar_position["speed"], ATTR_STATUS: self.traccar_device["status"], ATTR_TRACCAR_ID: self.traccar_device["id"], ATTR_TRACKER: DOMAIN, diff --git a/homeassistant/components/traccar_server/diagnostics.py b/homeassistant/components/traccar_server/diagnostics.py index ea861a9bffa..80dc7a9c7cd 100644 --- a/homeassistant/components/traccar_server/diagnostics.py +++ b/homeassistant/components/traccar_server/diagnostics.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import Any -from homeassistant.components.diagnostics import async_redact_data +from homeassistant.components.diagnostics import REDACTED, async_redact_data from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ADDRESS, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant @@ -13,21 +13,23 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from .const import DOMAIN from .coordinator import TraccarServerCoordinator -TO_REDACT = { +KEYS_TO_REDACT = { + "area", # This is the polygon area of a geofence CONF_ADDRESS, CONF_LATITUDE, CONF_LONGITUDE, - "area", # This is the polygon area of a geofence } def _entity_state( hass: HomeAssistant, entity: er.RegistryEntry, + coordinator: TraccarServerCoordinator, ) -> dict[str, Any] | None: + states_to_redact = {x["position"]["address"] for x in coordinator.data.values()} return ( { - "state": state.state, + "state": state.state if state.state not in states_to_redact else REDACTED, "attributes": state.attributes, } if (state := hass.states.get(entity.entity_id)) @@ -57,12 +59,13 @@ async def async_get_config_entry_diagnostics( { "enity_id": entity.entity_id, "disabled": entity.disabled, - "state": _entity_state(hass, entity), + "unit_of_measurement": entity.unit_of_measurement, + "state": _entity_state(hass, entity, coordinator), } for entity in entities ], }, - TO_REDACT, + KEYS_TO_REDACT, ) @@ -81,6 +84,7 @@ async def async_get_device_diagnostics( include_disabled_entities=True, ) + await hass.config_entries.async_reload(entry.entry_id) return async_redact_data( { "subscription_status": coordinator.client.subscription_status, @@ -90,10 +94,11 @@ async def async_get_device_diagnostics( { "enity_id": entity.entity_id, "disabled": entity.disabled, - "state": _entity_state(hass, entity), + "unit_of_measurement": entity.unit_of_measurement, + "state": _entity_state(hass, entity, coordinator), } for entity in entities ], }, - TO_REDACT, + KEYS_TO_REDACT, ) diff --git a/homeassistant/components/traccar_server/icons.json b/homeassistant/components/traccar_server/icons.json new file mode 100644 index 00000000000..59fc663e712 --- /dev/null +++ b/homeassistant/components/traccar_server/icons.json @@ -0,0 +1,15 @@ +{ + "entity": { + "sensor": { + "altitude": { + "default": "mdi:altimeter" + }, + "address": { + "default": "mdi:map-marker" + }, + "geofence": { + "default": "mdi:map-marker" + } + } + } +} diff --git a/homeassistant/components/traccar_server/sensor.py b/homeassistant/components/traccar_server/sensor.py new file mode 100644 index 00000000000..7f46399eb3f --- /dev/null +++ b/homeassistant/components/traccar_server/sensor.py @@ -0,0 +1,125 @@ +"""Support for Traccar server sensors.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Generic, Literal, TypeVar, cast + +from pytraccar import DeviceModel, GeofenceModel, PositionModel + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfLength, UnitOfSpeed +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 TraccarServerCoordinator +from .entity import TraccarServerEntity + +_T = TypeVar("_T") + + +@dataclass(frozen=True, kw_only=True) +class TraccarServerSensorEntityDescription(Generic[_T], SensorEntityDescription): + """Describe Traccar Server sensor entity.""" + + data_key: Literal["position", "device", "geofence", "attributes"] + entity_registry_enabled_default = False + entity_category = EntityCategory.DIAGNOSTIC + value_fn: Callable[[_T], StateType] + + +TRACCAR_SERVER_SENSOR_ENTITY_DESCRIPTIONS = ( + TraccarServerSensorEntityDescription[PositionModel]( + key="attributes.batteryLevel", + data_key="position", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=0, + value_fn=lambda x: x["attributes"].get("batteryLevel", -1), + ), + TraccarServerSensorEntityDescription[PositionModel]( + key="speed", + data_key="position", + device_class=SensorDeviceClass.SPEED, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfSpeed.KNOTS, + suggested_display_precision=0, + value_fn=lambda x: x["speed"], + ), + TraccarServerSensorEntityDescription[PositionModel]( + key="altitude", + data_key="position", + translation_key="altitude", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfLength.METERS, + suggested_display_precision=1, + value_fn=lambda x: x["altitude"], + ), + TraccarServerSensorEntityDescription[PositionModel]( + key="address", + data_key="position", + translation_key="address", + value_fn=lambda x: x["address"], + ), + TraccarServerSensorEntityDescription[GeofenceModel | None]( + key="name", + data_key="geofence", + translation_key="geofence", + value_fn=lambda x: x["name"] if x else None, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up sensor entities.""" + coordinator: TraccarServerCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + TraccarServerSensor( + coordinator=coordinator, + device=entry["device"], + description=cast(TraccarServerSensorEntityDescription, description), + ) + for entry in coordinator.data.values() + for description in TRACCAR_SERVER_SENSOR_ENTITY_DESCRIPTIONS + ) + + +class TraccarServerSensor(TraccarServerEntity, SensorEntity): + """Represent a tracked device.""" + + _attr_has_entity_name = True + entity_description: TraccarServerSensorEntityDescription + + def __init__( + self, + coordinator: TraccarServerCoordinator, + device: DeviceModel, + description: TraccarServerSensorEntityDescription[_T], + ) -> None: + """Initialize the Traccar Server sensor.""" + super().__init__(coordinator, device) + self.entity_description = description + self._attr_unique_id = ( + f"{device['uniqueId']}_{description.data_key}_{description.key}" + ) + + @property + def native_value(self) -> StateType: + """Return the value of the sensor.""" + return self.entity_description.value_fn( + getattr(self, f"traccar_{self.entity_description.data_key}") + ) diff --git a/homeassistant/components/traccar_server/strings.json b/homeassistant/components/traccar_server/strings.json index 87da7e8cdd1..41adaace77e 100644 --- a/homeassistant/components/traccar_server/strings.json +++ b/homeassistant/components/traccar_server/strings.json @@ -41,5 +41,18 @@ } } } + }, + "entity": { + "sensor": { + "address": { + "name": "Address" + }, + "altitude": { + "name": "Altitude" + }, + "geofence": { + "name": "Geofence" + } + } } } diff --git a/tests/components/traccar_server/fixtures/devices.json b/tests/components/traccar_server/fixtures/devices.json index b04d53d9fdf..f3db1322e0b 100644 --- a/tests/components/traccar_server/fixtures/devices.json +++ b/tests/components/traccar_server/fixtures/devices.json @@ -3,7 +3,7 @@ "id": 0, "name": "X-Wing", "uniqueId": "abc123", - "status": "unknown", + "status": "online", "disabled": false, "lastUpdate": "1970-01-01T00:00:00Z", "positionId": 0, diff --git a/tests/components/traccar_server/fixtures/positions.json b/tests/components/traccar_server/fixtures/positions.json index 6b65116e804..7f661a7092a 100644 --- a/tests/components/traccar_server/fixtures/positions.json +++ b/tests/components/traccar_server/fixtures/positions.json @@ -18,7 +18,8 @@ "network": {}, "geofenceIds": [0], "attributes": { - "custom_attr_1": "custom_attr_1_value" + "custom_attr_1": "custom_attr_1_value", + "batteryLevel": 15.00000867601 } } ] diff --git a/tests/components/traccar_server/fixtures/server.json b/tests/components/traccar_server/fixtures/server.json index 039b6bfa1f4..7de1152b63d 100644 --- a/tests/components/traccar_server/fixtures/server.json +++ b/tests/components/traccar_server/fixtures/server.json @@ -17,5 +17,7 @@ "coordinateFormat": null, "openIdEnabled": true, "openIdForce": true, - "attributes": {} + "attributes": { + "speedUnit": "kn" + } } diff --git a/tests/components/traccar_server/snapshots/test_diagnostics.ambr b/tests/components/traccar_server/snapshots/test_diagnostics.ambr index f8fe3cc60f7..300444f10f1 100644 --- a/tests/components/traccar_server/snapshots/test_diagnostics.ambr +++ b/tests/components/traccar_server/snapshots/test_diagnostics.ambr @@ -30,7 +30,7 @@ 'name': 'X-Wing', 'phone': None, 'positionId': 0, - 'status': 'unknown', + 'status': 'online', 'uniqueId': 'abc123', }), 'geofence': dict({ @@ -47,6 +47,7 @@ 'address': '**REDACTED**', 'altitude': 546841384638, 'attributes': dict({ + 'batteryLevel': 15.00000867601, 'custom_attr_1': 'custom_attr_1_value', }), 'course': 360, @@ -75,25 +76,84 @@ 'enity_id': 'device_tracker.x_wing', 'state': dict({ 'attributes': dict({ - 'address': '**REDACTED**', - 'altitude': 546841384638, - 'battery_level': -1, 'category': 'starfighter', 'custom_attr_1': 'custom_attr_1_value', 'friendly_name': 'X-Wing', - 'geofence': 'Tatooine', 'gps_accuracy': 3.5, 'latitude': '**REDACTED**', 'longitude': '**REDACTED**', 'motion': False, 'source_type': 'gps', - 'speed': 4568795, - 'status': 'unknown', + 'status': 'online', 'traccar_id': 0, 'tracker': 'traccar_server', }), 'state': 'not_home', }), + 'unit_of_measurement': None, + }), + dict({ + 'disabled': False, + 'enity_id': 'sensor.x_wing_battery', + 'state': dict({ + 'attributes': dict({ + 'device_class': 'battery', + 'friendly_name': 'X-Wing Battery', + 'state_class': 'measurement', + 'unit_of_measurement': '%', + }), + 'state': '15.00000867601', + }), + 'unit_of_measurement': '%', + }), + dict({ + 'disabled': False, + 'enity_id': 'sensor.x_wing_speed', + 'state': dict({ + 'attributes': dict({ + 'device_class': 'speed', + 'friendly_name': 'X-Wing Speed', + 'state_class': 'measurement', + 'unit_of_measurement': 'kn', + }), + 'state': '4568795', + }), + 'unit_of_measurement': 'kn', + }), + dict({ + 'disabled': False, + 'enity_id': 'sensor.x_wing_altitude', + 'state': dict({ + 'attributes': dict({ + 'friendly_name': 'X-Wing Altitude', + 'state_class': 'measurement', + 'unit_of_measurement': 'm', + }), + 'state': '546841384638', + }), + 'unit_of_measurement': 'm', + }), + dict({ + 'disabled': False, + 'enity_id': 'sensor.x_wing_address', + 'state': dict({ + 'attributes': dict({ + 'friendly_name': 'X-Wing Address', + }), + 'state': '**REDACTED**', + }), + 'unit_of_measurement': None, + }), + dict({ + 'disabled': False, + 'enity_id': 'sensor.x_wing_geofence', + 'state': dict({ + 'attributes': dict({ + 'friendly_name': 'X-Wing Geofence', + }), + 'state': 'Tatooine', + }), + 'unit_of_measurement': None, }), ]), 'subscription_status': 'disconnected', @@ -130,7 +190,7 @@ 'name': 'X-Wing', 'phone': None, 'positionId': 0, - 'status': 'unknown', + 'status': 'online', 'uniqueId': 'abc123', }), 'geofence': dict({ @@ -147,6 +207,7 @@ 'address': '**REDACTED**', 'altitude': 546841384638, 'attributes': dict({ + 'batteryLevel': 15.00000867601, 'custom_attr_1': 'custom_attr_1_value', }), 'course': 360, @@ -170,10 +231,41 @@ }), }), 'entities': list([ + dict({ + 'disabled': True, + 'enity_id': 'sensor.x_wing_battery', + 'state': None, + 'unit_of_measurement': '%', + }), + dict({ + 'disabled': True, + 'enity_id': 'sensor.x_wing_speed', + 'state': None, + 'unit_of_measurement': 'kn', + }), + dict({ + 'disabled': True, + 'enity_id': 'sensor.x_wing_altitude', + 'state': None, + 'unit_of_measurement': 'm', + }), + dict({ + 'disabled': True, + 'enity_id': 'sensor.x_wing_address', + 'state': None, + 'unit_of_measurement': None, + }), + dict({ + 'disabled': True, + 'enity_id': 'sensor.x_wing_geofence', + 'state': None, + 'unit_of_measurement': None, + }), dict({ 'disabled': True, 'enity_id': 'device_tracker.x_wing', 'state': None, + 'unit_of_measurement': None, }), ]), 'subscription_status': 'disconnected', @@ -210,7 +302,7 @@ 'name': 'X-Wing', 'phone': None, 'positionId': 0, - 'status': 'unknown', + 'status': 'online', 'uniqueId': 'abc123', }), 'geofence': dict({ @@ -227,6 +319,7 @@ 'address': '**REDACTED**', 'altitude': 546841384638, 'attributes': dict({ + 'batteryLevel': 15.00000867601, 'custom_attr_1': 'custom_attr_1_value', }), 'course': 360, @@ -250,30 +343,56 @@ }), }), 'entities': list([ + dict({ + 'disabled': True, + 'enity_id': 'sensor.x_wing_battery', + 'state': None, + 'unit_of_measurement': '%', + }), + dict({ + 'disabled': True, + 'enity_id': 'sensor.x_wing_speed', + 'state': None, + 'unit_of_measurement': 'kn', + }), + dict({ + 'disabled': True, + 'enity_id': 'sensor.x_wing_altitude', + 'state': None, + 'unit_of_measurement': 'm', + }), + dict({ + 'disabled': True, + 'enity_id': 'sensor.x_wing_address', + 'state': None, + 'unit_of_measurement': None, + }), + dict({ + 'disabled': True, + 'enity_id': 'sensor.x_wing_geofence', + 'state': None, + 'unit_of_measurement': None, + }), dict({ 'disabled': False, 'enity_id': 'device_tracker.x_wing', 'state': dict({ 'attributes': dict({ - 'address': '**REDACTED**', - 'altitude': 546841384638, - 'battery_level': -1, 'category': 'starfighter', 'custom_attr_1': 'custom_attr_1_value', 'friendly_name': 'X-Wing', - 'geofence': 'Tatooine', 'gps_accuracy': 3.5, 'latitude': '**REDACTED**', 'longitude': '**REDACTED**', 'motion': False, 'source_type': 'gps', - 'speed': 4568795, - 'status': 'unknown', + 'status': 'online', 'traccar_id': 0, 'tracker': 'traccar_server', }), 'state': 'not_home', }), + 'unit_of_measurement': None, }), ]), 'subscription_status': 'disconnected', diff --git a/tests/components/traccar_server/test_diagnostics.py b/tests/components/traccar_server/test_diagnostics.py index faf1b628fcd..493f0ae92d1 100644 --- a/tests/components/traccar_server/test_diagnostics.py +++ b/tests/components/traccar_server/test_diagnostics.py @@ -44,6 +44,7 @@ async def test_device_diagnostics( mock_config_entry: MockConfigEntry, snapshot: SnapshotAssertion, device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, ) -> None: """Test device diagnostics.""" await setup_integration(hass, mock_config_entry) @@ -58,6 +59,15 @@ async def test_device_diagnostics( for device in dr.async_entries_for_config_entry( device_registry, mock_config_entry.entry_id ): + entities = er.async_entries_for_device( + entity_registry, + device_id=device.id, + include_disabled_entities=True, + ) + # Enable all entitits to show everything in snapshots + for entity in entities: + entity_registry.async_update_entity(entity.entity_id, disabled_by=None) + result = await get_diagnostics_for_device( hass, hass_client, mock_config_entry, device=device ) From 31b0b823dff3da968392383f25960102767dd442 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 2 Apr 2024 12:17:52 +0200 Subject: [PATCH 161/967] Update frontend to 20240402.0 (#114627) --- 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 7864801a986..5eaa6e94769 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==20240329.1"] + "requirements": ["home-assistant-frontend==20240402.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0b961013b93..fdb08832a82 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -30,7 +30,7 @@ habluetooth==2.4.2 hass-nabucasa==0.79.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240329.1 +home-assistant-frontend==20240402.0 home-assistant-intents==2024.3.29 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index dfcff9119b6..41a3006141a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1081,7 +1081,7 @@ hole==0.8.0 holidays==0.45 # homeassistant.components.frontend -home-assistant-frontend==20240329.1 +home-assistant-frontend==20240402.0 # homeassistant.components.conversation home-assistant-intents==2024.3.29 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 794a4c40db6..0e4bb39fddb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -880,7 +880,7 @@ hole==0.8.0 holidays==0.45 # homeassistant.components.frontend -home-assistant-frontend==20240329.1 +home-assistant-frontend==20240402.0 # homeassistant.components.conversation home-assistant-intents==2024.3.29 From acf2f855fe353f7c9b107de7084c097ff8fdf907 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 2 Apr 2024 12:22:00 +0200 Subject: [PATCH 162/967] Raise ServiceValidationError on number out of range exception (#114589) --- homeassistant/components/number/__init__.py | 18 +++++++++++++----- homeassistant/components/number/const.py | 1 + homeassistant/components/number/strings.json | 5 +++++ tests/components/deconz/test_number.py | 3 ++- tests/components/demo/test_number.py | 3 ++- tests/components/flux_led/test_number.py | 10 +++++----- tests/components/knx/test_number.py | 5 +++-- tests/components/number/test_init.py | 11 +++++++++-- .../rituals_perfume_genie/test_number.py | 5 +++-- 9 files changed, 43 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index 8c55bbc2cba..d3785e0eae6 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -15,7 +15,7 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_MODE, CONF_UNIT_OF_MEASUREMENT, UnitOfTemperature from homeassistant.core import HomeAssistant, ServiceCall, async_get_hass, callback -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers.config_validation import ( PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, @@ -30,6 +30,7 @@ from .const import ( # noqa: F401 ATTR_MAX, ATTR_MIN, ATTR_STEP, + ATTR_STEP_VALIDATION, ATTR_VALUE, DEFAULT_MAX_VALUE, DEFAULT_MIN_VALUE, @@ -99,10 +100,17 @@ async def async_set_value(entity: NumberEntity, service_call: ServiceCall) -> No """Service call wrapper to set a new value.""" value = service_call.data["value"] if value < entity.min_value or value > entity.max_value: - raise ValueError( - f"Value {value} for {entity.entity_id} is outside valid range" - f" {entity.min_value} - {entity.max_value}" + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="out_of_range", + translation_placeholders={ + "value": value, + "entity_id": entity.entity_id, + "min_value": str(entity.min_value), + "max_value": str(entity.max_value), + }, ) + try: native_value = entity.convert_to_native_value(value) # Clamp to the native range @@ -174,7 +182,7 @@ class NumberEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): """Representation of a Number entity.""" _entity_component_unrecorded_attributes = frozenset( - {ATTR_MIN, ATTR_MAX, ATTR_STEP, ATTR_MODE} + {ATTR_MIN, ATTR_MAX, ATTR_STEP, ATTR_STEP_VALIDATION, ATTR_MODE} ) entity_description: NumberEntityDescription diff --git a/homeassistant/components/number/const.py b/homeassistant/components/number/const.py index 89829adcc50..f279ffb72a8 100644 --- a/homeassistant/components/number/const.py +++ b/homeassistant/components/number/const.py @@ -54,6 +54,7 @@ ATTR_VALUE = "value" ATTR_MIN = "min" ATTR_MAX = "max" ATTR_STEP = "step" +ATTR_STEP_VALIDATION = "step_validation" DEFAULT_MIN_VALUE = 0.0 DEFAULT_MAX_VALUE = 100.0 diff --git a/homeassistant/components/number/strings.json b/homeassistant/components/number/strings.json index ffddc0c2b3c..502b2b4affd 100644 --- a/homeassistant/components/number/strings.json +++ b/homeassistant/components/number/strings.json @@ -161,6 +161,11 @@ "name": "[%key:component::sensor::entity_component::wind_speed::name%]" } }, + "exceptions": { + "out_of_range": { + "message": "Value {value} for {entity_id} is outside valid range {min_value} - {max_value}." + } + }, "services": { "set_value": { "name": "Set", diff --git a/tests/components/deconz/test_number.py b/tests/components/deconz/test_number.py index 3f86182e032..655ae2f42e2 100644 --- a/tests/components/deconz/test_number.py +++ b/tests/components/deconz/test_number.py @@ -11,6 +11,7 @@ from homeassistant.components.number import ( ) from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, EntityCategory from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import device_registry as dr, entity_registry as er from .test_gateway import ( @@ -186,7 +187,7 @@ async def test_number_entities( # Service set value beyond the supported range - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, diff --git a/tests/components/demo/test_number.py b/tests/components/demo/test_number.py index 3c41b98a3fa..20e3ce8fc11 100644 --- a/tests/components/demo/test_number.py +++ b/tests/components/demo/test_number.py @@ -16,6 +16,7 @@ from homeassistant.components.number import ( ) from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE, Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError from homeassistant.setup import async_setup_component ENTITY_VOLUME = "number.volume" @@ -97,7 +98,7 @@ async def test_set_value_bad_range(hass: HomeAssistant) -> None: state = hass.states.get(ENTITY_VOLUME) assert state.state == "42.0" - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( DOMAIN, SERVICE_SET_VALUE, diff --git a/tests/components/flux_led/test_number.py b/tests/components/flux_led/test_number.py index 455bad05029..2ed0d34989f 100644 --- a/tests/components/flux_led/test_number.py +++ b/tests/components/flux_led/test_number.py @@ -21,7 +21,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -292,7 +292,7 @@ async def test_addressable_light_pixel_config(hass: HomeAssistant) -> None: assert state.state == "4" await hass.async_block_till_done(wait_background_tasks=True) - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, @@ -314,7 +314,7 @@ async def test_addressable_light_pixel_config(hass: HomeAssistant) -> None: bulb.async_set_device_config.assert_called_with(pixels_per_segment=100) bulb.async_set_device_config.reset_mock() - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, @@ -335,7 +335,7 @@ async def test_addressable_light_pixel_config(hass: HomeAssistant) -> None: bulb.async_set_device_config.assert_called_with(music_pixels_per_segment=100) bulb.async_set_device_config.reset_mock() - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, @@ -356,7 +356,7 @@ async def test_addressable_light_pixel_config(hass: HomeAssistant) -> None: bulb.async_set_device_config.assert_called_with(segments=5) bulb.async_set_device_config.reset_mock() - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, diff --git a/tests/components/knx/test_number.py b/tests/components/knx/test_number.py index be3fe070c10..5eec4530d4e 100644 --- a/tests/components/knx/test_number.py +++ b/tests/components/knx/test_number.py @@ -6,6 +6,7 @@ from homeassistant.components.knx.const import CONF_RESPOND_TO_READ, KNX_ADDRESS from homeassistant.components.knx.schema import NumberSchema from homeassistant.const import CONF_NAME, CONF_TYPE from homeassistant.core import HomeAssistant, State +from homeassistant.exceptions import ServiceValidationError from .conftest import KNXTestKit @@ -37,14 +38,14 @@ async def test_number_set_value(hass: HomeAssistant, knx: KNXTestKit) -> None: assert state.attributes.get("unit_of_measurement") == "%" # set value out of range - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( "number", "set_value", {"entity_id": "number.test", "value": 101.0}, blocking=True, ) - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( "number", "set_value", diff --git a/tests/components/number/test_init.py b/tests/components/number/test_init.py index 07d2baf4926..96ad4b4d2d4 100644 --- a/tests/components/number/test_init.py +++ b/tests/components/number/test_init.py @@ -36,6 +36,7 @@ from homeassistant.const import ( UnitOfVolumeFlowRate, ) from homeassistant.core import HomeAssistant, State +from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import STORAGE_KEY as RESTORE_STATE_KEY @@ -359,14 +360,20 @@ async def test_set_value( state = hass.states.get("number.test") assert state.state == "60.0" - # test ValueError trigger - with pytest.raises(ValueError): + # test range validation + with pytest.raises(ServiceValidationError) as exc: await hass.services.async_call( DOMAIN, SERVICE_SET_VALUE, {ATTR_VALUE: 110.0, ATTR_ENTITY_ID: "number.test"}, blocking=True, ) + assert exc.value.translation_domain == DOMAIN + assert exc.value.translation_key == "out_of_range" + assert ( + str(exc.value) + == "Value 110.0 for number.test is outside valid range 0.0 - 100.0" + ) await hass.async_block_till_done() state = hass.states.get("number.test") diff --git a/tests/components/rituals_perfume_genie/test_number.py b/tests/components/rituals_perfume_genie/test_number.py index f88bcc6d0cb..ddca70649b5 100644 --- a/tests/components/rituals_perfume_genie/test_number.py +++ b/tests/components/rituals_perfume_genie/test_number.py @@ -14,6 +14,7 @@ from homeassistant.components.number import ( ) from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -86,7 +87,7 @@ async def test_set_number_value_out_of_range(hass: HomeAssistant) -> None: assert state assert state.state == "2" - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, @@ -105,7 +106,7 @@ async def test_set_number_value_out_of_range(hass: HomeAssistant) -> None: assert state assert state.state == "2" - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, From f36c37a3b7ad788d8f90891c6d57dda5b61a3271 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Tue, 2 Apr 2024 20:23:08 +1000 Subject: [PATCH 163/967] Fix battery heater in Tessie (#114568) --- homeassistant/components/tessie/binary_sensor.py | 2 +- homeassistant/components/tessie/strings.json | 2 +- tests/components/tessie/snapshots/test_binary_sensors.ambr | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tessie/binary_sensor.py b/homeassistant/components/tessie/binary_sensor.py index 015fa63736f..9b7d6861dfb 100644 --- a/homeassistant/components/tessie/binary_sensor.py +++ b/homeassistant/components/tessie/binary_sensor.py @@ -34,7 +34,7 @@ DESCRIPTIONS: tuple[TessieBinarySensorEntityDescription, ...] = ( is_on=lambda x: x == TessieState.ONLINE, ), TessieBinarySensorEntityDescription( - key="charge_state_battery_heater_on", + key="climate_state_battery_heater", device_class=BinarySensorDeviceClass.HEAT, entity_category=EntityCategory.DIAGNOSTIC, ), diff --git a/homeassistant/components/tessie/strings.json b/homeassistant/components/tessie/strings.json index 62de4f276f4..8e1e47f934f 100644 --- a/homeassistant/components/tessie/strings.json +++ b/homeassistant/components/tessie/strings.json @@ -252,7 +252,7 @@ "state": { "name": "Status" }, - "charge_state_battery_heater_on": { + "climate_state_battery_heater": { "name": "Battery heater" }, "charge_state_charge_enable_request": { diff --git a/tests/components/tessie/snapshots/test_binary_sensors.ambr b/tests/components/tessie/snapshots/test_binary_sensors.ambr index 854e1350234..7bc191de6ed 100644 --- a/tests/components/tessie/snapshots/test_binary_sensors.ambr +++ b/tests/components/tessie/snapshots/test_binary_sensors.ambr @@ -165,8 +165,8 @@ 'platform': 'tessie', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'charge_state_battery_heater_on', - 'unique_id': 'VINVINVIN-charge_state_battery_heater_on', + 'translation_key': 'climate_state_battery_heater', + 'unique_id': 'VINVINVIN-climate_state_battery_heater', 'unit_of_measurement': None, }) # --- From 49fc8a12307856b3efe3a80ebe06c07c62d9d1f2 Mon Sep 17 00:00:00 2001 From: Austin Mroczek Date: Tue, 2 Apr 2024 03:23:44 -0700 Subject: [PATCH 164/967] Improve DeviceInfo for Total Connect (#114509) --- .../components/totalconnect/alarm_control_panel.py | 1 + .../components/totalconnect/binary_sensor.py | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/homeassistant/components/totalconnect/alarm_control_panel.py b/homeassistant/components/totalconnect/alarm_control_panel.py index 3f2c51989f9..436e3198650 100644 --- a/homeassistant/components/totalconnect/alarm_control_panel.py +++ b/homeassistant/components/totalconnect/alarm_control_panel.py @@ -116,6 +116,7 @@ class TotalConnectAlarm( return DeviceInfo( identifiers={(DOMAIN, self._device.serial_number)}, name=self._device.name, + serial_number=self._device.serial_number, ) @property diff --git a/homeassistant/components/totalconnect/binary_sensor.py b/homeassistant/components/totalconnect/binary_sensor.py index c6c7c75e0b5..6043d15d2d4 100644 --- a/homeassistant/components/totalconnect/binary_sensor.py +++ b/homeassistant/components/totalconnect/binary_sensor.py @@ -10,6 +10,7 @@ from homeassistant.components.binary_sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN @@ -63,6 +64,16 @@ class TotalConnectZoneBinarySensor(BinarySensorEntity): "partition": self._zone.partition, } + @property + def device_info(self) -> DeviceInfo: + """Return device info.""" + identifier = self._zone.sensor_serial_number or f"zone_{self._zone.zoneid}" + return DeviceInfo( + name=self._zone.description, + identifiers={(DOMAIN, identifier)}, + serial_number=self._zone.sensor_serial_number, + ) + class TotalConnectZoneSecurityBinarySensor(TotalConnectZoneBinarySensor): """Represent an TotalConnect security zone.""" From a3dce51d38a4425bb0e572963f27da4793b99da7 Mon Sep 17 00:00:00 2001 From: dotvav Date: Tue, 2 Apr 2024 13:08:53 +0200 Subject: [PATCH 165/967] Fix Overkiz Hitachi OVP air-to-air heat pump (#114611) --- .../climate_entities/hitachi_air_to_air_heat_pump_ovp.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/overkiz/climate_entities/hitachi_air_to_air_heat_pump_ovp.py b/homeassistant/components/overkiz/climate_entities/hitachi_air_to_air_heat_pump_ovp.py index b4d6ab788a1..b31ecf91ec0 100644 --- a/homeassistant/components/overkiz/climate_entities/hitachi_air_to_air_heat_pump_ovp.py +++ b/homeassistant/components/overkiz/climate_entities/hitachi_air_to_air_heat_pump_ovp.py @@ -298,6 +298,11 @@ class HitachiAirToAirHeatPumpOVP(OverkizEntity, ClimateEntity): OverkizState.OVP_FAN_SPEED, OverkizCommandParam.AUTO, ) + # Sanitize fan mode: Overkiz is sometimes providing a state that + # cannot be used as a command. Convert it to HA space and back to Overkiz + if fan_mode not in FAN_MODES_TO_OVERKIZ.values(): + fan_mode = FAN_MODES_TO_OVERKIZ[OVERKIZ_TO_FAN_MODES[fan_mode]] + hvac_mode = self._control_backfill( hvac_mode, OverkizState.OVP_MODE_CHANGE, From 8a86d7512a82cd57e278d1747a6d5fe590f5197c Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Tue, 2 Apr 2024 12:17:47 +0100 Subject: [PATCH 166/967] Bump ring_doorbell integration to 0.8.9 (#114631) --- homeassistant/components/ring/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index e682dc053b3..05c6dcd5ab1 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -14,5 +14,5 @@ "iot_class": "cloud_polling", "loggers": ["ring_doorbell"], "quality_scale": "silver", - "requirements": ["ring-doorbell[listen]==0.8.8"] + "requirements": ["ring-doorbell[listen]==0.8.9"] } diff --git a/requirements_all.txt b/requirements_all.txt index 41a3006141a..8aadc8f1b8a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2451,7 +2451,7 @@ rfk101py==0.0.1 rflink==0.0.66 # homeassistant.components.ring -ring-doorbell[listen]==0.8.8 +ring-doorbell[listen]==0.8.9 # homeassistant.components.fleetgo ritassist==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0e4bb39fddb..7190a935066 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1894,7 +1894,7 @@ reolink-aio==0.8.9 rflink==0.0.66 # homeassistant.components.ring -ring-doorbell[listen]==0.8.8 +ring-doorbell[listen]==0.8.9 # homeassistant.components.roku rokuecp==0.19.2 From 476e39dd2c4068a4f57ff441887a1960943a5097 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Tue, 2 Apr 2024 13:19:50 +0200 Subject: [PATCH 167/967] Bump uv to 0.1.27 (#114629) --- .pre-commit-config.yaml | 2 +- Dockerfile | 2 +- requirements_test.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ef4cdd98efb..0e5dac9d409 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -83,7 +83,7 @@ repos: pass_filenames: false language: script types: [text] - files: ^(homeassistant/.+/(icons|manifest|strings)\.json|homeassistant/brands/.*\.json|\.coveragerc|homeassistant/.+/services\.yaml|script/hassfest/(?!metadata|mypy_config).+\.py)$ + files: ^(homeassistant/.+/(icons|manifest|strings)\.json|homeassistant/brands/.*\.json|\.coveragerc|homeassistant/.+/services\.yaml|script/hassfest/(?!metadata|mypy_config).+\.py|requirements_test.txt)$ - id: hassfest-metadata name: hassfest-metadata entry: script/run-in-env.sh python3 -m script.hassfest -p metadata diff --git a/Dockerfile b/Dockerfile index 2a27402be6c..1f2f011b288 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ ENV \ ARG QEMU_CPU # Install uv -RUN pip3 install uv==0.1.24 +RUN pip3 install uv==0.1.27 WORKDIR /usr/src diff --git a/requirements_test.txt b/requirements_test.txt index d57d1c4a5df..c2e5774e137 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -51,4 +51,4 @@ types-pytz==2024.1.0.20240203 types-PyYAML==6.0.12.20240311 types-requests==2.31.0.3 types-xmltodict==0.13.0.3 -uv==0.1.24 +uv==0.1.27 From 0b7d9d6c4420b8bce27623902077bddcf8e79745 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 2 Apr 2024 13:36:44 +0200 Subject: [PATCH 168/967] Remove YAML configuration from Withings (#114626) * Remove YAML configuration from Withings * Remove YAML configuration from Withings * Remove YAML configuration from Withings --- homeassistant/components/withings/__init__.py | 70 +-------------- .../components/withings/config_flow.py | 3 +- homeassistant/components/withings/const.py | 2 - tests/components/withings/test_init.py | 89 +------------------ 4 files changed, 6 insertions(+), 158 deletions(-) diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index c14fb465731..1fe85f180da 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -16,14 +16,9 @@ from aiohttp.hdrs import METH_POST from aiohttp.web import Request, Response from aiowithings import NotificationCategory, WithingsClient from aiowithings.util import to_enum -import voluptuous as vol from yarl import URL from homeassistant.components import cloud -from homeassistant.components.application_credentials import ( - ClientCredential, - async_import_client_credential, -) from homeassistant.components.http import HomeAssistantView from homeassistant.components.webhook import ( async_generate_id as webhook_generate_id, @@ -34,25 +29,20 @@ from homeassistant.components.webhook import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_ACCESS_TOKEN, - CONF_CLIENT_ID, - CONF_CLIENT_SECRET, CONF_TOKEN, CONF_WEBHOOK_ID, EVENT_HOMEASSISTANT_STOP, Platform, ) -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant -from homeassistant.helpers import config_validation as cv +from homeassistant.core import HomeAssistant 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.event import async_call_later -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from homeassistant.helpers.typing import ConfigType -from .const import CONF_PROFILES, CONF_USE_WEBHOOK, DEFAULT_TITLE, DOMAIN, LOGGER +from .const import DEFAULT_TITLE, DOMAIN, LOGGER from .coordinator import ( WithingsActivityDataUpdateCoordinator, WithingsBedPresenceDataUpdateCoordinator, @@ -65,67 +55,11 @@ from .coordinator import ( PLATFORMS = [Platform.BINARY_SENSOR, Platform.CALENDAR, Platform.SENSOR] -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.All( - cv.deprecated(CONF_PROFILES), - cv.deprecated(CONF_CLIENT_ID), - cv.deprecated(CONF_CLIENT_SECRET), - vol.Schema( - { - vol.Optional(CONF_CLIENT_ID): vol.All(cv.string, vol.Length(min=1)), - vol.Optional(CONF_CLIENT_SECRET): vol.All( - cv.string, vol.Length(min=1) - ), - vol.Optional(CONF_USE_WEBHOOK): cv.boolean, - vol.Optional(CONF_PROFILES): vol.All( - cv.ensure_list, - vol.Unique(), - vol.Length(min=1), - [vol.All(cv.string, vol.Length(min=1))], - ), - } - ), - ) - }, - extra=vol.ALLOW_EXTRA, -) SUBSCRIBE_DELAY = timedelta(seconds=5) UNSUBSCRIBE_DELAY = timedelta(seconds=1) CONF_CLOUDHOOK_URL = "cloudhook_url" -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Withings component.""" - - if conf := config.get(DOMAIN): - async_create_issue( - hass, - HOMEASSISTANT_DOMAIN, - f"deprecated_yaml_{DOMAIN}", - breaks_in_ha_version="2024.4.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "Withings", - }, - ) - if CONF_CLIENT_ID in conf: - await async_import_client_credential( - hass, - DOMAIN, - ClientCredential( - conf[CONF_CLIENT_ID], - conf[CONF_CLIENT_SECRET], - ), - ) - - return True - - @dataclass(slots=True) class WithingsData: """Dataclass to hold withings domain data.""" diff --git a/homeassistant/components/withings/config_flow.py b/homeassistant/components/withings/config_flow.py index 1b92f23685f..aee25da507c 100644 --- a/homeassistant/components/withings/config_flow.py +++ b/homeassistant/components/withings/config_flow.py @@ -13,7 +13,7 @@ from homeassistant.config_entries import ConfigEntry, ConfigFlowResult from homeassistant.const import CONF_TOKEN, CONF_WEBHOOK_ID from homeassistant.helpers import config_entry_oauth2_flow -from .const import CONF_USE_WEBHOOK, DEFAULT_TITLE, DOMAIN +from .const import DEFAULT_TITLE, DOMAIN class WithingsFlowHandler( @@ -71,7 +71,6 @@ class WithingsFlowHandler( return self.async_create_entry( title=DEFAULT_TITLE, data={**data, CONF_WEBHOOK_ID: async_generate_id()}, - options={CONF_USE_WEBHOOK: False}, ) if self.reauth_entry.unique_id == user_id: diff --git a/homeassistant/components/withings/const.py b/homeassistant/components/withings/const.py index a87fc8bfe83..91a7b9d9450 100644 --- a/homeassistant/components/withings/const.py +++ b/homeassistant/components/withings/const.py @@ -5,8 +5,6 @@ import logging LOGGER = logging.getLogger(__package__) DEFAULT_TITLE = "Withings" -CONF_PROFILES = "profiles" -CONF_USE_WEBHOOK = "use_webhook" DOMAIN = "withings" diff --git a/tests/components/withings/test_init.py b/tests/components/withings/test_init.py index eb089f44216..ff0a098a7cb 100644 --- a/tests/components/withings/test_init.py +++ b/tests/components/withings/test_init.py @@ -2,7 +2,7 @@ from datetime import timedelta from typing import Any -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import AsyncMock, patch from urllib.parse import urlparse from aiohttp.hdrs import METH_HEAD @@ -13,15 +13,13 @@ from aiowithings import ( ) from freezegun.api import FrozenDateTimeFactory import pytest -import voluptuous as vol from homeassistant import config_entries from homeassistant.components import cloud from homeassistant.components.cloud import CloudNotAvailable from homeassistant.components.webhook import async_generate_url -from homeassistant.components.withings import CONFIG_SCHEMA, async_setup -from homeassistant.components.withings.const import CONF_USE_WEBHOOK, DOMAIN -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_WEBHOOK_ID +from homeassistant.components.withings.const import DOMAIN +from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util @@ -37,87 +35,6 @@ from tests.components.cloud import mock_cloud from tests.typing import ClientSessionGenerator -def config_schema_validate(withings_config) -> dict: - """Assert a schema config succeeds.""" - hass_config = {DOMAIN: withings_config} - - return CONFIG_SCHEMA(hass_config) - - -def config_schema_assert_fail(withings_config) -> None: - """Assert a schema config will fail.""" - with pytest.raises(vol.MultipleInvalid): - config_schema_validate(withings_config) - - -def test_config_schema_basic_config() -> None: - """Test schema.""" - config_schema_validate( - { - CONF_CLIENT_ID: "my_client_id", - CONF_CLIENT_SECRET: "my_client_secret", - CONF_USE_WEBHOOK: True, - } - ) - - -def test_config_schema_client_id() -> None: - """Test schema.""" - config_schema_assert_fail( - {CONF_CLIENT_SECRET: "my_client_secret", CONF_CLIENT_ID: ""} - ) - config_schema_validate( - {CONF_CLIENT_SECRET: "my_client_secret", CONF_CLIENT_ID: "my_client_id"} - ) - - -def test_config_schema_client_secret() -> None: - """Test schema.""" - config_schema_assert_fail({CONF_CLIENT_ID: "my_client_id", CONF_CLIENT_SECRET: ""}) - config_schema_validate( - {CONF_CLIENT_ID: "my_client_id", CONF_CLIENT_SECRET: "my_client_secret"} - ) - - -def test_config_schema_use_webhook() -> None: - """Test schema.""" - config_schema_validate( - {CONF_CLIENT_ID: "my_client_id", CONF_CLIENT_SECRET: "my_client_secret"} - ) - config = config_schema_validate( - { - CONF_CLIENT_ID: "my_client_id", - CONF_CLIENT_SECRET: "my_client_secret", - CONF_USE_WEBHOOK: True, - } - ) - assert config[DOMAIN][CONF_USE_WEBHOOK] is True - config = config_schema_validate( - { - CONF_CLIENT_ID: "my_client_id", - CONF_CLIENT_SECRET: "my_client_secret", - CONF_USE_WEBHOOK: False, - } - ) - assert config[DOMAIN][CONF_USE_WEBHOOK] is False - config_schema_assert_fail( - { - CONF_CLIENT_ID: "my_client_id", - CONF_CLIENT_SECRET: "my_client_secret", - CONF_USE_WEBHOOK: "A", - } - ) - - -async def test_async_setup_no_config(hass: HomeAssistant) -> None: - """Test method.""" - hass.async_create_task = MagicMock() - - await async_setup(hass, {}) - - hass.async_create_task.assert_not_called() - - async def test_data_manager_webhook_subscription( hass: HomeAssistant, withings: AsyncMock, From 991a4940cd95e3976a473f61c8978c1acb67d616 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 2 Apr 2024 13:53:58 +0200 Subject: [PATCH 169/967] Bump ruff to 0.3.5 (#114634) --- .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 0e5dac9d409..8280ac326a7 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.3.4 + rev: v0.3.5 hooks: - id: ruff args: diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index cb64db20dcd..dacdb752a8d 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.2.6 -ruff==0.3.4 +ruff==0.3.5 yamllint==1.35.1 From 1b875e7de2152c099c6287bf04640baa5296b0d8 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 2 Apr 2024 17:15:24 +0200 Subject: [PATCH 170/967] Update frontend to 20240402.1 (#114646) --- 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 5eaa6e94769..2010a9985b3 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==20240402.0"] + "requirements": ["home-assistant-frontend==20240402.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index fdb08832a82..1dc5cd9886a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -30,7 +30,7 @@ habluetooth==2.4.2 hass-nabucasa==0.79.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240402.0 +home-assistant-frontend==20240402.1 home-assistant-intents==2024.3.29 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 8aadc8f1b8a..a8761290168 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1081,7 +1081,7 @@ hole==0.8.0 holidays==0.45 # homeassistant.components.frontend -home-assistant-frontend==20240402.0 +home-assistant-frontend==20240402.1 # homeassistant.components.conversation home-assistant-intents==2024.3.29 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7190a935066..350bcbc13dd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -880,7 +880,7 @@ hole==0.8.0 holidays==0.45 # homeassistant.components.frontend -home-assistant-frontend==20240402.0 +home-assistant-frontend==20240402.1 # homeassistant.components.conversation home-assistant-intents==2024.3.29 From 581c67ed293ca5e7a3e4d26272c7690ae6f0973b Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 2 Apr 2024 08:16:59 -0700 Subject: [PATCH 171/967] Reduce ZHA OTA logbook entries and extraneous updates (#114591) --- .../components/zha/core/cluster_handlers/general.py | 7 +++++++ homeassistant/components/zha/update.py | 5 ----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/zha/core/cluster_handlers/general.py b/homeassistant/components/zha/core/cluster_handlers/general.py index 478f41da3b7..438fc6b1723 100644 --- a/homeassistant/components/zha/core/cluster_handlers/general.py +++ b/homeassistant/components/zha/core/cluster_handlers/general.py @@ -553,6 +553,13 @@ class OtaClientClusterHandler(ClientClusterHandler): Ota.AttributeDefs.current_file_version.name: True, } + @callback + def attribute_updated(self, attrid: int, value: Any, timestamp: Any) -> None: + """Handle an attribute updated on this cluster.""" + # We intentionally avoid the `ClientClusterHandler` attribute update handler: + # it emits a logbook event on every update, which pollutes the logbook + ClusterHandler.attribute_updated(self, attrid, value, timestamp) + @property def current_file_version(self) -> int | None: """Return cached value of current_file_version attribute.""" diff --git a/homeassistant/components/zha/update.py b/homeassistant/components/zha/update.py index 7ceba4fc924..0cb80d13119 100644 --- a/homeassistant/components/zha/update.py +++ b/homeassistant/components/zha/update.py @@ -130,14 +130,9 @@ class ZHAFirmwareUpdateEntity( def _get_cluster_version(self) -> str | None: """Synchronize current file version with the cluster.""" - device = self._ota_cluster_handler._endpoint.device # pylint: disable=protected-access - if self._ota_cluster_handler.current_file_version is not None: return f"0x{self._ota_cluster_handler.current_file_version:08x}" - if device.sw_version is not None: - return device.sw_version - return None @callback From 52bd3efad9f85154324f7a0906fa1257562b60f6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 2 Apr 2024 11:30:41 -0400 Subject: [PATCH 172/967] Clean up unnecessary setup calls in tests (#114644) --- tests/components/google_generative_ai_conversation/conftest.py | 1 - tests/components/google_generative_ai_conversation/test_init.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/tests/components/google_generative_ai_conversation/conftest.py b/tests/components/google_generative_ai_conversation/conftest.py index 5c979d3bc47..c377a469df0 100644 --- a/tests/components/google_generative_ai_conversation/conftest.py +++ b/tests/components/google_generative_ai_conversation/conftest.py @@ -27,7 +27,6 @@ def mock_config_entry(hass): @pytest.fixture async def mock_init_component(hass: HomeAssistant, mock_config_entry: ConfigEntry): """Initialize integration.""" - assert await async_setup_component(hass, "homeassistant", {}) with patch("google.generativeai.get_model"): assert await async_setup_component( hass, "google_generative_ai_conversation", {} diff --git a/tests/components/google_generative_ai_conversation/test_init.py b/tests/components/google_generative_ai_conversation/test_init.py index befe3b93d12..07254be9e3f 100644 --- a/tests/components/google_generative_ai_conversation/test_init.py +++ b/tests/components/google_generative_ai_conversation/test_init.py @@ -10,7 +10,6 @@ from homeassistant.components import conversation from homeassistant.core import Context, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import area_registry as ar, device_registry as dr, intent -from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -125,7 +124,6 @@ async def test_template_error( hass: HomeAssistant, mock_config_entry: MockConfigEntry ) -> None: """Test that template error handling works.""" - assert await async_setup_component(hass, "homeassistant", {}) hass.config_entries.async_update_entry( mock_config_entry, options={ From 25c920b1eec0496ece953789d1b26cb7a66cbd1c Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 2 Apr 2024 18:28:52 +0200 Subject: [PATCH 173/967] Add missing state to the Tractive tracker state sensor (#114654) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/tractive/sensor.py | 1 + homeassistant/components/tractive/strings.json | 1 + 2 files changed, 2 insertions(+) diff --git a/homeassistant/components/tractive/sensor.py b/homeassistant/components/tractive/sensor.py index 5e2f3288f57..1edee71467b 100644 --- a/homeassistant/components/tractive/sensor.py +++ b/homeassistant/components/tractive/sensor.py @@ -107,6 +107,7 @@ SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, device_class=SensorDeviceClass.ENUM, options=[ + "inaccurate_position", "not_reporting", "operational", "system_shutdown_user", diff --git a/homeassistant/components/tractive/strings.json b/homeassistant/components/tractive/strings.json index 82b7ecc295c..0690328c99c 100644 --- a/homeassistant/components/tractive/strings.json +++ b/homeassistant/components/tractive/strings.json @@ -70,6 +70,7 @@ "tracker_state": { "name": "Tracker state", "state": { + "inaccurate_position": "Inaccurate position", "not_reporting": "Not reporting", "operational": "Operational", "system_shutdown_user": "System shutdown user", From 9893a6c5e4103f059e9d294843c18829cca4c679 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 2 Apr 2024 18:33:12 +0200 Subject: [PATCH 174/967] Bump aiounifi to v74 (#114649) --- 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 63f9f67605e..05dc2189908 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==73"], + "requirements": ["aiounifi==74"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index a8761290168..f91e32c9f65 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -392,7 +392,7 @@ aiotankerkoenig==0.4.1 aiotractive==0.5.6 # homeassistant.components.unifi -aiounifi==73 +aiounifi==74 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 350bcbc13dd..98383cd0c4c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -365,7 +365,7 @@ aiotankerkoenig==0.4.1 aiotractive==0.5.6 # homeassistant.components.unifi -aiounifi==73 +aiounifi==74 # homeassistant.components.vlc_telnet aiovlc==0.1.0 From 17625dc74d4787003e872a36d954e7b155b72bbd Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Tue, 2 Apr 2024 18:52:41 +0200 Subject: [PATCH 175/967] Fix Google translate TTS test race condition (#114656) --- tests/components/google_translate/test_tts.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/components/google_translate/test_tts.py b/tests/components/google_translate/test_tts.py index 1df609b0db4..1cff6e97781 100644 --- a/tests/components/google_translate/test_tts.py +++ b/tests/components/google_translate/test_tts.py @@ -73,6 +73,8 @@ async def setup_fixture( else: raise RuntimeError("Invalid setup fixture") + await hass.async_block_till_done() + @pytest.fixture(name="config") def config_fixture() -> dict[str, Any]: From 6638d1c8e8653d55bc6b7d2924253d5d5817c78b Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 2 Apr 2024 20:58:18 +0300 Subject: [PATCH 176/967] Bump holidays to 0.46 (#114657) --- 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 f1bc60dece4..5a1edcd3c3f 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.45", "babel==2.13.1"] + "requirements": ["holidays==0.46", "babel==2.13.1"] } diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index 6b17a980870..314f4c6bcf4 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.45"] + "requirements": ["holidays==0.46"] } diff --git a/requirements_all.txt b/requirements_all.txt index f91e32c9f65..6b71c1e788d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1078,7 +1078,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.45 +holidays==0.46 # homeassistant.components.frontend home-assistant-frontend==20240402.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 98383cd0c4c..6ed14716033 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -877,7 +877,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.45 +holidays==0.46 # homeassistant.components.frontend home-assistant-frontend==20240402.1 From ef7836be730511ffd72b31b949fbdc2fa0ce53bc Mon Sep 17 00:00:00 2001 From: atlflyer <31390797+atlflyer@users.noreply.github.com> Date: Tue, 2 Apr 2024 13:59:57 -0400 Subject: [PATCH 177/967] Add icon to command_line cover config (#114645) * Add icon to command_line cover config * Remove unwanted #noqa tag * Remove redundancy from new test name * Apply requested changes --- .../components/command_line/__init__.py | 1 + .../components/command_line/cover.py | 2 + tests/components/command_line/test_cover.py | 45 +++++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/homeassistant/components/command_line/__init__.py b/homeassistant/components/command_line/__init__.py index 27b69e59ca4..0f217eb0ee1 100644 --- a/homeassistant/components/command_line/__init__.py +++ b/homeassistant/components/command_line/__init__.py @@ -102,6 +102,7 @@ COVER_SCHEMA = vol.Schema( vol.Optional(CONF_COMMAND_STATE): cv.string, vol.Optional(CONF_COMMAND_STOP, default="true"): cv.string, vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_ICON): cv.template, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, vol.Optional(CONF_UNIQUE_ID): cv.string, diff --git a/homeassistant/components/command_line/cover.py b/homeassistant/components/command_line/cover.py index c27cd97b39a..85c0ab605c7 100644 --- a/homeassistant/components/command_line/cover.py +++ b/homeassistant/components/command_line/cover.py @@ -12,6 +12,7 @@ from homeassistant.const import ( CONF_COMMAND_OPEN, CONF_COMMAND_STATE, CONF_COMMAND_STOP, + CONF_ICON, CONF_NAME, CONF_SCAN_INTERVAL, CONF_UNIQUE_ID, @@ -54,6 +55,7 @@ async def async_setup_platform( trigger_entity_config = { CONF_UNIQUE_ID: device_config.get(CONF_UNIQUE_ID), CONF_NAME: Template(device_config.get(CONF_NAME, device_name), hass), + CONF_ICON: device_config.get(CONF_ICON), CONF_AVAILABILITY: device_config.get(CONF_AVAILABILITY), } diff --git a/tests/components/command_line/test_cover.py b/tests/components/command_line/test_cover.py index 8b98d8d1623..7ed48909d79 100644 --- a/tests/components/command_line/test_cover.py +++ b/tests/components/command_line/test_cover.py @@ -383,3 +383,48 @@ async def test_availability( entity_state = hass.states.get("cover.test") assert entity_state assert entity_state.state == STATE_UNAVAILABLE + + +async def test_icon_template(hass: HomeAssistant) -> None: + """Test with state value.""" + with tempfile.TemporaryDirectory() as tempdirname: + path = os.path.join(tempdirname, "cover_status_icon") + await setup.async_setup_component( + hass, + DOMAIN, + { + "command_line": [ + { + "cover": { + "command_state": f"cat {path}", + "command_open": f"echo 100 > {path}", + "command_close": f"echo 0 > {path}", + "command_stop": f"echo 0 > {path}", + "name": "Test", + "icon": "{% if this.state=='open' %} mdi:open {% else %} mdi:closed {% endif %}", + } + } + ] + }, + ) + await hass.async_block_till_done() + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: "cover.test"}, + blocking=True, + ) + entity_state = hass.states.get("cover.test") + assert entity_state + assert entity_state.attributes.get("icon") == "mdi:closed" + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: "cover.test"}, + blocking=True, + ) + entity_state = hass.states.get("cover.test") + assert entity_state + assert entity_state.attributes.get("icon") == "mdi:open" From 850dac065589fc6cbbdb8a88e4147aefbeff0233 Mon Sep 17 00:00:00 2001 From: jayme-github Date: Tue, 2 Apr 2024 20:14:02 +0200 Subject: [PATCH 178/967] Don't overwrite target temperature by setting hvac mode in AVM Fritz!SmartHome (#112119) --- homeassistant/components/fritzbox/climate.py | 6 ++++++ tests/components/fritzbox/test_climate.py | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index 17accf35819..de9ec200e3e 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -29,6 +29,7 @@ from .const import ( ATTR_STATE_HOLIDAY_MODE, ATTR_STATE_SUMMER_MODE, ATTR_STATE_WINDOW_OPEN, + LOGGER, ) from .model import ClimateExtraAttributes @@ -129,6 +130,11 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity): async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new operation mode.""" + if self.hvac_mode == hvac_mode: + LOGGER.debug( + "%s is already in requested hvac mode %s", self.name, hvac_mode + ) + return if hvac_mode == HVACMode.OFF: await self.async_set_temperature(temperature=OFF_REPORT_SET_TEMPERATURE) else: diff --git a/tests/components/fritzbox/test_climate.py b/tests/components/fritzbox/test_climate.py index 073a67f22c1..54d222c6899 100644 --- a/tests/components/fritzbox/test_climate.py +++ b/tests/components/fritzbox/test_climate.py @@ -288,6 +288,7 @@ async def test_set_temperature_mode_off(hass: HomeAssistant, fritz: Mock) -> Non async def test_set_temperature_mode_heat(hass: HomeAssistant, fritz: Mock) -> None: """Test setting temperature by mode.""" device = FritzDeviceClimateMock() + device.target_temperature = 0.0 assert await setup_config_entry( hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz ) @@ -321,9 +322,26 @@ async def test_set_hvac_mode_off(hass: HomeAssistant, fritz: Mock) -> None: assert device.set_target_temperature.call_args_list == [call(0)] +async def test_no_reset_hvac_mode_heat(hass: HomeAssistant, fritz: Mock) -> None: + """Test setting hvac mode.""" + device = FritzDeviceClimateMock() + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.HEAT}, + True, + ) + assert device.set_target_temperature.call_count == 0 + + async def test_set_hvac_mode_heat(hass: HomeAssistant, fritz: Mock) -> None: """Test setting hvac mode.""" device = FritzDeviceClimateMock() + device.target_temperature = 0.0 assert await setup_config_entry( hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz ) From f85511255ce0e75f6fcab74d4c6e105b51f73c31 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Tue, 2 Apr 2024 20:19:43 +0200 Subject: [PATCH 179/967] Fix Rpi_power test race condition (#114662) --- .../rpi_power/test_binary_sensor.py | 34 ++++++++----------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/tests/components/rpi_power/test_binary_sensor.py b/tests/components/rpi_power/test_binary_sensor.py index 78b7b9261b9..1643df6c993 100644 --- a/tests/components/rpi_power/test_binary_sensor.py +++ b/tests/components/rpi_power/test_binary_sensor.py @@ -6,6 +6,7 @@ from unittest.mock import MagicMock import pytest +from homeassistant.components.rpi_power import binary_sensor from homeassistant.components.rpi_power.binary_sensor import ( DESCRIPTION_NORMALIZED, DESCRIPTION_UNDER_VOLTAGE, @@ -48,32 +49,25 @@ async def test_new_detected( """Test new entry with under voltage detected.""" mocked_under_voltage = await _async_setup_component(hass, True) state = hass.states.get(ENTITY_ID) + assert state assert state.state == STATE_ON assert ( - len( - [ - x - for x in caplog.records - if x.levelno == logging.WARNING - and x.message == DESCRIPTION_UNDER_VOLTAGE - ] - ) - == 1 - ) + binary_sensor.__name__, + logging.WARNING, + DESCRIPTION_UNDER_VOLTAGE, + ) in caplog.record_tuples # back to normal type(mocked_under_voltage).get = MagicMock(return_value=False) future = dt_util.utcnow() + timedelta(minutes=1) async_fire_time_changed(hass, future) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) + state = hass.states.get(ENTITY_ID) + assert state + assert state.state == STATE_OFF assert ( - len( - [ - x - for x in caplog.records - if x.levelno == logging.INFO and x.message == DESCRIPTION_NORMALIZED - ] - ) - == 1 - ) + binary_sensor.__name__, + logging.INFO, + DESCRIPTION_NORMALIZED, + ) in caplog.record_tuples From 7cb01f75ae709961ede7bb8c91826637f937b441 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 2 Apr 2024 20:21:55 +0200 Subject: [PATCH 180/967] Add typing to Roomba config flow (#114624) --- .../components/roomba/config_flow.py | 106 +++++++++--------- 1 file changed, 55 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/roomba/config_flow.py b/homeassistant/components/roomba/config_flow.py index 7b834421135..53ea9aa7c44 100644 --- a/homeassistant/components/roomba/config_flow.py +++ b/homeassistant/components/roomba/config_flow.py @@ -4,8 +4,9 @@ from __future__ import annotations import asyncio from functools import partial +from typing import Any -from roombapy import RoombaFactory +from roombapy import RoombaFactory, RoombaInfo from roombapy.discovery import RoombaDiscovery from roombapy.getpassword import RoombaPassword import voluptuous as vol @@ -15,7 +16,7 @@ from homeassistant.config_entries import ( ConfigEntry, ConfigFlow, ConfigFlowResult, - OptionsFlow, + OptionsFlowWithConfigEntry, ) from homeassistant.const import CONF_DELAY, CONF_HOST, CONF_NAME, CONF_PASSWORD from homeassistant.core import HomeAssistant, callback @@ -43,7 +44,7 @@ AUTH_HELP_URL_KEY = "auth_help_url" AUTH_HELP_URL_VALUE = "https://www.home-assistant.io/integrations/roomba/#manually-retrieving-your-credentials" -async def validate_input(hass: HomeAssistant, data): +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -75,20 +76,21 @@ class RoombaConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 - def __init__(self): + name: str | None = None + blid: str | None = None + host: str | None = None + + def __init__(self) -> None: """Initialize the roomba flow.""" - self.discovered_robots = {} - self.name = None - self.blid = None - self.host = None + self.discovered_robots: dict[str, RoombaInfo] = {} @staticmethod @callback def async_get_options_flow( config_entry: ConfigEntry, - ) -> OptionsFlowHandler: + ) -> RoombaOptionsFlowHandler: """Get the options flow for this handler.""" - return OptionsFlowHandler(config_entry) + return RoombaOptionsFlowHandler(config_entry) async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo @@ -135,8 +137,9 @@ class RoombaConfigFlow(ConfigFlow, domain=DOMAIN): self.context["title_placeholders"] = {"host": self.host, "name": self.blid} return await self.async_step_user() - async def _async_start_link(self): + async def _async_start_link(self) -> ConfigFlowResult: """Start linking.""" + assert self.host device = self.discovered_robots[self.host] self.blid = device.blid self.name = device.robot_name @@ -144,7 +147,9 @@ class RoombaConfigFlow(ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() return await self.async_step_link() - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: """Handle a flow start.""" # Check if user chooses manual entry if user_input is not None and not user_input.get(CONF_HOST): @@ -181,25 +186,23 @@ class RoombaConfigFlow(ConfigFlow, domain=DOMAIN): if not self.discovered_robots: return await self.async_step_manual() + hosts: dict[str | None, str] = { + **{ + device.ip: f"{device.robot_name} ({device.ip})" + for device in devices + if device.blid not in already_configured + }, + None: "Manually add a Roomba or Braava", + } + return self.async_show_form( step_id="user", - data_schema=vol.Schema( - { - vol.Optional("host"): vol.In( - { - **{ - device.ip: f"{device.robot_name} ({device.ip})" - for device in devices - if device.blid not in already_configured - }, - None: "Manually add a Roomba or Braava", - } - ) - } - ), + data_schema=vol.Schema({vol.Optional("host"): vol.In(hosts)}), ) - async def async_step_manual(self, user_input=None): + async def async_step_manual( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: """Handle manual device setup.""" if user_input is None: return self.async_show_form( @@ -224,7 +227,9 @@ class RoombaConfigFlow(ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() return await self.async_step_link() - async def async_step_link(self, user_input=None): + async def async_step_link( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: """Attempt to link with the Roomba. Given a configured host, will ask the user to press the home and target buttons @@ -235,7 +240,7 @@ class RoombaConfigFlow(ConfigFlow, domain=DOMAIN): step_id="link", description_placeholders={CONF_NAME: self.name or self.blid}, ) - + assert self.host roomba_pw = RoombaPassword(self.host) try: @@ -260,10 +265,12 @@ class RoombaConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_abort(reason="cannot_connect") self.name = info[CONF_NAME] - + assert self.name return self.async_create_entry(title=self.name, data=config) - async def async_step_link_manual(self, user_input=None): + async def async_step_link_manual( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: """Handle manual linking.""" errors = {} @@ -278,8 +285,7 @@ class RoombaConfigFlow(ConfigFlow, domain=DOMAIN): info = await validate_input(self.hass, config) except CannotConnect: errors = {"base": "cannot_connect"} - - if not errors: + else: return self.async_create_entry(title=info[CONF_NAME], data=config) return self.async_show_form( @@ -290,14 +296,12 @@ class RoombaConfigFlow(ConfigFlow, domain=DOMAIN): ) -class OptionsFlowHandler(OptionsFlow): +class RoombaOptionsFlowHandler(OptionsFlowWithConfigEntry): """Handle options.""" - def __init__(self, config_entry: ConfigEntry) -> None: - """Initialize options flow.""" - self.config_entry = config_entry - - async def async_step_init(self, user_input=None): + 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) @@ -308,15 +312,11 @@ class OptionsFlowHandler(OptionsFlow): { vol.Optional( CONF_CONTINUOUS, - default=self.config_entry.options.get( - CONF_CONTINUOUS, DEFAULT_CONTINUOUS - ), + default=self.options.get(CONF_CONTINUOUS, DEFAULT_CONTINUOUS), ): bool, vol.Optional( CONF_DELAY, - default=self.config_entry.options.get( - CONF_DELAY, DEFAULT_DELAY - ), + default=self.options.get(CONF_DELAY, DEFAULT_DELAY), ): int, } ), @@ -324,7 +324,7 @@ class OptionsFlowHandler(OptionsFlow): @callback -def _async_get_roomba_discovery(): +def _async_get_roomba_discovery() -> RoombaDiscovery: """Create a discovery object.""" discovery = RoombaDiscovery() discovery.amount_of_broadcasted_messages = MAX_NUM_DEVICES_TO_DISCOVER @@ -332,24 +332,28 @@ def _async_get_roomba_discovery(): @callback -def _async_blid_from_hostname(hostname): +def _async_blid_from_hostname(hostname: str) -> str: """Extract the blid from the hostname.""" return hostname.split("-")[1].split(".")[0].upper() -async def _async_discover_roombas(hass, host): - discovered_hosts = set() - devices = [] +async def _async_discover_roombas( + hass: HomeAssistant, host: str | None = None +) -> list[RoombaInfo]: + discovered_hosts: set[str] = set() + devices: list[RoombaInfo] = [] discover_lock = hass.data.setdefault(ROOMBA_DISCOVERY_LOCK, asyncio.Lock()) discover_attempts = HOST_ATTEMPTS if host else ALL_ATTEMPTS for attempt in range(discover_attempts + 1): async with discover_lock: discovery = _async_get_roomba_discovery() + discovered: set[RoombaInfo] = set() try: if host: device = await hass.async_add_executor_job(discovery.get, host) - discovered = [device] if device else [] + if device: + discovered.add(device) else: discovered = await hass.async_add_executor_job(discovery.get_all) except OSError: From 17f0002549adb1e4d43964acd1a1ad026a851897 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Tue, 2 Apr 2024 20:23:55 +0100 Subject: [PATCH 181/967] Azure DevOps integration tests (#114577) * Add tests to azure devops * Remove Azure DevOps files from coverage * Add assertion for entity registration in test_sensors() * Remove unnecessary code in test_sensor.py * Refactor test_sensors function * Fix * Test unique id * Refactor * Refactor reauth_flow test in azure_devops module * Suggested changes, batched Co-authored-by: Joost Lekkerkerker * Changes * Use snapshot * Remove redundant entry fetch --------- Co-authored-by: Joost Lekkerkerker --- .coveragerc | 2 - tests/components/azure_devops/__init__.py | 81 ++++ tests/components/azure_devops/conftest.py | 58 +++ .../azure_devops/snapshots/test_sensor.ambr | 59 +++ .../azure_devops/test_config_flow.py | 413 ++++++++---------- tests/components/azure_devops/test_init.py | 78 ++++ tests/components/azure_devops/test_sensor.py | 33 ++ 7 files changed, 501 insertions(+), 223 deletions(-) create mode 100644 tests/components/azure_devops/conftest.py create mode 100644 tests/components/azure_devops/snapshots/test_sensor.ambr create mode 100644 tests/components/azure_devops/test_init.py create mode 100644 tests/components/azure_devops/test_sensor.py diff --git a/.coveragerc b/.coveragerc index 54cc470ac63..cbabcb7733d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -108,8 +108,6 @@ omit = homeassistant/components/avea/light.py homeassistant/components/avion/light.py homeassistant/components/awair/coordinator.py - homeassistant/components/azure_devops/__init__.py - homeassistant/components/azure_devops/sensor.py homeassistant/components/azure_service_bus/* homeassistant/components/baf/__init__.py homeassistant/components/baf/climate.py diff --git a/tests/components/azure_devops/__init__.py b/tests/components/azure_devops/__init__.py index da15bc6723d..fb0817671b5 100644 --- a/tests/components/azure_devops/__init__.py +++ b/tests/components/azure_devops/__init__.py @@ -1 +1,82 @@ """Tests for the Azure DevOps integration.""" + +from typing import Final + +from aioazuredevops.builds import DevOpsBuild, DevOpsBuildDefinition +from aioazuredevops.core import DevOpsProject + +from homeassistant.components.azure_devops.const import CONF_ORG, CONF_PAT, CONF_PROJECT +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +ORGANIZATION: Final[str] = "testorg" +PROJECT: Final[str] = "testproject" +PAT: Final[str] = "abc123" + +UNIQUE_ID = f"{ORGANIZATION}_{PROJECT}" + + +FIXTURE_USER_INPUT = { + CONF_ORG: ORGANIZATION, + CONF_PROJECT: PROJECT, + CONF_PAT: PAT, +} + +FIXTURE_REAUTH_INPUT = { + CONF_PAT: PAT, +} + + +DEVOPS_PROJECT = DevOpsProject( + 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( + build_id=9876, + name="Test Build", + url=f"https://dev.azure.com/{ORGANIZATION}/{PROJECT}/_apis/build/definitions/1", + path="", + build_type="build", + queue_status="enabled", + revision=1, +) + +DEVOPS_BUILD = DevOpsBuild( + build_id=5678, + build_number="1", + status="completed", + result="succeeded", + source_branch="main", + source_version="123", + priority="normal", + reason="manual", + queue_time="2021-01-01T00:00:00Z", + start_time="2021-01-01T00:00:00Z", + finish_time="2021-01-01T00:00:00Z", + definition=DEVOPS_BUILD_DEFINITION, + project=DEVOPS_PROJECT, + links=None, +) + + +async def setup_integration( + hass: HomeAssistant, + config_entry: MockConfigEntry, +) -> bool: + """Fixture for setting up the component.""" + config_entry.add_to_hass(hass) + + result = await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return result diff --git a/tests/components/azure_devops/conftest.py b/tests/components/azure_devops/conftest.py new file mode 100644 index 00000000000..d51142cdced --- /dev/null +++ b/tests/components/azure_devops/conftest.py @@ -0,0 +1,58 @@ +"""Test fixtures for Azure DevOps.""" + +from collections.abc import AsyncGenerator, Generator +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from homeassistant.components.azure_devops.const import DOMAIN + +from . import DEVOPS_BUILD, DEVOPS_PROJECT, FIXTURE_USER_INPUT, PAT, UNIQUE_ID + +from tests.common import MockConfigEntry + + +@pytest.fixture +async def mock_devops_client() -> AsyncGenerator[MagicMock, None]: + """Mock the Azure DevOps client.""" + + with ( + patch( + "homeassistant.components.azure_devops.DevOpsClient", autospec=True + ) as mock_client, + patch( + "homeassistant.components.azure_devops.config_flow.DevOpsClient", + new=mock_client, + ), + ): + devops_client = mock_client.return_value + devops_client.authorized = True + devops_client.pat = PAT + devops_client.authorize.return_value = True + 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_items.return_value = None + + yield devops_client + + +@pytest.fixture +async def mock_config_entry() -> MockConfigEntry: + """Create a mock config entry.""" + return MockConfigEntry( + domain=DOMAIN, + data=FIXTURE_USER_INPUT, + unique_id=UNIQUE_ID, + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.azure_devops.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + yield mock_setup_entry diff --git a/tests/components/azure_devops/snapshots/test_sensor.ambr b/tests/components/azure_devops/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..b99d2c4e49d --- /dev/null +++ b/tests/components/azure_devops/snapshots/test_sensor.ambr @@ -0,0 +1,59 @@ +# serializer version: 1 +# name: test_sensors[sensor.testproject_test_build_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_test_build_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': 'Test Build 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_test_build_latest_build-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'definition_id': 9876, + 'definition_name': 'Test Build', + 'finish_time': '2021-01-01T00:00:00Z', + 'friendly_name': 'testproject Test Build 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_test_build_latest_build', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1', + }) +# --- diff --git a/tests/components/azure_devops/test_config_flow.py b/tests/components/azure_devops/test_config_flow.py index 84c2b5d3cca..8d36b731ff2 100644 --- a/tests/components/azure_devops/test_config_flow.py +++ b/tests/components/azure_devops/test_config_flow.py @@ -1,26 +1,18 @@ """Test the Azure DevOps config flow.""" -from unittest.mock import patch +from unittest.mock import AsyncMock from aioazuredevops.core import DevOpsProject import aiohttp from homeassistant import config_entries, data_entry_flow -from homeassistant.components.azure_devops.const import ( - CONF_ORG, - CONF_PAT, - CONF_PROJECT, - DOMAIN, -) +from homeassistant.components.azure_devops.const import CONF_ORG, CONF_PROJECT, DOMAIN from homeassistant.core import HomeAssistant +from . import FIXTURE_REAUTH_INPUT, FIXTURE_USER_INPUT + from tests.common import MockConfigEntry -FIXTURE_REAUTH_INPUT = {CONF_PAT: "abc123"} -FIXTURE_USER_INPUT = {CONF_ORG: "random", CONF_PROJECT: "project", CONF_PAT: "abc123"} - -UNIQUE_ID = "random_project" - async def test_show_user_form(hass: HomeAssistant) -> None: """Test that the setup form is served.""" @@ -32,259 +24,238 @@ async def test_show_user_form(hass: HomeAssistant) -> None: assert result["step_id"] == "user" -async def test_authorization_error(hass: HomeAssistant) -> None: +async def test_authorization_error( + hass: HomeAssistant, + mock_devops_client: AsyncMock, +) -> None: """Test we show user form on Azure DevOps authorization error.""" - with patch( - "homeassistant.components.azure_devops.config_flow.DevOpsClient.authorize", - return_value=False, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) + mock_devops_client.authorize.return_value = False + mock_devops_client.authorized = False - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "user" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - FIXTURE_USER_INPUT, - ) - await hass.async_block_till_done() + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" - assert result2["type"] == data_entry_flow.FlowResultType.FORM - assert result2["step_id"] == "user" - assert result2["errors"] == {"base": "invalid_auth"} + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + FIXTURE_USER_INPUT, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "invalid_auth"} -async def test_reauth_authorization_error(hass: HomeAssistant) -> None: +async def test_reauth_authorization_error( + hass: HomeAssistant, + mock_devops_client: AsyncMock, +) -> None: """Test we show user form on Azure DevOps authorization error.""" - with patch( - "homeassistant.components.azure_devops.config_flow.DevOpsClient.authorize", - return_value=False, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_REAUTH}, - data=FIXTURE_USER_INPUT, - ) + mock_devops_client.authorize.return_value = False + mock_devops_client.authorized = False - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "reauth" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_REAUTH}, + data=FIXTURE_USER_INPUT, + ) - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - FIXTURE_REAUTH_INPUT, - ) - await hass.async_block_till_done() + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "reauth" - assert result2["type"] == data_entry_flow.FlowResultType.FORM - assert result2["step_id"] == "reauth" - assert result2["errors"] == {"base": "invalid_auth"} + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + FIXTURE_REAUTH_INPUT, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["step_id"] == "reauth" + assert result2["errors"] == {"base": "invalid_auth"} -async def test_connection_error(hass: HomeAssistant) -> None: +async def test_connection_error( + hass: HomeAssistant, + mock_devops_client: AsyncMock, +) -> None: """Test we show user form on Azure DevOps connection error.""" - with patch( - "homeassistant.components.azure_devops.config_flow.DevOpsClient.authorize", - side_effect=aiohttp.ClientError, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) + mock_devops_client.authorize.side_effect = aiohttp.ClientError + mock_devops_client.authorized = False - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "user" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - FIXTURE_USER_INPUT, - ) - await hass.async_block_till_done() + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" - assert result2["type"] == data_entry_flow.FlowResultType.FORM - assert result2["step_id"] == "user" - assert result2["errors"] == {"base": "cannot_connect"} + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + FIXTURE_USER_INPUT, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "cannot_connect"} -async def test_reauth_connection_error(hass: HomeAssistant) -> None: +async def test_reauth_connection_error( + hass: HomeAssistant, + mock_devops_client: AsyncMock, +) -> None: """Test we show user form on Azure DevOps connection error.""" - with patch( - "homeassistant.components.azure_devops.config_flow.DevOpsClient.authorize", - side_effect=aiohttp.ClientError, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_REAUTH}, - data=FIXTURE_USER_INPUT, - ) + mock_devops_client.authorize.side_effect = aiohttp.ClientError + mock_devops_client.authorized = False - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "reauth" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_REAUTH}, + data=FIXTURE_USER_INPUT, + ) - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - FIXTURE_REAUTH_INPUT, - ) - await hass.async_block_till_done() + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "reauth" - assert result2["type"] == data_entry_flow.FlowResultType.FORM - assert result2["step_id"] == "reauth" - assert result2["errors"] == {"base": "cannot_connect"} + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + FIXTURE_REAUTH_INPUT, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["step_id"] == "reauth" + assert result2["errors"] == {"base": "cannot_connect"} -async def test_project_error(hass: HomeAssistant) -> None: +async def test_project_error( + hass: HomeAssistant, + mock_devops_client: AsyncMock, +) -> None: """Test we show user form on Azure DevOps connection error.""" - with ( - patch( - "homeassistant.components.azure_devops.config_flow.DevOpsClient.authorized", - return_value=True, - ), - patch( - "homeassistant.components.azure_devops.config_flow.DevOpsClient.authorize", - ), - patch( - "homeassistant.components.azure_devops.config_flow.DevOpsClient.get_project", - return_value=None, - ), - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) + mock_devops_client.authorize.return_value = True + mock_devops_client.authorized = True + mock_devops_client.get_project.return_value = None - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "user" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - FIXTURE_USER_INPUT, - ) - await hass.async_block_till_done() + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" - assert result2["type"] == data_entry_flow.FlowResultType.FORM - assert result2["step_id"] == "user" - assert result2["errors"] == {"base": "project_error"} + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + FIXTURE_USER_INPUT, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "project_error"} -async def test_reauth_project_error(hass: HomeAssistant) -> None: +async def test_reauth_project_error( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_devops_client: AsyncMock, +) -> None: """Test we show user form on Azure DevOps project error.""" - with ( - patch( - "homeassistant.components.azure_devops.config_flow.DevOpsClient.authorize", - ), - patch( - "homeassistant.components.azure_devops.config_flow.DevOpsClient.authorized", - return_value=True, - ), - patch( - "homeassistant.components.azure_devops.config_flow.DevOpsClient.get_project", - return_value=None, - ), - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_REAUTH}, - data=FIXTURE_USER_INPUT, - ) + mock_devops_client.authorize.return_value = True + mock_devops_client.authorized = True + mock_devops_client.get_project.return_value = None - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "reauth" + mock_config_entry.add_to_hass(hass) - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - FIXTURE_REAUTH_INPUT, - ) - await hass.async_block_till_done() + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_REAUTH}, + data=FIXTURE_USER_INPUT, + ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM - assert result2["step_id"] == "reauth" - assert result2["errors"] == {"base": "project_error"} + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "reauth" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + FIXTURE_REAUTH_INPUT, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["step_id"] == "reauth" + assert result2["errors"] == {"base": "project_error"} -async def test_reauth_flow(hass: HomeAssistant) -> None: +async def test_reauth_flow( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_devops_client: AsyncMock, +) -> None: """Test reauth works.""" - with patch( - "homeassistant.components.azure_devops.config_flow.DevOpsClient.authorize", - return_value=False, - ): - mock_config = MockConfigEntry( - domain=DOMAIN, unique_id=UNIQUE_ID, data=FIXTURE_USER_INPUT - ) - mock_config.add_to_hass(hass) + mock_devops_client.authorize.return_value = False + mock_devops_client.authorized = False - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_REAUTH}, - data=FIXTURE_USER_INPUT, - ) + mock_config_entry.add_to_hass(hass) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "reauth" - assert result["errors"] == {"base": "invalid_auth"} + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_REAUTH}, + data=FIXTURE_USER_INPUT, + ) - with ( - patch( - "homeassistant.components.azure_devops.config_flow.DevOpsClient.authorize", - ), - patch( - "homeassistant.components.azure_devops.config_flow.DevOpsClient.authorized", - return_value=True, - ), - patch( - "homeassistant.components.azure_devops.config_flow.DevOpsClient.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"], - FIXTURE_REAUTH_INPUT, - ) - await hass.async_block_till_done() + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "reauth" + assert result["errors"] == {"base": "invalid_auth"} - assert result2["type"] == data_entry_flow.FlowResultType.ABORT - assert result2["reason"] == "reauth_successful" + 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"], + FIXTURE_REAUTH_INPUT, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" -async def test_full_flow_implementation(hass: HomeAssistant) -> None: +async def test_full_flow_implementation( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_devops_client: AsyncMock, +) -> None: """Test registering an integration and finishing flow works.""" - with ( - patch( - "homeassistant.components.azure_devops.async_setup_entry", - return_value=True, - ) as mock_setup_entry, - patch( - "homeassistant.components.azure_devops.config_flow.DevOpsClient.authorized", - return_value=True, - ), - patch( - "homeassistant.components.azure_devops.config_flow.DevOpsClient.authorize", - ), - patch( - "homeassistant.components.azure_devops.config_flow.DevOpsClient.get_project", - return_value=DevOpsProject( - "abcd-abcd-abcd-abcd", FIXTURE_USER_INPUT[CONF_PROJECT] - ), - ), - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "user" + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - FIXTURE_USER_INPUT, - ) - await hass.async_block_till_done() - assert len(mock_setup_entry.mock_calls) == 1 + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + FIXTURE_USER_INPUT, + ) + await hass.async_block_till_done() + assert len(mock_setup_entry.mock_calls) == 1 - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert ( - result2["title"] - == f"{FIXTURE_USER_INPUT[CONF_ORG]}/{FIXTURE_USER_INPUT[CONF_PROJECT]}" - ) - assert result2["data"][CONF_ORG] == FIXTURE_USER_INPUT[CONF_ORG] - assert result2["data"][CONF_PROJECT] == FIXTURE_USER_INPUT[CONF_PROJECT] + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert ( + result2["title"] + == f"{FIXTURE_USER_INPUT[CONF_ORG]}/{FIXTURE_USER_INPUT[CONF_PROJECT]}" + ) + assert result2["data"][CONF_ORG] == FIXTURE_USER_INPUT[CONF_ORG] + assert result2["data"][CONF_PROJECT] == FIXTURE_USER_INPUT[CONF_PROJECT] diff --git a/tests/components/azure_devops/test_init.py b/tests/components/azure_devops/test_init.py new file mode 100644 index 00000000000..58e3621914d --- /dev/null +++ b/tests/components/azure_devops/test_init.py @@ -0,0 +1,78 @@ +"""Tests for init of Azure DevOps.""" + +from unittest.mock import AsyncMock, MagicMock + +import aiohttp + +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_config_entry: MockConfigEntry, + mock_devops_client: MagicMock, +) -> None: + """Test a successful setup entry.""" + assert await setup_integration(hass, mock_config_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_config_entry.state == ConfigEntryState.LOADED + + await hass.config_entries.async_remove(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state == ConfigEntryState.NOT_LOADED + + +async def test_auth_failed( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_devops_client: AsyncMock, +) -> None: + """Test a failed setup entry.""" + mock_devops_client.authorize.return_value = False + mock_devops_client.authorized = False + + await setup_integration(hass, mock_config_entry) + + assert not mock_devops_client.authorized + + assert mock_config_entry.state == ConfigEntryState.SETUP_ERROR + + +async def test_update_failed( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_devops_client: MagicMock, +) -> None: + """Test a failed update entry.""" + mock_devops_client.get_builds.side_effect = aiohttp.ClientError + + await setup_integration(hass, mock_config_entry) + + assert mock_devops_client.get_builds.call_count == 1 + + assert mock_config_entry.state == ConfigEntryState.SETUP_RETRY + + +async def test_no_builds( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_devops_client: MagicMock, +) -> None: + """Test a failed update entry.""" + mock_devops_client.get_builds.return_value = None + + await setup_integration(hass, mock_config_entry) + + assert mock_devops_client.get_builds.call_count == 1 + + assert mock_config_entry.state == ConfigEntryState.SETUP_RETRY diff --git a/tests/components/azure_devops/test_sensor.py b/tests/components/azure_devops/test_sensor.py new file mode 100644 index 00000000000..1c518d919c2 --- /dev/null +++ b/tests/components/azure_devops/test_sensor.py @@ -0,0 +1,33 @@ +"""Tests for init of Azure DevOps.""" + +from unittest.mock import AsyncMock + +import pytest +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 + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_sensors( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, + mock_devops_client: AsyncMock, +) -> None: + """Test the sensor entities.""" + assert await setup_integration(hass, mock_config_entry) + + assert ( + entry := entity_registry.async_get("sensor.testproject_test_build_latest_build") + ) + + assert entry == snapshot(name=f"{entry.entity_id}-entry") + + assert hass.states.get(entry.entity_id) == snapshot(name=f"{entry.entity_id}-state") From bf9627ad07f660ecdefef464778ada68ebe04359 Mon Sep 17 00:00:00 2001 From: Cyrill Raccaud Date: Tue, 2 Apr 2024 21:35:11 +0200 Subject: [PATCH 182/967] Add extra sensors to Swiss Public Transport (#114636) * convert extra_state_attributes to sensors * add deprecation notice for extra state attributes * cleanup after comments * remove exists_fn as it does not add value * move function outside the class --- .../swiss_public_transport/coordinator.py | 15 +++-- .../swiss_public_transport/icons.json | 22 ++++++- .../swiss_public_transport/sensor.py | 59 ++++++++++++------- .../swiss_public_transport/strings.json | 12 ++++ 4 files changed, 80 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/swiss_public_transport/coordinator.py b/homeassistant/components/swiss_public_transport/coordinator.py index 7df593d5667..eb6ab9c6017 100644 --- a/homeassistant/components/swiss_public_transport/coordinator.py +++ b/homeassistant/components/swiss_public_transport/coordinator.py @@ -25,16 +25,24 @@ class DataConnection(TypedDict): departure: datetime | None next_departure: datetime | None next_on_departure: datetime | None - duration: str + duration: int | None platform: str remaining_time: str start: str destination: str train_number: str - transfers: str + transfers: int delay: int +def calculate_duration_in_seconds(duration_text: str) -> int | None: + """Transform and calculate the duration into seconds.""" + # Transform 01d03:21:23 into 01 days 03:21:23 + duration_text_pg_format = duration_text.replace("d", " days ") + duration = dt_util.parse_duration(duration_text_pg_format) + return duration.seconds if duration else None + + class SwissPublicTransportDataUpdateCoordinator( DataUpdateCoordinator[list[DataConnection]] ): @@ -77,7 +85,6 @@ class SwissPublicTransportDataUpdateCoordinator( raise UpdateFailed from e connections = self._opendata.connections - return [ DataConnection( departure=self.nth_departure_time(i), @@ -86,7 +93,7 @@ class SwissPublicTransportDataUpdateCoordinator( train_number=connections[i]["number"], platform=connections[i]["platform"], transfers=connections[i]["transfers"], - duration=connections[i]["duration"], + duration=calculate_duration_in_seconds(connections[i]["duration"]), start=self._opendata.from_name, destination=self._opendata.to_name, remaining_time=str(self.remaining_time(connections[i]["departure"])), diff --git a/homeassistant/components/swiss_public_transport/icons.json b/homeassistant/components/swiss_public_transport/icons.json index fac54b10809..10573b8f5c3 100644 --- a/homeassistant/components/swiss_public_transport/icons.json +++ b/homeassistant/components/swiss_public_transport/icons.json @@ -1,8 +1,26 @@ { "entity": { "sensor": { - "departure": { - "default": "mdi:bus" + "departure0": { + "default": "mdi:bus-clock" + }, + "departure1": { + "default": "mdi:bus-clock" + }, + "departure2": { + "default": "mdi:bus-clock" + }, + "duration": { + "default": "mdi:timeline-clock" + }, + "transfers": { + "default": "mdi:transit-transfer" + }, + "platform": { + "default": "mdi:bus-stop-uncovered" + }, + "delay": { + "default": "mdi:clock-plus" } } } diff --git a/homeassistant/components/swiss_public_transport/sensor.py b/homeassistant/components/swiss_public_transport/sensor.py index a4a9605a603..f477c04f6ec 100644 --- a/homeassistant/components/swiss_public_transport/sensor.py +++ b/homeassistant/components/swiss_public_transport/sensor.py @@ -18,14 +18,14 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.const import CONF_NAME +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.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 homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( @@ -55,11 +55,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( class SwissPublicTransportSensorEntityDescription(SensorEntityDescription): """Describes swiss public transport sensor entity.""" - exists_fn: Callable[[DataConnection], bool] - value_fn: Callable[[DataConnection], datetime | None] + value_fn: Callable[[DataConnection], StateType | datetime] - index: int - has_legacy_attributes: bool + index: int = 0 + has_legacy_attributes: bool = False SENSORS: tuple[SwissPublicTransportSensorEntityDescription, ...] = ( @@ -70,11 +69,33 @@ SENSORS: tuple[SwissPublicTransportSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.TIMESTAMP, has_legacy_attributes=i == 0, value_fn=lambda data_connection: data_connection["departure"], - exists_fn=lambda data_connection: data_connection is not None, index=i, ) for i in range(SENSOR_CONNECTIONS_COUNT) ], + SwissPublicTransportSensorEntityDescription( + key="duration", + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=UnitOfTime.SECONDS, + value_fn=lambda data_connection: data_connection["duration"], + ), + SwissPublicTransportSensorEntityDescription( + key="transfers", + translation_key="transfers", + value_fn=lambda data_connection: data_connection["transfers"], + ), + SwissPublicTransportSensorEntityDescription( + key="platform", + translation_key="platform", + value_fn=lambda data_connection: data_connection["platform"], + ), + SwissPublicTransportSensorEntityDescription( + key="delay", + translation_key="delay", + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=UnitOfTime.MINUTES, + value_fn=lambda data_connection: data_connection["delay"], + ), ) @@ -167,14 +188,7 @@ class SwissPublicTransportSensor( ) @property - def enabled(self) -> bool: - """Enable the sensor if data is available.""" - return self.entity_description.exists_fn( - self.coordinator.data[self.entity_description.index] - ) - - @property - def native_value(self) -> datetime | None: + def native_value(self) -> StateType | datetime: """Return the state of the sensor.""" return self.entity_description.value_fn( self.coordinator.data[self.entity_description.index] @@ -196,10 +210,11 @@ class SwissPublicTransportSensor( @callback def _async_update_attrs(self) -> None: """Update the extra state attributes based on the coordinator data.""" - self._attr_extra_state_attributes = { - key: value - for key, value in self.coordinator.data[ - self.entity_description.index - ].items() - if key not in {"departure"} - } + 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"} + } diff --git a/homeassistant/components/swiss_public_transport/strings.json b/homeassistant/components/swiss_public_transport/strings.json index c080e785f2c..0a3114c914f 100644 --- a/homeassistant/components/swiss_public_transport/strings.json +++ b/homeassistant/components/swiss_public_transport/strings.json @@ -32,6 +32,18 @@ }, "departure2": { "name": "Departure +2" + }, + "duration": { + "name": "Duration" + }, + "transfers": { + "name": "Transfers" + }, + "platform": { + "name": "Platform" + }, + "delay": { + "name": "Delay" } } }, From 448f8a956887a7a732ae0888b133bc227446a570 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 2 Apr 2024 22:47:04 +0300 Subject: [PATCH 183/967] Refactor setup code in command_line (#114661) * Refactor setup code in command_line * Fix rebase * Review comments --- .../components/command_line/binary_sensor.py | 36 +++++---------- .../components/command_line/const.py | 23 +++++++++- .../components/command_line/cover.py | 36 +++++++-------- .../components/command_line/sensor.py | 40 +++++------------ .../components/command_line/switch.py | 44 ++++++++----------- 5 files changed, 75 insertions(+), 104 deletions(-) diff --git a/homeassistant/components/command_line/binary_sensor.py b/homeassistant/components/command_line/binary_sensor.py index a31f8205a28..2ff17e86efd 100644 --- a/homeassistant/components/command_line/binary_sensor.py +++ b/homeassistant/components/command_line/binary_sensor.py @@ -6,33 +6,24 @@ import asyncio from datetime import datetime, timedelta from typing import cast -from homeassistant.components.binary_sensor import ( - BinarySensorDeviceClass, - BinarySensorEntity, -) +from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.const import ( CONF_COMMAND, - CONF_DEVICE_CLASS, - CONF_ICON, CONF_NAME, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, CONF_SCAN_INTERVAL, - CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.template import Template -from homeassistant.helpers.trigger_template_entity import ( - CONF_AVAILABILITY, - ManualTriggerEntity, -) +from homeassistant.helpers.trigger_template_entity import ManualTriggerEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util -from .const import CONF_COMMAND_TIMEOUT, LOGGER +from .const import CONF_COMMAND_TIMEOUT, LOGGER, TRIGGER_ENTITY_OPTIONS from .sensor import CommandSensorData DEFAULT_NAME = "Binary Command Sensor" @@ -53,31 +44,24 @@ async def async_setup_platform( discovery_info = cast(DiscoveryInfoType, discovery_info) binary_sensor_config = discovery_info - name: str = binary_sensor_config.get(CONF_NAME, DEFAULT_NAME) command: str = binary_sensor_config[CONF_COMMAND] payload_off: str = binary_sensor_config[CONF_PAYLOAD_OFF] payload_on: str = binary_sensor_config[CONF_PAYLOAD_ON] - device_class: BinarySensorDeviceClass | None = binary_sensor_config.get( - CONF_DEVICE_CLASS - ) - icon: Template | None = binary_sensor_config.get(CONF_ICON) - value_template: Template | None = binary_sensor_config.get(CONF_VALUE_TEMPLATE) command_timeout: int = binary_sensor_config[CONF_COMMAND_TIMEOUT] - unique_id: str | None = binary_sensor_config.get(CONF_UNIQUE_ID) scan_interval: timedelta = binary_sensor_config.get( CONF_SCAN_INTERVAL, SCAN_INTERVAL ) - availability: Template | None = binary_sensor_config.get(CONF_AVAILABILITY) - if value_template is not None: + + if value_template := binary_sensor_config.get(CONF_VALUE_TEMPLATE): value_template.hass = hass + data = CommandSensorData(hass, command, command_timeout) trigger_entity_config = { - CONF_UNIQUE_ID: unique_id, - CONF_NAME: Template(name, hass), - CONF_DEVICE_CLASS: device_class, - CONF_ICON: icon, - CONF_AVAILABILITY: availability, + CONF_NAME: Template(binary_sensor_config.get(CONF_NAME, DEFAULT_NAME), hass), + **{ + k: v for k, v in binary_sensor_config.items() if k in TRIGGER_ENTITY_OPTIONS + }, } async_add_entities( diff --git a/homeassistant/components/command_line/const.py b/homeassistant/components/command_line/const.py index ff51cb7e331..0448180dc33 100644 --- a/homeassistant/components/command_line/const.py +++ b/homeassistant/components/command_line/const.py @@ -2,7 +2,18 @@ import logging -from homeassistant.const import Platform +from homeassistant.components.sensor import CONF_STATE_CLASS +from homeassistant.const import ( + CONF_DEVICE_CLASS, + CONF_ICON, + CONF_UNIQUE_ID, + CONF_UNIT_OF_MEASUREMENT, + Platform, +) +from homeassistant.helpers.trigger_template_entity import ( + CONF_AVAILABILITY, + CONF_PICTURE, +) LOGGER = logging.getLogger(__package__) @@ -15,3 +26,13 @@ PLATFORMS = [ Platform.SENSOR, Platform.SWITCH, ] + +TRIGGER_ENTITY_OPTIONS = { + CONF_AVAILABILITY, + CONF_DEVICE_CLASS, + CONF_ICON, + CONF_PICTURE, + CONF_STATE_CLASS, + CONF_UNIQUE_ID, + CONF_UNIT_OF_MEASUREMENT, +} diff --git a/homeassistant/components/command_line/cover.py b/homeassistant/components/command_line/cover.py index 85c0ab605c7..6400be7d92f 100644 --- a/homeassistant/components/command_line/cover.py +++ b/homeassistant/components/command_line/cover.py @@ -12,24 +12,19 @@ from homeassistant.const import ( CONF_COMMAND_OPEN, CONF_COMMAND_STATE, CONF_COMMAND_STOP, - CONF_ICON, CONF_NAME, CONF_SCAN_INTERVAL, - CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.template import Template -from homeassistant.helpers.trigger_template_entity import ( - CONF_AVAILABILITY, - ManualTriggerEntity, -) +from homeassistant.helpers.trigger_template_entity import ManualTriggerEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util, slugify -from .const import CONF_COMMAND_TIMEOUT, LOGGER +from .const import CONF_COMMAND_TIMEOUT, LOGGER, TRIGGER_ENTITY_OPTIONS from .utils import async_call_shell_with_timeout, async_check_output_or_log SCAN_INTERVAL = timedelta(seconds=15) @@ -45,30 +40,29 @@ async def async_setup_platform( covers = [] discovery_info = cast(DiscoveryInfoType, discovery_info) - entities: dict[str, Any] = {slugify(discovery_info[CONF_NAME]): discovery_info} + entities: dict[str, dict[str, Any]] = { + slugify(discovery_info[CONF_NAME]): discovery_info + } - for device_name, device_config in entities.items(): - value_template: Template | None = device_config.get(CONF_VALUE_TEMPLATE) - if value_template is not None: + for device_name, cover_config in entities.items(): + if value_template := cover_config.get(CONF_VALUE_TEMPLATE): value_template.hass = hass trigger_entity_config = { - CONF_UNIQUE_ID: device_config.get(CONF_UNIQUE_ID), - CONF_NAME: Template(device_config.get(CONF_NAME, device_name), hass), - CONF_ICON: device_config.get(CONF_ICON), - CONF_AVAILABILITY: device_config.get(CONF_AVAILABILITY), + CONF_NAME: Template(cover_config.get(CONF_NAME, device_name), hass), + **{k: v for k, v in cover_config.items() if k in TRIGGER_ENTITY_OPTIONS}, } covers.append( CommandCover( trigger_entity_config, - device_config[CONF_COMMAND_OPEN], - device_config[CONF_COMMAND_CLOSE], - device_config[CONF_COMMAND_STOP], - device_config.get(CONF_COMMAND_STATE), + cover_config[CONF_COMMAND_OPEN], + cover_config[CONF_COMMAND_CLOSE], + cover_config[CONF_COMMAND_STOP], + cover_config.get(CONF_COMMAND_STATE), value_template, - device_config[CONF_COMMAND_TIMEOUT], - device_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL), + cover_config[CONF_COMMAND_TIMEOUT], + cover_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL), ) ) diff --git a/homeassistant/components/command_line/sensor.py b/homeassistant/components/command_line/sensor.py index 4cfd9af0811..b0c2ca7cb66 100644 --- a/homeassistant/components/command_line/sensor.py +++ b/homeassistant/components/command_line/sensor.py @@ -8,16 +8,12 @@ from datetime import datetime, timedelta import json from typing import Any, cast -from homeassistant.components.sensor import CONF_STATE_CLASS, SensorDeviceClass +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.components.sensor.helpers import async_parse_date_datetime from homeassistant.const import ( CONF_COMMAND, - CONF_DEVICE_CLASS, - CONF_ICON, CONF_NAME, CONF_SCAN_INTERVAL, - CONF_UNIQUE_ID, - CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, ) from homeassistant.core import HomeAssistant @@ -25,31 +21,17 @@ from homeassistant.exceptions import TemplateError from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.template import Template -from homeassistant.helpers.trigger_template_entity import ( - CONF_AVAILABILITY, - CONF_PICTURE, - ManualTriggerSensorEntity, -) +from homeassistant.helpers.trigger_template_entity import ManualTriggerSensorEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util -from .const import CONF_COMMAND_TIMEOUT, LOGGER +from .const import CONF_COMMAND_TIMEOUT, LOGGER, TRIGGER_ENTITY_OPTIONS from .utils import async_check_output_or_log CONF_JSON_ATTRIBUTES = "json_attributes" DEFAULT_NAME = "Command Sensor" -TRIGGER_ENTITY_OPTIONS = ( - CONF_AVAILABILITY, - CONF_DEVICE_CLASS, - CONF_ICON, - CONF_PICTURE, - CONF_UNIQUE_ID, - CONF_STATE_CLASS, - CONF_UNIT_OF_MEASUREMENT, -) - SCAN_INTERVAL = timedelta(seconds=60) @@ -64,21 +46,19 @@ async def async_setup_platform( discovery_info = cast(DiscoveryInfoType, discovery_info) sensor_config = discovery_info - name: str = sensor_config[CONF_NAME] command: str = sensor_config[CONF_COMMAND] - value_template: Template | None = sensor_config.get(CONF_VALUE_TEMPLATE) command_timeout: int = sensor_config[CONF_COMMAND_TIMEOUT] - if value_template is not None: - value_template.hass = hass json_attributes: list[str] | None = sensor_config.get(CONF_JSON_ATTRIBUTES) scan_interval: timedelta = sensor_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) data = CommandSensorData(hass, command, command_timeout) - trigger_entity_config = {CONF_NAME: Template(name, hass)} - for key in TRIGGER_ENTITY_OPTIONS: - if key not in sensor_config: - continue - trigger_entity_config[key] = sensor_config[key] + if value_template := sensor_config.get(CONF_VALUE_TEMPLATE): + value_template.hass = hass + + trigger_entity_config = { + CONF_NAME: Template(sensor_config[CONF_NAME], hass), + **{k: v for k, v in sensor_config.items() if k in TRIGGER_ENTITY_OPTIONS}, + } async_add_entities( [ diff --git a/homeassistant/components/command_line/switch.py b/homeassistant/components/command_line/switch.py index f84c55d0320..fee94424fa1 100644 --- a/homeassistant/components/command_line/switch.py +++ b/homeassistant/components/command_line/switch.py @@ -11,24 +11,19 @@ from homeassistant.const import ( CONF_COMMAND_OFF, CONF_COMMAND_ON, CONF_COMMAND_STATE, - CONF_ICON, CONF_NAME, CONF_SCAN_INTERVAL, - CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.template import Template -from homeassistant.helpers.trigger_template_entity import ( - CONF_AVAILABILITY, - ManualTriggerEntity, -) +from homeassistant.helpers.trigger_template_entity import ManualTriggerEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util, slugify -from .const import CONF_COMMAND_TIMEOUT, LOGGER +from .const import CONF_COMMAND_TIMEOUT, LOGGER, TRIGGER_ENTITY_OPTIONS from .utils import async_call_shell_with_timeout, async_check_output_or_log SCAN_INTERVAL = timedelta(seconds=30) @@ -42,34 +37,31 @@ async def async_setup_platform( ) -> None: """Find and return switches controlled by shell commands.""" - discovery_info = cast(DiscoveryInfoType, discovery_info) - entities: dict[str, Any] = {slugify(discovery_info[CONF_NAME]): discovery_info} - switches = [] + discovery_info = cast(DiscoveryInfoType, discovery_info) + entities: dict[str, dict[str, Any]] = { + slugify(discovery_info[CONF_NAME]): discovery_info + } - for object_id, device_config in entities.items(): - trigger_entity_config = { - CONF_UNIQUE_ID: device_config.get(CONF_UNIQUE_ID), - CONF_NAME: Template(device_config.get(CONF_NAME, object_id), hass), - CONF_ICON: device_config.get(CONF_ICON), - CONF_AVAILABILITY: device_config.get(CONF_AVAILABILITY), - } - - value_template: Template | None = device_config.get(CONF_VALUE_TEMPLATE) - - if value_template is not None: + for object_id, switch_config in entities.items(): + if value_template := switch_config.get(CONF_VALUE_TEMPLATE): value_template.hass = hass + trigger_entity_config = { + CONF_NAME: Template(switch_config.get(CONF_NAME, object_id), hass), + **{k: v for k, v in switch_config.items() if k in TRIGGER_ENTITY_OPTIONS}, + } + switches.append( CommandSwitch( trigger_entity_config, object_id, - device_config[CONF_COMMAND_ON], - device_config[CONF_COMMAND_OFF], - device_config.get(CONF_COMMAND_STATE), + switch_config[CONF_COMMAND_ON], + switch_config[CONF_COMMAND_OFF], + switch_config.get(CONF_COMMAND_STATE), value_template, - device_config[CONF_COMMAND_TIMEOUT], - device_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL), + switch_config[CONF_COMMAND_TIMEOUT], + switch_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL), ) ) From 2175cd6039e7639b43336e81e1a74486c81e6ac4 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 2 Apr 2024 21:49:53 +0200 Subject: [PATCH 184/967] Add tests for Roomba Options flow (#114666) * Add tests for Roomba Options flow * Fix --- tests/components/roomba/test_config_flow.py | 178 ++++++++++++-------- 1 file changed, 109 insertions(+), 69 deletions(-) diff --git a/tests/components/roomba/test_config_flow.py b/tests/components/roomba/test_config_flow.py index 282884c0be3..e097a7bd0ea 100644 --- a/tests/components/roomba/test_config_flow.py +++ b/tests/components/roomba/test_config_flow.py @@ -6,12 +6,18 @@ from unittest.mock import MagicMock, PropertyMock, patch import pytest from roombapy import RoombaConnectionError, RoombaInfo -from homeassistant import config_entries, data_entry_flow from homeassistant.components import dhcp, zeroconf from homeassistant.components.roomba import config_flow from homeassistant.components.roomba.const import CONF_BLID, CONF_CONTINUOUS, DOMAIN +from homeassistant.config_entries import ( + SOURCE_DHCP, + SOURCE_IGNORE, + SOURCE_USER, + SOURCE_ZEROCONF, +) from homeassistant.const import CONF_DELAY, CONF_HOST, CONF_PASSWORD from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -20,7 +26,7 @@ VALID_CONFIG = {CONF_HOST: MOCK_IP, CONF_BLID: "BLID", CONF_PASSWORD: "password" DISCOVERY_DEVICES = [ ( - config_entries.SOURCE_DHCP, + SOURCE_DHCP, dhcp.DhcpServiceInfo( ip=MOCK_IP, macaddress="501479ddeeff", @@ -28,7 +34,7 @@ DISCOVERY_DEVICES = [ ), ), ( - config_entries.SOURCE_DHCP, + SOURCE_DHCP, dhcp.DhcpServiceInfo( ip=MOCK_IP, macaddress="80a589ddeeff", @@ -36,7 +42,7 @@ DISCOVERY_DEVICES = [ ), ), ( - config_entries.SOURCE_ZEROCONF, + SOURCE_ZEROCONF, zeroconf.ZeroconfServiceInfo( ip_address=ip_address(MOCK_IP), ip_addresses=[ip_address(MOCK_IP)], @@ -48,7 +54,7 @@ DISCOVERY_DEVICES = [ ), ), ( - config_entries.SOURCE_ZEROCONF, + SOURCE_ZEROCONF, zeroconf.ZeroconfServiceInfo( ip_address=ip_address(MOCK_IP), ip_addresses=[ip_address(MOCK_IP)], @@ -157,11 +163,11 @@ async def test_form_user_discovery_and_password_fetch(hass: HomeAssistant) -> No "homeassistant.components.roomba.config_flow.RoombaDiscovery", _mocked_discovery ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -170,7 +176,7 @@ async def test_form_user_discovery_and_password_fetch(hass: HomeAssistant) -> No {CONF_HOST: MOCK_IP}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] is None assert result2["step_id"] == "link" @@ -194,7 +200,7 @@ async def test_form_user_discovery_and_password_fetch(hass: HomeAssistant) -> No ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "robot_name" assert result3["result"].unique_id == "BLID" assert result3["data"] == { @@ -217,11 +223,11 @@ async def test_form_user_discovery_skips_known(hass: HomeAssistant) -> None: "homeassistant.components.roomba.config_flow.RoombaDiscovery", _mocked_discovery ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "manual" @@ -239,11 +245,11 @@ async def test_form_user_no_devices_found_discovery_aborts_already_configured( _mocked_no_devices_found_discovery, ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "manual" @@ -252,7 +258,7 @@ async def test_form_user_no_devices_found_discovery_aborts_already_configured( {CONF_HOST: MOCK_IP}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -270,11 +276,11 @@ async def test_form_user_discovery_manual_and_auto_password_fetch( "homeassistant.components.roomba.config_flow.RoombaDiscovery", _mocked_discovery ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -283,7 +289,7 @@ async def test_form_user_discovery_manual_and_auto_password_fetch( {CONF_HOST: None}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] is None assert result2["step_id"] == "manual" @@ -296,7 +302,7 @@ async def test_form_user_discovery_manual_and_auto_password_fetch( ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["errors"] is None with ( @@ -319,7 +325,7 @@ async def test_form_user_discovery_manual_and_auto_password_fetch( ) await hass.async_block_till_done() - assert result4["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == "robot_name" assert result4["result"].unique_id == "BLID" assert result4["data"] == { @@ -345,11 +351,11 @@ async def test_form_user_discover_fails_aborts_already_configured( _mocked_failed_discovery, ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "manual" @@ -358,7 +364,7 @@ async def test_form_user_discover_fails_aborts_already_configured( {CONF_HOST: MOCK_IP}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -371,11 +377,11 @@ async def test_form_user_discovery_manual_and_auto_password_fetch_but_cannot_con "homeassistant.components.roomba.config_flow.RoombaDiscovery", _mocked_discovery ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -384,7 +390,7 @@ async def test_form_user_discovery_manual_and_auto_password_fetch_but_cannot_con {CONF_HOST: None}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] is None assert result2["step_id"] == "manual" @@ -398,7 +404,7 @@ async def test_form_user_discovery_manual_and_auto_password_fetch_but_cannot_con ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "cannot_connect" @@ -417,11 +423,11 @@ async def test_form_user_discovery_no_devices_found_and_auto_password_fetch( _mocked_no_devices_found_discovery, ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "manual" @@ -433,7 +439,7 @@ async def test_form_user_discovery_no_devices_found_and_auto_password_fetch( {CONF_HOST: MOCK_IP}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] is None with ( @@ -456,7 +462,7 @@ async def test_form_user_discovery_no_devices_found_and_auto_password_fetch( ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "robot_name" assert result3["result"].unique_id == "BLID" assert result3["data"] == { @@ -484,11 +490,11 @@ async def test_form_user_discovery_no_devices_found_and_password_fetch_fails( _mocked_no_devices_found_discovery, ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "manual" @@ -500,7 +506,7 @@ async def test_form_user_discovery_no_devices_found_and_password_fetch_fails( {CONF_HOST: MOCK_IP}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] is None with patch( @@ -529,7 +535,7 @@ async def test_form_user_discovery_no_devices_found_and_password_fetch_fails( ) await hass.async_block_till_done() - assert result4["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == "myroomba" assert result4["result"].unique_id == "BLID" assert result4["data"] == { @@ -558,11 +564,11 @@ async def test_form_user_discovery_not_devices_found_and_password_fetch_fails_an _mocked_no_devices_found_discovery, ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "manual" @@ -574,7 +580,7 @@ async def test_form_user_discovery_not_devices_found_and_password_fetch_fails_an {CONF_HOST: MOCK_IP}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] is None with patch( @@ -603,7 +609,7 @@ async def test_form_user_discovery_not_devices_found_and_password_fetch_fails_an ) await hass.async_block_till_done() - assert result4["type"] == data_entry_flow.FlowResultType.FORM + assert result4["type"] is FlowResultType.FORM assert result4["errors"] == {"base": "cannot_connect"} assert len(mock_setup_entry.mock_calls) == 0 @@ -622,11 +628,11 @@ async def test_form_user_discovery_and_password_fetch_gets_connection_refused( "homeassistant.components.roomba.config_flow.RoombaDiscovery", _mocked_discovery ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -635,7 +641,7 @@ async def test_form_user_discovery_and_password_fetch_gets_connection_refused( {CONF_HOST: MOCK_IP}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] is None assert result2["step_id"] == "link" @@ -665,7 +671,7 @@ async def test_form_user_discovery_and_password_fetch_gets_connection_refused( ) await hass.async_block_till_done() - assert result4["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == "myroomba" assert result4["result"].unique_id == "BLID" assert result4["data"] == { @@ -701,7 +707,7 @@ async def test_dhcp_discovery_and_roomba_discovery_finds( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "link" assert result["description_placeholders"] == {"name": "robot_name"} @@ -726,7 +732,7 @@ async def test_dhcp_discovery_and_roomba_discovery_finds( ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "robot_name" assert result2["result"].unique_id == "BLID" assert result2["data"] == { @@ -755,12 +761,12 @@ async def test_dhcp_discovery_falls_back_to_manual( ): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DHCP}, + context={"source": SOURCE_DHCP}, data=discovery_data, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -769,7 +775,7 @@ async def test_dhcp_discovery_falls_back_to_manual( {}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] is None assert result2["step_id"] == "manual" @@ -781,7 +787,7 @@ async def test_dhcp_discovery_falls_back_to_manual( {CONF_HOST: MOCK_IP}, ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["errors"] is None with ( @@ -804,7 +810,7 @@ async def test_dhcp_discovery_falls_back_to_manual( ) await hass.async_block_till_done() - assert result4["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == "robot_name" assert result4["result"].unique_id == "BLID" assert result4["data"] == { @@ -834,12 +840,12 @@ async def test_dhcp_discovery_no_devices_falls_back_to_manual( ): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DHCP}, + context={"source": SOURCE_DHCP}, data=discovery_data, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "manual" @@ -851,7 +857,7 @@ async def test_dhcp_discovery_no_devices_falls_back_to_manual( {CONF_HOST: MOCK_IP}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] is None with ( @@ -874,7 +880,7 @@ async def test_dhcp_discovery_no_devices_falls_back_to_manual( ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "robot_name" assert result3["result"].unique_id == "BLID" assert result3["data"] == { @@ -890,9 +896,7 @@ async def test_dhcp_discovery_no_devices_falls_back_to_manual( async def test_dhcp_discovery_with_ignored(hass: HomeAssistant) -> None: """Test ignored entries do not break checking for existing entries.""" - config_entry = MockConfigEntry( - domain=DOMAIN, data={}, source=config_entries.SOURCE_IGNORE - ) + config_entry = MockConfigEntry(domain=DOMAIN, data={}, source=SOURCE_IGNORE) config_entry.add_to_hass(hass) with patch( @@ -900,7 +904,7 @@ async def test_dhcp_discovery_with_ignored(hass: HomeAssistant) -> None: ): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DHCP}, + context={"source": SOURCE_DHCP}, data=dhcp.DhcpServiceInfo( ip=MOCK_IP, macaddress="aabbccddeeff", @@ -909,7 +913,7 @@ async def test_dhcp_discovery_with_ignored(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM async def test_dhcp_discovery_already_configured_host(hass: HomeAssistant) -> None: @@ -923,7 +927,7 @@ async def test_dhcp_discovery_already_configured_host(hass: HomeAssistant) -> No ): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DHCP}, + context={"source": SOURCE_DHCP}, data=dhcp.DhcpServiceInfo( ip=MOCK_IP, macaddress="aabbccddeeff", @@ -932,7 +936,7 @@ async def test_dhcp_discovery_already_configured_host(hass: HomeAssistant) -> No ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -949,7 +953,7 @@ async def test_dhcp_discovery_already_configured_blid(hass: HomeAssistant) -> No ): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DHCP}, + context={"source": SOURCE_DHCP}, data=dhcp.DhcpServiceInfo( ip=MOCK_IP, macaddress="aabbccddeeff", @@ -958,7 +962,7 @@ async def test_dhcp_discovery_already_configured_blid(hass: HomeAssistant) -> No ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -975,7 +979,7 @@ async def test_dhcp_discovery_not_irobot(hass: HomeAssistant) -> None: ): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DHCP}, + context={"source": SOURCE_DHCP}, data=dhcp.DhcpServiceInfo( ip=MOCK_IP, macaddress="aabbccddeeff", @@ -984,7 +988,7 @@ async def test_dhcp_discovery_not_irobot(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_irobot_device" @@ -996,7 +1000,7 @@ async def test_dhcp_discovery_partial_hostname(hass: HomeAssistant) -> None: ): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DHCP}, + context={"source": SOURCE_DHCP}, data=dhcp.DhcpServiceInfo( ip=MOCK_IP, macaddress="aabbccddeeff", @@ -1013,7 +1017,7 @@ async def test_dhcp_discovery_partial_hostname(hass: HomeAssistant) -> None: ): result2 = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DHCP}, + context={"source": SOURCE_DHCP}, data=dhcp.DhcpServiceInfo( ip=MOCK_IP, macaddress="aabbccddeeff", @@ -1022,7 +1026,7 @@ async def test_dhcp_discovery_partial_hostname(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "link" current_flows = hass.config_entries.flow.async_progress() @@ -1034,7 +1038,7 @@ async def test_dhcp_discovery_partial_hostname(hass: HomeAssistant) -> None: ): result3 = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DHCP}, + context={"source": SOURCE_DHCP}, data=dhcp.DhcpServiceInfo( ip=MOCK_IP, macaddress="aabbccddeeff", @@ -1043,9 +1047,45 @@ async def test_dhcp_discovery_partial_hostname(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "abort" + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "short_blid" current_flows = hass.config_entries.flow.async_progress() assert len(current_flows) == 1 assert current_flows[0]["flow_id"] == result2["flow_id"] + + +async def test_options_flow( + hass: HomeAssistant, +) -> None: + """Test config flow options.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + data=VALID_CONFIG, + unique_id="BLID", + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.roomba.async_setup_entry", + return_value=True, + ): + 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) + await hass.async_block_till_done() + + 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_CONTINUOUS: True, CONF_DELAY: 1}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"] == {CONF_CONTINUOUS: True, CONF_DELAY: 1} + assert config_entry.options == {CONF_CONTINUOUS: True, CONF_DELAY: 1} From ea2bb2448452639488d7f2b16a69047794b6948d Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Tue, 2 Apr 2024 15:23:59 -0500 Subject: [PATCH 185/967] Remove old device tracker device cleanup code & test (#114668) --- .../components/device_tracker/config_entry.py | 22 ----- .../device_tracker/test_config_entry.py | 82 ------------------- 2 files changed, 104 deletions(-) diff --git a/homeassistant/components/device_tracker/config_entry.py b/homeassistant/components/device_tracker/config_entry.py index a1c1961dc43..99c152cd449 100644 --- a/homeassistant/components/device_tracker/config_entry.py +++ b/homeassistant/components/device_tracker/config_entry.py @@ -51,28 +51,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) component.register_shutdown() - # Clean up old devices created by device tracker entities in the past. - # Can be removed after 2022.6 - ent_reg = er.async_get(hass) - dev_reg = dr.async_get(hass) - - devices_with_trackers = set() - devices_with_non_trackers = set() - - for entity in ent_reg.entities.values(): - if entity.device_id is None: - continue - - if entity.domain == DOMAIN: - devices_with_trackers.add(entity.device_id) - else: - devices_with_non_trackers.add(entity.device_id) - - for device_id in devices_with_trackers - devices_with_non_trackers: - for entity in er.async_entries_for_device(ent_reg, device_id, True): - ent_reg.async_update_entity(entity.entity_id, device_id=None) - dev_reg.async_remove_device(device_id) - return await component.async_setup_entry(entry) diff --git a/tests/components/device_tracker/test_config_entry.py b/tests/components/device_tracker/test_config_entry.py index d8236c697c3..6a1731d5a77 100644 --- a/tests/components/device_tracker/test_config_entry.py +++ b/tests/components/device_tracker/test_config_entry.py @@ -581,88 +581,6 @@ def test_base_tracker_entity() -> None: assert entity.state_attributes is None -async def test_cleanup_legacy( - hass: HomeAssistant, - config_entry: MockConfigEntry, - entity_registry: er.EntityRegistry, - device_registry: dr.DeviceRegistry, -) -> None: - """Test we clean up devices created by old device tracker.""" - device_entry_1 = device_registry.async_get_or_create( - config_entry_id=config_entry.entry_id, identifiers={(DOMAIN, "device1")} - ) - device_entry_2 = device_registry.async_get_or_create( - config_entry_id=config_entry.entry_id, identifiers={(DOMAIN, "device2")} - ) - device_entry_3 = device_registry.async_get_or_create( - config_entry_id=config_entry.entry_id, identifiers={(DOMAIN, "device3")} - ) - - # Device with light + device tracker entity - entity_entry_1a = entity_registry.async_get_or_create( - DOMAIN, - "test", - "entity1a-unique", - config_entry=config_entry, - device_id=device_entry_1.id, - ) - entity_entry_1b = entity_registry.async_get_or_create( - "light", - "test", - "entity1b-unique", - config_entry=config_entry, - device_id=device_entry_1.id, - ) - # Just device tracker entity - entity_entry_2a = entity_registry.async_get_or_create( - DOMAIN, - "test", - "entity2a-unique", - config_entry=config_entry, - device_id=device_entry_2.id, - ) - # Device with no device tracker entities - entity_entry_3a = entity_registry.async_get_or_create( - "light", - "test", - "entity3a-unique", - config_entry=config_entry, - device_id=device_entry_3.id, - ) - # Device tracker but no device - entity_entry_4a = entity_registry.async_get_or_create( - DOMAIN, - "test", - "entity4a-unique", - config_entry=config_entry, - ) - # Completely different entity - entity_entry_5a = entity_registry.async_get_or_create( - "light", - "test", - "entity4a-unique", - config_entry=config_entry, - ) - - await create_mock_platform(hass, config_entry, []) - - for entity_entry in ( - entity_entry_1a, - entity_entry_1b, - entity_entry_3a, - entity_entry_4a, - entity_entry_5a, - ): - assert entity_registry.async_get(entity_entry.entity_id) is not None - - entity_entry = entity_registry.async_get(entity_entry_2a.entity_id) - assert entity_entry is not None - # We've removed device so device ID cleared - assert entity_entry.device_id is None - # Removed because only had device tracker entity - assert device_registry.async_get(device_entry_2.id) is None - - @pytest.mark.parametrize( ("mac_address", "unique_id"), [(TEST_MAC_ADDRESS, f"{TEST_MAC_ADDRESS}_yo1")] ) From 906d3198e3add1ecff6f70032abae3b684a2d237 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 2 Apr 2024 23:01:37 +0200 Subject: [PATCH 186/967] Use is in enum comparison in config flow tests F-J (#114670) * Use right enum expression F-J * Fix --- .../components/faa_delays/test_config_flow.py | 5 +- .../components/fastdotcom/test_config_flow.py | 8 +- tests/components/fibaro/test_config_flow.py | 36 ++-- tests/components/filesize/test_config_flow.py | 10 +- .../fireservicerota/test_config_flow.py | 13 +- tests/components/fitbit/test_config_flow.py | 26 +-- tests/components/fivem/test_config_flow.py | 12 +- .../fjaraskupan/test_config_flow.py | 8 +- .../flexit_bacnet/test_config_flow.py | 8 +- .../flick_electric/test_config_flow.py | 13 +- tests/components/flipr/test_config_flow.py | 11 +- tests/components/flux_led/test_config_flow.py | 26 +-- .../forecast_solar/test_config_flow.py | 16 +- .../forked_daapd/test_config_flow.py | 26 +-- tests/components/foscam/test_config_flow.py | 27 +-- tests/components/freebox/test_config_flow.py | 20 +-- .../components/freedompro/test_config_flow.py | 6 +- tests/components/fritz/test_config_flow.py | 49 +++--- tests/components/fritzbox/test_config_flow.py | 56 +++---- .../fritzbox_callmonitor/test_config_flow.py | 28 ++-- tests/components/fronius/test_config_flow.py | 26 +-- .../frontier_silicon/test_config_flow.py | 46 ++--- .../fully_kiosk/test_config_flow.py | 20 +-- tests/components/fyta/test_config_flow.py | 13 +- .../garages_amsterdam/test_config_flow.py | 6 +- tests/components/gdacs/test_config_flow.py | 9 +- tests/components/generic/test_config_flow.py | 68 ++++---- .../geo_json_events/test_config_flow.py | 8 +- .../components/geocaching/test_config_flow.py | 6 +- .../geonetnz_quakes/test_config_flow.py | 11 +- .../geonetnz_volcano/test_config_flow.py | 8 +- tests/components/gios/test_config_flow.py | 6 +- tests/components/github/test_config_flow.py | 14 +- tests/components/glances/test_config_flow.py | 18 +- tests/components/goalzero/test_config_flow.py | 24 +-- .../components/gogogate2/test_config_flow.py | 26 +-- tests/components/goodwe/test_config_flow.py | 12 +- tests/components/google/test_config_flow.py | 4 +- .../test_config_flow.py | 8 +- .../google_tasks/test_config_flow.py | 6 +- .../google_translate/test_config_flow.py | 10 +- .../google_travel_time/test_config_flow.py | 49 +++--- .../components/govee_ble/test_config_flow.py | 30 ++-- .../govee_light_local/test_config_flow.py | 11 +- tests/components/gpsd/test_config_flow.py | 10 +- tests/components/gree/test_config_flow.py | 11 +- tests/components/group/test_config_flow.py | 36 ++-- .../growatt_server/test_config_flow.py | 13 +- tests/components/guardian/test_config_flow.py | 26 +-- .../components/hardkernel/test_config_flow.py | 4 +- tests/components/harmony/test_config_flow.py | 7 +- tests/components/heos/test_config_flow.py | 16 +- .../here_travel_time/test_config_flow.py | 61 ++++--- tests/components/hive/test_config_flow.py | 77 ++++----- tests/components/hko/test_config_flow.py | 20 +-- tests/components/holiday/test_config_flow.py | 32 ++-- .../home_connect/test_config_flow.py | 5 +- .../homeassistant_green/test_config_flow.py | 20 +-- .../test_config_flow.py | 22 +-- .../homeassistant_yellow/test_config_flow.py | 48 +++--- tests/components/homekit/test_config_flow.py | 157 +++++++++--------- .../homekit_controller/test_config_flow.py | 26 +-- .../components/homewizard/test_config_flow.py | 38 ++--- .../components/homeworks/test_config_flow.py | 126 +++++++------- .../components/honeywell/test_config_flow.py | 21 ++- .../components/huawei_lte/test_config_flow.py | 29 ++-- .../components/huisbaasje/test_config_flow.py | 19 ++- .../test_config_flow.py | 38 ++--- .../husqvarna_automower/test_config_flow.py | 4 +- tests/components/huum/test_config_flow.py | 10 +- .../hvv_departures/test_config_flow.py | 10 +- .../components/hydrawise/test_config_flow.py | 20 +-- tests/components/hyperion/test_config_flow.py | 85 +++++----- tests/components/ialarm/test_config_flow.py | 13 +- tests/components/ibeacon/test_config_flow.py | 22 +-- tests/components/icloud/test_config_flow.py | 36 ++-- .../idasen_desk/test_config_flow.py | 30 ++-- tests/components/imap/test_config_flow.py | 74 ++++----- .../components/improv_ble/test_config_flow.py | 88 +++++----- tests/components/inkbird/test_config_flow.py | 30 ++-- tests/components/insteon/test_config_flow.py | 45 ++--- .../integration/test_config_flow.py | 8 +- .../intellifire/test_config_flow.py | 28 ++-- tests/components/iotawatt/test_config_flow.py | 16 +- tests/components/ipp/test_config_flow.py | 58 +++---- tests/components/iqvia/test_config_flow.py | 10 +- .../islamic_prayer_times/test_config_flow.py | 19 ++- tests/components/iss/test_config_flow.py | 8 +- tests/components/isy994/test_config_flow.py | 55 +++--- tests/components/izone/test_config_flow.py | 11 +- tests/components/jellyfin/test_config_flow.py | 21 +-- .../components/justnimbus/test_config_flow.py | 16 +- .../jvc_projector/test_config_flow.py | 38 ++--- 93 files changed, 1240 insertions(+), 1220 deletions(-) diff --git a/tests/components/faa_delays/test_config_flow.py b/tests/components/faa_delays/test_config_flow.py index 92a8929afbf..61e5ffb8e6b 100644 --- a/tests/components/faa_delays/test_config_flow.py +++ b/tests/components/faa_delays/test_config_flow.py @@ -5,10 +5,11 @@ from unittest.mock import patch from aiohttp import ClientConnectionError import faadelays -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.faa_delays.const import DOMAIN from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.exceptions import HomeAssistantError from tests.common import MockConfigEntry @@ -61,7 +62,7 @@ async def test_duplicate_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/fastdotcom/test_config_flow.py b/tests/components/fastdotcom/test_config_flow.py index d2122f4fe61..db28aaec703 100644 --- a/tests/components/fastdotcom/test_config_flow.py +++ b/tests/components/fastdotcom/test_config_flow.py @@ -19,7 +19,7 @@ async def test_user_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -31,7 +31,7 @@ async def test_user_form(hass: HomeAssistant) -> None: user_input={}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Fast.com" assert result["data"] == {} assert result["options"] == {} @@ -52,7 +52,7 @@ async def test_single_instance_allowed( DOMAIN, context={"source": source} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -66,7 +66,7 @@ async def test_import_flow_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Fast.com" assert result["data"] == {} assert result["options"] == {} diff --git a/tests/components/fibaro/test_config_flow.py b/tests/components/fibaro/test_config_flow.py index 2c7d05b87a3..dcf5f12a24a 100644 --- a/tests/components/fibaro/test_config_flow.py +++ b/tests/components/fibaro/test_config_flow.py @@ -35,7 +35,7 @@ async def _recovery_after_failure_works( }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_NAME assert result["data"] == { CONF_URL: TEST_URL, @@ -56,7 +56,7 @@ async def _recovery_after_reauth_failure_works( user_input={CONF_PASSWORD: "other_fake_password"}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" @@ -66,7 +66,7 @@ async def test_config_flow_user_initiated_success(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -79,7 +79,7 @@ async def test_config_flow_user_initiated_success(hass: HomeAssistant) -> None: }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_NAME assert result["data"] == { CONF_URL: TEST_URL, @@ -97,7 +97,7 @@ async def test_config_flow_user_initiated_connect_failure( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -112,7 +112,7 @@ async def test_config_flow_user_initiated_connect_failure( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -127,7 +127,7 @@ async def test_config_flow_user_initiated_auth_failure( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -142,7 +142,7 @@ async def test_config_flow_user_initiated_auth_failure( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -157,7 +157,7 @@ async def test_config_flow_user_initiated_unknown_failure_1( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -172,7 +172,7 @@ async def test_config_flow_user_initiated_unknown_failure_1( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -187,7 +187,7 @@ async def test_config_flow_user_initiated_unknown_failure_2( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -202,7 +202,7 @@ async def test_config_flow_user_initiated_unknown_failure_2( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -222,7 +222,7 @@ async def test_reauth_success( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -231,7 +231,7 @@ async def test_reauth_success( user_input={CONF_PASSWORD: "other_fake_password"}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" @@ -250,7 +250,7 @@ async def test_reauth_connect_failure( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -261,7 +261,7 @@ async def test_reauth_connect_failure( user_input={CONF_PASSWORD: "other_fake_password"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "cannot_connect"} @@ -283,7 +283,7 @@ async def test_reauth_auth_failure( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -294,7 +294,7 @@ async def test_reauth_auth_failure( user_input={CONF_PASSWORD: "other_fake_password"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "invalid_auth"} diff --git a/tests/components/filesize/test_config_flow.py b/tests/components/filesize/test_config_flow.py index 38209a3014e..4b275e66d02 100644 --- a/tests/components/filesize/test_config_flow.py +++ b/tests/components/filesize/test_config_flow.py @@ -27,7 +27,7 @@ async def test_full_user_flow(hass: HomeAssistant, tmp_path: Path) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( @@ -35,7 +35,7 @@ async def test_full_user_flow(hass: HomeAssistant, tmp_path: Path) -> None: user_input={CONF_FILE_PATH: test_file}, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == TEST_FILE_NAME assert result2.get("data") == {CONF_FILE_PATH: test_file} @@ -53,7 +53,7 @@ async def test_unique_path( DOMAIN, context={"source": SOURCE_USER}, data={CONF_FILE_PATH: test_file} ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -66,7 +66,7 @@ async def test_flow_fails_on_validation(hass: HomeAssistant, tmp_path: Path) -> DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( @@ -103,7 +103,7 @@ async def test_flow_fails_on_validation(hass: HomeAssistant, tmp_path: Path) -> }, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_FILE_NAME assert result2["data"] == { CONF_FILE_PATH: test_file, diff --git a/tests/components/fireservicerota/test_config_flow.py b/tests/components/fireservicerota/test_config_flow.py index e2bf5911089..539906d800b 100644 --- a/tests/components/fireservicerota/test_config_flow.py +++ b/tests/components/fireservicerota/test_config_flow.py @@ -4,10 +4,11 @@ from unittest.mock import patch from pyfireservicerota import InvalidAuthError -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.fireservicerota.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -44,7 +45,7 @@ async def test_show_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -57,7 +58,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_CONF ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -95,7 +96,7 @@ async def test_step_user(hass: HomeAssistant) -> None: await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_CONF[CONF_USERNAME] assert result["data"] == { "auth_implementation": "fireservicerota", @@ -135,7 +136,7 @@ async def test_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with ( patch( @@ -154,5 +155,5 @@ async def test_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" diff --git a/tests/components/fitbit/test_config_flow.py b/tests/components/fitbit/test_config_flow.py index 78d20b0fb58..2fd431176b9 100644 --- a/tests/components/fitbit/test_config_flow.py +++ b/tests/components/fitbit/test_config_flow.py @@ -51,7 +51,7 @@ async def test_full_flow( "redirect_uri": REDIRECT_URL, }, ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" f"&redirect_uri={REDIRECT_URL}" @@ -118,7 +118,7 @@ async def test_token_error( "redirect_uri": REDIRECT_URL, }, ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" f"&redirect_uri={REDIRECT_URL}" @@ -137,7 +137,7 @@ async def test_token_error( ) result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == error_reason @@ -177,7 +177,7 @@ async def test_api_failure( "redirect_uri": REDIRECT_URL, }, ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" f"&redirect_uri={REDIRECT_URL}" @@ -203,7 +203,7 @@ async def test_api_failure( ) result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == error_reason @@ -233,7 +233,7 @@ async def test_config_entry_already_exists( "redirect_uri": REDIRECT_URL, }, ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" f"&redirect_uri={REDIRECT_URL}" @@ -252,7 +252,7 @@ async def test_config_entry_already_exists( ) result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -487,7 +487,7 @@ async def test_reauth_flow( flow_id=result["flow_id"], user_input={}, ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -522,7 +522,7 @@ async def test_reauth_flow( ) as mock_setup: result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "reauth_successful" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -554,14 +554,14 @@ async def test_reauth_wrong_user_id( "entry_id": config_entry.entry_id, }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( flow_id=result["flow_id"], user_input={}, ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -596,7 +596,7 @@ async def test_reauth_wrong_user_id( ) as mock_setup: result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "wrong_account" assert len(mock_setup.mock_calls) == 0 @@ -630,7 +630,7 @@ async def test_partial_profile_data( "redirect_uri": REDIRECT_URL, }, ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" f"&redirect_uri={REDIRECT_URL}" diff --git a/tests/components/fivem/test_config_flow.py b/tests/components/fivem/test_config_flow.py index 174078c5420..2189a6ec34b 100644 --- a/tests/components/fivem/test_config_flow.py +++ b/tests/components/fivem/test_config_flow.py @@ -55,7 +55,7 @@ async def test_show_config_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -64,7 +64,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -83,7 +83,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == USER_INPUT[CONF_HOST] assert result2["data"] == USER_INPUT assert len(mock_setup_entry.mock_calls) == 1 @@ -105,7 +105,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -125,7 +125,7 @@ async def test_form_invalid(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -145,5 +145,5 @@ async def test_form_invalid_game_name(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_game_name"} diff --git a/tests/components/fjaraskupan/test_config_flow.py b/tests/components/fjaraskupan/test_config_flow.py index b6da1fcf5b5..fa0df9241dd 100644 --- a/tests/components/fjaraskupan/test_config_flow.py +++ b/tests/components/fjaraskupan/test_config_flow.py @@ -34,10 +34,10 @@ async def test_configure(hass: HomeAssistant, mock_setup_entry) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Fjäråskupan" assert result["data"] == {} @@ -56,8 +56,8 @@ async def test_scan_no_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" diff --git a/tests/components/flexit_bacnet/test_config_flow.py b/tests/components/flexit_bacnet/test_config_flow.py index 7d864a80c2d..77895ac647e 100644 --- a/tests/components/flexit_bacnet/test_config_flow.py +++ b/tests/components/flexit_bacnet/test_config_flow.py @@ -23,7 +23,7 @@ async def test_form( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Device Name" assert result["context"]["unique_id"] == "0000-0001" assert result["data"] == { @@ -67,7 +67,7 @@ async def test_flow_fails( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": message} assert len(mock_setup_entry.mock_calls) == 0 @@ -81,7 +81,7 @@ async def test_flow_fails( }, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Device Name" assert result2["context"]["unique_id"] == "0000-0001" assert result2["data"] == { @@ -105,5 +105,5 @@ async def test_form_device_already_exist( }, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/flick_electric/test_config_flow.py b/tests/components/flick_electric/test_config_flow.py index 9635f3a1526..1b56aaf6376 100644 --- a/tests/components/flick_electric/test_config_flow.py +++ b/tests/components/flick_electric/test_config_flow.py @@ -4,10 +4,11 @@ from unittest.mock import patch from pyflick.authentication import AuthException -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.flick_electric.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -47,7 +48,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Flick Electric: test-username" assert result2["data"] == CONF assert len(mock_setup_entry.mock_calls) == 1 @@ -69,7 +70,7 @@ async def test_form_duplicate_login(hass: HomeAssistant) -> None: ): result = await _flow_submit(hass) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -81,7 +82,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: ): result = await _flow_submit(hass) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -93,7 +94,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: ): result = await _flow_submit(hass) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -105,5 +106,5 @@ async def test_form_generic_exception(hass: HomeAssistant) -> None: ): result = await _flow_submit(hass) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "unknown"} diff --git a/tests/components/flipr/test_config_flow.py b/tests/components/flipr/test_config_flow.py index 4ee6d85cead..60dcc15a701 100644 --- a/tests/components/flipr/test_config_flow.py +++ b/tests/components/flipr/test_config_flow.py @@ -5,10 +5,11 @@ from unittest.mock import patch import pytest from requests.exceptions import HTTPError, Timeout -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.flipr.const import CONF_FLIPR_ID, DOMAIN from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType @pytest.fixture(name="mock_setup") @@ -27,7 +28,7 @@ async def test_show_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER @@ -69,7 +70,7 @@ async def test_nominal_case(hass: HomeAssistant, mock_setup) -> None: assert len(mock_flipr_client.mock_calls) == 1 - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "flipid" assert result["data"] == { CONF_EMAIL: "dummylogin", @@ -93,7 +94,7 @@ async def test_multiple_flip_id(hass: HomeAssistant, mock_setup) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "flipr_id" result = await hass.config_entries.flow.async_configure( @@ -103,7 +104,7 @@ async def test_multiple_flip_id(hass: HomeAssistant, mock_setup) -> None: assert len(mock_flipr_client.mock_calls) == 1 - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "FLIP2" assert result["data"] == { CONF_EMAIL: "dummylogin", diff --git a/tests/components/flux_led/test_config_flow.py b/tests/components/flux_led/test_config_flow.py index 63a7a671871..a4ba42ed629 100644 --- a/tests/components/flux_led/test_config_flow.py +++ b/tests/components/flux_led/test_config_flow.py @@ -393,7 +393,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: data=FLUX_DISCOVERY, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_wifibulb(): @@ -403,7 +403,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: data=DHCP_DISCOVERY, ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_in_progress" with _patch_discovery(), _patch_wifibulb(): @@ -417,7 +417,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: ), ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "already_in_progress" @@ -432,7 +432,7 @@ async def test_discovered_by_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -471,7 +471,7 @@ async def test_discovered_by_dhcp_udp_responds(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -510,7 +510,7 @@ async def test_discovered_by_dhcp_no_udp_response(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -545,7 +545,7 @@ async def test_discovered_by_dhcp_partial_udp_response_fallback_tcp( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -581,7 +581,7 @@ async def test_discovered_by_dhcp_no_udp_response_or_tcp_response( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -605,7 +605,7 @@ async def test_discovered_by_dhcp_or_discovery_adds_missing_unique_id( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.unique_id == MAC_ADDRESS @@ -628,7 +628,7 @@ async def test_mac_address_off_by_one_updated_via_discovery( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.unique_id == MAC_ADDRESS @@ -649,7 +649,7 @@ async def test_mac_address_off_by_one_not_updated_from_dhcp( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.unique_id == MAC_ADDRESS_ONE_OFF @@ -677,7 +677,7 @@ async def test_discovered_by_dhcp_or_discovery_mac_address_mismatch_host_already ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.unique_id == MAC_ADDRESS_DIFFERENT @@ -745,5 +745,5 @@ async def test_discovered_can_be_ignored(hass: HomeAssistant, source, data) -> N ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/forecast_solar/test_config_flow.py b/tests/components/forecast_solar/test_config_flow.py index 015bd809b20..abaad402e1b 100644 --- a/tests/components/forecast_solar/test_config_flow.py +++ b/tests/components/forecast_solar/test_config_flow.py @@ -25,7 +25,7 @@ async def test_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( @@ -40,7 +40,7 @@ async def test_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No }, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "Name" assert result2.get("data") == { CONF_LATITUDE: 52.42, @@ -67,7 +67,7 @@ async def test_options_flow_invalid_api( result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "init" result2 = await hass.config_entries.options.async_configure( @@ -84,7 +84,7 @@ async def test_options_flow_invalid_api( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2["errors"] == {CONF_API_KEY: "invalid_api_key"} @@ -100,7 +100,7 @@ async def test_options_flow( result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "init" # With the API key @@ -118,7 +118,7 @@ async def test_options_flow( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("data") == { CONF_API_KEY: "SolarForecast150", CONF_DECLINATION: 21, @@ -142,7 +142,7 @@ async def test_options_flow_without_key( result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "init" # Without the API key @@ -159,7 +159,7 @@ async def test_options_flow_without_key( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("data") == { CONF_API_KEY: None, CONF_DECLINATION: 21, diff --git a/tests/components/forked_daapd/test_config_flow.py b/tests/components/forked_daapd/test_config_flow.py index a7f0dc3f603..593b527009b 100644 --- a/tests/components/forked_daapd/test_config_flow.py +++ b/tests/components/forked_daapd/test_config_flow.py @@ -5,7 +5,6 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest -from homeassistant import data_entry_flow from homeassistant.components import zeroconf from homeassistant.components.forked_daapd.const import ( CONF_LIBRESPOT_JAVA_PORT, @@ -18,6 +17,7 @@ from homeassistant.components.forked_daapd.media_player import async_setup_entry from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.exceptions import PlatformNotReady from tests.common import MockConfigEntry @@ -63,7 +63,7 @@ async def test_show_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -86,7 +86,7 @@ async def test_config_flow(hass: HomeAssistant, config_entry) -> None: DOMAIN, context={"source": SOURCE_USER}, data=config_data ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "My Music on myhost" assert result["data"][CONF_HOST] == config_data[CONF_HOST] assert result["data"][CONF_PORT] == config_data[CONF_PORT] @@ -99,7 +99,7 @@ async def test_config_flow(hass: HomeAssistant, config_entry) -> None: data=config_entry.data, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT async def test_zeroconf_updates_title(hass: HomeAssistant, config_entry) -> None: @@ -120,7 +120,7 @@ async def test_zeroconf_updates_title(hass: HomeAssistant, config_entry) -> None DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert config_entry.title == "zeroconf_test" assert len(hass.config_entries.async_entries(DOMAIN)) == 2 @@ -136,7 +136,7 @@ async def test_config_flow_no_websocket(hass: HomeAssistant, config_entry) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config_entry.data ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM async def test_config_flow_zeroconf_invalid(hass: HomeAssistant) -> None: @@ -154,7 +154,7 @@ async def test_config_flow_zeroconf_invalid(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) # doesn't create the entry, tries to show form but gets abort - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_forked_daapd" # test with forked-daapd version < 27 discovery_info = zeroconf.ZeroconfServiceInfo( @@ -169,7 +169,7 @@ async def test_config_flow_zeroconf_invalid(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) # doesn't create the entry, tries to show form but gets abort - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_forked_daapd" # test with verbose mtd-version from Firefly discovery_info = zeroconf.ZeroconfServiceInfo( @@ -184,7 +184,7 @@ async def test_config_flow_zeroconf_invalid(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) # doesn't create the entry, tries to show form but gets abort - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_forked_daapd" # test with svn mtd-version from Firefly discovery_info = zeroconf.ZeroconfServiceInfo( @@ -199,7 +199,7 @@ async def test_config_flow_zeroconf_invalid(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) # doesn't create the entry, tries to show form but gets abort - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_forked_daapd" @@ -221,7 +221,7 @@ async def test_config_flow_zeroconf_valid(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM async def test_options_flow(hass: HomeAssistant, config_entry) -> None: @@ -237,7 +237,7 @@ async def test_options_flow(hass: HomeAssistant, config_entry) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -248,7 +248,7 @@ async def test_options_flow(hass: HomeAssistant, config_entry) -> None: CONF_MAX_PLAYLISTS: 8, }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_async_setup_entry_not_ready(hass: HomeAssistant, config_entry) -> None: diff --git a/tests/components/foscam/test_config_flow.py b/tests/components/foscam/test_config_flow.py index 64ad2b946da..9c0a07aa67c 100644 --- a/tests/components/foscam/test_config_flow.py +++ b/tests/components/foscam/test_config_flow.py @@ -9,9 +9,10 @@ from libpyfoscam.foscam import ( ERROR_FOSCAM_UNKNOWN, ) -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.foscam import config_flow from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -82,7 +83,7 @@ async def test_user_valid(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -103,7 +104,7 @@ async def test_user_valid(hass: HomeAssistant) -> None: await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == CAMERA_NAME assert result["data"] == VALID_CONFIG @@ -116,7 +117,7 @@ async def test_user_invalid_auth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -134,7 +135,7 @@ async def test_user_invalid_auth(hass: HomeAssistant) -> None: await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -144,7 +145,7 @@ async def test_user_cannot_connect(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -162,7 +163,7 @@ async def test_user_cannot_connect(hass: HomeAssistant) -> None: await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -172,7 +173,7 @@ async def test_user_invalid_response(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -192,7 +193,7 @@ async def test_user_invalid_response(hass: HomeAssistant) -> None: await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_response"} @@ -208,7 +209,7 @@ async def test_user_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -223,7 +224,7 @@ async def test_user_already_configured(hass: HomeAssistant) -> None: await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -233,7 +234,7 @@ async def test_user_unknown_exception(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -248,5 +249,5 @@ async def test_user_unknown_exception(hass: HomeAssistant) -> None: await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "unknown"} diff --git a/tests/components/freebox/test_config_flow.py b/tests/components/freebox/test_config_flow.py index a7dff79ecfb..ca9e9c12937 100644 --- a/tests/components/freebox/test_config_flow.py +++ b/tests/components/freebox/test_config_flow.py @@ -9,12 +9,12 @@ from freebox_api.exceptions import ( InvalidTokenError, ) -from homeassistant import data_entry_flow from homeassistant.components import zeroconf from homeassistant.components.freebox.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .const import MOCK_HOST, MOCK_PORT @@ -46,7 +46,7 @@ async def test_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # test with all provided @@ -55,7 +55,7 @@ async def test_user(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" @@ -66,7 +66,7 @@ async def test_zeroconf(hass: HomeAssistant) -> None: context={"source": SOURCE_ZEROCONF}, data=MOCK_ZEROCONF_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" @@ -83,7 +83,7 @@ async def internal_test_link(hass: HomeAssistant) -> None: ) result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == MOCK_HOST assert result["title"] == MOCK_HOST assert result["data"][CONF_HOST] == MOCK_HOST @@ -112,7 +112,7 @@ async def test_link_bridge_mode_error( data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}, ) result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -130,7 +130,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -147,7 +147,7 @@ async def test_on_link_failed(hass: HomeAssistant) -> None: side_effect=AuthorizationError(), ): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "register_failed"} with patch( @@ -155,7 +155,7 @@ async def test_on_link_failed(hass: HomeAssistant) -> None: side_effect=HttpRequestError(), ): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} with patch( @@ -163,5 +163,5 @@ async def test_on_link_failed(hass: HomeAssistant) -> None: side_effect=InvalidTokenError(), ): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "unknown"} diff --git a/tests/components/freedompro/test_config_flow.py b/tests/components/freedompro/test_config_flow.py index a0063f72557..0999f157661 100644 --- a/tests/components/freedompro/test_config_flow.py +++ b/tests/components/freedompro/test_config_flow.py @@ -4,11 +4,11 @@ from unittest.mock import patch import pytest -from homeassistant import data_entry_flow from homeassistant.components.freedompro.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .const import DEVICES @@ -25,7 +25,7 @@ async def test_show_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -80,6 +80,6 @@ async def test_create_entry(hass: HomeAssistant) -> None: data=VALID_CONFIG, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Freedompro" assert result["data"][CONF_API_KEY] == "ksdjfgslkjdfksjdfksjgfksjd" diff --git a/tests/components/fritz/test_config_flow.py b/tests/components/fritz/test_config_flow.py index fbd9886f468..074d32bf0ca 100644 --- a/tests/components/fritz/test_config_flow.py +++ b/tests/components/fritz/test_config_flow.py @@ -10,7 +10,6 @@ from fritzconnection.core.exceptions import ( ) import pytest -from homeassistant import data_entry_flow from homeassistant.components.device_tracker import ( CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME, @@ -71,13 +70,13 @@ async def test_user(hass: HomeAssistant, fc_class_mock, mock_get_source_ip) -> N result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_PASSWORD] == "fake_pass" assert result["data"][CONF_USERNAME] == "fake_user" @@ -127,13 +126,13 @@ async def test_user_already_configured( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "already_configured" @@ -150,7 +149,7 @@ async def test_exception_security( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -161,7 +160,7 @@ async def test_exception_security( result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == ERROR_AUTH_INVALID @@ -172,7 +171,7 @@ async def test_exception_connection(hass: HomeAssistant, mock_get_source_ip) -> result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -183,7 +182,7 @@ async def test_exception_connection(hass: HomeAssistant, mock_get_source_ip) -> result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == ERROR_CANNOT_CONNECT @@ -194,7 +193,7 @@ async def test_exception_unknown(hass: HomeAssistant, mock_get_source_ip) -> Non result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -205,7 +204,7 @@ async def test_exception_unknown(hass: HomeAssistant, mock_get_source_ip) -> Non result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == ERROR_UNKNOWN @@ -248,7 +247,7 @@ async def test_reauth_successful( data=mock_config.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -259,7 +258,7 @@ async def test_reauth_successful( }, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert mock_setup_entry.called @@ -291,7 +290,7 @@ async def test_reauth_not_successful( data=mock_config.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -302,7 +301,7 @@ async def test_reauth_not_successful( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"]["base"] == error @@ -332,7 +331,7 @@ async def test_ssdp_already_configured( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -361,7 +360,7 @@ async def test_ssdp_already_configured_host( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -390,7 +389,7 @@ async def test_ssdp_already_configured_host_uuid( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -405,7 +404,7 @@ async def test_ssdp_already_in_progress_host( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" MOCK_NO_UNIQUE_ID = dataclasses.replace(MOCK_SSDP_DATA) @@ -414,7 +413,7 @@ async def test_ssdp_already_in_progress_host( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_NO_UNIQUE_ID ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -441,7 +440,7 @@ async def test_ssdp(hass: HomeAssistant, fc_class_mock, mock_get_source_ip) -> N result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -452,7 +451,7 @@ async def test_ssdp(hass: HomeAssistant, fc_class_mock, mock_get_source_ip) -> N }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_HOST] == MOCK_IPS["fritz.box"] assert result["data"][CONF_PASSWORD] == "fake_pass" assert result["data"][CONF_USERNAME] == "fake_user" @@ -469,7 +468,7 @@ async def test_ssdp_exception(hass: HomeAssistant, mock_get_source_ip) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -480,7 +479,7 @@ async def test_ssdp_exception(hass: HomeAssistant, mock_get_source_ip) -> None: }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" @@ -500,7 +499,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_OLD_DISCOVERY: False, CONF_CONSIDER_HOME: 37, diff --git a/tests/components/fritzbox/test_config_flow.py b/tests/components/fritzbox/test_config_flow.py index 690082085f8..53a4f1c5205 100644 --- a/tests/components/fritzbox/test_config_flow.py +++ b/tests/components/fritzbox/test_config_flow.py @@ -68,13 +68,13 @@ async def test_user(hass: HomeAssistant, fritz: Mock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "10.0.0.1" assert result["data"][CONF_HOST] == "10.0.0.1" assert result["data"][CONF_PASSWORD] == "fake_pass" @@ -89,7 +89,7 @@ async def test_user_auth_failed(hass: HomeAssistant, fritz: Mock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "invalid_auth" @@ -101,7 +101,7 @@ async def test_user_not_successful(hass: HomeAssistant, fritz: Mock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -110,13 +110,13 @@ async def test_user_already_configured(hass: HomeAssistant, fritz: Mock) -> None result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert not result["result"].unique_id result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -130,7 +130,7 @@ async def test_reauth_success(hass: HomeAssistant, fritz: Mock) -> None: context={"source": SOURCE_REAUTH, "entry_id": mock_config.entry_id}, data=mock_config.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -141,7 +141,7 @@ async def test_reauth_success(hass: HomeAssistant, fritz: Mock) -> None: }, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert mock_config.data[CONF_USERNAME] == "other_fake_user" assert mock_config.data[CONF_PASSWORD] == "other_fake_password" @@ -159,7 +159,7 @@ async def test_reauth_auth_failed(hass: HomeAssistant, fritz: Mock) -> None: context={"source": SOURCE_REAUTH, "entry_id": mock_config.entry_id}, data=mock_config.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -170,7 +170,7 @@ async def test_reauth_auth_failed(hass: HomeAssistant, fritz: Mock) -> None: }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"]["base"] == "invalid_auth" @@ -187,7 +187,7 @@ async def test_reauth_not_successful(hass: HomeAssistant, fritz: Mock) -> None: context={"source": SOURCE_REAUTH, "entry_id": mock_config.entry_id}, data=mock_config.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -198,7 +198,7 @@ async def test_reauth_not_successful(hass: HomeAssistant, fritz: Mock) -> None: }, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -222,7 +222,7 @@ async def test_ssdp( ) assert result["type"] == expected_result - if expected_result == FlowResultType.ABORT: + if expected_result is FlowResultType.ABORT: return assert result["step_id"] == "confirm" @@ -231,7 +231,7 @@ async def test_ssdp( result["flow_id"], user_input={CONF_PASSWORD: "fake_pass", CONF_USERNAME: "fake_user"}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == CONF_FAKE_NAME assert result["data"][CONF_HOST] == urlparse(test_data.ssdp_location).hostname assert result["data"][CONF_PASSWORD] == "fake_pass" @@ -247,14 +247,14 @@ async def test_ssdp_no_friendly_name(hass: HomeAssistant, fritz: Mock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_NO_NAME ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "fake_pass", CONF_USERNAME: "fake_user"}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "10.0.0.1" assert result["data"][CONF_HOST] == "10.0.0.1" assert result["data"][CONF_PASSWORD] == "fake_pass" @@ -269,7 +269,7 @@ async def test_ssdp_auth_failed(hass: HomeAssistant, fritz: Mock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA["ip4_valid"] ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {} @@ -277,7 +277,7 @@ async def test_ssdp_auth_failed(hass: HomeAssistant, fritz: Mock) -> None: result["flow_id"], user_input={CONF_PASSWORD: "whatever", CONF_USERNAME: "whatever"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"]["base"] == "invalid_auth" @@ -289,14 +289,14 @@ async def test_ssdp_not_successful(hass: HomeAssistant, fritz: Mock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA["ip4_valid"] ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "whatever", CONF_USERNAME: "whatever"}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -307,14 +307,14 @@ async def test_ssdp_not_supported(hass: HomeAssistant, fritz: Mock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA["ip4_valid"] ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "whatever", CONF_USERNAME: "whatever"}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -325,13 +325,13 @@ async def test_ssdp_already_in_progress_unique_id( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA["ip4_valid"] ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA["ip4_valid"] ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -340,7 +340,7 @@ async def test_ssdp_already_in_progress_host(hass: HomeAssistant, fritz: Mock) - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA["ip4_valid"] ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" MOCK_NO_UNIQUE_ID = dataclasses.replace(MOCK_SSDP_DATA["ip4_valid"]) @@ -349,7 +349,7 @@ async def test_ssdp_already_in_progress_host(hass: HomeAssistant, fritz: Mock) - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_NO_UNIQUE_ID ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -358,12 +358,12 @@ async def test_ssdp_already_configured(hass: HomeAssistant, fritz: Mock) -> None result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert not result["result"].unique_id result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA["ip4_valid"] ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" assert result["result"].unique_id == "only-a-test" diff --git a/tests/components/fritzbox_callmonitor/test_config_flow.py b/tests/components/fritzbox_callmonitor/test_config_flow.py index 33e2d8fb125..14f18e84e0c 100644 --- a/tests/components/fritzbox_callmonitor/test_config_flow.py +++ b/tests/components/fritzbox_callmonitor/test_config_flow.py @@ -90,7 +90,7 @@ async def test_setup_one_phonebook(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -129,7 +129,7 @@ async def test_setup_one_phonebook(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_PHONEBOOK_NAME_1 assert result["data"] == MOCK_CONFIG_ENTRY assert len(mock_setup_entry.mock_calls) == 1 @@ -141,7 +141,7 @@ async def test_setup_multiple_phonebooks(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -172,7 +172,7 @@ async def test_setup_multiple_phonebooks(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "phonebook" assert result["errors"] == {} @@ -191,7 +191,7 @@ async def test_setup_multiple_phonebooks(hass: HomeAssistant) -> None: {CONF_PHONEBOOK: MOCK_PHONEBOOK_NAME_2}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_PHONEBOOK_NAME_2 assert result["data"] == { CONF_HOST: MOCK_HOST, @@ -219,7 +219,7 @@ async def test_setup_cannot_connect(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == ConnectResult.NO_DEVIES_FOUND @@ -238,7 +238,7 @@ async def test_setup_insufficient_permissions(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == ConnectResult.INSUFFICIENT_PERMISSIONS @@ -260,7 +260,7 @@ async def test_setup_invalid_auth( result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": ConnectResult.INVALID_AUTH} @@ -282,14 +282,14 @@ async def test_options_flow_correct_prefixes(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + 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_PREFIXES: "+49, 491234"} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_PREFIXES: ["+49", "491234"]} @@ -311,14 +311,14 @@ async def test_options_flow_incorrect_prefixes(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + 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_PREFIXES: ""} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": ConnectResult.MALFORMED_PREFIXES} @@ -340,12 +340,12 @@ async def test_options_flow_no_prefixes(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + 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"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_PREFIXES: None} diff --git a/tests/components/fronius/test_config_flow.py b/tests/components/fronius/test_config_flow.py index c09baeb2d22..bf5ef360752 100644 --- a/tests/components/fronius/test_config_flow.py +++ b/tests/components/fronius/test_config_flow.py @@ -49,7 +49,7 @@ async def test_form_with_logger(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -70,7 +70,7 @@ async def test_form_with_logger(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "SolarNet Datalogger at 10.9.8.1" assert result2["data"] == { "host": "10.9.8.1", @@ -84,7 +84,7 @@ async def test_form_with_inverter(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -109,7 +109,7 @@ async def test_form_with_inverter(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "SolarNet Inverter at 10.9.1.1" assert result2["data"] == { "host": "10.9.1.1", @@ -141,7 +141,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -168,7 +168,7 @@ async def test_form_no_device(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -189,7 +189,7 @@ async def test_form_unexpected(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -217,7 +217,7 @@ async def test_form_already_existing(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -259,7 +259,7 @@ async def test_form_updates_host( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" mock_unload_entry.assert_called_with(hass, entry) @@ -283,13 +283,13 @@ async def test_dhcp(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -> result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=MOCK_DHCP_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm_discovery" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"SolarNet Datalogger at {MOCK_DHCP_DATA.ip}" assert result["data"] == { "host": MOCK_DHCP_DATA.ip, @@ -314,7 +314,7 @@ async def test_dhcp_already_configured( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=MOCK_DHCP_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -336,5 +336,5 @@ async def test_dhcp_invalid( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=MOCK_DHCP_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_host" diff --git a/tests/components/frontier_silicon/test_config_flow.py b/tests/components/frontier_silicon/test_config_flow.py index 6a5e62f7dce..04bd1febdf8 100644 --- a/tests/components/frontier_silicon/test_config_flow.py +++ b/tests/components/frontier_silicon/test_config_flow.py @@ -52,7 +52,7 @@ async def test_form_default_pin( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -67,7 +67,7 @@ async def test_form_default_pin( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Name of the device" assert result2["data"] == { CONF_WEBFSAPI_URL: "http://1.1.1.1:80/webfsapi", @@ -90,7 +90,7 @@ async def test_form_nondefault_pin( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -104,7 +104,7 @@ async def test_form_nondefault_pin( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "device_config" assert result2["errors"] is None @@ -119,7 +119,7 @@ async def test_form_nondefault_pin( ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Name of the device" assert result3["data"] == { CONF_WEBFSAPI_URL: "http://1.1.1.1:80/webfsapi", @@ -146,7 +146,7 @@ async def test_form_nondefault_pin_invalid( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -160,7 +160,7 @@ async def test_form_nondefault_pin_invalid( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "device_config" assert result2["errors"] is None @@ -174,7 +174,7 @@ async def test_form_nondefault_pin_invalid( ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result2["step_id"] == "device_config" assert result3["errors"] == {"base": result_error} @@ -184,7 +184,7 @@ async def test_form_nondefault_pin_invalid( ) await hass.async_block_till_done() - assert result4["type"] == FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == "Name of the device" assert result4["data"] == { CONF_WEBFSAPI_URL: "http://1.1.1.1:80/webfsapi", @@ -210,7 +210,7 @@ async def test_invalid_device_url( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -224,7 +224,7 @@ async def test_invalid_device_url( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": result_error} @@ -234,7 +234,7 @@ async def test_invalid_device_url( ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Name of the device" assert result3["data"] == { CONF_WEBFSAPI_URL: "http://1.1.1.1:80/webfsapi", @@ -265,7 +265,7 @@ async def test_ssdp( data=MOCK_DISCOVERY, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result2 = await hass.config_entries.flow.async_configure( @@ -273,7 +273,7 @@ async def test_ssdp( {}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Name of the device" assert result2["data"] == { CONF_WEBFSAPI_URL: "http://1.1.1.1:80/webfsapi", @@ -291,7 +291,7 @@ async def test_ssdp_invalid_location(hass: HomeAssistant) -> None: data=INVALID_MOCK_DISCOVERY, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -308,7 +308,7 @@ async def test_ssdp_already_configured( data=MOCK_DISCOVERY, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -330,7 +330,7 @@ async def test_ssdp_fail( data=MOCK_DISCOVERY, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == result_error @@ -347,7 +347,7 @@ async def test_ssdp_nondefault_pin(hass: HomeAssistant) -> None: data=MOCK_DISCOVERY, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_auth" @@ -365,14 +365,14 @@ async def test_reauth_flow(hass: HomeAssistant, config_entry: MockConfigEntry) - }, data=config_entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "device_config" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PIN: "4242"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert config_entry.data[CONF_PIN] == "4242" @@ -404,7 +404,7 @@ async def test_reauth_flow_friendly_name_error( }, data=config_entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "device_config" with patch( @@ -417,7 +417,7 @@ async def test_reauth_flow_friendly_name_error( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "device_config" assert result2["errors"] == {"base": reason} @@ -425,6 +425,6 @@ async def test_reauth_flow_friendly_name_error( result["flow_id"], user_input={CONF_PIN: "4242"}, ) - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" assert config_entry.data[CONF_PIN] == "4242" diff --git a/tests/components/fully_kiosk/test_config_flow.py b/tests/components/fully_kiosk/test_config_flow.py index 6a78eda070f..873fb2c6796 100644 --- a/tests/components/fully_kiosk/test_config_flow.py +++ b/tests/components/fully_kiosk/test_config_flow.py @@ -32,7 +32,7 @@ async def test_user_flow( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( @@ -45,7 +45,7 @@ async def test_user_flow( }, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "Test device" assert result2.get("data") == { CONF_HOST: "1.1.1.1", @@ -94,7 +94,7 @@ async def test_errors( }, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "user" assert result2.get("errors") == {"base": reason} @@ -112,7 +112,7 @@ async def test_errors( }, ) - assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY assert result3.get("title") == "Test device" assert result3.get("data") == { CONF_HOST: "1.1.1.1", @@ -139,7 +139,7 @@ async def test_duplicate_updates_existing_entry( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( @@ -152,7 +152,7 @@ async def test_duplicate_updates_existing_entry( }, ) - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "already_configured" assert mock_config_entry.data == { CONF_HOST: "1.1.1.1", @@ -182,7 +182,7 @@ async def test_dhcp_discovery_updates_entry( ), ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" assert mock_config_entry.data == { CONF_HOST: "127.0.0.2", @@ -210,7 +210,7 @@ async def test_dhcp_unknown_device( ), ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "unknown" @@ -234,7 +234,7 @@ async def test_mqtt_discovery_flow( timestamp=None, ), ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "discovery_confirm" confirmResult = await hass.config_entries.flow.async_configure( @@ -247,7 +247,7 @@ async def test_mqtt_discovery_flow( ) assert confirmResult - assert confirmResult.get("type") == FlowResultType.CREATE_ENTRY + assert confirmResult.get("type") is FlowResultType.CREATE_ENTRY assert confirmResult.get("title") == "Test device" assert confirmResult.get("data") == { CONF_HOST: "192.168.1.234", diff --git a/tests/components/fyta/test_config_flow.py b/tests/components/fyta/test_config_flow.py index b21be5abb90..93b83caa379 100644 --- a/tests/components/fyta/test_config_flow.py +++ b/tests/components/fyta/test_config_flow.py @@ -10,10 +10,11 @@ from fyta_cli.fyta_exceptions import ( ) import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.fyta.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -31,7 +32,7 @@ async def test_user_flow( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -75,7 +76,7 @@ async def test_form_exceptions( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == error @@ -87,7 +88,7 @@ async def test_form_exceptions( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == USERNAME assert result["data"][CONF_USERNAME] == USERNAME assert result["data"][CONF_PASSWORD] == PASSWORD @@ -108,7 +109,7 @@ async def test_duplicate_entry(hass: HomeAssistant, mock_fyta: AsyncMock) -> Non DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -118,5 +119,5 @@ async def test_duplicate_entry(hass: HomeAssistant, mock_fyta: AsyncMock) -> Non ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/garages_amsterdam/test_config_flow.py b/tests/components/garages_amsterdam/test_config_flow.py index ae735d71e55..729d31e413c 100644 --- a/tests/components/garages_amsterdam/test_config_flow.py +++ b/tests/components/garages_amsterdam/test_config_flow.py @@ -18,7 +18,7 @@ async def test_full_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM with patch( "homeassistant.components.garages_amsterdam.async_setup_entry", @@ -30,7 +30,7 @@ async def test_full_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "IJDok" assert "result" in result2 assert result2["result"].unique_id == "IJDok" @@ -59,5 +59,5 @@ async def test_error_handling( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == reason diff --git a/tests/components/gdacs/test_config_flow.py b/tests/components/gdacs/test_config_flow.py index 71e5dfdb5d5..f11848162cd 100644 --- a/tests/components/gdacs/test_config_flow.py +++ b/tests/components/gdacs/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.gdacs import CONF_CATEGORIES, DOMAIN from homeassistant.const import ( CONF_LATITUDE, @@ -13,6 +13,7 @@ from homeassistant.const import ( CONF_SCAN_INTERVAL, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType @pytest.fixture(name="gdacs_setup", autouse=True) @@ -30,7 +31,7 @@ async def test_duplicate_error(hass: HomeAssistant, config_entry) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -39,7 +40,7 @@ async def test_show_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -52,7 +53,7 @@ async def test_step_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "-41.2, 174.7" assert result["data"] == { CONF_LATITUDE: -41.2, diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py index d9b3c848eb6..751361d47dd 100644 --- a/tests/components/generic/test_config_flow.py +++ b/tests/components/generic/test_config_flow.py @@ -84,7 +84,7 @@ async def test_form( user_flow["flow_id"], TESTDATA, ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "user_confirm_still" client = await hass_client() preview_id = result1["flow_id"] @@ -97,7 +97,7 @@ async def test_form( user_input={CONF_CONFIRMED_OK: True}, ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "127_0_0_1" assert result2["options"] == { CONF_STILL_IMAGE_URL: "http://127.0.0.1/testurl/1", @@ -138,13 +138,13 @@ async def test_form_only_stillimage( data, ) await hass.async_block_till_done() - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "user_confirm_still" result2 = await hass.config_entries.flow.async_configure( result1["flow_id"], user_input={CONF_CONFIRMED_OK: True}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "127_0_0_1" assert result2["options"] == { CONF_STILL_IMAGE_URL: "http://127.0.0.1/testurl/1", @@ -171,13 +171,13 @@ async def test_form_reject_still_preview( user_flow["flow_id"], TESTDATA, ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "user_confirm_still" result2 = await hass.config_entries.flow.async_configure( result1["flow_id"], user_input={CONF_CONFIRMED_OK: False}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" @@ -201,7 +201,7 @@ async def test_form_still_preview_cam_off( user_flow["flow_id"], TESTDATA, ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "user_confirm_still" preview_id = result1["flow_id"] # Try to view the image, should be unavailable. @@ -222,14 +222,14 @@ async def test_form_only_stillimage_gif( user_flow["flow_id"], data, ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "user_confirm_still" result2 = await hass.config_entries.flow.async_configure( result1["flow_id"], user_input={CONF_CONFIRMED_OK: True}, ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["options"][CONF_CONTENT_TYPE] == "image/gif" @@ -247,14 +247,14 @@ async def test_form_only_svg_whitespace( user_flow["flow_id"], data, ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "user_confirm_still" result2 = await hass.config_entries.flow.async_configure( result1["flow_id"], user_input={CONF_CONFIRMED_OK: True}, ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY @respx.mock @@ -282,14 +282,14 @@ async def test_form_only_still_sample( user_flow["flow_id"], data, ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "user_confirm_still" result2 = await hass.config_entries.flow.async_configure( result1["flow_id"], user_input={CONF_CONFIRMED_OK: True}, ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY @respx.mock @@ -371,13 +371,13 @@ async def test_form_rtsp_mode( result1 = await hass.config_entries.flow.async_configure( user_flow["flow_id"], data ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "user_confirm_still" result2 = await hass.config_entries.flow.async_configure( result1["flow_id"], user_input={CONF_CONFIRMED_OK: True}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "127_0_0_1" assert result2["options"] == { CONF_STILL_IMAGE_URL: "http://127.0.0.1/testurl/1", @@ -408,14 +408,14 @@ async def test_form_only_stream( user_flow["flow_id"], data, ) - assert result1["type"] == FlowResultType.FORM + 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"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "127_0_0_1" assert result3["options"] == { CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, @@ -451,7 +451,7 @@ async def test_form_still_and_stream_not_provided( CONF_VERIFY_SSL: False, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "no_still_image_or_stream_url"} @@ -680,7 +680,7 @@ async def test_options_template_error( await hass.async_block_till_done() result = await hass.config_entries.options.async_init(mock_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" # try updating the still image url @@ -691,16 +691,16 @@ async def test_options_template_error( result["flow_id"], user_input=data, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "confirm_still" result2a = await hass.config_entries.options.async_configure( result2["flow_id"], user_input={CONF_CONFIRMED_OK: True} ) - assert result2a["type"] == FlowResultType.CREATE_ENTRY + assert result2a["type"] is FlowResultType.CREATE_ENTRY result3 = await hass.config_entries.options.async_init(mock_entry.entry_id) - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "init" # verify that an invalid template reports the correct UI error. @@ -709,7 +709,7 @@ async def test_options_template_error( result3["flow_id"], user_input=data, ) - assert result4.get("type") == FlowResultType.FORM + assert result4.get("type") is FlowResultType.FORM assert result4["errors"] == {"still_image_url": "template_error"} # verify that an invalid template reports the correct UI error. @@ -720,7 +720,7 @@ async def test_options_template_error( user_input=data, ) - assert result5.get("type") == FlowResultType.FORM + assert result5.get("type") is FlowResultType.FORM assert result5["errors"] == {"stream_source": "template_error"} # verify that an relative stream url is rejected. @@ -730,7 +730,7 @@ async def test_options_template_error( result5["flow_id"], user_input=data, ) - assert result6.get("type") == FlowResultType.FORM + assert result6.get("type") is FlowResultType.FORM assert result6["errors"] == {"stream_source": "relative_url"} # verify that an malformed stream url is rejected. @@ -740,7 +740,7 @@ async def test_options_template_error( result6["flow_id"], user_input=data, ) - assert result7.get("type") == FlowResultType.FORM + assert result7.get("type") is FlowResultType.FORM assert result7["errors"] == {"stream_source": "malformed_url"} @@ -778,7 +778,7 @@ async def test_options_only_stream( await hass.async_block_till_done() result = await hass.config_entries.options.async_init(mock_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" # try updating the config options @@ -787,13 +787,13 @@ async def test_options_only_stream( result["flow_id"], user_input=data, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "confirm_still" result3 = await hass.config_entries.options.async_configure( result2["flow_id"], user_input={CONF_CONFIRMED_OK: True} ) - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["data"][CONF_CONTENT_TYPE] == "image/jpeg" @@ -886,7 +886,7 @@ async def test_use_wallclock_as_timestamps_option( result = await hass.config_entries.options.async_init( mock_entry.entry_id, context={"show_advanced_options": True} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" with ( patch("homeassistant.components.generic.async_setup_entry", return_value=True), @@ -896,12 +896,12 @@ async def test_use_wallclock_as_timestamps_option( result["flow_id"], user_input={CONF_USE_WALLCLOCK_AS_TIMESTAMPS: True, **TESTDATA}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM # Test what happens if user rejects the preview result3 = await hass.config_entries.options.async_configure( result2["flow_id"], user_input={CONF_CONFIRMED_OK: False} ) - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "init" with ( patch("homeassistant.components.generic.async_setup_entry", return_value=True), @@ -911,10 +911,10 @@ async def test_use_wallclock_as_timestamps_option( result3["flow_id"], user_input={CONF_USE_WALLCLOCK_AS_TIMESTAMPS: True, **TESTDATA}, ) - assert result4["type"] == FlowResultType.FORM + assert result4["type"] is FlowResultType.FORM assert result4["step_id"] == "confirm_still" result5 = await hass.config_entries.options.async_configure( result4["flow_id"], user_input={CONF_CONFIRMED_OK: True}, ) - assert result5["type"] == FlowResultType.CREATE_ENTRY + assert result5["type"] is FlowResultType.CREATE_ENTRY diff --git a/tests/components/geo_json_events/test_config_flow.py b/tests/components/geo_json_events/test_config_flow.py index a6e20ad4ba8..fe21bccc7aa 100644 --- a/tests/components/geo_json_events/test_config_flow.py +++ b/tests/components/geo_json_events/test_config_flow.py @@ -31,7 +31,7 @@ async def test_duplicate_error_user( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -44,7 +44,7 @@ async def test_duplicate_error_user( }, }, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -54,7 +54,7 @@ async def test_step_user(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -67,7 +67,7 @@ async def test_step_user(hass: HomeAssistant) -> None: }, }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert ( result["title"] == "http://geo.json.local/geo_json_events.json (-41.2, 174.7)" ) diff --git a/tests/components/geocaching/test_config_flow.py b/tests/components/geocaching/test_config_flow.py index 15f7ee0972f..f4e8f0c8a96 100644 --- a/tests/components/geocaching/test_config_flow.py +++ b/tests/components/geocaching/test_config_flow.py @@ -61,7 +61,7 @@ async def test_full_flow( }, ) - assert result.get("type") == FlowResultType.EXTERNAL_STEP + assert result.get("type") is FlowResultType.EXTERNAL_STEP assert result.get("step_id") == "auth" assert result.get("url") == ( f"{CURRENT_ENVIRONMENT_URLS['authorize_url']}?response_type=code&client_id={CLIENT_ID}" @@ -156,7 +156,7 @@ async def test_oauth_error( "redirect_uri": REDIRECT_URI, }, ) - assert result.get("type") == FlowResultType.EXTERNAL_STEP + assert result.get("type") is FlowResultType.EXTERNAL_STEP client = await hass_client_no_auth() resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") @@ -176,7 +176,7 @@ async def test_oauth_error( ) result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "oauth_error" assert len(hass.config_entries.async_entries(DOMAIN)) == 0 diff --git a/tests/components/geonetnz_quakes/test_config_flow.py b/tests/components/geonetnz_quakes/test_config_flow.py index d4b406cf054..61729776f9c 100644 --- a/tests/components/geonetnz_quakes/test_config_flow.py +++ b/tests/components/geonetnz_quakes/test_config_flow.py @@ -3,7 +3,7 @@ from datetime import timedelta from unittest.mock import patch -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.geonetnz_quakes import ( CONF_MINIMUM_MAGNITUDE, CONF_MMI, @@ -17,6 +17,7 @@ from homeassistant.const import ( CONF_UNIT_SYSTEM, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType async def test_duplicate_error(hass: HomeAssistant, config_entry) -> None: @@ -27,7 +28,7 @@ async def test_duplicate_error(hass: HomeAssistant, config_entry) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -36,7 +37,7 @@ async def test_show_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -64,7 +65,7 @@ async def test_step_import(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=conf ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "-41.2, 174.7" assert result["data"] == { CONF_LATITUDE: -41.2, @@ -95,7 +96,7 @@ async def test_step_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "-41.2, 174.7" assert result["data"] == { CONF_LATITUDE: -41.2, diff --git a/tests/components/geonetnz_volcano/test_config_flow.py b/tests/components/geonetnz_volcano/test_config_flow.py index e314896dd6b..b074bdffa20 100644 --- a/tests/components/geonetnz_volcano/test_config_flow.py +++ b/tests/components/geonetnz_volcano/test_config_flow.py @@ -3,7 +3,6 @@ from datetime import timedelta from unittest.mock import patch -from homeassistant import data_entry_flow from homeassistant.components.geonetnz_volcano import config_flow from homeassistant.const import ( CONF_LATITUDE, @@ -13,6 +12,7 @@ from homeassistant.const import ( CONF_UNIT_SYSTEM, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType async def test_duplicate_error(hass: HomeAssistant, config_entry) -> None: @@ -34,7 +34,7 @@ async def test_show_form(hass: HomeAssistant) -> None: result = await flow.async_step_user(user_input=None) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -61,7 +61,7 @@ async def test_step_import(hass: HomeAssistant) -> None: ), ): result = await flow.async_step_import(import_config=conf) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "-41.2, 174.7" assert result["data"] == { CONF_LATITUDE: -41.2, @@ -91,7 +91,7 @@ async def test_step_user(hass: HomeAssistant) -> None: ), ): result = await flow.async_step_user(user_input=conf) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "-41.2, 174.7" assert result["data"] == { CONF_LATITUDE: -41.2, diff --git a/tests/components/gios/test_config_flow.py b/tests/components/gios/test_config_flow.py index 4471cfa64ec..a96b065574a 100644 --- a/tests/components/gios/test_config_flow.py +++ b/tests/components/gios/test_config_flow.py @@ -5,11 +5,11 @@ from unittest.mock import patch from gios import ApiError -from homeassistant import data_entry_flow from homeassistant.components.gios import config_flow from homeassistant.components.gios.const import CONF_STATION_ID from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import STATIONS @@ -28,7 +28,7 @@ async def test_show_form(hass: HomeAssistant) -> None: result = await flow.async_step_user(user_input=None) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -112,7 +112,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: result = await flow.async_step_user(user_input=CONFIG) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Test Name 1" assert result["data"][CONF_STATION_ID] == CONFIG[CONF_STATION_ID] diff --git a/tests/components/github/test_config_flow.py b/tests/components/github/test_config_flow.py index c715889b7dc..882ed88edb2 100644 --- a/tests/components/github/test_config_flow.py +++ b/tests/components/github/test_config_flow.py @@ -60,7 +60,7 @@ async def test_full_user_flow_implementation( ) assert result["step_id"] == "device" - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS # Wait for the task to start before configuring await hass.async_block_till_done() @@ -74,7 +74,7 @@ async def test_full_user_flow_implementation( ) assert result["title"] == "" - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert "data" in result assert result["data"][CONF_ACCESS_TOKEN] == MOCK_ACCESS_TOKEN assert "options" in result @@ -94,7 +94,7 @@ async def test_flow_with_registration_failure( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result.get("reason") == "could_not_register" @@ -123,11 +123,11 @@ async def test_flow_with_activation_failure( context={"source": config_entries.SOURCE_USER}, ) assert result["step_id"] == "device" - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS await hass.async_block_till_done() result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "could_not_register" @@ -157,7 +157,7 @@ async def test_flow_with_remove_while_activating( context={"source": config_entries.SOURCE_USER}, ) assert result["step_id"] == "device" - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert hass.config_entries.flow.async_get(result["flow_id"]) @@ -181,7 +181,7 @@ async def test_already_configured( context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result.get("reason") == "already_configured" diff --git a/tests/components/glances/test_config_flow.py b/tests/components/glances/test_config_flow.py index 09dc638bb53..a7d6934e32d 100644 --- a/tests/components/glances/test_config_flow.py +++ b/tests/components/glances/test_config_flow.py @@ -32,14 +32,14 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( glances.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_INPUT ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "0.0.0.0:61208" assert result["data"] == MOCK_USER_INPUT @@ -65,7 +65,7 @@ async def test_form_fails( result["flow_id"], user_input=MOCK_USER_INPUT ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": message} @@ -80,7 +80,7 @@ async def test_form_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_INPUT ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -98,7 +98,7 @@ async def test_reauth_success(hass: HomeAssistant) -> None: data=MOCK_USER_INPUT, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["description_placeholders"] == {"username": "username"} @@ -109,7 +109,7 @@ async def test_reauth_success(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -137,7 +137,7 @@ async def test_reauth_fails( data=MOCK_USER_INPUT, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["description_placeholders"] == {"username": "username"} @@ -148,7 +148,7 @@ async def test_reauth_fails( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": message} result3 = await hass.config_entries.flow.async_configure( @@ -158,5 +158,5 @@ async def test_reauth_fails( }, ) - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" diff --git a/tests/components/goalzero/test_config_flow.py b/tests/components/goalzero/test_config_flow.py index 7e57312c5b6..a8a8f67bcc1 100644 --- a/tests/components/goalzero/test_config_flow.py +++ b/tests/components/goalzero/test_config_flow.py @@ -4,10 +4,10 @@ from unittest.mock import patch from goalzero import exceptions -from homeassistant import data_entry_flow from homeassistant.components.goalzero.const import DEFAULT_NAME, DOMAIN, MANUFACTURER from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import ( CONF_DATA, @@ -35,7 +35,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == CONF_DATA assert result["result"].unique_id == MAC @@ -48,7 +48,7 @@ async def test_flow_user_already_configured(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -59,7 +59,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "cannot_connect" @@ -71,7 +71,7 @@ async def test_flow_user_invalid_host(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "invalid_host" @@ -83,7 +83,7 @@ async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "unknown" @@ -98,14 +98,14 @@ async def test_dhcp_discovery(hass: HomeAssistant) -> None: context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {}, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MANUFACTURER assert result["data"] == CONF_DATA assert result["result"].unique_id == MAC @@ -115,7 +115,7 @@ async def test_dhcp_discovery(hass: HomeAssistant) -> None: context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -129,7 +129,7 @@ async def test_dhcp_discovery_failed(hass: HomeAssistant) -> None: context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" with patch_config_flow_yeti(mocked_yeti) as yetimock: @@ -139,7 +139,7 @@ async def test_dhcp_discovery_failed(hass: HomeAssistant) -> None: context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_host" with patch_config_flow_yeti(mocked_yeti) as yetimock: @@ -149,5 +149,5 @@ async def test_dhcp_discovery_failed(hass: HomeAssistant) -> None: context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" diff --git a/tests/components/gogogate2/test_config_flow.py b/tests/components/gogogate2/test_config_flow.py index a88dbd45116..25fb5922506 100644 --- a/tests/components/gogogate2/test_config_flow.py +++ b/tests/components/gogogate2/test_config_flow.py @@ -57,7 +57,7 @@ async def test_auth_fail( }, ) assert result - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == { "base": "invalid_auth", } @@ -77,7 +77,7 @@ async def test_auth_fail( }, ) assert result - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} api.reset_mock() @@ -95,7 +95,7 @@ async def test_auth_fail( }, ) assert result - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -115,7 +115,7 @@ async def test_form_homekit_unique_id_already_setup(hass: HomeAssistant) -> None type="mock_type", ), ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} flow = next( flow @@ -143,7 +143,7 @@ async def test_form_homekit_unique_id_already_setup(hass: HomeAssistant) -> None type="mock_type", ), ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT async def test_form_homekit_ip_address_already_setup(hass: HomeAssistant) -> None: @@ -168,7 +168,7 @@ async def test_form_homekit_ip_address_already_setup(hass: HomeAssistant) -> Non type="mock_type", ), ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT async def test_form_homekit_ip_address(hass: HomeAssistant) -> None: @@ -187,7 +187,7 @@ async def test_form_homekit_ip_address(hass: HomeAssistant) -> None: type="mock_type", ), ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} data_schema = result["data_schema"] @@ -217,7 +217,7 @@ async def test_discovered_dhcp( ip="1.2.3.4", macaddress=MOCK_MAC_ADDR, hostname="mock_hostname" ), ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -229,7 +229,7 @@ async def test_discovered_dhcp( }, ) assert result2 - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} api.reset_mock() @@ -245,7 +245,7 @@ async def test_discovered_dhcp( }, ) assert result3 - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["data"] == { "device": "ismartgate", "ip_address": "1.2.3.4", @@ -270,7 +270,7 @@ async def test_discovered_by_homekit_and_dhcp(hass: HomeAssistant) -> None: type="mock_type", ), ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_init( @@ -280,7 +280,7 @@ async def test_discovered_by_homekit_and_dhcp(hass: HomeAssistant) -> None: ip="1.2.3.4", macaddress=MOCK_MAC_ADDR, hostname="mock_hostname" ), ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_in_progress" result3 = await hass.config_entries.flow.async_init( @@ -290,5 +290,5 @@ async def test_discovered_by_homekit_and_dhcp(hass: HomeAssistant) -> None: ip="1.2.3.4", macaddress="00:00:00:00:00:00", hostname="mock_hostname" ), ) - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "already_in_progress" diff --git a/tests/components/goodwe/test_config_flow.py b/tests/components/goodwe/test_config_flow.py index 0d0a1249ea1..bede53ec9ed 100644 --- a/tests/components/goodwe/test_config_flow.py +++ b/tests/components/goodwe/test_config_flow.py @@ -32,7 +32,7 @@ async def test_manual_setup(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -50,7 +50,7 @@ async def test_manual_setup(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == { CONF_HOST: TEST_HOST, @@ -68,7 +68,7 @@ async def test_manual_setup_already_exists(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -84,7 +84,7 @@ async def test_manual_setup_already_exists(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -93,7 +93,7 @@ async def test_manual_setup_device_offline(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -106,5 +106,5 @@ async def test_manual_setup_device_offline(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_HOST: "connection_error"} diff --git a/tests/components/google/test_config_flow.py b/tests/components/google/test_config_flow.py index f8eff022d9f..c27808c24aa 100644 --- a/tests/components/google/test_config_flow.py +++ b/tests/components/google/test_config_flow.py @@ -716,7 +716,7 @@ async def test_web_auth_compatibility( "homeassistant.components.google.async_setup_entry", return_value=True ) as mock_setup: result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY token = result.get("data", {}).get("token", {}) del token["expires_at"] assert token == { @@ -820,7 +820,7 @@ async def test_web_reauth_flow( "homeassistant.components.google.async_setup_entry", return_value=True ) as mock_setup: result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" entries = hass.config_entries.async_entries(DOMAIN) 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 6ae42a350e6..3bac01db42d 100644 --- a/tests/components/google_generative_ai_conversation/test_config_flow.py +++ b/tests/components/google_generative_ai_conversation/test_config_flow.py @@ -36,7 +36,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -56,7 +56,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { "api_key": "bla", } @@ -78,7 +78,7 @@ async def test_options( }, ) await hass.async_block_till_done() - assert options["type"] == FlowResultType.CREATE_ENTRY + assert options["type"] is FlowResultType.CREATE_ENTRY assert options["data"]["prompt"] == "Speak like a pirate" assert options["data"]["temperature"] == 0.3 assert options["data"][CONF_CHAT_MODEL] == DEFAULT_CHAT_MODEL @@ -121,5 +121,5 @@ async def test_form_errors(hass: HomeAssistant, side_effect, error) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": error} diff --git a/tests/components/google_tasks/test_config_flow.py b/tests/components/google_tasks/test_config_flow.py index d7ad21292fc..24801959674 100644 --- a/tests/components/google_tasks/test_config_flow.py +++ b/tests/components/google_tasks/test_config_flow.py @@ -70,7 +70,7 @@ async def test_full_flow( patch("homeassistant.components.google_tasks.config_flow.build"), ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(mock_setup.mock_calls) == 1 @@ -126,7 +126,7 @@ async def test_api_not_enabled( ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "access_not_configured" assert ( result["description_placeholders"]["message"] @@ -182,5 +182,5 @@ async def test_general_exception( ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" diff --git a/tests/components/google_translate/test_config_flow.py b/tests/components/google_translate/test_config_flow.py index a4104fc0908..36399c6770a 100644 --- a/tests/components/google_translate/test_config_flow.py +++ b/tests/components/google_translate/test_config_flow.py @@ -20,7 +20,7 @@ async def test_user_step(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -32,7 +32,7 @@ async def test_user_step(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Google Translate text-to-speech" assert result["data"] == { CONF_LANG: "de", @@ -53,7 +53,7 @@ async def test_already_configured( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -64,7 +64,7 @@ async def test_already_configured( }, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(mock_setup_entry.mock_calls) == 0 @@ -77,7 +77,7 @@ async def test_onboarding_flow( DOMAIN, context={"source": "onboarding"} ) - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == "Google Translate text-to-speech" assert result.get("data") == { CONF_LANG: "en", diff --git a/tests/components/google_travel_time/test_config_flow.py b/tests/components/google_travel_time/test_config_flow.py index 24e9cb1297a..6e73bfd8d23 100644 --- a/tests/components/google_travel_time/test_config_flow.py +++ b/tests/components/google_travel_time/test_config_flow.py @@ -2,7 +2,7 @@ import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.google_travel_time.const import ( ARRIVAL_TIME, CONF_ARRIVAL_TIME, @@ -23,6 +23,7 @@ from homeassistant.components.google_travel_time.const import ( ) from homeassistant.const import CONF_API_KEY, CONF_LANGUAGE, CONF_MODE, CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .const import MOCK_CONFIG @@ -33,7 +34,7 @@ async def test_minimum_fields(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -41,7 +42,7 @@ async def test_minimum_fields(hass: HomeAssistant) -> None: MOCK_CONFIG, ) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == DEFAULT_NAME assert result2["data"] == { CONF_NAME: DEFAULT_NAME, @@ -57,14 +58,14 @@ async def test_invalid_config_entry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + 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"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -74,14 +75,14 @@ async def test_invalid_api_key(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + 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"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -91,14 +92,14 @@ async def test_transport_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + 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"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -108,14 +109,14 @@ async def test_timeout(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + 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"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "timeout_connect"} @@ -124,14 +125,14 @@ async def test_malformed_api_key(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + 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"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -154,7 +155,7 @@ async def test_options_flow(hass: HomeAssistant, mock_config) -> None: mock_config.entry_id, data=None ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -171,7 +172,7 @@ async def test_options_flow(hass: HomeAssistant, mock_config) -> None: CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"] == { CONF_MODE: "driving", @@ -215,7 +216,7 @@ async def test_options_flow_departure_time(hass: HomeAssistant, mock_config) -> mock_config.entry_id, data=None ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -232,7 +233,7 @@ async def test_options_flow_departure_time(hass: HomeAssistant, mock_config) -> CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"] == { CONF_MODE: "driving", @@ -285,7 +286,7 @@ async def test_reset_departure_time(hass: HomeAssistant, mock_config) -> None: mock_config.entry_id, data=None ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -331,7 +332,7 @@ async def test_reset_arrival_time(hass: HomeAssistant, mock_config) -> None: mock_config.entry_id, data=None ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -375,7 +376,7 @@ async def test_reset_options_flow_fields(hass: HomeAssistant, mock_config) -> No mock_config.entry_id, data=None ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -401,7 +402,7 @@ async def test_dupe(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -413,13 +414,13 @@ async def test_dupe(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -432,4 +433,4 @@ async def test_dupe(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY diff --git a/tests/components/govee_ble/test_config_flow.py b/tests/components/govee_ble/test_config_flow.py index 4b498b2618a..0c340c01f2a 100644 --- a/tests/components/govee_ble/test_config_flow.py +++ b/tests/components/govee_ble/test_config_flow.py @@ -19,7 +19,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=GVH5075_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( "homeassistant.components.govee_ble.async_setup_entry", return_value=True @@ -27,7 +27,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "H5075 2762" assert result2["data"] == {} assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" @@ -40,7 +40,7 @@ async def test_async_step_bluetooth_not_govee(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_GOVEE_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -50,7 +50,7 @@ async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -64,7 +64,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( "homeassistant.components.govee_ble.async_setup_entry", return_value=True @@ -73,7 +73,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: result["flow_id"], user_input={"address": "4125DDBA-2774-4851-9889-6AADDD4CAC3D"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "H5177 2EC8" assert result2["data"] == {} assert result2["result"].unique_id == "4125DDBA-2774-4851-9889-6AADDD4CAC3D" @@ -89,7 +89,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -105,7 +105,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - result["flow_id"], user_input={"address": "4125DDBA-2774-4851-9889-6AADDD4CAC3D"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -127,7 +127,7 @@ async def test_async_step_user_with_found_devices_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -144,7 +144,7 @@ async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant) - context={"source": config_entries.SOURCE_BLUETOOTH}, data=GVH5177_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -155,7 +155,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=GVH5177_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -163,7 +163,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=GVH5177_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -176,7 +176,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=GVH5177_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -187,7 +187,7 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.govee_ble.async_setup_entry", return_value=True @@ -196,7 +196,7 @@ async def test_async_step_user_takes_precedence_over_discovery( result["flow_id"], user_input={"address": "4125DDBA-2774-4851-9889-6AADDD4CAC3D"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "H5177 2EC8" assert result2["data"] == {} assert result2["result"].unique_id == "4125DDBA-2774-4851-9889-6AADDD4CAC3D" diff --git a/tests/components/govee_light_local/test_config_flow.py b/tests/components/govee_light_local/test_config_flow.py index 79baef33969..1f935f18530 100644 --- a/tests/components/govee_light_local/test_config_flow.py +++ b/tests/components/govee_light_local/test_config_flow.py @@ -4,9 +4,10 @@ from unittest.mock import AsyncMock, patch from govee_local_api import GoveeDevice -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.govee_light_local.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .conftest import DEFAULT_CAPABILITEIS @@ -33,10 +34,10 @@ async def test_creating_entry_has_no_devices( ) # Confirmation form - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT await hass.async_block_till_done() @@ -70,10 +71,10 @@ async def test_creating_entry_has_with_devices( ) # Confirmation form - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() diff --git a/tests/components/gpsd/test_config_flow.py b/tests/components/gpsd/test_config_flow.py index 81d6681dabd..6f330571076 100644 --- a/tests/components/gpsd/test_config_flow.py +++ b/tests/components/gpsd/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import AsyncMock, patch from gps3.agps3threaded import GPSD_PORT as DEFAULT_PORT -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.gpsd.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT from homeassistant.core import HomeAssistant @@ -18,7 +18,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch("socket.socket") as mock_socket: mock_connect = mock_socket.return_value.connect @@ -32,7 +32,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == f"GPS {HOST}" assert result2["data"] == { CONF_HOST: HOST, @@ -53,7 +53,7 @@ async def test_connection_error(hass: HomeAssistant) -> None: data={CONF_HOST: "nonexistent.local", CONF_PORT: 1234}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -68,7 +68,7 @@ async def test_import(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_IMPORT}, data={CONF_HOST: HOST, CONF_PORT: 1234, CONF_NAME: "MyGPS"}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "MyGPS" assert result["data"] == { CONF_HOST: HOST, diff --git a/tests/components/gree/test_config_flow.py b/tests/components/gree/test_config_flow.py index 7127af6b913..af374fb4245 100644 --- a/tests/components/gree/test_config_flow.py +++ b/tests/components/gree/test_config_flow.py @@ -4,9 +4,10 @@ from unittest.mock import AsyncMock, patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.gree.const import DOMAIN as GREE_DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .common import FakeDiscovery @@ -27,10 +28,10 @@ async def test_creating_entry_sets_up_climate( ) # Confirmation form - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -53,10 +54,10 @@ async def test_creating_entry_has_no_devices( ) # Confirmation form - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT await hass.async_block_till_done() diff --git a/tests/components/group/test_config_flow.py b/tests/components/group/test_config_flow.py index 623bcb5c1ee..3de6d9ac32d 100644 --- a/tests/components/group/test_config_flow.py +++ b/tests/components/group/test_config_flow.py @@ -76,14 +76,14 @@ async def test_config_flow( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU result = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": group_type}, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == group_type with patch( @@ -99,7 +99,7 @@ async def test_config_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Living Room" assert result["data"] == {} assert result["options"] == { @@ -170,14 +170,14 @@ async def test_config_flow_hides_members( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU result = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": group_type}, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == group_type result = await hass.config_entries.flow.async_configure( @@ -191,7 +191,7 @@ async def test_config_flow_hides_members( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert entity_registry.async_get(f"{group_type}.one").hidden_by == hidden_by assert entity_registry.async_get(f"{group_type}.three").hidden_by == hidden_by @@ -261,7 +261,7 @@ async def test_options( config_entry = hass.config_entries.async_entries(DOMAIN)[0] result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == group_type assert get_suggested(result["data_schema"].schema, "entities") == members1 assert "name" not in result["data_schema"].schema @@ -273,7 +273,7 @@ async def test_options( result["flow_id"], user_input={"entities": members2, **options_options}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "entities": members2, "group_type": group_type, @@ -300,14 +300,14 @@ async def test_options( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU result = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": group_type}, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == group_type assert get_suggested(result["data_schema"].schema, "entities") is None @@ -357,7 +357,7 @@ async def test_all_options( result = await hass.config_entries.options.async_init( config_entry.entry_id, context={"show_advanced_options": advanced} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == group_type result = await hass.config_entries.options.async_configure( @@ -366,7 +366,7 @@ async def test_all_options( "entities": members2, }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "entities": members2, "group_type": group_type, @@ -454,7 +454,7 @@ async def test_options_flow_hides_members( await hass.async_block_till_done() result = await hass.config_entries.options.async_init(group_config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -465,7 +465,7 @@ async def test_options_flow_hides_members( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert entity_registry.async_get(f"{group_type}.one").hidden_by == hidden_by assert entity_registry.async_get(f"{group_type}.three").hidden_by == hidden_by @@ -518,14 +518,14 @@ async def test_config_flow_preview( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU result = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": domain}, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == domain assert result["errors"] is None assert result["preview"] == "group" @@ -626,7 +626,7 @@ async def test_option_flow_preview( client = await hass_ws_client(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["preview"] == "group" @@ -681,7 +681,7 @@ async def test_option_flow_sensor_preview_config_entry_removed( 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["type"] is FlowResultType.FORM assert result["errors"] is None assert result["preview"] == "group" diff --git a/tests/components/growatt_server/test_config_flow.py b/tests/components/growatt_server/test_config_flow.py index 57777a57783..81ca870a22e 100644 --- a/tests/components/growatt_server/test_config_flow.py +++ b/tests/components/growatt_server/test_config_flow.py @@ -3,7 +3,7 @@ from copy import deepcopy from unittest.mock import patch -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.growatt_server.const import ( CONF_PLANT_ID, DEFAULT_URL, @@ -12,6 +12,7 @@ from homeassistant.components.growatt_server.const import ( ) from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -52,7 +53,7 @@ async def test_show_authenticate_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -70,7 +71,7 @@ async def test_incorrect_login(hass: HomeAssistant) -> None: result["flow_id"], FIXTURE_USER_INPUT ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -116,7 +117,7 @@ async def test_multiple_plant_ids(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "plant" user_input = {CONF_PLANT_ID: "123456"} @@ -125,7 +126,7 @@ async def test_multiple_plant_ids(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME] assert result["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] assert result["data"][CONF_PLANT_ID] == "123456" @@ -153,7 +154,7 @@ async def test_one_plant_on_account(hass: HomeAssistant) -> None: result["flow_id"], user_input ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME] assert result["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] assert result["data"][CONF_PLANT_ID] == "123456" diff --git a/tests/components/guardian/test_config_flow.py b/tests/components/guardian/test_config_flow.py index 3922b196e4b..06ce37a32af 100644 --- a/tests/components/guardian/test_config_flow.py +++ b/tests/components/guardian/test_config_flow.py @@ -6,7 +6,6 @@ from unittest.mock import patch from aioguardian.errors import GuardianError import pytest -from homeassistant import data_entry_flow from homeassistant.components import dhcp, zeroconf from homeassistant.components.guardian import CONF_UID, DOMAIN from homeassistant.components.guardian.config_flow import ( @@ -16,6 +15,7 @@ from homeassistant.components.guardian.config_flow import ( from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -29,7 +29,7 @@ async def test_duplicate_error( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -42,7 +42,7 @@ async def test_connect_error(hass: HomeAssistant, config) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_IP_ADDRESS: "cannot_connect"} @@ -63,13 +63,13 @@ async def test_step_user(hass: HomeAssistant, config, setup_guardian) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "ABCDEF123456" assert result["data"] == { CONF_IP_ADDRESS: "192.168.1.100", @@ -93,13 +93,13 @@ async def test_step_zeroconf(hass: HomeAssistant, setup_guardian) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf_data ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "ABCDEF123456" assert result["data"] == { CONF_IP_ADDRESS: "192.168.1.100", @@ -123,7 +123,7 @@ async def test_step_zeroconf_already_in_progress(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf_data ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_init( @@ -144,13 +144,13 @@ async def test_step_dhcp(hass: HomeAssistant, setup_guardian) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, data=dhcp_data ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "ABCDEF123456" assert result["data"] == { CONF_IP_ADDRESS: "192.168.1.100", @@ -170,7 +170,7 @@ async def test_step_dhcp_already_in_progress(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, data=dhcp_data ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_init( @@ -196,7 +196,7 @@ async def test_step_dhcp_already_setup_match_mac(hass: HomeAssistant) -> None: macaddress="aabbccddabcd", ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -218,5 +218,5 @@ async def test_step_dhcp_already_setup_match_ip(hass: HomeAssistant) -> None: macaddress="aabbccddabcd", ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/hardkernel/test_config_flow.py b/tests/components/hardkernel/test_config_flow.py index 1965e2af8c7..998c024dc72 100644 --- a/tests/components/hardkernel/test_config_flow.py +++ b/tests/components/hardkernel/test_config_flow.py @@ -21,7 +21,7 @@ async def test_config_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": "system"} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Hardkernel" assert result["data"] == {} assert result["options"] == {} @@ -54,6 +54,6 @@ async def test_config_flow_single_entry(hass: HomeAssistant) -> None: DOMAIN, context={"source": "system"} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" mock_setup_entry.assert_not_called() diff --git a/tests/components/harmony/test_config_flow.py b/tests/components/harmony/test_config_flow.py index c2daa98728b..3dc5a612452 100644 --- a/tests/components/harmony/test_config_flow.py +++ b/tests/components/harmony/test_config_flow.py @@ -4,12 +4,13 @@ from unittest.mock import AsyncMock, MagicMock, patch import aiohttp -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.components.harmony.config_flow import CannotConnect from homeassistant.components.harmony.const import DOMAIN, PREVIOUS_ACTIVE_ACTIVITY 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 @@ -211,7 +212,7 @@ async def test_options_flow(hass: HomeAssistant, mock_hc, mock_write_config) -> assert await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -219,7 +220,7 @@ async def test_options_flow(hass: HomeAssistant, mock_hc, mock_write_config) -> user_input={"activity": PREVIOUS_ACTIVE_ACTIVITY, "delay_secs": 0.4}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { "activity": PREVIOUS_ACTIVE_ACTIVITY, "delay_secs": 0.4, diff --git a/tests/components/heos/test_config_flow.py b/tests/components/heos/test_config_flow.py index 7f0cd6cbd5a..7b737d7bb4b 100644 --- a/tests/components/heos/test_config_flow.py +++ b/tests/components/heos/test_config_flow.py @@ -5,13 +5,13 @@ from urllib.parse import urlparse from pyheos import HeosError -from homeassistant import data_entry_flow from homeassistant.components import heos, ssdp from homeassistant.components.heos.config_flow import HeosFlowHandler from homeassistant.components.heos.const import DATA_DISCOVERED_HOSTS, DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_SSDP, SOURCE_USER from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType async def test_flow_aborts_already_setup(hass: HomeAssistant, config_entry) -> None: @@ -20,7 +20,7 @@ async def test_flow_aborts_already_setup(hass: HomeAssistant, config_entry) -> N flow = HeosFlowHandler() flow.hass = hass result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -29,7 +29,7 @@ async def test_no_host_shows_form(hass: HomeAssistant) -> None: flow = HeosFlowHandler() flow.hass = hass result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -40,7 +40,7 @@ async def test_cannot_connect_shows_error_form(hass: HomeAssistant, controller) result = await hass.config_entries.flow.async_init( heos.DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "127.0.0.1"} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"][CONF_HOST] == "cannot_connect" assert controller.connect.call_count == 1 @@ -56,7 +56,7 @@ async def test_create_entry_when_host_valid(hass: HomeAssistant, controller) -> result = await hass.config_entries.flow.async_init( heos.DOMAIN, context={"source": SOURCE_USER}, data=data ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == DOMAIN assert result["title"] == "Controller (127.0.0.1)" assert result["data"] == data @@ -74,7 +74,7 @@ async def test_create_entry_when_friendly_name_valid( result = await hass.config_entries.flow.async_init( heos.DOMAIN, context={"source": SOURCE_USER}, data=data ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == DOMAIN assert result["title"] == "Controller (127.0.0.1)" assert result["data"] == {CONF_HOST: "127.0.0.1"} @@ -122,7 +122,7 @@ async def test_discovery_flow_aborts_already_setup( flow = HeosFlowHandler() flow.hass = hass result = await flow.async_step_ssdp(discovery_data) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -155,5 +155,5 @@ async def test_import_sets_the_unique_id(hass: HomeAssistant, controller) -> Non data={CONF_HOST: "127.0.0.2"}, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == DOMAIN diff --git a/tests/components/here_travel_time/test_config_flow.py b/tests/components/here_travel_time/test_config_flow.py index 51b12978856..1309878a2f3 100644 --- a/tests/components/here_travel_time/test_config_flow.py +++ b/tests/components/here_travel_time/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import patch from here_routing import HERERoutingError, HERERoutingUnauthorizedError import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.here_travel_time.const import ( CONF_ARRIVAL_TIME, CONF_DEPARTURE_TIME, @@ -22,6 +22,7 @@ from homeassistant.components.here_travel_time.const import ( ) from homeassistant.const import CONF_API_KEY, CONF_MODE, CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .const import ( API_KEY, @@ -46,7 +47,7 @@ def bypass_setup_fixture(): @pytest.fixture(name="user_step_result") -async def user_step_result_fixture(hass: HomeAssistant) -> data_entry_flow.FlowResult: +async def user_step_result_fixture(hass: HomeAssistant) -> FlowResultType: """Provide the result of a completed user step.""" init_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -64,7 +65,7 @@ async def user_step_result_fixture(hass: HomeAssistant) -> data_entry_flow.FlowR @pytest.fixture(name="option_init_result") -async def option_init_result_fixture(hass: HomeAssistant) -> data_entry_flow.FlowResult: +async def option_init_result_fixture(hass: HomeAssistant) -> FlowResultType: """Provide the result of a completed options init step.""" entry = MockConfigEntry( domain=DOMAIN, @@ -94,8 +95,8 @@ async def option_init_result_fixture(hass: HomeAssistant) -> data_entry_flow.Flo @pytest.fixture(name="origin_step_result") async def origin_step_result_fixture( - hass: HomeAssistant, user_step_result: data_entry_flow.FlowResult -) -> data_entry_flow.FlowResult: + hass: HomeAssistant, user_step_result: FlowResultType +) -> FlowResultType: """Provide the result of a completed origin by coordinates step.""" origin_menu_result = await hass.config_entries.flow.async_configure( user_step_result["flow_id"], {"next_step_id": "origin_coordinates"} @@ -124,7 +125,7 @@ async def test_step_user(hass: HomeAssistant, menu_options) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -137,19 +138,19 @@ async def test_step_user(hass: HomeAssistant, menu_options) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.MENU + assert result2["type"] is FlowResultType.MENU assert result2["menu_options"] == menu_options @pytest.mark.usefixtures("valid_response") async def test_step_origin_coordinates( - hass: HomeAssistant, user_step_result: data_entry_flow.FlowResult + hass: HomeAssistant, user_step_result: FlowResultType ) -> None: """Test the origin coordinates step.""" menu_result = await hass.config_entries.flow.async_configure( user_step_result["flow_id"], {"next_step_id": "origin_coordinates"} ) - assert menu_result["type"] == data_entry_flow.FlowResultType.FORM + assert menu_result["type"] is FlowResultType.FORM location_selector_result = await hass.config_entries.flow.async_configure( menu_result["flow_id"], @@ -161,35 +162,35 @@ async def test_step_origin_coordinates( } }, ) - assert location_selector_result["type"] == data_entry_flow.FlowResultType.MENU + assert location_selector_result["type"] is FlowResultType.MENU @pytest.mark.usefixtures("valid_response") async def test_step_origin_entity( - hass: HomeAssistant, user_step_result: data_entry_flow.FlowResult + hass: HomeAssistant, user_step_result: FlowResultType ) -> None: """Test the origin coordinates step.""" menu_result = await hass.config_entries.flow.async_configure( user_step_result["flow_id"], {"next_step_id": "origin_entity"} ) - assert menu_result["type"] == data_entry_flow.FlowResultType.FORM + assert menu_result["type"] is FlowResultType.FORM entity_selector_result = await hass.config_entries.flow.async_configure( menu_result["flow_id"], {"origin_entity_id": "zone.home"}, ) - assert entity_selector_result["type"] == data_entry_flow.FlowResultType.MENU + assert entity_selector_result["type"] is FlowResultType.MENU @pytest.mark.usefixtures("valid_response") async def test_step_destination_coordinates( - hass: HomeAssistant, origin_step_result: data_entry_flow.FlowResult + hass: HomeAssistant, origin_step_result: FlowResultType ) -> None: """Test the origin coordinates step.""" menu_result = await hass.config_entries.flow.async_configure( origin_step_result["flow_id"], {"next_step_id": "destination_coordinates"} ) - assert menu_result["type"] == data_entry_flow.FlowResultType.FORM + assert menu_result["type"] is FlowResultType.FORM location_selector_result = await hass.config_entries.flow.async_configure( menu_result["flow_id"], @@ -201,9 +202,7 @@ async def test_step_destination_coordinates( } }, ) - assert ( - location_selector_result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - ) + assert location_selector_result["type"] is FlowResultType.CREATE_ENTRY entry = hass.config_entries.async_entries(DOMAIN)[0] assert entry.data == { CONF_NAME: "test", @@ -219,19 +218,19 @@ async def test_step_destination_coordinates( @pytest.mark.usefixtures("valid_response") async def test_step_destination_entity( hass: HomeAssistant, - origin_step_result: data_entry_flow.FlowResult, + origin_step_result: FlowResultType, ) -> None: """Test the origin coordinates step.""" menu_result = await hass.config_entries.flow.async_configure( origin_step_result["flow_id"], {"next_step_id": "destination_entity"} ) - assert menu_result["type"] == data_entry_flow.FlowResultType.FORM + assert menu_result["type"] is FlowResultType.FORM entity_selector_result = await hass.config_entries.flow.async_configure( menu_result["flow_id"], {"destination_entity_id": "zone.home"}, ) - assert entity_selector_result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert entity_selector_result["type"] is FlowResultType.CREATE_ENTRY entry = hass.config_entries.async_entries(DOMAIN)[0] assert entry.data == { CONF_NAME: "test", @@ -310,7 +309,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -320,18 +319,18 @@ async def test_options_flow(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.MENU + assert result["type"] is FlowResultType.MENU @pytest.mark.usefixtures("valid_response") async def test_options_flow_arrival_time_step( - hass: HomeAssistant, option_init_result: data_entry_flow.FlowResult + hass: HomeAssistant, option_init_result: FlowResultType ) -> None: """Test the options flow arrival time type.""" menu_result = await hass.config_entries.options.async_configure( option_init_result["flow_id"], {"next_step_id": "arrival_time"} ) - assert menu_result["type"] == data_entry_flow.FlowResultType.FORM + assert menu_result["type"] is FlowResultType.FORM time_selector_result = await hass.config_entries.options.async_configure( option_init_result["flow_id"], user_input={ @@ -339,7 +338,7 @@ async def test_options_flow_arrival_time_step( }, ) - assert time_selector_result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert time_selector_result["type"] is FlowResultType.CREATE_ENTRY entry = hass.config_entries.async_entries(DOMAIN)[0] assert entry.options == { CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, @@ -349,13 +348,13 @@ async def test_options_flow_arrival_time_step( @pytest.mark.usefixtures("valid_response") async def test_options_flow_departure_time_step( - hass: HomeAssistant, option_init_result: data_entry_flow.FlowResult + hass: HomeAssistant, option_init_result: FlowResultType ) -> None: """Test the options flow departure time type.""" menu_result = await hass.config_entries.options.async_configure( option_init_result["flow_id"], {"next_step_id": "departure_time"} ) - assert menu_result["type"] == data_entry_flow.FlowResultType.FORM + assert menu_result["type"] is FlowResultType.FORM time_selector_result = await hass.config_entries.options.async_configure( option_init_result["flow_id"], user_input={ @@ -363,7 +362,7 @@ async def test_options_flow_departure_time_step( }, ) - assert time_selector_result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert time_selector_result["type"] is FlowResultType.CREATE_ENTRY entry = hass.config_entries.async_entries(DOMAIN)[0] assert entry.options == { CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, @@ -373,14 +372,14 @@ async def test_options_flow_departure_time_step( @pytest.mark.usefixtures("valid_response") async def test_options_flow_no_time_step( - hass: HomeAssistant, option_init_result: data_entry_flow.FlowResult + hass: HomeAssistant, option_init_result: FlowResultType ) -> None: """Test the options flow arrival time type.""" menu_result = await hass.config_entries.options.async_configure( option_init_result["flow_id"], {"next_step_id": "no_time"} ) - assert menu_result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert menu_result["type"] is FlowResultType.CREATE_ENTRY entry = hass.config_entries.async_entries(DOMAIN)[0] assert entry.options == { CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, diff --git a/tests/components/hive/test_config_flow.py b/tests/components/hive/test_config_flow.py index b1553b2c485..fd6eb564a39 100644 --- a/tests/components/hive/test_config_flow.py +++ b/tests/components/hive/test_config_flow.py @@ -4,10 +4,11 @@ from unittest.mock import patch from apyhiveapi.helper import hive_exceptions -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.hive.const import CONF_CODE, CONF_DEVICE_NAME, DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -52,7 +53,7 @@ async def test_import_flow(hass: HomeAssistant) -> None: data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == USERNAME assert result["data"] == { CONF_USERNAME: USERNAME, @@ -76,7 +77,7 @@ async def test_user_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -104,7 +105,7 @@ async def test_user_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == USERNAME assert result2["data"] == { CONF_USERNAME: USERNAME, @@ -129,7 +130,7 @@ async def test_user_flow_2fa(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -146,7 +147,7 @@ async def test_user_flow_2fa(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == CONF_CODE assert result2["errors"] == {} @@ -167,7 +168,7 @@ async def test_user_flow_2fa(hass: HomeAssistant) -> None: }, ) - assert result3["type"] == data_entry_flow.FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "configuration" assert result3["errors"] == {} @@ -200,7 +201,7 @@ async def test_user_flow_2fa(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result4["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == USERNAME assert result4["data"] == { CONF_USERNAME: USERNAME, @@ -254,7 +255,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: data=mock_config.data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_password"} with patch( @@ -278,7 +279,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: assert mock_config.data.get("username") == USERNAME assert mock_config.data.get("password") == UPDATED_PASSWORD - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -313,7 +314,7 @@ async def test_reauth_2fa_flow(hass: HomeAssistant) -> None: data=mock_config.data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_password"} with patch( @@ -356,7 +357,7 @@ async def test_reauth_2fa_flow(hass: HomeAssistant) -> None: assert mock_config.data.get("username") == USERNAME assert mock_config.data.get("password") == UPDATED_PASSWORD - assert result3["type"] == data_entry_flow.FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -388,14 +389,14 @@ async def test_option_flow(hass: HomeAssistant) -> None: data=None, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_SCAN_INTERVAL: UPDATED_SCAN_INTERVAL} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_SCAN_INTERVAL] == UPDATED_SCAN_INTERVAL @@ -405,7 +406,7 @@ async def test_user_flow_2fa_send_new_code(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -422,7 +423,7 @@ async def test_user_flow_2fa_send_new_code(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == CONF_CODE assert result2["errors"] == {} @@ -437,7 +438,7 @@ async def test_user_flow_2fa_send_new_code(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == CONF_CODE assert result3["errors"] == {} @@ -458,7 +459,7 @@ async def test_user_flow_2fa_send_new_code(hass: HomeAssistant) -> None: }, ) - assert result4["type"] == data_entry_flow.FlowResultType.FORM + assert result4["type"] is FlowResultType.FORM assert result4["step_id"] == "configuration" assert result4["errors"] == {} @@ -488,7 +489,7 @@ async def test_user_flow_2fa_send_new_code(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result5["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result5["type"] is FlowResultType.CREATE_ENTRY assert result5["title"] == USERNAME assert result5["data"] == { CONF_USERNAME: USERNAME, @@ -530,7 +531,7 @@ async def test_abort_if_existing_entry(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -540,7 +541,7 @@ async def test_user_flow_invalid_username(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -552,7 +553,7 @@ async def test_user_flow_invalid_username(hass: HomeAssistant) -> None: {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_username"} @@ -563,7 +564,7 @@ async def test_user_flow_invalid_password(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -575,7 +576,7 @@ async def test_user_flow_invalid_password(hass: HomeAssistant) -> None: {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_password"} @@ -587,7 +588,7 @@ async def test_user_flow_no_internet_connection(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -599,7 +600,7 @@ async def test_user_flow_no_internet_connection(hass: HomeAssistant) -> None: {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "no_internet_available"} @@ -611,7 +612,7 @@ async def test_user_flow_2fa_no_internet_connection(hass: HomeAssistant) -> None DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -625,7 +626,7 @@ async def test_user_flow_2fa_no_internet_connection(hass: HomeAssistant) -> None {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == CONF_CODE assert result2["errors"] == {} @@ -638,7 +639,7 @@ async def test_user_flow_2fa_no_internet_connection(hass: HomeAssistant) -> None {CONF_CODE: MFA_CODE}, ) - assert result3["type"] == data_entry_flow.FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == CONF_CODE assert result3["errors"] == {"base": "no_internet_available"} @@ -649,7 +650,7 @@ async def test_user_flow_2fa_invalid_code(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -663,7 +664,7 @@ async def test_user_flow_2fa_invalid_code(hass: HomeAssistant) -> None: {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == CONF_CODE assert result2["errors"] == {} @@ -675,7 +676,7 @@ async def test_user_flow_2fa_invalid_code(hass: HomeAssistant) -> None: result["flow_id"], {CONF_CODE: MFA_INVALID_CODE}, ) - assert result3["type"] == data_entry_flow.FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == CONF_CODE assert result3["errors"] == {"base": "invalid_code"} @@ -686,7 +687,7 @@ async def test_user_flow_unknown_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -699,7 +700,7 @@ async def test_user_flow_unknown_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -709,7 +710,7 @@ async def test_user_flow_2fa_unknown_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -723,7 +724,7 @@ async def test_user_flow_2fa_unknown_error(hass: HomeAssistant) -> None: {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == CONF_CODE with patch( @@ -735,7 +736,7 @@ async def test_user_flow_2fa_unknown_error(hass: HomeAssistant) -> None: {CONF_CODE: MFA_CODE}, ) - assert result3["type"] == data_entry_flow.FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "configuration" assert result3["errors"] == {} @@ -759,6 +760,6 @@ async def test_user_flow_2fa_unknown_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result4["type"] == data_entry_flow.FlowResultType.FORM + assert result4["type"] is FlowResultType.FORM assert result4["step_id"] == "configuration" assert result4["errors"] == {"base": "unknown"} diff --git a/tests/components/hko/test_config_flow.py b/tests/components/hko/test_config_flow.py index ce32d2cd0da..7a2cec961db 100644 --- a/tests/components/hko/test_config_flow.py +++ b/tests/components/hko/test_config_flow.py @@ -17,7 +17,7 @@ async def test_config_flow_default(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == SOURCE_USER assert "flow_id" in result @@ -26,7 +26,7 @@ async def test_config_flow_default(hass: HomeAssistant) -> None: user_input={CONF_LOCATION: DEFAULT_LOCATION}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == DEFAULT_LOCATION assert result2["result"].unique_id == DEFAULT_LOCATION assert result2["data"][CONF_LOCATION] == DEFAULT_LOCATION @@ -42,7 +42,7 @@ async def test_config_flow_cannot_connect(hass: HomeAssistant) -> None: data={CONF_LOCATION: DEFAULT_LOCATION}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == "cannot_connect" client_mock.side_effect = None @@ -53,7 +53,7 @@ async def test_config_flow_cannot_connect(hass: HomeAssistant) -> None: data={CONF_LOCATION: DEFAULT_LOCATION}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == DEFAULT_LOCATION assert result["data"][CONF_LOCATION] == DEFAULT_LOCATION @@ -68,7 +68,7 @@ async def test_config_flow_timeout(hass: HomeAssistant) -> None: data={CONF_LOCATION: DEFAULT_LOCATION}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == "unknown" client_mock.side_effect = None @@ -79,7 +79,7 @@ async def test_config_flow_timeout(hass: HomeAssistant) -> None: data={CONF_LOCATION: DEFAULT_LOCATION}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == DEFAULT_LOCATION assert result["data"][CONF_LOCATION] == DEFAULT_LOCATION @@ -89,24 +89,24 @@ async def test_config_flow_already_configured(hass: HomeAssistant) -> None: r1 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert r1["type"] == FlowResultType.FORM + assert r1["type"] is FlowResultType.FORM assert r1["step_id"] == SOURCE_USER assert "flow_id" in r1 result1 = await hass.config_entries.flow.async_configure( r1["flow_id"], user_input={CONF_LOCATION: DEFAULT_LOCATION}, ) - assert result1["type"] == FlowResultType.CREATE_ENTRY + assert result1["type"] is FlowResultType.CREATE_ENTRY r2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert r2["type"] == FlowResultType.FORM + assert r2["type"] is FlowResultType.FORM assert r2["step_id"] == SOURCE_USER assert "flow_id" in r2 result2 = await hass.config_entries.flow.async_configure( r2["flow_id"], user_input={CONF_LOCATION: DEFAULT_LOCATION}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/holiday/test_config_flow.py b/tests/components/holiday/test_config_flow.py index 44a72f58404..14e2b68234c 100644 --- a/tests/components/holiday/test_config_flow.py +++ b/tests/components/holiday/test_config_flow.py @@ -20,7 +20,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -30,7 +30,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], @@ -40,7 +40,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Germany, BW" assert result3["data"] == { "country": "DE", @@ -54,7 +54,7 @@ async def test_form_no_subdivision(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -64,7 +64,7 @@ async def test_form_no_subdivision(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Sweden" assert result2["data"] == { "country": "SE", @@ -108,7 +108,7 @@ async def test_single_combination_country_province(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, data=data_se, ) - assert result_se["type"] == FlowResultType.ABORT + assert result_se["type"] is FlowResultType.ABORT assert result_se["reason"] == "already_configured" # Test for country with subdivisions @@ -117,7 +117,7 @@ async def test_single_combination_country_province(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, data=data_de, ) - assert result_de_step1["type"] == FlowResultType.FORM + assert result_de_step1["type"] is FlowResultType.FORM result_de_step2 = await hass.config_entries.flow.async_configure( result_de_step1["flow_id"], @@ -125,7 +125,7 @@ async def test_single_combination_country_province(hass: HomeAssistant) -> None: CONF_PROVINCE: data_de[CONF_PROVINCE], }, ) - assert result_de_step2["type"] == FlowResultType.ABORT + assert result_de_step2["type"] is FlowResultType.ABORT assert result_de_step2["reason"] == "already_configured" @@ -167,7 +167,7 @@ async def test_form_babel_unresolved_language(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Germany, BW" assert result["data"] == { "country": "DE", @@ -213,7 +213,7 @@ async def test_form_babel_replace_dash_with_underscore(hass: HomeAssistant) -> N ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Germany, BW" assert result["data"] == { "country": "DE", @@ -237,7 +237,7 @@ async def test_reconfigure(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> "entry_id": entry.entry_id, }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -247,7 +247,7 @@ async def test_reconfigure(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" entry = hass.config_entries.async_get_entry(entry.entry_id) assert entry.title == "Germany, NW" @@ -274,7 +274,7 @@ async def test_reconfigure_incorrect_language( "entry_id": entry.entry_id, }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -284,7 +284,7 @@ async def test_reconfigure_incorrect_language( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" entry = hass.config_entries.async_get_entry(entry.entry_id) assert entry.title == "Germany, NW" @@ -315,7 +315,7 @@ async def test_reconfigure_entry_exists( "entry_id": entry.entry_id, }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -325,7 +325,7 @@ async def test_reconfigure_entry_exists( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" entry = hass.config_entries.async_get_entry(entry.entry_id) assert entry.title == "Germany, BW" diff --git a/tests/components/home_connect/test_config_flow.py b/tests/components/home_connect/test_config_flow.py index 74ca918889d..2c094c74246 100644 --- a/tests/components/home_connect/test_config_flow.py +++ b/tests/components/home_connect/test_config_flow.py @@ -3,7 +3,7 @@ from http import HTTPStatus from unittest.mock import patch -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, setup from homeassistant.components.application_credentials import ( ClientCredential, async_import_client_credential, @@ -14,6 +14,7 @@ from homeassistant.components.home_connect.const import ( OAUTH2_TOKEN, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow from tests.test_util.aiohttp import AiohttpClientMocker @@ -47,7 +48,7 @@ async def test_full_flow( }, ) - assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" diff --git a/tests/components/homeassistant_green/test_config_flow.py b/tests/components/homeassistant_green/test_config_flow.py index f0bf15aaa53..5fa71d9a091 100644 --- a/tests/components/homeassistant_green/test_config_flow.py +++ b/tests/components/homeassistant_green/test_config_flow.py @@ -49,7 +49,7 @@ async def test_config_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": "system"} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Home Assistant Green" assert result["data"] == {} assert result["options"] == {} @@ -83,7 +83,7 @@ async def test_config_flow_single_entry(hass: HomeAssistant) -> None: DOMAIN, context={"source": "system"} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" mock_setup_entry.assert_not_called() @@ -109,7 +109,7 @@ async def test_option_flow_non_hassio( ): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_hassio" @@ -132,14 +132,14 @@ async def test_option_flow_led_settings( config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "hardware_settings" result = await hass.config_entries.options.async_configure( result["flow_id"], {"activity_led": False, "power_led": False, "system_health_led": False}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY set_green_settings.assert_called_once_with( hass, {"activity_led": False, "power_led": False, "system_health_led": False} ) @@ -164,14 +164,14 @@ async def test_option_flow_led_settings_unchanged( config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "hardware_settings" result = await hass.config_entries.options.async_configure( result["flow_id"], {"activity_led": True, "power_led": True, "system_health_led": True}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY set_green_settings.assert_not_called() @@ -195,7 +195,7 @@ async def test_option_flow_led_settings_fail_1(hass: HomeAssistant) -> None: ): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "read_hw_settings_error" @@ -216,7 +216,7 @@ async def test_option_flow_led_settings_fail_2( config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "hardware_settings" with patch( @@ -227,5 +227,5 @@ async def test_option_flow_led_settings_fail_2( result["flow_id"], {"activity_led": False, "power_led": False, "system_health_led": False}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "write_hw_settings_error" diff --git a/tests/components/homeassistant_sky_connect/test_config_flow.py b/tests/components/homeassistant_sky_connect/test_config_flow.py index 957a407cc0e..9647cef4721 100644 --- a/tests/components/homeassistant_sky_connect/test_config_flow.py +++ b/tests/components/homeassistant_sky_connect/test_config_flow.py @@ -75,7 +75,7 @@ async def test_config_flow( "description": usb_data.description, } - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == title assert result["data"] == expected_data assert result["options"] == {} @@ -123,7 +123,7 @@ async def test_config_flow_multiple_entries( DOMAIN, context={"source": "usb"}, data=usb_data ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY @pytest.mark.parametrize( @@ -172,7 +172,7 @@ async def test_config_flow_update_device( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_unload_entry.mock_calls) == 1 @@ -221,7 +221,7 @@ async def test_option_flow_install_multi_pan_addon( side_effect=Mock(return_value=True), ): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "addon_not_installed" result = await hass.config_entries.options.async_configure( @@ -230,7 +230,7 @@ async def test_option_flow_install_multi_pan_addon( "enable_multi_pan": True, }, ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "install_addon" assert result["progress_action"] == "install_addon" @@ -238,7 +238,7 @@ async def test_option_flow_install_multi_pan_addon( install_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" set_addon_options.assert_called_once_with( hass, @@ -257,7 +257,7 @@ async def test_option_flow_install_multi_pan_addon( start_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY def mock_detect_radio_type(radio_type=RadioType.ezsp, ret=True): @@ -329,7 +329,7 @@ async def test_option_flow_install_multi_pan_addon_zha( side_effect=Mock(return_value=True), ): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "addon_not_installed" result = await hass.config_entries.options.async_configure( @@ -338,7 +338,7 @@ async def test_option_flow_install_multi_pan_addon_zha( "enable_multi_pan": True, }, ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "install_addon" assert result["progress_action"] == "install_addon" @@ -346,7 +346,7 @@ async def test_option_flow_install_multi_pan_addon_zha( install_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" set_addon_options.assert_called_once_with( hass, @@ -374,4 +374,4 @@ async def test_option_flow_install_multi_pan_addon_zha( start_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY diff --git a/tests/components/homeassistant_yellow/test_config_flow.py b/tests/components/homeassistant_yellow/test_config_flow.py index 821621d5e57..206ad4dce15 100644 --- a/tests/components/homeassistant_yellow/test_config_flow.py +++ b/tests/components/homeassistant_yellow/test_config_flow.py @@ -65,7 +65,7 @@ async def test_config_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": "system"} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Home Assistant Yellow" assert result["data"] == {} assert result["options"] == {} @@ -99,7 +99,7 @@ async def test_config_flow_single_entry(hass: HomeAssistant) -> None: DOMAIN, context={"source": "system"} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" mock_setup_entry.assert_not_called() @@ -126,7 +126,7 @@ async def test_option_flow_install_multi_pan_addon( config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU with patch( "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", @@ -136,7 +136,7 @@ async def test_option_flow_install_multi_pan_addon( result["flow_id"], {"next_step_id": "multipan_settings"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "addon_not_installed" result = await hass.config_entries.options.async_configure( @@ -145,7 +145,7 @@ async def test_option_flow_install_multi_pan_addon( "enable_multi_pan": True, }, ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "install_addon" assert result["progress_action"] == "install_addon" @@ -153,7 +153,7 @@ async def test_option_flow_install_multi_pan_addon( install_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" set_addon_options.assert_called_once_with( hass, @@ -172,7 +172,7 @@ async def test_option_flow_install_multi_pan_addon( start_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_option_flow_install_multi_pan_addon_zha( @@ -205,7 +205,7 @@ async def test_option_flow_install_multi_pan_addon_zha( zha_config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU with patch( "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", @@ -215,7 +215,7 @@ async def test_option_flow_install_multi_pan_addon_zha( result["flow_id"], {"next_step_id": "multipan_settings"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "addon_not_installed" result = await hass.config_entries.options.async_configure( @@ -224,7 +224,7 @@ async def test_option_flow_install_multi_pan_addon_zha( "enable_multi_pan": True, }, ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "install_addon" assert result["progress_action"] == "install_addon" @@ -232,7 +232,7 @@ async def test_option_flow_install_multi_pan_addon_zha( install_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" set_addon_options.assert_called_once_with( hass, @@ -260,7 +260,7 @@ async def test_option_flow_install_multi_pan_addon_zha( start_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY @pytest.mark.parametrize( @@ -289,20 +289,20 @@ async def test_option_flow_led_settings( config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "main_menu" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "hardware_settings"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.options.async_configure( result["flow_id"], {"disk_led": False, "heartbeat_led": False, "power_led": False}, ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "reboot_menu" set_yellow_settings.assert_called_once_with( hass, {"disk_led": False, "heartbeat_led": False, "power_led": False} @@ -312,7 +312,7 @@ async def test_option_flow_led_settings( result["flow_id"], {"next_step_id": reboot_menu_choice}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert len(reboot_host.mock_calls) == reboot_calls @@ -335,20 +335,20 @@ async def test_option_flow_led_settings_unchanged( config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "main_menu" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "hardware_settings"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.options.async_configure( result["flow_id"], {"disk_led": True, "heartbeat_led": True, "power_led": True}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY set_yellow_settings.assert_not_called() @@ -367,7 +367,7 @@ async def test_option_flow_led_settings_fail_1(hass: HomeAssistant) -> None: config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "main_menu" with patch( @@ -378,7 +378,7 @@ async def test_option_flow_led_settings_fail_1(hass: HomeAssistant) -> None: result["flow_id"], {"next_step_id": "hardware_settings"}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "read_hw_settings_error" @@ -399,14 +399,14 @@ async def test_option_flow_led_settings_fail_2( config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "main_menu" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "hardware_settings"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.homeassistant_yellow.config_flow.async_set_yellow_settings", @@ -416,5 +416,5 @@ async def test_option_flow_led_settings_fail_2( result["flow_id"], {"disk_led": False, "heartbeat_led": False, "power_led": False}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "write_hw_settings_error" diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index b3b8a70b1a1..0cd8e3284db 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, Mock, patch import pytest import voluptuous as vol -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.homekit.const import ( CONF_FILTER, DOMAIN, @@ -14,6 +14,7 @@ from homeassistant.components.homekit.const import ( 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.entityfilter import CONF_INCLUDE_DOMAINS from homeassistant.setup import async_setup_component @@ -57,7 +58,7 @@ async def test_setup_in_bridge_mode(hass: HomeAssistant, mock_get_source_ip) -> result["flow_id"], {"include_domains": ["light"]}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pairing" with ( @@ -79,7 +80,7 @@ async def test_setup_in_bridge_mode(hass: HomeAssistant, mock_get_source_ip) -> ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY bridge_name = (result3["title"].split(":"))[0] assert bridge_name == SHORT_BRIDGE_NAME assert result3["data"] == { @@ -119,7 +120,7 @@ async def test_setup_in_bridge_mode_name_taken( result["flow_id"], {"include_domains": ["light"]}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pairing" with ( @@ -141,7 +142,7 @@ async def test_setup_in_bridge_mode_name_taken( ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] != SHORT_BRIDGE_NAME assert result3["title"].startswith(SHORT_BRIDGE_NAME) bridge_name = (result3["title"].split(":"))[0] @@ -205,7 +206,7 @@ async def test_setup_creates_entries_for_accessory_mode_devices( result["flow_id"], {"include_domains": ["camera", "media_player", "light", "lock", "remote"]}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pairing" with ( @@ -227,7 +228,7 @@ async def test_setup_creates_entries_for_accessory_mode_devices( ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"][:11] == "HASS Bridge" bridge_name = (result3["title"].split(":"))[0] assert result3["data"] == { @@ -272,7 +273,7 @@ async def test_import(hass: HomeAssistant, mock_get_source_ip) -> None: context={"source": config_entries.SOURCE_IMPORT}, data={CONF_NAME: "mock_name", CONF_PORT: 12345}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "port_name_in_use" with ( @@ -291,7 +292,7 @@ async def test_import(hass: HomeAssistant, mock_get_source_ip) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "othername:56789" assert result2["data"] == { "name": "othername", @@ -316,7 +317,7 @@ async def test_options_flow_exclude_mode_advanced( config_entry.entry_id, context={"show_advanced_options": True} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -327,14 +328,14 @@ async def test_options_flow_exclude_mode_advanced( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "exclude" result2 = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"entities": ["climate.old"]}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "advanced" with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): @@ -343,7 +344,7 @@ async def test_options_flow_exclude_mode_advanced( user_input={}, ) - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { "devices": [], "mode": "bridge", @@ -373,7 +374,7 @@ async def test_options_flow_exclude_mode_basic( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -384,7 +385,7 @@ async def test_options_flow_exclude_mode_basic( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "exclude" entities = result["data_schema"]({})["entities"] assert entities == ["climate.front_gate"] @@ -397,7 +398,7 @@ async def test_options_flow_exclude_mode_basic( result["flow_id"], user_input={"entities": ["climate.old"]}, ) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { @@ -458,7 +459,7 @@ async def test_options_flow_devices( config_entry.entry_id, context={"show_advanced_options": True} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -469,7 +470,7 @@ async def test_options_flow_devices( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "exclude" entry = entity_registry.async_get("light.ceiling_lights") @@ -491,7 +492,7 @@ async def test_options_flow_devices( user_input={"devices": [device_id]}, ) - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { "devices": [device_id], "mode": "bridge", @@ -547,7 +548,7 @@ async def test_options_flow_devices_preserved_when_advanced_off( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -558,7 +559,7 @@ async def test_options_flow_devices_preserved_when_advanced_off( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "exclude" result2 = await hass.config_entries.options.async_configure( @@ -568,7 +569,7 @@ async def test_options_flow_devices_preserved_when_advanced_off( }, ) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { "devices": ["1fabcabcabcabcabcabcabcabcabc"], "mode": "bridge", @@ -607,7 +608,7 @@ async def test_options_flow_include_mode_with_non_existant_entity( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -618,7 +619,7 @@ async def test_options_flow_include_mode_with_non_existant_entity( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "include" entities = result["data_schema"]({})["entities"] @@ -630,7 +631,7 @@ async def test_options_flow_include_mode_with_non_existant_entity( "entities": ["climate.new", "climate.front_gate"], }, ) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { @@ -668,7 +669,7 @@ async def test_options_flow_exclude_mode_with_non_existant_entity( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -679,7 +680,7 @@ async def test_options_flow_exclude_mode_with_non_existant_entity( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "exclude" entities = result["data_schema"]({})["entities"] @@ -691,7 +692,7 @@ async def test_options_flow_exclude_mode_with_non_existant_entity( "entities": ["climate.new", "climate.front_gate"], }, ) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { @@ -722,7 +723,7 @@ async def test_options_flow_include_mode_basic( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -733,14 +734,14 @@ async def test_options_flow_include_mode_basic( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "include" result2 = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"entities": ["climate.new"]}, ) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { @@ -772,7 +773,7 @@ async def test_options_flow_exclude_mode_with_cameras( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -783,7 +784,7 @@ async def test_options_flow_exclude_mode_with_cameras( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "exclude" result2 = await hass.config_entries.options.async_configure( @@ -792,7 +793,7 @@ async def test_options_flow_exclude_mode_with_cameras( "entities": ["climate.old", "camera.excluded"], }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "cameras" result3 = await hass.config_entries.options.async_configure( @@ -800,7 +801,7 @@ async def test_options_flow_exclude_mode_with_cameras( user_input={"camera_copy": ["camera.native_h264"]}, ) - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { @@ -818,7 +819,7 @@ async def test_options_flow_exclude_mode_with_cameras( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -829,7 +830,7 @@ async def test_options_flow_exclude_mode_with_cameras( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "exclude" result2 = await hass.config_entries.options.async_configure( @@ -838,7 +839,7 @@ async def test_options_flow_exclude_mode_with_cameras( "entities": ["climate.old", "camera.excluded"], }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "cameras" result3 = await hass.config_entries.options.async_configure( @@ -846,7 +847,7 @@ async def test_options_flow_exclude_mode_with_cameras( user_input={"camera_copy": ["camera.native_h264"]}, ) - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", @@ -881,7 +882,7 @@ async def test_options_flow_include_mode_with_cameras( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -892,7 +893,7 @@ async def test_options_flow_include_mode_with_cameras( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "include" result2 = await hass.config_entries.options.async_configure( @@ -901,7 +902,7 @@ async def test_options_flow_include_mode_with_cameras( "entities": ["camera.native_h264", "camera.transcode_h264"], }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "cameras" result3 = await hass.config_entries.options.async_configure( @@ -909,7 +910,7 @@ async def test_options_flow_include_mode_with_cameras( user_input={"camera_copy": ["camera.native_h264"]}, ) - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { @@ -927,7 +928,7 @@ async def test_options_flow_include_mode_with_cameras( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({}) == { "domains": ["climate", "fan", "vacuum", "camera"], @@ -952,7 +953,7 @@ async def test_options_flow_include_mode_with_cameras( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "exclude" assert result["data_schema"]({}) == { "entities": ["camera.native_h264", "camera.transcode_h264"], @@ -969,7 +970,7 @@ async def test_options_flow_include_mode_with_cameras( "entities": ["climate.old", "camera.excluded"], }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "cameras" assert result2["data_schema"]({}) == { "camera_copy": ["camera.native_h264"], @@ -983,7 +984,7 @@ async def test_options_flow_include_mode_with_cameras( user_input={"camera_copy": []}, ) - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { "entity_config": {}, "filter": { @@ -1017,7 +1018,7 @@ async def test_options_flow_with_camera_audio( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -1028,7 +1029,7 @@ async def test_options_flow_with_camera_audio( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "include" result2 = await hass.config_entries.options.async_configure( @@ -1037,7 +1038,7 @@ async def test_options_flow_with_camera_audio( "entities": ["camera.audio", "camera.no_audio"], }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "cameras" result3 = await hass.config_entries.options.async_configure( @@ -1045,7 +1046,7 @@ async def test_options_flow_with_camera_audio( user_input={"camera_audio": ["camera.audio"]}, ) - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { @@ -1063,7 +1064,7 @@ async def test_options_flow_with_camera_audio( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({}) == { "domains": ["climate", "fan", "vacuum", "camera"], @@ -1088,7 +1089,7 @@ async def test_options_flow_with_camera_audio( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "exclude" assert result["data_schema"]({}) == { "entities": ["camera.audio", "camera.no_audio"], @@ -1105,7 +1106,7 @@ async def test_options_flow_with_camera_audio( "entities": ["climate.old", "camera.excluded"], }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "cameras" assert result2["data_schema"]({}) == { "camera_copy": [], @@ -1119,7 +1120,7 @@ async def test_options_flow_with_camera_audio( user_input={"camera_audio": []}, ) - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { "entity_config": {}, "filter": { @@ -1164,7 +1165,7 @@ async def test_options_flow_blocked_when_from_yaml( result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "yaml" with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): @@ -1172,7 +1173,7 @@ async def test_options_flow_blocked_when_from_yaml( result["flow_id"], user_input={}, ) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY await hass.config_entries.async_unload(config_entry.entry_id) @@ -1197,7 +1198,7 @@ async def test_options_flow_include_mode_basic_accessory( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({}) == { "domains": [ @@ -1217,7 +1218,7 @@ async def test_options_flow_include_mode_basic_accessory( user_input={"domains": ["media_player"], "mode": "accessory"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "accessory" assert _get_schema_default(result2["data_schema"].schema, "entities") is None @@ -1225,7 +1226,7 @@ async def test_options_flow_include_mode_basic_accessory( result2["flow_id"], user_input={"entities": "media_player.tv"}, ) - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "accessory", "filter": { @@ -1243,7 +1244,7 @@ async def test_options_flow_include_mode_basic_accessory( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({}) == { "domains": ["media_player"], @@ -1256,7 +1257,7 @@ async def test_options_flow_include_mode_basic_accessory( user_input={"domains": ["media_player"], "mode": "accessory"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "accessory" assert ( _get_schema_default(result2["data_schema"].schema, "entities") @@ -1267,7 +1268,7 @@ async def test_options_flow_include_mode_basic_accessory( result2["flow_id"], user_input={"entities": "media_player.tv"}, ) - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "accessory", "filter": { @@ -1296,7 +1297,7 @@ async def test_converting_bridge_to_accessory_mode( result["flow_id"], {"include_domains": ["light"]}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pairing" # We need to actually setup the config entry or the data @@ -1317,7 +1318,7 @@ async def test_converting_bridge_to_accessory_mode( ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"][:11] == "HASS Bridge" bridge_name = (result3["title"].split(":"))[0] assert result3["data"] == { @@ -1345,7 +1346,7 @@ async def test_converting_bridge_to_accessory_mode( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema assert _get_schema_default(schema, "mode") == "bridge" @@ -1356,14 +1357,14 @@ async def test_converting_bridge_to_accessory_mode( user_input={"domains": ["camera"], "mode": "accessory"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "accessory" result2 = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"entities": "camera.tv"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "cameras" with ( @@ -1379,7 +1380,7 @@ async def test_converting_bridge_to_accessory_mode( ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { "entity_config": {"camera.tv": {"video_codec": "copy"}}, "mode": "accessory", @@ -1444,7 +1445,7 @@ async def test_options_flow_exclude_mode_skips_category_entities( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({}) == { "domains": [ @@ -1468,7 +1469,7 @@ async def test_options_flow_exclude_mode_skips_category_entities( }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "exclude" assert _get_schema_default(result2["data_schema"].schema, "entities") == [] @@ -1490,7 +1491,7 @@ async def test_options_flow_exclude_mode_skips_category_entities( ] }, ) - assert result4["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { @@ -1539,7 +1540,7 @@ async def test_options_flow_exclude_mode_skips_hidden_entities( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({}) == { "domains": [ @@ -1563,7 +1564,7 @@ async def test_options_flow_exclude_mode_skips_hidden_entities( }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "exclude" assert _get_schema_default(result2["data_schema"].schema, "entities") == [] @@ -1579,7 +1580,7 @@ async def test_options_flow_exclude_mode_skips_hidden_entities( result2["flow_id"], user_input={"entities": ["media_player.tv", "switch.other"]}, ) - assert result4["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { @@ -1624,7 +1625,7 @@ async def test_options_flow_include_mode_allows_hidden_entities( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({}) == { "domains": [ @@ -1648,7 +1649,7 @@ async def test_options_flow_include_mode_allows_hidden_entities( }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "include" assert _get_schema_default(result2["data_schema"].schema, "entities") == [] @@ -1664,7 +1665,7 @@ async def test_options_flow_include_mode_allows_hidden_entities( ] }, ) - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 6b658e9eef4..fbbd945b987 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -13,7 +13,7 @@ from aiohomekit.model.services import ServicesTypes from bleak.exc import BleakError import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.homekit_controller import config_flow from homeassistant.components.homekit_controller.const import KNOWN_DEVICES @@ -520,7 +520,7 @@ async def test_discovery_ignored_config_entry(hass: HomeAssistant, controller) - assert config_entry_count == 1 # We should abort since there is no accessory id in the data - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -1110,11 +1110,11 @@ async def test_mdns_update_to_paired_during_pairing( context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info_paired, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_paired" mdns_update_to_paired.set() result = await task - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Koogeek-LS1-20833F" assert result["data"] == {} @@ -1130,7 +1130,7 @@ async def test_discovery_no_bluetooth_support(hass: HomeAssistant, controller) - context={"source": config_entries.SOURCE_BLUETOOTH}, data=HK_BLUETOOTH_SERVICE_INFO_NOT_DISCOVERED, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "ignored_model" @@ -1145,7 +1145,7 @@ async def test_bluetooth_not_homekit(hass: HomeAssistant, controller) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_HK_BLUETOOTH_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "ignored_model" @@ -1162,7 +1162,7 @@ async def test_bluetooth_valid_device_no_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=HK_BLUETOOTH_SERVICE_INFO_NOT_DISCOVERED, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "accessory_not_found_error" @@ -1182,7 +1182,7 @@ async def test_bluetooth_valid_device_discovery_paired( data=HK_BLUETOOTH_SERVICE_INFO_DISCOVERED_PAIRED, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_paired" @@ -1203,7 +1203,7 @@ async def test_bluetooth_valid_device_discovery_unpaired( data=HK_BLUETOOTH_SERVICE_INFO_DISCOVERED_UNPAIRED, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair" assert storage.get_map("00:00:00:00:00:00") is None @@ -1214,11 +1214,11 @@ async def test_bluetooth_valid_device_discovery_unpaired( } result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], user_input={"pairing_code": "111-22-333"} ) - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Koogeek-LS1-20833F" assert result3["data"] == {} @@ -1256,7 +1256,7 @@ async def test_discovery_updates_ip_when_config_entry_set_up( context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" await hass.async_block_till_done() @@ -1294,7 +1294,7 @@ async def test_discovery_updates_ip_config_entry_not_set_up( context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" await hass.async_block_till_done() diff --git a/tests/components/homewizard/test_config_flow.py b/tests/components/homewizard/test_config_flow.py index f0776877aec..08f1436c03a 100644 --- a/tests/components/homewizard/test_config_flow.py +++ b/tests/components/homewizard/test_config_flow.py @@ -29,14 +29,14 @@ async def test_manual_flow_works( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_IP_ADDRESS: "2.2.2.2"} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result == snapshot assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -71,21 +71,21 @@ async def test_discovery_flow_works( ), ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=None ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"ip_address": "127.0.0.1"} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result == snapshot @@ -117,7 +117,7 @@ async def test_discovery_flow_during_onboarding( ), ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result == snapshot assert len(mock_setup_entry.mock_calls) == 1 @@ -154,7 +154,7 @@ async def test_discovery_flow_during_onboarding_disabled_api( ), ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" assert result["errors"] == {"base": "api_not_enabled"} @@ -166,7 +166,7 @@ async def test_discovery_flow_during_onboarding_disabled_api( result["flow_id"], user_input={"ip_address": "127.0.0.1"} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result == snapshot assert len(mock_setup_entry.mock_calls) == 1 @@ -198,7 +198,7 @@ async def test_discovery_disabled_api( ), ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" mock_homewizardenergy.device.side_effect = DisabledError @@ -207,7 +207,7 @@ async def test_discovery_disabled_api( result["flow_id"], user_input={"ip_address": "127.0.0.1"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "api_not_enabled"} @@ -233,7 +233,7 @@ async def test_discovery_missing_data_in_service_info(hass: HomeAssistant) -> No ), ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_discovery_parameters" @@ -259,7 +259,7 @@ async def test_discovery_invalid_api(hass: HomeAssistant) -> None: ), ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unsupported_api_version" @@ -281,14 +281,14 @@ async def test_error_flow( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_IP_ADDRESS: "127.0.0.1"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": reason} assert result["data_schema"]({}) == {CONF_IP_ADDRESS: "127.0.0.1"} @@ -299,7 +299,7 @@ async def test_error_flow( result["flow_id"], {CONF_IP_ADDRESS: "127.0.0.1"} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY @pytest.mark.parametrize( @@ -329,7 +329,7 @@ async def test_abort_flow( result["flow_id"], {CONF_IP_ADDRESS: "2.2.2.2"} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == reason @@ -354,7 +354,7 @@ async def test_reauth_flow( result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" @@ -375,10 +375,10 @@ async def test_reauth_error( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "api_not_enabled"} diff --git a/tests/components/homeworks/test_config_flow.py b/tests/components/homeworks/test_config_flow.py index 4bdb5938f1c..53128c4cd65 100644 --- a/tests/components/homeworks/test_config_flow.py +++ b/tests/components/homeworks/test_config_flow.py @@ -48,7 +48,7 @@ async def test_user_flow( CONF_PORT: 1234, }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Main controller" assert result["data"] == {} assert result["options"] == { @@ -82,7 +82,7 @@ async def test_user_flow_already_exists( CONF_PORT: 1234, }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "duplicated_host_port"} @@ -94,7 +94,7 @@ async def test_user_flow_already_exists( CONF_PORT: 1234, }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "duplicated_controller_id"} @@ -125,7 +125,7 @@ async def test_user_flow_cannot_connect( CONF_PORT: 1234, }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": error} assert result["step_id"] == "user" @@ -185,19 +185,19 @@ async def test_import_flow( ], }, ) - assert result["type"] == FlowResultType.FORM + 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"] == FlowResultType.FORM + 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"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Main controller" assert result["data"] == {} assert result["options"] == { @@ -241,7 +241,7 @@ async def test_import_flow_already_exists( context={"source": SOURCE_IMPORT}, data={"host": "192.168.0.1", "port": 1234, CONF_DIMMERS: [], CONF_KEYPADS: []}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(issue_registry.issues) == 1 @@ -256,13 +256,13 @@ async def test_import_flow_controller_id_exists( context={"source": SOURCE_IMPORT}, data={"host": "192.168.0.2", "port": 1234, CONF_DIMMERS: [], CONF_KEYPADS: []}, ) - assert result["type"] == FlowResultType.FORM + 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"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "import_controller_name" assert result["errors"] == {"base": "duplicated_controller_id"} @@ -277,7 +277,7 @@ async def test_reconfigure_flow( DOMAIN, context={"source": SOURCE_RECONFIGURE, "entry_id": mock_config_entry.entry_id}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reconfigure" result = await hass.config_entries.flow.async_configure( @@ -288,7 +288,7 @@ async def test_reconfigure_flow( }, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" assert mock_config_entry.options == { "controller_id": "main_controller", @@ -345,7 +345,7 @@ async def test_reconfigure_flow_flow_duplicate( DOMAIN, context={"source": SOURCE_RECONFIGURE, "entry_id": entry1.entry_id}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reconfigure" result = await hass.config_entries.flow.async_configure( @@ -355,7 +355,7 @@ async def test_reconfigure_flow_flow_duplicate( CONF_PORT: 1234, }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reconfigure" assert result["errors"] == {"base": "duplicated_host_port"} @@ -370,7 +370,7 @@ async def test_reconfigure_flow_flow_no_change( DOMAIN, context={"source": SOURCE_RECONFIGURE, "entry_id": mock_config_entry.entry_id}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reconfigure" result = await hass.config_entries.flow.async_configure( @@ -380,7 +380,7 @@ async def test_reconfigure_flow_flow_no_change( CONF_PORT: 1234, }, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" assert mock_config_entry.options == { "controller_id": "main_controller", @@ -422,14 +422,14 @@ async def test_options_add_light_flow( result = await hass.config_entries.options.async_init( mock_empty_config_entry.entry_id ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "add_light"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "add_light" result = await hass.config_entries.options.async_configure( @@ -440,7 +440,7 @@ async def test_options_add_light_flow( CONF_RATE: 2.0, }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "controller_id": "main_controller", "dimmers": [ @@ -469,14 +469,14 @@ async def test_options_add_remove_light_flow( assert hass.states.async_entity_ids("light") == unordered(["light.foyer_sconces"]) result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "add_light"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "add_light" result = await hass.config_entries.options.async_configure( @@ -487,7 +487,7 @@ async def test_options_add_remove_light_flow( CONF_RATE: 2.0, }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "controller_id": "main_controller", "dimmers": [ @@ -523,14 +523,14 @@ async def test_options_add_remove_light_flow( # Now remove the original light result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "remove_light"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "remove_light" assert result["data_schema"].schema["index"].options == { "0": "Foyer Sconces ([02:08:01:01])", @@ -540,7 +540,7 @@ async def test_options_add_remove_light_flow( result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_INDEX: ["0"]} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "controller_id": "main_controller", "dimmers": [ @@ -583,14 +583,14 @@ async def test_options_add_remove_keypad_flow( await hass.async_block_till_done() result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "add_keypad"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "add_keypad" result = await hass.config_entries.options.async_configure( @@ -600,7 +600,7 @@ async def test_options_add_remove_keypad_flow( CONF_NAME: "Hall Keypad", }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "controller_id": "main_controller", "dimmers": [ @@ -631,14 +631,14 @@ async def test_options_add_remove_keypad_flow( # Now remove the original keypad result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "remove_keypad"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "remove_keypad" assert result["data_schema"].schema["index"].options == { "0": "Foyer Keypad ([02:08:02:01])", @@ -648,7 +648,7 @@ async def test_options_add_remove_keypad_flow( result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_INDEX: ["0"]} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "controller_id": "main_controller", "dimmers": [ @@ -670,14 +670,14 @@ async def test_options_add_keypad_with_error( await hass.async_block_till_done() result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "add_keypad"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "add_keypad" # Try an invalid address @@ -688,7 +688,7 @@ async def test_options_add_keypad_with_error( CONF_NAME: "Hall Keypad", }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "add_keypad" assert result["errors"] == {"base": "invalid_addr"} @@ -700,7 +700,7 @@ async def test_options_add_keypad_with_error( CONF_NAME: "Hall Keypad", }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "add_keypad" assert result["errors"] == {"base": "duplicated_addr"} @@ -712,7 +712,7 @@ async def test_options_add_keypad_with_error( CONF_NAME: "Hall Keypad", }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "add_keypad" assert result["errors"] == {"base": "duplicated_addr"} @@ -727,13 +727,13 @@ async def test_options_edit_light_no_lights_flow( assert hass.states.async_entity_ids("light") == unordered(["light.foyer_sconces"]) result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "select_edit_light"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_edit_light" assert result["data_schema"].schema["index"].container == { "0": "Foyer Sconces ([02:08:01:01])" @@ -743,7 +743,7 @@ async def test_options_edit_light_no_lights_flow( result["flow_id"], {"index": "0"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "edit_light" result = await hass.config_entries.options.async_configure( @@ -751,7 +751,7 @@ async def test_options_edit_light_no_lights_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "controller_id": "main_controller", "dimmers": [{"addr": "[02:08:01:01]", "name": "Foyer Sconces", "rate": 3.0}], @@ -795,13 +795,13 @@ async def test_options_edit_light_flow_empty( result = await hass.config_entries.options.async_init( mock_empty_config_entry.entry_id ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "select_edit_light"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_edit_light" assert result["data_schema"].schema["index"].container == {} @@ -817,13 +817,13 @@ async def test_options_add_button_flow( assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 3 result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "select_edit_keypad"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_edit_keypad" assert result["data_schema"].schema["index"].container == { "0": "Foyer Keypad ([02:08:02:01])" @@ -833,14 +833,14 @@ async def test_options_add_button_flow( result["flow_id"], {"index": "0"}, ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "edit_keypad" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "add_button"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "add_button" result = await hass.config_entries.options.async_configure( @@ -854,7 +854,7 @@ async def test_options_add_button_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "controller_id": "main_controller", "dimmers": [{"addr": "[02:08:01:01]", "name": "Foyer Sconces", "rate": 1.0}], @@ -902,13 +902,13 @@ async def test_options_add_button_flow_duplicate( assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 3 result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "select_edit_keypad"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_edit_keypad" assert result["data_schema"].schema["index"].container == { "0": "Foyer Keypad ([02:08:02:01])" @@ -918,14 +918,14 @@ async def test_options_add_button_flow_duplicate( result["flow_id"], {"index": "0"}, ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "edit_keypad" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "add_button"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "add_button" result = await hass.config_entries.options.async_configure( @@ -937,7 +937,7 @@ async def test_options_add_button_flow_duplicate( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "duplicated_number"} @@ -952,13 +952,13 @@ async def test_options_edit_button_flow( assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 3 result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "select_edit_keypad"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_edit_keypad" assert result["data_schema"].schema["index"].container == { "0": "Foyer Keypad ([02:08:02:01])" @@ -968,13 +968,13 @@ async def test_options_edit_button_flow( result["flow_id"], {"index": "0"}, ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "edit_keypad" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "select_edit_button"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_edit_button" assert result["data_schema"].schema["index"].container == { "0": "Morning (1)", @@ -986,7 +986,7 @@ async def test_options_edit_button_flow( result["flow_id"], {"index": "0"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "edit_button" result = await hass.config_entries.options.async_configure( @@ -998,7 +998,7 @@ async def test_options_edit_button_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "controller_id": "main_controller", "dimmers": [{"addr": "[02:08:01:01]", "name": "Foyer Sconces", "rate": 1.0}], @@ -1039,13 +1039,13 @@ async def test_options_remove_button_flow( assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 3 result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "select_edit_keypad"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_edit_keypad" assert result["data_schema"].schema["index"].container == { "0": "Foyer Keypad ([02:08:02:01])" @@ -1055,14 +1055,14 @@ async def test_options_remove_button_flow( result["flow_id"], {"index": "0"}, ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "edit_keypad" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "remove_button"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "remove_button" assert result["data_schema"].schema["index"].options == { "0": "Morning (1)", @@ -1074,7 +1074,7 @@ async def test_options_remove_button_flow( result["flow_id"], user_input={CONF_INDEX: ["0"]} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "controller_id": "main_controller", "dimmers": [{"addr": "[02:08:01:01]", "name": "Foyer Sconces", "rate": 1.0}], diff --git a/tests/components/honeywell/test_config_flow.py b/tests/components/honeywell/test_config_flow.py index a978a14daa1..7cd987f0d83 100644 --- a/tests/components/honeywell/test_config_flow.py +++ b/tests/components/honeywell/test_config_flow.py @@ -5,7 +5,6 @@ from unittest.mock import MagicMock, patch import aiosomecomfort import pytest -from homeassistant import data_entry_flow from homeassistant.components.honeywell.const import ( CONF_COOL_AWAY_TEMPERATURE, CONF_HEAT_AWAY_TEMPERATURE, @@ -33,7 +32,7 @@ async def test_show_authenticate_form(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -67,7 +66,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == FAKE_CONFIG @@ -87,7 +86,7 @@ async def test_show_option_form( ): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" @@ -114,7 +113,7 @@ async def test_create_option_entry( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_COOL_AWAY_TEMPERATURE: 1, CONF_HEAT_AWAY_TEMPERATURE: 2, @@ -147,7 +146,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -160,7 +159,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert mock_entry.data == { CONF_USERNAME: "new-username", @@ -190,7 +189,7 @@ async def test_reauth_flow_auth_error(hass: HomeAssistant, client: MagicMock) -> await hass.async_block_till_done() assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} client.login.side_effect = aiosomecomfort.device.AuthError @@ -204,7 +203,7 @@ async def test_reauth_flow_auth_error(hass: HomeAssistant, client: MagicMock) -> ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -239,7 +238,7 @@ async def test_reauth_flow_connnection_error( await hass.async_block_till_done() assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} client.login.side_effect = error @@ -250,5 +249,5 @@ async def test_reauth_flow_connnection_error( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/huawei_lte/test_config_flow.py b/tests/components/huawei_lte/test_config_flow.py index f8ddaa42ac1..200796c87e7 100644 --- a/tests/components/huawei_lte/test_config_flow.py +++ b/tests/components/huawei_lte/test_config_flow.py @@ -12,7 +12,7 @@ from requests.exceptions import ConnectionError import requests_mock from requests_mock import ANY -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.components.huawei_lte.const import CONF_UNAUTHENTICATED_MODE, DOMAIN from homeassistant.const import ( @@ -24,6 +24,7 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -48,7 +49,7 @@ async def test_show_set_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -63,7 +64,7 @@ async def test_urlize_plain_host( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=user_input ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert user_input[CONF_URL] == f"http://{host}/" @@ -96,7 +97,7 @@ async def test_already_configured( data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -127,7 +128,7 @@ async def test_connection_errors( data=FIXTURE_USER_INPUT | data_patch, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == errors @@ -219,7 +220,7 @@ async def test_login_error( data={**FIXTURE_USER_INPUT, **fixture_override}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == errors @@ -250,7 +251,7 @@ async def test_success(hass: HomeAssistant, login_requests_mock, scheme: str) -> ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_URL] == user_input[CONF_URL] assert result["data"][CONF_USERNAME] == user_input[CONF_USERNAME] assert result["data"][CONF_PASSWORD] == user_input[CONF_PASSWORD] @@ -270,7 +271,7 @@ async def test_success(hass: HomeAssistant, login_requests_mock, scheme: str) -> ssdp.ATTR_UPNP_SERIAL: "00000000", }, { - "type": data_entry_flow.FlowResultType.FORM, + "type": FlowResultType.FORM, "step_id": "user", "errors": {}, }, @@ -286,7 +287,7 @@ async def test_success(hass: HomeAssistant, login_requests_mock, scheme: str) -> # No ssdp.ATTR_UPNP_SERIAL }, { - "type": data_entry_flow.FlowResultType.FORM, + "type": FlowResultType.FORM, "step_id": "user", "errors": {}, }, @@ -301,7 +302,7 @@ async def test_success(hass: HomeAssistant, login_requests_mock, scheme: str) -> # Does not matter }, { - "type": data_entry_flow.FlowResultType.ABORT, + "type": FlowResultType.ABORT, "reason": "unsupported_device", }, ), @@ -351,7 +352,7 @@ async def test_ssdp( ( "OK", { - "type": data_entry_flow.FlowResultType.ABORT, + "type": FlowResultType.ABORT, "reason": "reauth_successful", }, FIXTURE_USER_INPUT, @@ -359,7 +360,7 @@ async def test_ssdp( ( f"{LoginErrorEnum.PASSWORD_WRONG}", { - "type": data_entry_flow.FlowResultType.FORM, + "type": FlowResultType.FORM, "errors": {CONF_PASSWORD: "incorrect_password"}, "step_id": "reauth_confirm", }, @@ -393,7 +394,7 @@ async def test_reauth( DOMAIN, context=context, data=entry.data ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["data_schema"] is not None assert result["data_schema"]({}) == { @@ -431,7 +432,7 @@ async def test_options(hass: HomeAssistant) -> None: config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" recipient = "+15555550000" diff --git a/tests/components/huisbaasje/test_config_flow.py b/tests/components/huisbaasje/test_config_flow.py index 0c65d425d4d..cca5c572712 100644 --- a/tests/components/huisbaasje/test_config_flow.py +++ b/tests/components/huisbaasje/test_config_flow.py @@ -8,9 +8,10 @@ from energyflip import ( EnergyFlipUnauthenticatedException, ) -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.huisbaasje.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -21,7 +22,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -80,7 +81,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert form_result["type"] == data_entry_flow.FlowResultType.FORM + assert form_result["type"] is FlowResultType.FORM assert form_result["errors"] == {"base": "invalid_auth"} @@ -102,7 +103,7 @@ async def test_form_authenticate_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert form_result["type"] == data_entry_flow.FlowResultType.FORM + assert form_result["type"] is FlowResultType.FORM assert form_result["errors"] == {"base": "cannot_connect"} @@ -124,7 +125,7 @@ async def test_form_authenticate_unknown_error(hass: HomeAssistant) -> None: }, ) - assert form_result["type"] == data_entry_flow.FlowResultType.FORM + assert form_result["type"] is FlowResultType.FORM assert form_result["errors"] == {"base": "unknown"} @@ -149,7 +150,7 @@ async def test_form_customer_overview_cannot_connect(hass: HomeAssistant) -> Non }, ) - assert form_result["type"] == data_entry_flow.FlowResultType.FORM + assert form_result["type"] is FlowResultType.FORM assert form_result["errors"] == {"base": "cannot_connect"} @@ -174,7 +175,7 @@ async def test_form_customer_overview_authentication_error(hass: HomeAssistant) }, ) - assert form_result["type"] == data_entry_flow.FlowResultType.FORM + assert form_result["type"] is FlowResultType.FORM assert form_result["errors"] == {"base": "invalid_auth"} @@ -199,7 +200,7 @@ async def test_form_customer_overview_unknown_error(hass: HomeAssistant) -> None }, ) - assert form_result["type"] == data_entry_flow.FlowResultType.FORM + assert form_result["type"] is FlowResultType.FORM assert form_result["errors"] == {"base": "unknown"} @@ -240,5 +241,5 @@ async def test_form_entry_exists(hass: HomeAssistant) -> None: }, ) - assert form_result["type"] == data_entry_flow.FlowResultType.ABORT + assert form_result["type"] is FlowResultType.ABORT assert form_result["reason"] == "already_configured" diff --git a/tests/components/hunterdouglas_powerview/test_config_flow.py b/tests/components/hunterdouglas_powerview/test_config_flow.py index ac4f6368f38..b9721f4adb1 100644 --- a/tests/components/hunterdouglas_powerview/test_config_flow.py +++ b/tests/components/hunterdouglas_powerview/test_config_flow.py @@ -28,7 +28,7 @@ async def test_user_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -37,7 +37,7 @@ async def test_user_form( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == f"Powerview Generation {api_version}" assert result2["data"] == {CONF_HOST: "1.2.3.4", CONF_API_VERSION: api_version} assert result2["result"].unique_id == "A1B2C3D4E5G6H7" @@ -47,14 +47,14 @@ async def test_user_form( result3 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {} result4 = await hass.config_entries.flow.async_configure( result3["flow_id"], {CONF_HOST: "1.2.3.4"}, ) - assert result4["type"] == FlowResultType.ABORT + assert result4["type"] is FlowResultType.ABORT assert result4["reason"] == "already_configured" @@ -84,7 +84,7 @@ async def test_form_homekit_and_dhcp_cannot_connect( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" # test we can recover from the failed entry @@ -97,7 +97,7 @@ async def test_form_homekit_and_dhcp_cannot_connect( result3 = await hass.config_entries.flow.async_configure(result2["flow_id"], {}) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == f"Powerview Generation {api_version}" assert result3["data"] == {CONF_HOST: "1.2.3.4", CONF_API_VERSION: api_version} assert result3["result"].unique_id == "A1B2C3D4E5G6H7" @@ -127,7 +127,7 @@ async def test_form_homekit_and_dhcp( data=discovery_info, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] is None assert result["description_placeholders"] == { @@ -139,7 +139,7 @@ async def test_form_homekit_and_dhcp( result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == f"Powerview Generation {api_version}" assert result2["data"] == {CONF_HOST: "1.2.3.4", CONF_API_VERSION: api_version} assert result2["result"].unique_id == "A1B2C3D4E5G6H7" @@ -151,7 +151,7 @@ async def test_form_homekit_and_dhcp( context={"source": source}, data=discovery_info, ) - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT @pytest.mark.usefixtures("mock_hunterdouglas_hub") @@ -178,7 +178,7 @@ async def test_discovered_by_homekit_and_dhcp( data=homekit_discovery, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" result2 = await hass.config_entries.flow.async_init( @@ -187,7 +187,7 @@ async def test_discovered_by_homekit_and_dhcp( data=dhcp_discovery, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_in_progress" @@ -213,7 +213,7 @@ async def test_form_cannot_connect( {CONF_HOST: "1.2.3.4"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} # Now try again without the patch in place to make sure we can recover @@ -222,7 +222,7 @@ async def test_form_cannot_connect( {CONF_HOST: "1.2.3.4"}, ) - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == f"Powerview Generation {api_version}" assert result3["data"] == {CONF_HOST: "1.2.3.4", CONF_API_VERSION: api_version} assert result3["result"].unique_id == "A1B2C3D4E5G6H7" @@ -257,7 +257,7 @@ async def test_form_no_data( {CONF_HOST: "1.2.3.4"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} # Now try again without the patch in place to make sure we can recover @@ -266,7 +266,7 @@ async def test_form_no_data( {CONF_HOST: "1.2.3.4"}, ) - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == f"Powerview Generation {api_version}" assert result3["data"] == {CONF_HOST: "1.2.3.4", CONF_API_VERSION: api_version} assert result3["result"].unique_id == "A1B2C3D4E5G6H7" @@ -296,7 +296,7 @@ async def test_form_unknown_exception( {CONF_HOST: "1.2.3.4"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} # Now try again without the patch in place to make sure we can recover @@ -305,7 +305,7 @@ async def test_form_unknown_exception( {CONF_HOST: "1.2.3.4"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == f"Powerview Generation {api_version}" assert result2["data"] == {CONF_HOST: "1.2.3.4", CONF_API_VERSION: api_version} assert result2["result"].unique_id == "A1B2C3D4E5G6H7" @@ -335,7 +335,7 @@ async def test_form_unsupported_device( {CONF_HOST: "1.2.3.4"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unsupported_device"} # Now try again without the patch in place to make sure we can recover @@ -344,7 +344,7 @@ async def test_form_unsupported_device( {CONF_HOST: "1.2.3.4"}, ) - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == f"Powerview Generation {api_version}" assert result3["data"] == {CONF_HOST: "1.2.3.4", CONF_API_VERSION: api_version} assert result3["result"].unique_id == "A1B2C3D4E5G6H7" diff --git a/tests/components/husqvarna_automower/test_config_flow.py b/tests/components/husqvarna_automower/test_config_flow.py index e22ab7718ec..ce986c4c724 100644 --- a/tests/components/husqvarna_automower/test_config_flow.py +++ b/tests/components/husqvarna_automower/test_config_flow.py @@ -98,7 +98,7 @@ async def test_config_non_unique_profile( }, ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" @@ -125,7 +125,7 @@ async def test_config_non_unique_profile( }, ) result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/huum/test_config_flow.py b/tests/components/huum/test_config_flow.py index 219783079e3..9917f71fc08 100644 --- a/tests/components/huum/test_config_flow.py +++ b/tests/components/huum/test_config_flow.py @@ -23,7 +23,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -45,7 +45,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_USERNAME assert result2["data"] == { CONF_USERNAME: TEST_USERNAME, @@ -89,7 +89,7 @@ async def test_signup_flow_already_set_up(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT @pytest.mark.parametrize( @@ -122,7 +122,7 @@ async def test_huum_errors( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": error_base} with ( @@ -142,4 +142,4 @@ async def test_huum_errors( CONF_PASSWORD: TEST_PASSWORD, }, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY diff --git a/tests/components/hvv_departures/test_config_flow.py b/tests/components/hvv_departures/test_config_flow.py index 2712e1bbca9..5a7aa8f8dde 100644 --- a/tests/components/hvv_departures/test_config_flow.py +++ b/tests/components/hvv_departures/test_config_flow.py @@ -5,7 +5,6 @@ from unittest.mock import patch from pygti.exceptions import CannotConnect, InvalidAuth -from homeassistant import data_entry_flow from homeassistant.components.hvv_departures.const import ( CONF_FILTER, CONF_REAL_TIME, @@ -15,6 +14,7 @@ from homeassistant.components.hvv_departures.const import ( from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_HOST, CONF_OFFSET, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry, load_fixture @@ -289,7 +289,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -297,7 +297,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: user_input={CONF_FILTER: ["0"], CONF_OFFSET: 15, CONF_REAL_TIME: False}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_FILTER: [ { @@ -349,7 +349,7 @@ async def test_options_flow_invalid_auth(hass: HomeAssistant) -> None: ): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {"base": "invalid_auth"} @@ -388,7 +388,7 @@ async def test_options_flow_cannot_connect(hass: HomeAssistant) -> None: ): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/hydrawise/test_config_flow.py b/tests/components/hydrawise/test_config_flow.py index 17c3eda1699..b0d5b098309 100644 --- a/tests/components/hydrawise/test_config_flow.py +++ b/tests/components/hydrawise/test_config_flow.py @@ -28,7 +28,7 @@ async def test_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -38,7 +38,7 @@ async def test_form( mock_pydrawise.get_user.return_value = user await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Hydrawise" assert result2["data"] == {"api_key": "abc123"} assert len(mock_setup_entry.mock_calls) == 1 @@ -58,13 +58,13 @@ async def test_form_api_error( result = await hass.config_entries.flow.async_configure( init_result["flow_id"], data ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} mock_pydrawise.get_user.reset_mock(side_effect=True) mock_pydrawise.get_user.return_value = user result2 = await hass.config_entries.flow.async_configure(result["flow_id"], data) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY async def test_form_connect_timeout( @@ -80,13 +80,13 @@ async def test_form_connect_timeout( init_result["flow_id"], data ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "timeout_connect"} mock_pydrawise.get_user.reset_mock(side_effect=True) mock_pydrawise.get_user.return_value = user result2 = await hass.config_entries.flow.async_configure(result["flow_id"], data) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY async def test_flow_import_success( @@ -104,7 +104,7 @@ async def test_flow_import_success( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Hydrawise" assert result["data"] == { CONF_API_KEY: "__api_key__", @@ -131,7 +131,7 @@ async def test_flow_import_api_error( }, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" issue_registry = ir.async_get(hass) @@ -155,7 +155,7 @@ async def test_flow_import_connect_timeout( }, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "timeout_connect" issue_registry = ir.async_get(hass) @@ -191,7 +191,7 @@ async def test_flow_import_already_imported( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result.get("reason") == "already_configured" issue_registry = ir.async_get(hass) diff --git a/tests/components/hyperion/test_config_flow.py b/tests/components/hyperion/test_config_flow.py index 86dc4c5c39d..a05316a4bc9 100644 --- a/tests/components/hyperion/test_config_flow.py +++ b/tests/components/hyperion/test_config_flow.py @@ -10,7 +10,6 @@ from unittest.mock import AsyncMock, Mock, patch from hyperion import const -from homeassistant import data_entry_flow from homeassistant.components import ssdp from homeassistant.components.hyperion.const import ( CONF_AUTH_ID, @@ -30,7 +29,7 @@ from homeassistant.const import ( SERVICE_TURN_ON, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult +from homeassistant.data_entry_flow import FlowResult, FlowResultType from . import ( TEST_AUTH_REQUIRED_RESP, @@ -165,7 +164,7 @@ async def test_user_if_no_configuration(hass: HomeAssistant) -> None: """Check flow behavior when no configuration is present.""" result = await _init_flow(hass) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["handler"] == DOMAIN @@ -180,7 +179,7 @@ async def test_user_existing_id_abort(hass: HomeAssistant) -> None: "homeassistant.components.hyperion.client.HyperionClient", return_value=client ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -196,7 +195,7 @@ async def test_user_client_errors(hass: HomeAssistant) -> None: "homeassistant.components.hyperion.client.HyperionClient", return_value=client ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == "cannot_connect" # Fail the auth check call. @@ -206,7 +205,7 @@ async def test_user_client_errors(hass: HomeAssistant) -> None: "homeassistant.components.hyperion.client.HyperionClient", return_value=client ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "auth_required_error" @@ -225,7 +224,7 @@ async def test_user_confirm_cannot_connect(hass: HomeAssistant) -> None: side_effect=[good_client, bad_client], ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -241,7 +240,7 @@ async def test_user_confirm_id_error(hass: HomeAssistant) -> None: "homeassistant.components.hyperion.client.HyperionClient", return_value=client ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_id" @@ -255,7 +254,7 @@ async def test_user_noauth_flow_success(hass: HomeAssistant) -> None: ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["handler"] == DOMAIN assert result["title"] == TEST_TITLE assert result["data"] == { @@ -274,7 +273,7 @@ async def test_user_auth_required(hass: HomeAssistant) -> None: "homeassistant.components.hyperion.client.HyperionClient", return_value=client ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" @@ -288,7 +287,7 @@ async def test_auth_static_token_auth_required_fail(hass: HomeAssistant) -> None "homeassistant.components.hyperion.client.HyperionClient", return_value=client ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "auth_required_error" @@ -308,7 +307,7 @@ async def test_auth_static_token_success(hass: HomeAssistant) -> None: hass, result, user_input={CONF_CREATE_TOKEN: False, CONF_TOKEN: TEST_TOKEN} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["handler"] == DOMAIN assert result["title"] == TEST_TITLE assert result["data"] == { @@ -334,7 +333,7 @@ async def test_auth_static_token_login_connect_fail(hass: HomeAssistant) -> None hass, result, user_input={CONF_CREATE_TOKEN: False, CONF_TOKEN: TEST_TOKEN} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -357,7 +356,7 @@ async def test_auth_static_token_login_fail(hass: HomeAssistant) -> None: hass, result, user_input={CONF_CREATE_TOKEN: False, CONF_TOKEN: TEST_TOKEN} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == "invalid_access_token" @@ -372,7 +371,7 @@ async def test_auth_create_token_approval_declined(hass: HomeAssistant) -> None: "homeassistant.components.hyperion.client.HyperionClient", return_value=client ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" client.async_request_token = AsyncMock(return_value=TEST_REQUEST_TOKEN_FAIL) @@ -390,7 +389,7 @@ async def test_auth_create_token_approval_declined(hass: HomeAssistant) -> None: hass, result, user_input={CONF_CREATE_TOKEN: True} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "create_token" assert result["description_placeholders"] == { CONF_AUTH_ID: TEST_AUTH_ID, @@ -398,13 +397,13 @@ async def test_auth_create_token_approval_declined(hass: HomeAssistant) -> None: result = await _configure_flow(hass, result) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["step_id"] == "create_token_external" # The flow will be automatically advanced by the auth token response. result = await _configure_flow(hass, result) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "auth_new_token_not_granted_error" @@ -486,7 +485,7 @@ async def test_auth_create_token_when_issued_token_fails( "homeassistant.components.hyperion.client.HyperionClient", return_value=client ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" client.async_request_token = AsyncMock(return_value=TEST_REQUEST_TOKEN_SUCCESS) @@ -503,14 +502,14 @@ async def test_auth_create_token_when_issued_token_fails( result = await _configure_flow( hass, result, user_input={CONF_CREATE_TOKEN: True} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "create_token" assert result["description_placeholders"] == { CONF_AUTH_ID: TEST_AUTH_ID, } result = await _configure_flow(hass, result) - assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["step_id"] == "create_token_external" # The flow will be automatically advanced by the auth token response. @@ -519,7 +518,7 @@ async def test_auth_create_token_when_issued_token_fails( client.async_client_connect = AsyncMock(return_value=False) result = await _configure_flow(hass, result) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -534,7 +533,7 @@ async def test_auth_create_token_success(hass: HomeAssistant) -> None: "homeassistant.components.hyperion.client.HyperionClient", return_value=client ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" client.async_request_token = AsyncMock(return_value=TEST_REQUEST_TOKEN_SUCCESS) @@ -551,19 +550,19 @@ async def test_auth_create_token_success(hass: HomeAssistant) -> None: result = await _configure_flow( hass, result, user_input={CONF_CREATE_TOKEN: True} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "create_token" assert result["description_placeholders"] == { CONF_AUTH_ID: TEST_AUTH_ID, } result = await _configure_flow(hass, result) - assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["step_id"] == "create_token_external" # The flow will be automatically advanced by the auth token response. result = await _configure_flow(hass, result) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["handler"] == DOMAIN assert result["title"] == TEST_TITLE assert result["data"] == { @@ -613,7 +612,7 @@ async def test_auth_create_token_success_but_login_fail( # The flow will be automatically advanced by the auth token response. result = await _configure_flow(hass, result) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "auth_new_token_not_work_error" @@ -633,7 +632,7 @@ async def test_ssdp_success(hass: HomeAssistant) -> None: ): result = await _configure_flow(hass, result) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["handler"] == DOMAIN assert result["title"] == TEST_TITLE assert result["data"] == { @@ -654,7 +653,7 @@ async def test_ssdp_cannot_connect(hass: HomeAssistant) -> None: result = await _init_flow(hass, source=SOURCE_SSDP, data=TEST_SSDP_SERVICE_INFO) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -672,7 +671,7 @@ async def test_ssdp_missing_serial(hass: HomeAssistant) -> None: result = await _init_flow(hass, source=SOURCE_SSDP, data=bad_data) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_id" @@ -691,7 +690,7 @@ async def test_ssdp_failure_bad_port_json(hass: HomeAssistant) -> None: result = await _configure_flow(hass, result) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_PORT] == const.DEFAULT_PORT_JSON @@ -716,7 +715,7 @@ async def test_ssdp_failure_bad_port_ui(hass: HomeAssistant) -> None: ): result = await _init_flow(hass, source=SOURCE_SSDP, data=bad_data) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" client.async_request_token = AsyncMock(return_value=TEST_REQUEST_TOKEN_FAIL) @@ -725,7 +724,7 @@ async def test_ssdp_failure_bad_port_ui(hass: HomeAssistant) -> None: hass, result, user_input={CONF_CREATE_TOKEN: True} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "create_token" # Verify a working URL is used despite the bad port number @@ -749,8 +748,8 @@ async def test_ssdp_abort_duplicates(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result_1["type"] == data_entry_flow.FlowResultType.FORM - assert result_2["type"] == data_entry_flow.FlowResultType.ABORT + assert result_1["type"] is FlowResultType.FORM + assert result_2["type"] is FlowResultType.ABORT assert result_2["reason"] == "already_in_progress" @@ -768,7 +767,7 @@ async def test_options_priority(hass: HomeAssistant) -> None: assert hass.states.get(TEST_ENTITY_ID_1) is not None result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" new_priority = 1 @@ -777,7 +776,7 @@ async def test_options_priority(hass: HomeAssistant) -> None: user_input={CONF_PRIORITY: new_priority}, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_PRIORITY] == new_priority # Turn the light on and ensure the new priority is used. @@ -810,14 +809,14 @@ async def test_options_effect_show_list(hass: HomeAssistant) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_EFFECT_SHOW_LIST: ["effect1", "effect3"]}, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY # effect1 and effect3 only, so effect2 is hidden. assert result["data"][CONF_EFFECT_HIDE_LIST] == ["effect2"] @@ -838,7 +837,7 @@ async def test_options_effect_hide_list_cannot_connect(hass: HomeAssistant) -> N client.async_client_connect = AsyncMock(return_value=False) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -867,13 +866,13 @@ async def test_reauth_success(hass: HomeAssistant) -> None: data=config_data, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await _configure_flow( hass, result, user_input={CONF_CREATE_TOKEN: False, CONF_TOKEN: TEST_TOKEN} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert CONF_TOKEN in config_entry.data @@ -899,5 +898,5 @@ async def test_reauth_cannot_connect(hass: HomeAssistant) -> None: data=config_data, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" diff --git a/tests/components/ialarm/test_config_flow.py b/tests/components/ialarm/test_config_flow.py index 816f03efa9e..d20200a1457 100644 --- a/tests/components/ialarm/test_config_flow.py +++ b/tests/components/ialarm/test_config_flow.py @@ -2,10 +2,11 @@ from unittest.mock import patch -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.ialarm.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -20,7 +21,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -42,7 +43,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_DATA["host"] assert result2["data"] == TEST_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -62,7 +63,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: result["flow_id"], TEST_DATA ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -80,7 +81,7 @@ async def test_form_exception(hass: HomeAssistant) -> None: result["flow_id"], TEST_DATA ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -106,5 +107,5 @@ async def test_form_already_exists(hass: HomeAssistant) -> None: result["flow_id"], TEST_DATA ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/ibeacon/test_config_flow.py b/tests/components/ibeacon/test_config_flow.py index 3a3e1d90d91..0833508d03f 100644 --- a/tests/components/ibeacon/test_config_flow.py +++ b/tests/components/ibeacon/test_config_flow.py @@ -18,7 +18,7 @@ async def test_setup_user_no_bluetooth( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "bluetooth_not_available" @@ -28,13 +28,13 @@ async def test_setup_user(hass: HomeAssistant, enable_bluetooth: None) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch("homeassistant.components.ibeacon.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "iBeacon Tracker" assert result2["data"] == {} @@ -48,7 +48,7 @@ async def test_setup_user_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -62,7 +62,7 @@ async def test_options_flow(hass: HomeAssistant, enable_bluetooth: None) -> None result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" # test save invalid uuid @@ -72,7 +72,7 @@ async def test_options_flow(hass: HomeAssistant, enable_bluetooth: None) -> None "new_uuid": "invalid", }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {"new_uuid": "invalid_uuid_format"} @@ -84,13 +84,13 @@ async def test_options_flow(hass: HomeAssistant, enable_bluetooth: None) -> None "new_uuid": uuid, }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_ALLOW_NAMELESS_UUIDS: [uuid]} # test save duplicate uuid result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -100,13 +100,13 @@ async def test_options_flow(hass: HomeAssistant, enable_bluetooth: None) -> None "new_uuid": uuid, }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_ALLOW_NAMELESS_UUIDS: [uuid]} # delete result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -115,5 +115,5 @@ async def test_options_flow(hass: HomeAssistant, enable_bluetooth: None) -> None CONF_ALLOW_NAMELESS_UUIDS: [], }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_ALLOW_NAMELESS_UUIDS: []} diff --git a/tests/components/icloud/test_config_flow.py b/tests/components/icloud/test_config_flow.py index f13a0e14595..ec8d11f1135 100644 --- a/tests/components/icloud/test_config_flow.py +++ b/tests/components/icloud/test_config_flow.py @@ -5,7 +5,6 @@ from unittest.mock import MagicMock, Mock, patch from pyicloud.exceptions import PyiCloudFailedLoginException import pytest -from homeassistant import data_entry_flow from homeassistant.components.icloud.config_flow import ( CONF_TRUSTED_DEVICE, CONF_VERIFICATION_CODE, @@ -22,6 +21,7 @@ from homeassistant.components.icloud.const import ( from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .const import ( MOCK_CONFIG, @@ -159,7 +159,7 @@ async def test_user(hass: HomeAssistant, service: MagicMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=None ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # test with required @@ -168,7 +168,7 @@ async def test_user(hass: HomeAssistant, service: MagicMock) -> None: context={"source": SOURCE_USER}, data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == CONF_TRUSTED_DEVICE @@ -186,7 +186,7 @@ async def test_user_with_cookie( CONF_WITH_FAMILY: WITH_FAMILY, }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == USERNAME assert result["title"] == USERNAME assert result["data"][CONF_USERNAME] == USERNAME @@ -207,7 +207,7 @@ async def test_login_failed(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_PASSWORD: "invalid_auth"} @@ -220,7 +220,7 @@ async def test_no_device( context={"source": SOURCE_USER}, data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_device" @@ -233,7 +233,7 @@ async def test_trusted_device(hass: HomeAssistant, service: MagicMock) -> None: ) result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == CONF_TRUSTED_DEVICE @@ -248,7 +248,7 @@ async def test_trusted_device_success(hass: HomeAssistant, service: MagicMock) - result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_TRUSTED_DEVICE: 0} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == CONF_VERIFICATION_CODE @@ -265,7 +265,7 @@ async def test_send_verification_code_failed( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_TRUSTED_DEVICE: 0} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == CONF_TRUSTED_DEVICE assert result["errors"] == {CONF_TRUSTED_DEVICE: "send_verification_code"} @@ -282,7 +282,7 @@ async def test_verification_code(hass: HomeAssistant, service: MagicMock) -> Non ) result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == CONF_VERIFICATION_CODE @@ -303,7 +303,7 @@ async def test_verification_code_success( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_VERIFICATION_CODE: "0"} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == USERNAME assert result["title"] == USERNAME assert result["data"][CONF_USERNAME] == USERNAME @@ -329,7 +329,7 @@ async def test_validate_verification_code_failed( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_VERIFICATION_CODE: "0"} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == CONF_TRUSTED_DEVICE assert result["errors"] == {"base": "validate_verification_code"} @@ -348,7 +348,7 @@ async def test_2fa_code_success(hass: HomeAssistant, service_2fa: MagicMock) -> result["flow_id"], {CONF_VERIFICATION_CODE: "0"} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == USERNAME assert result["title"] == USERNAME assert result["data"][CONF_USERNAME] == USERNAME @@ -372,7 +372,7 @@ async def test_validate_2fa_code_failed( result["flow_id"], {CONF_VERIFICATION_CODE: "0"} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == CONF_VERIFICATION_CODE assert result["errors"] == {"base": "validate_verification_code"} @@ -392,13 +392,13 @@ async def test_password_update( data={**MOCK_CONFIG}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_PASSWORD: PASSWORD_2} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert config_entry.data[CONF_PASSWORD] == PASSWORD_2 @@ -416,7 +416,7 @@ async def test_password_update_wrong_password(hass: HomeAssistant) -> None: data={**MOCK_CONFIG}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.icloud.config_flow.PyiCloudService.authenticate", @@ -426,5 +426,5 @@ async def test_password_update_wrong_password(hass: HomeAssistant) -> None: result["flow_id"], {CONF_PASSWORD: PASSWORD_2} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_PASSWORD: "invalid_auth"} diff --git a/tests/components/idasen_desk/test_config_flow.py b/tests/components/idasen_desk/test_config_flow.py index 78eacfb6942..a861dc5f5e2 100644 --- a/tests/components/idasen_desk/test_config_flow.py +++ b/tests/components/idasen_desk/test_config_flow.py @@ -26,7 +26,7 @@ async def test_user_step_success(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -46,7 +46,7 @@ async def test_user_step_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == IDASEN_DISCOVERY_INFO.name assert result2["data"] == { CONF_ADDRESS: IDASEN_DISCOVERY_INFO.address, @@ -64,7 +64,7 @@ async def test_user_step_no_devices_found(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -85,7 +85,7 @@ async def test_user_step_no_new_devices_found(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -101,7 +101,7 @@ async def test_user_step_cannot_connect( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -120,7 +120,7 @@ async def test_user_step_cannot_connect( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -140,7 +140,7 @@ async def test_user_step_cannot_connect( ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == IDASEN_DISCOVERY_INFO.name assert result3["data"] == { CONF_ADDRESS: IDASEN_DISCOVERY_INFO.address, @@ -158,7 +158,7 @@ async def test_user_step_auth_failed(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -177,7 +177,7 @@ async def test_user_step_auth_failed(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "auth_failed"} @@ -197,7 +197,7 @@ async def test_user_step_auth_failed(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == IDASEN_DISCOVERY_INFO.name assert result3["data"] == { CONF_ADDRESS: IDASEN_DISCOVERY_INFO.address, @@ -215,7 +215,7 @@ async def test_user_step_unknown_exception(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -236,7 +236,7 @@ async def test_user_step_unknown_exception(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "unknown"} @@ -260,7 +260,7 @@ async def test_user_step_unknown_exception(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == IDASEN_DISCOVERY_INFO.name assert result3["data"] == { CONF_ADDRESS: IDASEN_DISCOVERY_INFO.address, @@ -276,7 +276,7 @@ async def test_bluetooth_step_success(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=IDASEN_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -298,7 +298,7 @@ async def test_bluetooth_step_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == IDASEN_DISCOVERY_INFO.name assert result2["data"] == { CONF_ADDRESS: IDASEN_DISCOVERY_INFO.address, diff --git a/tests/components/imap/test_config_flow.py b/tests/components/imap/test_config_flow.py index 9d9edae5b14..2354c5fc9b9 100644 --- a/tests/components/imap/test_config_flow.py +++ b/tests/components/imap/test_config_flow.py @@ -7,7 +7,7 @@ from aioimaplib import AioImapException import pytest import voluptuous as vol -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.imap.const import ( CONF_CHARSET, CONF_FOLDER, @@ -44,7 +44,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -59,7 +59,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "email@email.com" assert result2["data"] == MOCK_CONFIG assert len(mock_setup_entry.mock_calls) == 1 @@ -73,7 +73,7 @@ async def test_entry_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -89,7 +89,7 @@ async def test_entry_already_configured(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -107,7 +107,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: result["flow_id"], MOCK_CONFIG ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == { CONF_USERNAME: "invalid_auth", CONF_PASSWORD: "invalid_auth", @@ -138,7 +138,7 @@ async def test_form_cannot_connect( result["flow_id"], MOCK_CONFIG ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": error} # make sure we do not lose the user input if somethings gets wrong @@ -165,7 +165,7 @@ async def test_form_invalid_charset(hass: HomeAssistant) -> None: result["flow_id"], MOCK_CONFIG ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {CONF_CHARSET: "invalid_charset"} @@ -183,7 +183,7 @@ async def test_form_invalid_folder(hass: HomeAssistant) -> None: result["flow_id"], MOCK_CONFIG ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {CONF_FOLDER: "invalid_folder"} @@ -201,7 +201,7 @@ async def test_form_invalid_search(hass: HomeAssistant) -> None: result["flow_id"], MOCK_CONFIG ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {CONF_SEARCH: "invalid_search"} @@ -222,7 +222,7 @@ async def test_reauth_success(hass: HomeAssistant, mock_setup_entry: AsyncMock) data=MOCK_CONFIG, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["description_placeholders"] == {CONF_USERNAME: "email@email.com"} @@ -241,7 +241,7 @@ async def test_reauth_success(hass: HomeAssistant, mock_setup_entry: AsyncMock) ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 @@ -263,7 +263,7 @@ async def test_reauth_failed(hass: HomeAssistant) -> None: data=MOCK_CONFIG, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( @@ -277,7 +277,7 @@ async def test_reauth_failed(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == { CONF_USERNAME: "invalid_auth", CONF_PASSWORD: "invalid_auth", @@ -301,7 +301,7 @@ async def test_reauth_failed_conn_error(hass: HomeAssistant) -> None: data=MOCK_CONFIG, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( @@ -315,7 +315,7 @@ async def test_reauth_failed_conn_error(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -328,7 +328,7 @@ async def test_options_form(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" new_config = MOCK_OPTIONS.copy() @@ -344,7 +344,7 @@ async def test_options_form(hass: HomeAssistant) -> None: result["flow_id"], new_config ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {CONF_SEARCH: "invalid_search"} new_config["search"] = "UnSeen UnDeleted" @@ -358,7 +358,7 @@ async def test_options_form(hass: HomeAssistant) -> None: new_config, ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["data"] == {} for key, value in new_config.items(): assert entry.data[key] == value @@ -381,7 +381,7 @@ async def test_key_options_in_options_form(hass: HomeAssistant) -> None: # so that it conflicts with that of entry1 result = await hass.config_entries.options.async_init(entry2.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" new_config = MOCK_OPTIONS.copy() @@ -395,26 +395,26 @@ async def test_key_options_in_options_form(hass: HomeAssistant) -> None: new_config, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "already_configured"} @pytest.mark.parametrize( ("advanced_options", "assert_result"), [ - ({"max_message_size": 8192}, data_entry_flow.FlowResultType.CREATE_ENTRY), - ({"max_message_size": 1024}, data_entry_flow.FlowResultType.FORM), - ({"max_message_size": 65536}, data_entry_flow.FlowResultType.FORM), + ({"max_message_size": 8192}, FlowResultType.CREATE_ENTRY), + ({"max_message_size": 1024}, FlowResultType.FORM), + ({"max_message_size": 65536}, FlowResultType.FORM), ( {"custom_event_data_template": "{{ subject }}"}, - data_entry_flow.FlowResultType.CREATE_ENTRY, + FlowResultType.CREATE_ENTRY, ), ( {"custom_event_data_template": "{{ invalid_syntax"}, - data_entry_flow.FlowResultType.FORM, + FlowResultType.FORM, ), - ({"enable_push": True}, data_entry_flow.FlowResultType.CREATE_ENTRY), - ({"enable_push": False}, data_entry_flow.FlowResultType.CREATE_ENTRY), + ({"enable_push": True}, FlowResultType.CREATE_ENTRY), + ({"enable_push": False}, FlowResultType.CREATE_ENTRY), ], ids=[ "valid_message_size", @@ -429,7 +429,7 @@ async def test_key_options_in_options_form(hass: HomeAssistant) -> None: async def test_advanced_options_form( hass: HomeAssistant, advanced_options: dict[str, str], - assert_result: data_entry_flow.FlowResultType, + assert_result: FlowResultType, ) -> None: """Test we show the advanced options.""" @@ -442,7 +442,7 @@ async def test_advanced_options_form( context={"source": config_entries.SOURCE_USER, "show_advanced_options": True}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" new_config = MOCK_OPTIONS.copy() @@ -460,14 +460,14 @@ async def test_advanced_options_form( assert result2["type"] == assert_result if result2.get("errors") is not None: - assert assert_result == data_entry_flow.FlowResultType.FORM + assert assert_result is FlowResultType.FORM else: # Check if entry was updated for key, value in new_config.items(): assert entry.data[key] == value except vol.Invalid: # Check if form was expected with these options - assert assert_result == data_entry_flow.FlowResultType.FORM + assert assert_result is FlowResultType.FORM @pytest.mark.parametrize("cipher_list", ["python_default", "modern", "intermediate"]) @@ -483,7 +483,7 @@ async def test_config_flow_with_cipherlist_and_ssl_verify( DOMAIN, context={"source": config_entries.SOURCE_USER, "show_advanced_options": True}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -498,7 +498,7 @@ async def test_config_flow_with_cipherlist_and_ssl_verify( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "email@email.com" assert result2["data"] == config assert len(mock_setup_entry.mock_calls) == 1 @@ -515,7 +515,7 @@ async def test_config_flow_from_with_advanced_settings( DOMAIN, context={"source": config_entries.SOURCE_USER, "show_advanced_options": True}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -527,7 +527,7 @@ async def test_config_flow_from_with_advanced_settings( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"]["base"] == "cannot_connect" assert "ssl_cipher_list" in result2["data_schema"].schema @@ -544,7 +544,7 @@ async def test_config_flow_from_with_advanced_settings( ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "email@email.com" assert result3["data"] == config assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/improv_ble/test_config_flow.py b/tests/components/improv_ble/test_config_flow.py index bafc32907ab..53da1f28425 100644 --- a/tests/components/improv_ble/test_config_flow.py +++ b/tests/components/improv_ble/test_config_flow.py @@ -44,7 +44,7 @@ async def test_user_step_success( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -62,7 +62,7 @@ async def test_user_step_success_authorize(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -83,7 +83,7 @@ async def test_user_step_no_devices_found(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -96,7 +96,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=IMPROV_BLE_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -107,7 +107,7 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM await _test_common_success_wo_identify( hass, result, IMPROV_BLE_DISCOVERY_INFO.address @@ -124,7 +124,7 @@ async def test_bluetooth_step_provisioned_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=PROVISIONED_IMPROV_BLE_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_provisioned" @@ -138,7 +138,7 @@ async def test_bluetooth_step_provisioned_device_2(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=IMPROV_BLE_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" assert len(hass.config_entries.flow.async_progress_by_handler("improv_ble")) == 1 @@ -156,7 +156,7 @@ async def test_bluetooth_step_success(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=IMPROV_BLE_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" assert result["errors"] is None @@ -172,7 +172,7 @@ async def test_bluetooth_step_success_identify(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=IMPROV_BLE_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" assert result["errors"] is None @@ -192,7 +192,7 @@ async def _test_common_success_with_identify( result["flow_id"], {CONF_ADDRESS: address}, ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["menu_options"] == ["identify", "provision"] assert result["step_id"] == "main_menu" @@ -201,12 +201,12 @@ async def _test_common_success_with_identify( result["flow_id"], {"next_step_id": "identify"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "identify" assert result["errors"] is None result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["menu_options"] == ["identify", "provision"] assert result["step_id"] == "main_menu" @@ -214,7 +214,7 @@ async def _test_common_success_with_identify( result["flow_id"], {"next_step_id": "provision"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "provision" assert result["errors"] is None @@ -237,7 +237,7 @@ async def _test_common_success_wo_identify( result["flow_id"], {CONF_ADDRESS: address}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "provision" assert result["errors"] is None @@ -266,14 +266,14 @@ async def _test_common_success( result = await hass.config_entries.flow.async_configure( result["flow_id"], {"ssid": "MyWIFI", "password": "secret"} ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["progress_action"] == "provisioning" assert result["step_id"] == "do_provision" await hass.async_block_till_done() result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result.get("description_placeholders") == placeholders - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == abort_reason mock_provision.assert_awaited_once_with("MyWIFI", "secret", None) @@ -290,7 +290,7 @@ async def _test_common_success_wo_identify_w_authorize( result["flow_id"], {CONF_ADDRESS: address}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "provision" assert result["errors"] is None @@ -321,7 +321,7 @@ async def _test_common_success_w_authorize( result = await hass.config_entries.flow.async_configure( result["flow_id"], {"ssid": "MyWIFI", "password": "secret"} ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["progress_action"] == "authorize" assert result["step_id"] == "authorize" mock_subscribe_state_updates.assert_awaited_once() @@ -338,14 +338,14 @@ async def _test_common_success_w_authorize( ) as mock_provision, ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["progress_action"] == "provisioning" assert result["step_id"] == "do_provision" await hass.async_block_till_done() result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["description_placeholders"] == {"url": "http://blabla.local"} - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "provision_successful_url" mock_provision.assert_awaited_once_with("MyWIFI", "secret", None) @@ -358,7 +358,7 @@ async def test_bluetooth_step_already_in_progress(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=IMPROV_BLE_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -366,7 +366,7 @@ async def test_bluetooth_step_already_in_progress(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=IMPROV_BLE_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -385,12 +385,12 @@ async def test_can_identify_fails(hass: HomeAssistant, exc, error) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=IMPROV_BLE_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" assert result["errors"] is None result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" assert result["errors"] is None @@ -401,7 +401,7 @@ async def test_can_identify_fails(hass: HomeAssistant, exc, error) -> None: result["flow_id"], {CONF_ADDRESS: IMPROV_BLE_DISCOVERY_INFO.address}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == error @@ -420,12 +420,12 @@ async def test_identify_fails(hass: HomeAssistant, exc, error) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=IMPROV_BLE_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" assert result["errors"] is None result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" assert result["errors"] is None @@ -436,7 +436,7 @@ async def test_identify_fails(hass: HomeAssistant, exc, error) -> None: result["flow_id"], {CONF_ADDRESS: IMPROV_BLE_DISCOVERY_INFO.address}, ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "main_menu" with patch(f"{IMPROV_BLE}.config_flow.ImprovBLEClient.identify", side_effect=exc): @@ -444,7 +444,7 @@ async def test_identify_fails(hass: HomeAssistant, exc, error) -> None: result["flow_id"], {"next_step_id": "identify"}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == error @@ -463,12 +463,12 @@ async def test_need_authorization_fails(hass: HomeAssistant, exc, error) -> None context={"source": config_entries.SOURCE_BLUETOOTH}, data=IMPROV_BLE_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" assert result["errors"] is None result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" assert result["errors"] is None @@ -479,7 +479,7 @@ async def test_need_authorization_fails(hass: HomeAssistant, exc, error) -> None result["flow_id"], {CONF_ADDRESS: IMPROV_BLE_DISCOVERY_INFO.address}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "provision" with patch( @@ -488,7 +488,7 @@ async def test_need_authorization_fails(hass: HomeAssistant, exc, error) -> None result = await hass.config_entries.flow.async_configure( result["flow_id"], {"ssid": "MyWIFI", "password": "secret"} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == error @@ -507,12 +507,12 @@ async def test_authorize_fails(hass: HomeAssistant, exc, error) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=IMPROV_BLE_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" assert result["errors"] is None result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" assert result["errors"] is None @@ -523,7 +523,7 @@ async def test_authorize_fails(hass: HomeAssistant, exc, error) -> None: result["flow_id"], {CONF_ADDRESS: IMPROV_BLE_DISCOVERY_INFO.address}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "provision" with ( @@ -539,7 +539,7 @@ async def test_authorize_fails(hass: HomeAssistant, exc, error) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], {"ssid": "MyWIFI", "password": "secret"} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == error @@ -550,12 +550,12 @@ async def _test_provision_error(hass: HomeAssistant, exc) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=IMPROV_BLE_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" assert result["errors"] is None result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" assert result["errors"] is None @@ -566,7 +566,7 @@ async def _test_provision_error(hass: HomeAssistant, exc) -> None: result["flow_id"], {CONF_ADDRESS: IMPROV_BLE_DISCOVERY_INFO.address}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "provision" with ( @@ -582,7 +582,7 @@ async def _test_provision_error(hass: HomeAssistant, exc) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], {"ssid": "MyWIFI", "password": "secret"} ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["progress_action"] == "provisioning" assert result["step_id"] == "do_provision" await hass.async_block_till_done() @@ -604,7 +604,7 @@ async def test_provision_fails(hass: HomeAssistant, exc, error) -> None: flow_id = await _test_provision_error(hass, exc) result = await hass.config_entries.flow.async_configure(flow_id) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == error @@ -627,7 +627,7 @@ async def test_provision_not_authorized(hass: HomeAssistant, exc, error) -> None ): flow_id = await _test_provision_error(hass, exc) result = await hass.config_entries.flow.async_configure(flow_id) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["progress_action"] == "authorize" assert result["step_id"] == "authorize" @@ -646,6 +646,6 @@ async def test_provision_retry(hass: HomeAssistant, exc, error) -> None: flow_id = await _test_provision_error(hass, exc) result = await hass.config_entries.flow.async_configure(flow_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "provision" assert result["errors"] == {"base": error} diff --git a/tests/components/inkbird/test_config_flow.py b/tests/components/inkbird/test_config_flow.py index ffb25ebd093..154132c34fc 100644 --- a/tests/components/inkbird/test_config_flow.py +++ b/tests/components/inkbird/test_config_flow.py @@ -19,13 +19,13 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=IBBQ_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch("homeassistant.components.inkbird.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "iBBQ AC3D" assert result2["data"] == {} assert result2["result"].unique_id == "4125DDBA-2774-4851-9889-6AADDD4CAC3D" @@ -38,7 +38,7 @@ async def test_async_step_bluetooth_not_inkbird(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_INKBIRD_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -48,7 +48,7 @@ async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -62,14 +62,14 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch("homeassistant.components.inkbird.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"address": "61DE521B-F0BF-9F44-64D4-75BBE1738105"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "IBS-TH 8105" assert result2["data"] == {} assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" @@ -85,7 +85,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -99,7 +99,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - result["flow_id"], user_input={"address": "61DE521B-F0BF-9F44-64D4-75BBE1738105"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -121,7 +121,7 @@ async def test_async_step_user_with_found_devices_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -138,7 +138,7 @@ async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant) - context={"source": config_entries.SOURCE_BLUETOOTH}, data=SPS_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -149,7 +149,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=SPS_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -157,7 +157,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=SPS_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -170,7 +170,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=SPS_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -181,14 +181,14 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch("homeassistant.components.inkbird.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"address": "61DE521B-F0BF-9F44-64D4-75BBE1738105"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "IBS-TH 8105" assert result2["data"] == {} assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" diff --git a/tests/components/insteon/test_config_flow.py b/tests/components/insteon/test_config_flow.py index df7430bc254..35ebfb62b77 100644 --- a/tests/components/insteon/test_config_flow.py +++ b/tests/components/insteon/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import patch import pytest from voluptuous_serialize import convert -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import dhcp, usb from homeassistant.components.insteon.config_flow import ( STEP_ADD_OVERRIDE, @@ -40,6 +40,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .const import ( MOCK_DEVICE, @@ -91,7 +92,7 @@ async def _init_form(hass, modem_type): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.MENU + assert result["type"] is FlowResultType.MENU result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -142,7 +143,7 @@ async def test_fail_on_existing(hass: HomeAssistant) -> None: data={**MOCK_USER_INPUT_HUB_V2, CONF_HUB_VERSION: 2}, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -238,13 +239,13 @@ async def test_form_discovery_dhcp(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=discovery_info ) - assert result["type"] == data_entry_flow.FlowResultType.MENU + assert result["type"] is FlowResultType.MENU result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": STEP_HUB_V2}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM schema = convert(result2["data_schema"]) found_host = False for field in schema: @@ -298,7 +299,7 @@ async def _options_init_form(hass, entry_id, step): with patch(PATCH_ASYNC_SETUP_ENTRY, return_value=True): result = await hass.config_entries.options.async_init(entry_id) - assert result["type"] == data_entry_flow.FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "init" result2 = await hass.config_entries.options.async_configure( @@ -346,7 +347,7 @@ async def test_options_change_hub_config(hass: HomeAssistant) -> None: CONF_PASSWORD: "new password", } result, _ = await _options_form(hass, result["flow_id"], user_input) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == {} assert config_entry.data == {**user_input, CONF_HUB_VERSION: 2} @@ -375,7 +376,7 @@ async def test_options_change_hub_bad_config(hass: HomeAssistant) -> None: hass, result["flow_id"], user_input, mock_failed_connection ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == "cannot_connect" @@ -396,7 +397,7 @@ async def test_options_change_plm_config(hass: HomeAssistant) -> None: user_input = {CONF_DEVICE: "/dev/ttyUSB0"} result, _ = await _options_form(hass, result["flow_id"], user_input) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == {} assert config_entry.data == user_input @@ -420,9 +421,9 @@ async def test_options_change_plm_bad_config(hass: HomeAssistant) -> None: hass, result["flow_id"], user_input, mock_failed_connection ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == "cannot_connect" @@ -445,7 +446,7 @@ async def test_options_add_device_override(hass: HomeAssistant) -> None: } result, _ = await _options_form(hass, result["flow_id"], user_input) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert len(config_entry.options[CONF_OVERRIDE]) == 1 assert config_entry.options[CONF_OVERRIDE][0][CONF_ADDRESS] == "1A.2B.3C" assert config_entry.options[CONF_OVERRIDE][0][CONF_CAT] == 4 @@ -489,7 +490,7 @@ async def test_options_remove_device_override(hass: HomeAssistant) -> None: user_input = {CONF_ADDRESS: "1A.2B.3C"} result, _ = await _options_form(hass, result["flow_id"], user_input) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert len(config_entry.options[CONF_OVERRIDE]) == 1 @@ -521,7 +522,7 @@ async def test_options_remove_device_override_with_x10(hass: HomeAssistant) -> N user_input = {CONF_ADDRESS: "1A.2B.3C"} result, _ = await _options_form(hass, result["flow_id"], user_input) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert len(config_entry.options[CONF_OVERRIDE]) == 1 assert len(config_entry.options[CONF_X10]) == 1 @@ -546,7 +547,7 @@ async def test_options_add_x10_device(hass: HomeAssistant) -> None: } result2, _ = await _options_form(hass, result["flow_id"], user_input) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert len(config_entry.options[CONF_X10]) == 1 assert config_entry.options[CONF_X10][0][CONF_HOUSECODE] == "c" assert config_entry.options[CONF_X10][0][CONF_UNITCODE] == 12 @@ -562,7 +563,7 @@ async def test_options_add_x10_device(hass: HomeAssistant) -> None: } result3, _ = await _options_form(hass, result["flow_id"], user_input) - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert len(config_entry.options[CONF_X10]) == 2 assert config_entry.options[CONF_X10][1][CONF_HOUSECODE] == "d" assert config_entry.options[CONF_X10][1][CONF_UNITCODE] == 10 @@ -603,7 +604,7 @@ async def test_options_remove_x10_device(hass: HomeAssistant) -> None: user_input = {CONF_DEVICE: "Housecode: C, Unitcode: 4"} result, _ = await _options_form(hass, result["flow_id"], user_input) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert len(config_entry.options[CONF_X10]) == 1 @@ -638,7 +639,7 @@ async def test_options_remove_x10_device_with_override(hass: HomeAssistant) -> N user_input = {CONF_DEVICE: "Housecode: C, Unitcode: 4"} result, _ = await _options_form(hass, result["flow_id"], user_input) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert len(config_entry.options[CONF_X10]) == 1 assert len(config_entry.options[CONF_OVERRIDE]) == 1 @@ -663,7 +664,7 @@ async def test_options_override_bad_data(hass: HomeAssistant) -> None: } result, _ = await _options_form(hass, result["flow_id"], user_input) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "input_error"} @@ -681,7 +682,7 @@ async def test_discovery_via_usb(hass: HomeAssistant) -> None: "insteon", context={"source": config_entries.SOURCE_USB}, data=discovery_info ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm_usb" with patch(PATCH_CONNECTION), patch(PATCH_ASYNC_SETUP, return_value=True): @@ -690,7 +691,7 @@ async def test_discovery_via_usb(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == {"device": "/dev/ttyINSTEON"} @@ -714,5 +715,5 @@ async def test_discovery_via_usb_already_setup(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/integration/test_config_flow.py b/tests/components/integration/test_config_flow.py index 4f811e98de2..179984f20f2 100644 --- a/tests/components/integration/test_config_flow.py +++ b/tests/components/integration/test_config_flow.py @@ -20,7 +20,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -39,7 +39,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "My integration" assert result["data"] == {} assert result["options"] == { @@ -96,7 +96,7 @@ async def test_options(hass: HomeAssistant, platform) -> None: 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["type"] is FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema assert get_suggested(schema, "round") == 1.0 @@ -107,7 +107,7 @@ async def test_options(hass: HomeAssistant, platform) -> None: "round": 2.0, }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "method": "left", "name": "My integration", diff --git a/tests/components/intellifire/test_config_flow.py b/tests/components/intellifire/test_config_flow.py index 7f6f509a3a3..6c5e082ba13 100644 --- a/tests/components/intellifire/test_config_flow.py +++ b/tests/components/intellifire/test_config_flow.py @@ -36,7 +36,7 @@ async def test_no_discovery( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual_device_entry" @@ -48,7 +48,7 @@ async def test_no_discovery( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "api_config" result3 = await hass.config_entries.flow.async_configure( @@ -57,7 +57,7 @@ async def test_no_discovery( ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Fireplace 12345" assert result3["data"] == { CONF_HOST: "1.1.1.1", @@ -98,7 +98,7 @@ async def test_single_discovery( {CONF_USERNAME: "test", CONF_PASSWORD: "AROONIE"}, ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": "iftapi_connect"} @@ -131,7 +131,7 @@ async def test_single_discovery_loign_error( {CONF_USERNAME: "test", CONF_PASSWORD: "AROONIE"}, ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": "api_error"} @@ -195,14 +195,14 @@ async def test_multi_discovery_cannot_connect( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pick_device" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "192.168.1.33"} ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -217,7 +217,7 @@ async def test_form_cannot_connect_manual_entry( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual_device_entry" result2 = await hass.config_entries.flow.async_configure( @@ -227,7 +227,7 @@ async def test_form_cannot_connect_manual_entry( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -262,7 +262,7 @@ async def test_picker_already_discovered( CONF_HOST: "192.168.1.4", }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert len(mock_setup_entry.mock_calls) == 0 @@ -299,7 +299,7 @@ async def test_reauth_flow( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "api_config" result3 = await hass.config_entries.flow.async_configure( @@ -307,7 +307,7 @@ async def test_reauth_flow( {CONF_USERNAME: "test", CONF_PASSWORD: "AROONIE"}, ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert entry.data[CONF_PASSWORD] == "AROONIE" assert entry.data[CONF_USERNAME] == "test" @@ -327,10 +327,10 @@ async def test_dhcp_discovery_intellifire_device( hostname="zentrios-Test", ), ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "dhcp_confirm" result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "dhcp_confirm" result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], user_input={} diff --git a/tests/components/iotawatt/test_config_flow.py b/tests/components/iotawatt/test_config_flow.py index d4980ba978e..a14d833c044 100644 --- a/tests/components/iotawatt/test_config_flow.py +++ b/tests/components/iotawatt/test_config_flow.py @@ -16,7 +16,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER with ( @@ -38,7 +38,7 @@ async def test_form(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { "host": "1.1.1.1", } @@ -50,7 +50,7 @@ async def test_form_auth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -63,7 +63,7 @@ async def test_form_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "auth" with patch( @@ -79,7 +79,7 @@ async def test_form_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "auth" assert result3["errors"] == {"base": "invalid_auth"} @@ -102,7 +102,7 @@ async def test_form_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result4["type"] == FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert len(mock_setup_entry.mock_calls) == 1 assert result4["data"] == { "host": "1.1.1.1", @@ -126,7 +126,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: {"host": "1.1.1.1"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -145,5 +145,5 @@ async def test_form_setup_exception(hass: HomeAssistant) -> None: {"host": "1.1.1.1"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/ipp/test_config_flow.py b/tests/components/ipp/test_config_flow.py index 1ea75ecf167..57e229a995b 100644 --- a/tests/components/ipp/test_config_flow.py +++ b/tests/components/ipp/test_config_flow.py @@ -40,7 +40,7 @@ async def test_show_user_form(hass: HomeAssistant) -> None: ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM async def test_show_zeroconf_form( @@ -56,7 +56,7 @@ async def test_show_zeroconf_form( ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["description_placeholders"] == {CONF_NAME: "EPSON XP-6000 Series"} @@ -75,7 +75,7 @@ async def test_connection_error( ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -93,7 +93,7 @@ async def test_zeroconf_connection_error( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -109,7 +109,7 @@ async def test_zeroconf_confirm_connection_error( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -128,7 +128,7 @@ async def test_user_connection_upgrade_required( ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "connection_upgrade"} @@ -146,7 +146,7 @@ async def test_zeroconf_connection_upgrade_required( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "connection_upgrade" @@ -164,7 +164,7 @@ async def test_user_parse_error( data=user_input, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "parse_error" @@ -182,7 +182,7 @@ async def test_zeroconf_parse_error( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "parse_error" @@ -200,7 +200,7 @@ async def test_user_ipp_error( data=user_input, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "ipp_error" @@ -218,7 +218,7 @@ async def test_zeroconf_ipp_error( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "ipp_error" @@ -236,7 +236,7 @@ async def test_user_ipp_version_error( data=user_input, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "ipp_version_error" @@ -254,7 +254,7 @@ async def test_zeroconf_ipp_version_error( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "ipp_version_error" @@ -273,7 +273,7 @@ async def test_user_device_exists_abort( data=user_input, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -292,7 +292,7 @@ async def test_zeroconf_device_exists_abort( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -316,7 +316,7 @@ async def test_zeroconf_with_uuid_device_exists_abort( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -342,7 +342,7 @@ async def test_zeroconf_with_uuid_device_exists_abort_new_host( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_config_entry.data[CONF_HOST] == "1.2.3.9" @@ -366,14 +366,14 @@ async def test_zeroconf_empty_unique_id( data=discovery_info, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "192.168.1.31", CONF_BASE_PATH: "/ipp/print"}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "EPSON XP-6000 Series" assert result["data"] @@ -399,14 +399,14 @@ async def test_zeroconf_no_unique_id( data=discovery_info, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "192.168.1.31", CONF_BASE_PATH: "/ipp/print"}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "EPSON XP-6000 Series" assert result["data"] @@ -428,14 +428,14 @@ async def test_full_user_flow_implementation( ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "192.168.1.31", CONF_BASE_PATH: "/ipp/print"}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "192.168.1.31" assert result["data"] @@ -459,13 +459,13 @@ async def test_full_zeroconf_flow_implementation( ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "EPSON XP-6000 Series" assert result["data"] @@ -491,14 +491,14 @@ async def test_full_zeroconf_tls_flow_implementation( ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["description_placeholders"] == {CONF_NAME: "EPSON XP-6000 Series"} result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "EPSON XP-6000 Series" assert result["data"] @@ -535,14 +535,14 @@ async def test_zeroconf_empty_unique_id_uses_serial(hass: HomeAssistant) -> None data=discovery_info, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "192.168.1.31", CONF_BASE_PATH: "/ipp/print"}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "EPSON XP-6000 Series" assert result["data"] diff --git a/tests/components/iqvia/test_config_flow.py b/tests/components/iqvia/test_config_flow.py index a75eed8ecd0..17c977a6b4c 100644 --- a/tests/components/iqvia/test_config_flow.py +++ b/tests/components/iqvia/test_config_flow.py @@ -1,9 +1,9 @@ """Define tests for the IQVIA config flow.""" -from homeassistant import data_entry_flow from homeassistant.components.iqvia import CONF_ZIP_CODE, DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType async def test_duplicate_error(hass: HomeAssistant, config, config_entry) -> None: @@ -11,7 +11,7 @@ async def test_duplicate_error(hass: HomeAssistant, config, config_entry) -> Non result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -20,7 +20,7 @@ async def test_invalid_zip_code(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={CONF_ZIP_CODE: "bad"} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_ZIP_CODE: "invalid_zip_code"} @@ -29,7 +29,7 @@ async def test_show_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -38,6 +38,6 @@ async def test_step_user(hass: HomeAssistant, config, setup_iqvia) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "12345" assert result["data"] == {CONF_ZIP_CODE: "12345"} diff --git a/tests/components/islamic_prayer_times/test_config_flow.py b/tests/components/islamic_prayer_times/test_config_flow.py index 41a5c3df0ac..be8eca210d3 100644 --- a/tests/components/islamic_prayer_times/test_config_flow.py +++ b/tests/components/islamic_prayer_times/test_config_flow.py @@ -6,7 +6,7 @@ from prayer_times_calculator import InvalidResponseError import pytest from requests.exceptions import ConnectionError as ConnError -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import islamic_prayer_times from homeassistant.components.islamic_prayer_times.const import ( CONF_CALC_METHOD, @@ -16,6 +16,7 @@ from homeassistant.components.islamic_prayer_times.const import ( DOMAIN, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import MOCK_CONFIG, MOCK_USER_INPUT @@ -29,7 +30,7 @@ async def test_flow_works(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( islamic_prayer_times.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -41,7 +42,7 @@ async def test_flow_works(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Home" @@ -59,7 +60,7 @@ async def test_flow_error( result = await hass.config_entries.flow.async_init( islamic_prayer_times.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -71,7 +72,7 @@ async def test_flow_error( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == error @@ -87,7 +88,7 @@ async def test_options(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -99,7 +100,7 @@ async def test_options(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_CALC_METHOD] == "makkah" assert result["data"][CONF_LAT_ADJ_METHOD] == "one_seventh" assert result["data"][CONF_MIDNIGHT_MODE] == "standard" @@ -115,7 +116,7 @@ async def test_integration_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( islamic_prayer_times.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -123,5 +124,5 @@ async def test_integration_already_configured(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/iss/test_config_flow.py b/tests/components/iss/test_config_flow.py index 77a61eaa770..73261fbc2e7 100644 --- a/tests/components/iss/test_config_flow.py +++ b/tests/components/iss/test_config_flow.py @@ -2,11 +2,11 @@ from unittest.mock import patch -from homeassistant import data_entry_flow from homeassistant.components.iss.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_SHOW_ON_MAP from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -18,7 +18,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" with patch("homeassistant.components.iss.async_setup_entry", return_value=True): @@ -27,7 +27,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: {}, ) - assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("result").data == {} @@ -43,7 +43,7 @@ async def test_integration_already_exists(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data={} ) - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" diff --git a/tests/components/isy994/test_config_flow.py b/tests/components/isy994/test_config_flow.py index b29b1dbc775..dc9c19e9e75 100644 --- a/tests/components/isy994/test_config_flow.py +++ b/tests/components/isy994/test_config_flow.py @@ -6,7 +6,7 @@ from unittest.mock import patch from pyisy import ISYConnectionError, ISYInvalidAuthError import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import dhcp, ssdp from homeassistant.components.isy994.const import ( CONF_TLS_VER, @@ -17,6 +17,7 @@ from homeassistant.components.isy994.const import ( from homeassistant.config_entries import SOURCE_DHCP, SOURCE_IGNORE, SOURCE_SSDP from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -88,7 +89,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -103,7 +104,7 @@ async def test_form(hass: HomeAssistant) -> None: MOCK_USER_INPUT, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == f"{MOCK_DEVICE_NAME} ({MOCK_HOSTNAME})" assert result2["result"].unique_id == MOCK_UUID assert result2["data"] == MOCK_USER_INPUT @@ -126,7 +127,7 @@ async def test_form_invalid_host(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_host"} @@ -144,7 +145,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: MOCK_USER_INPUT, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {CONF_PASSWORD: "invalid_auth"} @@ -162,7 +163,7 @@ async def test_form_unknown_exeption(hass: HomeAssistant) -> None: MOCK_USER_INPUT, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -180,7 +181,7 @@ async def test_form_isy_connection_error(hass: HomeAssistant) -> None: MOCK_USER_INPUT, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -200,7 +201,7 @@ async def test_form_isy_parse_response_error( MOCK_USER_INPUT, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert "ISY Could not parse response, poorly formatted XML." in caplog.text @@ -220,7 +221,7 @@ async def test_form_no_name_in_response(hass: HomeAssistant) -> None: MOCK_USER_INPUT, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -231,7 +232,7 @@ async def test_form_existing_config_entry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE): @@ -239,7 +240,7 @@ async def test_form_existing_config_entry(hass: HomeAssistant) -> None: result["flow_id"], MOCK_USER_INPUT, ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT async def test_form_ssdp_already_configured(hass: HomeAssistant) -> None: @@ -264,7 +265,7 @@ async def test_form_ssdp_already_configured(hass: HomeAssistant) -> None: }, ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT async def test_form_ssdp(hass: HomeAssistant) -> None: @@ -283,7 +284,7 @@ async def test_form_ssdp(hass: HomeAssistant) -> None: }, ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -300,7 +301,7 @@ async def test_form_ssdp(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == f"{MOCK_DEVICE_NAME} ({MOCK_HOSTNAME})" assert result2["result"].unique_id == MOCK_UUID assert result2["data"] == MOCK_USER_INPUT @@ -333,7 +334,7 @@ async def test_form_ssdp_existing_entry(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == f"http://3.3.3.3:80{ISY_URL_POSTFIX}" @@ -364,7 +365,7 @@ async def test_form_ssdp_existing_entry_with_no_port(hass: HomeAssistant) -> Non ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == f"http://3.3.3.3:80/{ISY_URL_POSTFIX}" @@ -397,7 +398,7 @@ async def test_form_ssdp_existing_entry_with_alternate_port( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == f"http://3.3.3.3:1443/{ISY_URL_POSTFIX}" @@ -428,7 +429,7 @@ async def test_form_ssdp_existing_entry_no_port_https(hass: HomeAssistant) -> No ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == f"https://3.3.3.3:443/{ISY_URL_POSTFIX}" @@ -445,7 +446,7 @@ async def test_form_dhcp(hass: HomeAssistant) -> None: macaddress=MOCK_MAC, ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -462,7 +463,7 @@ async def test_form_dhcp(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == f"{MOCK_DEVICE_NAME} ({MOCK_HOSTNAME})" assert result2["result"].unique_id == MOCK_UUID assert result2["data"] == MOCK_USER_INPUT @@ -481,7 +482,7 @@ async def test_form_dhcp_with_polisy(hass: HomeAssistant) -> None: macaddress=MOCK_POLISY_MAC, ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} assert ( @@ -502,7 +503,7 @@ async def test_form_dhcp_with_polisy(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == f"{MOCK_DEVICE_NAME} ({MOCK_HOSTNAME})" assert result2["result"].unique_id == MOCK_UUID assert result2["data"] == MOCK_IOX_USER_INPUT @@ -521,7 +522,7 @@ async def test_form_dhcp_with_eisy(hass: HomeAssistant) -> None: macaddress=MOCK_MAC, ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} assert ( @@ -542,7 +543,7 @@ async def test_form_dhcp_with_eisy(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == f"{MOCK_DEVICE_NAME} ({MOCK_HOSTNAME})" assert result2["result"].unique_id == MOCK_UUID assert result2["data"] == MOCK_IOX_USER_INPUT @@ -571,7 +572,7 @@ async def test_form_dhcp_existing_entry(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == f"http://1.2.3.4{ISY_URL_POSTFIX}" @@ -601,7 +602,7 @@ async def test_form_dhcp_existing_entry_preserves_port(hass: HomeAssistant) -> N ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == f"http://1.2.3.4:1443{ISY_URL_POSTFIX}" assert entry.data[CONF_USERNAME] == "bob" @@ -627,7 +628,7 @@ async def test_form_dhcp_existing_ignored_entry(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/izone/test_config_flow.py b/tests/components/izone/test_config_flow.py index 0988640d644..9f668e1ec62 100644 --- a/tests/components/izone/test_config_flow.py +++ b/tests/components/izone/test_config_flow.py @@ -4,9 +4,10 @@ from unittest.mock import Mock, patch import pytest -from homeassistant import config_entries, data_entry_flow +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 @pytest.fixture @@ -46,10 +47,10 @@ async def test_not_found(hass: HomeAssistant, mock_disco) -> None: ) # Confirmation form - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT await hass.async_block_till_done() @@ -79,10 +80,10 @@ async def test_found(hass: HomeAssistant, mock_disco) -> None: ) # Confirmation form - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() diff --git a/tests/components/jellyfin/test_config_flow.py b/tests/components/jellyfin/test_config_flow.py index 23c530d7e4d..2256a3d5d89 100644 --- a/tests/components/jellyfin/test_config_flow.py +++ b/tests/components/jellyfin/test_config_flow.py @@ -4,10 +4,11 @@ from unittest.mock import MagicMock import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.jellyfin.const import CONF_CLIENT_DEVICE_ID, DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import async_load_json_fixture from .const import REAUTH_INPUT, TEST_PASSWORD, TEST_URL, TEST_USERNAME, USER_INPUT @@ -24,7 +25,7 @@ async def test_abort_if_existing_entry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -225,7 +226,7 @@ async def test_reauth( data=USER_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -241,7 +242,7 @@ async def test_reauth( ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -275,7 +276,7 @@ async def test_reauth_cannot_connect( data=USER_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -308,7 +309,7 @@ async def test_reauth_cannot_connect( result["flow_id"], user_input=REAUTH_INPUT, ) - assert result3["type"] == data_entry_flow.FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" @@ -342,7 +343,7 @@ async def test_reauth_invalid( data=USER_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -369,7 +370,7 @@ async def test_reauth_invalid( result["flow_id"], user_input=REAUTH_INPUT, ) - assert result3["type"] == data_entry_flow.FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" @@ -403,7 +404,7 @@ async def test_reauth_exception( data=USER_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -432,5 +433,5 @@ async def test_reauth_exception( result["flow_id"], user_input=REAUTH_INPUT, ) - assert result3["type"] == data_entry_flow.FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" diff --git a/tests/components/justnimbus/test_config_flow.py b/tests/components/justnimbus/test_config_flow.py index b96b6c8aa5c..f66693a752c 100644 --- a/tests/components/justnimbus/test_config_flow.py +++ b/tests/components/justnimbus/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import patch from justnimbus.exceptions import InvalidClientID, JustNimbusError import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.justnimbus.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -20,7 +20,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None await _set_up_justnimbus(hass=hass, flow_id=result["flow_id"]) @@ -62,7 +62,7 @@ async def test_form_errors( user_input=FIXTURE_USER_INPUT, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == errors await _set_up_justnimbus(hass=hass, flow_id=result["flow_id"]) @@ -81,7 +81,7 @@ async def test_abort_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") is None result2 = await hass.config_entries.flow.async_configure( @@ -89,7 +89,7 @@ async def test_abort_already_configured(hass: HomeAssistant) -> None: user_input=FIXTURE_USER_INPUT, ) - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "already_configured" @@ -108,7 +108,7 @@ async def _set_up_justnimbus(hass: HomeAssistant, flow_id: str) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "JustNimbus" assert result2["data"] == FIXTURE_USER_INPUT assert len(mock_setup_entry.mock_calls) == 1 @@ -134,7 +134,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: data=FIXTURE_OLD_USER_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -147,6 +147,6 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert mock_config.data == FIXTURE_USER_INPUT diff --git a/tests/components/jvc_projector/test_config_flow.py b/tests/components/jvc_projector/test_config_flow.py index a35dcd1ca38..282411540a4 100644 --- a/tests/components/jvc_projector/test_config_flow.py +++ b/tests/components/jvc_projector/test_config_flow.py @@ -26,7 +26,7 @@ async def test_user_config_flow_success( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -39,7 +39,7 @@ async def test_user_config_flow_success( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert "data" in result assert result["data"][CONF_HOST] == MOCK_HOST assert result["data"][CONF_PORT] == MOCK_PORT @@ -59,7 +59,7 @@ async def test_user_config_flow_bad_connect_errors( data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT, CONF_PASSWORD: MOCK_PASSWORD}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -73,7 +73,7 @@ async def test_user_config_flow_bad_connect_errors( data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT, CONF_PASSWORD: MOCK_PASSWORD}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert "data" in result assert result["data"][CONF_HOST] == MOCK_HOST assert result["data"][CONF_PORT] == MOCK_PORT @@ -90,7 +90,7 @@ async def test_user_config_flow_device_exists_abort( context={"source": SOURCE_USER}, data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT, CONF_PASSWORD: MOCK_PASSWORD}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -105,7 +105,7 @@ async def test_user_config_flow_bad_host_errors( data={CONF_HOST: "", CONF_PORT: MOCK_PORT, CONF_PASSWORD: MOCK_PASSWORD}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_host"} @@ -117,7 +117,7 @@ async def test_user_config_flow_bad_host_errors( data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT, CONF_PASSWORD: MOCK_PASSWORD}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert "data" in result assert result["data"][CONF_HOST] == MOCK_HOST assert result["data"][CONF_PORT] == MOCK_PORT @@ -137,7 +137,7 @@ async def test_user_config_flow_bad_auth_errors( data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT, CONF_PASSWORD: MOCK_PASSWORD}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -151,7 +151,7 @@ async def test_user_config_flow_bad_auth_errors( data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT, CONF_PASSWORD: MOCK_PASSWORD}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert "data" in result assert result["data"][CONF_HOST] == MOCK_HOST assert result["data"][CONF_PORT] == MOCK_PORT @@ -171,7 +171,7 @@ async def test_reauth_config_flow_success( }, data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -179,7 +179,7 @@ async def test_reauth_config_flow_success( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert mock_integration.data[CONF_HOST] == MOCK_HOST @@ -202,7 +202,7 @@ async def test_reauth_config_flow_auth_error( }, data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -210,7 +210,7 @@ async def test_reauth_config_flow_auth_error( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "invalid_auth"} @@ -226,7 +226,7 @@ async def test_reauth_config_flow_auth_error( }, data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -234,7 +234,7 @@ async def test_reauth_config_flow_auth_error( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert mock_integration.data[CONF_HOST] == MOCK_HOST @@ -257,7 +257,7 @@ async def test_reauth_config_flow_connect_error( }, data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -265,7 +265,7 @@ async def test_reauth_config_flow_connect_error( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "cannot_connect"} @@ -281,7 +281,7 @@ async def test_reauth_config_flow_connect_error( }, data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -289,7 +289,7 @@ async def test_reauth_config_flow_connect_error( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert mock_integration.data[CONF_HOST] == MOCK_HOST From 83b56ab0054e7da78edb0de32dcd426f4ab134ae Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 2 Apr 2024 23:05:05 +0200 Subject: [PATCH 187/967] Add IMAP seen, move and delete service (#114501) * Add seen, move and delete IMAP services * Add entry_id to the imap_content event data * Use config validation library * Add tests * Add logging * Typo in docstr * Add guard --- homeassistant/components/imap/__init__.py | 170 ++++++++++++++++++- homeassistant/components/imap/coordinator.py | 3 +- homeassistant/components/imap/icons.json | 5 + homeassistant/components/imap/services.yaml | 45 +++++ homeassistant/components/imap/strings.json | 78 +++++++++ tests/components/imap/conftest.py | 18 +- tests/components/imap/test_init.py | 137 +++++++++++++++ 7 files changed, 450 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/imap/services.yaml diff --git a/homeassistant/components/imap/__init__.py b/homeassistant/components/imap/__init__.py index 7504446f3fb..6c90889a7d6 100644 --- a/homeassistant/components/imap/__init__.py +++ b/homeassistant/components/imap/__init__.py @@ -2,16 +2,23 @@ from __future__ import annotations -from aioimaplib import IMAP4_SSL, AioImapException +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.const import EVENT_HOMEASSISTANT_STOP, Platform -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ( ConfigEntryAuthFailed, ConfigEntryError, ConfigEntryNotReady, + ServiceValidationError, ) +import homeassistant.helpers.config_validation as cv from .const import CONF_ENABLE_PUSH, DOMAIN from .coordinator import ( @@ -23,6 +30,70 @@ from .errors import InvalidAuth, InvalidFolder PLATFORMS: list[Platform] = [Platform.SENSOR] +CONF_ENTRY = "entry" +CONF_SEEN = "seen" +CONF_UID = "uid" +CONF_TARGET_FOLDER = "target_folder" + +_LOGGER = logging.getLogger(__name__) + +_SERVICE_UID_SCHEMA = vol.Schema( + { + vol.Required(CONF_ENTRY): cv.string, + vol.Required(CONF_UID): cv.string, + } +) + +SERVICE_SEEN_SCHEMA = _SERVICE_UID_SCHEMA +SERVICE_MOVE_SCHEMA = _SERVICE_UID_SCHEMA.extend( + { + vol.Optional(CONF_SEEN): cv.boolean, + vol.Required(CONF_TARGET_FOLDER): cv.string, + } +) +SERVICE_DELETE_SCHEMA = _SERVICE_UID_SCHEMA + + +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: + 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: + raise ServiceValidationError( + translation_domain=DOMAIN, translation_key="invalid_auth" + ) from exc + except InvalidFolder as exc: + raise ServiceValidationError( + translation_domain=DOMAIN, translation_key="invalid_folder" + ) from exc + except (TimeoutError, AioImapException) as exc: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="imap_server_fail", + translation_placeholders={"error": str(exc)}, + ) from exc + return client + + +@callback +def raise_on_error(response: Response, translation_key: str) -> None: + """Get error message from response.""" + if response.result != "OK": + error: str = response.lines[0].decode("utf-8") + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key=translation_key, + translation_placeholders={"error": error}, + ) + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up imap from a config entry.""" @@ -49,6 +120,97 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) await coordinator.async_config_entry_first_refresh() + async def async_seen(call: ServiceCall) -> None: + """Process mark as seen service call.""" + entry_id: str = call.data[CONF_ENTRY] + uid: str = call.data[CONF_UID] + _LOGGER.debug( + "Mark message %s as seen. Entry: %s", + uid, + entry_id, + ) + client = await async_get_imap_client(hass, entry_id) + try: + response = await client.store(uid, "+FLAGS (\\Seen)") + except (TimeoutError, AioImapException) as exc: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="imap_server_fail", + translation_placeholders={"error": str(exc)}, + ) from exc + raise_on_error(response, "seen_failed") + await client.close() + + if not hass.services.has_service(DOMAIN, "seen"): + hass.services.async_register(DOMAIN, "seen", async_seen, SERVICE_SEEN_SCHEMA) + + async def async_move(call: ServiceCall) -> None: + """Process move email service call.""" + entry_id: str = call.data[CONF_ENTRY] + uid: str = call.data[CONF_UID] + seen = bool(call.data.get(CONF_SEEN)) + target_folder: str = call.data[CONF_TARGET_FOLDER] + _LOGGER.debug( + "Move message %s to folder %s. Mark as seen: %s. Entry: %s", + uid, + target_folder, + seen, + entry_id, + ) + client = await async_get_imap_client(hass, entry_id) + try: + if seen: + response = await client.store(uid, "+FLAGS (\\Seen)") + raise_on_error(response, "seen_failed") + response = await client.copy(uid, target_folder) + raise_on_error(response, "copy_failed") + response = await client.store(uid, "+FLAGS (\\Deleted)") + raise_on_error(response, "delete_failed") + response = await asyncio.wait_for( + client.protocol.expunge(uid, by_uid=True), client.timeout + ) + raise_on_error(response, "expunge_failed") + except (TimeoutError, AioImapException) as exc: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="imap_server_fail", + translation_placeholders={"error": str(exc)}, + ) from exc + await client.close() + + if not hass.services.has_service(DOMAIN, "move"): + hass.services.async_register(DOMAIN, "move", async_move, SERVICE_MOVE_SCHEMA) + + async def async_delete(call: ServiceCall) -> None: + """Process deleting email service call.""" + entry_id: str = call.data[CONF_ENTRY] + uid: str = call.data[CONF_UID] + _LOGGER.debug( + "Delete message %s. Entry: %s", + uid, + entry_id, + ) + client = await async_get_imap_client(hass, entry_id) + try: + response = await client.store(uid, "+FLAGS (\\Deleted)") + raise_on_error(response, "delete_failed") + response = await asyncio.wait_for( + client.protocol.expunge(uid, by_uid=True), client.timeout + ) + raise_on_error(response, "expunge_failed") + except (TimeoutError, AioImapException) as exc: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="imap_server_fail", + translation_placeholders={"error": str(exc)}, + ) from exc + await client.close() + + if not hass.services.has_service(DOMAIN, "delete"): + hass.services.async_register( + DOMAIN, "delete", async_delete, SERVICE_DELETE_SCHEMA + ) + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator entry.async_on_unload( @@ -67,4 +229,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ImapPushDataUpdateCoordinator | ImapPollingDataUpdateCoordinator ) = hass.data[DOMAIN].pop(entry.entry_id) await coordinator.shutdown() + if not hass.data[DOMAIN]: + hass.services.async_remove(DOMAIN, "seen") + hass.services.async_remove(DOMAIN, "move") + hass.services.async_remove(DOMAIN, "delete") return unload_ok diff --git a/homeassistant/components/imap/coordinator.py b/homeassistant/components/imap/coordinator.py index 7f857ff857f..997bff13534 100644 --- a/homeassistant/components/imap/coordinator.py +++ b/homeassistant/components/imap/coordinator.py @@ -1,4 +1,4 @@ -"""Coordinator for imag integration.""" +"""Coordinator for imap integration.""" from __future__ import annotations @@ -254,6 +254,7 @@ class ImapDataUpdateCoordinator(DataUpdateCoordinator[int | None]): initial = False self._last_message_id = message_id data = { + "entry_id": self.config_entry.entry_id, "server": self.config_entry.data[CONF_SERVER], "username": self.config_entry.data[CONF_USERNAME], "search": self.config_entry.data[CONF_SEARCH], diff --git a/homeassistant/components/imap/icons.json b/homeassistant/components/imap/icons.json index a4a79aef60e..2e61cf56573 100644 --- a/homeassistant/components/imap/icons.json +++ b/homeassistant/components/imap/icons.json @@ -8,5 +8,10 @@ } } } + }, + "services": { + "seen": "mdi:email-open-outline", + "move": "mdi:email-arrow-right-outline", + "delete": "mdi:trash-can-outline" } } diff --git a/homeassistant/components/imap/services.yaml b/homeassistant/components/imap/services.yaml new file mode 100644 index 00000000000..f0694bfba70 --- /dev/null +++ b/homeassistant/components/imap/services.yaml @@ -0,0 +1,45 @@ +seen: + fields: + entry: + required: true + selector: + config_entry: + integration: "imap" + uid: + required: true + example: "12" + selector: + text: +move: + fields: + entry: + required: true + selector: + config_entry: + integration: "imap" + uid: + required: true + example: "12" + selector: + text: + seen: + selector: + boolean: + target_folder: + required: true + example: "INBOX.Trash" + selector: + text: + +delete: + fields: + entry: + required: true + selector: + config_entry: + integration: "imap" + uid: + example: "12" + required: true + selector: + text: diff --git a/homeassistant/components/imap/strings.json b/homeassistant/components/imap/strings.json index c332e3e8edb..8c06889361c 100644 --- a/homeassistant/components/imap/strings.json +++ b/homeassistant/components/imap/strings.json @@ -35,6 +35,32 @@ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } }, + "exceptions": { + "copy_failed": { + "message": "Copying the message failed with '{error}'." + }, + "delete_failed": { + "message": "Marking the the message for deletion failed with '{error}'." + }, + "expunge_failed": { + "message": "Expungling the the message failed with '{error}'." + }, + "invalid_entry": { + "message": "No valid IMAP entry was found." + }, + "invalid_auth": { + "message": "[%key:common::config_flow::error::invalid_auth%]" + }, + "invalid_folder": { + "message": "[%key:component::imap::config::error::invalid_folder%]" + }, + "imap_server_fail": { + "message": "The IMAP server failed to connect: {error}." + }, + "seen_failed": { + "message": "Marking message as seen failed with '{error}'." + } + }, "options": { "step": { "init": { @@ -64,5 +90,57 @@ "intermediate": "Intermediate ciphers" } } + }, + "services": { + "seen": { + "name": "Mark message as seen", + "description": "Mark an email as seen.", + "fields": { + "entry": { + "name": "Entry", + "description": "The IMAP config entry." + }, + "uid": { + "name": "UID", + "description": "The email identifier (UID)." + } + } + }, + "move": { + "name": "Move message", + "description": "Move an email to a target folder.", + "fields": { + "entry": { + "name": "[%key:component::imap::services::seen::fields::entry::name%]", + "description": "[%key:component::imap::services::seen::fields::entry::description%]" + }, + "seen": { + "name": "Seen", + "description": "Mark the email as seen." + }, + "uid": { + "name": "[%key:component::imap::services::seen::fields::uid::name%]", + "description": "[%key:component::imap::services::seen::fields::uid::description%]" + }, + "target_folder": { + "name": "Target folder", + "description": "The target folder the email should be moved to." + } + } + }, + "delete": { + "name": "Delete message", + "description": "Delete an email.", + "fields": { + "entry": { + "name": "[%key:component::imap::services::seen::fields::entry::name%]", + "description": "[%key:component::imap::services::seen::fields::entry::description%]" + }, + "uid": { + "name": "[%key:component::imap::services::seen::fields::uid::name%]", + "description": "[%key:component::imap::services::seen::fields::uid::description%]" + } + } + } } } diff --git a/tests/components/imap/conftest.py b/tests/components/imap/conftest.py index 74176efab11..dfe5fa2040f 100644 --- a/tests/components/imap/conftest.py +++ b/tests/components/imap/conftest.py @@ -1,6 +1,6 @@ """Fixtures for imap tests.""" -from collections.abc import Generator +from collections.abc import AsyncGenerator, Generator from unittest.mock import AsyncMock, MagicMock, patch from aioimaplib import AUTH, LOGOUT, NONAUTH, SELECTED, STARTED, Response @@ -62,7 +62,7 @@ async def mock_imap_protocol( imap_pending_idle: bool, imap_login_state: str, imap_select_state: str, -) -> Generator[MagicMock, None]: +) -> AsyncGenerator[MagicMock, None]: """Mock the aioimaplib IMAP protocol handler.""" with patch( @@ -79,7 +79,14 @@ async def mock_imap_protocol( async def close() -> Response: """Mock imap close the selected folder.""" - imap_mock.protocol.state = imap_login_state + return Response("OK", []) + + async def store(uid: str, flags: str) -> Response: + """Mock imap store command.""" + return Response("OK", []) + + async def copy(uid: str, folder: str) -> Response: + """Mock imap store command.""" return Response("OK", []) async def logout() -> Response: @@ -101,12 +108,17 @@ async def mock_imap_protocol( imap_mock.has_pending_idle.return_value = imap_pending_idle imap_mock.protocol = MagicMock() imap_mock.protocol.state = STARTED + imap_mock.protocol.expunge = AsyncMock() + imap_mock.protocol.expunge.return_value = Response("OK", []) imap_mock.has_capability.return_value = imap_has_capability imap_mock.login.side_effect = login imap_mock.close.side_effect = close + imap_mock.copy.side_effect = copy imap_mock.logout.side_effect = logout imap_mock.select.side_effect = select imap_mock.search.return_value = Response(*imap_search) + imap_mock.store.side_effect = store imap_mock.fetch.return_value = Response(*imap_fetch) imap_mock.wait_hello_from_server.side_effect = wait_hello_from_server + imap_mock.timeout = 3 yield imap_mock diff --git a/tests/components/imap/test_init.py b/tests/components/imap/test_init.py index aba9bd88c44..b0cfb9051a4 100644 --- a/tests/components/imap/test_init.py +++ b/tests/components/imap/test_init.py @@ -14,6 +14,7 @@ from homeassistant.components.imap.errors import InvalidAuth, InvalidFolder from homeassistant.components.sensor.const import SensorStateClass from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError from homeassistant.util.dt import utcnow from .const import ( @@ -780,3 +781,139 @@ async def test_enforce_polling( mock_imap_protocol.wait_server_push.assert_not_called() else: mock_imap_protocol.assert_has_calls([call.wait_server_push]) + + +@pytest.mark.parametrize( + ("imap_search", "imap_fetch"), + [(TEST_SEARCH_RESPONSE, TEST_FETCH_RESPONSE_TEXT_PLAIN)], +) +@pytest.mark.parametrize("imap_has_capability", [True, False], ids=["push", "poll"]) +async def test_services(hass: HomeAssistant, mock_imap_protocol: MagicMock) -> None: + """Test receiving a message successfully.""" + event_called = async_capture_events(hass, "imap_content") + + config = MOCK_CONFIG.copy() + config_entry = MockConfigEntry(domain=DOMAIN, data=config) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + # Make sure we have had one update (when polling) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) + await hass.async_block_till_done() + state = hass.states.get("sensor.imap_email_email_com") + # we should have received one message + assert state is not None + assert state.state == "1" + assert state.attributes["state_class"] == SensorStateClass.MEASUREMENT + + # we should have received one event + assert len(event_called) == 1 + data: dict[str, Any] = event_called[0].data + assert data["server"] == "imap.server.com" + assert data["username"] == "email@email.com" + assert data["search"] == "UnSeen UnDeleted" + assert data["folder"] == "INBOX" + assert data["sender"] == "john.doe@example.com" + assert data["subject"] == "Test subject" + assert data["uid"] == "1" + assert data["entry_id"] == config_entry.entry_id + + # Test seen service + data = {"entry": config_entry.entry_id, "uid": "1"} + await hass.services.async_call(DOMAIN, "seen", data, blocking=True) + mock_imap_protocol.store.assert_called_with("1", "+FLAGS (\\Seen)") + mock_imap_protocol.store.reset_mock() + + # Test move service + data = { + "entry": config_entry.entry_id, + "uid": "1", + "seen": True, + "target_folder": "Trash", + } + await hass.services.async_call(DOMAIN, "move", data, blocking=True) + mock_imap_protocol.store.assert_has_calls( + [call("1", "+FLAGS (\\Seen)"), call("1", "+FLAGS (\\Deleted)")] + ) + mock_imap_protocol.copy.assert_called_with("1", "Trash") + mock_imap_protocol.protocol.expunge.assert_called_once() + mock_imap_protocol.store.reset_mock() + mock_imap_protocol.copy.reset_mock() + mock_imap_protocol.protocol.expunge.reset_mock() + + # Test delete service + data = {"entry": config_entry.entry_id, "uid": "1"} + await hass.services.async_call(DOMAIN, "delete", data, blocking=True) + mock_imap_protocol.store.assert_called_with("1", "+FLAGS (\\Deleted)") + mock_imap_protocol.protocol.expunge.assert_called_once() + + # Test with invalid entry_id + data = {"entry": "invalid", "uid": "1"} + with pytest.raises(ServiceValidationError) as exc: + await hass.services.async_call(DOMAIN, "seen", data, blocking=True) + assert exc.value.translation_domain == DOMAIN + assert exc.value.translation_key == "invalid_entry" + + # Test processing imap client failures + exceptions = { + "invalid_auth": {"exc": InvalidAuth(), "translation_placeholders": None}, + "invalid_folder": {"exc": InvalidFolder(), "translation_placeholders": None}, + "imap_server_fail": { + "exc": AioImapException("Bla"), + "translation_placeholders": {"error": "Bla"}, + }, + } + for translation_key, attrs in exceptions.items(): + with patch( + "homeassistant.components.imap.connect_to_server", side_effect=attrs["exc"] + ): + data = {"entry": config_entry.entry_id, "uid": "1"} + with pytest.raises(ServiceValidationError) as exc: + await hass.services.async_call(DOMAIN, "seen", data, blocking=True) + assert exc.value.translation_domain == DOMAIN + assert exc.value.translation_key == translation_key + assert ( + exc.value.translation_placeholders == attrs["translation_placeholders"] + ) + + # Test unexpected errors with storing a flag during a service call + service_calls = { + "seen": {"entry": config_entry.entry_id, "uid": "1"}, + "move": { + "entry": config_entry.entry_id, + "uid": "1", + "seen": False, + "target_folder": "Trash", + }, + "delete": {"entry": config_entry.entry_id, "uid": "1"}, + } + store_error_translation_key = { + "seen": "seen_failed", + "move": "copy_failed", + "delete": "delete_failed", + } + for service, data in service_calls.items(): + with ( + pytest.raises(ServiceValidationError) as exc, + patch.object( + mock_imap_protocol, "store", side_effect=AioImapException("Bla") + ), + ): + await hass.services.async_call(DOMAIN, service, data, blocking=True) + assert exc.value.translation_domain == DOMAIN + assert exc.value.translation_key == "imap_server_fail" + assert exc.value.translation_placeholders == {"error": "Bla"} + # Test with bad responses on store command + with ( + pytest.raises(ServiceValidationError) as exc, + patch.object( + mock_imap_protocol, "store", return_value=Response("BAD", [b"Bla"]) + ), + patch.object( + mock_imap_protocol, "copy", return_value=Response("BAD", [b"Bla"]) + ), + ): + await hass.services.async_call(DOMAIN, service, data, blocking=True) + assert exc.value.translation_domain == DOMAIN + assert exc.value.translation_key == store_error_translation_key[service] + assert exc.value.translation_placeholders == {"error": "Bla"} From 2ef0521d3d605572a88476335962fd60d4368ebc Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 2 Apr 2024 23:09:56 +0200 Subject: [PATCH 188/967] Use is in enum comparison in config flow tests U-Z (#114677) * Use right enum expression U-Z * Fix --- .../ukraine_alarm/test_config_flow.py | 44 +++--- tests/components/unifi/test_config_flow.py | 47 +++--- .../unifiprotect/test_config_flow.py | 58 +++---- tests/components/upcloud/test_config_flow.py | 12 +- tests/components/upnp/test_config_flow.py | 43 +++--- tests/components/uptime/test_config_flow.py | 6 +- .../uptimerobot/test_config_flow.py | 30 ++-- .../utility_meter/test_config_flow.py | 36 ++--- tests/components/v2c/test_config_flow.py | 8 +- tests/components/vallox/test_config_flow.py | 16 +- tests/components/velbus/test_config_flow.py | 22 +-- tests/components/velux/test_config_flow.py | 12 +- tests/components/venstar/test_config_flow.py | 12 +- tests/components/vera/test_config_flow.py | 14 +- tests/components/verisure/test_config_flow.py | 58 +++---- tests/components/version/test_config_flow.py | 28 ++-- tests/components/vesync/test_config_flow.py | 10 +- tests/components/vicare/test_config_flow.py | 22 +-- tests/components/vilfo/test_config_flow.py | 17 +- tests/components/vizio/test_config_flow.py | 120 +++++++------- .../components/vlc_telnet/test_config_flow.py | 22 +-- .../vodafone_station/test_config_flow.py | 23 ++- tests/components/voip/test_config_flow.py | 10 +- .../volvooncall/test_config_flow.py | 18 +-- tests/components/vulcan/test_config_flow.py | 113 +++++++------- tests/components/wallbox/test_config_flow.py | 5 +- tests/components/waqi/test_config_flow.py | 20 +-- tests/components/watttime/test_config_flow.py | 22 +-- .../waze_travel_time/test_config_flow.py | 23 +-- .../weatherflow/test_config_flow.py | 8 +- .../weatherflow_cloud/test_config_flow.py | 18 +-- .../components/weatherkit/test_config_flow.py | 14 +- tests/components/webmin/test_config_flow.py | 12 +- tests/components/webostv/test_config_flow.py | 44 +++--- tests/components/wemo/test_config_flow.py | 8 +- .../components/whirlpool/test_config_flow.py | 16 +- tests/components/whois/test_config_flow.py | 12 +- tests/components/wiffi/test_config_flow.py | 14 +- tests/components/wilight/test_config_flow.py | 16 +- tests/components/withings/test_config_flow.py | 16 +- tests/components/wiz/test_config_flow.py | 14 +- tests/components/wled/test_config_flow.py | 28 ++-- tests/components/wolflink/test_config_flow.py | 11 +- tests/components/workday/test_config_flow.py | 32 ++-- tests/components/ws66i/test_config_flow.py | 7 +- tests/components/wyoming/test_config_flow.py | 30 ++-- tests/components/xbox/test_config_flow.py | 5 +- .../components/xiaomi_ble/test_config_flow.py | 146 +++++++++--------- .../xiaomi_miio/test_config_flow.py | 11 +- .../yale_smart_alarm/test_config_flow.py | 20 +-- .../components/yalexs_ble/test_config_flow.py | 84 +++++----- .../yamaha_musiccast/test_config_flow.py | 33 ++-- tests/components/yardian/test_config_flow.py | 16 +- tests/components/yeelight/test_config_flow.py | 30 ++-- tests/components/yolink/test_config_flow.py | 13 +- tests/components/youtube/test_config_flow.py | 14 +- tests/components/zamg/test_config_flow.py | 18 +-- .../components/zeversolar/test_config_flow.py | 10 +- tests/components/zha/test_config_flow.py | 116 +++++++------- tests/components/zodiac/test_config_flow.py | 6 +- tests/components/zwave_me/test_config_flow.py | 18 +-- 61 files changed, 861 insertions(+), 850 deletions(-) diff --git a/tests/components/ukraine_alarm/test_config_flow.py b/tests/components/ukraine_alarm/test_config_flow.py index 6d9ce7b7f72..da4f242ea7a 100644 --- a/tests/components/ukraine_alarm/test_config_flow.py +++ b/tests/components/ukraine_alarm/test_config_flow.py @@ -56,10 +56,10 @@ async def test_state(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM with patch( "homeassistant.components.ukraine_alarm.async_setup_entry", @@ -73,7 +73,7 @@ async def test_state(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "State 1" assert result3["data"] == { "region": "1", @@ -87,10 +87,10 @@ async def test_state_district(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM result3 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -98,7 +98,7 @@ async def test_state_district(hass: HomeAssistant) -> None: "region": "2", }, ) - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM with patch( "homeassistant.components.ukraine_alarm.async_setup_entry", @@ -112,7 +112,7 @@ async def test_state_district(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result4["type"] == FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == "District 2.2" assert result4["data"] == { "region": "2.2", @@ -126,10 +126,10 @@ async def test_state_district_pick_region(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM result3 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -137,7 +137,7 @@ async def test_state_district_pick_region(hass: HomeAssistant) -> None: "region": "2", }, ) - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM with patch( "homeassistant.components.ukraine_alarm.async_setup_entry", @@ -151,7 +151,7 @@ async def test_state_district_pick_region(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result4["type"] == FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == "State 2" assert result4["data"] == { "region": "2", @@ -165,12 +165,12 @@ async def test_state_district_community(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM result3 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -178,7 +178,7 @@ async def test_state_district_community(hass: HomeAssistant) -> None: "region": "3", }, ) - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM result4 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -186,7 +186,7 @@ async def test_state_district_community(hass: HomeAssistant) -> None: "region": "3.2", }, ) - assert result4["type"] == FlowResultType.FORM + assert result4["type"] is FlowResultType.FORM with patch( "homeassistant.components.ukraine_alarm.async_setup_entry", @@ -200,7 +200,7 @@ async def test_state_district_community(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result5["type"] == FlowResultType.CREATE_ENTRY + assert result5["type"] is FlowResultType.CREATE_ENTRY assert result5["title"] == "Community 3.2.1" assert result5["data"] == { "region": "3.2.1", @@ -231,7 +231,7 @@ async def test_rate_limit(hass: HomeAssistant, mock_get_regions: AsyncMock) -> N result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "rate_limit" @@ -243,7 +243,7 @@ async def test_server_error(hass: HomeAssistant, mock_get_regions) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -253,7 +253,7 @@ async def test_cannot_connect(hass: HomeAssistant, mock_get_regions: AsyncMock) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -265,7 +265,7 @@ async def test_unknown_client_error( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -275,7 +275,7 @@ async def test_timeout_error(hass: HomeAssistant, mock_get_regions: AsyncMock) - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "timeout" @@ -287,5 +287,5 @@ async def test_no_regions_returned( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index ee309ca2579..2f9bf68f086 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import patch import aiounifi -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.components.unifi.config_flow import _async_discover_unifi from homeassistant.components.unifi.const import ( @@ -33,6 +33,7 @@ from homeassistant.const import ( CONTENT_TYPE_JSON, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .test_hub import setup_unifi_integration @@ -105,7 +106,7 @@ async def test_flow_works( UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["data_schema"]({CONF_USERNAME: "", CONF_PASSWORD: ""}) == { CONF_HOST: "unifi", @@ -145,7 +146,7 @@ async def test_flow_works( }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Site name" assert result["data"] == { CONF_HOST: "1.2.3.4", @@ -165,7 +166,7 @@ async def test_flow_works_negative_discovery( UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["data_schema"]({CONF_USERNAME: "", CONF_PASSWORD: ""}) == { CONF_HOST: "", @@ -184,7 +185,7 @@ async def test_flow_multiple_sites( UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" aioclient_mock.get("https://1.2.3.4:1234", status=302) @@ -218,7 +219,7 @@ async def test_flow_multiple_sites( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "site" assert result["data_schema"]({"site": "1"}) assert result["data_schema"]({"site": "2"}) @@ -234,7 +235,7 @@ async def test_flow_raise_already_configured( UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" aioclient_mock.clear_requests() @@ -269,7 +270,7 @@ async def test_flow_raise_already_configured( }, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -291,7 +292,7 @@ async def test_flow_aborts_configuration_updated( UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" aioclient_mock.get("https://1.2.3.4:1234", status=302) @@ -325,7 +326,7 @@ async def test_flow_aborts_configuration_updated( }, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "configuration_updated" @@ -337,7 +338,7 @@ async def test_flow_fails_user_credentials_faulty( UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" aioclient_mock.get("https://1.2.3.4:1234", status=302) @@ -354,7 +355,7 @@ async def test_flow_fails_user_credentials_faulty( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "faulty_credentials"} @@ -366,7 +367,7 @@ async def test_flow_fails_hub_unavailable( UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" aioclient_mock.get("https://1.2.3.4:1234", status=302) @@ -383,7 +384,7 @@ async def test_flow_fails_hub_unavailable( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "service_unavailable"} @@ -405,7 +406,7 @@ async def test_reauth_flow_update_configuration( data=config_entry.data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" aioclient_mock.clear_requests() @@ -440,7 +441,7 @@ async def test_reauth_flow_update_configuration( }, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert config_entry.data[CONF_HOST] == "1.2.3.4" assert config_entry.data[CONF_USERNAME] == "new_name" @@ -465,7 +466,7 @@ async def test_advanced_option_flow( config_entry.entry_id, context={"show_advanced_options": True} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure_entity_sources" assert not result["last_step"] assert list(result["data_schema"].schema[CONF_CLIENT_SOURCE].options.keys()) == [ @@ -476,7 +477,7 @@ async def test_advanced_option_flow( user_input={CONF_CLIENT_SOURCE: ["00:00:00:00:00:01"]}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "device_tracker" assert not result["last_step"] assert list(result["data_schema"].schema[CONF_SSID_FILTER].options.keys()) == [ @@ -498,7 +499,7 @@ async def test_advanced_option_flow( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "client_control" assert not result["last_step"] @@ -510,7 +511,7 @@ async def test_advanced_option_flow( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "statistics_sensors" assert result["last_step"] @@ -522,7 +523,7 @@ async def test_advanced_option_flow( }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_CLIENT_SOURCE: ["00:00:00:00:00:01"], CONF_TRACK_CLIENTS: False, @@ -550,7 +551,7 @@ async def test_simple_option_flow( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "simple_options" assert result["last_step"] @@ -563,7 +564,7 @@ async def test_simple_option_flow( }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_TRACK_CLIENTS: False, CONF_TRACK_DEVICES: False, diff --git a/tests/components/unifiprotect/test_config_flow.py b/tests/components/unifiprotect/test_config_flow.py index 7c9f584af15..58228a08d0d 100644 --- a/tests/components/unifiprotect/test_config_flow.py +++ b/tests/components/unifiprotect/test_config_flow.py @@ -63,7 +63,7 @@ async def test_form(hass: HomeAssistant, bootstrap: Bootstrap, nvr: NVR) -> None result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] bootstrap.nvr = nvr @@ -91,7 +91,7 @@ async def test_form(hass: HomeAssistant, bootstrap: Bootstrap, nvr: NVR) -> None ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "UnifiProtect" assert result2["data"] == { "host": "1.1.1.1", @@ -127,7 +127,7 @@ async def test_form_version_too_old( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "protect_version"} @@ -150,7 +150,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"password": "invalid_auth"} @@ -178,7 +178,7 @@ async def test_form_cloud_user( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cloud_user"} @@ -201,7 +201,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -230,7 +230,7 @@ async def test_form_reauth_auth( "entry_id": mock_config.entry_id, }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN) assert flows[0]["context"]["title_placeholders"] == { @@ -250,7 +250,7 @@ async def test_form_reauth_auth( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"password": "invalid_auth"} assert result2["step_id"] == "reauth_confirm" @@ -274,7 +274,7 @@ async def test_form_reauth_auth( ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" assert len(mock_setup.mock_calls) == 1 @@ -310,7 +310,7 @@ async def test_form_options(hass: HomeAssistant, ufp_client: ProtectApiClient) - assert mock_config.state == config_entries.ConfigEntryState.LOADED result = await hass.config_entries.options.async_init(mock_config.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] assert result["step_id"] == "init" @@ -323,7 +323,7 @@ async def test_form_options(hass: HomeAssistant, ufp_client: ProtectApiClient) - }, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { "all_updates": True, "disable_rtsp": True, @@ -354,7 +354,7 @@ async def test_discovered_by_ssdp_or_dhcp( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "discovery_started" @@ -371,7 +371,7 @@ async def test_discovered_by_unifi_discovery_direct_connect( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN) assert flows[0]["context"]["title_placeholders"] == { @@ -405,7 +405,7 @@ async def test_discovered_by_unifi_discovery_direct_connect( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "UnifiProtect" assert result2["data"] == { "host": DIRECT_CONNECT_DOMAIN, @@ -446,7 +446,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_updated( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_config.data[CONF_HOST] == DIRECT_CONNECT_DOMAIN @@ -484,7 +484,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_updated_but_not_usin ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_config.data[CONF_HOST] == "127.0.0.1" @@ -522,7 +522,7 @@ async def test_discovered_by_unifi_discovery_does_not_update_ip_when_console_is_ ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_config.data[CONF_HOST] == "1.2.2.2" @@ -553,7 +553,7 @@ async def test_discovered_host_not_updated_if_existing_is_a_hostname( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_config.data[CONF_HOST] == "a.hostname" @@ -571,7 +571,7 @@ async def test_discovered_by_unifi_discovery( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN) assert flows[0]["context"]["title_placeholders"] == { @@ -605,7 +605,7 @@ async def test_discovered_by_unifi_discovery( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "UnifiProtect" assert result2["data"] == { "host": DEVICE_IP_ADDRESS, @@ -632,7 +632,7 @@ async def test_discovered_by_unifi_discovery_partial( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN) assert flows[0]["context"]["title_placeholders"] == { @@ -666,7 +666,7 @@ async def test_discovered_by_unifi_discovery_partial( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "UnifiProtect" assert result2["data"] == { "host": DEVICE_IP_ADDRESS, @@ -706,7 +706,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -736,7 +736,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -777,7 +777,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -814,7 +814,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN) assert flows[0]["context"]["title_placeholders"] == { @@ -848,7 +848,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "UnifiProtect" assert result2["data"] == { "host": "nomatchsameip.ui.direct", @@ -892,7 +892,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -913,5 +913,5 @@ async def test_discovery_can_be_ignored(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/upcloud/test_config_flow.py b/tests/components/upcloud/test_config_flow.py index 86f73463fab..4ce87bf38ab 100644 --- a/tests/components/upcloud/test_config_flow.py +++ b/tests/components/upcloud/test_config_flow.py @@ -31,7 +31,7 @@ async def test_show_set_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -44,7 +44,7 @@ async def test_connection_error( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=FIXTURE_USER_INPUT ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -65,7 +65,7 @@ async def test_login_error( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=FIXTURE_USER_INPUT ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -80,7 +80,7 @@ async def test_success( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=FIXTURE_USER_INPUT ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME] assert result["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] @@ -98,7 +98,7 @@ async def test_options(hass: HomeAssistant) -> None: 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["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -129,7 +129,7 @@ async def test_already_configured(hass: HomeAssistant, requests_mock) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=new_user_input ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_USERNAME] == new_user_input[CONF_USERNAME] assert config_entry.data[CONF_PASSWORD] == new_user_input[CONF_PASSWORD] diff --git a/tests/components/upnp/test_config_flow.py b/tests/components/upnp/test_config_flow.py index 67b4e5b10e6..a3d2b97f3ed 100644 --- a/tests/components/upnp/test_config_flow.py +++ b/tests/components/upnp/test_config_flow.py @@ -6,7 +6,7 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.components.upnp.const import ( CONFIG_ENTRY_HOST, @@ -19,6 +19,7 @@ from homeassistant.components.upnp.const import ( ST_IGD_V1, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .conftest import ( TEST_DISCOVERY, @@ -48,7 +49,7 @@ async def test_flow_ssdp(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=TEST_DISCOVERY, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "ssdp_confirm" # Confirm via step ssdp_confirm. @@ -56,7 +57,7 @@ async def test_flow_ssdp(hass: HomeAssistant) -> None: result["flow_id"], user_input={}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_FRIENDLY_NAME assert result["data"] == { CONFIG_ENTRY_ST: TEST_ST, @@ -82,7 +83,7 @@ async def test_flow_ssdp_ignore(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=TEST_DISCOVERY, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "ssdp_confirm" # Ignore entry. @@ -91,7 +92,7 @@ async def test_flow_ssdp_ignore(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_IGNORE}, data={"unique_id": TEST_USN, "title": TEST_FRIENDLY_NAME}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_FRIENDLY_NAME assert result["data"] == { CONFIG_ENTRY_ST: TEST_ST, @@ -121,7 +122,7 @@ async def test_flow_ssdp_incomplete_discovery(hass: HomeAssistant) -> None: }, ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "incomplete_discovery" @@ -143,7 +144,7 @@ async def test_flow_ssdp_non_igd_device(hass: HomeAssistant) -> None: }, ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "non_igd_device" @@ -161,7 +162,7 @@ async def test_flow_ssdp_no_mac_address(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=TEST_DISCOVERY, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "ssdp_confirm" # Confirm via step ssdp_confirm. @@ -169,7 +170,7 @@ async def test_flow_ssdp_no_mac_address(hass: HomeAssistant) -> None: result["flow_id"], user_input={}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_FRIENDLY_NAME assert result["data"] == { CONFIG_ENTRY_ST: TEST_ST, @@ -209,7 +210,7 @@ async def test_flow_ssdp_discovery_changed_udn_match_mac(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_SSDP}, data=new_discovery, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "config_entry_updated" @@ -241,7 +242,7 @@ async def test_flow_ssdp_discovery_changed_udn_match_host(hass: HomeAssistant) - context={"source": config_entries.SOURCE_SSDP}, data=new_discovery, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "config_entry_updated" @@ -283,7 +284,7 @@ async def test_flow_ssdp_discovery_changed_udn_but_st_differs( context={"source": config_entries.SOURCE_SSDP}, data=new_discovery, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "ssdp_confirm" # UDN + ST different: New discovery via step ssdp. @@ -301,7 +302,7 @@ async def test_flow_ssdp_discovery_changed_udn_but_st_differs( context={"source": config_entries.SOURCE_SSDP}, data=new_discovery, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "ssdp_confirm" @@ -333,7 +334,7 @@ async def test_flow_ssdp_discovery_changed_location(hass: HomeAssistant) -> None context={"source": config_entries.SOURCE_SSDP}, data=new_discovery, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" # Test if location is updated. @@ -362,7 +363,7 @@ async def test_flow_ssdp_discovery_ignored_entry(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=TEST_DISCOVERY, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -395,7 +396,7 @@ async def test_flow_ssdp_discovery_changed_udn_ignored_entry( context={"source": config_entries.SOURCE_SSDP}, data=new_discovery, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "discovery_ignored" @@ -411,7 +412,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # Confirmed via step user. @@ -419,7 +420,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: result["flow_id"], user_input={"unique_id": TEST_USN}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_FRIENDLY_NAME assert result["data"] == { CONFIG_ENTRY_ST: TEST_ST, @@ -442,7 +443,7 @@ async def test_flow_user_no_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -463,7 +464,7 @@ async def test_flow_ssdp_with_mismatched_udn(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=test_discovery, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "ssdp_confirm" # Confirm via step ssdp_confirm. @@ -471,7 +472,7 @@ async def test_flow_ssdp_with_mismatched_udn(hass: HomeAssistant) -> None: result["flow_id"], user_input={}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_FRIENDLY_NAME assert result["data"] == { CONFIG_ENTRY_ST: TEST_ST, diff --git a/tests/components/uptime/test_config_flow.py b/tests/components/uptime/test_config_flow.py index a2234882b27..2c9c4edacde 100644 --- a/tests/components/uptime/test_config_flow.py +++ b/tests/components/uptime/test_config_flow.py @@ -21,7 +21,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( @@ -29,7 +29,7 @@ async def test_full_user_flow( user_input={}, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2 == snapshot @@ -44,5 +44,5 @@ async def test_single_instance_allowed( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" diff --git a/tests/components/uptimerobot/test_config_flow.py b/tests/components/uptimerobot/test_config_flow.py index 58faa524d6f..1cf0a358a87 100644 --- a/tests/components/uptimerobot/test_config_flow.py +++ b/tests/components/uptimerobot/test_config_flow.py @@ -30,7 +30,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -50,7 +50,7 @@ async def test_form(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result2["result"].unique_id == MOCK_UPTIMEROBOT_UNIQUE_ID - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == MOCK_UPTIMEROBOT_ACCOUNT["email"] assert result2["data"] == {CONF_API_KEY: MOCK_UPTIMEROBOT_API_KEY} assert len(mock_setup_entry.mock_calls) == 1 @@ -62,7 +62,7 @@ async def test_form_read_only(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -75,7 +75,7 @@ async def test_form_read_only(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"]["base"] == "not_main_key" @@ -102,7 +102,7 @@ async def test_form_exception_thrown(hass: HomeAssistant, exception, error_key) {CONF_API_KEY: MOCK_UPTIMEROBOT_API_KEY}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"]["base"] == error_key @@ -137,7 +137,7 @@ async def test_user_unique_id_already_exists( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -157,7 +157,7 @@ async def test_user_unique_id_already_exists( await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 0 - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -178,7 +178,7 @@ async def test_reauthentication( data=old_entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "reauth_confirm" @@ -198,7 +198,7 @@ async def test_reauthentication( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -219,7 +219,7 @@ async def test_reauthentication_failure( data=old_entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "reauth_confirm" @@ -240,7 +240,7 @@ async def test_reauthentication_failure( await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"]["base"] == "unknown" @@ -263,7 +263,7 @@ async def test_reauthentication_failure_no_existing_entry( data=old_entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "reauth_confirm" @@ -283,7 +283,7 @@ async def test_reauthentication_failure_no_existing_entry( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_failed_existing" @@ -304,7 +304,7 @@ async def test_reauthentication_failure_account_not_matching( data=old_entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "reauth_confirm" @@ -328,5 +328,5 @@ async def test_reauthentication_failure_account_not_matching( await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"]["base"] == "reauth_failed_matching_account" diff --git a/tests/components/utility_meter/test_config_flow.py b/tests/components/utility_meter/test_config_flow.py index 1bf1c02385d..b5553b1efe7 100644 --- a/tests/components/utility_meter/test_config_flow.py +++ b/tests/components/utility_meter/test_config_flow.py @@ -21,7 +21,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -40,7 +40,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Electricity meter" assert result["data"] == {} assert result["options"] == { @@ -79,7 +79,7 @@ async def test_tariffs(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -94,7 +94,7 @@ async def test_tariffs(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Electricity meter" assert result["data"] == {} assert result["options"] == { @@ -127,7 +127,7 @@ async def test_tariffs(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -142,7 +142,7 @@ async def test_tariffs(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == "tariffs_not_unique" @@ -153,7 +153,7 @@ async def test_non_periodically_resetting(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -169,7 +169,7 @@ async def test_non_periodically_resetting(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Electricity meter" assert result["data"] == {} assert result["options"] == { @@ -206,7 +206,7 @@ async def test_always_available(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -223,7 +223,7 @@ async def test_always_available(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Electricity meter" assert result["data"] == {} assert result["options"] == { @@ -290,7 +290,7 @@ async def test_options(hass: HomeAssistant) -> None: 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["type"] is FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema assert get_suggested(schema, "source") == input_sensor1_entity_id @@ -304,7 +304,7 @@ async def test_options(hass: HomeAssistant) -> None: "always_available": True, }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "cycle": "monthly", "delta_values": False, @@ -428,7 +428,7 @@ async def test_change_device_source(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init( utility_meter_config_entry.entry_id ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ @@ -436,7 +436,7 @@ async def test_change_device_source(hass: HomeAssistant) -> None: "source": current_entity_source.entity_id, }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() # Confirm that the configuration entry has been removed from the source entity 1 (previous) device registry @@ -458,7 +458,7 @@ async def test_change_device_source(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init( utility_meter_config_entry.entry_id ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ @@ -466,7 +466,7 @@ async def test_change_device_source(hass: HomeAssistant) -> None: "source": current_entity_source.entity_id, }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() # Confirm that the configuration entry has been removed from the source entity 2 (previous) device registry @@ -490,7 +490,7 @@ async def test_change_device_source(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init( utility_meter_config_entry.entry_id ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ @@ -498,7 +498,7 @@ async def test_change_device_source(hass: HomeAssistant) -> None: "source": current_entity_source.entity_id, }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() # Confirm that the configuration entry has been added to the source entity 2 (current) device registry diff --git a/tests/components/v2c/test_config_flow.py b/tests/components/v2c/test_config_flow.py index a0657fa0c7c..04cf66d1d58 100644 --- a/tests/components/v2c/test_config_flow.py +++ b/tests/components/v2c/test_config_flow.py @@ -16,7 +16,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -31,7 +31,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "EVSE 1.1.1.1" assert result2["data"] == { "host": "1.1.1.1", @@ -65,7 +65,7 @@ async def test_form_cannot_connect( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": error} with patch( @@ -80,7 +80,7 @@ async def test_form_cannot_connect( ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "EVSE 1.1.1.1" assert result3["data"] == { "host": "1.1.1.1", diff --git a/tests/components/vallox/test_config_flow.py b/tests/components/vallox/test_config_flow.py index 00c11854fe2..cfeb7152b17 100644 --- a/tests/components/vallox/test_config_flow.py +++ b/tests/components/vallox/test_config_flow.py @@ -20,7 +20,7 @@ async def test_form_no_input(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None @@ -30,7 +30,7 @@ async def test_form_create_entry(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert init["type"] == FlowResultType.FORM + assert init["type"] is FlowResultType.FORM assert init["errors"] is None with ( @@ -49,7 +49,7 @@ async def test_form_create_entry(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Vallox" assert result["data"] == {"host": "1.2.3.4", "name": "Vallox"} assert len(mock_setup_entry.mock_calls) == 1 @@ -67,7 +67,7 @@ async def test_form_invalid_ip(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"host": "invalid_host"} @@ -87,7 +87,7 @@ async def test_form_vallox_api_exception_cannot_connect(hass: HomeAssistant) -> ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"host": "cannot_connect"} @@ -107,7 +107,7 @@ async def test_form_os_error_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"host": "cannot_connect"} @@ -127,7 +127,7 @@ async def test_form_unknown_exception(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"host": "unknown"} @@ -152,5 +152,5 @@ async def test_form_already_configured(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/velbus/test_config_flow.py b/tests/components/velbus/test_config_flow.py index 4e3eeaf0fb8..79d67415c4f 100644 --- a/tests/components/velbus/test_config_flow.py +++ b/tests/components/velbus/test_config_flow.py @@ -7,12 +7,12 @@ import pytest import serial.tools.list_ports from velbusaio.exceptions import VelbusConnectionFailed -from homeassistant import data_entry_flow from homeassistant.components import usb from homeassistant.components.velbus.const import DOMAIN from homeassistant.config_entries import SOURCE_USB, SOURCE_USER from homeassistant.const import CONF_NAME, CONF_PORT, CONF_SOURCE from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .const import PORT_SERIAL, PORT_TCP @@ -73,7 +73,7 @@ async def test_user(hass: HomeAssistant) -> None: ) assert result assert result.get("flow_id") - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" # try with a serial port @@ -83,7 +83,7 @@ async def test_user(hass: HomeAssistant) -> None: data={CONF_NAME: "Velbus Test Serial", CONF_PORT: PORT_SERIAL}, ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == "velbus_test_serial" data = result.get("data") assert data @@ -96,7 +96,7 @@ async def test_user(hass: HomeAssistant) -> None: data={CONF_NAME: "Velbus Test TCP", CONF_PORT: PORT_TCP}, ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == "velbus_test_tcp" data = result.get("data") assert data @@ -112,7 +112,7 @@ async def test_user_fail(hass: HomeAssistant) -> None: data={CONF_NAME: "Velbus Test Serial", CONF_PORT: PORT_SERIAL}, ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {CONF_PORT: "cannot_connect"} result = await hass.config_entries.flow.async_init( @@ -121,7 +121,7 @@ async def test_user_fail(hass: HomeAssistant) -> None: data={CONF_NAME: "Velbus Test TCP", CONF_PORT: PORT_TCP}, ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {CONF_PORT: "cannot_connect"} @@ -134,7 +134,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant) -> None: data={CONF_PORT: PORT_TCP, CONF_NAME: "velbus test"}, ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -148,7 +148,7 @@ async def test_flow_usb(hass: HomeAssistant) -> None: data=DISCOVERY_INFO, ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "discovery_confirm" result = await hass.config_entries.flow.async_configure( @@ -156,7 +156,7 @@ async def test_flow_usb(hass: HomeAssistant) -> None: user_input={}, ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY # test an already configured discovery entry = MockConfigEntry( @@ -170,7 +170,7 @@ async def test_flow_usb(hass: HomeAssistant) -> None: data=DISCOVERY_INFO, ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -184,5 +184,5 @@ async def test_flow_usb_failed(hass: HomeAssistant) -> None: data=DISCOVERY_INFO, ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "cannot_connect" diff --git a/tests/components/velux/test_config_flow.py b/tests/components/velux/test_config_flow.py index 816dbf95420..8021ad52810 100644 --- a/tests/components/velux/test_config_flow.py +++ b/tests/components/velux/test_config_flow.py @@ -9,11 +9,11 @@ from unittest.mock import patch import pytest from pyvlx import PyVLXException -from homeassistant import data_entry_flow from homeassistant.components.velux 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 @@ -45,7 +45,7 @@ async def test_user_success(hass: HomeAssistant) -> None: client_mock.return_value.disconnect.assert_called_once() client_mock.return_value.connect.assert_called_once() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DUMMY_DATA[CONF_HOST] assert result["data"] == DUMMY_DATA @@ -64,7 +64,7 @@ async def test_user_errors( connect_mock.assert_called_once() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": error_name} @@ -77,7 +77,7 @@ async def test_import_valid_config(hass: HomeAssistant) -> None: context={"source": SOURCE_IMPORT}, data=DUMMY_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DUMMY_DATA[CONF_HOST] assert result["data"] == DUMMY_DATA @@ -97,7 +97,7 @@ async def test_flow_duplicate_entry(hass: HomeAssistant, flow_source: str) -> No context={"source": flow_source}, data=DUMMY_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -115,5 +115,5 @@ async def test_import_errors( context={"source": SOURCE_IMPORT}, data=DUMMY_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == error_name diff --git a/tests/components/venstar/test_config_flow.py b/tests/components/venstar/test_config_flow.py index 077f87975f0..b560a75c8a8 100644 --- a/tests/components/venstar/test_config_flow.py +++ b/tests/components/venstar/test_config_flow.py @@ -37,7 +37,7 @@ async def test_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -56,7 +56,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == TEST_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -76,7 +76,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: TEST_DATA, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -95,7 +95,7 @@ async def test_unknown_error(hass: HomeAssistant) -> None: TEST_DATA, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -107,7 +107,7 @@ async def test_already_configured(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -126,5 +126,5 @@ async def test_already_configured(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/vera/test_config_flow.py b/tests/components/vera/test_config_flow.py index 2262347450d..e5d60aa3e23 100644 --- a/tests/components/vera/test_config_flow.py +++ b/tests/components/vera/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock, patch from requests.exceptions import RequestException -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.vera import CONF_CONTROLLER, CONF_LEGACY_UNIQUE_ID, DOMAIN from homeassistant.const import CONF_EXCLUDE, CONF_LIGHTS, CONF_SOURCE from homeassistant.core import HomeAssistant @@ -25,7 +25,7 @@ async def test_async_step_user_success(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER result = await hass.config_entries.flow.async_configure( @@ -36,7 +36,7 @@ async def test_async_step_user_success(hass: HomeAssistant) -> None: CONF_EXCLUDE: "14 15", }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "http://127.0.0.1:123" assert result["data"] == { CONF_CONTROLLER: "http://127.0.0.1:123", @@ -65,7 +65,7 @@ async def test_async_step_import_success(hass: HomeAssistant) -> None: data={CONF_CONTROLLER: "http://127.0.0.1:123/"}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "http://127.0.0.1:123" assert result["data"] == { CONF_CONTROLLER: "http://127.0.0.1:123", @@ -95,7 +95,7 @@ async def test_async_step_import_success_with_legacy_unique_id( data={CONF_CONTROLLER: "http://127.0.0.1:123/"}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "http://127.0.0.1:123" assert result["data"] == { CONF_CONTROLLER: "http://127.0.0.1:123", @@ -139,7 +139,7 @@ async def test_options(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init( entry.entry_id, context={"source": "test"}, data=None ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -149,7 +149,7 @@ async def test_options(hass: HomeAssistant) -> None: CONF_EXCLUDE: "8,9;10 11 12_13bb14", }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_LIGHTS: [1, 2, 3, 4, 5, 6, 7], CONF_EXCLUDE: [8, 9, 10, 11, 12, 13, 14], diff --git a/tests/components/verisure/test_config_flow.py b/tests/components/verisure/test_config_flow.py index 62ae00b5622..cf478b093c0 100644 --- a/tests/components/verisure/test_config_flow.py +++ b/tests/components/verisure/test_config_flow.py @@ -32,7 +32,7 @@ async def test_full_user_flow_single_installation( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result.get("step_id") == "user" - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {} mock_verisure_config_flow.get_installations.return_value = { @@ -49,7 +49,7 @@ async def test_full_user_flow_single_installation( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "ascending (12345th street)" assert result2.get("data") == { CONF_GIID: "12345", @@ -71,7 +71,7 @@ async def test_full_user_flow_multiple_installations( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result.get("step_id") == "user" - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {} result2 = await hass.config_entries.flow.async_configure( @@ -84,7 +84,7 @@ async def test_full_user_flow_multiple_installations( await hass.async_block_till_done() assert result2.get("step_id") == "installation" - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("errors") is None result3 = await hass.config_entries.flow.async_configure( @@ -92,7 +92,7 @@ async def test_full_user_flow_multiple_installations( ) await hass.async_block_till_done() - assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY assert result3.get("title") == "descending (54321th street)" assert result3.get("data") == { CONF_GIID: "54321", @@ -114,7 +114,7 @@ async def test_full_user_flow_single_installation_with_mfa( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result.get("step_id") == "user" - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {} mock_verisure_config_flow.login.side_effect = VerisureLoginError( @@ -130,7 +130,7 @@ async def test_full_user_flow_single_installation_with_mfa( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "mfa" mock_verisure_config_flow.login.side_effect = None @@ -147,7 +147,7 @@ async def test_full_user_flow_single_installation_with_mfa( ) await hass.async_block_till_done() - assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY assert result3.get("title") == "ascending (12345th street)" assert result3.get("data") == { CONF_GIID: "12345", @@ -171,7 +171,7 @@ async def test_full_user_flow_multiple_installations_with_mfa( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result.get("step_id") == "user" - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {} mock_verisure_config_flow.login.side_effect = VerisureLoginError( @@ -187,7 +187,7 @@ async def test_full_user_flow_multiple_installations_with_mfa( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "mfa" mock_verisure_config_flow.login.side_effect = None @@ -201,7 +201,7 @@ async def test_full_user_flow_multiple_installations_with_mfa( await hass.async_block_till_done() assert result3.get("step_id") == "installation" - assert result3.get("type") == FlowResultType.FORM + assert result3.get("type") is FlowResultType.FORM assert result3.get("errors") is None result4 = await hass.config_entries.flow.async_configure( @@ -209,7 +209,7 @@ async def test_full_user_flow_multiple_installations_with_mfa( ) await hass.async_block_till_done() - assert result4.get("type") == FlowResultType.CREATE_ENTRY + assert result4.get("type") is FlowResultType.CREATE_ENTRY assert result4.get("title") == "descending (54321th street)" assert result4.get("data") == { CONF_GIID: "54321", @@ -252,7 +252,7 @@ async def test_verisure_errors( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "user" assert result2.get("errors") == {"base": error} @@ -272,7 +272,7 @@ async def test_verisure_errors( mock_verisure_config_flow.request_mfa.side_effect = None - assert result3.get("type") == FlowResultType.FORM + assert result3.get("type") is FlowResultType.FORM assert result3.get("step_id") == "user" assert result3.get("errors") == {"base": "unknown_mfa"} @@ -285,7 +285,7 @@ async def test_verisure_errors( ) await hass.async_block_till_done() - assert result4.get("type") == FlowResultType.FORM + assert result4.get("type") is FlowResultType.FORM assert result4.get("step_id") == "mfa" mock_verisure_config_flow.validate_mfa.side_effect = side_effect @@ -296,7 +296,7 @@ async def test_verisure_errors( "code": "123456", }, ) - assert result5.get("type") == FlowResultType.FORM + assert result5.get("type") is FlowResultType.FORM assert result5.get("step_id") == "mfa" assert result5.get("errors") == {"base": error} @@ -315,7 +315,7 @@ async def test_verisure_errors( ) await hass.async_block_till_done() - assert result6.get("type") == FlowResultType.CREATE_ENTRY + assert result6.get("type") is FlowResultType.CREATE_ENTRY assert result6.get("title") == "ascending (12345th street)" assert result6.get("data") == { CONF_GIID: "12345", @@ -339,7 +339,7 @@ async def test_dhcp(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_DHCP}, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" @@ -362,7 +362,7 @@ async def test_reauth_flow( data=mock_config_entry.data, ) assert result.get("step_id") == "reauth_confirm" - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {} result2 = await hass.config_entries.flow.async_configure( @@ -374,7 +374,7 @@ async def test_reauth_flow( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "reauth_successful" assert mock_config_entry.data == { CONF_GIID: "12345", @@ -405,7 +405,7 @@ async def test_reauth_flow_with_mfa( data=mock_config_entry.data, ) assert result.get("step_id") == "reauth_confirm" - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {} mock_verisure_config_flow.login.side_effect = VerisureLoginError( @@ -421,7 +421,7 @@ async def test_reauth_flow_with_mfa( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "reauth_mfa" mock_verisure_config_flow.login.side_effect = None @@ -434,7 +434,7 @@ async def test_reauth_flow_with_mfa( ) await hass.async_block_till_done() - assert result3.get("type") == FlowResultType.ABORT + assert result3.get("type") is FlowResultType.ABORT assert result3.get("reason") == "reauth_successful" assert mock_config_entry.data == { CONF_GIID: "12345", @@ -487,7 +487,7 @@ async def test_reauth_flow_errors( await hass.async_block_till_done() assert result2.get("step_id") == "reauth_confirm" - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("errors") == {"base": error} mock_verisure_config_flow.login.side_effect = VerisureLoginError( @@ -504,7 +504,7 @@ async def test_reauth_flow_errors( ) await hass.async_block_till_done() - assert result3.get("type") == FlowResultType.FORM + assert result3.get("type") is FlowResultType.FORM assert result3.get("step_id") == "reauth_confirm" assert result3.get("errors") == {"base": "unknown_mfa"} @@ -519,7 +519,7 @@ async def test_reauth_flow_errors( ) await hass.async_block_till_done() - assert result4.get("type") == FlowResultType.FORM + assert result4.get("type") is FlowResultType.FORM assert result4.get("step_id") == "reauth_mfa" mock_verisure_config_flow.validate_mfa.side_effect = side_effect @@ -530,7 +530,7 @@ async def test_reauth_flow_errors( "code": "123456", }, ) - assert result5.get("type") == FlowResultType.FORM + assert result5.get("type") is FlowResultType.FORM assert result5.get("step_id") == "reauth_mfa" assert result5.get("errors") == {"base": error} @@ -575,7 +575,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "init" result = await hass.config_entries.options.async_configure( @@ -583,5 +583,5 @@ async def test_options_flow(hass: HomeAssistant) -> None: user_input={CONF_LOCK_CODE_DIGITS: 4}, ) - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("data") == {CONF_LOCK_CODE_DIGITS: DEFAULT_LOCK_CODE_DIGITS} diff --git a/tests/components/version/test_config_flow.py b/tests/components/version/test_config_flow.py index d7edb5526d5..1779b24b45d 100644 --- a/tests/components/version/test_config_flow.py +++ b/tests/components/version/test_config_flow.py @@ -53,7 +53,7 @@ async def test_basic_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER, "show_advanced_options": False}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.version.async_setup_entry", @@ -65,7 +65,7 @@ async def test_basic_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == VERSION_SOURCE_DOCKER_HUB assert result2["data"] == { **DEFAULT_CONFIGURATION, @@ -81,7 +81,7 @@ async def test_advanced_form_pypi(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER, "show_advanced_options": True}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -89,11 +89,11 @@ async def test_advanced_form_pypi(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "version_source" result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "version_source" with patch( @@ -105,7 +105,7 @@ async def test_advanced_form_pypi(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == VERSION_SOURCE_PYPI assert result["data"] == { **DEFAULT_CONFIGURATION, @@ -122,7 +122,7 @@ async def test_advanced_form_container(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER, "show_advanced_options": True}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -130,11 +130,11 @@ async def test_advanced_form_container(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "version_source" result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "version_source" with patch( @@ -146,7 +146,7 @@ async def test_advanced_form_container(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == VERSION_SOURCE_DOCKER_HUB assert result["data"] == { **DEFAULT_CONFIGURATION, @@ -163,7 +163,7 @@ async def test_advanced_form_supervisor(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER, "show_advanced_options": True}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -171,11 +171,11 @@ async def test_advanced_form_supervisor(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "version_source" result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "version_source" with patch( @@ -188,7 +188,7 @@ async def test_advanced_form_supervisor(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"{VERSION_SOURCE_VERSIONS} Dev" assert result["data"] == { **DEFAULT_CONFIGURATION, diff --git a/tests/components/vesync/test_config_flow.py b/tests/components/vesync/test_config_flow.py index a283b89b841..22a93e1ba56 100644 --- a/tests/components/vesync/test_config_flow.py +++ b/tests/components/vesync/test_config_flow.py @@ -2,10 +2,10 @@ from unittest.mock import patch -from homeassistant import data_entry_flow from homeassistant.components.vesync import DOMAIN, config_flow from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -19,7 +19,7 @@ async def test_abort_already_setup(hass: HomeAssistant) -> None: ) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -31,7 +31,7 @@ async def test_invalid_login_error(hass: HomeAssistant) -> None: with patch("pyvesync.vesync.VeSync.login", return_value=False): result = await flow.async_step_user(user_input=test_dict) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -40,11 +40,11 @@ async def test_config_flow_user_input(hass: HomeAssistant) -> None: flow = config_flow.VeSyncFlowHandler() flow.hass = hass result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch("pyvesync.vesync.VeSync.login", return_value=True): result = await flow.async_step_user( {CONF_USERNAME: "user", CONF_PASSWORD: "pass"} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_USERNAME] == "user" assert result["data"][CONF_PASSWORD] == "pass" diff --git a/tests/components/vicare/test_config_flow.py b/tests/components/vicare/test_config_flow.py index 031fcdff9d3..edef1606572 100644 --- a/tests/components/vicare/test_config_flow.py +++ b/tests/components/vicare/test_config_flow.py @@ -43,7 +43,7 @@ async def test_user_create_entry( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -59,7 +59,7 @@ async def test_user_create_entry( VALID_CONFIG, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -73,7 +73,7 @@ async def test_user_create_entry( VALID_CONFIG, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -88,7 +88,7 @@ async def test_user_create_entry( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "ViCare" assert result["data"] == snapshot mock_setup_entry.assert_called_once() @@ -109,7 +109,7 @@ async def test_step_reauth(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> context={"source": SOURCE_REAUTH, "entry_id": config_entry.entry_id}, data=VALID_CONFIG, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" # test PyViCareInvalidConfigurationError @@ -123,7 +123,7 @@ async def test_step_reauth(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> result["flow_id"], user_input={CONF_PASSWORD: new_password, CONF_CLIENT_ID: new_client_id}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "invalid_auth"} @@ -136,7 +136,7 @@ async def test_step_reauth(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> result["flow_id"], user_input={CONF_PASSWORD: new_password, CONF_CLIENT_ID: new_client_id}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 @@ -159,7 +159,7 @@ async def test_form_dhcp( context={"source": SOURCE_DHCP}, data=DHCP_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -173,7 +173,7 @@ async def test_form_dhcp( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "ViCare" assert result["data"] == snapshot mock_setup_entry.assert_called_once() @@ -192,7 +192,7 @@ async def test_dhcp_single_instance_allowed(hass: HomeAssistant) -> None: context={"source": SOURCE_DHCP}, data=DHCP_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -208,5 +208,5 @@ async def test_user_input_single_instance_allowed(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/vilfo/test_config_flow.py b/tests/components/vilfo/test_config_flow.py index c1755f95043..51c2698e241 100644 --- a/tests/components/vilfo/test_config_flow.py +++ b/tests/components/vilfo/test_config_flow.py @@ -4,11 +4,12 @@ from unittest.mock import Mock, patch import vilfo -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.vilfo import config_flow from homeassistant.components.vilfo.const import DOMAIN from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_ID, CONF_MAC from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType async def test_form(hass: HomeAssistant) -> None: @@ -19,7 +20,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -35,7 +36,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "testadmin.vilfo.com" assert result2["data"] == { "host": "testadmin.vilfo.com", @@ -64,7 +65,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: {"host": "testadmin.vilfo.com", "access_token": "test-token"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -83,7 +84,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: {"host": "testadmin.vilfo.com", "access_token": "test-token"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} with ( @@ -95,7 +96,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: {"host": "testadmin.vilfo.com", "access_token": "test-token"}, ) - assert result3["type"] == data_entry_flow.FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": "cannot_connect"} @@ -148,8 +149,8 @@ async def test_form_already_configured(hass: HomeAssistant) -> None: {CONF_HOST: "testadmin.vilfo.com", CONF_ACCESS_TOKEN: "test-token"}, ) - assert first_flow_result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert second_flow_result2["type"] == data_entry_flow.FlowResultType.ABORT + assert first_flow_result2["type"] is FlowResultType.CREATE_ENTRY + assert second_flow_result2["type"] is FlowResultType.ABORT assert second_flow_result2["reason"] == "already_configured" diff --git a/tests/components/vizio/test_config_flow.py b/tests/components/vizio/test_config_flow.py index d19cf319a5a..712dd2a31b5 100644 --- a/tests/components/vizio/test_config_flow.py +++ b/tests/components/vizio/test_config_flow.py @@ -5,7 +5,6 @@ import dataclasses import pytest import voluptuous as vol -from homeassistant import data_entry_flow from homeassistant.components.media_player import MediaPlayerDeviceClass from homeassistant.components.vizio.config_flow import _get_config_schema from homeassistant.components.vizio.const import ( @@ -32,6 +31,7 @@ from homeassistant.const import ( CONF_PIN, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .const import ( ACCESS_TOKEN, @@ -67,14 +67,14 @@ async def test_user_flow_minimum_fields( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_SPEAKER_CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST @@ -92,14 +92,14 @@ async def test_user_flow_all_fields( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_VALID_TV_CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST @@ -118,19 +118,19 @@ async def test_speaker_options_flow( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_SPEAKER_CONFIG ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY entry = result["result"] result = await hass.config_entries.options.async_init(entry.entry_id, data=None) - assert result["type"] == data_entry_flow.FlowResultType.FORM + 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_VOLUME_STEP: VOLUME_STEP} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"][CONF_VOLUME_STEP] == VOLUME_STEP assert CONF_APPS not in result["data"] @@ -146,12 +146,12 @@ async def test_tv_options_flow_no_apps( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY entry = result["result"] result = await hass.config_entries.options.async_init(entry.entry_id, data=None) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" options = {CONF_VOLUME_STEP: VOLUME_STEP} @@ -161,7 +161,7 @@ async def test_tv_options_flow_no_apps( result["flow_id"], user_input=options ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"][CONF_VOLUME_STEP] == VOLUME_STEP assert CONF_APPS not in result["data"] @@ -177,12 +177,12 @@ async def test_tv_options_flow_with_apps( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY entry = result["result"] result = await hass.config_entries.options.async_init(entry.entry_id, data=None) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" options = {CONF_VOLUME_STEP: VOLUME_STEP} @@ -192,7 +192,7 @@ async def test_tv_options_flow_with_apps( result["flow_id"], user_input=options ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"][CONF_VOLUME_STEP] == VOLUME_STEP assert CONF_APPS in result["data"] @@ -209,13 +209,13 @@ async def test_tv_options_flow_start_with_volume( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY entry = result["result"] result = await hass.config_entries.options.async_init( entry.entry_id, data={CONF_VOLUME_STEP: VOLUME_STEP} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert entry.options assert entry.options == {CONF_VOLUME_STEP: VOLUME_STEP} @@ -224,7 +224,7 @@ async def test_tv_options_flow_start_with_volume( result = await hass.config_entries.options.async_init(entry.entry_id, data=None) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" options = {CONF_VOLUME_STEP: VOLUME_STEP} @@ -234,7 +234,7 @@ async def test_tv_options_flow_start_with_volume( result["flow_id"], user_input=options ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"][CONF_VOLUME_STEP] == VOLUME_STEP assert CONF_APPS in result["data"] @@ -261,7 +261,7 @@ async def test_user_host_already_configured( DOMAIN, context={"source": SOURCE_USER}, data=fail_entry ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_HOST: "existing_config_entry_found"} @@ -285,7 +285,7 @@ async def test_user_serial_number_already_exists( DOMAIN, context={"source": SOURCE_USER}, data=fail_entry ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_HOST: "existing_config_entry_found"} @@ -297,7 +297,7 @@ async def test_user_error_on_could_not_connect( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_HOST: "cannot_connect"} @@ -309,7 +309,7 @@ async def test_user_error_on_could_not_connect_invalid_token( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -324,19 +324,19 @@ async def test_user_tv_pairing_no_apps( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_TV_CONFIG_NO_TOKEN ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair_tv" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_PIN_CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing_complete" result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST @@ -355,7 +355,7 @@ async def test_user_start_pairing_failure( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_TV_CONFIG_NO_TOKEN ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -371,14 +371,14 @@ async def test_user_invalid_pin( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_TV_CONFIG_NO_TOKEN ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair_tv" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_PIN_CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair_tv" assert result["errors"] == {CONF_PIN: "complete_pairing_failed"} @@ -400,7 +400,7 @@ async def test_user_ignore( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_SPEAKER_CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_import_flow_minimum_fields( @@ -417,7 +417,7 @@ async def test_import_flow_minimum_fields( ), ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"][CONF_NAME] == DEFAULT_NAME assert result["data"][CONF_HOST] == HOST @@ -437,7 +437,7 @@ async def test_import_flow_all_fields( data=vol.Schema(VIZIO_SCHEMA)(MOCK_IMPORT_VALID_TV_CONFIG), ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST @@ -464,7 +464,7 @@ async def test_import_entity_already_configured( DOMAIN, context={"source": SOURCE_IMPORT}, data=fail_entry ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured_device" @@ -482,7 +482,7 @@ async def test_import_flow_update_options( await hass.async_block_till_done() assert result["result"].options == {CONF_VOLUME_STEP: DEFAULT_VOLUME_STEP} - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY entry_id = result["result"].entry_id updated_config = MOCK_SPEAKER_CONFIG.copy() @@ -493,7 +493,7 @@ async def test_import_flow_update_options( data=vol.Schema(VIZIO_SCHEMA)(updated_config), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "updated_entry" config_entry = hass.config_entries.async_get_entry(entry_id) assert config_entry.options[CONF_VOLUME_STEP] == VOLUME_STEP + 1 @@ -513,7 +513,7 @@ async def test_import_flow_update_name_and_apps( await hass.async_block_till_done() assert result["result"].data[CONF_NAME] == NAME - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY entry_id = result["result"].entry_id updated_config = MOCK_IMPORT_VALID_TV_CONFIG.copy() @@ -525,7 +525,7 @@ async def test_import_flow_update_name_and_apps( data=vol.Schema(VIZIO_SCHEMA)(updated_config), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "updated_entry" config_entry = hass.config_entries.async_get_entry(entry_id) assert config_entry.data[CONF_NAME] == NAME2 @@ -547,7 +547,7 @@ async def test_import_flow_update_remove_apps( await hass.async_block_till_done() assert result["result"].data[CONF_NAME] == NAME - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY config_entry = hass.config_entries.async_get_entry(result["result"].entry_id) assert CONF_APPS in config_entry.data assert CONF_APPS in config_entry.options @@ -560,7 +560,7 @@ async def test_import_flow_update_remove_apps( data=vol.Schema(VIZIO_SCHEMA)(updated_config), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "updated_entry" assert CONF_APPS not in config_entry.data assert CONF_APPS not in config_entry.options @@ -577,26 +577,26 @@ async def test_import_needs_pairing( DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_TV_CONFIG_NO_TOKEN ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_TV_CONFIG_NO_TOKEN ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair_tv" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_PIN_CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing_complete_import" result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST @@ -617,7 +617,7 @@ async def test_import_with_apps_needs_pairing( DOMAIN, context={"source": SOURCE_IMPORT}, data=import_config ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # Mock inputting info without apps to make sure apps get stored @@ -626,19 +626,19 @@ async def test_import_with_apps_needs_pairing( user_input=_get_config_schema(MOCK_TV_CONFIG_NO_TOKEN)(MOCK_TV_CONFIG_NO_TOKEN), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair_tv" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_PIN_CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing_complete_import" result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST @@ -660,7 +660,7 @@ async def test_import_flow_additional_configs( await hass.async_block_till_done() assert result["result"].data[CONF_NAME] == NAME - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY config_entry = hass.config_entries.async_get_entry(result["result"].entry_id) assert CONF_APPS in config_entry.data assert CONF_APPS not in config_entry.options @@ -689,7 +689,7 @@ async def test_import_error( data=vol.Schema(VIZIO_SCHEMA)(fail_entry), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM # Ensure error gets logged vizio_log_list = [ @@ -720,7 +720,7 @@ async def test_import_ignore( data=vol.Schema(VIZIO_SCHEMA)(MOCK_SPEAKER_CONFIG), ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_zeroconf_flow( @@ -736,7 +736,7 @@ async def test_zeroconf_flow( ) # Form should always show even if all required properties are discovered - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # Apply discovery updates to entry to mimic when user hits submit without changing @@ -753,7 +753,7 @@ async def test_zeroconf_flow( result["flow_id"], user_input=user_input ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_NAME] == NAME @@ -782,7 +782,7 @@ async def test_zeroconf_flow_already_configured( ) # Flow should abort because device is already setup - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -810,7 +810,7 @@ async def test_zeroconf_flow_with_port_in_host( ) # Flow should abort because device is already setup - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -827,7 +827,7 @@ async def test_zeroconf_dupe_fail( ) # Form should always show even if all required properties are discovered - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" discovery_info = dataclasses.replace(MOCK_ZEROCONF_SERVICE_INFO) @@ -836,7 +836,7 @@ async def test_zeroconf_dupe_fail( ) # Flow should abort because device is already setup - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -860,7 +860,7 @@ async def test_zeroconf_ignore( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM async def test_zeroconf_no_unique_id( @@ -875,7 +875,7 @@ async def test_zeroconf_no_unique_id( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -900,7 +900,7 @@ async def test_zeroconf_abort_when_ignored( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -929,7 +929,7 @@ async def test_zeroconf_flow_already_configured_hostname( ) # Flow should abort because device is already setup - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -954,7 +954,7 @@ async def test_import_flow_already_configured_hostname( ) # Flow should abort because device was updated - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "updated_entry" assert entry.data[CONF_HOST] == HOST diff --git a/tests/components/vlc_telnet/test_config_flow.py b/tests/components/vlc_telnet/test_config_flow.py index f5207c52c99..54edafab14a 100644 --- a/tests/components/vlc_telnet/test_config_flow.py +++ b/tests/components/vlc_telnet/test_config_flow.py @@ -51,7 +51,7 @@ async def test_user_flow( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -69,7 +69,7 @@ async def test_user_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == entry_data["host"] assert result["data"] == entry_data assert len(mock_setup_entry.mock_calls) == 1 @@ -94,7 +94,7 @@ async def test_abort_already_configured(hass: HomeAssistant, source: str) -> Non data=entry_data, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -137,7 +137,7 @@ async def test_errors( {"password": "test-password"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": error} @@ -178,7 +178,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 assert dict(entry.data) == {**entry_data, "password": "new-password"} @@ -237,7 +237,7 @@ async def test_reauth_errors( {"password": "test-password"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": error} @@ -272,11 +272,11 @@ async def test_hassio_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == test_data.config["name"] assert result2["data"] == test_data.config assert len(mock_setup_entry.mock_calls) == 1 @@ -303,7 +303,7 @@ async def test_hassio_already_configured(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT @pytest.mark.parametrize( @@ -352,9 +352,9 @@ async def test_hassio_errors( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == error diff --git a/tests/components/vodafone_station/test_config_flow.py b/tests/components/vodafone_station/test_config_flow.py index f2619044861..0492d32070f 100644 --- a/tests/components/vodafone_station/test_config_flow.py +++ b/tests/components/vodafone_station/test_config_flow.py @@ -5,7 +5,6 @@ from unittest.mock import patch from aiovodafone import exceptions as aiovodafone_exceptions import pytest -from homeassistant import data_entry_flow from homeassistant.components.device_tracker import CONF_CONSIDER_HOME from homeassistant.components.vodafone_station.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER @@ -34,13 +33,13 @@ async def test_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_USERNAME] == "fake_username" assert result["data"][CONF_PASSWORD] == "fake_password" @@ -66,7 +65,7 @@ async def test_exception_connection(hass: HomeAssistant, side_effect, error) -> result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" with patch( @@ -77,7 +76,7 @@ async def test_exception_connection(hass: HomeAssistant, side_effect, error) -> result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is not None assert result["errors"]["base"] == error @@ -111,7 +110,7 @@ async def test_exception_connection(hass: HomeAssistant, side_effect, error) -> ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "fake_host" assert result2["data"] == { "host": "fake_host", @@ -143,7 +142,7 @@ async def test_reauth_successful(hass: HomeAssistant) -> None: data=mock_config.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -154,7 +153,7 @@ async def test_reauth_successful(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" @@ -191,7 +190,7 @@ async def test_reauth_not_successful(hass: HomeAssistant, side_effect, error) -> data=mock_config.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -201,7 +200,7 @@ async def test_reauth_not_successful(hass: HomeAssistant, side_effect, error) -> }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] is not None assert result["errors"]["base"] == error @@ -233,7 +232,7 @@ async def test_reauth_not_successful(hass: HomeAssistant, side_effect, error) -> ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -253,7 +252,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_CONSIDER_HOME: 37, } diff --git a/tests/components/voip/test_config_flow.py b/tests/components/voip/test_config_flow.py index 079177db139..ec2a65576e6 100644 --- a/tests/components/voip/test_config_flow.py +++ b/tests/components/voip/test_config_flow.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import voip from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -29,7 +29,7 @@ async def test_form_user(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {} assert len(mock_setup_entry.mock_calls) == 1 @@ -59,7 +59,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init( config_entry.entry_id, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" # Default @@ -67,7 +67,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result["flow_id"], user_input={}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == {"sip_port": 5060} # Manual @@ -78,5 +78,5 @@ async def test_options_flow(hass: HomeAssistant) -> None: result["flow_id"], user_input={"sip_port": 5061}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == {"sip_port": 5061} diff --git a/tests/components/volvooncall/test_config_flow.py b/tests/components/volvooncall/test_config_flow.py index 3c866da58ea..8bf8bcc7412 100644 --- a/tests/components/volvooncall/test_config_flow.py +++ b/tests/components/volvooncall/test_config_flow.py @@ -17,7 +17,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert len(result["errors"]) == 0 with ( @@ -39,7 +39,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "test-username" assert result2["data"] == { "username": "test-username", @@ -74,7 +74,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -86,7 +86,7 @@ async def test_flow_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert len(result["errors"]) == 0 with ( @@ -108,7 +108,7 @@ async def test_flow_already_configured(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -133,7 +133,7 @@ async def test_form_other_exception(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -162,7 +162,7 @@ async def test_reauth(hass: HomeAssistant) -> None: ) # the first form is just the confirmation prompt - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -171,7 +171,7 @@ async def test_reauth(hass: HomeAssistant) -> None: await hass.async_block_till_done() # the second form is the user flow where reauth happens - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM with patch("volvooncall.Connection.get"): result3 = await hass.config_entries.flow.async_configure( @@ -186,5 +186,5 @@ async def test_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" diff --git a/tests/components/vulcan/test_config_flow.py b/tests/components/vulcan/test_config_flow.py index b0b928cfde2..01c6bf3edaf 100644 --- a/tests/components/vulcan/test_config_flow.py +++ b/tests/components/vulcan/test_config_flow.py @@ -14,11 +14,12 @@ from vulcan import ( ) from vulcan.model import Student -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.vulcan import config_flow, const, register from homeassistant.components.vulcan.config_flow import ClientConnectionError, Keystore from homeassistant.const import CONF_PIN, CONF_REGION, CONF_TOKEN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry, load_fixture @@ -38,7 +39,7 @@ async def test_show_form(hass: HomeAssistant) -> None: result = await flow.async_step_user(user_input=None) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" @@ -58,7 +59,7 @@ async def test_config_flow_auth_success( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] is None @@ -72,7 +73,7 @@ async def test_config_flow_auth_success( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Jan Kowalski" assert len(mock_setup_entry.mock_calls) == 1 @@ -97,7 +98,7 @@ async def test_config_flow_auth_success_with_multiple_students( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] is None @@ -106,7 +107,7 @@ async def test_config_flow_auth_success_with_multiple_students( {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_student" assert result["errors"] == {} @@ -119,7 +120,7 @@ async def test_config_flow_auth_success_with_multiple_students( {"student": "0"}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Jan Kowalski" assert len(mock_setup_entry.mock_calls) == 1 @@ -145,7 +146,7 @@ async def test_config_flow_reauth_success( const.DOMAIN, context={"source": config_entries.SOURCE_REAUTH} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -158,7 +159,7 @@ async def test_config_flow_reauth_success( {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 @@ -184,7 +185,7 @@ async def test_config_flow_reauth_without_matching_entries( const.DOMAIN, context={"source": config_entries.SOURCE_REAUTH} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -193,7 +194,7 @@ async def test_config_flow_reauth_without_matching_entries( {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_matching_entries" @@ -208,7 +209,7 @@ async def test_config_flow_reauth_with_errors( result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_REAUTH} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} with patch( @@ -220,7 +221,7 @@ async def test_config_flow_reauth_with_errors( {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "invalid_token"} @@ -233,7 +234,7 @@ async def test_config_flow_reauth_with_errors( {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "expired_token"} @@ -246,7 +247,7 @@ async def test_config_flow_reauth_with_errors( {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "invalid_pin"} @@ -259,7 +260,7 @@ async def test_config_flow_reauth_with_errors( {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "invalid_symbol"} @@ -272,7 +273,7 @@ async def test_config_flow_reauth_with_errors( {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "cannot_connect"} @@ -285,7 +286,7 @@ async def test_config_flow_reauth_with_errors( {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "unknown"} @@ -312,7 +313,7 @@ async def test_multiple_config_entries( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} @@ -321,7 +322,7 @@ async def test_multiple_config_entries( {"use_saved_credentials": False}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] is None @@ -334,7 +335,7 @@ async def test_multiple_config_entries( {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Jan Kowalski" assert len(mock_setup_entry.mock_calls) == 2 @@ -357,7 +358,7 @@ async def test_multiple_config_entries_using_saved_credentials( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} @@ -370,7 +371,7 @@ async def test_multiple_config_entries_using_saved_credentials( {"use_saved_credentials": True}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Jan Kowalski" assert len(mock_setup_entry.mock_calls) == 2 @@ -394,7 +395,7 @@ async def test_multiple_config_entries_using_saved_credentials_2( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} @@ -403,7 +404,7 @@ async def test_multiple_config_entries_using_saved_credentials_2( {"use_saved_credentials": True}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_student" assert result["errors"] == {} @@ -416,7 +417,7 @@ async def test_multiple_config_entries_using_saved_credentials_2( {"student": "0"}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Jan Kowalski" assert len(mock_setup_entry.mock_calls) == 2 @@ -447,7 +448,7 @@ async def test_multiple_config_entries_using_saved_credentials_3( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} @@ -456,7 +457,7 @@ async def test_multiple_config_entries_using_saved_credentials_3( {"use_saved_credentials": True}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_saved_credentials" assert result["errors"] is None @@ -469,7 +470,7 @@ async def test_multiple_config_entries_using_saved_credentials_3( {"credentials": "123"}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Jan Kowalski" assert len(mock_setup_entry.mock_calls) == 3 @@ -501,7 +502,7 @@ async def test_multiple_config_entries_using_saved_credentials_4( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} @@ -510,7 +511,7 @@ async def test_multiple_config_entries_using_saved_credentials_4( {"use_saved_credentials": True}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_saved_credentials" assert result["errors"] is None @@ -519,7 +520,7 @@ async def test_multiple_config_entries_using_saved_credentials_4( {"credentials": "123"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_student" assert result["errors"] == {} @@ -532,7 +533,7 @@ async def test_multiple_config_entries_using_saved_credentials_4( {"student": "0"}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Jan Kowalski" assert len(mock_setup_entry.mock_calls) == 3 @@ -559,7 +560,7 @@ async def test_multiple_config_entries_without_valid_saved_credentials( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} @@ -571,7 +572,7 @@ async def test_multiple_config_entries_without_valid_saved_credentials( "homeassistant.components.vulcan.config_flow.Vulcan.get_students", side_effect=UnauthorizedCertificateException, ): - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_saved_credentials" assert result["errors"] is None @@ -580,7 +581,7 @@ async def test_multiple_config_entries_without_valid_saved_credentials( {"credentials": "123"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "expired_credentials"} @@ -607,7 +608,7 @@ async def test_multiple_config_entries_using_saved_credentials_with_connections_ const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} @@ -619,7 +620,7 @@ async def test_multiple_config_entries_using_saved_credentials_with_connections_ "homeassistant.components.vulcan.config_flow.Vulcan.get_students", side_effect=ClientConnectionError, ): - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_saved_credentials" assert result["errors"] is None @@ -628,7 +629,7 @@ async def test_multiple_config_entries_using_saved_credentials_with_connections_ {"credentials": "123"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_saved_credentials" assert result["errors"] == {"base": "cannot_connect"} @@ -655,7 +656,7 @@ async def test_multiple_config_entries_using_saved_credentials_with_unknown_erro const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} @@ -667,7 +668,7 @@ async def test_multiple_config_entries_using_saved_credentials_with_unknown_erro "homeassistant.components.vulcan.config_flow.Vulcan.get_students", side_effect=Exception, ): - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_saved_credentials" assert result["errors"] is None @@ -676,7 +677,7 @@ async def test_multiple_config_entries_using_saved_credentials_with_unknown_erro {"credentials": "123"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "unknown"} @@ -706,7 +707,7 @@ async def test_student_already_exists( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} @@ -715,7 +716,7 @@ async def test_student_already_exists( {"use_saved_credentials": True}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "all_student_already_configured" @@ -733,7 +734,7 @@ async def test_config_flow_auth_invalid_token( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] is None @@ -742,7 +743,7 @@ async def test_config_flow_auth_invalid_token( {CONF_TOKEN: "3S20000", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "invalid_token"} @@ -761,7 +762,7 @@ async def test_config_flow_auth_invalid_region( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] is None @@ -770,7 +771,7 @@ async def test_config_flow_auth_invalid_region( {CONF_TOKEN: "3S10000", CONF_REGION: "invalid_region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "invalid_symbol"} @@ -787,7 +788,7 @@ async def test_config_flow_auth_invalid_pin(mock_keystore, hass: HomeAssistant) const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] is None @@ -796,7 +797,7 @@ async def test_config_flow_auth_invalid_pin(mock_keystore, hass: HomeAssistant) {CONF_TOKEN: "3S10000", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "invalid_pin"} @@ -815,7 +816,7 @@ async def test_config_flow_auth_expired_token( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] is None @@ -824,7 +825,7 @@ async def test_config_flow_auth_expired_token( {CONF_TOKEN: "3S10000", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "expired_token"} @@ -843,7 +844,7 @@ async def test_config_flow_auth_connection_error( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] is None @@ -852,7 +853,7 @@ async def test_config_flow_auth_connection_error( {CONF_TOKEN: "3S10000", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "cannot_connect"} @@ -871,7 +872,7 @@ async def test_config_flow_auth_unknown_error( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] is None @@ -880,6 +881,6 @@ async def test_config_flow_auth_unknown_error( {CONF_TOKEN: "3S10000", CONF_REGION: "invalid_region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "unknown"} diff --git a/tests/components/wallbox/test_config_flow.py b/tests/components/wallbox/test_config_flow.py index ebb3a2fd693..c0428ef47db 100644 --- a/tests/components/wallbox/test_config_flow.py +++ b/tests/components/wallbox/test_config_flow.py @@ -5,7 +5,7 @@ import json import requests_mock -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.wallbox import config_flow from homeassistant.components.wallbox.const import ( CHARGER_ADDED_ENERGY_KEY, @@ -18,6 +18,7 @@ from homeassistant.components.wallbox.const import ( DOMAIN, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import ( authorisation_response, @@ -47,7 +48,7 @@ async def test_show_set_form(hass: HomeAssistant) -> None: flow.hass = hass result = await flow.async_step_user(user_input=None) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" diff --git a/tests/components/waqi/test_config_flow.py b/tests/components/waqi/test_config_flow.py index 712ad8dd39e..fecac7ea0bd 100644 --- a/tests/components/waqi/test_config_flow.py +++ b/tests/components/waqi/test_config_flow.py @@ -52,7 +52,7 @@ async def test_full_map_flow( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with ( patch( @@ -71,7 +71,7 @@ async def test_full_map_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == method with ( @@ -97,7 +97,7 @@ async def test_full_map_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "de Jongweg, Utrecht" assert result["data"] == { CONF_API_KEY: "asd", @@ -137,7 +137,7 @@ async def test_flow_errors( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": error} with ( @@ -157,7 +157,7 @@ async def test_flow_errors( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "map" with ( @@ -179,7 +179,7 @@ async def test_flow_errors( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY @pytest.mark.parametrize( @@ -231,7 +231,7 @@ async def test_error_in_second_step( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with ( patch( @@ -250,7 +250,7 @@ async def test_error_in_second_step( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == method with ( @@ -266,7 +266,7 @@ async def test_error_in_second_step( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": error} with ( @@ -292,7 +292,7 @@ async def test_error_in_second_step( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "de Jongweg, Utrecht" assert result["data"] == { CONF_API_KEY: "asd", diff --git a/tests/components/watttime/test_config_flow.py b/tests/components/watttime/test_config_flow.py index c8e4ed5b06b..f8eee6b48bf 100644 --- a/tests/components/watttime/test_config_flow.py +++ b/tests/components/watttime/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, patch from aiowatttime.errors import CoordinatesNotFoundError, InvalidCredentialsError import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.watttime.config_flow import ( CONF_LOCATION_TYPE, LOCATION_TYPE_HOME, @@ -41,7 +41,7 @@ async def test_auth_errors( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=config_auth ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": error} @@ -76,7 +76,7 @@ async def test_coordinate_errors( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config_coordinates ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == errors @@ -93,7 +93,7 @@ async def test_duplicate_error( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config_location_type ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -104,13 +104,13 @@ async def test_options_flow(hass: HomeAssistant, config_entry) -> None: ): await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + 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_SHOW_ON_MAP: False} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_SHOW_ON_MAP: False} @@ -128,7 +128,7 @@ async def test_show_form_coordinates( result["flow_id"], user_input=config_location_type ) result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "coordinates" assert result["errors"] is None @@ -138,7 +138,7 @@ async def test_show_form_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None @@ -164,7 +164,7 @@ async def test_step_reauth( user_input={CONF_PASSWORD: "password"}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 @@ -186,7 +186,7 @@ async def test_step_user_coordinates( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config_coordinates ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "32.87336, -117.22743" assert result["data"] == { CONF_USERNAME: "user", @@ -211,7 +211,7 @@ async def test_step_user_home( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config_location_type ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "32.87336, -117.22743" assert result["data"] == { CONF_USERNAME: "user", diff --git a/tests/components/waze_travel_time/test_config_flow.py b/tests/components/waze_travel_time/test_config_flow.py index 9bd5016c2f4..6d155c4e79b 100644 --- a/tests/components/waze_travel_time/test_config_flow.py +++ b/tests/components/waze_travel_time/test_config_flow.py @@ -2,7 +2,7 @@ import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.waze_travel_time.const import ( CONF_AVOID_FERRIES, CONF_AVOID_SUBSCRIPTION_ROADS, @@ -21,6 +21,7 @@ from homeassistant.components.waze_travel_time.const import ( ) from homeassistant.const import CONF_NAME, CONF_REGION from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .const import CONFIG_FLOW_USER_INPUT, MOCK_CONFIG @@ -33,7 +34,7 @@ async def test_minimum_fields(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -42,7 +43,7 @@ async def test_minimum_fields(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == DEFAULT_NAME assert result2["data"] == { CONF_NAME: DEFAULT_NAME, @@ -65,7 +66,7 @@ async def test_options(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id, data=None) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -81,7 +82,7 @@ async def test_options(hass: HomeAssistant) -> None: CONF_VEHICLE_TYPE: "taxi", }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"] == { CONF_AVOID_FERRIES: True, @@ -112,7 +113,7 @@ async def test_dupe(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -121,13 +122,13 @@ async def test_dupe(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -136,7 +137,7 @@ async def test_dupe(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY @pytest.mark.usefixtures("invalidate_config_entry") @@ -147,14 +148,14 @@ async def test_invalid_config_entry( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], CONFIG_FLOW_USER_INPUT, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} assert "Error trying to validate entry" in caplog.text diff --git a/tests/components/weatherflow/test_config_flow.py b/tests/components/weatherflow/test_config_flow.py index 51aec02cab7..f46c1cbf75a 100644 --- a/tests/components/weatherflow/test_config_flow.py +++ b/tests/components/weatherflow/test_config_flow.py @@ -30,7 +30,7 @@ async def test_single_instance( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -48,7 +48,7 @@ async def test_devices_with_mocks( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {} @@ -80,12 +80,12 @@ async def test_devices_with_various_mocks_errors( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == error_msg assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure(result["flow_id"]) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {} diff --git a/tests/components/weatherflow_cloud/test_config_flow.py b/tests/components/weatherflow_cloud/test_config_flow.py index cef0e224434..b111ef462e6 100644 --- a/tests/components/weatherflow_cloud/test_config_flow.py +++ b/tests/components/weatherflow_cloud/test_config_flow.py @@ -17,7 +17,7 @@ async def test_config(hass: HomeAssistant, mock_get_stations) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -27,7 +27,7 @@ async def test_config(hass: HomeAssistant, mock_get_stations) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_config_flow_abort(hass: HomeAssistant, mock_get_stations) -> None: @@ -43,7 +43,7 @@ async def test_config_flow_abort(hass: HomeAssistant, mock_get_stations) -> None result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -51,7 +51,7 @@ async def test_config_flow_abort(hass: HomeAssistant, mock_get_stations) -> None }, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -71,7 +71,7 @@ async def test_config_errors( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( @@ -80,7 +80,7 @@ async def test_config_errors( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": expected_error} with mock_get_stations: @@ -90,7 +90,7 @@ async def test_config_errors( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_reauth(hass: HomeAssistant, mock_get_stations_401_error) -> None: @@ -110,7 +110,7 @@ async def test_reauth(hass: HomeAssistant, mock_get_stations_401_error) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_REAUTH, "entry_id": entry.entry_id}, data=None ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_REAUTH, "entry_id": entry.entry_id}, @@ -118,4 +118,4 @@ async def test_reauth(hass: HomeAssistant, mock_get_stations_401_error) -> None: ) assert result["reason"] == "reauth_successful" - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT diff --git a/tests/components/weatherkit/test_config_flow.py b/tests/components/weatherkit/test_config_flow.py index 58397ac2ed0..396889dd815 100644 --- a/tests/components/weatherkit/test_config_flow.py +++ b/tests/components/weatherkit/test_config_flow.py @@ -46,7 +46,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -59,7 +59,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY location = EXAMPLE_USER_INPUT[CONF_LOCATION] assert result["title"] == f"{location[CONF_LATITUDE]}, {location[CONF_LONGITUDE]}" @@ -94,7 +94,7 @@ async def test_error_handling( EXAMPLE_USER_INPUT, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": expected_error} @@ -113,7 +113,7 @@ async def test_form_unsupported_location(hass: HomeAssistant) -> None: EXAMPLE_USER_INPUT, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "unsupported_location"} # Test that we can recover from this error by changing the location @@ -126,7 +126,7 @@ async def test_form_unsupported_location(hass: HomeAssistant) -> None: EXAMPLE_USER_INPUT, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY @pytest.mark.parametrize( @@ -159,7 +159,7 @@ async def test_auto_fix_key_input( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -174,7 +174,7 @@ async def test_auto_fix_key_input( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_KEY_PEM] == EXAMPLE_CONFIG_DATA[CONF_KEY_PEM] assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/webmin/test_config_flow.py b/tests/components/webmin/test_config_flow.py index e680f0e164a..a9f5eafc5c7 100644 --- a/tests/components/webmin/test_config_flow.py +++ b/tests/components/webmin/test_config_flow.py @@ -28,7 +28,7 @@ async def user_flow(hass: HomeAssistant) -> str: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None return result["flow_id"] @@ -47,7 +47,7 @@ async def test_form_user( user_flow, TEST_USER_INPUT ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_USER_INPUT[CONF_HOST] assert result["options"] == TEST_USER_INPUT @@ -89,7 +89,7 @@ async def test_form_user_errors( user_flow, TEST_USER_INPUT ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": error_type} @@ -101,7 +101,7 @@ async def test_form_user_errors( result["flow_id"], TEST_USER_INPUT ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_USER_INPUT[CONF_HOST] assert result["options"] == TEST_USER_INPUT @@ -121,7 +121,7 @@ async def test_duplicate_entry( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_USER_INPUT[CONF_HOST] assert result["options"] == TEST_USER_INPUT @@ -137,5 +137,5 @@ async def test_duplicate_entry( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/webostv/test_config_flow.py b/tests/components/webostv/test_config_flow.py index 07a11b5bf29..afda36d913f 100644 --- a/tests/components/webostv/test_config_flow.py +++ b/tests/components/webostv/test_config_flow.py @@ -45,7 +45,7 @@ async def test_form(hass: HomeAssistant, client) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_init( @@ -55,7 +55,7 @@ async def test_form(hass: HomeAssistant, client) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing" result = await hass.config_entries.flow.async_init( @@ -65,7 +65,7 @@ async def test_form(hass: HomeAssistant, client) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing" result = await hass.config_entries.flow.async_configure( @@ -74,7 +74,7 @@ async def test_form(hass: HomeAssistant, client) -> None: await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TV_NAME @@ -106,7 +106,7 @@ async def test_options_flow_live_tv_in_apps( result = await hass.config_entries.options.async_init(entry.entry_id) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result2 = await hass.config_entries.options.async_configure( @@ -115,7 +115,7 @@ async def test_options_flow_live_tv_in_apps( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"][CONF_SOURCES] == ["Live TV", "Input01", "Input02"] @@ -127,7 +127,7 @@ async def test_options_flow_cannot_retrieve(hass: HomeAssistant, client) -> None result = await hass.config_entries.options.async_init(entry.entry_id) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_retrieve"} @@ -145,7 +145,7 @@ async def test_form_cannot_connect(hass: HomeAssistant, client) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -163,7 +163,7 @@ async def test_form_pairexception(hass: HomeAssistant, client) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "error_pairing" @@ -178,7 +178,7 @@ async def test_entry_already_configured(hass: HomeAssistant, client) -> None: data=MOCK_USER_CONFIG, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -191,7 +191,7 @@ async def test_form_ssdp(hass: HomeAssistant, client) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing" @@ -206,7 +206,7 @@ async def test_ssdp_in_progress(hass: HomeAssistant, client) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing" result2 = await hass.config_entries.flow.async_init( @@ -214,7 +214,7 @@ async def test_ssdp_in_progress(hass: HomeAssistant, client) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_in_progress" @@ -229,7 +229,7 @@ async def test_ssdp_update_uuid(hass: HomeAssistant, client) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.unique_id == MOCK_DISCOVERY_INFO.upnp[ssdp.ATTR_UPNP_UDN][5:] @@ -248,7 +248,7 @@ async def test_ssdp_not_update_uuid(hass: HomeAssistant, client) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pairing" assert entry.unique_id is None @@ -266,7 +266,7 @@ async def test_form_abort_uuid_configured(hass: HomeAssistant, client) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" user_config = { @@ -281,7 +281,7 @@ async def test_form_abort_uuid_configured(hass: HomeAssistant, client) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing" result = await hass.config_entries.flow.async_configure( @@ -290,7 +290,7 @@ async def test_form_abort_uuid_configured(hass: HomeAssistant, client) -> None: await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == "new_host" @@ -309,7 +309,7 @@ async def test_reauth_successful(hass: HomeAssistant, client, monkeypatch) -> No result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert entry.data[CONF_CLIENT_SECRET] == CLIENT_KEY @@ -318,7 +318,7 @@ async def test_reauth_successful(hass: HomeAssistant, client, monkeypatch) -> No result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data[CONF_CLIENT_SECRET] == "new_key" @@ -346,7 +346,7 @@ async def test_reauth_errors( result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" monkeypatch.setattr(client, "connect", Mock(side_effect=side_effect)) @@ -354,5 +354,5 @@ async def test_reauth_errors( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == reason diff --git a/tests/components/wemo/test_config_flow.py b/tests/components/wemo/test_config_flow.py index 5cb2b54c9a0..6eaa32b960e 100644 --- a/tests/components/wemo/test_config_flow.py +++ b/tests/components/wemo/test_config_flow.py @@ -21,7 +21,7 @@ async def test_not_discovered(hass: HomeAssistant) -> None: with patch("homeassistant.components.wemo.config_flow.pywemo") as mock_pywemo: mock_pywemo.discover_devices.return_value = [] result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -33,14 +33,14 @@ async def test_options(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input=asdict(options) ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert Options(**result["data"]) == options @@ -51,7 +51,7 @@ async def test_invalid_options(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" # enable_subscription must be True if enable_long_press is True (default). diff --git a/tests/components/whirlpool/test_config_flow.py b/tests/components/whirlpool/test_config_flow.py index 273f0e6737d..debee3df743 100644 --- a/tests/components/whirlpool/test_config_flow.py +++ b/tests/components/whirlpool/test_config_flow.py @@ -202,7 +202,7 @@ async def test_no_appliances_flow(hass: HomeAssistant, region, brand) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER with ( @@ -222,7 +222,7 @@ async def test_no_appliances_flow(hass: HomeAssistant, region, brand) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "no_appliances"} @@ -246,7 +246,7 @@ async def test_reauth_flow(hass: HomeAssistant, region, brand) -> None: ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -274,7 +274,7 @@ async def test_reauth_flow(hass: HomeAssistant, region, brand) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert mock_entry.data == { CONF_USERNAME: "test-username", @@ -310,7 +310,7 @@ async def test_reauth_flow_auth_error(hass: HomeAssistant, region, brand) -> Non ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( patch( @@ -329,7 +329,7 @@ async def test_reauth_flow_auth_error(hass: HomeAssistant, region, brand) -> Non ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -356,7 +356,7 @@ async def test_reauth_flow_connnection_error( ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -379,5 +379,5 @@ async def test_reauth_flow_connnection_error( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/whois/test_config_flow.py b/tests/components/whois/test_config_flow.py index 1d3f1a8c6d2..35e40c4e809 100644 --- a/tests/components/whois/test_config_flow.py +++ b/tests/components/whois/test_config_flow.py @@ -31,7 +31,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( @@ -39,7 +39,7 @@ async def test_full_user_flow( user_input={CONF_DOMAIN: "Example.com"}, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2 == snapshot assert len(mock_setup_entry.mock_calls) == 1 @@ -71,7 +71,7 @@ async def test_full_flow_with_error( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" mock_whois.side_effect = throw @@ -80,7 +80,7 @@ async def test_full_flow_with_error( user_input={CONF_DOMAIN: "Example.com"}, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "user" assert result2.get("errors") == {"base": reason} @@ -93,7 +93,7 @@ async def test_full_flow_with_error( user_input={CONF_DOMAIN: "Example.com"}, ) - assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY assert result3 == snapshot assert len(mock_setup_entry.mock_calls) == 1 @@ -115,7 +115,7 @@ async def test_already_configured( data={CONF_DOMAIN: "HOME-Assistant.io"}, ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/wiffi/test_config_flow.py b/tests/components/wiffi/test_config_flow.py index 14cb8a03f7a..f7231f56062 100644 --- a/tests/components/wiffi/test_config_flow.py +++ b/tests/components/wiffi/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.wiffi.const import DOMAIN from homeassistant.const import CONF_PORT, CONF_TIMEOUT from homeassistant.core import HomeAssistant @@ -77,7 +77,7 @@ async def test_form(hass: HomeAssistant, dummy_tcp_server) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == config_entries.SOURCE_USER @@ -85,7 +85,7 @@ async def test_form(hass: HomeAssistant, dummy_tcp_server) -> None: result["flow_id"], user_input=MOCK_CONFIG, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY async def test_form_addr_in_use(hass: HomeAssistant, addr_in_use) -> None: @@ -98,7 +98,7 @@ async def test_form_addr_in_use(hass: HomeAssistant, addr_in_use) -> None: result["flow_id"], user_input=MOCK_CONFIG, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "addr_in_use" @@ -114,7 +114,7 @@ async def test_form_start_server_failed( result["flow_id"], user_input=MOCK_CONFIG, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "start_server_failed" @@ -127,13 +127,13 @@ async def test_option_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id, data=None) - assert result["type"] == data_entry_flow.FlowResultType.FORM + 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_TIMEOUT: 9} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"][CONF_TIMEOUT] == 9 diff --git a/tests/components/wilight/test_config_flow.py b/tests/components/wilight/test_config_flow.py index e3496010c95..ba97f1f7d94 100644 --- a/tests/components/wilight/test_config_flow.py +++ b/tests/components/wilight/test_config_flow.py @@ -59,7 +59,7 @@ async def test_show_ssdp_form(hass: HomeAssistant) -> None: DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["description_placeholders"] == { CONF_NAME: f"WL{WILIGHT_ID}", @@ -75,7 +75,7 @@ async def test_ssdp_not_wilight_abort_1(hass: HomeAssistant) -> None: DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_wilight_device" @@ -87,7 +87,7 @@ async def test_ssdp_not_wilight_abort_2(hass: HomeAssistant) -> None: DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_wilight_device" @@ -101,7 +101,7 @@ async def test_ssdp_not_wilight_abort_3( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_wilight_device" @@ -115,7 +115,7 @@ async def test_ssdp_not_supported_abort( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported_device" @@ -140,7 +140,7 @@ async def test_ssdp_device_exists_abort(hass: HomeAssistant) -> None: data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -152,7 +152,7 @@ async def test_full_ssdp_flow_implementation(hass: HomeAssistant) -> None: DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["description_placeholders"] == { CONF_NAME: f"WL{WILIGHT_ID}", @@ -163,7 +163,7 @@ async def test_full_ssdp_flow_implementation(hass: HomeAssistant) -> None: result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"WL{WILIGHT_ID}" assert result["data"] diff --git a/tests/components/withings/test_config_flow.py b/tests/components/withings/test_config_flow.py index 9852461f5e2..2bafbc97573 100644 --- a/tests/components/withings/test_config_flow.py +++ b/tests/components/withings/test_config_flow.py @@ -34,7 +34,7 @@ async def test_full_flow( }, ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["url"] == ( "https://account.withings.com/oauth2_user/authorize2?" f"response_type=code&client_id={CLIENT_ID}&" @@ -69,7 +69,7 @@ async def test_full_flow( assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(mock_setup.mock_calls) == 1 - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Withings" assert "result" in result assert result["result"].unique_id == "600" @@ -100,7 +100,7 @@ async def test_config_non_unique_profile( }, ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["url"] == ( "https://account.withings.com/oauth2_user/authorize2?" f"response_type=code&client_id={CLIENT_ID}&" @@ -128,7 +128,7 @@ async def test_config_non_unique_profile( }, ) result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -190,7 +190,7 @@ async def test_config_reauth_profile( result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" @@ -252,7 +252,7 @@ async def test_config_reauth_wrong_account( result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "wrong_account" @@ -276,7 +276,7 @@ async def test_config_flow_with_invalid_credentials( }, ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["url"] == ( "https://account.withings.com/oauth2_user/authorize2?" f"response_type=code&client_id={CLIENT_ID}&" @@ -303,5 +303,5 @@ async def test_config_flow_with_invalid_credentials( result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "oauth_error" diff --git a/tests/components/wiz/test_config_flow.py b/tests/components/wiz/test_config_flow.py index 1b84a048fd2..3969c3ab1b3 100644 --- a/tests/components/wiz/test_config_flow.py +++ b/tests/components/wiz/test_config_flow.py @@ -91,7 +91,7 @@ async def test_user_flow_enters_dns_name(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "no_ip"} with ( @@ -193,7 +193,7 @@ async def test_discovered_by_dhcp_connection_fails( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -270,7 +270,7 @@ async def test_discovered_by_dhcp_or_integration_discovery( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" with ( @@ -324,7 +324,7 @@ async def test_discovered_by_dhcp_or_integration_discovery_updates_host( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == FAKE_IP @@ -353,7 +353,7 @@ async def test_discovered_by_dhcp_or_integration_discovery_avoid_waiting_for_ret ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.state is config_entries.ConfigEntryState.LOADED @@ -483,7 +483,7 @@ async def test_setup_via_discovery_exception_finds_nothing(hass: HomeAssistant) result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "no_devices_found" @@ -501,7 +501,7 @@ async def test_discovery_with_firmware_update(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" # In between discovery and when the user clicks to set it up the firmware diff --git a/tests/components/wled/test_config_flow.py b/tests/components/wled/test_config_flow.py index fc2a11c3e46..a1529eda1c7 100644 --- a/tests/components/wled/test_config_flow.py +++ b/tests/components/wled/test_config_flow.py @@ -25,14 +25,14 @@ async def test_full_user_flow_implementation(hass: HomeAssistant) -> None: ) assert result.get("step_id") == "user" - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "192.168.1.123"} ) assert result.get("title") == "WLED RGB Light" - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert "data" in result assert result["data"][CONF_HOST] == "192.168.1.123" assert "result" in result @@ -64,14 +64,14 @@ async def test_full_zeroconf_flow_implementation(hass: HomeAssistant) -> None: ) assert result.get("description_placeholders") == {CONF_NAME: "WLED RGB Light"} assert result.get("step_id") == "zeroconf_confirm" - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) assert result2.get("title") == "WLED RGB Light" - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert "data" in result2 assert result2["data"][CONF_HOST] == "192.168.1.123" @@ -101,7 +101,7 @@ async def test_zeroconf_during_onboarding( ) assert result.get("title") == "WLED RGB Light" - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("data") == {CONF_HOST: "192.168.1.123"} assert "result" in result @@ -120,7 +120,7 @@ async def test_connection_error(hass: HomeAssistant, mock_wled: MagicMock) -> No data={CONF_HOST: "example.com"}, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" assert result.get("errors") == {"base": "cannot_connect"} @@ -145,7 +145,7 @@ async def test_zeroconf_connection_error( ), ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "cannot_connect" @@ -163,7 +163,7 @@ async def test_user_device_exists_abort( data={CONF_HOST: "192.168.1.123"}, ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -180,7 +180,7 @@ async def test_user_with_cct_channel_abort( data={CONF_HOST: "192.168.1.123"}, ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "cct_unsupported" @@ -205,7 +205,7 @@ async def test_zeroconf_without_mac_device_exists_abort( ), ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -230,7 +230,7 @@ async def test_zeroconf_with_mac_device_exists_abort( ), ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -255,7 +255,7 @@ async def test_zeroconf_with_cct_channel_abort( ), ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "cct_unsupported" @@ -267,7 +267,7 @@ async def test_options_flow( result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "init" result2 = await hass.config_entries.options.async_configure( @@ -275,7 +275,7 @@ async def test_options_flow( user_input={CONF_KEEP_MAIN_LIGHT: True}, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("data") == { CONF_KEEP_MAIN_LIGHT: True, } diff --git a/tests/components/wolflink/test_config_flow.py b/tests/components/wolflink/test_config_flow.py index bee646deae8..8c497ae3943 100644 --- a/tests/components/wolflink/test_config_flow.py +++ b/tests/components/wolflink/test_config_flow.py @@ -6,7 +6,7 @@ from httpcore import ConnectError from wolf_comm.models import Device from wolf_comm.token_auth import InvalidAuth -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.wolflink.const import ( DEVICE_GATEWAY, DEVICE_ID, @@ -15,6 +15,7 @@ from homeassistant.components.wolflink.const import ( ) from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -40,7 +41,7 @@ async def test_show_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -54,7 +55,7 @@ async def test_device_step_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=INPUT_CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "device" @@ -76,7 +77,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: {"device_name": CONFIG[DEVICE_NAME]}, ) - assert result_create_entry["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result_create_entry["type"] is FlowResultType.CREATE_ENTRY assert result_create_entry["title"] == CONFIG[DEVICE_NAME] assert result_create_entry["data"] == CONFIG @@ -145,5 +146,5 @@ async def test_already_configured_error(hass: HomeAssistant) -> None: {"device_name": CONFIG[DEVICE_NAME]}, ) - assert result_create_entry["type"] == data_entry_flow.FlowResultType.ABORT + assert result_create_entry["type"] is FlowResultType.ABORT assert result_create_entry["reason"] == "already_configured" diff --git a/tests/components/workday/test_config_flow.py b/tests/components/workday/test_config_flow.py index 75677143ecb..7eb3065e576 100644 --- a/tests/components/workday/test_config_flow.py +++ b/tests/components/workday/test_config_flow.py @@ -35,7 +35,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -58,7 +58,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Workday Sensor" assert result3["options"] == { "name": "Workday Sensor", @@ -78,7 +78,7 @@ async def test_form_no_country(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -99,7 +99,7 @@ async def test_form_no_country(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Workday Sensor" assert result3["options"] == { "name": "Workday Sensor", @@ -117,7 +117,7 @@ async def test_form_no_subdivision(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -139,7 +139,7 @@ async def test_form_no_subdivision(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Workday Sensor" assert result3["options"] == { "name": "Workday Sensor", @@ -185,7 +185,7 @@ async def test_options_form(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { "name": "Workday Sensor", "country": "DE", @@ -205,7 +205,7 @@ async def test_form_incorrect_dates(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -257,7 +257,7 @@ async def test_form_incorrect_dates(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Workday Sensor" assert result3["options"] == { "name": "Workday Sensor", @@ -333,7 +333,7 @@ async def test_options_form_incorrect_dates(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { "name": "Workday Sensor", "country": "DE", @@ -392,7 +392,7 @@ async def test_options_form_abort_duplicate(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "already_configured"} @@ -402,7 +402,7 @@ async def test_form_incorrect_date_range(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -454,7 +454,7 @@ async def test_form_incorrect_date_range(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Workday Sensor" assert result3["options"] == { "name": "Workday Sensor", @@ -530,7 +530,7 @@ async def test_options_form_incorrect_date_ranges(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { "name": "Workday Sensor", "country": "DE", @@ -563,7 +563,7 @@ async def test_language( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -586,7 +586,7 @@ async def test_language( ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Workday Sensor" assert result3["options"] == { "name": "Workday Sensor", diff --git a/tests/components/ws66i/test_config_flow.py b/tests/components/ws66i/test_config_flow.py index 19a329cb913..4124bb9b662 100644 --- a/tests/components/ws66i/test_config_flow.py +++ b/tests/components/ws66i/test_config_flow.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.ws66i.const import ( CONF_SOURCE_1, CONF_SOURCE_2, @@ -16,6 +16,7 @@ from homeassistant.components.ws66i.const import ( ) from homeassistant.const import CONF_IP_ADDRESS from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .test_media_player import AttrDict @@ -130,7 +131,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -145,7 +146,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options[CONF_SOURCES] == { "1": "one", "2": "too", diff --git a/tests/components/wyoming/test_config_flow.py b/tests/components/wyoming/test_config_flow.py index c15eb81a1e2..e363a0650bc 100644 --- a/tests/components/wyoming/test_config_flow.py +++ b/tests/components/wyoming/test_config_flow.py @@ -46,7 +46,7 @@ async def test_form_stt(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> Non result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -62,7 +62,7 @@ async def test_form_stt(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> Non ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Test ASR" assert result2["data"] == { "host": "1.1.1.1", @@ -76,7 +76,7 @@ async def test_form_tts(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> Non result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -92,7 +92,7 @@ async def test_form_tts(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> Non ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Test TTS" assert result2["data"] == { "host": "1.1.1.1", @@ -119,7 +119,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -141,7 +141,7 @@ async def test_no_supported_services(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "no_services" @@ -159,7 +159,7 @@ async def test_hassio_addon_discovery( context={"source": config_entries.SOURCE_HASSIO}, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "hassio_confirm" assert result.get("description_placeholders") == {"addon": "Piper"} @@ -169,7 +169,7 @@ async def test_hassio_addon_discovery( ) as mock_wyoming: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2 == snapshot assert len(mock_setup_entry.mock_calls) == 1 @@ -189,7 +189,7 @@ async def test_hassio_addon_already_configured(hass: HomeAssistant) -> None: data=ADDON_DISCOVERY, context={"source": config_entries.SOURCE_HASSIO}, ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -207,7 +207,7 @@ async def test_hassio_addon_cannot_connect(hass: HomeAssistant) -> None: ): result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("errors") == {"base": "cannot_connect"} @@ -225,7 +225,7 @@ async def test_hassio_addon_no_supported_services(hass: HomeAssistant) -> None: ): result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "no_services" @@ -245,14 +245,14 @@ async def test_zeroconf_discovery( context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "zeroconf_confirm" assert result.get("description_placeholders") == { "name": SATELLITE_INFO.satellite.name } result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2 == snapshot @@ -275,7 +275,7 @@ async def test_zeroconf_discovery_no_port( context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "no_port" @@ -295,5 +295,5 @@ async def test_zeroconf_discovery_no_services( context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "no_services" diff --git a/tests/components/xbox/test_config_flow.py b/tests/components/xbox/test_config_flow.py index d4f7a697839..5abf9ad25d9 100644 --- a/tests/components/xbox/test_config_flow.py +++ b/tests/components/xbox/test_config_flow.py @@ -3,13 +3,14 @@ from http import HTTPStatus from unittest.mock import patch -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, setup from homeassistant.components.application_credentials import ( ClientCredential, async_import_client_credential, ) from homeassistant.components.xbox.const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow from tests.common import MockConfigEntry @@ -27,7 +28,7 @@ async def test_abort_if_existing_entry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( "xbox", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/xiaomi_ble/test_config_flow.py b/tests/components/xiaomi_ble/test_config_flow.py index 8b3ff2ef4ab..b61615e0f79 100644 --- a/tests/components/xiaomi_ble/test_config_flow.py +++ b/tests/components/xiaomi_ble/test_config_flow.py @@ -30,7 +30,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=MMC_T201_1_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True @@ -38,7 +38,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Baby Thermometer 6FC1 (MMC-T201-1)" assert result2["data"] == {} assert result2["result"].unique_id == "00:81:F9:DD:6F:C1" @@ -57,7 +57,7 @@ async def test_async_step_bluetooth_valid_device_but_missing_payload( context={"source": config_entries.SOURCE_BLUETOOTH}, data=MISSING_PAYLOAD_ENCRYPTED, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm_slow" with patch( @@ -66,7 +66,7 @@ async def test_async_step_bluetooth_valid_device_but_missing_payload( result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Temperature/Humidity Sensor 5384 (LYWSD03MMC)" assert result2["data"] == {} assert result2["result"].unique_id == "A4:C1:38:56:53:84" @@ -96,7 +96,7 @@ async def test_async_step_bluetooth_valid_device_but_missing_payload_then_full( context={"source": config_entries.SOURCE_BLUETOOTH}, data=MISSING_PAYLOAD_ENCRYPTED, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "get_encryption_key_4_5" with patch( @@ -107,7 +107,7 @@ async def test_async_step_bluetooth_valid_device_but_missing_payload_then_full( user_input={"bindkey": "a115210eed7a88e50ad52662e732a9fb"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == {"bindkey": "a115210eed7a88e50ad52662e732a9fb"} assert result2["result"].unique_id == "A4:C1:38:56:53:84" @@ -129,7 +129,7 @@ async def test_async_step_bluetooth_during_onboarding(hass: HomeAssistant) -> No data=MMC_T201_1_SERVICE_INFO, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Baby Thermometer 6FC1 (MMC-T201-1)" assert result["data"] == {} assert result["result"].unique_id == "00:81:F9:DD:6F:C1" @@ -146,7 +146,7 @@ async def test_async_step_bluetooth_valid_device_legacy_encryption( context={"source": config_entries.SOURCE_BLUETOOTH}, data=YLKG07YL_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "get_encryption_key_legacy" with patch( @@ -156,7 +156,7 @@ async def test_async_step_bluetooth_valid_device_legacy_encryption( result["flow_id"], user_input={"bindkey": "b853075158487ca39a5b5ea9"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Dimmer Switch 988B (YLKG07YL/YLKG08YL)" assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} assert result2["result"].unique_id == "F8:24:41:C5:98:8B" @@ -171,14 +171,14 @@ async def test_async_step_bluetooth_valid_device_legacy_encryption_wrong_key( context={"source": config_entries.SOURCE_BLUETOOTH}, data=YLKG07YL_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "get_encryption_key_legacy" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"bindkey": "aaaaaaaaaaaaaaaaaaaaaaaa"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "get_encryption_key_legacy" assert result2["errors"]["bindkey"] == "decryption_failed" @@ -190,7 +190,7 @@ async def test_async_step_bluetooth_valid_device_legacy_encryption_wrong_key( result["flow_id"], user_input={"bindkey": "b853075158487ca39a5b5ea9"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Dimmer Switch 988B (YLKG07YL/YLKG08YL)" assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} assert result2["result"].unique_id == "F8:24:41:C5:98:8B" @@ -205,14 +205,14 @@ async def test_async_step_bluetooth_valid_device_legacy_encryption_wrong_key_len context={"source": config_entries.SOURCE_BLUETOOTH}, data=YLKG07YL_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "get_encryption_key_legacy" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"bindkey": "aaaaaaaaaaaaaaaaaaaaaaa"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "get_encryption_key_legacy" assert result2["errors"]["bindkey"] == "expected_24_characters" @@ -224,7 +224,7 @@ async def test_async_step_bluetooth_valid_device_legacy_encryption_wrong_key_len result["flow_id"], user_input={"bindkey": "b853075158487ca39a5b5ea9"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Dimmer Switch 988B (YLKG07YL/YLKG08YL)" assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} assert result2["result"].unique_id == "F8:24:41:C5:98:8B" @@ -239,7 +239,7 @@ async def test_async_step_bluetooth_valid_device_v4_encryption( context={"source": config_entries.SOURCE_BLUETOOTH}, data=JTYJGD03MI_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "get_encryption_key_4_5" with patch( @@ -250,7 +250,7 @@ async def test_async_step_bluetooth_valid_device_v4_encryption( user_input={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Smoke Detector 9CBC (JTYJGD03MI)" assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" @@ -265,7 +265,7 @@ async def test_async_step_bluetooth_valid_device_v4_encryption_wrong_key( context={"source": config_entries.SOURCE_BLUETOOTH}, data=JTYJGD03MI_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "get_encryption_key_4_5" result2 = await hass.config_entries.flow.async_configure( @@ -273,7 +273,7 @@ async def test_async_step_bluetooth_valid_device_v4_encryption_wrong_key( user_input={"bindkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "get_encryption_key_4_5" assert result2["errors"]["bindkey"] == "decryption_failed" @@ -286,7 +286,7 @@ async def test_async_step_bluetooth_valid_device_v4_encryption_wrong_key( user_input={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Smoke Detector 9CBC (JTYJGD03MI)" assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" @@ -301,7 +301,7 @@ async def test_async_step_bluetooth_valid_device_v4_encryption_wrong_key_length( context={"source": config_entries.SOURCE_BLUETOOTH}, data=JTYJGD03MI_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "get_encryption_key_4_5" result2 = await hass.config_entries.flow.async_configure( @@ -309,7 +309,7 @@ async def test_async_step_bluetooth_valid_device_v4_encryption_wrong_key_length( user_input={"bindkey": "5b51a7c91cde6707c9ef18fda143a58"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "get_encryption_key_4_5" assert result2["errors"]["bindkey"] == "expected_32_characters" @@ -322,7 +322,7 @@ async def test_async_step_bluetooth_valid_device_v4_encryption_wrong_key_length( user_input={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Smoke Detector 9CBC (JTYJGD03MI)" assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" @@ -335,7 +335,7 @@ async def test_async_step_bluetooth_not_xiaomi(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_SENSOR_PUSH_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -345,7 +345,7 @@ async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -362,7 +362,7 @@ async def test_async_step_user_no_devices_found_2(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -376,7 +376,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True @@ -385,7 +385,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: result["flow_id"], user_input={"address": "58:2D:34:35:93:21"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Temperature/Humidity Sensor 9321 (LYWSDCGQ)" assert result2["data"] == {} assert result2["result"].unique_id == "58:2D:34:35:93:21" @@ -401,7 +401,7 @@ async def test_async_step_user_short_payload(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( "homeassistant.components.xiaomi_ble.config_flow.async_process_advertisements", @@ -411,7 +411,7 @@ async def test_async_step_user_short_payload(hass: HomeAssistant) -> None: result["flow_id"], user_input={"address": "A4:C1:38:56:53:84"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "confirm_slow" with patch( @@ -420,7 +420,7 @@ async def test_async_step_user_short_payload(hass: HomeAssistant) -> None: result3 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Temperature/Humidity Sensor 5384 (LYWSD03MMC)" assert result3["data"] == {} assert result3["result"].unique_id == "A4:C1:38:56:53:84" @@ -436,7 +436,7 @@ async def test_async_step_user_short_payload_then_full(hass: HomeAssistant) -> N DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" async def _async_process_advertisements( @@ -457,7 +457,7 @@ async def test_async_step_user_short_payload_then_full(hass: HomeAssistant) -> N result["flow_id"], user_input={"address": "A4:C1:38:56:53:84"}, ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "get_encryption_key_4_5" with patch( @@ -468,7 +468,7 @@ async def test_async_step_user_short_payload_then_full(hass: HomeAssistant) -> N user_input={"bindkey": "a115210eed7a88e50ad52662e732a9fb"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Temperature/Humidity Sensor 5384 (LYWSD03MMC)" assert result2["data"] == {"bindkey": "a115210eed7a88e50ad52662e732a9fb"} @@ -485,14 +485,14 @@ async def test_async_step_user_with_found_devices_v4_encryption( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result1 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"address": "54:EF:44:E3:9C:BC"}, ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "get_encryption_key_4_5" with patch( @@ -503,7 +503,7 @@ async def test_async_step_user_with_found_devices_v4_encryption( user_input={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Smoke Detector 9CBC (JTYJGD03MI)" assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" @@ -522,7 +522,7 @@ async def test_async_step_user_with_found_devices_v4_encryption_wrong_key( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # Pick a device @@ -530,7 +530,7 @@ async def test_async_step_user_with_found_devices_v4_encryption_wrong_key( result["flow_id"], user_input={"address": "54:EF:44:E3:9C:BC"}, ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "get_encryption_key_4_5" # Try an incorrect key @@ -538,7 +538,7 @@ async def test_async_step_user_with_found_devices_v4_encryption_wrong_key( result["flow_id"], user_input={"bindkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "get_encryption_key_4_5" assert result2["errors"]["bindkey"] == "decryption_failed" @@ -551,7 +551,7 @@ async def test_async_step_user_with_found_devices_v4_encryption_wrong_key( user_input={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Smoke Detector 9CBC (JTYJGD03MI)" assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" @@ -570,7 +570,7 @@ async def test_async_step_user_with_found_devices_v4_encryption_wrong_key_length DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # Select a single device @@ -578,7 +578,7 @@ async def test_async_step_user_with_found_devices_v4_encryption_wrong_key_length result["flow_id"], user_input={"address": "54:EF:44:E3:9C:BC"}, ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "get_encryption_key_4_5" # Try an incorrect key @@ -587,8 +587,8 @@ async def test_async_step_user_with_found_devices_v4_encryption_wrong_key_length user_input={"bindkey": "5b51a7c91cde6707c9ef1dfda143a58"}, ) - assert result2["type"] == FlowResultType.FORM - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "get_encryption_key_4_5" assert result2["errors"]["bindkey"] == "expected_32_characters" @@ -601,7 +601,7 @@ async def test_async_step_user_with_found_devices_v4_encryption_wrong_key_length user_input={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Smoke Detector 9CBC (JTYJGD03MI)" assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" @@ -619,14 +619,14 @@ async def test_async_step_user_with_found_devices_legacy_encryption( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result1 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"address": "F8:24:41:C5:98:8B"}, ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "get_encryption_key_legacy" with patch( @@ -636,7 +636,7 @@ async def test_async_step_user_with_found_devices_legacy_encryption( result["flow_id"], user_input={"bindkey": "b853075158487ca39a5b5ea9"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Dimmer Switch 988B (YLKG07YL/YLKG08YL)" assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} assert result2["result"].unique_id == "F8:24:41:C5:98:8B" @@ -654,14 +654,14 @@ async def test_async_step_user_with_found_devices_legacy_encryption_wrong_key( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result1 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"address": "F8:24:41:C5:98:8B"}, ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "get_encryption_key_legacy" # Enter an incorrect code @@ -669,7 +669,7 @@ async def test_async_step_user_with_found_devices_legacy_encryption_wrong_key( result["flow_id"], user_input={"bindkey": "aaaaaaaaaaaaaaaaaaaaaaaa"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "get_encryption_key_legacy" assert result2["errors"]["bindkey"] == "decryption_failed" @@ -681,7 +681,7 @@ async def test_async_step_user_with_found_devices_legacy_encryption_wrong_key( result["flow_id"], user_input={"bindkey": "b853075158487ca39a5b5ea9"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Dimmer Switch 988B (YLKG07YL/YLKG08YL)" assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} assert result2["result"].unique_id == "F8:24:41:C5:98:8B" @@ -699,14 +699,14 @@ async def test_async_step_user_with_found_devices_legacy_encryption_wrong_key_le DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result1 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"address": "F8:24:41:C5:98:8B"}, ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "get_encryption_key_legacy" # Enter an incorrect code @@ -714,7 +714,7 @@ async def test_async_step_user_with_found_devices_legacy_encryption_wrong_key_le result["flow_id"], user_input={"bindkey": "b85307518487ca39a5b5ea9"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "get_encryption_key_legacy" assert result2["errors"]["bindkey"] == "expected_24_characters" @@ -726,7 +726,7 @@ async def test_async_step_user_with_found_devices_legacy_encryption_wrong_key_le result["flow_id"], user_input={"bindkey": "b853075158487ca39a5b5ea9"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Dimmer Switch 988B (YLKG07YL/YLKG08YL)" assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} assert result2["result"].unique_id == "F8:24:41:C5:98:8B" @@ -742,7 +742,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -758,7 +758,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - result["flow_id"], user_input={"address": "58:2D:34:35:93:21"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -780,7 +780,7 @@ async def test_async_step_user_with_found_devices_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -797,7 +797,7 @@ async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant) - context={"source": config_entries.SOURCE_BLUETOOTH}, data=MMC_T201_1_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -808,7 +808,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=MMC_T201_1_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -816,7 +816,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=MMC_T201_1_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -829,7 +829,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=MMC_T201_1_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -840,7 +840,7 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True @@ -849,7 +849,7 @@ async def test_async_step_user_takes_precedence_over_discovery( result["flow_id"], user_input={"address": "00:81:F9:DD:6F:C1"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Baby Thermometer 6FC1 (MMC-T201-1)" assert result2["data"] == {} assert result2["result"].unique_id == "00:81:F9:DD:6F:C1" @@ -903,7 +903,7 @@ async def test_async_step_reauth_legacy(hass: HomeAssistant) -> None: result["flow_id"], user_input={"bindkey": "b853075158487ca39a5b5ea9"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -952,7 +952,7 @@ async def test_async_step_reauth_legacy_wrong_key(hass: HomeAssistant) -> None: result["flow_id"], user_input={"bindkey": "b85307515a487ca39a5b5ea9"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result["step_id"] == "get_encryption_key_legacy" assert result2["errors"]["bindkey"] == "decryption_failed" @@ -960,7 +960,7 @@ async def test_async_step_reauth_legacy_wrong_key(hass: HomeAssistant) -> None: result["flow_id"], user_input={"bindkey": "b853075158487ca39a5b5ea9"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -1009,7 +1009,7 @@ async def test_async_step_reauth_v4(hass: HomeAssistant) -> None: result["flow_id"], user_input={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -1058,7 +1058,7 @@ async def test_async_step_reauth_v4_wrong_key(hass: HomeAssistant) -> None: result["flow_id"], user_input={"bindkey": "5b51a7c91cde6707c9ef18dada143a58"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "get_encryption_key_4_5" assert result2["errors"]["bindkey"] == "decryption_failed" @@ -1066,7 +1066,7 @@ async def test_async_step_reauth_v4_wrong_key(hass: HomeAssistant) -> None: result["flow_id"], user_input={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -1094,5 +1094,5 @@ async def test_async_step_reauth_abort_early(hass: HomeAssistant) -> None: data=entry.data | {"device": device}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" diff --git a/tests/components/xiaomi_miio/test_config_flow.py b/tests/components/xiaomi_miio/test_config_flow.py index 7645f67732e..87f3fb383c3 100644 --- a/tests/components/xiaomi_miio/test_config_flow.py +++ b/tests/components/xiaomi_miio/test_config_flow.py @@ -8,11 +8,12 @@ from micloud.micloudexception import MiCloudAccessDenied from miio import DeviceException import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.xiaomi_miio import const from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_MAC, CONF_MODEL, CONF_TOKEN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import TEST_MAC @@ -897,7 +898,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -907,7 +908,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { const.CONF_CLOUD_SUBDEVICES: True, } @@ -937,7 +938,7 @@ async def test_options_flow_incomplete(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -946,7 +947,7 @@ async def test_options_flow_incomplete(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {"base": "cloud_credentials_incomplete"} diff --git a/tests/components/yale_smart_alarm/test_config_flow.py b/tests/components/yale_smart_alarm/test_config_flow.py index 5eed34a2423..4ef201d2122 100644 --- a/tests/components/yale_smart_alarm/test_config_flow.py +++ b/tests/components/yale_smart_alarm/test_config_flow.py @@ -21,7 +21,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -43,7 +43,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "test-username" assert result2["data"] == { "username": "test-username", @@ -85,7 +85,7 @@ async def test_form_invalid_auth( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": p_error} with ( @@ -107,7 +107,7 @@ async def test_form_invalid_auth( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "test-username" assert result2["data"] == { "username": "test-username", @@ -142,7 +142,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: data=entry.data, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -163,7 +163,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == { "username": "test-username", @@ -226,7 +226,7 @@ async def test_reauth_flow_error( await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": p_error} with ( @@ -248,7 +248,7 @@ async def test_reauth_flow_error( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == { "username": "test-username", @@ -288,7 +288,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -296,5 +296,5 @@ async def test_options_flow(hass: HomeAssistant) -> None: user_input={"lock_code_digits": 6}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {"lock_code_digits": 6} diff --git a/tests/components/yalexs_ble/test_config_flow.py b/tests/components/yalexs_ble/test_config_flow.py index 34ffc55ac3f..15552fdec5f 100644 --- a/tests/components/yalexs_ble/test_config_flow.py +++ b/tests/components/yalexs_ble/test_config_flow.py @@ -57,7 +57,7 @@ async def test_user_step_success(hass: HomeAssistant, slot: int) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -80,7 +80,7 @@ async def test_user_step_success(hass: HomeAssistant, slot: int) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name assert result2["data"] == { CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, @@ -101,7 +101,7 @@ async def test_user_step_no_devices_found(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -125,7 +125,7 @@ async def test_user_step_no_new_devices_found(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -138,7 +138,7 @@ async def test_user_step_invalid_keys(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -150,7 +150,7 @@ async def test_user_step_invalid_keys(hass: HomeAssistant) -> None: CONF_SLOT: 66, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {CONF_KEY: "invalid_key_format"} @@ -162,7 +162,7 @@ async def test_user_step_invalid_keys(hass: HomeAssistant) -> None: CONF_SLOT: 66, }, ) - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "user" assert result3["errors"] == {CONF_KEY: "invalid_key_format"} @@ -174,7 +174,7 @@ async def test_user_step_invalid_keys(hass: HomeAssistant) -> None: CONF_SLOT: 999, }, ) - assert result4["type"] == FlowResultType.FORM + assert result4["type"] is FlowResultType.FORM assert result4["step_id"] == "user" assert result4["errors"] == {CONF_SLOT: "invalid_key_index"} @@ -197,7 +197,7 @@ async def test_user_step_invalid_keys(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result5["type"] == FlowResultType.CREATE_ENTRY + assert result5["type"] is FlowResultType.CREATE_ENTRY assert result5["title"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name assert result5["data"] == { CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, @@ -218,7 +218,7 @@ async def test_user_step_cannot_connect(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -236,7 +236,7 @@ async def test_user_step_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -259,7 +259,7 @@ async def test_user_step_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name assert result3["data"] == { CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, @@ -280,7 +280,7 @@ async def test_user_step_auth_exception(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -298,7 +298,7 @@ async def test_user_step_auth_exception(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {CONF_KEY: "invalid_auth"} @@ -321,7 +321,7 @@ async def test_user_step_auth_exception(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name assert result3["data"] == { CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, @@ -342,7 +342,7 @@ async def test_user_step_unknown_exception(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -360,7 +360,7 @@ async def test_user_step_unknown_exception(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "unknown"} @@ -383,7 +383,7 @@ async def test_user_step_unknown_exception(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name assert result3["data"] == { CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, @@ -402,7 +402,7 @@ async def test_bluetooth_step_success(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=YALE_ACCESS_LOCK_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -425,7 +425,7 @@ async def test_bluetooth_step_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name assert result2["data"] == { CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, @@ -454,7 +454,7 @@ async def test_integration_discovery_success(hass: HomeAssistant) -> None: "serial": "M1XXX012LU", }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "integration_discovery_confirm" assert result["errors"] is None @@ -468,7 +468,7 @@ async def test_integration_discovery_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Front Door" assert result2["data"] == { CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, @@ -497,7 +497,7 @@ async def test_integration_discovery_device_not_found(hass: HomeAssistant) -> No "serial": "M1XXX012LU", }, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -510,7 +510,7 @@ async def test_integration_discovery_takes_precedence_over_bluetooth( context={"source": config_entries.SOURCE_BLUETOOTH}, data=YALE_ACCESS_LOCK_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} flows = [ @@ -538,7 +538,7 @@ async def test_integration_discovery_takes_precedence_over_bluetooth( }, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "integration_discovery_confirm" assert result["errors"] is None @@ -563,7 +563,7 @@ async def test_integration_discovery_takes_precedence_over_bluetooth( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Front Door" assert result2["data"] == { CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, @@ -619,7 +619,7 @@ async def test_integration_discovery_updates_key_unique_local_name( }, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_KEY] == "2fd51b8621c6a139eaffbedcb846b60f" assert entry.data[CONF_SLOT] == 66 @@ -658,7 +658,7 @@ async def test_integration_discovery_updates_key_without_unique_local_name( }, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_KEY] == "2fd51b8621c6a139eaffbedcb846b60f" assert entry.data[CONF_SLOT] == 66 @@ -707,7 +707,7 @@ async def test_integration_discovery_updates_key_duplicate_local_name( }, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_KEY] == "2fd51b8621c6a139eaffbedcb846b60f" assert entry.data[CONF_SLOT] == 66 @@ -725,7 +725,7 @@ async def test_integration_discovery_takes_precedence_over_bluetooth_uuid_addres context={"source": config_entries.SOURCE_BLUETOOTH}, data=LOCK_DISCOVERY_INFO_UUID_ADDRESS, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} flows = [ @@ -753,7 +753,7 @@ async def test_integration_discovery_takes_precedence_over_bluetooth_uuid_addres }, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "integration_discovery_confirm" assert result["errors"] is None @@ -778,7 +778,7 @@ async def test_integration_discovery_takes_precedence_over_bluetooth_uuid_addres ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Front Door" assert result2["data"] == { CONF_LOCAL_NAME: LOCK_DISCOVERY_INFO_UUID_ADDRESS.name, @@ -805,7 +805,7 @@ async def test_integration_discovery_takes_precedence_over_bluetooth_non_unique_ context={"source": config_entries.SOURCE_BLUETOOTH}, data=OLD_FIRMWARE_LOCK_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} flows = [ @@ -833,7 +833,7 @@ async def test_integration_discovery_takes_precedence_over_bluetooth_non_unique_ }, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "integration_discovery_confirm" assert result["errors"] is None @@ -863,7 +863,7 @@ async def test_user_is_setting_up_lock_and_discovery_happens_in_the_middle( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -912,13 +912,13 @@ async def test_user_is_setting_up_lock_and_discovery_happens_in_the_middle( }, ) await hass.async_block_till_done() - assert discovery_result["type"] == FlowResultType.ABORT + assert discovery_result["type"] is FlowResultType.ABORT assert discovery_result["reason"] == "already_in_progress" user_flow_event.set() user_flow_result = await user_flow_task - assert user_flow_result["type"] == FlowResultType.CREATE_ENTRY + assert user_flow_result["type"] is FlowResultType.CREATE_ENTRY assert user_flow_result["title"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name assert user_flow_result["data"] == { CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, @@ -950,7 +950,7 @@ async def test_reauth(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_REAUTH, "entry_id": entry.entry_id}, data=entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_validate" with patch( @@ -966,7 +966,7 @@ async def test_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "reauth_validate" assert result2["errors"] == {"base": "no_longer_in_range"} @@ -992,7 +992,7 @@ async def test_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 @@ -1022,7 +1022,7 @@ async def test_options(hass: HomeAssistant) -> None: entry.entry_id, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "device_options" with patch( @@ -1037,6 +1037,6 @@ async def test_options(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert entry.options == {CONF_ALWAYS_CONNECTED: True} assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/yamaha_musiccast/test_config_flow.py b/tests/components/yamaha_musiccast/test_config_flow.py index 9740bd70a87..1c51b315a5a 100644 --- a/tests/components/yamaha_musiccast/test_config_flow.py +++ b/tests/components/yamaha_musiccast/test_config_flow.py @@ -5,12 +5,13 @@ from unittest.mock import patch from aiomusiccast import MusicCastConnectionException import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.components.yamaha_musiccast.const import DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -135,13 +136,13 @@ async def test_user_input_device_not_found( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "none"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -153,13 +154,13 @@ async def test_user_input_non_yamaha_device_found( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "127.0.0.1"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "no_musiccast_device"} @@ -183,7 +184,7 @@ async def test_user_input_device_already_existing( {"host": "192.168.188.18"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -195,13 +196,13 @@ async def test_user_input_unknown_error( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "127.0.0.1"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -216,13 +217,13 @@ async def test_user_input_device_found( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "127.0.0.1"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert isinstance(result2["result"], ConfigEntry) assert result2["data"] == { "host": "127.0.0.1", @@ -242,13 +243,13 @@ async def test_user_input_device_found_no_ssdp( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "127.0.0.1"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert isinstance(result2["result"], ConfigEntry) assert result2["data"] == { "host": "127.0.0.1", @@ -278,7 +279,7 @@ async def test_ssdp_discovery_failed( ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "yxc_control_url_missing" @@ -300,7 +301,7 @@ async def test_ssdp_discovery_successful_add_device( ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "confirm" @@ -309,7 +310,7 @@ async def test_ssdp_discovery_successful_add_device( {}, ) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert isinstance(result2["result"], ConfigEntry) assert result2["data"] == { "host": "127.0.0.1", @@ -341,7 +342,7 @@ async def test_ssdp_discovery_existing_device_update( }, ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_entry.data[CONF_HOST] == "127.0.0.1" assert mock_entry.data["upnp_description"] == "http://127.0.0.1/desc.xml" diff --git a/tests/components/yardian/test_config_flow.py b/tests/components/yardian/test_config_flow.py index c93f48d1c48..1630286733f 100644 --- a/tests/components/yardian/test_config_flow.py +++ b/tests/components/yardian/test_config_flow.py @@ -18,7 +18,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -34,7 +34,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == PRODUCT_NAME assert result2["data"] == { "host": "fake_host", @@ -65,7 +65,7 @@ async def test_form_invalid_auth( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} # Should be recoverable after hits error @@ -82,7 +82,7 @@ async def test_form_invalid_auth( ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == PRODUCT_NAME assert result3["data"] == { "host": "fake_host", @@ -113,7 +113,7 @@ async def test_form_cannot_connect( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} # Should be recoverable after hits error @@ -130,7 +130,7 @@ async def test_form_cannot_connect( ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == PRODUCT_NAME assert result3["data"] == { "host": "fake_host", @@ -161,7 +161,7 @@ async def test_form_uncategorized_error( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} # Should be recoverable after hits error @@ -178,7 +178,7 @@ async def test_form_uncategorized_error( ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == PRODUCT_NAME assert result3["data"] == { "host": "fake_host", diff --git a/tests/components/yeelight/test_config_flow.py b/tests/components/yeelight/test_config_flow.py index 41d60c8652a..06ad341b739 100644 --- a/tests/components/yeelight/test_config_flow.py +++ b/tests/components/yeelight/test_config_flow.py @@ -500,7 +500,7 @@ async def test_discovered_by_homekit_and_dhcp(hass: HomeAssistant) -> None: ), ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -516,7 +516,7 @@ async def test_discovered_by_homekit_and_dhcp(hass: HomeAssistant) -> None: ), ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_in_progress" with ( @@ -532,7 +532,7 @@ async def test_discovered_by_homekit_and_dhcp(hass: HomeAssistant) -> None: ), ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "already_in_progress" with ( @@ -549,7 +549,7 @@ async def test_discovered_by_homekit_and_dhcp(hass: HomeAssistant) -> None: ), ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "cannot_connect" @@ -590,7 +590,7 @@ async def test_discovered_by_dhcp_or_homekit(hass: HomeAssistant, source, data) ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -623,7 +623,7 @@ async def test_discovered_by_dhcp_or_homekit(hass: HomeAssistant, source, data) DOMAIN, context={"source": source}, data=data ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "already_configured" @@ -665,7 +665,7 @@ async def test_discovered_by_dhcp_or_homekit_failed_to_get_id( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": source}, data=data ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -683,7 +683,7 @@ async def test_discovered_ssdp(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -717,7 +717,7 @@ async def test_discovered_ssdp(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -737,7 +737,7 @@ async def test_discovered_zeroconf(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -773,7 +773,7 @@ async def test_discovered_zeroconf(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" mocked_bulb = _mocked_bulb() @@ -789,7 +789,7 @@ async def test_discovered_zeroconf(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -813,7 +813,7 @@ async def test_discovery_updates_ip(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == IP_ADDRESS @@ -844,7 +844,7 @@ async def test_discovery_updates_ip_no_reload_setup_in_progress( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == IP_ADDRESS assert len(mock_setup_entry.mock_calls) == 0 @@ -868,7 +868,7 @@ async def test_discovery_adds_missing_ip_id_only(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == IP_ADDRESS diff --git a/tests/components/yolink/test_config_flow.py b/tests/components/yolink/test_config_flow.py index f7abda0bc4b..db12cecf296 100644 --- a/tests/components/yolink/test_config_flow.py +++ b/tests/components/yolink/test_config_flow.py @@ -5,9 +5,10 @@ from unittest.mock import patch from yolink.const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, setup from homeassistant.components import application_credentials from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow from tests.common import MockConfigEntry @@ -24,7 +25,7 @@ async def test_abort_if_no_configuration(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "missing_credentials" @@ -34,7 +35,7 @@ async def test_abort_if_existing_entry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -65,7 +66,7 @@ async def test_full_flow( "redirect_uri": "https://example.com/auth/external/callback", }, ) - assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" @@ -136,7 +137,7 @@ async def test_abort_if_authorization_timeout( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "authorize_url_timeout" @@ -217,6 +218,6 @@ async def test_reauthentication( assert token_data["refresh_token"] == "mock-refresh-token" assert token_data["type"] == "Bearer" assert token_data["expires_in"] == 60 - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(mock_setup.mock_calls) == 1 diff --git a/tests/components/youtube/test_config_flow.py b/tests/components/youtube/test_config_flow.py index c8857626384..30800e5399a 100644 --- a/tests/components/youtube/test_config_flow.py +++ b/tests/components/youtube/test_config_flow.py @@ -65,7 +65,7 @@ async def test_full_flow( ), ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "channels" result = await hass.config_entries.flow.async_configure( @@ -122,7 +122,7 @@ async def test_flow_abort_without_channel( ), ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_channel" @@ -163,7 +163,7 @@ async def test_flow_abort_without_subscriptions( ), ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_subscriptions" @@ -203,7 +203,7 @@ async def test_flow_http_error( ), ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "access_not_configured" assert result["description_placeholders"]["message"] == ( "YouTube Data API v3 has not been used in project 0 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/youtube.googleapis.com/overview?project=0 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry." @@ -345,7 +345,7 @@ async def test_flow_exception( "homeassistant.components.youtube.config_flow.YouTube", side_effect=Exception ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -362,7 +362,7 @@ async def test_options_flow( result = await hass.config_entries.options.async_init(entry.entry_id) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -371,5 +371,5 @@ async def test_options_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_CHANNELS: ["UC_x5XG1OV2P6uZZ5FSM9Ttw"]} diff --git a/tests/components/zamg/test_config_flow.py b/tests/components/zamg/test_config_flow.py index bc95afa7936..f67eda67a49 100644 --- a/tests/components/zamg/test_config_flow.py +++ b/tests/components/zamg/test_config_flow.py @@ -23,14 +23,14 @@ async def test_full_user_flow_implementation( context={"source": SOURCE_USER}, ) assert result.get("step_id") == "user" - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM LOGGER.debug(result) assert result.get("data_schema") != "" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_STATION_ID: TEST_STATION_ID}, ) - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert "data" in result assert result["data"][CONF_STATION_ID] == TEST_STATION_ID assert "result" in result @@ -48,7 +48,7 @@ async def test_error_closest_station( DOMAIN, context={"source": SOURCE_USER}, ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "cannot_connect" @@ -63,7 +63,7 @@ async def test_error_update( context={"source": SOURCE_USER}, ) assert result.get("step_id") == "user" - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM LOGGER.debug(result) assert result.get("data_schema") != "" mock_zamg.update.side_effect = ZamgApiError @@ -71,7 +71,7 @@ async def test_error_update( result["flow_id"], user_input={CONF_STATION_ID: TEST_STATION_ID}, ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "cannot_connect" @@ -87,12 +87,12 @@ async def test_user_flow_duplicate( ) assert result.get("step_id") == "user" - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_STATION_ID: TEST_STATION_ID}, ) - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert "data" in result assert result["data"][CONF_STATION_ID] == TEST_STATION_ID assert "result" in result @@ -103,10 +103,10 @@ async def test_user_flow_duplicate( context={"source": SOURCE_USER}, ) assert result.get("step_id") == "user" - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_STATION_ID: TEST_STATION_ID}, ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" diff --git a/tests/components/zeversolar/test_config_flow.py b/tests/components/zeversolar/test_config_flow.py index 0bfa5ad547d..53d72f743cb 100644 --- a/tests/components/zeversolar/test_config_flow.py +++ b/tests/components/zeversolar/test_config_flow.py @@ -23,7 +23,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None await _set_up_zeversolar(hass=hass, flow_id=result["flow_id"]) @@ -71,7 +71,7 @@ async def test_form_errors( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == errors await _set_up_zeversolar(hass=hass, flow_id=result["flow_id"]) @@ -90,7 +90,7 @@ async def test_abort_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") is None assert "flow_id" in result @@ -110,7 +110,7 @@ async def test_abort_already_configured(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "already_configured" assert len(mock_setup_entry.mock_calls) == 0 @@ -134,7 +134,7 @@ async def _set_up_zeversolar(hass: HomeAssistant, flow_id: str) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Zeversolar" assert result2["data"] == { CONF_HOST: "test_ip", diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index bbfca1b1a13..29b78ce450d 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -169,7 +169,7 @@ async def test_zeroconf_discovery_znp(hass: HomeAssistant) -> None: result1["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.MENU + assert result2["type"] is FlowResultType.MENU assert result2["step_id"] == "choose_formation_strategy" result3 = await hass.config_entries.flow.async_configure( @@ -178,7 +178,7 @@ async def test_zeroconf_discovery_znp(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "socket://192.168.1.200:6638" assert result3["data"] == { CONF_DEVICE: { @@ -226,7 +226,7 @@ async def test_zigate_via_zeroconf(setup_entry_mock, hass: HomeAssistant) -> Non result1["flow_id"], user_input={} ) - assert result3["type"] == FlowResultType.MENU + assert result3["type"] is FlowResultType.MENU assert result3["step_id"] == "choose_formation_strategy" result4 = await hass.config_entries.flow.async_configure( @@ -235,7 +235,7 @@ async def test_zigate_via_zeroconf(setup_entry_mock, hass: HomeAssistant) -> Non ) await hass.async_block_till_done() - assert result4["type"] == FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == "socket://192.168.1.200:1234" assert result4["data"] == { CONF_DEVICE: { @@ -276,7 +276,7 @@ async def test_efr32_via_zeroconf(hass: HomeAssistant) -> None: result1["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.MENU + assert result2["type"] is FlowResultType.MENU assert result2["step_id"] == "choose_formation_strategy" result3 = await hass.config_entries.flow.async_configure( @@ -285,7 +285,7 @@ async def test_efr32_via_zeroconf(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "socket://192.168.1.200:1234" assert result3["data"] == { CONF_DEVICE: { @@ -321,7 +321,7 @@ async def test_discovery_via_zeroconf_ip_change_ignored(hass: HomeAssistant) -> DOMAIN, context={"source": SOURCE_ZEROCONF}, data=service_info ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_DEVICE] == { CONF_DEVICE_PATH: "socket://192.168.1.22:6638", @@ -355,7 +355,7 @@ async def test_discovery_confirm_final_abort_if_entries(hass: HomeAssistant) -> ) # Config will fail - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -375,7 +375,7 @@ async def test_discovery_via_usb(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "confirm" result2 = await hass.config_entries.flow.async_configure( @@ -383,7 +383,7 @@ async def test_discovery_via_usb(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.MENU + assert result2["type"] is FlowResultType.MENU assert result2["step_id"] == "choose_formation_strategy" with patch("homeassistant.components.zha.async_setup_entry", return_value=True): @@ -393,7 +393,7 @@ async def test_discovery_via_usb(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "zigbee radio" assert result3["data"] == { "device": { @@ -420,7 +420,7 @@ async def test_zigate_discovery_via_usb(probe_mock, hass: HomeAssistant) -> None DOMAIN, context={"source": SOURCE_USB}, data=discovery_info ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result2 = await hass.config_entries.flow.async_configure( @@ -433,7 +433,7 @@ async def test_zigate_discovery_via_usb(probe_mock, hass: HomeAssistant) -> None ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.MENU + assert result3["type"] is FlowResultType.MENU assert result3["step_id"] == "choose_formation_strategy" with patch("homeassistant.components.zha.async_setup_entry", return_value=True): @@ -443,7 +443,7 @@ async def test_zigate_discovery_via_usb(probe_mock, hass: HomeAssistant) -> None ) await hass.async_block_till_done() - assert result4["type"] == FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == "zigate radio" assert result4["data"] == { "device": { @@ -473,7 +473,7 @@ async def test_discovery_via_usb_no_radio(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USB}, data=discovery_info ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" with patch("homeassistant.components.zha.async_setup_entry", return_value=True): @@ -482,7 +482,7 @@ async def test_discovery_via_usb_no_radio(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "usb_probe_failed" @@ -507,7 +507,7 @@ async def test_discovery_via_usb_already_setup(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -541,7 +541,7 @@ async def test_discovery_via_usb_path_does_not_change(hass: HomeAssistant) -> No ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_DEVICE] == { CONF_DEVICE_PATH: "/dev/ttyUSB1", @@ -626,7 +626,7 @@ async def test_discovery_via_usb_deconz_ignored(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" @@ -654,7 +654,7 @@ async def test_discovery_via_usb_zha_ignored_updates(hass: HomeAssistant) -> Non ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_DEVICE] == { CONF_DEVICE_PATH: "/dev/ttyZIGBEE", @@ -706,7 +706,7 @@ async def test_user_flow(hass: HomeAssistant) -> None: zigpy.config.CONF_DEVICE_PATH: port_select, }, ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "choose_formation_strategy" with patch("homeassistant.components.zha.async_setup_entry", return_value=True): @@ -716,7 +716,7 @@ async def test_user_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"].startswith(port.description) assert result2["data"] == { "device": { @@ -749,7 +749,7 @@ async def test_user_flow_not_detected(hass: HomeAssistant) -> None: }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual_pick_radio_type" @@ -761,7 +761,7 @@ async def test_user_flow_show_form(hass: HomeAssistant) -> None: context={CONF_SOURCE: SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "choose_serial_port" @@ -773,7 +773,7 @@ async def test_user_flow_show_manual(hass: HomeAssistant) -> None: context={CONF_SOURCE: SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual_pick_radio_type" @@ -785,7 +785,7 @@ async def test_user_flow_manual(hass: HomeAssistant) -> None: context={CONF_SOURCE: SOURCE_USER}, data={zigpy.config.CONF_DEVICE_PATH: config_flow.CONF_MANUAL_PATH}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual_pick_radio_type" @@ -798,7 +798,7 @@ async def test_pick_radio_flow(hass: HomeAssistant, radio_type) -> None: context={CONF_SOURCE: "manual_pick_radio_type"}, data={CONF_RADIO_TYPE: radio_type}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual_port_config" @@ -885,7 +885,7 @@ async def test_user_port_config_fail(probe_mock, hass: HomeAssistant) -> None: result["flow_id"], user_input={zigpy.config.CONF_DEVICE_PATH: "/dev/ttyUSB33"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual_port_config" assert result["errors"]["base"] == "cannot_connect" assert probe_mock.await_count == 1 @@ -907,7 +907,7 @@ async def test_user_port_config(probe_mock, hass: HomeAssistant) -> None: user_input={zigpy.config.CONF_DEVICE_PATH: "/dev/ttyUSB33"}, ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "choose_formation_strategy" result2 = await hass.config_entries.flow.async_configure( @@ -946,7 +946,7 @@ async def test_hardware(onboarded, hass: HomeAssistant) -> None: if onboarded: # Confirm discovery - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "confirm" result2 = await hass.config_entries.flow.async_configure( @@ -957,7 +957,7 @@ async def test_hardware(onboarded, hass: HomeAssistant) -> None: # No need to confirm result2 = result1 - assert result2["type"] == FlowResultType.MENU + assert result2["type"] is FlowResultType.MENU assert result2["step_id"] == "choose_formation_strategy" result3 = await hass.config_entries.flow.async_configure( @@ -997,7 +997,7 @@ async def test_hardware_already_setup(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_HARDWARE}, data=data ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -1011,7 +1011,7 @@ async def test_hardware_invalid_data(hass: HomeAssistant, data) -> None: DOMAIN, context={"source": config_entries.SOURCE_HARDWARE}, data=data ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_hardware_data" @@ -1056,7 +1056,7 @@ def pick_radio(hass): }, ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "choose_formation_strategy" return result, port @@ -1096,7 +1096,7 @@ async def test_formation_strategy_form_new_network( # A new network will be formed mock_app.form_network.assert_called_once() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY async def test_formation_strategy_form_initial_network( @@ -1115,7 +1115,7 @@ async def test_formation_strategy_form_initial_network( # A new network will be formed mock_app.form_network.assert_called_once() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY @patch(f"zigpy_znp.{PROBE_FUNCTION_PATH}", AsyncMock(return_value=True)) @@ -1143,7 +1143,7 @@ async def test_onboarding_auto_formation_new_hardware( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "zigbee radio" assert result["data"] == { "device": { @@ -1170,7 +1170,7 @@ async def test_formation_strategy_reuse_settings( # Nothing will be written when settings are reused mock_app.write_network_info.assert_not_called() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY @patch("homeassistant.components.zha.config_flow.process_uploaded_file") @@ -1200,7 +1200,7 @@ async def test_formation_strategy_restore_manual_backup_non_ezsp( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "upload_manual_backup" with patch( @@ -1215,7 +1215,7 @@ async def test_formation_strategy_restore_manual_backup_non_ezsp( mock_app.backups.restore_backup.assert_called_once() allow_overwrite_ieee_mock.assert_not_called() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["data"][CONF_RADIO_TYPE] == "znp" @@ -1232,7 +1232,7 @@ async def test_formation_strategy_restore_manual_backup_overwrite_ieee_ezsp( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "upload_manual_backup" with patch( @@ -1244,7 +1244,7 @@ async def test_formation_strategy_restore_manual_backup_overwrite_ieee_ezsp( user_input={config_flow.UPLOADED_BACKUP_FILE: str(uuid.uuid4())}, ) - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "maybe_confirm_ezsp_restore" result4 = await hass.config_entries.flow.async_configure( @@ -1255,7 +1255,7 @@ async def test_formation_strategy_restore_manual_backup_overwrite_ieee_ezsp( allow_overwrite_ieee_mock.assert_called_once() mock_app.backups.restore_backup.assert_called_once() - assert result4["type"] == FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["data"][CONF_RADIO_TYPE] == "ezsp" @@ -1272,7 +1272,7 @@ async def test_formation_strategy_restore_manual_backup_ezsp( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "upload_manual_backup" backup = zigpy.backups.NetworkBackup() @@ -1286,7 +1286,7 @@ async def test_formation_strategy_restore_manual_backup_ezsp( user_input={config_flow.UPLOADED_BACKUP_FILE: str(uuid.uuid4())}, ) - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "maybe_confirm_ezsp_restore" result4 = await hass.config_entries.flow.async_configure( @@ -1297,7 +1297,7 @@ async def test_formation_strategy_restore_manual_backup_ezsp( allow_overwrite_ieee_mock.assert_not_called() mock_app.backups.restore_backup.assert_called_once_with(backup) - assert result4["type"] == FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["data"][CONF_RADIO_TYPE] == "ezsp" @@ -1313,7 +1313,7 @@ async def test_formation_strategy_restore_manual_backup_invalid_upload( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "upload_manual_backup" with patch( @@ -1327,7 +1327,7 @@ async def test_formation_strategy_restore_manual_backup_invalid_upload( mock_app.backups.restore_backup.assert_not_called() - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "upload_manual_backup" assert result3["errors"]["base"] == "invalid_backup_json" @@ -1372,7 +1372,7 @@ async def test_formation_strategy_restore_automatic_backup_ezsp( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "choose_automatic_backup" result3 = await hass.config_entries.flow.async_configure( @@ -1382,7 +1382,7 @@ async def test_formation_strategy_restore_automatic_backup_ezsp( }, ) - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "maybe_confirm_ezsp_restore" result4 = await hass.config_entries.flow.async_configure( @@ -1392,7 +1392,7 @@ async def test_formation_strategy_restore_automatic_backup_ezsp( mock_app.backups.restore_backup.assert_called_once() - assert result4["type"] == FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["data"][CONF_RADIO_TYPE] == "ezsp" @@ -1428,7 +1428,7 @@ async def test_formation_strategy_restore_automatic_backup_non_ezsp( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "choose_automatic_backup" # We don't prompt for overwriting the IEEE address, since only EZSP needs this @@ -1450,7 +1450,7 @@ async def test_formation_strategy_restore_automatic_backup_non_ezsp( mock_app.backups.restore_backup.assert_called_once_with(backup) - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["data"][CONF_RADIO_TYPE] == "znp" @@ -1480,7 +1480,7 @@ async def test_ezsp_restore_without_settings_change_ieee( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "upload_manual_backup" with patch( @@ -1496,7 +1496,7 @@ async def test_ezsp_restore_without_settings_change_ieee( allow_overwrite_ieee_mock.assert_not_called() mock_app.backups.restore_backup.assert_called_once_with(backup, create_new=False) - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["data"][CONF_RADIO_TYPE] == "ezsp" @@ -1609,7 +1609,7 @@ async def test_options_flow_defaults( ) await hass.async_block_till_done() - assert result6["type"] == FlowResultType.CREATE_ENTRY + assert result6["type"] is FlowResultType.CREATE_ENTRY assert result6["data"] == {} # The updated entry contains correct settings @@ -1884,7 +1884,7 @@ async def test_probe_wrong_firmware_installed(hass: HomeAssistant) -> None: }, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "wrong_firmware_installed" @@ -1906,7 +1906,7 @@ async def test_discovery_wrong_firmware_installed(hass: HomeAssistant) -> None: data={}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "wrong_firmware_installed" diff --git a/tests/components/zodiac/test_config_flow.py b/tests/components/zodiac/test_config_flow.py index 15e8bb04ef6..e027229def9 100644 --- a/tests/components/zodiac/test_config_flow.py +++ b/tests/components/zodiac/test_config_flow.py @@ -18,7 +18,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" with patch( @@ -30,7 +30,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: user_input={}, ) - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == "Zodiac" assert result.get("data") == {} assert result.get("options") == {} @@ -51,5 +51,5 @@ async def test_single_instance_allowed( DOMAIN, context={"source": source} ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" diff --git a/tests/components/zwave_me/test_config_flow.py b/tests/components/zwave_me/test_config_flow.py index 52d5e8fce6f..a71df8751b6 100644 --- a/tests/components/zwave_me/test_config_flow.py +++ b/tests/components/zwave_me/test_config_flow.py @@ -42,7 +42,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -53,7 +53,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "ws://192.168.1.14" assert result2["data"] == { "url": "ws://192.168.1.14", @@ -79,7 +79,7 @@ async def test_zeroconf(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_ZEROCONF}, data=MOCK_ZEROCONF_DATA, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( @@ -90,7 +90,7 @@ async def test_zeroconf(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "ws://192.168.1.14" assert result2["data"] == { "url": "ws://192.168.1.14", @@ -107,7 +107,7 @@ async def test_error_handling_zeroconf(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_ZEROCONF}, data=MOCK_ZEROCONF_DATA, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_valid_uuid_set" @@ -117,7 +117,7 @@ async def test_handle_error_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -148,7 +148,7 @@ async def test_duplicate_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -157,7 +157,7 @@ async def test_duplicate_user(hass: HomeAssistant) -> None: "token": "test-token", }, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -183,5 +183,5 @@ async def test_duplicate_zeroconf(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_ZEROCONF}, data=MOCK_ZEROCONF_DATA, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" From 3875533f9576ad3b1298254cb3575c7bd7ed26a7 Mon Sep 17 00:00:00 2001 From: Dos Moonen Date: Tue, 2 Apr 2024 23:16:39 +0200 Subject: [PATCH 189/967] Bump solax to 3.1.0 (#114617) 0.3.2 was succeeded by 0.3.4. 0.3.3 was yanked 0.3.4 was succeeded by 3.0.5. 3.0.5 is succeeded by 3.1.0. --- homeassistant/components/solax/manifest.json | 2 +- homeassistant/components/solax/sensor.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/solax/test_config_flow.py | 6 +++++- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/solax/manifest.json b/homeassistant/components/solax/manifest.json index d3f677fa894..be81dd65e89 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==0.3.2"] + "requirements": ["solax==3.1.0"] } diff --git a/homeassistant/components/solax/sensor.py b/homeassistant/components/solax/sensor.py index ccd1a8c96c9..a8c09bdc880 100644 --- a/homeassistant/components/solax/sensor.py +++ b/homeassistant/components/solax/sensor.py @@ -6,7 +6,7 @@ import asyncio from datetime import timedelta from solax import RealTimeAPI -from solax.discovery import InverterError +from solax.inverter import InverterError from solax.units import Units from homeassistant.components.sensor import ( diff --git a/requirements_all.txt b/requirements_all.txt index 6b71c1e788d..8a35cd7a2ee 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2581,7 +2581,7 @@ solaredge-local==0.2.3 solaredge==0.0.2 # homeassistant.components.solax -solax==0.3.2 +solax==3.1.0 # homeassistant.components.somfy_mylink somfy-mylink-synergy==1.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6ed14716033..903f14831b3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1985,7 +1985,7 @@ soco==0.30.2 solaredge==0.0.2 # homeassistant.components.solax -solax==0.3.2 +solax==3.1.0 # homeassistant.components.somfy_mylink somfy-mylink-synergy==1.0.6 diff --git a/tests/components/solax/test_config_flow.py b/tests/components/solax/test_config_flow.py index c671fe39cec..61bd9003439 100644 --- a/tests/components/solax/test_config_flow.py +++ b/tests/components/solax/test_config_flow.py @@ -18,7 +18,11 @@ def __mock_real_time_api_success(): def __mock_get_data(): return InverterResponse( - data=None, serial_number="ABCDEFGHIJ", version="2.034.06", type=4 + data=None, + dongle_serial_number="ABCDEFGHIJ", + version="2.034.06", + type=4, + inverter_serial_number="XXXXXXX", ) From 5d500cb74beb986153df71aea72018354be4df32 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 2 Apr 2024 23:21:42 +0200 Subject: [PATCH 190/967] Use is in enum comparison in config flow tests K-O (#114672) --- .../kaleidescape/test_config_flow.py | 18 +-- .../keenetic_ndms2/test_config_flow.py | 29 ++-- tests/components/kegtron/test_config_flow.py | 30 ++-- .../keymitt_ble/test_config_flow.py | 22 +-- .../kitchen_sink/test_config_flow.py | 9 +- tests/components/kmtronic/test_config_flow.py | 7 +- tests/components/knx/test_config_flow.py | 134 +++++++++--------- .../lacrosse_view/test_config_flow.py | 26 ++-- .../components/lamarzocco/test_config_flow.py | 46 +++--- tests/components/lametric/test_config_flow.py | 62 ++++---- .../landisgyr_heat_meter/test_config_flow.py | 22 +-- tests/components/lastfm/test_config_flow.py | 34 ++--- .../launch_library/test_config_flow.py | 8 +- .../components/laundrify/test_config_flow.py | 18 +-- tests/components/lcn/test_config_flow.py | 9 +- .../components/ld2410_ble/test_config_flow.py | 24 ++-- tests/components/leaone/test_config_flow.py | 12 +- tests/components/led_ble/test_config_flow.py | 26 ++-- tests/components/lidarr/test_config_flow.py | 20 +-- tests/components/lifx/test_config_flow.py | 14 +- .../linear_garage_door/test_config_flow.py | 12 +- tests/components/litejet/test_config_flow.py | 7 +- .../litterrobot/test_config_flow.py | 10 +- tests/components/livisi/test_config_flow.py | 8 +- .../local_calendar/test_config_flow.py | 8 +- tests/components/local_ip/test_config_flow.py | 8 +- .../components/local_todo/test_config_flow.py | 8 +- .../logi_circle/test_config_flow.py | 25 ++-- tests/components/lookin/test_config_flow.py | 14 +- tests/components/loqed/test_config_flow.py | 20 +-- .../components/luftdaten/test_config_flow.py | 20 +-- tests/components/lupusec/test_config_flow.py | 20 +-- tests/components/lutron/test_config_flow.py | 26 ++-- .../lutron_caseta/test_config_flow.py | 11 +- tests/components/lyric/test_config_flow.py | 9 +- tests/components/matter/test_config_flow.py | 108 +++++++------- tests/components/meater/test_config_flow.py | 13 +- .../components/medcom_ble/test_config_flow.py | 26 ++-- tests/components/melcloud/test_config_flow.py | 12 +- tests/components/melnor/test_config_flow.py | 12 +- .../met_eireann/test_config_flow.py | 9 +- .../meteo_france/test_config_flow.py | 14 +- .../meteoclimatic/test_config_flow.py | 10 +- .../components/microbees/test_config_flow.py | 20 +-- tests/components/mikrotik/test_config_flow.py | 15 +- tests/components/mill/test_config_flow.py | 30 ++-- tests/components/min_max/test_config_flow.py | 8 +- .../minecraft_server/test_config_flow.py | 16 +-- tests/components/mjpeg/test_config_flow.py | 32 ++--- tests/components/moat/test_config_flow.py | 30 ++-- .../modem_callerid/test_config_flow.py | 22 +-- .../modern_forms/test_config_flow.py | 18 +-- .../moehlenhoff_alpha2/test_config_flow.py | 10 +- .../components/monoprice/test_config_flow.py | 7 +- tests/components/moon/test_config_flow.py | 6 +- tests/components/mopeka/test_config_flow.py | 30 ++-- .../motion_blinds/test_config_flow.py | 7 +- .../motionblinds_ble/test_config_flow.py | 39 ++--- .../components/motioneye/test_config_flow.py | 35 ++--- .../motionmount/test_config_flow.py | 46 +++--- tests/components/mqtt/test_config_flow.py | 47 +++--- tests/components/mullvad/test_config_flow.py | 8 +- .../components/mysensors/test_config_flow.py | 18 +-- tests/components/mystrom/test_config_flow.py | 14 +- tests/components/myuplink/test_config_flow.py | 2 +- tests/components/nam/test_config_flow.py | 38 ++--- tests/components/neato/test_config_flow.py | 9 +- tests/components/netatmo/test_config_flow.py | 27 ++-- tests/components/netgear/test_config_flow.py | 38 ++--- .../netgear_lte/test_config_flow.py | 3 +- tests/components/nextbus/test_config_flow.py | 12 +- .../components/nextcloud/test_config_flow.py | 24 ++-- tests/components/nextdns/test_config_flow.py | 10 +- .../nfandroidtv/test_config_flow.py | 11 +- .../nibe_heatpump/test_config_flow.py | 24 ++-- .../components/nightscout/test_config_flow.py | 15 +- tests/components/nina/test_config_flow.py | 28 ++-- .../nmap_tracker/test_config_flow.py | 7 +- tests/components/notion/test_config_flow.py | 14 +- tests/components/nuki/test_config_flow.py | 37 ++--- tests/components/nut/test_config_flow.py | 51 +++---- tests/components/nzbget/test_config_flow.py | 14 +- tests/components/obihai/test_config_flow.py | 12 +- .../components/octoprint/test_config_flow.py | 11 +- tests/components/ollama/test_config_flow.py | 28 ++-- .../components/omnilogic/test_config_flow.py | 7 +- tests/components/oncue/test_config_flow.py | 12 +- .../components/ondilo_ico/test_config_flow.py | 5 +- tests/components/onewire/test_config_flow.py | 26 ++-- tests/components/onvif/test_config_flow.py | 84 +++++------ .../components/open_meteo/test_config_flow.py | 4 +- .../openai_conversation/test_config_flow.py | 8 +- .../openexchangerates/test_config_flow.py | 24 ++-- .../components/opengarage/test_config_flow.py | 12 +- tests/components/openhome/test_config_flow.py | 11 +- tests/components/opensky/test_config_flow.py | 11 +- .../opentherm_gw/test_config_flow.py | 15 +- tests/components/openuv/test_config_flow.py | 20 +-- .../openweathermap/test_config_flow.py | 14 +- tests/components/opower/test_config_flow.py | 28 ++-- tests/components/oralb/test_config_flow.py | 34 ++--- .../components/osoenergy/test_config_flow.py | 21 +-- tests/components/otbr/test_config_flow.py | 42 +++--- .../ourgroceries/test_config_flow.py | 8 +- tests/components/overkiz/test_config_flow.py | 41 +++--- .../components/ovo_energy/test_config_flow.py | 29 ++-- .../components/owntracks/test_config_flow.py | 15 +- 107 files changed, 1177 insertions(+), 1147 deletions(-) diff --git a/tests/components/kaleidescape/test_config_flow.py b/tests/components/kaleidescape/test_config_flow.py index 8171ed0955b..5d9f8dba146 100644 --- a/tests/components/kaleidescape/test_config_flow.py +++ b/tests/components/kaleidescape/test_config_flow.py @@ -21,7 +21,7 @@ async def test_user_config_flow_success( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -29,7 +29,7 @@ async def test_user_config_flow_success( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert "data" in result assert result["data"][CONF_HOST] == MOCK_HOST @@ -44,7 +44,7 @@ async def test_user_config_flow_bad_connect_errors( DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: MOCK_HOST} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -59,7 +59,7 @@ async def test_user_config_flow_unsupported_device_errors( DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: MOCK_HOST} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unsupported"} @@ -71,7 +71,7 @@ async def test_user_config_flow_device_exists_abort( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: MOCK_HOST} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -83,7 +83,7 @@ async def test_ssdp_config_flow_success( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_configure( @@ -91,7 +91,7 @@ async def test_ssdp_config_flow_success( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert "data" in result assert result["data"][CONF_HOST] == MOCK_HOST @@ -107,7 +107,7 @@ async def test_ssdp_config_flow_bad_connect_aborts( DOMAIN, context={"source": SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -122,5 +122,5 @@ async def test_ssdp_config_flow_unsupported_device_aborts( DOMAIN, context={"source": SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unsupported" diff --git a/tests/components/keenetic_ndms2/test_config_flow.py b/tests/components/keenetic_ndms2/test_config_flow.py index e1cb083dc73..18bacc3a32c 100644 --- a/tests/components/keenetic_ndms2/test_config_flow.py +++ b/tests/components/keenetic_ndms2/test_config_flow.py @@ -7,11 +7,12 @@ from ndms2_client import ConnectionException from ndms2_client.client import InterfaceInfo, RouterInfo import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import keenetic_ndms2 as keenetic, ssdp from homeassistant.components.keenetic_ndms2 import const from homeassistant.const import CONF_HOST, CONF_SOURCE from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import MOCK_DATA, MOCK_NAME, MOCK_OPTIONS, MOCK_SSDP_DISCOVERY_INFO @@ -51,7 +52,7 @@ async def test_flow_works(hass: HomeAssistant, connect) -> None: result = await hass.config_entries.flow.async_init( keenetic.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -63,7 +64,7 @@ async def test_flow_works(hass: HomeAssistant, connect) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == MOCK_NAME assert result2["data"] == MOCK_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -98,7 +99,7 @@ async def test_options(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.options.async_configure( @@ -106,7 +107,7 @@ async def test_options(hass: HomeAssistant) -> None: user_input=MOCK_OPTIONS, ) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == MOCK_OPTIONS @@ -126,7 +127,7 @@ async def test_host_already_configured(hass: HomeAssistant, connect) -> None: result["flow_id"], user_input=MOCK_DATA ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -139,7 +140,7 @@ async def test_connection_error(hass: HomeAssistant, connect_error) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_DATA ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -153,7 +154,7 @@ async def test_ssdp_works(hass: HomeAssistant, connect) -> None: data=discovery_info, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -168,7 +169,7 @@ async def test_ssdp_works(hass: HomeAssistant, connect) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == MOCK_NAME assert result2["data"] == MOCK_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -189,7 +190,7 @@ async def test_ssdp_already_configured(hass: HomeAssistant) -> None: data=discovery_info, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -210,7 +211,7 @@ async def test_ssdp_ignored(hass: HomeAssistant) -> None: data=discovery_info, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -236,7 +237,7 @@ async def test_ssdp_update_host(hass: HomeAssistant) -> None: data=discovery_info, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == new_ip @@ -254,7 +255,7 @@ async def test_ssdp_reject_no_udn(hass: HomeAssistant) -> None: data=discovery_info, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_udn" @@ -270,5 +271,5 @@ async def test_ssdp_reject_non_keenetic(hass: HomeAssistant) -> None: data=discovery_info, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_keenetic_ndms2" diff --git a/tests/components/kegtron/test_config_flow.py b/tests/components/kegtron/test_config_flow.py index 4e21dc238bc..fe5cc0e5e5e 100644 --- a/tests/components/kegtron/test_config_flow.py +++ b/tests/components/kegtron/test_config_flow.py @@ -23,13 +23,13 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=KEGTRON_KT100_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch("homeassistant.components.kegtron.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Kegtron KT-100 9B75" assert result2["data"] == {} assert result2["result"].unique_id == "D0:CF:5E:5C:9B:75" @@ -42,7 +42,7 @@ async def test_async_step_bluetooth_not_kegtron(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_KEGTRON_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -52,7 +52,7 @@ async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -66,14 +66,14 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch("homeassistant.components.kegtron.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"address": "D0:CF:5E:5C:9B:75"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Kegtron KT-200 9B75" assert result2["data"] == {} assert result2["result"].unique_id == "D0:CF:5E:5C:9B:75" @@ -89,7 +89,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -103,7 +103,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - result["flow_id"], user_input={"address": "D0:CF:5E:5C:9B:75"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -125,7 +125,7 @@ async def test_async_step_user_with_found_devices_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -142,7 +142,7 @@ async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant) - context={"source": config_entries.SOURCE_BLUETOOTH}, data=KEGTRON_KT100_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -153,7 +153,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=KEGTRON_KT100_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -161,7 +161,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=KEGTRON_KT100_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -174,7 +174,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=KEGTRON_KT100_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -185,14 +185,14 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch("homeassistant.components.kegtron.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"address": "D0:CF:5E:5C:9B:75"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Kegtron KT-100 9B75" assert result2["data"] == {} assert result2["result"].unique_id == "D0:CF:5E:5C:9B:75" diff --git a/tests/components/keymitt_ble/test_config_flow.py b/tests/components/keymitt_ble/test_config_flow.py index 7e60bdfca53..029b50b9728 100644 --- a/tests/components/keymitt_ble/test_config_flow.py +++ b/tests/components/keymitt_ble/test_config_flow.py @@ -33,7 +33,7 @@ async def test_bluetooth_discovery(hass: HomeAssistant) -> None: context={"source": SOURCE_BLUETOOTH}, data=SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" with patch_async_setup_entry() as mock_setup_entry, patch_microbot_api(): @@ -43,7 +43,7 @@ async def test_bluetooth_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert len(mock_setup_entry.mock_calls) == 0 @@ -64,7 +64,7 @@ async def test_bluetooth_discovery_already_setup(hass: HomeAssistant) -> None: context={"source": SOURCE_BLUETOOTH}, data=SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -78,7 +78,7 @@ async def test_user_setup(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {} @@ -89,7 +89,7 @@ async def test_user_setup(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "link" assert result2["errors"] is None @@ -100,7 +100,7 @@ async def test_user_setup(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["result"].data == { CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", CONF_ACCESS_TOKEN: ANY, @@ -125,7 +125,7 @@ async def test_user_setup_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -141,7 +141,7 @@ async def test_user_no_devices(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -158,7 +158,7 @@ async def test_no_link(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {} @@ -168,7 +168,7 @@ async def test_no_link(hass: HomeAssistant) -> None: USER_INPUT, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "link" with ( patch( @@ -183,7 +183,7 @@ async def test_no_link(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "link" assert result3["errors"] == {"base": "linking"} diff --git a/tests/components/kitchen_sink/test_config_flow.py b/tests/components/kitchen_sink/test_config_flow.py index 86c1698669e..e530ed0e6f3 100644 --- a/tests/components/kitchen_sink/test_config_flow.py +++ b/tests/components/kitchen_sink/test_config_flow.py @@ -2,9 +2,10 @@ from unittest.mock import patch -from homeassistant import config_entries, data_entry_flow, setup +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 @@ -30,7 +31,7 @@ async def test_import_once(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_IMPORT}, data={}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Kitchen Sink" assert result["data"] == {} assert result["options"] == {} @@ -45,7 +46,7 @@ async def test_import_once(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_IMPORT}, data={}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" mock_setup_entry.assert_not_called() @@ -63,5 +64,5 @@ async def test_reauth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure(flows[0]["flow_id"], {}) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" diff --git a/tests/components/kmtronic/test_config_flow.py b/tests/components/kmtronic/test_config_flow.py index 222bb8bead2..a59821b4ec5 100644 --- a/tests/components/kmtronic/test_config_flow.py +++ b/tests/components/kmtronic/test_config_flow.py @@ -6,10 +6,11 @@ from unittest.mock import AsyncMock, Mock, patch from aiohttp import ClientConnectorError, ClientResponseError import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.kmtronic.const import CONF_REVERSE, DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker @@ -74,14 +75,14 @@ async def test_form_options( assert config_entry.state is ConfigEntryState.LOADED result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + 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_REVERSE: True} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_REVERSE: True} await hass.async_block_till_done() diff --git a/tests/components/knx/test_config_flow.py b/tests/components/knx/test_config_flow.py index f5b3d9595d0..44d5b8bcf80 100644 --- a/tests/components/knx/test_config_flow.py +++ b/tests/components/knx/test_config_flow.py @@ -163,7 +163,7 @@ async def test_routing_setup( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -172,7 +172,7 @@ async def test_routing_setup( CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "routing" assert result2["errors"] == {"base": "no_router_discovered"} @@ -185,7 +185,7 @@ async def test_routing_setup( }, ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Routing as 1.1.110" assert result3["data"] == { **DEFAULT_ENTRY_DATA, @@ -217,7 +217,7 @@ async def test_routing_setup_advanced( "show_advanced_options": True, }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -226,7 +226,7 @@ async def test_routing_setup_advanced( CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "routing" assert result2["errors"] == {"base": "no_router_discovered"} @@ -240,7 +240,7 @@ async def test_routing_setup_advanced( CONF_KNX_LOCAL_IP: "no_local_ip", }, ) - assert result_invalid_input["type"] == FlowResultType.FORM + assert result_invalid_input["type"] is FlowResultType.FORM assert result_invalid_input["step_id"] == "routing" assert result_invalid_input["errors"] == { CONF_KNX_MCAST_GRP: "invalid_ip_address", @@ -260,7 +260,7 @@ async def test_routing_setup_advanced( }, ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Routing as 1.1.110" assert result3["data"] == { **DEFAULT_ENTRY_DATA, @@ -288,7 +288,7 @@ async def test_routing_secure_manual_setup( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -297,7 +297,7 @@ async def test_routing_secure_manual_setup( CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "routing" assert result2["errors"] == {"base": "no_router_discovered"} @@ -310,14 +310,14 @@ async def test_routing_secure_manual_setup( CONF_KNX_ROUTING_SECURE: True, }, ) - assert result3["type"] == FlowResultType.MENU + assert result3["type"] is FlowResultType.MENU assert result3["step_id"] == "secure_key_source_menu_routing" result4 = await hass.config_entries.flow.async_configure( result3["flow_id"], {"next_step_id": "secure_routing_manual"}, ) - assert result4["type"] == FlowResultType.FORM + assert result4["type"] is FlowResultType.FORM assert result4["step_id"] == "secure_routing_manual" assert not result4["errors"] @@ -328,7 +328,7 @@ async def test_routing_secure_manual_setup( CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE: 2000, }, ) - assert result_invalid_key1["type"] == FlowResultType.FORM + assert result_invalid_key1["type"] is FlowResultType.FORM assert result_invalid_key1["step_id"] == "secure_routing_manual" assert result_invalid_key1["errors"] == {"backbone_key": "invalid_backbone_key"} @@ -339,7 +339,7 @@ async def test_routing_secure_manual_setup( CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE: 2000, }, ) - assert result_invalid_key2["type"] == FlowResultType.FORM + assert result_invalid_key2["type"] is FlowResultType.FORM assert result_invalid_key2["step_id"] == "secure_routing_manual" assert result_invalid_key2["errors"] == {"backbone_key": "invalid_backbone_key"} @@ -351,7 +351,7 @@ async def test_routing_secure_manual_setup( }, ) await hass.async_block_till_done() - assert secure_routing_manual["type"] == FlowResultType.CREATE_ENTRY + assert secure_routing_manual["type"] is FlowResultType.CREATE_ENTRY assert secure_routing_manual["title"] == "Secure Routing as 0.0.123" assert secure_routing_manual["data"] == { **DEFAULT_ENTRY_DATA, @@ -378,7 +378,7 @@ async def test_routing_secure_keyfile( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -387,7 +387,7 @@ async def test_routing_secure_keyfile( CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "routing" assert result2["errors"] == {"base": "no_router_discovered"} @@ -400,14 +400,14 @@ async def test_routing_secure_keyfile( CONF_KNX_ROUTING_SECURE: True, }, ) - assert result3["type"] == FlowResultType.MENU + assert result3["type"] is FlowResultType.MENU assert result3["step_id"] == "secure_key_source_menu_routing" result4 = await hass.config_entries.flow.async_configure( result3["flow_id"], {"next_step_id": "secure_knxkeys"}, ) - assert result4["type"] == FlowResultType.FORM + assert result4["type"] is FlowResultType.FORM assert result4["step_id"] == "secure_knxkeys" assert not result4["errors"] @@ -420,7 +420,7 @@ async def test_routing_secure_keyfile( }, ) await hass.async_block_till_done() - assert routing_secure_knxkeys["type"] == FlowResultType.CREATE_ENTRY + assert routing_secure_knxkeys["type"] is FlowResultType.CREATE_ENTRY assert routing_secure_knxkeys["title"] == "Secure Routing as 0.0.123" assert routing_secure_knxkeys["data"] == { **DEFAULT_ENTRY_DATA, @@ -525,7 +525,7 @@ async def test_tunneling_setup_manual( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -534,7 +534,7 @@ async def test_tunneling_setup_manual( CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "manual_tunnel" assert result2["errors"] == {"base": "no_tunnel_discovered"} @@ -553,7 +553,7 @@ async def test_tunneling_setup_manual( user_input, ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == title assert result3["data"] == config_entry_data knx_setup.assert_called_once() @@ -682,7 +682,7 @@ async def test_tunneling_setup_manual_request_description_error( }, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Tunneling TCP @ 192.168.0.1" assert result["data"] == { **DEFAULT_ENTRY_DATA, @@ -716,7 +716,7 @@ async def test_tunneling_setup_for_local_ip( "show_advanced_options": True, }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -725,7 +725,7 @@ async def test_tunneling_setup_for_local_ip( CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "manual_tunnel" assert result2["errors"] == {"base": "no_tunnel_discovered"} @@ -739,7 +739,7 @@ async def test_tunneling_setup_for_local_ip( CONF_KNX_LOCAL_IP: "192.168.1.112", }, ) - assert result_invalid_host["type"] == FlowResultType.FORM + assert result_invalid_host["type"] is FlowResultType.FORM assert result_invalid_host["step_id"] == "manual_tunnel" assert result_invalid_host["errors"] == { CONF_HOST: "invalid_ip_address", @@ -755,7 +755,7 @@ async def test_tunneling_setup_for_local_ip( CONF_KNX_LOCAL_IP: "asdf", }, ) - assert result_invalid_local["type"] == FlowResultType.FORM + assert result_invalid_local["type"] is FlowResultType.FORM assert result_invalid_local["step_id"] == "manual_tunnel" assert result_invalid_local["errors"] == { CONF_KNX_LOCAL_IP: "invalid_ip_address", @@ -773,7 +773,7 @@ async def test_tunneling_setup_for_local_ip( }, ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Tunneling UDP @ 192.168.0.2" assert result3["data"] == { **DEFAULT_ENTRY_DATA, @@ -804,7 +804,7 @@ async def test_tunneling_setup_for_multiple_found_gateways( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] tunnel_flow = await hass.config_entries.flow.async_configure( @@ -813,7 +813,7 @@ async def test_tunneling_setup_for_multiple_found_gateways( CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, }, ) - assert tunnel_flow["type"] == FlowResultType.FORM + assert tunnel_flow["type"] is FlowResultType.FORM assert tunnel_flow["step_id"] == "tunnel" assert not tunnel_flow["errors"] @@ -822,7 +822,7 @@ async def test_tunneling_setup_for_multiple_found_gateways( {CONF_KNX_GATEWAY: str(gateway)}, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { **DEFAULT_ENTRY_DATA, CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, @@ -859,7 +859,7 @@ async def test_manual_tunnel_step_with_found_gateway( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] tunnel_flow = await hass.config_entries.flow.async_configure( @@ -868,7 +868,7 @@ async def test_manual_tunnel_step_with_found_gateway( CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, }, ) - assert tunnel_flow["type"] == FlowResultType.FORM + assert tunnel_flow["type"] is FlowResultType.FORM assert tunnel_flow["step_id"] == "tunnel" assert not tunnel_flow["errors"] @@ -878,7 +878,7 @@ async def test_manual_tunnel_step_with_found_gateway( CONF_KNX_GATEWAY: OPTION_MANUAL_TUNNEL, }, ) - assert manual_tunnel_flow["type"] == FlowResultType.FORM + assert manual_tunnel_flow["type"] is FlowResultType.FORM assert manual_tunnel_flow["step_id"] == "manual_tunnel" assert not manual_tunnel_flow["errors"] @@ -896,7 +896,7 @@ async def test_form_with_automatic_connection_handling( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -906,7 +906,7 @@ async def test_form_with_automatic_connection_handling( }, ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == CONF_KNX_AUTOMATIC.capitalize() assert result2["data"] == { # don't use **DEFAULT_ENTRY_DATA here to check for correct usage of defaults @@ -939,7 +939,7 @@ async def _get_menu_step_secure_tunnel(hass: HomeAssistant) -> FlowResult: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -948,7 +948,7 @@ async def _get_menu_step_secure_tunnel(hass: HomeAssistant) -> FlowResult: CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "tunnel" assert not result2["errors"] @@ -956,7 +956,7 @@ async def _get_menu_step_secure_tunnel(hass: HomeAssistant) -> FlowResult: result2["flow_id"], {CONF_KNX_GATEWAY: str(gateway)}, ) - assert result3["type"] == FlowResultType.MENU + assert result3["type"] is FlowResultType.MENU assert result3["step_id"] == "secure_key_source_menu_tunnel" return result3 @@ -988,7 +988,7 @@ async def test_get_secure_menu_step_manual_tunnelling( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -997,7 +997,7 @@ async def test_get_secure_menu_step_manual_tunnelling( CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "tunnel" assert not result2["errors"] @@ -1016,7 +1016,7 @@ async def test_get_secure_menu_step_manual_tunnelling( CONF_PORT: 3675, }, ) - assert result3["type"] == FlowResultType.MENU + assert result3["type"] is FlowResultType.MENU assert result3["step_id"] == "secure_key_source_menu_tunnel" @@ -1028,7 +1028,7 @@ async def test_configure_secure_tunnel_manual(hass: HomeAssistant, knx_setup) -> menu_step["flow_id"], {"next_step_id": "secure_tunnel_manual"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "secure_tunnel_manual" assert not result["errors"] @@ -1041,7 +1041,7 @@ async def test_configure_secure_tunnel_manual(hass: HomeAssistant, knx_setup) -> }, ) await hass.async_block_till_done() - assert secure_tunnel_manual["type"] == FlowResultType.CREATE_ENTRY + assert secure_tunnel_manual["type"] is FlowResultType.CREATE_ENTRY assert secure_tunnel_manual["data"] == { **DEFAULT_ENTRY_DATA, CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING_TCP_SECURE, @@ -1066,7 +1066,7 @@ async def test_configure_secure_knxkeys(hass: HomeAssistant, knx_setup) -> None: menu_step["flow_id"], {"next_step_id": "secure_knxkeys"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "secure_knxkeys" assert not result["errors"] @@ -1078,7 +1078,7 @@ async def test_configure_secure_knxkeys(hass: HomeAssistant, knx_setup) -> None: CONF_KNX_KNXKEY_PASSWORD: "test", }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert secure_knxkeys["step_id"] == "knxkeys_tunnel_select" assert not result["errors"] secure_knxkeys = await hass.config_entries.flow.async_configure( @@ -1087,7 +1087,7 @@ async def test_configure_secure_knxkeys(hass: HomeAssistant, knx_setup) -> None: ) await hass.async_block_till_done() - assert secure_knxkeys["type"] == FlowResultType.CREATE_ENTRY + assert secure_knxkeys["type"] is FlowResultType.CREATE_ENTRY assert secure_knxkeys["data"] == { **DEFAULT_ENTRY_DATA, CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING_TCP_SECURE, @@ -1116,7 +1116,7 @@ async def test_configure_secure_knxkeys_invalid_signature(hass: HomeAssistant) - menu_step["flow_id"], {"next_step_id": "secure_knxkeys"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "secure_knxkeys" assert not result["errors"] @@ -1130,7 +1130,7 @@ async def test_configure_secure_knxkeys_invalid_signature(hass: HomeAssistant) - CONF_KNX_KNXKEY_PASSWORD: "password", }, ) - assert secure_knxkeys["type"] == FlowResultType.FORM + assert secure_knxkeys["type"] is FlowResultType.FORM assert secure_knxkeys["errors"] assert ( secure_knxkeys["errors"][CONF_KNX_KNXKEY_PASSWORD] @@ -1146,7 +1146,7 @@ async def test_configure_secure_knxkeys_no_tunnel_for_host(hass: HomeAssistant) menu_step["flow_id"], {"next_step_id": "secure_knxkeys"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "secure_knxkeys" assert not result["errors"] @@ -1159,7 +1159,7 @@ async def test_configure_secure_knxkeys_no_tunnel_for_host(hass: HomeAssistant) CONF_KNX_KNXKEY_PASSWORD: "password", }, ) - assert secure_knxkeys["type"] == FlowResultType.FORM + assert secure_knxkeys["type"] is FlowResultType.FORM assert secure_knxkeys["errors"] == {"base": "keyfile_no_tunnel_for_host"} @@ -1183,7 +1183,7 @@ async def test_options_flow_connection_type( menu_step["flow_id"], {"next_step_id": "connection_type"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "connection_type" result2 = await hass.config_entries.options.async_configure( @@ -1192,7 +1192,7 @@ async def test_options_flow_connection_type( CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "tunnel" result3 = await hass.config_entries.options.async_configure( @@ -1202,7 +1202,7 @@ async def test_options_flow_connection_type( }, ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert not result3["data"] assert mock_config_entry.data == { CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, @@ -1263,7 +1263,7 @@ async def test_options_flow_secure_manual_to_keyfile( menu_step["flow_id"], {"next_step_id": "connection_type"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "connection_type" result2 = await hass.config_entries.options.async_configure( @@ -1272,7 +1272,7 @@ async def test_options_flow_secure_manual_to_keyfile( CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "tunnel" assert not result2["errors"] @@ -1280,14 +1280,14 @@ async def test_options_flow_secure_manual_to_keyfile( result2["flow_id"], {CONF_KNX_GATEWAY: str(gateway)}, ) - assert result3["type"] == FlowResultType.MENU + assert result3["type"] is FlowResultType.MENU assert result3["step_id"] == "secure_key_source_menu_tunnel" result4 = await hass.config_entries.options.async_configure( result3["flow_id"], {"next_step_id": "secure_knxkeys"}, ) - assert result4["type"] == FlowResultType.FORM + assert result4["type"] is FlowResultType.FORM assert result4["step_id"] == "secure_knxkeys" assert not result4["errors"] @@ -1299,7 +1299,7 @@ async def test_options_flow_secure_manual_to_keyfile( CONF_KNX_KNXKEY_PASSWORD: "test", }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert secure_knxkeys["step_id"] == "knxkeys_tunnel_select" assert not result["errors"] secure_knxkeys = await hass.config_entries.options.async_configure( @@ -1308,7 +1308,7 @@ async def test_options_flow_secure_manual_to_keyfile( ) await hass.async_block_till_done() - assert secure_knxkeys["type"] == FlowResultType.CREATE_ENTRY + assert secure_knxkeys["type"] is FlowResultType.CREATE_ENTRY assert mock_config_entry.data == { **DEFAULT_ENTRY_DATA, CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING_TCP_SECURE, @@ -1341,7 +1341,7 @@ async def test_options_communication_settings( menu_step["flow_id"], {"next_step_id": "communication_settings"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "communication_settings" result2 = await hass.config_entries.options.async_configure( @@ -1353,7 +1353,7 @@ async def test_options_communication_settings( }, ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert not result2.get("data") assert mock_config_entry.data == { **DEFAULT_ENTRY_DATA, @@ -1394,7 +1394,7 @@ async def test_options_update_keyfile(hass: HomeAssistant, knx_setup) -> None: menu_step["flow_id"], {"next_step_id": "secure_knxkeys"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "secure_knxkeys" with patch_file_upload(): @@ -1406,7 +1406,7 @@ async def test_options_update_keyfile(hass: HomeAssistant, knx_setup) -> None: }, ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert not result2.get("data") assert mock_config_entry.data == { **start_data, @@ -1442,7 +1442,7 @@ async def test_options_keyfile_upload(hass: HomeAssistant, knx_setup) -> None: menu_step["flow_id"], {"next_step_id": "secure_knxkeys"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "secure_knxkeys" with patch_file_upload(): @@ -1454,7 +1454,7 @@ async def test_options_keyfile_upload(hass: HomeAssistant, knx_setup) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "knxkeys_tunnel_select" result3 = await hass.config_entries.options.async_configure( @@ -1464,7 +1464,7 @@ async def test_options_keyfile_upload(hass: HomeAssistant, knx_setup) -> None: }, ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert not result3.get("data") assert mock_config_entry.data == { **start_data, diff --git a/tests/components/lacrosse_view/test_config_flow.py b/tests/components/lacrosse_view/test_config_flow.py index 195c004179b..5a48b3d15fe 100644 --- a/tests/components/lacrosse_view/test_config_flow.py +++ b/tests/components/lacrosse_view/test_config_flow.py @@ -20,7 +20,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -42,7 +42,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "location" assert result2["errors"] is None @@ -54,7 +54,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Test" assert result3["data"] == { "username": "test-username", @@ -83,7 +83,7 @@ async def test_form_auth_false(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -102,7 +102,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -124,7 +124,7 @@ async def test_form_login_first(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -149,7 +149,7 @@ async def test_form_no_locations(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "no_locations"} @@ -171,7 +171,7 @@ async def test_form_unexpected_error(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -196,7 +196,7 @@ async def test_already_configured_device( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -218,7 +218,7 @@ async def test_already_configured_device( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "location" assert result2["errors"] is None @@ -230,7 +230,7 @@ async def test_already_configured_device( ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "already_configured" assert len(mock_setup_entry.mock_calls) == 0 @@ -261,7 +261,7 @@ async def test_reauth(hass: HomeAssistant) -> None: }, data=data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" new_username = "new-username" @@ -283,7 +283,7 @@ async def test_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 diff --git a/tests/components/lamarzocco/test_config_flow.py b/tests/components/lamarzocco/test_config_flow.py index 37d9c9a3e95..8539f14ce7f 100644 --- a/tests/components/lamarzocco/test_config_flow.py +++ b/tests/components/lamarzocco/test_config_flow.py @@ -30,7 +30,7 @@ async def __do_successful_user_step( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "machine_selection" return result2 @@ -48,7 +48,7 @@ async def __do_sucessful_machine_selection_step( ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == mock_lamarzocco.serial_number assert result3["data"] == { @@ -63,7 +63,7 @@ async def test_form(hass: HomeAssistant, mock_lamarzocco: MagicMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "user" @@ -82,7 +82,7 @@ async def test_form_abort_already_configured( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -91,7 +91,7 @@ async def test_form_abort_already_configured( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "machine_selection" result3 = await hass.config_entries.flow.async_configure( @@ -103,7 +103,7 @@ async def test_form_abort_already_configured( ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "already_configured" @@ -122,7 +122,7 @@ async def test_form_invalid_auth( USER_INPUT, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} assert len(mock_lamarzocco.get_all_machines.mock_calls) == 1 @@ -139,7 +139,7 @@ async def test_form_invalid_host( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -150,7 +150,7 @@ async def test_form_invalid_host( mock_lamarzocco.check_local_connection.return_value = False - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "machine_selection" result3 = await hass.config_entries.flow.async_configure( @@ -162,7 +162,7 @@ async def test_form_invalid_host( ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"host": "cannot_connect"} assert len(mock_lamarzocco.get_all_machines.mock_calls) == 1 @@ -187,7 +187,7 @@ async def test_form_cannot_connect( USER_INPUT, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "no_machines"} assert len(mock_lamarzocco.get_all_machines.mock_calls) == 1 @@ -197,7 +197,7 @@ async def test_form_cannot_connect( USER_INPUT, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} assert len(mock_lamarzocco.get_all_machines.mock_calls) == 2 @@ -224,7 +224,7 @@ async def test_reauth_flow( data=mock_config_entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result2 = await hass.config_entries.flow.async_configure( @@ -232,7 +232,7 @@ async def test_reauth_flow( {CONF_PASSWORD: "new_password"}, ) - assert result2["type"] == FlowResultType.ABORT + 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 @@ -250,14 +250,14 @@ async def test_bluetooth_discovery( DOMAIN, context={"source": config_entries.SOURCE_BLUETOOTH}, data=service_info ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], USER_INPUT, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "machine_selection" assert len(mock_lamarzocco.get_all_machines.mock_calls) == 1 @@ -269,7 +269,7 @@ async def test_bluetooth_discovery( ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == mock_lamarzocco.serial_number assert result3["data"] == { @@ -296,7 +296,7 @@ async def test_bluetooth_discovery_errors( data=service_info, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" mock_lamarzocco.get_all_machines.return_value = [("GS98765", "GS3 MP")] @@ -304,7 +304,7 @@ async def test_bluetooth_discovery_errors( result["flow_id"], USER_INPUT, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "machine_not_found"} assert len(mock_lamarzocco.get_all_machines.mock_calls) == 1 @@ -315,7 +315,7 @@ async def test_bluetooth_discovery_errors( result["flow_id"], USER_INPUT, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "machine_selection" assert len(mock_lamarzocco.get_all_machines.mock_calls) == 2 @@ -327,7 +327,7 @@ async def test_bluetooth_discovery_errors( ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == mock_lamarzocco.serial_number assert result3["data"] == { @@ -350,7 +350,7 @@ async def test_options_flow( result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result2 = await hass.config_entries.options.async_configure( @@ -361,7 +361,7 @@ async def test_options_flow( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { CONF_USE_BLUETOOTH: False, } diff --git a/tests/components/lametric/test_config_flow.py b/tests/components/lametric/test_config_flow.py index e5fa1229e07..2a21423ad03 100644 --- a/tests/components/lametric/test_config_flow.py +++ b/tests/components/lametric/test_config_flow.py @@ -60,7 +60,7 @@ async def test_full_cloud_import_flow_multiple_devices( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.MENU + assert result.get("type") is FlowResultType.MENU assert result.get("step_id") == "choice_enter_manual_or_fetch_cloud" assert result.get("menu_options") == ["pick_implementation", "manual_entry"] flow_id = result["flow_id"] @@ -77,7 +77,7 @@ async def test_full_cloud_import_flow_multiple_devices( }, ) - assert result2.get("type") == FlowResultType.EXTERNAL_STEP + assert result2.get("type") is FlowResultType.EXTERNAL_STEP assert result2.get("url") == ( "https://developer.lametric.com/api/v2/oauth2/authorize" "?response_type=code&client_id=client" @@ -103,14 +103,14 @@ async def test_full_cloud_import_flow_multiple_devices( result3 = await hass.config_entries.flow.async_configure(flow_id) - assert result3.get("type") == FlowResultType.FORM + assert result3.get("type") is FlowResultType.FORM assert result3.get("step_id") == "cloud_select_device" result4 = await hass.config_entries.flow.async_configure( flow_id, user_input={CONF_DEVICE: "SA110405124500W00BS9"} ) - assert result4.get("type") == FlowResultType.CREATE_ENTRY + assert result4.get("type") is FlowResultType.CREATE_ENTRY assert result4.get("title") == "Frenck's LaMetric" assert result4.get("data") == { CONF_HOST: "127.0.0.1", @@ -140,7 +140,7 @@ async def test_full_cloud_import_flow_single_device( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.MENU + assert result.get("type") is FlowResultType.MENU assert result.get("step_id") == "choice_enter_manual_or_fetch_cloud" assert result.get("menu_options") == ["pick_implementation", "manual_entry"] flow_id = result["flow_id"] @@ -157,7 +157,7 @@ async def test_full_cloud_import_flow_single_device( }, ) - assert result2.get("type") == FlowResultType.EXTERNAL_STEP + assert result2.get("type") is FlowResultType.EXTERNAL_STEP assert result2.get("url") == ( "https://developer.lametric.com/api/v2/oauth2/authorize" "?response_type=code&client_id=client" @@ -188,7 +188,7 @@ async def test_full_cloud_import_flow_single_device( ] result3 = await hass.config_entries.flow.async_configure(flow_id) - assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY assert result3.get("title") == "Frenck's LaMetric" assert result3.get("data") == { CONF_HOST: "127.0.0.1", @@ -214,7 +214,7 @@ async def test_full_manual( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.MENU + assert result.get("type") is FlowResultType.MENU assert result.get("step_id") == "choice_enter_manual_or_fetch_cloud" assert result.get("menu_options") == ["pick_implementation", "manual_entry"] flow_id = result["flow_id"] @@ -223,14 +223,14 @@ async def test_full_manual( flow_id, user_input={"next_step_id": "manual_entry"} ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "manual_entry" result3 = await hass.config_entries.flow.async_configure( flow_id, user_input={CONF_HOST: "127.0.0.1", CONF_API_KEY: "mock-api-key"} ) - assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY assert result3.get("title") == "Frenck's LaMetric" assert result3.get("data") == { CONF_HOST: "127.0.0.1", @@ -263,7 +263,7 @@ async def test_full_ssdp_with_cloud_import( DOMAIN, context={"source": SOURCE_SSDP}, data=SSDP_DISCOVERY_INFO ) - assert result.get("type") == FlowResultType.MENU + assert result.get("type") is FlowResultType.MENU assert result.get("step_id") == "choice_enter_manual_or_fetch_cloud" assert result.get("menu_options") == ["pick_implementation", "manual_entry"] flow_id = result["flow_id"] @@ -280,7 +280,7 @@ async def test_full_ssdp_with_cloud_import( }, ) - assert result2.get("type") == FlowResultType.EXTERNAL_STEP + assert result2.get("type") is FlowResultType.EXTERNAL_STEP assert result2.get("url") == ( "https://developer.lametric.com/api/v2/oauth2/authorize" "?response_type=code&client_id=client" @@ -306,7 +306,7 @@ async def test_full_ssdp_with_cloud_import( result3 = await hass.config_entries.flow.async_configure(flow_id) - assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY assert result3.get("title") == "Frenck's LaMetric" assert result3.get("data") == { CONF_HOST: "127.0.0.1", @@ -332,7 +332,7 @@ async def test_full_ssdp_manual_entry( DOMAIN, context={"source": SOURCE_SSDP}, data=SSDP_DISCOVERY_INFO ) - assert result.get("type") == FlowResultType.MENU + assert result.get("type") is FlowResultType.MENU assert result.get("step_id") == "choice_enter_manual_or_fetch_cloud" assert result.get("menu_options") == ["pick_implementation", "manual_entry"] flow_id = result["flow_id"] @@ -341,14 +341,14 @@ async def test_full_ssdp_manual_entry( flow_id, user_input={"next_step_id": "manual_entry"} ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "manual_entry" result3 = await hass.config_entries.flow.async_configure( flow_id, user_input={CONF_API_KEY: "mock-api-key"} ) - assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY assert result3.get("title") == "Frenck's LaMetric" assert result3.get("data") == { CONF_HOST: "127.0.0.1", @@ -390,7 +390,7 @@ async def test_ssdp_abort_invalid_discovery( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=data ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == reason @@ -439,7 +439,7 @@ async def test_cloud_import_updates_existing_entry( flow_id, user_input={CONF_DEVICE: "SA110405124500W00BS9"} ) - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "already_configured" assert mock_config_entry.data == { CONF_HOST: "127.0.0.1", @@ -473,7 +473,7 @@ async def test_manual_updates_existing_entry( flow_id, user_input={CONF_HOST: "127.0.0.1", CONF_API_KEY: "mock-api-key"} ) - assert result3.get("type") == FlowResultType.ABORT + assert result3.get("type") is FlowResultType.ABORT assert result3.get("reason") == "already_configured" assert mock_config_entry.data == { CONF_HOST: "127.0.0.1", @@ -495,7 +495,7 @@ async def test_discovery_updates_existing_entry( DOMAIN, context={"source": SOURCE_SSDP}, data=SSDP_DISCOVERY_INFO ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" assert mock_config_entry.data == { CONF_HOST: "127.0.0.1", @@ -544,7 +544,7 @@ async def test_cloud_abort_no_devices( mock_lametric_cloud.devices.return_value = [] result2 = await hass.config_entries.flow.async_configure(flow_id) - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "no_devices" assert len(mock_lametric_cloud.devices.mock_calls) == 1 @@ -581,7 +581,7 @@ async def test_manual_errors( flow_id, user_input={CONF_HOST: "127.0.0.1", CONF_API_KEY: "mock-api-key"} ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "manual_entry" assert result2.get("errors") == {"base": reason} @@ -594,7 +594,7 @@ async def test_manual_errors( flow_id, user_input={CONF_HOST: "127.0.0.1", CONF_API_KEY: "mock-api-key"} ) - assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY assert result3.get("title") == "Frenck's LaMetric" assert result3.get("data") == { CONF_HOST: "127.0.0.1", @@ -664,7 +664,7 @@ async def test_cloud_errors( flow_id, user_input={CONF_DEVICE: "SA110405124500W00BS9"} ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "cloud_select_device" assert result2.get("errors") == {"base": reason} @@ -678,7 +678,7 @@ async def test_cloud_errors( flow_id, user_input={CONF_DEVICE: "SA110405124500W00BS9"} ) - assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY assert result3.get("title") == "Frenck's LaMetric" assert result3.get("data") == { CONF_HOST: "127.0.0.1", @@ -711,7 +711,7 @@ async def test_dhcp_discovery_updates_entry( ), ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" assert mock_config_entry.data == { CONF_API_KEY: "mock-from-fixture", @@ -737,7 +737,7 @@ async def test_dhcp_unknown_device( ), ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "unknown" @@ -791,7 +791,7 @@ async def test_reauth_cloud_import( result2 = await hass.config_entries.flow.async_configure(flow_id) - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "reauth_successful" assert mock_config_entry.data == { CONF_HOST: "127.0.0.1", @@ -855,7 +855,7 @@ async def test_reauth_cloud_abort_device_not_found( result2 = await hass.config_entries.flow.async_configure(flow_id) - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "reauth_device_not_found" assert len(mock_lametric_cloud.devices.mock_calls) == 1 @@ -892,7 +892,7 @@ async def test_reauth_manual( flow_id, user_input={CONF_API_KEY: "mock-api-key"} ) - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "reauth_successful" assert mock_config_entry.data == { CONF_HOST: "127.0.0.1", @@ -934,7 +934,7 @@ async def test_reauth_manual_sky( flow_id, user_input={CONF_API_KEY: "mock-api-key"} ) - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "reauth_successful" assert mock_config_entry.data == { CONF_HOST: "127.0.0.1", diff --git a/tests/components/landisgyr_heat_meter/test_config_flow.py b/tests/components/landisgyr_heat_meter/test_config_flow.py index d53a81a7edf..fe62d530719 100644 --- a/tests/components/landisgyr_heat_meter/test_config_flow.py +++ b/tests/components/landisgyr_heat_meter/test_config_flow.py @@ -49,7 +49,7 @@ async def test_manual_entry(mock_heat_meter, hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -57,7 +57,7 @@ async def test_manual_entry(mock_heat_meter, hass: HomeAssistant) -> None: result["flow_id"], {"device": "Enter Manually"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_serial_manual_path" assert result["errors"] == {} @@ -65,7 +65,7 @@ async def test_manual_entry(mock_heat_meter, hass: HomeAssistant) -> None: result["flow_id"], {"device": "/dev/ttyUSB0"} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "LUGCUH50" assert result["data"] == { "device": "/dev/ttyUSB0", @@ -85,14 +85,14 @@ async def test_list_entry(mock_port, mock_heat_meter, hass: HomeAssistant) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + 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"], {"device": port.device} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "LUGCUH50" assert result["data"] == { "device": port.device, @@ -110,7 +110,7 @@ async def test_manual_entry_fail(mock_heat_meter, hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -118,7 +118,7 @@ async def test_manual_entry_fail(mock_heat_meter, hass: HomeAssistant) -> None: result["flow_id"], {"device": "Enter Manually"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_serial_manual_path" assert result["errors"] == {} @@ -126,7 +126,7 @@ async def test_manual_entry_fail(mock_heat_meter, hass: HomeAssistant) -> None: result["flow_id"], {"device": "/dev/ttyUSB0"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_serial_manual_path" assert result["errors"] == {"base": "cannot_connect"} @@ -142,14 +142,14 @@ async def test_list_entry_fail(mock_port, mock_heat_meter, hass: HomeAssistant) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + 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"], {"device": port.device} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -185,5 +185,5 @@ async def test_already_configured( result["flow_id"], {"device": port.device} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/lastfm/test_config_flow.py b/tests/components/lastfm/test_config_flow.py index 93fc9e5a206..40710af3569 100644 --- a/tests/components/lastfm/test_config_flow.py +++ b/tests/components/lastfm/test_config_flow.py @@ -5,7 +5,6 @@ from unittest.mock import patch from pylast import WSError import pytest -from homeassistant import data_entry_flow from homeassistant.components.lastfm.const import ( CONF_MAIN_USER, CONF_USERS, @@ -15,6 +14,7 @@ from homeassistant.components.lastfm.const import ( from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import ( API_KEY, @@ -42,14 +42,14 @@ async def test_full_user_flow(hass: HomeAssistant, default_user: MockUser) -> No result["flow_id"], user_input=CONF_USER_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] assert result["step_id"] == "friends" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONF_FRIENDS_DATA ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["options"] == CONF_DATA @@ -78,7 +78,7 @@ async def test_flow_fails( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_USER_DATA ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == message @@ -87,14 +87,14 @@ async def test_flow_fails( result["flow_id"], user_input=CONF_USER_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] assert result["step_id"] == "friends" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONF_FRIENDS_DATA ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["options"] == CONF_DATA @@ -112,7 +112,7 @@ async def test_flow_friends_invalid_username( result["flow_id"], user_input=CONF_USER_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "friends" with patch( @@ -124,7 +124,7 @@ async def test_flow_friends_invalid_username( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONF_FRIENDS_DATA ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "friends" assert result["errors"]["base"] == "invalid_account" @@ -132,7 +132,7 @@ async def test_flow_friends_invalid_username( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONF_FRIENDS_DATA ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["options"] == CONF_DATA @@ -153,7 +153,7 @@ async def test_flow_friends_no_friends( result["flow_id"], user_input=CONF_USER_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "friends" assert len(result["data_schema"].schema[CONF_USERS].config["options"]) == 0 @@ -171,7 +171,7 @@ async def test_options_flow( result = await hass.config_entries.options.async_init(entry.entry_id) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -180,7 +180,7 @@ async def test_options_flow( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_API_KEY: API_KEY, CONF_MAIN_USER: USERNAME_1, @@ -201,7 +201,7 @@ async def test_options_flow_incorrect_username( result = await hass.config_entries.options.async_init(entry.entry_id) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" with patch( @@ -216,7 +216,7 @@ async def test_options_flow_incorrect_username( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"]["base"] == "invalid_account" @@ -227,7 +227,7 @@ async def test_options_flow_incorrect_username( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_API_KEY: API_KEY, CONF_MAIN_USER: USERNAME_1, @@ -248,7 +248,7 @@ async def test_options_flow_from_import( result = await hass.config_entries.options.async_init(entry.entry_id) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert len(result["data_schema"].schema[CONF_USERS].config["options"]) == 0 @@ -266,6 +266,6 @@ async def test_options_flow_without_friends( result = await hass.config_entries.options.async_init(entry.entry_id) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert len(result["data_schema"].schema[CONF_USERS].config["options"]) == 0 diff --git a/tests/components/launch_library/test_config_flow.py b/tests/components/launch_library/test_config_flow.py index 80a634318b9..d3cee3b54e3 100644 --- a/tests/components/launch_library/test_config_flow.py +++ b/tests/components/launch_library/test_config_flow.py @@ -2,10 +2,10 @@ from unittest.mock import patch -from homeassistant import data_entry_flow from homeassistant.components.launch_library.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -17,7 +17,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" with patch( @@ -28,7 +28,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: {}, ) - assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("result").data == {} @@ -44,5 +44,5 @@ async def test_integration_already_exists(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data={} ) - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" diff --git a/tests/components/laundrify/test_config_flow.py b/tests/components/laundrify/test_config_flow.py index 2583e8a5639..69a4b957cf5 100644 --- a/tests/components/laundrify/test_config_flow.py +++ b/tests/components/laundrify/test_config_flow.py @@ -17,7 +17,7 @@ async def test_form(hass: HomeAssistant, laundrify_setup_entry) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -26,7 +26,7 @@ async def test_form(hass: HomeAssistant, laundrify_setup_entry) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DOMAIN assert result["data"] == { CONF_ACCESS_TOKEN: VALID_ACCESS_TOKEN, @@ -46,7 +46,7 @@ async def test_form_invalid_format( data={CONF_CODE: "invalidFormat"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_CODE: "invalid_format"} @@ -59,7 +59,7 @@ async def test_form_invalid_auth(hass: HomeAssistant, laundrify_exchange_code) - data=VALID_USER_INPUT, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_CODE: "invalid_auth"} @@ -74,7 +74,7 @@ async def test_form_cannot_connect( data=VALID_USER_INPUT, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -89,7 +89,7 @@ async def test_form_unkown_exception( data=VALID_USER_INPUT, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "unknown"} @@ -99,7 +99,7 @@ async def test_step_reauth(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_REAUTH} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -108,7 +108,7 @@ async def test_step_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM async def test_integration_already_exists(hass: HomeAssistant) -> None: @@ -125,5 +125,5 @@ async def test_integration_already_exists(hass: HomeAssistant) -> None: }, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/lcn/test_config_flow.py b/tests/components/lcn/test_config_flow.py index aa1b5086e65..e1705e4b349 100644 --- a/tests/components/lcn/test_config_flow.py +++ b/tests/components/lcn/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import patch from pypck.connection import PchkAuthenticationError, PchkLicenseError import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.lcn.const import CONF_DIM_MODE, CONF_SK_NUM_TRIES, DOMAIN from homeassistant.const import ( CONF_DEVICES, @@ -17,6 +17,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -47,7 +48,7 @@ async def test_step_import(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "pchk" assert result["data"] == IMPORT_DATA @@ -69,7 +70,7 @@ async def test_step_import_existing_host(hass: HomeAssistant) -> None: await hass.async_block_till_done() # Check if config entry was updated - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "existing_configuration_updated" assert mock_entry.source == config_entries.SOURCE_IMPORT assert mock_entry.data == IMPORT_DATA @@ -95,5 +96,5 @@ async def test_step_import_error(hass: HomeAssistant, error, reason) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == reason diff --git a/tests/components/ld2410_ble/test_config_flow.py b/tests/components/ld2410_ble/test_config_flow.py index 74e7e8a2c8e..78db43b3a23 100644 --- a/tests/components/ld2410_ble/test_config_flow.py +++ b/tests/components/ld2410_ble/test_config_flow.py @@ -24,7 +24,7 @@ async def test_user_step_success(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -45,7 +45,7 @@ async def test_user_step_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == LD2410_BLE_DISCOVERY_INFO.name assert result2["data"] == { CONF_ADDRESS: LD2410_BLE_DISCOVERY_INFO.address, @@ -63,7 +63,7 @@ async def test_user_step_no_devices_found(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -84,7 +84,7 @@ async def test_user_step_no_new_devices_found(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -97,7 +97,7 @@ async def test_user_step_cannot_connect(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -113,7 +113,7 @@ async def test_user_step_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -134,7 +134,7 @@ async def test_user_step_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == LD2410_BLE_DISCOVERY_INFO.name assert result3["data"] == { CONF_ADDRESS: LD2410_BLE_DISCOVERY_INFO.address, @@ -152,7 +152,7 @@ async def test_user_step_unknown_exception(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -168,7 +168,7 @@ async def test_user_step_unknown_exception(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "unknown"} @@ -189,7 +189,7 @@ async def test_user_step_unknown_exception(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == LD2410_BLE_DISCOVERY_INFO.name assert result3["data"] == { CONF_ADDRESS: LD2410_BLE_DISCOVERY_INFO.address, @@ -205,7 +205,7 @@ async def test_bluetooth_step_success(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=LD2410_BLE_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -226,7 +226,7 @@ async def test_bluetooth_step_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == LD2410_BLE_DISCOVERY_INFO.name assert result2["data"] == { CONF_ADDRESS: LD2410_BLE_DISCOVERY_INFO.address, diff --git a/tests/components/leaone/test_config_flow.py b/tests/components/leaone/test_config_flow.py index edacc3975c4..9712fce9c14 100644 --- a/tests/components/leaone/test_config_flow.py +++ b/tests/components/leaone/test_config_flow.py @@ -18,7 +18,7 @@ async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -32,14 +32,14 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch("homeassistant.components.leaone.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"address": "5F:5A:5C:52:D3:94"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "TZC4 D394" assert result2["data"] == {} assert result2["result"].unique_id == "5F:5A:5C:52:D3:94" @@ -55,7 +55,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -69,7 +69,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - result["flow_id"], user_input={"address": "5F:5A:5C:52:D3:94"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -91,5 +91,5 @@ async def test_async_step_user_with_found_devices_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" diff --git a/tests/components/led_ble/test_config_flow.py b/tests/components/led_ble/test_config_flow.py index 5ceda954ba8..c22c62e2fb1 100644 --- a/tests/components/led_ble/test_config_flow.py +++ b/tests/components/led_ble/test_config_flow.py @@ -28,7 +28,7 @@ async def test_user_step_success(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -49,7 +49,7 @@ async def test_user_step_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == LED_BLE_DISCOVERY_INFO.name assert result2["data"] == { CONF_ADDRESS: LED_BLE_DISCOVERY_INFO.address, @@ -67,7 +67,7 @@ async def test_user_step_no_devices_found(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -88,7 +88,7 @@ async def test_user_step_no_new_devices_found(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -101,7 +101,7 @@ async def test_user_step_cannot_connect(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -117,7 +117,7 @@ async def test_user_step_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -138,7 +138,7 @@ async def test_user_step_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == LED_BLE_DISCOVERY_INFO.name assert result3["data"] == { CONF_ADDRESS: LED_BLE_DISCOVERY_INFO.address, @@ -156,7 +156,7 @@ async def test_user_step_unknown_exception(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -172,7 +172,7 @@ async def test_user_step_unknown_exception(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "unknown"} @@ -193,7 +193,7 @@ async def test_user_step_unknown_exception(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == LED_BLE_DISCOVERY_INFO.name assert result3["data"] == { CONF_ADDRESS: LED_BLE_DISCOVERY_INFO.address, @@ -209,7 +209,7 @@ async def test_bluetooth_step_success(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=LED_BLE_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -230,7 +230,7 @@ async def test_bluetooth_step_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == LED_BLE_DISCOVERY_INFO.name assert result2["data"] == { CONF_ADDRESS: LED_BLE_DISCOVERY_INFO.address, @@ -246,5 +246,5 @@ async def test_bluetooth_unsupported_model(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=UNSUPPORTED_LED_BLE_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" diff --git a/tests/components/lidarr/test_config_flow.py b/tests/components/lidarr/test_config_flow.py index 01fa05ebb18..e44b03cd2a2 100644 --- a/tests/components/lidarr/test_config_flow.py +++ b/tests/components/lidarr/test_config_flow.py @@ -16,14 +16,14 @@ async def test_flow_user_form(hass: HomeAssistant, connection) -> None: context={CONF_SOURCE: SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_INPUT, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == CONF_DATA @@ -35,7 +35,7 @@ async def test_flow_user_invalid_auth(hass: HomeAssistant, invalid_auth) -> None context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "invalid_auth" @@ -48,7 +48,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant, cannot_connect) -> data=CONF_DATA, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "cannot_connect" @@ -61,7 +61,7 @@ async def test_wrong_app(hass: HomeAssistant, wrong_app) -> None: data=MOCK_INPUT, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "wrong_app" @@ -74,7 +74,7 @@ async def test_zeroconf_failed(hass: HomeAssistant, zeroconf_failed) -> None: data=MOCK_INPUT, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "zeroconf_failed" @@ -89,7 +89,7 @@ async def test_flow_user_unknown_error(hass: HomeAssistant, unknown) -> None: result["flow_id"], user_input=CONF_DATA, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "unknown" @@ -109,18 +109,18 @@ async def test_flow_reauth( }, data=CONF_DATA, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_API_KEY: "abc123"}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data[CONF_API_KEY] == "abc123" diff --git a/tests/components/lifx/test_config_flow.py b/tests/components/lifx/test_config_flow.py index 0a0c26da424..67fcc1b7dfa 100644 --- a/tests/components/lifx/test_config_flow.py +++ b/tests/components/lifx/test_config_flow.py @@ -353,7 +353,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: data={CONF_HOST: IP_ADDRESS, CONF_SERIAL: SERIAL}, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_config_flow_try_connect(): @@ -365,7 +365,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: ), ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_in_progress" with _patch_discovery(), _patch_config_flow_try_connect(): @@ -377,7 +377,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: ), ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "already_in_progress" with ( @@ -392,7 +392,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: ), ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "cannot_connect" @@ -434,7 +434,7 @@ async def test_discovered_by_dhcp_or_discovery( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -496,7 +496,7 @@ async def test_discovered_by_dhcp_or_discovery_failed_to_get_device( DOMAIN, context={"source": source}, data=data ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -538,7 +538,7 @@ async def test_discovered_by_dhcp_or_homekit_updates_ip( data=data, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == IP_ADDRESS diff --git a/tests/components/linear_garage_door/test_config_flow.py b/tests/components/linear_garage_door/test_config_flow.py index 1851b61fc15..9704268e650 100644 --- a/tests/components/linear_garage_door/test_config_flow.py +++ b/tests/components/linear_garage_door/test_config_flow.py @@ -17,7 +17,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -56,7 +56,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "test-site-name" assert result3["data"] == { "email": "test-email", @@ -86,7 +86,7 @@ async def test_reauth(hass: HomeAssistant) -> None: }, data=entry.data, ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "user" with ( @@ -116,7 +116,7 @@ async def test_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" entries = hass.config_entries.async_entries() @@ -153,7 +153,7 @@ async def test_form_invalid_login(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -176,5 +176,5 @@ async def test_form_exception(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/litejet/test_config_flow.py b/tests/components/litejet/test_config_flow.py index b92aa59c9ce..4803fd393d8 100644 --- a/tests/components/litejet/test_config_flow.py +++ b/tests/components/litejet/test_config_flow.py @@ -4,10 +4,11 @@ from unittest.mock import patch from serial import SerialException -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.litejet.const import CONF_DEFAULT_TRANSITION, DOMAIN from homeassistant.const import CONF_PORT from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -75,7 +76,7 @@ async def test_options(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -83,5 +84,5 @@ async def test_options(hass: HomeAssistant) -> None: user_input={CONF_DEFAULT_TRANSITION: 12}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_DEFAULT_TRANSITION: 12} diff --git a/tests/components/litterrobot/test_config_flow.py b/tests/components/litterrobot/test_config_flow.py index d516a3f14a2..b8739a73fb8 100644 --- a/tests/components/litterrobot/test_config_flow.py +++ b/tests/components/litterrobot/test_config_flow.py @@ -134,7 +134,7 @@ async def test_step_reauth(hass: HomeAssistant, mock_account: Account) -> None: data=entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with ( @@ -151,7 +151,7 @@ async def test_step_reauth(hass: HomeAssistant, mock_account: Account) -> None: result["flow_id"], user_input={CONF_PASSWORD: CONFIG[litterrobot.DOMAIN][CONF_PASSWORD]}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 @@ -174,7 +174,7 @@ async def test_step_reauth_failed(hass: HomeAssistant, mock_account: Account) -> data=entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( @@ -186,7 +186,7 @@ async def test_step_reauth_failed(hass: HomeAssistant, mock_account: Account) -> user_input={CONF_PASSWORD: CONFIG[litterrobot.DOMAIN][CONF_PASSWORD]}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} with ( @@ -203,6 +203,6 @@ async def test_step_reauth_failed(hass: HomeAssistant, mock_account: Account) -> result["flow_id"], user_input={CONF_PASSWORD: CONFIG[litterrobot.DOMAIN][CONF_PASSWORD]}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/livisi/test_config_flow.py b/tests/components/livisi/test_config_flow.py index 7f4f8568030..9f492b9a45a 100644 --- a/tests/components/livisi/test_config_flow.py +++ b/tests/components/livisi/test_config_flow.py @@ -5,10 +5,10 @@ from unittest.mock import patch from aiolivisi import errors as livisi_errors import pytest -from homeassistant import data_entry_flow from homeassistant.components.livisi.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import ( VALID_CONFIG, @@ -30,7 +30,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: VALID_CONFIG, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "SHC Classic" assert result["data"]["host"] == "1.1.1.1" assert result["data"]["password"] == "test" @@ -59,11 +59,11 @@ async def test_create_entity_after_login_error( result = await hass.config_entries.flow.async_configure( result["flow_id"], VALID_CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == expected_reason with mocked_livisi_login(), mocked_livisi_controller(), mocked_livisi_setup_entry(): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=VALID_CONFIG, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY diff --git a/tests/components/local_calendar/test_config_flow.py b/tests/components/local_calendar/test_config_flow.py index 89ea9d21ff5..c76fd9e283d 100644 --- a/tests/components/local_calendar/test_config_flow.py +++ b/tests/components/local_calendar/test_config_flow.py @@ -19,7 +19,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -34,7 +34,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "My Calendar" assert result2["data"] == { CONF_CALENDAR_NAME: "My Calendar", @@ -51,7 +51,7 @@ async def test_duplicate_name( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result.get("errors") result2 = await hass.config_entries.flow.async_configure( @@ -63,5 +63,5 @@ async def test_duplicate_name( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/local_ip/test_config_flow.py b/tests/components/local_ip/test_config_flow.py index 82fcbf6d6e6..554163bbc1c 100644 --- a/tests/components/local_ip/test_config_flow.py +++ b/tests/components/local_ip/test_config_flow.py @@ -2,10 +2,10 @@ from __future__ import annotations -from homeassistant import data_entry_flow from homeassistant.components.local_ip.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -15,10 +15,10 @@ async def test_config_flow(hass: HomeAssistant, mock_get_source_ip) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() state = hass.states.get(f"sensor.{DOMAIN}") @@ -36,5 +36,5 @@ async def test_already_setup(hass: HomeAssistant, mock_get_source_ip) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/local_todo/test_config_flow.py b/tests/components/local_todo/test_config_flow.py index 381c97be167..b399753d286 100644 --- a/tests/components/local_todo/test_config_flow.py +++ b/tests/components/local_todo/test_config_flow.py @@ -21,7 +21,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result.get("errors") result2 = await hass.config_entries.flow.async_configure( @@ -32,7 +32,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == TODO_NAME assert result2["data"] == { "todo_list_name": TODO_NAME, @@ -49,7 +49,7 @@ async def test_duplicate_todo_list_name( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result.get("errors") result2 = await hass.config_entries.flow.async_configure( @@ -61,5 +61,5 @@ async def test_duplicate_todo_list_name( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/logi_circle/test_config_flow.py b/tests/components/logi_circle/test_config_flow.py index f0de828c186..2525354598d 100644 --- a/tests/components/logi_circle/test_config_flow.py +++ b/tests/components/logi_circle/test_config_flow.py @@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, Mock, patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.http import KEY_HASS from homeassistant.components.logi_circle import config_flow from homeassistant.components.logi_circle.config_flow import ( @@ -15,6 +15,7 @@ from homeassistant.components.logi_circle.config_flow import ( LogiCircleAuthCallbackView, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import AbortFlow, FlowResultType from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -67,7 +68,7 @@ async def test_step_import(hass: HomeAssistant, mock_logi_circle) -> None: flow = init_config_flow(hass) result = await flow.async_step_import() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" @@ -85,18 +86,18 @@ async def test_full_flow_implementation(hass: HomeAssistant, mock_logi_circle) - flow = init_config_flow(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await flow.async_step_user({"flow_impl": "test-other"}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["description_placeholders"] == { "authorization_url": "http://example.com" } result = await flow.async_step_code("123ABC") - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Logi Circle ({})".format("testId") @@ -114,7 +115,7 @@ async def test_abort_if_no_implementation_registered(hass: HomeAssistant) -> Non flow.hass = hass result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "missing_configuration" @@ -127,21 +128,21 @@ async def test_abort_if_already_setup(hass: HomeAssistant) -> None: config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" - with pytest.raises(data_entry_flow.AbortFlow): + with pytest.raises(AbortFlow): result = await flow.async_step_code() result = await flow.async_step_auth() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "external_setup" @@ -160,7 +161,7 @@ async def test_abort_if_authorize_fails( mock_logi_circle.authorize.side_effect = side_effect result = await flow.async_step_code("123ABC") - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "external_error" result = await flow.async_step_auth() @@ -172,7 +173,7 @@ async def test_not_pick_implementation_if_only_one(hass: HomeAssistant) -> None: flow = init_config_flow(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" diff --git a/tests/components/lookin/test_config_flow.py b/tests/components/lookin/test_config_flow.py index 18cbe33db3a..e3896b9c3d4 100644 --- a/tests/components/lookin/test_config_flow.py +++ b/tests/components/lookin/test_config_flow.py @@ -44,7 +44,7 @@ async def test_manual_setup(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_HOST: IP_ADDRESS} assert result["title"] == DEFAULT_ENTRY_TITLE assert len(mock_setup_entry.mock_calls) == 1 @@ -69,7 +69,7 @@ async def test_manual_setup_already_exists(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -122,7 +122,7 @@ async def test_discovered_zeroconf(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -134,7 +134,7 @@ async def test_discovered_zeroconf(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == {CONF_HOST: IP_ADDRESS} assert result2["title"] == DEFAULT_ENTRY_TITLE assert mock_async_setup_entry.called @@ -156,7 +156,7 @@ async def test_discovered_zeroconf(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == "127.0.0.2" @@ -172,7 +172,7 @@ async def test_discovered_zeroconf_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -187,5 +187,5 @@ async def test_discovered_zeroconf_unknown_exception(hass: HomeAssistant) -> Non ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" diff --git a/tests/components/loqed/test_config_flow.py b/tests/components/loqed/test_config_flow.py index ed18f0ae40e..d59ed60796b 100644 --- a/tests/components/loqed/test_config_flow.py +++ b/tests/components/loqed/test_config_flow.py @@ -42,7 +42,7 @@ async def test_create_entry_zeroconf(hass: HomeAssistant) -> None: data=zeroconf_data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None mock_lock = Mock(spec=loqed.Lock, id="Foo") @@ -76,7 +76,7 @@ async def test_create_entry_zeroconf(hass: HomeAssistant) -> None: await hass.async_block_till_done() found_lock = all_locks_response["data"][0] - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "LOQED Touch Smart Lock" assert result2["data"] == { "id": "Foo", @@ -101,7 +101,7 @@ async def test_create_entry_user( context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None lock_result = json.loads(load_fixture("loqed/status_ok.json")) @@ -137,7 +137,7 @@ async def test_create_entry_user( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "LOQED Touch Smart Lock" assert result2["data"] == { "id": "Foo", @@ -162,7 +162,7 @@ async def test_cannot_connect( context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -175,7 +175,7 @@ async def test_cannot_connect( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -188,7 +188,7 @@ async def test_invalid_auth_when_lock_not_found( context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None all_locks_response = json.loads(load_fixture("loqed/get_all_locks.json")) @@ -203,7 +203,7 @@ async def test_invalid_auth_when_lock_not_found( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -216,7 +216,7 @@ async def test_cannot_connect_when_lock_not_reachable( context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None all_locks_response = json.loads(load_fixture("loqed/get_all_locks.json")) @@ -236,5 +236,5 @@ async def test_cannot_connect_when_lock_not_reachable( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/luftdaten/test_config_flow.py b/tests/components/luftdaten/test_config_flow.py index 7469fe6e486..ea9b6211823 100644 --- a/tests/components/luftdaten/test_config_flow.py +++ b/tests/components/luftdaten/test_config_flow.py @@ -25,7 +25,7 @@ async def test_duplicate_error( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( @@ -33,7 +33,7 @@ async def test_duplicate_error( user_input={CONF_SENSOR_ID: 12345}, ) - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "already_configured" @@ -45,7 +45,7 @@ async def test_communication_error( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" mock_luftdaten.get_data.side_effect = LuftdatenConnectionError @@ -54,7 +54,7 @@ async def test_communication_error( user_input={CONF_SENSOR_ID: 12345}, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "user" assert result2.get("errors") == {CONF_SENSOR_ID: "cannot_connect"} @@ -64,7 +64,7 @@ async def test_communication_error( user_input={CONF_SENSOR_ID: 12345}, ) - assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY assert result3.get("title") == "12345" assert result3.get("data") == { CONF_SENSOR_ID: 12345, @@ -78,7 +78,7 @@ async def test_invalid_sensor(hass: HomeAssistant, mock_luftdaten: MagicMock) -> DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" mock_luftdaten.validate_sensor.return_value = False @@ -87,7 +87,7 @@ async def test_invalid_sensor(hass: HomeAssistant, mock_luftdaten: MagicMock) -> user_input={CONF_SENSOR_ID: 11111}, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "user" assert result2.get("errors") == {CONF_SENSOR_ID: "invalid_sensor"} @@ -97,7 +97,7 @@ async def test_invalid_sensor(hass: HomeAssistant, mock_luftdaten: MagicMock) -> user_input={CONF_SENSOR_ID: 12345}, ) - assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY assert result3.get("title") == "12345" assert result3.get("data") == { CONF_SENSOR_ID: 12345, @@ -114,7 +114,7 @@ async def test_step_user( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( @@ -125,7 +125,7 @@ async def test_step_user( }, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "12345" assert result2.get("data") == { CONF_SENSOR_ID: 12345, diff --git a/tests/components/lupusec/test_config_flow.py b/tests/components/lupusec/test_config_flow.py index 2ca313139dc..e106bbd5001 100644 --- a/tests/components/lupusec/test_config_flow.py +++ b/tests/components/lupusec/test_config_flow.py @@ -45,7 +45,7 @@ async def test_form_valid_input(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -63,7 +63,7 @@ async def test_form_valid_input(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == MOCK_DATA_STEP[CONF_HOST] assert result2["data"] == MOCK_DATA_STEP assert len(mock_setup_entry.mock_calls) == 1 @@ -85,7 +85,7 @@ async def test_flow_user_init_data_error_and_recover( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -98,7 +98,7 @@ async def test_flow_user_init_data_error_and_recover( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": text_error} assert len(mock_initialize_lupusec.mock_calls) == 1 @@ -120,7 +120,7 @@ async def test_flow_user_init_data_error_and_recover( await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == MOCK_DATA_STEP[CONF_HOST] assert result3["data"] == MOCK_DATA_STEP assert len(mock_setup_entry.mock_calls) == 1 @@ -141,7 +141,7 @@ async def test_flow_user_init_data_already_configured(hass: HomeAssistant) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -151,7 +151,7 @@ async def test_flow_user_init_data_already_configured(hass: HomeAssistant) -> No await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -183,7 +183,7 @@ async def test_flow_source_import( await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == mock_title assert result["data"] == MOCK_DATA_STEP assert len(mock_setup_entry.mock_calls) == 1 @@ -214,7 +214,7 @@ async def test_flow_source_import_error_and_recover( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == text_error assert len(mock_initialize_lupusec.mock_calls) == 1 @@ -236,5 +236,5 @@ async def test_flow_source_import_already_configured(hass: HomeAssistant) -> Non data=MOCK_IMPORT_STEP, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/lutron/test_config_flow.py b/tests/components/lutron/test_config_flow.py index db3faa7f911..e4904838e1a 100644 --- a/tests/components/lutron/test_config_flow.py +++ b/tests/components/lutron/test_config_flow.py @@ -27,7 +27,7 @@ async def test_full_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -39,7 +39,7 @@ async def test_full_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No user_input=MOCK_DATA_STEP, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].title == "Lutron" assert result["data"] == MOCK_DATA_STEP @@ -63,7 +63,7 @@ async def test_flow_failure( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -75,7 +75,7 @@ async def test_flow_failure( user_input=MOCK_DATA_STEP, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": text_error} with ( @@ -87,7 +87,7 @@ async def test_flow_failure( user_input=MOCK_DATA_STEP, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].title == "Lutron" assert result["data"] == MOCK_DATA_STEP @@ -101,7 +101,7 @@ async def test_flow_incorrect_guid( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -113,7 +113,7 @@ async def test_flow_incorrect_guid( user_input=MOCK_DATA_STEP, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} with ( @@ -125,7 +125,7 @@ async def test_flow_incorrect_guid( user_input=MOCK_DATA_STEP, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_flow_single_instance_allowed(hass: HomeAssistant) -> None: @@ -137,7 +137,7 @@ async def test_flow_single_instance_allowed(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -162,7 +162,7 @@ async def test_import( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == MOCK_DATA_IMPORT assert len(mock_setup_entry.mock_calls) == 1 @@ -187,7 +187,7 @@ async def test_import_flow_failure( DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_DATA_IMPORT ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == reason @@ -202,7 +202,7 @@ async def test_import_flow_guid_failure(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_DATA_IMPORT ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -218,5 +218,5 @@ async def test_import_already_configured(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_DATA_IMPORT ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/lutron_caseta/test_config_flow.py b/tests/components/lutron_caseta/test_config_flow.py index 15a4fca7d33..fd529353b98 100644 --- a/tests/components/lutron_caseta/test_config_flow.py +++ b/tests/components/lutron_caseta/test_config_flow.py @@ -9,7 +9,7 @@ from pylutron_caseta.pairing import PAIR_CA, PAIR_CERT, PAIR_KEY from pylutron_caseta.smartbridge import Smartbridge import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.lutron_caseta import DOMAIN import homeassistant.components.lutron_caseta.config_flow as CasetaConfigFlow @@ -22,6 +22,7 @@ from homeassistant.components.lutron_caseta.const import ( ) from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import ENTRY_MOCK_DATA, MockBridge @@ -107,7 +108,7 @@ async def test_bridge_cannot_connect(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == CasetaConfigFlow.ABORT_REASON_CANNOT_CONNECT @@ -130,7 +131,7 @@ async def test_bridge_cannot_connect_unknown_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == CasetaConfigFlow.ABORT_REASON_CANNOT_CONNECT @@ -150,7 +151,7 @@ async def test_bridge_invalid_ssl_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == CasetaConfigFlow.ABORT_REASON_CANNOT_CONNECT @@ -171,7 +172,7 @@ async def test_duplicate_bridge_import(hass: HomeAssistant) -> None: data=ENTRY_MOCK_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/lyric/test_config_flow.py b/tests/components/lyric/test_config_flow.py index 00d623ea3ce..d53b0d57613 100644 --- a/tests/components/lyric/test_config_flow.py +++ b/tests/components/lyric/test_config_flow.py @@ -5,13 +5,14 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.application_credentials import ( ClientCredential, async_import_client_credential, ) from homeassistant.components.lyric.const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN 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 @@ -39,7 +40,7 @@ async def test_abort_if_no_configuration(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "missing_credentials" @@ -62,7 +63,7 @@ async def test_full_flow( }, ) - assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" @@ -163,7 +164,7 @@ async def test_reauthentication_flow( ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(mock_setup.mock_calls) == 1 diff --git a/tests/components/matter/test_config_flow.py b/tests/components/matter/test_config_flow.py index 283642c8964..33e94a743f7 100644 --- a/tests/components/matter/test_config_flow.py +++ b/tests/components/matter/test_config_flow.py @@ -89,7 +89,7 @@ async def test_manual_create_entry( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -101,7 +101,7 @@ async def test_manual_create_entry( await hass.async_block_till_done() assert client_connect.call_count == 1 - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Matter" assert result["data"] == { "url": "ws://localhost:5580/ws", @@ -139,7 +139,7 @@ async def test_manual_errors( ) assert client_connect.call_count == 1 - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": error} @@ -158,7 +158,7 @@ async def test_manual_already_configured( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -170,7 +170,7 @@ async def test_manual_already_configured( await hass.async_block_till_done() assert client_connect.call_count == 1 - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfiguration_successful" assert entry.data["url"] == "ws://localhost:5580/ws" assert entry.data["use_addon"] is False @@ -198,7 +198,7 @@ async def test_zeroconf_discovery( properties={"SII": "3300", "SAI": "1100", "T": "0"}, ), ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" assert result["errors"] is None @@ -211,7 +211,7 @@ async def test_zeroconf_discovery( await hass.async_block_till_done() assert client_connect.call_count == 1 - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Matter" assert result["data"] == { "url": "ws://localhost:5580/ws", @@ -247,7 +247,7 @@ async def test_supervisor_discovery( assert addon_info.call_count == 1 assert client_connect.call_count == 0 - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Matter" assert result["data"] == { "url": "ws://host1:5581/ws", @@ -282,13 +282,13 @@ async def test_supervisor_discovery_addon_info_failed( ), ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "hassio_confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert addon_info.call_count == 1 - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "addon_info_failed" @@ -313,14 +313,14 @@ async def test_clean_supervisor_discovery_on_user_create( ), ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "hassio_confirm" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( @@ -328,7 +328,7 @@ async def test_clean_supervisor_discovery_on_user_create( ) assert addon_info.call_count == 0 - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" result = await hass.config_entries.flow.async_configure( @@ -341,7 +341,7 @@ async def test_clean_supervisor_discovery_on_user_create( assert len(hass.config_entries.flow.async_progress()) == 0 assert client_connect.call_count == 1 - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Matter" assert result["data"] == { "url": "ws://localhost:5580/ws", @@ -377,7 +377,7 @@ async def test_abort_supervisor_discovery_with_existing_entry( ) assert addon_info.call_count == 0 - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -392,7 +392,7 @@ async def test_abort_supervisor_discovery_with_existing_flow( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_init( @@ -407,7 +407,7 @@ async def test_abort_supervisor_discovery_with_existing_flow( ) assert addon_info.call_count == 0 - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -434,7 +434,7 @@ async def test_abort_supervisor_discovery_for_other_addon( ) assert addon_info.call_count == 0 - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_matter_addon" @@ -461,12 +461,12 @@ async def test_supervisor_discovery_addon_not_running( assert addon_info.call_count == 0 assert result["step_id"] == "hassio_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert addon_info.call_count == 1 - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" await hass.async_block_till_done() @@ -475,7 +475,7 @@ async def test_supervisor_discovery_addon_not_running( assert start_addon.call_args == call(hass, "core_matter_server") assert client_connect.call_count == 1 - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Matter" assert result["data"] == { "url": "ws://host1:5581/ws", @@ -511,20 +511,20 @@ async def test_supervisor_discovery_addon_not_installed( assert addon_info.call_count == 0 assert addon_store_info.call_count == 0 assert result["step_id"] == "hassio_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert addon_info.call_count == 0 assert addon_store_info.call_count == 1 assert result["step_id"] == "install_addon" - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS await hass.async_block_till_done() result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert install_addon.call_args == call(hass, "core_matter_server") - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" await hass.async_block_till_done() @@ -533,7 +533,7 @@ async def test_supervisor_discovery_addon_not_installed( assert start_addon.call_args == call(hass, "core_matter_server") assert client_connect.call_count == 1 - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Matter" assert result["data"] == { "url": "ws://host1:5581/ws", @@ -554,14 +554,14 @@ async def test_not_addon( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"use_addon": False} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" result = await hass.config_entries.flow.async_configure( @@ -573,7 +573,7 @@ async def test_not_addon( await hass.async_block_till_done() assert client_connect.call_count == 1 - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Matter" assert result["data"] == { "url": "ws://localhost:5581/ws", @@ -597,7 +597,7 @@ async def test_addon_running( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( @@ -607,7 +607,7 @@ async def test_addon_running( assert addon_info.call_count == 1 assert client_connect.call_count == 1 - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Matter" assert result["data"] == { "url": "ws://host1:5581/ws", @@ -688,7 +688,7 @@ async def test_addon_running_failures( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( @@ -698,7 +698,7 @@ async def test_addon_running_failures( assert addon_info.call_count == 1 assert get_addon_discovery_info.called is discovery_info_called assert client_connect.called is client_connect_called - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == abort_reason @@ -724,7 +724,7 @@ async def test_addon_running_already_configured( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( @@ -733,7 +733,7 @@ async def test_addon_running_already_configured( await hass.async_block_till_done() assert addon_info.call_count == 1 - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfiguration_successful" assert entry.data["url"] == "ws://host1:5581/ws" assert entry.title == "Matter" @@ -754,7 +754,7 @@ async def test_addon_installed( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( @@ -762,7 +762,7 @@ async def test_addon_installed( ) assert addon_info.call_count == 1 - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" await hass.async_block_till_done() @@ -770,7 +770,7 @@ async def test_addon_installed( await hass.async_block_till_done() assert start_addon.call_args == call(hass, "core_matter_server") - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Matter" assert result["data"] == { "url": "ws://host1:5581/ws", @@ -833,7 +833,7 @@ async def test_addon_installed_failures( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( @@ -841,7 +841,7 @@ async def test_addon_installed_failures( ) assert addon_info.call_count == 1 - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" await hass.async_block_till_done() @@ -850,7 +850,7 @@ async def test_addon_installed_failures( assert start_addon.call_args == call(hass, "core_matter_server") assert get_addon_discovery_info.called is discovery_info_called assert client_connect.called is client_connect_called - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "addon_start_failed" @@ -877,7 +877,7 @@ async def test_addon_installed_already_configured( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( @@ -885,7 +885,7 @@ async def test_addon_installed_already_configured( ) assert addon_info.call_count == 1 - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" await hass.async_block_till_done() @@ -893,7 +893,7 @@ async def test_addon_installed_already_configured( await hass.async_block_till_done() assert start_addon.call_args == call(hass, "core_matter_server") - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfiguration_successful" assert entry.data["url"] == "ws://host1:5581/ws" assert entry.title == "Matter" @@ -916,7 +916,7 @@ async def test_addon_not_installed( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( @@ -925,7 +925,7 @@ async def test_addon_not_installed( assert addon_info.call_count == 0 assert addon_store_info.call_count == 1 - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "install_addon" # Make sure the flow continues when the progress task is done. @@ -933,7 +933,7 @@ async def test_addon_not_installed( result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert install_addon.call_args == call(hass, "core_matter_server") - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" await hass.async_block_till_done() @@ -941,7 +941,7 @@ async def test_addon_not_installed( await hass.async_block_till_done() assert start_addon.call_args == call(hass, "core_matter_server") - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Matter" assert result["data"] == { "url": "ws://host1:5581/ws", @@ -965,14 +965,14 @@ async def test_addon_not_installed_failures( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"use_addon": True} ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "install_addon" # Make sure the flow continues when the progress task is done. @@ -981,7 +981,7 @@ async def test_addon_not_installed_failures( assert install_addon.call_args == call(hass, "core_matter_server") assert addon_info.call_count == 0 - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "addon_install_failed" @@ -1011,7 +1011,7 @@ async def test_addon_not_installed_already_configured( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( @@ -1020,7 +1020,7 @@ async def test_addon_not_installed_already_configured( assert addon_info.call_count == 0 assert addon_store_info.call_count == 1 - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "install_addon" # Make sure the flow continues when the progress task is done. @@ -1028,7 +1028,7 @@ async def test_addon_not_installed_already_configured( result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert install_addon.call_args == call(hass, "core_matter_server") - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" await hass.async_block_till_done() @@ -1037,7 +1037,7 @@ async def test_addon_not_installed_already_configured( assert start_addon.call_args == call(hass, "core_matter_server") assert client_connect.call_count == 1 - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfiguration_successful" assert entry.data["url"] == "ws://host1:5581/ws" assert entry.title == "Matter" diff --git a/tests/components/meater/test_config_flow.py b/tests/components/meater/test_config_flow.py index 6d294259c01..b8c1be15268 100644 --- a/tests/components/meater/test_config_flow.py +++ b/tests/components/meater/test_config_flow.py @@ -5,10 +5,11 @@ from unittest.mock import AsyncMock, patch from meater import AuthenticationError, ServiceUnavailableError import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.meater import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -39,7 +40,7 @@ async def test_duplicate_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -85,7 +86,7 @@ async def test_user_flow(hass: HomeAssistant, mock_meater) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -95,7 +96,7 @@ async def test_user_flow(hass: HomeAssistant, mock_meater) -> None: result = await hass.config_entries.flow.async_configure(result["flow_id"], conf) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_USERNAME: "user@host.com", CONF_PASSWORD: "password123", @@ -128,7 +129,7 @@ async def test_reauth_flow(hass: HomeAssistant, mock_meater) -> None: data=data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] is None @@ -138,7 +139,7 @@ async def test_reauth_flow(hass: HomeAssistant, mock_meater) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" config_entry = hass.config_entries.async_entries(DOMAIN)[0] diff --git a/tests/components/medcom_ble/test_config_flow.py b/tests/components/medcom_ble/test_config_flow.py index ca75dfd80ab..b470245978d 100644 --- a/tests/components/medcom_ble/test_config_flow.py +++ b/tests/components/medcom_ble/test_config_flow.py @@ -31,7 +31,7 @@ async def test_bluetooth_discovery(hass: HomeAssistant) -> None: data=MEDCOM_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" assert result["description_placeholders"] == {"name": "InspectorBLE-D9A0"} @@ -52,7 +52,7 @@ async def test_bluetooth_discovery(hass: HomeAssistant) -> None: result["flow_id"], user_input={"not": "empty"} ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "InspectorBLE-D9A0" assert result["result"].unique_id == "a0:d9:5a:57:0b:00" @@ -69,7 +69,7 @@ async def test_bluetooth_discovery_already_setup(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=MEDCOM_DEVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -82,7 +82,7 @@ async def test_user_setup(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None assert result["data_schema"] is not None @@ -113,7 +113,7 @@ async def test_user_setup(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "InspectorBLE-D9A0" assert result["result"].unique_id == "a0:d9:5a:57:0b:00" @@ -127,7 +127,7 @@ async def test_user_setup_no_device(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -145,7 +145,7 @@ async def test_user_setup_existing_and_unknown_device(hass: HomeAssistant) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None assert result["data_schema"] is not None @@ -154,7 +154,7 @@ async def test_user_setup_existing_and_unknown_device(hass: HomeAssistant) -> No result["flow_id"], user_input={CONF_ADDRESS: "a0:d9:5a:57:0b:00"} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -167,7 +167,7 @@ async def test_user_setup_unknown_device(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -180,7 +180,7 @@ async def test_user_setup_unknown_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None assert result["data_schema"] is not None @@ -193,7 +193,7 @@ async def test_user_setup_unknown_error(hass: HomeAssistant) -> None: result["flow_id"], user_input={CONF_ADDRESS: "a0:d9:5a:57:0b:00"} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -206,7 +206,7 @@ async def test_user_setup_unable_to_connect(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None assert result["data_schema"] is not None @@ -224,5 +224,5 @@ async def test_user_setup_unable_to_connect(hass: HomeAssistant) -> None: result["flow_id"], user_input={CONF_ADDRESS: "a0:d9:5a:57:0b:00"} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" diff --git a/tests/components/melcloud/test_config_flow.py b/tests/components/melcloud/test_config_flow.py index 0a21a8747e3..312daebd93f 100644 --- a/tests/components/melcloud/test_config_flow.py +++ b/tests/components/melcloud/test_config_flow.py @@ -174,7 +174,7 @@ async def test_token_reauthentication( }, data=mock_entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( @@ -187,7 +187,7 @@ async def test_token_reauthentication( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 @@ -231,7 +231,7 @@ async def test_form_errors_reauthentication( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == reason mock_login.side_effect = None @@ -245,7 +245,7 @@ async def test_form_errors_reauthentication( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" @@ -290,7 +290,7 @@ async def test_client_errors_reauthentication( await hass.async_block_till_done() assert result["errors"]["base"] == reason - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM mock_login.side_effect = None with patch( @@ -303,5 +303,5 @@ async def test_client_errors_reauthentication( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" diff --git a/tests/components/melnor/test_config_flow.py b/tests/components/melnor/test_config_flow.py index ae4e7b84288..377954c22df 100644 --- a/tests/components/melnor/test_config_flow.py +++ b/tests/components/melnor/test_config_flow.py @@ -29,7 +29,7 @@ async def test_user_step_no_devices( context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" mock_setup_entry.assert_not_called() @@ -46,7 +46,7 @@ async def test_user_step_discovered_devices( context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pick_device" with pytest.raises(vol.Invalid): @@ -58,7 +58,7 @@ async def test_user_step_discovered_devices( result["flow_id"], user_input={CONF_ADDRESS: FAKE_ADDRESS_1} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == {CONF_ADDRESS: FAKE_ADDRESS_1} mock_setup_entry.assert_called_once() @@ -94,7 +94,7 @@ async def test_user_step_with_existing_device( context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with pytest.raises(vol.Invalid): await hass.config_entries.flow.async_configure( @@ -115,7 +115,7 @@ async def test_bluetooth_discovered( data=FAKE_SERVICE_INFO_1, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" assert result["description_placeholders"] == {"name": FAKE_ADDRESS_1} @@ -143,7 +143,7 @@ async def test_bluetooth_confirm( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == FAKE_ADDRESS_1 assert result2["data"] == {CONF_ADDRESS: FAKE_ADDRESS_1} diff --git a/tests/components/met_eireann/test_config_flow.py b/tests/components/met_eireann/test_config_flow.py index 8c5e7f43ced..2bb5c9ffe8f 100644 --- a/tests/components/met_eireann/test_config_flow.py +++ b/tests/components/met_eireann/test_config_flow.py @@ -4,10 +4,11 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.met_eireann.const import DOMAIN, HOME_LOCATION_NAME from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType @pytest.fixture(name="met_eireann_setup", autouse=True) @@ -66,7 +67,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == test_data.get("name") assert result["data"] == test_data @@ -87,11 +88,11 @@ async def test_flow_entry_already_exists(hass: HomeAssistant) -> None: result1 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data ) - assert result1["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result1["type"] is FlowResultType.CREATE_ENTRY # Create the second entry and assert that it is aborted result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/meteo_france/test_config_flow.py b/tests/components/meteo_france/test_config_flow.py index 4b9e26f883b..5e6f3b845e9 100644 --- a/tests/components/meteo_france/test_config_flow.py +++ b/tests/components/meteo_france/test_config_flow.py @@ -5,11 +5,11 @@ from unittest.mock import patch from meteofrance_api.model import Place import pytest -from homeassistant import data_entry_flow from homeassistant.components.meteo_france.const import CONF_CITY, DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -114,7 +114,7 @@ async def test_user(hass: HomeAssistant, client_single) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # test with all provided with search returning only 1 place @@ -123,7 +123,7 @@ async def test_user(hass: HomeAssistant, client_single) -> None: context={"source": SOURCE_USER}, data={CONF_CITY: CITY_1_POSTAL}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == f"{CITY_1_LAT}, {CITY_1_LON}" assert result["title"] == f"{CITY_1}" assert result["data"][CONF_LATITUDE] == str(CITY_1_LAT) @@ -139,14 +139,14 @@ async def test_user_list(hass: HomeAssistant, client_multiple) -> None: context={"source": SOURCE_USER}, data={CONF_CITY: CITY_2_NAME}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cities" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_CITY: f"{CITY_3};{CITY_3_LAT};{CITY_3_LON}"}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == f"{CITY_3_LAT}, {CITY_3_LON}" assert result["title"] == f"{CITY_3}" assert result["data"][CONF_LATITUDE] == str(CITY_3_LAT) @@ -161,7 +161,7 @@ async def test_search_failed(hass: HomeAssistant, client_empty) -> None: data={CONF_CITY: CITY_1_POSTAL}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_CITY: "empty"} @@ -179,5 +179,5 @@ async def test_abort_if_already_setup(hass: HomeAssistant, client_single) -> Non context={"source": SOURCE_USER}, data={CONF_CITY: CITY_1_POSTAL}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/meteoclimatic/test_config_flow.py b/tests/components/meteoclimatic/test_config_flow.py index 42fb5e78d4a..ff9de358e86 100644 --- a/tests/components/meteoclimatic/test_config_flow.py +++ b/tests/components/meteoclimatic/test_config_flow.py @@ -5,10 +5,10 @@ from unittest.mock import patch from meteoclimatic.exceptions import MeteoclimaticError, StationNotFound import pytest -from homeassistant import data_entry_flow from homeassistant.components.meteoclimatic.const import CONF_STATION_CODE, DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType TEST_STATION_CODE = "ESCAT4300000043206B" TEST_STATION_NAME = "Reus (Tarragona)" @@ -44,7 +44,7 @@ async def test_user(hass: HomeAssistant, client) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # test with all provided @@ -53,7 +53,7 @@ async def test_user(hass: HomeAssistant, client) -> None: context={"source": SOURCE_USER}, data={CONF_STATION_CODE: TEST_STATION_CODE}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == TEST_STATION_CODE assert result["title"] == TEST_STATION_NAME assert result["data"][CONF_STATION_CODE] == TEST_STATION_CODE @@ -70,7 +70,7 @@ async def test_not_found(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, data={CONF_STATION_CODE: TEST_STATION_CODE}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "not_found" @@ -86,5 +86,5 @@ async def test_unknown_error(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, data={CONF_STATION_CODE: TEST_STATION_CODE}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" diff --git a/tests/components/microbees/test_config_flow.py b/tests/components/microbees/test_config_flow.py index 5e57c723f5b..9f8ca8c7747 100644 --- a/tests/components/microbees/test_config_flow.py +++ b/tests/components/microbees/test_config_flow.py @@ -37,7 +37,7 @@ async def test_full_flow( "redirect_uri": "https://example.com/auth/external/callback", }, ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{MICROBEES_AUTH_URI}?" f"response_type=code&client_id={CLIENT_ID}&" @@ -71,7 +71,7 @@ async def test_full_flow( assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(mock_setup.mock_calls) == 1 - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "test@microbees.com" assert "result" in result assert result["result"].unique_id == 54321 @@ -101,7 +101,7 @@ async def test_config_non_unique_profile( }, ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{MICROBEES_AUTH_URI}?" f"response_type=code&client_id={CLIENT_ID}&" @@ -129,7 +129,7 @@ async def test_config_non_unique_profile( ) result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -190,7 +190,7 @@ async def test_config_reauth_profile( result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" @@ -251,7 +251,7 @@ async def test_config_reauth_wrong_account( result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "wrong_account" @@ -274,7 +274,7 @@ async def test_config_flow_with_invalid_credentials( }, ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{MICROBEES_AUTH_URI}?" f"response_type=code&client_id={CLIENT_ID}&" @@ -299,7 +299,7 @@ async def test_config_flow_with_invalid_credentials( result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "oauth_error" @@ -334,7 +334,7 @@ async def test_unexpected_exceptions( "redirect_uri": "https://example.com/auth/external/callback", }, ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{MICROBEES_AUTH_URI}?" f"response_type=code&client_id={CLIENT_ID}&" @@ -362,5 +362,5 @@ async def test_unexpected_exceptions( result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == error diff --git a/tests/components/mikrotik/test_config_flow.py b/tests/components/mikrotik/test_config_flow.py index f48446e3e14..0cc62b9bd8e 100644 --- a/tests/components/mikrotik/test_config_flow.py +++ b/tests/components/mikrotik/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import patch import librouteros import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.mikrotik.const import ( CONF_ARP_PING, CONF_DETECTION_TIME, @@ -20,6 +20,7 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -75,14 +76,14 @@ async def test_flow_works(hass: HomeAssistant, api) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=DEMO_USER_INPUT ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Mikrotik (0.0.0.0)" assert result["data"][CONF_HOST] == "0.0.0.0" assert result["data"][CONF_USERNAME] == "username" @@ -100,7 +101,7 @@ async def test_options(hass: HomeAssistant, api) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "device_tracker" result = await hass.config_entries.options.async_configure( @@ -112,7 +113,7 @@ async def test_options(hass: HomeAssistant, api) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_DETECTION_TIME: 30, CONF_ARP_PING: True, @@ -145,7 +146,7 @@ async def test_connection_error(hass: HomeAssistant, conn_error) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=DEMO_USER_INPUT ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -159,7 +160,7 @@ async def test_wrong_credentials(hass: HomeAssistant, auth_error) -> None: result["flow_id"], user_input=DEMO_USER_INPUT ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == { CONF_USERNAME: "invalid_auth", CONF_PASSWORD: "invalid_auth", diff --git a/tests/components/mill/test_config_flow.py b/tests/components/mill/test_config_flow.py index d7740502412..23c7c3d0e67 100644 --- a/tests/components/mill/test_config_flow.py +++ b/tests/components/mill/test_config_flow.py @@ -17,7 +17,7 @@ async def test_show_config_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -26,7 +26,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -35,7 +35,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: CONNECTION_TYPE: CLOUD, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM with patch("mill.Mill.connect", return_value=True): result = await hass.config_entries.flow.async_configure( @@ -74,7 +74,7 @@ async def test_flow_entry_already_exists(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -83,7 +83,7 @@ async def test_flow_entry_already_exists(hass: HomeAssistant) -> None: CONNECTION_TYPE: CLOUD, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM with patch("mill.Mill.connect", return_value=True): result = await hass.config_entries.flow.async_configure( @@ -101,7 +101,7 @@ async def test_connection_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -110,7 +110,7 @@ async def test_connection_error(hass: HomeAssistant) -> None: CONNECTION_TYPE: CLOUD, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM with patch("mill.Mill.connect", return_value=False): result = await hass.config_entries.flow.async_configure( @@ -121,7 +121,7 @@ async def test_connection_error(hass: HomeAssistant) -> None: }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -130,7 +130,7 @@ async def test_local_create_entry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -139,7 +139,7 @@ async def test_local_create_entry(hass: HomeAssistant) -> None: CONNECTION_TYPE: LOCAL, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM test_data = { CONF_IP_ADDRESS: "192.168.1.59", @@ -182,7 +182,7 @@ async def test_local_flow_entry_already_exists(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -191,7 +191,7 @@ async def test_local_flow_entry_already_exists(hass: HomeAssistant) -> None: CONNECTION_TYPE: LOCAL, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM test_data = { CONF_IP_ADDRESS: "192.168.1.59", @@ -221,7 +221,7 @@ async def test_local_connection_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -230,7 +230,7 @@ async def test_local_connection_error(hass: HomeAssistant) -> None: CONNECTION_TYPE: LOCAL, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM test_data = { CONF_IP_ADDRESS: "192.168.1.59", @@ -245,5 +245,5 @@ async def test_local_connection_error(hass: HomeAssistant) -> None: test_data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/min_max/test_config_flow.py b/tests/components/min_max/test_config_flow.py index 367c4cd717d..4a408524d09 100644 --- a/tests/components/min_max/test_config_flow.py +++ b/tests/components/min_max/test_config_flow.py @@ -20,7 +20,7 @@ async def test_config_flow(hass: HomeAssistant, platform: str) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -33,7 +33,7 @@ async def test_config_flow(hass: HomeAssistant, platform: str) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "My min_max" assert result["data"] == {} assert result["options"] == { @@ -93,7 +93,7 @@ async def test_options(hass: HomeAssistant, platform: str) -> None: 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["type"] is FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema assert get_suggested(schema, "entity_ids") == input_sensors1 @@ -108,7 +108,7 @@ async def test_options(hass: HomeAssistant, platform: str) -> None: "type": "mean", }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "entity_ids": input_sensors2, "name": "My min_max", diff --git a/tests/components/minecraft_server/test_config_flow.py b/tests/components/minecraft_server/test_config_flow.py index 188b68ce5af..21136ac0815 100644 --- a/tests/components/minecraft_server/test_config_flow.py +++ b/tests/components/minecraft_server/test_config_flow.py @@ -31,7 +31,7 @@ async def test_show_config_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -51,7 +51,7 @@ async def test_address_validation_failure(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -75,7 +75,7 @@ async def test_java_connection_failure(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -95,7 +95,7 @@ async def test_bedrock_connection_failure(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -119,7 +119,7 @@ async def test_java_connection(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == USER_INPUT[CONF_ADDRESS] assert result["data"][CONF_NAME] == USER_INPUT[CONF_NAME] assert result["data"][CONF_ADDRESS] == TEST_ADDRESS @@ -142,7 +142,7 @@ async def test_bedrock_connection(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == USER_INPUT[CONF_ADDRESS] assert result["data"][CONF_NAME] == USER_INPUT[CONF_NAME] assert result["data"][CONF_ADDRESS] == TEST_ADDRESS @@ -164,7 +164,7 @@ async def test_recovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} with ( @@ -180,7 +180,7 @@ async def test_recovery(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( flow_id=result["flow_id"], user_input=USER_INPUT ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == USER_INPUT[CONF_ADDRESS] assert result2["data"][CONF_NAME] == USER_INPUT[CONF_NAME] assert result2["data"][CONF_ADDRESS] == TEST_ADDRESS diff --git a/tests/components/mjpeg/test_config_flow.py b/tests/components/mjpeg/test_config_flow.py index a60df88b789..c2d5a9b014b 100644 --- a/tests/components/mjpeg/test_config_flow.py +++ b/tests/components/mjpeg/test_config_flow.py @@ -35,7 +35,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( @@ -50,7 +50,7 @@ async def test_full_user_flow( }, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "Spy cam" assert result2.get("data") == {} assert result2.get("options") == { @@ -80,7 +80,7 @@ async def test_full_flow_with_authentication_error( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" mock_mjpeg_requests.get( @@ -96,7 +96,7 @@ async def test_full_flow_with_authentication_error( }, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "user" assert result2.get("errors") == {"username": "invalid_auth"} @@ -114,7 +114,7 @@ async def test_full_flow_with_authentication_error( }, ) - assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY assert result3.get("title") == "Sky cam" assert result3.get("data") == {} assert result3.get("options") == { @@ -140,7 +140,7 @@ async def test_connection_error( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" # Test connectione error on MJPEG url @@ -156,7 +156,7 @@ async def test_connection_error( }, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "user" assert result2.get("errors") == {"mjpeg_url": "cannot_connect"} @@ -179,7 +179,7 @@ async def test_connection_error( }, ) - assert result3.get("type") == FlowResultType.FORM + assert result3.get("type") is FlowResultType.FORM assert result3.get("step_id") == "user" assert result3.get("errors") == {"still_image_url": "cannot_connect"} @@ -199,7 +199,7 @@ async def test_connection_error( }, ) - assert result4.get("type") == FlowResultType.CREATE_ENTRY + assert result4.get("type") is FlowResultType.CREATE_ENTRY assert result4.get("title") == "My cam" assert result4.get("data") == {} assert result4.get("options") == { @@ -236,7 +236,7 @@ async def test_already_configured( }, ) - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "already_configured" @@ -248,7 +248,7 @@ async def test_options_flow( """Test options config flow.""" result = await hass.config_entries.options.async_init(init_integration.entry_id) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "init" # Register a second camera @@ -276,7 +276,7 @@ async def test_options_flow( }, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "init" assert result2.get("errors") == {"mjpeg_url": "already_configured"} @@ -294,7 +294,7 @@ async def test_options_flow( }, ) - assert result3.get("type") == FlowResultType.FORM + assert result3.get("type") is FlowResultType.FORM assert result3.get("step_id") == "init" assert result3.get("errors") == {"mjpeg_url": "cannot_connect"} @@ -312,7 +312,7 @@ async def test_options_flow( }, ) - assert result4.get("type") == FlowResultType.FORM + assert result4.get("type") is FlowResultType.FORM assert result4.get("step_id") == "init" assert result4.get("errors") == {"still_image_url": "cannot_connect"} @@ -331,7 +331,7 @@ async def test_options_flow( }, ) - assert result5.get("type") == FlowResultType.FORM + assert result5.get("type") is FlowResultType.FORM assert result5.get("step_id") == "init" assert result5.get("errors") == {"username": "invalid_auth"} @@ -347,7 +347,7 @@ async def test_options_flow( }, ) - assert result6.get("type") == FlowResultType.CREATE_ENTRY + assert result6.get("type") is FlowResultType.CREATE_ENTRY assert result6.get("data") == { CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, CONF_MJPEG_URL: "https://example.com/mjpeg", diff --git a/tests/components/moat/test_config_flow.py b/tests/components/moat/test_config_flow.py index ab0825c884e..43840330313 100644 --- a/tests/components/moat/test_config_flow.py +++ b/tests/components/moat/test_config_flow.py @@ -19,13 +19,13 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=MOAT_S2_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch("homeassistant.components.moat.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Moat S2 EEFF" assert result2["data"] == {} assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" @@ -38,7 +38,7 @@ async def test_async_step_bluetooth_not_moat(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_MOAT_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -48,7 +48,7 @@ async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -62,14 +62,14 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch("homeassistant.components.moat.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Moat S2 EEFF" assert result2["data"] == {} assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" @@ -85,7 +85,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -99,7 +99,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -121,7 +121,7 @@ async def test_async_step_user_with_found_devices_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -138,7 +138,7 @@ async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant) - context={"source": config_entries.SOURCE_BLUETOOTH}, data=MOAT_S2_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -149,7 +149,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=MOAT_S2_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -157,7 +157,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=MOAT_S2_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -170,7 +170,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=MOAT_S2_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -181,14 +181,14 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch("homeassistant.components.moat.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Moat S2 EEFF" assert result2["data"] == {} assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" diff --git a/tests/components/modem_callerid/test_config_flow.py b/tests/components/modem_callerid/test_config_flow.py index aeb8fb6d966..2ae4d6659e7 100644 --- a/tests/components/modem_callerid/test_config_flow.py +++ b/tests/components/modem_callerid/test_config_flow.py @@ -4,12 +4,12 @@ from unittest.mock import MagicMock, patch import phone_modem -from homeassistant import data_entry_flow from homeassistant.components import usb from homeassistant.components.modem_callerid.const import DOMAIN from homeassistant.config_entries import SOURCE_USB, SOURCE_USER from homeassistant.const import CONF_DEVICE, CONF_SOURCE from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import com_port, patch_config_flow_modem @@ -38,14 +38,14 @@ async def test_flow_usb(hass: HomeAssistant) -> None: context={CONF_SOURCE: SOURCE_USB}, data=DISCOVERY_INFO, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "usb_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_DEVICE: phone_modem.DEFAULT_PORT}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_DEVICE: com_port().device} @@ -57,7 +57,7 @@ async def test_flow_usb_cannot_connect(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USB}, data=DISCOVERY_INFO ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -79,7 +79,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: context={CONF_SOURCE: SOURCE_USER}, data={CONF_DEVICE: port_select}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_DEVICE: port.device} result = await hass.config_entries.flow.async_init( @@ -87,7 +87,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: context={CONF_SOURCE: SOURCE_USER}, data={CONF_DEVICE: port_select}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -108,7 +108,7 @@ async def test_flow_user_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data={CONF_DEVICE: port_select} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -117,7 +117,7 @@ async def test_flow_user_error(hass: HomeAssistant) -> None: result["flow_id"], user_input={CONF_DEVICE: port_select}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_DEVICE: port.device} @@ -130,7 +130,7 @@ async def test_flow_user_no_port_list(hass: HomeAssistant) -> None: context={CONF_SOURCE: SOURCE_USER}, data={CONF_DEVICE: phone_modem.DEFAULT_PORT}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -142,7 +142,7 @@ async def test_abort_user_with_existing_flow(hass: HomeAssistant) -> None: context={CONF_SOURCE: SOURCE_USB}, data=DISCOVERY_INFO, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "usb_confirm" result2 = await hass.config_entries.flow.async_init( @@ -151,5 +151,5 @@ async def test_abort_user_with_existing_flow(hass: HomeAssistant) -> None: data={}, ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_in_progress" diff --git a/tests/components/modern_forms/test_config_flow.py b/tests/components/modern_forms/test_config_flow.py index 6e1d2452479..56c293b241a 100644 --- a/tests/components/modern_forms/test_config_flow.py +++ b/tests/components/modern_forms/test_config_flow.py @@ -35,7 +35,7 @@ async def test_full_user_flow_implementation( ) assert result.get("step_id") == "user" - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM with patch( "homeassistant.components.modern_forms.async_setup_entry", @@ -47,7 +47,7 @@ async def test_full_user_flow_implementation( assert result2.get("title") == "ModernFormsFan" assert "data" in result2 - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2["data"][CONF_HOST] == "192.168.1.123" assert result2["data"][CONF_MAC] == "AA:BB:CC:DD:EE:FF" assert len(mock_setup_entry.mock_calls) == 1 @@ -82,7 +82,7 @@ async def test_full_zeroconf_flow_implementation( assert result.get("description_placeholders") == {CONF_NAME: "example"} assert result.get("step_id") == "zeroconf_confirm" - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM flow = flows[0] assert "context" in flow @@ -94,7 +94,7 @@ async def test_full_zeroconf_flow_implementation( ) assert result2.get("title") == "example" - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert "data" in result2 assert result2["data"][CONF_HOST] == "192.168.1.123" @@ -117,7 +117,7 @@ async def test_connection_error( data={CONF_HOST: "example.com"}, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" assert result.get("errors") == {"base": "cannot_connect"} @@ -146,7 +146,7 @@ async def test_zeroconf_connection_error( ), ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "cannot_connect" @@ -178,7 +178,7 @@ async def test_zeroconf_confirm_connection_error( ), ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "cannot_connect" @@ -214,7 +214,7 @@ async def test_user_device_exists_abort( }, ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -248,5 +248,5 @@ async def test_zeroconf_with_mac_device_exists_abort( ), ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" diff --git a/tests/components/moehlenhoff_alpha2/test_config_flow.py b/tests/components/moehlenhoff_alpha2/test_config_flow.py index fcce2d139d2..33c67421958 100644 --- a/tests/components/moehlenhoff_alpha2/test_config_flow.py +++ b/tests/components/moehlenhoff_alpha2/test_config_flow.py @@ -29,7 +29,7 @@ async def test_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] with ( @@ -45,7 +45,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == MOCK_BASE_NAME assert result2["data"] == {"host": MOCK_BASE_HOST} assert len(mock_setup_entry.mock_calls) == 1 @@ -68,7 +68,7 @@ async def test_form_duplicate_error(hass: HomeAssistant) -> None: data={"host": MOCK_BASE_HOST}, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -83,7 +83,7 @@ async def test_form_cannot_connect_error(hass: HomeAssistant) -> None: user_input={"host": MOCK_BASE_HOST}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -98,5 +98,5 @@ async def test_form_unexpected_error(hass: HomeAssistant) -> None: user_input={"host": MOCK_BASE_HOST}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/monoprice/test_config_flow.py b/tests/components/monoprice/test_config_flow.py index 74c69078b1d..30d0266eeea 100644 --- a/tests/components/monoprice/test_config_flow.py +++ b/tests/components/monoprice/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch from serial import SerialException -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.monoprice.const import ( CONF_SOURCE_1, CONF_SOURCE_4, @@ -14,6 +14,7 @@ from homeassistant.components.monoprice.const import ( ) from homeassistant.const import CONF_PORT from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -112,7 +113,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -120,5 +121,5 @@ async def test_options_flow(hass: HomeAssistant) -> None: user_input={CONF_SOURCE_1: "one", CONF_SOURCE_4: "", CONF_SOURCE_5: "five"}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options[CONF_SOURCES] == {"1": "one", "5": "five"} diff --git a/tests/components/moon/test_config_flow.py b/tests/components/moon/test_config_flow.py index 8fbab51f5a2..aac4381de59 100644 --- a/tests/components/moon/test_config_flow.py +++ b/tests/components/moon/test_config_flow.py @@ -19,7 +19,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( @@ -27,7 +27,7 @@ async def test_full_user_flow( user_input={}, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "Moon" assert result2.get("data") == {} @@ -43,5 +43,5 @@ async def test_single_instance_allowed( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" diff --git a/tests/components/mopeka/test_config_flow.py b/tests/components/mopeka/test_config_flow.py index 8de1fd81add..826fe8db2aa 100644 --- a/tests/components/mopeka/test_config_flow.py +++ b/tests/components/mopeka/test_config_flow.py @@ -19,13 +19,13 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=PRO_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch("homeassistant.components.mopeka.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Pro Plus EEFF" assert result2["data"] == {} assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" @@ -38,7 +38,7 @@ async def test_async_step_bluetooth_not_mopeka(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_MOPEKA_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -48,7 +48,7 @@ async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -62,14 +62,14 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch("homeassistant.components.mopeka.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Pro Plus EEFF" assert result2["data"] == {} assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" @@ -85,7 +85,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -99,7 +99,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -121,7 +121,7 @@ async def test_async_step_user_with_found_devices_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -138,7 +138,7 @@ async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant) - context={"source": config_entries.SOURCE_BLUETOOTH}, data=PRO_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -149,7 +149,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=PRO_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -157,7 +157,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=PRO_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -170,7 +170,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=PRO_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -181,14 +181,14 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch("homeassistant.components.mopeka.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Pro Plus EEFF" assert result2["data"] == {} assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" diff --git a/tests/components/motion_blinds/test_config_flow.py b/tests/components/motion_blinds/test_config_flow.py index 8d290b0b380..ac49d51f7ef 100644 --- a/tests/components/motion_blinds/test_config_flow.py +++ b/tests/components/motion_blinds/test_config_flow.py @@ -5,12 +5,13 @@ from unittest.mock import Mock, patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.components.motion_blinds import const from homeassistant.components.motion_blinds.config_flow import DEFAULT_GATEWAY_NAME from homeassistant.const import CONF_API_KEY, CONF_HOST from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -435,7 +436,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -443,7 +444,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: user_input={const.CONF_WAIT_FOR_PUSH: False}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { const.CONF_WAIT_FOR_PUSH: False, } diff --git a/tests/components/motionblinds_ble/test_config_flow.py b/tests/components/motionblinds_ble/test_config_flow.py index 9451e04830a..7b964f7d5e9 100644 --- a/tests/components/motionblinds_ble/test_config_flow.py +++ b/tests/components/motionblinds_ble/test_config_flow.py @@ -4,11 +4,12 @@ from unittest.mock import patch from motionblindsble.const import MotionBlindType -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.bluetooth.models import BluetoothServiceInfoBleak from homeassistant.components.motionblinds_ble import const from homeassistant.const import CONF_ADDRESS from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .conftest import TEST_ADDRESS, TEST_MAC, TEST_NAME @@ -48,7 +49,7 @@ async def test_config_flow_manual_success( result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -56,14 +57,14 @@ async def test_config_flow_manual_success( result["flow_id"], {const.CONF_MAC_CODE: TEST_MAC}, ) - assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], {const.CONF_BLIND_TYPE: MotionBlindType.ROLLER.name.lower()}, ) - assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"Motionblind {TEST_MAC.upper()}" assert result["data"] == { CONF_ADDRESS: TEST_ADDRESS, @@ -83,7 +84,7 @@ async def test_config_flow_manual_error_invalid_mac( result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -92,7 +93,7 @@ async def test_config_flow_manual_error_invalid_mac( result["flow_id"], {const.CONF_MAC_CODE: "AABBCC"}, # A MAC code should be 4 characters ) - assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": const.ERROR_INVALID_MAC_CODE} @@ -101,7 +102,7 @@ async def test_config_flow_manual_error_invalid_mac( result["flow_id"], {const.CONF_MAC_CODE: TEST_MAC}, ) - assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" # Finish flow @@ -109,7 +110,7 @@ async def test_config_flow_manual_error_invalid_mac( result["flow_id"], {const.CONF_BLIND_TYPE: MotionBlindType.ROLLER.name.lower()}, ) - assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"Motionblind {TEST_MAC.upper()}" assert result["data"] == { CONF_ADDRESS: TEST_ADDRESS, @@ -133,14 +134,14 @@ async def test_config_flow_manual_error_no_bluetooth_adapter( result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] is data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == const.ERROR_NO_BLUETOOTH_ADAPTER # Try discovery with zero Bluetooth adapters result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -152,7 +153,7 @@ async def test_config_flow_manual_error_no_bluetooth_adapter( result["flow_id"], {const.CONF_MAC_CODE: TEST_MAC}, ) - assert result["type"] is data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == const.ERROR_NO_BLUETOOTH_ADAPTER @@ -165,7 +166,7 @@ async def test_config_flow_manual_error_could_not_find_motor( result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -175,7 +176,7 @@ async def test_config_flow_manual_error_could_not_find_motor( result["flow_id"], {const.CONF_MAC_CODE: TEST_MAC}, ) - assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": const.ERROR_COULD_NOT_FIND_MOTOR} @@ -185,7 +186,7 @@ async def test_config_flow_manual_error_could_not_find_motor( result["flow_id"], {const.CONF_MAC_CODE: TEST_MAC}, ) - assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" # Finish flow @@ -193,7 +194,7 @@ async def test_config_flow_manual_error_could_not_find_motor( result["flow_id"], {const.CONF_BLIND_TYPE: MotionBlindType.ROLLER.name.lower()}, ) - assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"Motionblind {TEST_MAC.upper()}" assert result["data"] == { CONF_ADDRESS: TEST_ADDRESS, @@ -213,7 +214,7 @@ async def test_config_flow_manual_error_no_devices_found( result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -223,7 +224,7 @@ async def test_config_flow_manual_error_no_devices_found( result["flow_id"], {const.CONF_MAC_CODE: TEST_MAC}, ) - assert result["type"] is data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == const.ERROR_NO_DEVICES_FOUND @@ -237,7 +238,7 @@ async def test_config_flow_bluetooth_success( data=BLIND_SERVICE_INFO, ) - assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -245,7 +246,7 @@ async def test_config_flow_bluetooth_success( {const.CONF_BLIND_TYPE: MotionBlindType.ROLLER.name.lower()}, ) - assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"Motionblind {TEST_MAC.upper()}" assert result["data"] == { CONF_ADDRESS: TEST_ADDRESS, diff --git a/tests/components/motioneye/test_config_flow.py b/tests/components/motioneye/test_config_flow.py index 7163f2c8152..404200bd01a 100644 --- a/tests/components/motioneye/test_config_flow.py +++ b/tests/components/motioneye/test_config_flow.py @@ -8,7 +8,7 @@ from motioneye_client.client import ( MotionEyeClientRequestError, ) -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.hassio import HassioServiceInfo from homeassistant.components.motioneye.const import ( CONF_ADMIN_PASSWORD, @@ -22,6 +22,7 @@ from homeassistant.components.motioneye.const import ( ) from homeassistant.const import CONF_URL, CONF_WEBHOOK_ID from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import TEST_URL, create_mock_motioneye_client, create_mock_motioneye_config_entry @@ -88,12 +89,12 @@ async def test_hassio_success(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_HASSIO}, ) - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "hassio_confirm" assert result.get("description_placeholders") == {"addon": "motionEye"} result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2.get("type") == data_entry_flow.FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "user" mock_client = create_mock_motioneye_client() @@ -119,7 +120,7 @@ async def test_hassio_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY assert result3.get("title") == "Add-on" assert result3.get("data") == { CONF_URL: TEST_URL, @@ -300,7 +301,7 @@ async def test_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert dict(config_entry.data) == {**new_data, CONF_WEBHOOK_ID: "test-webhook-id"} @@ -350,7 +351,7 @@ async def test_duplicate(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_client.async_client_close.called @@ -372,7 +373,7 @@ async def test_hassio_already_configured(hass: HomeAssistant) -> None: ), context={"source": config_entries.SOURCE_HASSIO}, ) - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -392,7 +393,7 @@ async def test_hassio_ignored(hass: HomeAssistant) -> None: ), context={"source": config_entries.SOURCE_HASSIO}, ) - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -401,7 +402,7 @@ async def test_hassio_abort_if_already_in_progress(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM result2 = await hass.config_entries.flow.async_init( DOMAIN, @@ -413,7 +414,7 @@ async def test_hassio_abort_if_already_in_progress(hass: HomeAssistant) -> None: ), context={"source": config_entries.SOURCE_HASSIO}, ) - assert result2.get("type") == data_entry_flow.FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "already_in_progress" @@ -430,12 +431,12 @@ async def test_hassio_clean_up_on_user_flow(hass: HomeAssistant) -> None: ), context={"source": config_entries.SOURCE_HASSIO}, ) - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result2.get("type") == data_entry_flow.FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM mock_client = create_mock_motioneye_client() @@ -461,7 +462,7 @@ async def test_hassio_clean_up_on_user_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY assert len(mock_setup_entry.mock_calls) == 1 flows = hass.config_entries.flow.async_progress() @@ -487,7 +488,7 @@ async def test_options(hass: HomeAssistant) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -498,7 +499,7 @@ async def test_options(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_WEBHOOK_SET] assert result["data"][CONF_WEBHOOK_SET_OVERWRITE] assert CONF_STREAM_URL_TEMPLATE not in result["data"] @@ -533,7 +534,7 @@ async def test_advanced_options(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_WEBHOOK_SET] assert result["data"][CONF_WEBHOOK_SET_OVERWRITE] assert CONF_STREAM_URL_TEMPLATE not in result["data"] @@ -552,7 +553,7 @@ async def test_advanced_options(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_WEBHOOK_SET] assert result["data"][CONF_WEBHOOK_SET_OVERWRITE] assert result["data"][CONF_STREAM_URL_TEMPLATE] == "http://moo" diff --git a/tests/components/motionmount/test_config_flow.py b/tests/components/motionmount/test_config_flow.py index f24c4e7a2e4..4de23de63c9 100644 --- a/tests/components/motionmount/test_config_flow.py +++ b/tests/components/motionmount/test_config_flow.py @@ -38,7 +38,7 @@ async def test_show_user_form(hass: HomeAssistant) -> None: ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM async def test_user_connection_error( @@ -56,7 +56,7 @@ async def test_user_connection_error( data=user_input, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -75,7 +75,7 @@ async def test_user_connection_error_invalid_hostname( data=user_input, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -94,7 +94,7 @@ async def test_user_timeout_error( data=user_input, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "time_out" @@ -113,7 +113,7 @@ async def test_user_not_connected_error( data=user_input, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_connected" @@ -134,7 +134,7 @@ async def test_user_response_error_single_device_old_ce_old_new_pro( data=user_input, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"] @@ -162,7 +162,7 @@ async def test_user_response_error_single_device_new_ce_old_pro( data=user_input, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == ZEROCONF_NAME assert result["data"] @@ -188,7 +188,7 @@ async def test_user_response_error_single_device_new_ce_new_pro( data=user_input, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == ZEROCONF_NAME assert result["data"] @@ -219,7 +219,7 @@ async def test_user_response_error_multi_device_old_ce_old_new_pro( data=user_input, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -242,7 +242,7 @@ async def test_user_response_error_multi_device_new_ce_new_pro( data=user_input, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -261,7 +261,7 @@ async def test_zeroconf_connection_error( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -280,7 +280,7 @@ async def test_zeroconf_connection_error_invalid_hostname( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -299,7 +299,7 @@ async def test_zeroconf_timout_error( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "time_out" @@ -318,7 +318,7 @@ async def test_zeroconf_not_connected_error( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_connected" @@ -339,7 +339,7 @@ async def test_show_zeroconf_form_old_ce_old_pro( ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["description_placeholders"] == {CONF_NAME: "My MotionMount"} @@ -360,7 +360,7 @@ async def test_show_zeroconf_form_old_ce_new_pro( ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["description_placeholders"] == {CONF_NAME: "My MotionMount"} @@ -381,7 +381,7 @@ async def test_show_zeroconf_form_new_ce_old_pro( ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["description_placeholders"] == {CONF_NAME: "My MotionMount"} @@ -400,7 +400,7 @@ async def test_show_zeroconf_form_new_ce_new_pro( ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["description_placeholders"] == {CONF_NAME: "My MotionMount"} @@ -419,7 +419,7 @@ async def test_zeroconf_device_exists_abort( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -437,14 +437,14 @@ async def test_full_user_flow_implementation( ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_INPUT.copy(), ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == ZEROCONF_NAME assert result["data"] @@ -471,13 +471,13 @@ async def test_full_zeroconf_flow_implementation( ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == ZEROCONF_NAME assert result["data"] diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 719117e59a9..f160fc0561a 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -11,10 +11,11 @@ from uuid import uuid4 import pytest import voluptuous as vol -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import mqtt from homeassistant.components.hassio import HassioServiceInfo from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry from tests.typing import MqttMockHAClientGenerator, MqttMockPahoClient @@ -354,7 +355,7 @@ async def test_hassio_ignored(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_HASSIO}, ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -473,7 +474,7 @@ async def test_option_flow( mqtt_mock.async_connect.reset_mock() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "broker" result = await hass.config_entries.options.async_configure( @@ -485,7 +486,7 @@ async def test_option_flow( mqtt.CONF_PASSWORD: "pass", }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options" await hass.async_block_till_done() @@ -510,7 +511,7 @@ async def test_option_flow( "will_retain": True, }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {} assert config_entry.data == { mqtt.CONF_BROKER: "another-broker", @@ -609,7 +610,7 @@ async def test_bad_certificate( mqtt_mock.async_connect.reset_mock() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "broker" result = await hass.config_entries.options.async_configure( @@ -682,7 +683,7 @@ async def test_keepalive_validation( mqtt_mock.async_connect.reset_mock() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "broker" if error: @@ -722,7 +723,7 @@ async def test_disable_birth_will( mqtt_mock.async_connect.reset_mock() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "broker" result = await hass.config_entries.options.async_configure( @@ -734,7 +735,7 @@ async def test_disable_birth_will( mqtt.CONF_PASSWORD: "pass", }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options" await hass.async_block_till_done() @@ -757,7 +758,7 @@ async def test_disable_birth_will( "will_retain": True, }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {} assert config_entry.data == { mqtt.CONF_BROKER: "another-broker", @@ -799,7 +800,7 @@ async def test_invalid_discovery_prefix( mqtt_mock.async_connect.reset_mock() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "broker" result = await hass.config_entries.options.async_configure( @@ -809,7 +810,7 @@ async def test_invalid_discovery_prefix( mqtt.CONF_PORT: 2345, }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options" await hass.async_block_till_done() @@ -822,7 +823,7 @@ async def test_invalid_discovery_prefix( mqtt.CONF_DISCOVERY_PREFIX: "homeassistant#invalid", }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options" assert result["errors"]["base"] == "bad_discovery_prefix" assert config_entry.data == { @@ -892,7 +893,7 @@ async def test_option_flow_default_suggested_values( # Test default/suggested values from config result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "broker" defaults = { mqtt.CONF_BROKER: "test-broker", @@ -916,7 +917,7 @@ async def test_option_flow_default_suggested_values( mqtt.CONF_PASSWORD: "p4ss", }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options" defaults = { mqtt.CONF_DISCOVERY: True, @@ -950,11 +951,11 @@ async def test_option_flow_default_suggested_values( "will_retain": True, }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY # Test updated default/suggested values from config result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "broker" defaults = { mqtt.CONF_BROKER: "another-broker", @@ -973,7 +974,7 @@ async def test_option_flow_default_suggested_values( result["flow_id"], user_input={mqtt.CONF_BROKER: "another-broker", mqtt.CONF_PORT: 2345}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options" defaults = { mqtt.CONF_DISCOVERY: False, @@ -1007,7 +1008,7 @@ async def test_option_flow_default_suggested_values( "will_retain": True, }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY # Make sure all MQTT related jobs are done before ending the test await hass.async_block_till_done() @@ -1049,7 +1050,7 @@ async def test_skipping_advanced_options( result = await hass.config_entries.options.async_init( config_entry.entry_id, context={"show_advanced_options": True} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "broker" result = await hass.config_entries.options.async_configure( @@ -1221,7 +1222,7 @@ async def test_try_connection_with_advanced_parameters( # Test default/suggested values from config result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "broker" defaults = { mqtt.CONF_BROKER: "test-broker", @@ -1261,7 +1262,7 @@ async def test_try_connection_with_advanced_parameters( mqtt.CONF_WS_HEADERS: '{"h3": "v3"}', }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "options" await hass.async_block_till_done() @@ -1292,7 +1293,7 @@ async def test_try_connection_with_advanced_parameters( result["flow_id"], user_input={}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() diff --git a/tests/components/mullvad/test_config_flow.py b/tests/components/mullvad/test_config_flow.py index da9ce91eeed..a2468fbe7d2 100644 --- a/tests/components/mullvad/test_config_flow.py +++ b/tests/components/mullvad/test_config_flow.py @@ -18,7 +18,7 @@ async def test_form_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] with ( @@ -50,7 +50,7 @@ async def test_form_user_only_once(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -71,7 +71,7 @@ async def test_connection_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -92,5 +92,5 @@ async def test_unknown_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/mysensors/test_config_flow.py b/tests/components/mysensors/test_config_flow.py index f532d09c6bf..c75f8743cde 100644 --- a/tests/components/mysensors/test_config_flow.py +++ b/tests/components/mysensors/test_config_flow.py @@ -44,13 +44,13 @@ async def get_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU result = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": GATEWAY_TYPE_TO_STEP[gateway_type]} ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == expected_step_id return result @@ -78,7 +78,7 @@ async def test_config_mqtt(hass: HomeAssistant, mqtt: None) -> None: if "errors" in result: assert not result["errors"] - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "mqtt" assert result["data"] == { CONF_DEVICE: "mqtt", @@ -96,7 +96,7 @@ async def test_missing_mqtt(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -104,7 +104,7 @@ async def test_missing_mqtt(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "mqtt_required" @@ -139,7 +139,7 @@ async def test_config_serial(hass: HomeAssistant) -> None: if "errors" in result: assert not result["errors"] - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "/dev/ttyACM0" assert result["data"] == { CONF_DEVICE: "/dev/ttyACM0", @@ -177,7 +177,7 @@ async def test_config_tcp(hass: HomeAssistant) -> None: if "errors" in result: assert not result["errors"] - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "127.0.0.1" assert result["data"] == { CONF_DEVICE: "127.0.0.1", @@ -213,7 +213,7 @@ async def test_fail_to_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert "errors" in result errors = result["errors"] assert errors @@ -374,7 +374,7 @@ async def test_config_invalid( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert "errors" in result errors = result["errors"] assert errors diff --git a/tests/components/mystrom/test_config_flow.py b/tests/components/mystrom/test_config_flow.py index b64b4edf547..d0b3603b211 100644 --- a/tests/components/mystrom/test_config_flow.py +++ b/tests/components/mystrom/test_config_flow.py @@ -22,7 +22,7 @@ async def test_form_combined(hass: HomeAssistant, mock_setup_entry: AsyncMock) - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -38,7 +38,7 @@ async def test_form_combined(hass: HomeAssistant, mock_setup_entry: AsyncMock) - ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "myStrom Device" assert result2["data"] == {"host": "1.1.1.1"} @@ -50,7 +50,7 @@ async def test_form_duplicates( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -66,7 +66,7 @@ async def test_form_duplicates( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" mock_session.assert_called_once() @@ -78,7 +78,7 @@ async def test_wong_answer_from_device(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} with patch( @@ -93,7 +93,7 @@ async def test_wong_answer_from_device(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -108,6 +108,6 @@ async def test_wong_answer_from_device(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "myStrom Device" assert result2["data"] == {"host": "1.1.1.1"} diff --git a/tests/components/myuplink/test_config_flow.py b/tests/components/myuplink/test_config_flow.py index 1a4656bed73..7c5ae2c8657 100644 --- a/tests/components/myuplink/test_config_flow.py +++ b/tests/components/myuplink/test_config_flow.py @@ -155,7 +155,7 @@ async def test_flow_reauth( result = await hass.config_entries.flow.async_configure(result["flow_id"]) await hass.async_block_till_done() - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "reauth_successful" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 diff --git a/tests/components/nam/test_config_flow.py b/tests/components/nam/test_config_flow.py index 71bf3cf1525..5dff9855988 100644 --- a/tests/components/nam/test_config_flow.py +++ b/tests/components/nam/test_config_flow.py @@ -6,11 +6,11 @@ from unittest.mock import patch from nettigo_air_monitor import ApiError, AuthFailedError, CannotGetMacError import pytest -from homeassistant import data_entry_flow from homeassistant.components import zeroconf from homeassistant.components.nam.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER, SOURCE_ZEROCONF from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -34,7 +34,7 @@ async def test_form_create_entry_without_auth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -57,7 +57,7 @@ async def test_form_create_entry_without_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "10.10.2.3" assert result["data"]["host"] == "10.10.2.3" assert len(mock_setup_entry.mock_calls) == 1 @@ -68,7 +68,7 @@ async def test_form_create_entry_with_auth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -90,7 +90,7 @@ async def test_form_create_entry_with_auth(hass: HomeAssistant) -> None: VALID_CONFIG, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "credentials" result = await hass.config_entries.flow.async_configure( @@ -99,7 +99,7 @@ async def test_form_create_entry_with_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "10.10.2.3" assert result["data"]["host"] == "10.10.2.3" assert result["data"]["username"] == "fake_username" @@ -133,7 +133,7 @@ async def test_reauth_successful(hass: HomeAssistant) -> None: data=entry.data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -141,7 +141,7 @@ async def test_reauth_successful(hass: HomeAssistant) -> None: user_input=VALID_AUTH, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" @@ -165,7 +165,7 @@ async def test_reauth_unsuccessful(hass: HomeAssistant) -> None: data=entry.data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -173,7 +173,7 @@ async def test_reauth_unsuccessful(hass: HomeAssistant) -> None: user_input=VALID_AUTH, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_unsuccessful" @@ -205,7 +205,7 @@ async def test_form_with_auth_errors(hass: HomeAssistant, error) -> None: data=VALID_CONFIG, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "credentials" with patch( @@ -262,7 +262,7 @@ async def test_form_abort(hass: HomeAssistant) -> None: data=VALID_CONFIG, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "device_unsupported" @@ -292,7 +292,7 @@ async def test_form_already_configured(hass: HomeAssistant) -> None: {"host": "1.1.1.1"}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" # Test config entry got updated with latest IP @@ -322,7 +322,7 @@ async def test_zeroconf(hass: HomeAssistant) -> None: if flow["flow_id"] == result["flow_id"] ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert context["title_placeholders"]["host"] == "10.10.2.3" assert context["confirm_only"] is True @@ -337,7 +337,7 @@ async def test_zeroconf(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "10.10.2.3" assert result["data"] == {"host": "10.10.2.3"} assert len(mock_setup_entry.mock_calls) == 1 @@ -366,7 +366,7 @@ async def test_zeroconf_with_auth(hass: HomeAssistant) -> None: if flow["flow_id"] == result["flow_id"] ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "credentials" assert result["errors"] == {} assert context["title_placeholders"]["host"] == "10.10.2.3" @@ -390,7 +390,7 @@ async def test_zeroconf_with_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "10.10.2.3" assert result["data"]["host"] == "10.10.2.3" assert result["data"]["username"] == "fake_username" @@ -411,7 +411,7 @@ async def test_zeroconf_host_already_configured(hass: HomeAssistant) -> None: context={"source": SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -435,5 +435,5 @@ async def test_zeroconf_errors(hass: HomeAssistant, error) -> None: context={"source": SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == reason diff --git a/tests/components/neato/test_config_flow.py b/tests/components/neato/test_config_flow.py index 34b1f37f56f..506a29b559f 100644 --- a/tests/components/neato/test_config_flow.py +++ b/tests/components/neato/test_config_flow.py @@ -4,13 +4,14 @@ from unittest.mock import patch from pybotvac.neato import Neato -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, setup from homeassistant.components.application_credentials import ( ClientCredential, async_import_client_credential, ) from homeassistant.components.neato.const import NEATO_DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow from tests.common import MockConfigEntry @@ -92,7 +93,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( "neato", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -118,7 +119,7 @@ async def test_reauth( result = await hass.config_entries.flow.async_init( "neato", context={"source": config_entries.SOURCE_REAUTH} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" # Confirm reauth flow @@ -155,7 +156,7 @@ async def test_reauth( new_entry = hass.config_entries.async_get_entry("my_entry") - assert result3["type"] == data_entry_flow.FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" assert new_entry.state == config_entries.ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(NEATO_DOMAIN)) == 1 diff --git a/tests/components/netatmo/test_config_flow.py b/tests/components/netatmo/test_config_flow.py index 7866e448734..c828fae7ba2 100644 --- a/tests/components/netatmo/test_config_flow.py +++ b/tests/components/netatmo/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import patch from pyatmo.const import ALL_SCOPES -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.netatmo import config_flow from homeassistant.components.netatmo.const import ( @@ -16,6 +16,7 @@ from homeassistant.components.netatmo.const import ( OAUTH2_TOKEN, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow from .conftest import CLIENT_ID @@ -37,7 +38,7 @@ async def test_abort_if_existing_entry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( "netatmo", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" result = await hass.config_entries.flow.async_init( @@ -53,7 +54,7 @@ async def test_abort_if_existing_entry(hass: HomeAssistant) -> None: type="mock_type", ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -140,28 +141,28 @@ async def test_option_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "public_weather_areas" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_NEW_AREA: "Home"} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "public_weather" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input=valid_option ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "public_weather_areas" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY for k, v in expected_result.items(): assert config_entry.options[CONF_WEATHER_AREAS]["Home"][k] == v @@ -198,28 +199,28 @@ async def test_option_flow_wrong_coordinates(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "public_weather_areas" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_NEW_AREA: "Home"} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "public_weather" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input=valid_option ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "public_weather_areas" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY for k, v in expected_result.items(): assert config_entry.options[CONF_WEATHER_AREAS]["Home"][k] == v @@ -282,7 +283,7 @@ async def test_reauth( result = await hass.config_entries.flow.async_init( "netatmo", context={"source": config_entries.SOURCE_REAUTH} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" # Confirm reauth flow @@ -319,7 +320,7 @@ async def test_reauth( new_entry2 = hass.config_entries.async_entries(DOMAIN)[0] - assert result3["type"] == data_entry_flow.FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" assert new_entry2.state == config_entries.ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 diff --git a/tests/components/netgear/test_config_flow.py b/tests/components/netgear/test_config_flow.py index c0649d3646e..724a0568580 100644 --- a/tests/components/netgear/test_config_flow.py +++ b/tests/components/netgear/test_config_flow.py @@ -5,7 +5,6 @@ from unittest.mock import Mock, patch from pynetgear import DEFAULT_USER import pytest -from homeassistant import data_entry_flow from homeassistant.components import ssdp from homeassistant.components.netgear.const import ( CONF_CONSIDER_HOME, @@ -23,6 +22,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -83,7 +83,7 @@ async def test_user(hass: HomeAssistant, service) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # Have to provide all config @@ -95,7 +95,7 @@ async def test_user(hass: HomeAssistant, service) -> None: CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == TITLE assert result["data"].get(CONF_HOST) == HOST @@ -110,7 +110,7 @@ async def test_user_connect_error(hass: HomeAssistant, service) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" service.return_value.get_info = Mock(return_value=None) @@ -124,7 +124,7 @@ async def test_user_connect_error(hass: HomeAssistant, service) -> None: CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "info"} @@ -138,7 +138,7 @@ async def test_user_connect_error(hass: HomeAssistant, service) -> None: CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "config"} @@ -148,7 +148,7 @@ async def test_user_incomplete_info(hass: HomeAssistant, service) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" router_infos = ROUTER_INFOS.copy() @@ -164,7 +164,7 @@ async def test_user_incomplete_info(hass: HomeAssistant, service) -> None: CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == TITLE_INCOMPLETE assert result["data"].get(CONF_HOST) == HOST @@ -186,14 +186,14 @@ async def test_abort_if_already_setup(hass: HomeAssistant, service) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -219,7 +219,7 @@ async def test_ssdp_already_configured(hass: HomeAssistant) -> None: }, ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -238,7 +238,7 @@ async def test_ssdp_no_serial(hass: HomeAssistant) -> None: }, ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_serial" @@ -264,7 +264,7 @@ async def test_ssdp_ipv6(hass: HomeAssistant) -> None: }, ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_ipv4_address" @@ -284,13 +284,13 @@ async def test_ssdp(hass: HomeAssistant, service) -> None: }, ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_PASSWORD: PASSWORD} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == TITLE assert result["data"].get(CONF_HOST) == HOST @@ -316,7 +316,7 @@ async def test_ssdp_port_5555(hass: HomeAssistant, service) -> None: }, ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" service.return_value.port = 5555 @@ -325,7 +325,7 @@ async def test_ssdp_port_5555(hass: HomeAssistant, service) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_PASSWORD: PASSWORD} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == TITLE assert result["data"].get(CONF_HOST) == HOST @@ -350,7 +350,7 @@ async def test_options_flow(hass: HomeAssistant, service) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -360,7 +360,7 @@ async def test_options_flow(hass: HomeAssistant, service) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_CONSIDER_HOME: 1800, } diff --git a/tests/components/netgear_lte/test_config_flow.py b/tests/components/netgear_lte/test_config_flow.py index 71fe8ddb774..6b969e33475 100644 --- a/tests/components/netgear_lte/test_config_flow.py +++ b/tests/components/netgear_lte/test_config_flow.py @@ -9,6 +9,7 @@ from homeassistant.components.netgear_lte.const import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_SOURCE from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .conftest import CONF_DATA @@ -51,7 +52,7 @@ async def test_flow_already_configured( data=CONF_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/nextbus/test_config_flow.py b/tests/components/nextbus/test_config_flow.py index 466ecb9df61..5d0158a50cb 100644 --- a/tests/components/nextbus/test_config_flow.py +++ b/tests/components/nextbus/test_config_flow.py @@ -47,7 +47,7 @@ async def test_import_config( ) await hass.async_block_till_done() - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert ( result.get("title") == "San Francisco Muni F - Market & Wharves Market St & 7th St (Outbound)" @@ -64,7 +64,7 @@ async def test_import_config( ) await hass.async_block_till_done() - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -100,7 +100,7 @@ async def test_import_config_invalid( ) await hass.async_block_till_done() - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == expected_reason @@ -112,7 +112,7 @@ async def test_user_config( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "agency" # Select agency @@ -136,7 +136,7 @@ async def test_user_config( ) await hass.async_block_till_done() - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "stop" # Select stop @@ -148,7 +148,7 @@ async def test_user_config( ) await hass.async_block_till_done() - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("data") == { "agency": "sf-muni", "route": "F", diff --git a/tests/components/nextcloud/test_config_flow.py b/tests/components/nextcloud/test_config_flow.py index 32c688fb8c2..9a881197cf9 100644 --- a/tests/components/nextcloud/test_config_flow.py +++ b/tests/components/nextcloud/test_config_flow.py @@ -36,7 +36,7 @@ async def test_user_create_entry( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -50,7 +50,7 @@ async def test_user_create_entry( VALID_CONFIG, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -64,7 +64,7 @@ async def test_user_create_entry( VALID_CONFIG, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "connection_error"} @@ -78,7 +78,7 @@ async def test_user_create_entry( VALID_CONFIG, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "connection_error"} @@ -93,7 +93,7 @@ async def test_user_create_entry( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "nc_url" assert result["data"] == snapshot @@ -113,7 +113,7 @@ async def test_user_already_configured( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -127,7 +127,7 @@ async def test_user_already_configured( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -149,7 +149,7 @@ async def test_reauth( context={"source": SOURCE_REAUTH, "entry_id": entry.entry_id}, data=entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" # test NextcloudMonitorAuthorizationError @@ -165,7 +165,7 @@ async def test_reauth( }, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "invalid_auth"} @@ -182,7 +182,7 @@ async def test_reauth( }, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "connection_error"} @@ -199,7 +199,7 @@ async def test_reauth( }, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "connection_error"} @@ -217,6 +217,6 @@ async def test_reauth( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data == snapshot diff --git a/tests/components/nextdns/test_config_flow.py b/tests/components/nextdns/test_config_flow.py index 4d9961474c5..9247288eebf 100644 --- a/tests/components/nextdns/test_config_flow.py +++ b/tests/components/nextdns/test_config_flow.py @@ -5,11 +5,11 @@ from unittest.mock import patch from nextdns import ApiError, InvalidApiKeyError import pytest -from homeassistant import data_entry_flow from homeassistant.components.nextdns.const import CONF_PROFILE_ID, DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_PROFILE_NAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import PROFILES, init_integration @@ -19,7 +19,7 @@ async def test_form_create_entry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -37,7 +37,7 @@ async def test_form_create_entry(hass: HomeAssistant) -> None: {CONF_API_KEY: "fake_api_key"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "profiles" result = await hass.config_entries.flow.async_configure( @@ -45,7 +45,7 @@ async def test_form_create_entry(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Fake Profile" assert result["data"][CONF_API_KEY] == "fake_api_key" assert result["data"][CONF_PROFILE_ID] == "xyz12" @@ -97,5 +97,5 @@ async def test_form_already_configured(hass: HomeAssistant) -> None: result["flow_id"], {CONF_PROFILE_NAME: "Fake Profile"} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/nfandroidtv/test_config_flow.py b/tests/components/nfandroidtv/test_config_flow.py index 04fcf699513..271961fbee7 100644 --- a/tests/components/nfandroidtv/test_config_flow.py +++ b/tests/components/nfandroidtv/test_config_flow.py @@ -4,9 +4,10 @@ from unittest.mock import patch from notifications_android_tv.notifications import ConnectError -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.nfandroidtv.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import ( CONF_CONFIG_FLOW, @@ -39,7 +40,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_CONFIG_FLOW, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"] == CONF_DATA @@ -64,7 +65,7 @@ async def test_flow_user_already_configured(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_CONFIG_FLOW, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -78,7 +79,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, data=CONF_CONFIG_FLOW, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -93,6 +94,6 @@ async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, data=CONF_CONFIG_FLOW, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} diff --git a/tests/components/nibe_heatpump/test_config_flow.py b/tests/components/nibe_heatpump/test_config_flow.py index b4c0b223998..471f7f4c593 100644 --- a/tests/components/nibe_heatpump/test_config_flow.py +++ b/tests/components/nibe_heatpump/test_config_flow.py @@ -43,13 +43,13 @@ async def _get_connection_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU result = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": connection_type} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None return result @@ -67,7 +67,7 @@ async def test_nibegw_form( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "F1155 at 127.0.0.1" assert result2["data"] == { "model": "F1155", @@ -94,7 +94,7 @@ async def test_modbus_form( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "S1155 at 127.0.0.1" assert result2["data"] == { "model": "S1155", @@ -116,7 +116,7 @@ async def test_modbus_invalid_url( result["flow_id"], {**MOCK_FLOW_MODBUS_USERDATA, "modbus_url": "invalid://url"} ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"modbus_url": "url"} @@ -131,7 +131,7 @@ async def test_nibegw_address_inuse(hass: HomeAssistant, mock_connection: Mock) result["flow_id"], MOCK_FLOW_NIBEGW_USERDATA ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"listening_port": "address_in_use"} mock_connection.start.side_effect = Exception() @@ -140,7 +140,7 @@ async def test_nibegw_address_inuse(hass: HomeAssistant, mock_connection: Mock) result["flow_id"], MOCK_FLOW_NIBEGW_USERDATA ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -161,7 +161,7 @@ async def test_read_timeout( result2 = await hass.config_entries.flow.async_configure(result["flow_id"], data) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "read"} @@ -182,7 +182,7 @@ async def test_write_timeout( result2 = await hass.config_entries.flow.async_configure(result["flow_id"], data) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "write"} @@ -203,7 +203,7 @@ async def test_unexpected_exception( result2 = await hass.config_entries.flow.async_configure(result["flow_id"], data) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -224,7 +224,7 @@ async def test_nibegw_invalid_host( result2 = await hass.config_entries.flow.async_configure(result["flow_id"], data) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM if connection_type == "nibegw": assert result2["errors"] == {"ip_address": "address"} else: @@ -248,5 +248,5 @@ async def test_model_missing_coil( result2 = await hass.config_entries.flow.async_configure(result["flow_id"], data) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "model"} diff --git a/tests/components/nightscout/test_config_flow.py b/tests/components/nightscout/test_config_flow.py index c3723596a84..d139a66270c 100644 --- a/tests/components/nightscout/test_config_flow.py +++ b/tests/components/nightscout/test_config_flow.py @@ -5,11 +5,12 @@ from unittest.mock import patch from aiohttp import ClientConnectionError, ClientResponseError -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.nightscout.const import DOMAIN from homeassistant.components.nightscout.utils import hash_from_url from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import GLUCOSE_READINGS, SERVER_STATUS, SERVER_STATUS_STATUS_ONLY @@ -24,7 +25,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -37,7 +38,7 @@ async def test_form(hass: HomeAssistant) -> None: CONFIG, ) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == SERVER_STATUS.name # pylint: disable=maybe-no-member assert result2["data"] == CONFIG await hass.async_block_till_done() @@ -59,7 +60,7 @@ async def test_user_form_cannot_connect(hass: HomeAssistant) -> None: {CONF_URL: "https://some.url:1234"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -84,7 +85,7 @@ async def test_user_form_api_key_required(hass: HomeAssistant) -> None: {CONF_URL: "https://some.url:1234"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -103,7 +104,7 @@ async def test_user_form_unexpected_exception(hass: HomeAssistant) -> None: {CONF_URL: "https://some.url:1234"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -119,7 +120,7 @@ async def test_user_form_duplicate(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, data=CONFIG, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/nina/test_config_flow.py b/tests/components/nina/test_config_flow.py index d3c44258c23..804b614fe92 100644 --- a/tests/components/nina/test_config_flow.py +++ b/tests/components/nina/test_config_flow.py @@ -9,7 +9,6 @@ from unittest.mock import patch from pynina import ApiError -from homeassistant import data_entry_flow from homeassistant.components.nina.const import ( CONF_AREA_FILTER, CONF_HEADLINE_FILTER, @@ -25,6 +24,7 @@ from homeassistant.components.nina.const import ( ) from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import entity_registry as er from . import mocked_request_function @@ -61,7 +61,7 @@ async def test_show_set_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -75,7 +75,7 @@ async def test_step_user_connection_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=deepcopy(DUMMY_DATA) ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -89,7 +89,7 @@ async def test_step_user_unexpected_exception(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=deepcopy(DUMMY_DATA) ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT async def test_step_user(hass: HomeAssistant) -> None: @@ -108,7 +108,7 @@ async def test_step_user(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=deepcopy(DUMMY_DATA) ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "NINA" @@ -122,7 +122,7 @@ async def test_step_user_no_selection(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data={CONF_HEADLINE_FILTER: ""} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "no_selection"} @@ -141,7 +141,7 @@ async def test_step_user_already_configured(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=deepcopy(DUMMY_DATA) ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -172,7 +172,7 @@ async def test_options_flow_init(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -187,7 +187,7 @@ async def test_options_flow_init(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] is None assert dict(config_entry.data) == { @@ -227,7 +227,7 @@ async def test_options_flow_with_no_selection(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -243,7 +243,7 @@ async def test_options_flow_with_no_selection(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {"base": "no_selection"} @@ -272,7 +272,7 @@ async def test_options_flow_connection_error(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -300,7 +300,7 @@ async def test_options_flow_unexpected_exception(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT async def test_options_flow_entity_removal(hass: HomeAssistant) -> None: @@ -339,7 +339,7 @@ async def test_options_flow_entity_removal(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY entity_registry: er = er.async_get(hass) entries = er.async_entries_for_config_entry( diff --git a/tests/components/nmap_tracker/test_config_flow.py b/tests/components/nmap_tracker/test_config_flow.py index ccbdc112e46..f9b6964dc7d 100644 --- a/tests/components/nmap_tracker/test_config_flow.py +++ b/tests/components/nmap_tracker/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.device_tracker import ( CONF_CONSIDER_HOME, CONF_SCAN_INTERVAL, @@ -17,6 +17,7 @@ from homeassistant.components.nmap_tracker.const import ( ) from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS from homeassistant.core import CoreState, HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -203,7 +204,7 @@ async def test_options_flow(hass: HomeAssistant, mock_get_source_ip) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({}) == { @@ -232,7 +233,7 @@ async def test_options_flow(hass: HomeAssistant, mock_get_source_ip) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_HOSTS: "192.168.1.0/24,192.168.2.0/24", CONF_HOME_INTERVAL: 5, diff --git a/tests/components/notion/test_config_flow.py b/tests/components/notion/test_config_flow.py index 827565db339..2cc5e3f04b7 100644 --- a/tests/components/notion/test_config_flow.py +++ b/tests/components/notion/test_config_flow.py @@ -5,11 +5,11 @@ from unittest.mock import AsyncMock, patch from aionotion.errors import InvalidCredentialsError, NotionError import pytest -from homeassistant import data_entry_flow from homeassistant.components.notion import CONF_REFRESH_TOKEN, CONF_USER_UUID, DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .conftest import TEST_PASSWORD, TEST_REFRESH_TOKEN, TEST_USER_UUID, TEST_USERNAME @@ -35,7 +35,7 @@ async def test_create_entry( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # Test errors that can arise when getting a Notion API client: @@ -51,7 +51,7 @@ async def test_create_entry( CONF_PASSWORD: TEST_PASSWORD, }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == errors result = await hass.config_entries.flow.async_configure( @@ -61,7 +61,7 @@ async def test_create_entry( CONF_PASSWORD: TEST_PASSWORD, }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_USERNAME assert result["data"] == { CONF_REFRESH_TOKEN: TEST_REFRESH_TOKEN, @@ -75,7 +75,7 @@ async def test_duplicate_error(hass: HomeAssistant, config, config_entry) -> Non result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -115,7 +115,7 @@ async def test_reauth( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "password"} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == errors result = await hass.config_entries.flow.async_configure( @@ -126,6 +126,6 @@ async def test_reauth( # to setup the config entry via reload. await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 diff --git a/tests/components/nuki/test_config_flow.py b/tests/components/nuki/test_config_flow.py index c7575f71545..58cbfde3d92 100644 --- a/tests/components/nuki/test_config_flow.py +++ b/tests/components/nuki/test_config_flow.py @@ -5,11 +5,12 @@ from unittest.mock import patch from pynuki.bridge import InvalidCredentialsException from requests.exceptions import RequestException -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.components.nuki.const import DOMAIN from homeassistant.const import CONF_TOKEN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .mock import DHCP_FORMATTED_MAC, HOST, MOCK_INFO, NAME, setup_nuki_integration @@ -20,7 +21,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -43,7 +44,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "75BCD15" assert result2["data"] == { "host": "1.1.1.1", @@ -72,7 +73,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -95,7 +96,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -118,7 +119,7 @@ async def test_form_unknown_exception(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -142,7 +143,7 @@ async def test_form_already_configured(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -156,7 +157,7 @@ async def test_dhcp_flow(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER with ( @@ -178,7 +179,7 @@ async def test_dhcp_flow(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "75BCD15" assert result2["data"] == { "host": "1.1.1.1", @@ -201,7 +202,7 @@ async def test_dhcp_flow_already_configured(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -212,7 +213,7 @@ async def test_reauth_success(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=entry.data ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with ( @@ -231,7 +232,7 @@ async def test_reauth_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data[CONF_TOKEN] == "new-token" @@ -243,7 +244,7 @@ async def test_reauth_invalid_auth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=entry.data ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( @@ -255,7 +256,7 @@ async def test_reauth_invalid_auth(hass: HomeAssistant) -> None: user_input={CONF_TOKEN: "new-token"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "reauth_confirm" assert result2["errors"] == {"base": "invalid_auth"} @@ -267,7 +268,7 @@ async def test_reauth_cannot_connect(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=entry.data ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( @@ -279,7 +280,7 @@ async def test_reauth_cannot_connect(hass: HomeAssistant) -> None: user_input={CONF_TOKEN: "new-token"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "reauth_confirm" assert result2["errors"] == {"base": "cannot_connect"} @@ -291,7 +292,7 @@ async def test_reauth_unknown_exception(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=entry.data ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( @@ -303,6 +304,6 @@ async def test_reauth_unknown_exception(hass: HomeAssistant) -> None: user_input={CONF_TOKEN: "new-token"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "reauth_confirm" assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/nut/test_config_flow.py b/tests/components/nut/test_config_flow.py index 56a7d7d9089..537b6aba5ac 100644 --- a/tests/components/nut/test_config_flow.py +++ b/tests/components/nut/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import patch from aionut import NUTError, NUTLoginError -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, setup from homeassistant.components import zeroconf from homeassistant.components.nut.const import DOMAIN from homeassistant.const import ( @@ -19,6 +19,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .util import _get_mock_nutclient @@ -47,7 +48,7 @@ async def test_form_zeroconf(hass: HomeAssistant) -> None: type="mock_type", ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -71,7 +72,7 @@ async def test_form_zeroconf(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "192.168.1.5:1234" assert result2["data"] == { CONF_HOST: "192.168.1.5", @@ -89,7 +90,7 @@ async def test_form_user_one_ups(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} mock_pynut = _get_mock_nutclient( @@ -117,7 +118,7 @@ async def test_form_user_one_ups(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "1.1.1.1:2222" assert result2["data"] == { CONF_HOST: "1.1.1.1", @@ -141,7 +142,7 @@ async def test_form_user_multiple_ups(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} mock_pynut = _get_mock_nutclient( @@ -164,7 +165,7 @@ async def test_form_user_multiple_ups(hass: HomeAssistant) -> None: ) assert result2["step_id"] == "ups" - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM with ( patch( @@ -182,7 +183,7 @@ async def test_form_user_multiple_ups(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "ups2@1.1.1.1:2222" assert result3["data"] == { CONF_HOST: "1.1.1.1", @@ -205,7 +206,7 @@ async def test_form_user_one_ups_with_ignored_entry(hass: HomeAssistant) -> None result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} mock_pynut = _get_mock_nutclient( @@ -233,7 +234,7 @@ async def test_form_user_one_ups_with_ignored_entry(hass: HomeAssistant) -> None ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "1.1.1.1:2222" assert result2["data"] == { CONF_HOST: "1.1.1.1", @@ -266,7 +267,7 @@ async def test_form_no_upses_found(hass: HomeAssistant) -> None: }, ) - assert result2["type"] is data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "no_ups_found" @@ -296,7 +297,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} assert result2["description_placeholders"] == {"error": "no route to host"} @@ -320,7 +321,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} mock_pynut = _get_mock_nutclient( @@ -347,7 +348,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "1.1.1.1:2222" assert result2["data"] == { CONF_HOST: "1.1.1.1", @@ -384,7 +385,7 @@ async def test_auth_failures(hass: HomeAssistant) -> None: }, ) - assert result2["type"] is data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"password": "invalid_auth"} mock_pynut = _get_mock_nutclient( @@ -411,7 +412,7 @@ async def test_auth_failures(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "1.1.1.1:2222" assert result2["data"] == { CONF_HOST: "1.1.1.1", @@ -457,7 +458,7 @@ async def test_reauth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] is data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"password": "invalid_auth"} mock_pynut = _get_mock_nutclient( @@ -482,7 +483,7 @@ async def test_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] is data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 @@ -520,7 +521,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -559,7 +560,7 @@ async def test_abort_if_already_setup_alias(hass: HomeAssistant) -> None: ) assert result2["step_id"] == "ups" - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM with patch( "homeassistant.components.nut.AIONUTClient", @@ -570,7 +571,7 @@ async def test_abort_if_already_setup_alias(hass: HomeAssistant) -> None: {CONF_ALIAS: "ups1"}, ) - assert result3["type"] == data_entry_flow.FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "already_configured" @@ -587,14 +588,14 @@ async def test_options_flow(hass: HomeAssistant) -> None: with patch("homeassistant.components.nut.async_setup_entry", return_value=True): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + 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"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_SCAN_INTERVAL: 60, } @@ -602,7 +603,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: with patch("homeassistant.components.nut.async_setup_entry", return_value=True): result2 = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "init" result2 = await hass.config_entries.options.async_configure( @@ -610,7 +611,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: user_input={CONF_SCAN_INTERVAL: 12}, ) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_SCAN_INTERVAL: 12, } diff --git a/tests/components/nzbget/test_config_flow.py b/tests/components/nzbget/test_config_flow.py index c299d1d6dd5..fb826407558 100644 --- a/tests/components/nzbget/test_config_flow.py +++ b/tests/components/nzbget/test_config_flow.py @@ -28,7 +28,7 @@ async def test_user_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -43,7 +43,7 @@ async def test_user_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "10.10.10.30" assert result["data"] == {**USER_INPUT, CONF_VERIFY_SSL: False} @@ -56,7 +56,7 @@ async def test_user_form_show_advanced_options(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER, "show_advanced_options": True} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} user_input_advanced = { @@ -76,7 +76,7 @@ async def test_user_form_show_advanced_options(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "10.10.10.30" assert result["data"] == {**USER_INPUT, CONF_VERIFY_SSL: True} @@ -98,7 +98,7 @@ async def test_user_form_cannot_connect(hass: HomeAssistant) -> None: USER_INPUT, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -117,7 +117,7 @@ async def test_user_form_unexpected_exception(hass: HomeAssistant) -> None: USER_INPUT, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -131,5 +131,5 @@ async def test_user_form_single_instance_allowed(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, data=USER_INPUT, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/obihai/test_config_flow.py b/tests/components/obihai/test_config_flow.py index d9ca52424c2..4ad06f33cd1 100644 --- a/tests/components/obihai/test_config_flow.py +++ b/tests/components/obihai/test_config_flow.py @@ -24,7 +24,7 @@ async def test_user_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -35,7 +35,7 @@ async def test_user_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "10.10.10.30" assert result["data"] == {**USER_INPUT} @@ -55,7 +55,7 @@ async def test_auth_failure(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "invalid_auth" @@ -73,7 +73,7 @@ async def test_connect_failure(hass: HomeAssistant, mock_gaierror: Generator) -> ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "cannot_connect" @@ -92,7 +92,7 @@ async def test_dhcp_flow(hass: HomeAssistant) -> None: ) flows = hass.config_entries.flow.async_progress() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert len(flows) == 1 assert ( get_schema_suggestion(result["data_schema"].schema, CONF_USERNAME) @@ -114,7 +114,7 @@ async def test_dhcp_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_dhcp_flow_auth_failure(hass: HomeAssistant) -> None: diff --git a/tests/components/octoprint/test_config_flow.py b/tests/components/octoprint/test_config_flow.py index 4c8e22e524c..3fb0d2a10bc 100644 --- a/tests/components/octoprint/test_config_flow.py +++ b/tests/components/octoprint/test_config_flow.py @@ -5,10 +5,11 @@ from unittest.mock import patch from pyoctoprintapi import ApiError, DiscoverySettings -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import ssdp, zeroconf from homeassistant.components.octoprint.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -242,7 +243,7 @@ async def test_show_zerconf_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_show_ssdp_form(hass: HomeAssistant) -> None: @@ -311,7 +312,7 @@ async def test_show_ssdp_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_import_yaml(hass: HomeAssistant) -> None: @@ -347,7 +348,7 @@ async def test_import_yaml(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert "errors" not in result @@ -384,7 +385,7 @@ async def test_import_duplicate_yaml(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(request_app_key.mock_calls) == 0 - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/ollama/test_config_flow.py b/tests/components/ollama/test_config_flow.py index 825f3eac436..c58f14a8c87 100644 --- a/tests/components/ollama/test_config_flow.py +++ b/tests/components/ollama/test_config_flow.py @@ -28,7 +28,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( ollama.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -49,13 +49,13 @@ async def test_form(hass: HomeAssistant) -> None: await hass.async_block_till_done() # Step 2: model - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], {ollama.CONF_MODEL: TEST_MODEL} ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["data"] == { ollama.CONF_URL: "http://localhost:11434", ollama.CONF_MODEL: TEST_MODEL, @@ -75,7 +75,7 @@ async def test_form_need_download(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( ollama.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None pull_ready = asyncio.Event() @@ -113,14 +113,14 @@ async def test_form_need_download(hass: HomeAssistant) -> None: await hass.async_block_till_done() # Step 2: model - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], {ollama.CONF_MODEL: TEST_MODEL} ) await hass.async_block_till_done() # Step 3: download - assert result3["type"] == FlowResultType.SHOW_PROGRESS + assert result3["type"] is FlowResultType.SHOW_PROGRESS result4 = await hass.config_entries.flow.async_configure( result3["flow_id"], ) @@ -128,12 +128,12 @@ async def test_form_need_download(hass: HomeAssistant) -> None: # Run again without the task finishing. # We should still be downloading. - assert result4["type"] == FlowResultType.SHOW_PROGRESS + assert result4["type"] is FlowResultType.SHOW_PROGRESS result4 = await hass.config_entries.flow.async_configure( result4["flow_id"], ) await hass.async_block_till_done() - assert result4["type"] == FlowResultType.SHOW_PROGRESS + assert result4["type"] is FlowResultType.SHOW_PROGRESS # Signal fake pull method to complete pull_ready.set() @@ -147,7 +147,7 @@ async def test_form_need_download(hass: HomeAssistant) -> None: result4["flow_id"], ) - assert result5["type"] == FlowResultType.CREATE_ENTRY + assert result5["type"] is FlowResultType.CREATE_ENTRY assert result5["data"] == { ollama.CONF_URL: "http://localhost:11434", ollama.CONF_MODEL: TEST_MODEL, @@ -167,7 +167,7 @@ async def test_options( {ollama.CONF_PROMPT: "test prompt", ollama.CONF_MAX_HISTORY: 100}, ) await hass.async_block_till_done() - assert options["type"] == FlowResultType.CREATE_ENTRY + assert options["type"] is FlowResultType.CREATE_ENTRY assert options["data"] == { ollama.CONF_PROMPT: "test prompt", ollama.CONF_MAX_HISTORY: 100, @@ -195,7 +195,7 @@ async def test_form_errors(hass: HomeAssistant, side_effect, error) -> None: result["flow_id"], {ollama.CONF_URL: "http://localhost:11434"} ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": error} @@ -220,15 +220,15 @@ async def test_download_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], {ollama.CONF_MODEL: TEST_MODEL} ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.SHOW_PROGRESS + assert result3["type"] is FlowResultType.SHOW_PROGRESS result4 = await hass.config_entries.flow.async_configure(result3["flow_id"]) await hass.async_block_till_done() - assert result4["type"] == FlowResultType.ABORT + assert result4["type"] is FlowResultType.ABORT assert result4["reason"] == "download_failed" diff --git a/tests/components/omnilogic/test_config_flow.py b/tests/components/omnilogic/test_config_flow.py index 1c0d6aefcf8..25d1c41ba68 100644 --- a/tests/components/omnilogic/test_config_flow.py +++ b/tests/components/omnilogic/test_config_flow.py @@ -4,9 +4,10 @@ from unittest.mock import patch from omnilogic import LoginException, OmniLogicException -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.omnilogic.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -134,7 +135,7 @@ async def test_option_flow(hass: HomeAssistant) -> None: data=None, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -142,6 +143,6 @@ async def test_option_flow(hass: HomeAssistant) -> None: user_input={"polling_interval": 9}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"]["polling_interval"] == 9 diff --git a/tests/components/oncue/test_config_flow.py b/tests/components/oncue/test_config_flow.py index d757adec771..2f327dec052 100644 --- a/tests/components/oncue/test_config_flow.py +++ b/tests/components/oncue/test_config_flow.py @@ -17,7 +17,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -36,7 +36,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "test-username" assert result2["data"] == { "username": "TEST-username", @@ -63,7 +63,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -85,7 +85,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -107,7 +107,7 @@ async def test_form_unknown_exception(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -136,5 +136,5 @@ async def test_already_configured(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/ondilo_ico/test_config_flow.py b/tests/components/ondilo_ico/test_config_flow.py index 53483241c0b..6b8fcbeefea 100644 --- a/tests/components/ondilo_ico/test_config_flow.py +++ b/tests/components/ondilo_ico/test_config_flow.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, setup from homeassistant.components.ondilo_ico.const import ( DOMAIN, OAUTH2_AUTHORIZE, @@ -12,6 +12,7 @@ from homeassistant.components.ondilo_ico.const import ( ) from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow from tests.common import MockConfigEntry @@ -29,7 +30,7 @@ async def test_abort_if_existing_entry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/onewire/test_config_flow.py b/tests/components/onewire/test_config_flow.py index 980ecb22d32..c147a522a59 100644 --- a/tests/components/onewire/test_config_flow.py +++ b/tests/components/onewire/test_config_flow.py @@ -41,7 +41,7 @@ async def test_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] # Invalid server @@ -54,7 +54,7 @@ async def test_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 1234}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -67,7 +67,7 @@ async def test_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 1234}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "1.2.3.4" assert result["data"] == { CONF_HOST: "1.2.3.4", @@ -89,7 +89,7 @@ async def test_user_duplicate( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -98,7 +98,7 @@ async def test_user_duplicate( result["flow_id"], user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 1234}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -122,7 +122,7 @@ async def test_user_options_clear( result["flow_id"], user_input={INPUT_ENTRY_CLEAR_OPTIONS: True}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {} @@ -146,7 +146,7 @@ async def test_user_options_empty_selection( result["flow_id"], user_input={INPUT_ENTRY_DEVICE_SELECTION: []}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "device_selection" assert result["errors"] == {"base": "device_not_selected"} @@ -174,7 +174,7 @@ async def test_user_options_set_single( result["flow_id"], user_input={INPUT_ENTRY_DEVICE_SELECTION: ["28.111111111111"]}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["description_placeholders"]["sensor_id"] == "28.111111111111" # Verify that the setting for the device comes back as default when no input is given @@ -182,7 +182,7 @@ async def test_user_options_set_single( result["flow_id"], user_input={}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert ( result["data"]["device_options"]["28.111111111111"]["precision"] == "temperature" @@ -220,7 +220,7 @@ async def test_user_options_set_multiple( ] }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert ( result["description_placeholders"]["sensor_id"] == "Given Name (28.222222222222)" @@ -231,7 +231,7 @@ async def test_user_options_set_multiple( result["flow_id"], user_input={"precision": "temperature"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert ( result["description_placeholders"]["sensor_id"] == "Given Name (28.111111111111)" @@ -242,7 +242,7 @@ async def test_user_options_set_multiple( result["flow_id"], user_input={"precision": "temperature9"}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert ( result["data"]["device_options"]["28.222222222222"]["precision"] == "temperature" @@ -262,5 +262,5 @@ async def test_user_options_no_devices( # Verify that first config step comes back with an empty list of possible devices to choose from result = await hass.config_entries.options.async_init(config_entry.entry_id) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "No configurable devices found." diff --git a/tests/components/onvif/test_config_flow.py b/tests/components/onvif/test_config_flow.py index e59db13d3bb..b08615add0e 100644 --- a/tests/components/onvif/test_config_flow.py +++ b/tests/components/onvif/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock, patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.components.onvif import DOMAIN, config_flow from homeassistant.config_entries import SOURCE_DHCP @@ -107,7 +107,7 @@ async def test_flow_discovered_devices(hass: HomeAssistant) -> None: config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -127,7 +127,7 @@ async def test_flow_discovered_devices(hass: HomeAssistant) -> None: result["flow_id"], user_input={"auto": True} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "device" container = result["data_schema"].schema[config_flow.CONF_HOST].container assert len(container) == 3 @@ -141,7 +141,7 @@ async def test_flow_discovered_devices(hass: HomeAssistant) -> None: result["flow_id"], user_input={config_flow.CONF_HOST: HOST} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure" with patch( @@ -158,7 +158,7 @@ async def test_flow_discovered_devices(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"{URN} - {MAC}" assert result["data"] == { config_flow.CONF_NAME: URN, @@ -180,7 +180,7 @@ async def test_flow_discovered_devices_ignore_configured_manual_input( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -200,7 +200,7 @@ async def test_flow_discovered_devices_ignore_configured_manual_input( result["flow_id"], user_input={"auto": True} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "device" assert len(result["data_schema"].schema[config_flow.CONF_HOST].container) == 2 @@ -209,7 +209,7 @@ async def test_flow_discovered_devices_ignore_configured_manual_input( user_input={config_flow.CONF_HOST: config_flow.CONF_MANUAL_INPUT}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure" @@ -221,7 +221,7 @@ async def test_flow_discovered_no_device(hass: HomeAssistant) -> None: config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -241,7 +241,7 @@ async def test_flow_discovered_no_device(hass: HomeAssistant) -> None: result["flow_id"], user_input={"auto": True} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure" @@ -266,7 +266,7 @@ async def test_flow_discovery_ignore_existing_and_abort(hass: HomeAssistant) -> config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -287,7 +287,7 @@ async def test_flow_discovery_ignore_existing_and_abort(hass: HomeAssistant) -> ) # It should skip to manual entry if the only devices are already configured - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure" result = await hass.config_entries.flow.async_configure( @@ -302,7 +302,7 @@ async def test_flow_discovery_ignore_existing_and_abort(hass: HomeAssistant) -> ) # It should abort if already configured and entered manually - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT async def test_flow_manual_entry(hass: HomeAssistant) -> None: @@ -312,7 +312,7 @@ async def test_flow_manual_entry(hass: HomeAssistant) -> None: config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -334,7 +334,7 @@ async def test_flow_manual_entry(hass: HomeAssistant) -> None: user_input={"auto": False}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure" with patch( @@ -354,7 +354,7 @@ async def test_flow_manual_entry(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"{NAME} - {MAC}" assert result["data"] == { config_flow.CONF_NAME: NAME, @@ -371,7 +371,7 @@ async def test_flow_manual_entry_no_profiles(hass: HomeAssistant) -> None: config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -403,7 +403,7 @@ async def test_flow_manual_entry_no_profiles(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_h264" @@ -413,7 +413,7 @@ async def test_flow_manual_entry_no_mac(hass: HomeAssistant) -> None: config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -447,7 +447,7 @@ async def test_flow_manual_entry_no_mac(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_mac" @@ -457,7 +457,7 @@ async def test_flow_manual_entry_fails(hass: HomeAssistant) -> None: config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -481,7 +481,7 @@ async def test_flow_manual_entry_fails(hass: HomeAssistant) -> None: user_input={"auto": False}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure" with patch( @@ -501,7 +501,7 @@ async def test_flow_manual_entry_fails(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 0 - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure" assert result["errors"] == {"base": "onvif_error"} assert result["description_placeholders"] == {"error": "camera not ready"} @@ -526,7 +526,7 @@ async def test_flow_manual_entry_fails(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 0 - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure" assert result["errors"] == {"base": "onvif_error"} assert result["description_placeholders"] == { @@ -567,7 +567,7 @@ async def test_flow_manual_entry_wrong_password(hass: HomeAssistant) -> None: config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -589,7 +589,7 @@ async def test_flow_manual_entry_wrong_password(hass: HomeAssistant) -> None: user_input={"auto": False}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure" with patch( @@ -609,7 +609,7 @@ async def test_flow_manual_entry_wrong_password(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 0 - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure" assert result["errors"] == {"password": "auth_failed"} assert result["description_placeholders"] == {"error": "Authority failure"} @@ -651,7 +651,7 @@ async def test_option_flow(hass: HomeAssistant, option_value: bool) -> None: entry.entry_id, context={"show_advanced_options": True} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "onvif_devices" result = await hass.config_entries.options.async_configure( @@ -664,7 +664,7 @@ async def test_option_flow(hass: HomeAssistant, option_value: bool) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { config_flow.CONF_EXTRA_ARGUMENTS: "", config_flow.CONF_RTSP_TRANSPORT: list(config_flow.RTSP_TRANSPORTS)[1], @@ -691,7 +691,7 @@ async def test_discovered_by_dhcp_updates_host(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == DHCP_DISCOVERY.ip @@ -716,7 +716,7 @@ async def test_discovered_by_dhcp_does_nothing_if_host_is_the_same( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == DHCP_DISCOVERY_SAME_IP.ip @@ -740,7 +740,7 @@ async def test_discovered_by_dhcp_does_not_update_if_already_loaded( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] != DHCP_DISCOVERY.ip @@ -754,7 +754,7 @@ async def test_discovered_by_dhcp_does_not_update_if_no_matching_entry( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -775,7 +775,7 @@ async def test_form_reauth(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_REAUTH, "entry_id": entry.entry_id}, data=entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert ( _get_schema_default(result["data_schema"].schema, CONF_USERNAME) @@ -804,7 +804,7 @@ async def test_form_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "reauth_confirm" assert result2["errors"] == {config_flow.CONF_PASSWORD: "auth_failed"} assert result2["description_placeholders"] == { @@ -833,7 +833,7 @@ async def test_form_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 assert entry.data[config_flow.CONF_USERNAME] == "new-test-username" @@ -850,7 +850,7 @@ async def test_flow_manual_entry_updates_existing_user_password( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -871,7 +871,7 @@ async def test_flow_manual_entry_updates_existing_user_password( result["flow_id"], user_input={"auto": False}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure" with patch( @@ -890,7 +890,7 @@ async def test_flow_manual_entry_updates_existing_user_password( await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[config_flow.CONF_USERNAME] == USERNAME assert entry.data[config_flow.CONF_PASSWORD] == "new_password" @@ -903,7 +903,7 @@ async def test_flow_manual_entry_wrong_port(hass: HomeAssistant) -> None: config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -925,7 +925,7 @@ async def test_flow_manual_entry_wrong_port(hass: HomeAssistant) -> None: user_input={"auto": False}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure" with patch( @@ -945,7 +945,7 @@ async def test_flow_manual_entry_wrong_port(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 0 - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure" assert result["errors"] == {"port": "no_onvif_service"} assert result["description_placeholders"] == {} diff --git a/tests/components/open_meteo/test_config_flow.py b/tests/components/open_meteo/test_config_flow.py index 2eda6a8192b..5ff01da0fe9 100644 --- a/tests/components/open_meteo/test_config_flow.py +++ b/tests/components/open_meteo/test_config_flow.py @@ -19,7 +19,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( @@ -27,6 +27,6 @@ async def test_full_user_flow( user_input={CONF_ZONE: ENTITY_ID_HOME}, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "test home" assert result2.get("data") == {CONF_ZONE: ENTITY_ID_HOME} diff --git a/tests/components/openai_conversation/test_config_flow.py b/tests/components/openai_conversation/test_config_flow.py index 659b3825472..57f03d0c0bf 100644 --- a/tests/components/openai_conversation/test_config_flow.py +++ b/tests/components/openai_conversation/test_config_flow.py @@ -30,7 +30,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -50,7 +50,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { "api_key": "bla", } @@ -72,7 +72,7 @@ async def test_options( }, ) await hass.async_block_till_done() - assert options["type"] == FlowResultType.CREATE_ENTRY + assert options["type"] is FlowResultType.CREATE_ENTRY assert options["data"]["prompt"] == "Speak like a pirate" assert options["data"]["max_tokens"] == 200 assert options["data"][CONF_CHAT_MODEL] == DEFAULT_CHAT_MODEL @@ -113,5 +113,5 @@ async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> Non }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": error} diff --git a/tests/components/openexchangerates/test_config_flow.py b/tests/components/openexchangerates/test_config_flow.py index e0896f64340..7c8ad7dfc77 100644 --- a/tests/components/openexchangerates/test_config_flow.py +++ b/tests/components/openexchangerates/test_config_flow.py @@ -38,7 +38,7 @@ async def test_user_create_entry( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -47,7 +47,7 @@ async def test_user_create_entry( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "USD" assert result["data"] == { "api_key": "test-api-key", @@ -71,7 +71,7 @@ async def test_form_invalid_auth( {"api_key": "bad-api-key"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -90,7 +90,7 @@ async def test_form_cannot_connect( {"api_key": "test-api-key"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -109,7 +109,7 @@ async def test_form_unknown_error( {"api_key": "test-api-key"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "unknown"} @@ -123,7 +123,7 @@ async def test_already_configured_service( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -131,7 +131,7 @@ async def test_already_configured_service( {"api_key": "test-api-key"}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -141,7 +141,7 @@ async def test_no_currencies(hass: HomeAssistant, currencies: AsyncMock) -> None result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -160,7 +160,7 @@ async def test_currencies_timeout(hass: HomeAssistant, currencies: AsyncMock) -> result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "timeout_connect" @@ -188,7 +188,7 @@ async def test_latest_rates_timeout( {"api_key": "test-api-key"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "timeout_connect"} @@ -210,7 +210,7 @@ async def test_reauth( result = await hass.config_entries.flow.async_init( DOMAIN, context=flow_context, data=mock_config_entry.data ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None mock_latest_rates_config_flow.side_effect = OpenExchangeRatesAuthError() @@ -222,7 +222,7 @@ async def test_reauth( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} mock_latest_rates_config_flow.side_effect = None diff --git a/tests/components/opengarage/test_config_flow.py b/tests/components/opengarage/test_config_flow.py index 7d3e44017b0..be15ea425ae 100644 --- a/tests/components/opengarage/test_config_flow.py +++ b/tests/components/opengarage/test_config_flow.py @@ -18,7 +18,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -37,7 +37,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Name of the device" assert result2["data"] == { "host": "http://1.1.1.1", @@ -63,7 +63,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: {"host": "http://1.1.1.1", "device_key": "AfsasdnfkjDD"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -82,7 +82,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: {"host": "http://1.1.1.1", "device_key": "AfsasdnfkjDD"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -101,7 +101,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: {"host": "http://1.1.1.1", "device_key": "AfsasdnfkjDD"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -132,5 +132,5 @@ async def test_flow_entry_already_exists(hass: HomeAssistant) -> None: }, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/openhome/test_config_flow.py b/tests/components/openhome/test_config_flow.py index 4cc5c58dda5..7ab1e69106c 100644 --- a/tests/components/openhome/test_config_flow.py +++ b/tests/components/openhome/test_config_flow.py @@ -1,6 +1,5 @@ """Tests for the Openhome config flow module.""" -from homeassistant import data_entry_flow from homeassistant.components import ssdp from homeassistant.components.openhome.const import DOMAIN from homeassistant.components.ssdp import ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_UDN @@ -31,7 +30,7 @@ async def test_ssdp(hass: HomeAssistant) -> None: data=MOCK_DISCOVER, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["description_placeholders"] == {CONF_NAME: MOCK_FRIENDLY_NAME} @@ -55,7 +54,7 @@ async def test_device_exists(hass: HomeAssistant) -> None: context={CONF_SOURCE: SOURCE_SSDP}, data=MOCK_DISCOVER, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -74,7 +73,7 @@ async def test_missing_udn(hass: HomeAssistant) -> None: context={CONF_SOURCE: SOURCE_SSDP}, data=broken_discovery, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "incomplete_discovery" @@ -91,7 +90,7 @@ async def test_missing_ssdp_location(hass: HomeAssistant) -> None: context={CONF_SOURCE: SOURCE_SSDP}, data=broken_discovery, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "incomplete_discovery" @@ -110,7 +109,7 @@ async def test_host_updated(hass: HomeAssistant) -> None: context={CONF_SOURCE: SOURCE_SSDP}, data=MOCK_DISCOVER, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == MOCK_SSDP_LOCATION diff --git a/tests/components/opensky/test_config_flow.py b/tests/components/opensky/test_config_flow.py index 8168511a16c..e30d5ad8475 100644 --- a/tests/components/opensky/test_config_flow.py +++ b/tests/components/opensky/test_config_flow.py @@ -6,7 +6,6 @@ from unittest.mock import AsyncMock import pytest from python_opensky.exceptions import OpenSkyUnauthenticatedError -from homeassistant import data_entry_flow from homeassistant.components.opensky.const import ( CONF_ALTITUDE, CONF_CONTRIBUTING_USER, @@ -43,7 +42,7 @@ async def test_full_user_flow(hass: HomeAssistant, mock_setup_entry) -> None: CONF_ALTITUDE: 0, }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "OpenSky" assert result["data"] == { CONF_LATITUDE: 0.0, @@ -89,7 +88,7 @@ async def test_options_flow_failures( result = await hass.config_entries.options.async_init(config_entry.entry_id) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -98,7 +97,7 @@ async def test_options_flow_failures( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"]["base"] == error opensky_client.authenticate.side_effect = None @@ -113,7 +112,7 @@ async def test_options_flow_failures( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_RADIUS: 10000, CONF_USERNAME: "homeassistant", @@ -143,7 +142,7 @@ async def test_options_flow( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_RADIUS: 10000, CONF_USERNAME: "homeassistant", diff --git a/tests/components/opentherm_gw/test_config_flow.py b/tests/components/opentherm_gw/test_config_flow.py index c92f23f46b4..ff4ea9d7bb4 100644 --- a/tests/components/opentherm_gw/test_config_flow.py +++ b/tests/components/opentherm_gw/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import patch from pyotgw.vars import OTGW, OTGW_ABOUT from serial import SerialException -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.opentherm_gw.const import ( CONF_FLOOR_TEMP, CONF_PRECISION, @@ -22,6 +22,7 @@ from homeassistant.const import ( PRECISION_TENTHS, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -241,7 +242,7 @@ async def test_options_migration(hass: HomeAssistant) -> None: entry.entry_id, context={"source": config_entries.SOURCE_USER}, data=None ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -249,7 +250,7 @@ async def test_options_migration(hass: HomeAssistant) -> None: user_input={}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_READ_PRECISION] == PRECISION_TENTHS assert result["data"][CONF_SET_PRECISION] == PRECISION_TENTHS assert result["data"][CONF_FLOOR_TEMP] is True @@ -281,7 +282,7 @@ async def test_options_form(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init( entry.entry_id, context={"source": "test"}, data=None ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -294,7 +295,7 @@ async def test_options_form(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_READ_PRECISION] == PRECISION_HALVES assert result["data"][CONF_SET_PRECISION] == PRECISION_HALVES assert result["data"][CONF_TEMPORARY_OVRD_MODE] is True @@ -308,7 +309,7 @@ async def test_options_form(hass: HomeAssistant) -> None: result["flow_id"], user_input={CONF_READ_PRECISION: 0} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_READ_PRECISION] == 0.0 assert result["data"][CONF_SET_PRECISION] == PRECISION_HALVES assert result["data"][CONF_TEMPORARY_OVRD_MODE] is True @@ -328,7 +329,7 @@ async def test_options_form(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_READ_PRECISION] == PRECISION_TENTHS assert result["data"][CONF_SET_PRECISION] == PRECISION_HALVES assert result["data"][CONF_TEMPORARY_OVRD_MODE] is False diff --git a/tests/components/openuv/test_config_flow.py b/tests/components/openuv/test_config_flow.py index db71b712fd9..3d31cf53250 100644 --- a/tests/components/openuv/test_config_flow.py +++ b/tests/components/openuv/test_config_flow.py @@ -6,7 +6,6 @@ from pyopenuv.errors import InvalidApiKeyError import pytest import voluptuous as vol -from homeassistant import data_entry_flow from homeassistant.components.openuv import CONF_FROM_WINDOW, CONF_TO_WINDOW, DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import ( @@ -16,6 +15,7 @@ from homeassistant.const import ( CONF_LONGITUDE, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .conftest import TEST_API_KEY, TEST_ELEVATION, TEST_LATITUDE, TEST_LONGITUDE @@ -27,7 +27,7 @@ async def test_create_entry(hass: HomeAssistant, client, config, mock_pyopenuv) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # Test an error occurring: @@ -35,7 +35,7 @@ async def test_create_entry(hass: HomeAssistant, client, config, mock_pyopenuv) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {CONF_API_KEY: "invalid_api_key"} @@ -43,7 +43,7 @@ async def test_create_entry(hass: HomeAssistant, client, config, mock_pyopenuv) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"{TEST_LATITUDE}, {TEST_LONGITUDE}" assert result["data"] == { CONF_API_KEY: TEST_API_KEY, @@ -60,7 +60,7 @@ async def test_duplicate_error( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -69,7 +69,7 @@ async def test_options_flow( ) -> None: """Test config flow options.""" result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" def get_schema_marker(data_schema: vol.Schema, key: str) -> vol.Marker: @@ -89,12 +89,12 @@ async def test_options_flow( result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_FROM_WINDOW: 3.5, CONF_TO_WINDOW: 2.0} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_FROM_WINDOW: 3.5, CONF_TO_WINDOW: 2.0} # Subsequent schema uses previous input for suggested values: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert get_schema_marker(result["data_schema"], CONF_FROM_WINDOW).description == { "suggested_value": 3.5 @@ -114,12 +114,12 @@ async def test_step_reauth( assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_API_KEY: "new_api_key"} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 diff --git a/tests/components/openweathermap/test_config_flow.py b/tests/components/openweathermap/test_config_flow.py index 77a10c5b26f..d6c043b62a8 100644 --- a/tests/components/openweathermap/test_config_flow.py +++ b/tests/components/openweathermap/test_config_flow.py @@ -4,7 +4,6 @@ from unittest.mock import MagicMock, patch from pyowm.commons.exceptions import APIRequestError, UnauthorizedError -from homeassistant import data_entry_flow from homeassistant.components.openweathermap.const import ( DEFAULT_FORECAST_MODE, DEFAULT_LANGUAGE, @@ -20,6 +19,7 @@ from homeassistant.const import ( CONF_NAME, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -47,7 +47,7 @@ async def test_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -65,7 +65,7 @@ async def test_form(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert entry.state == ConfigEntryState.NOT_LOADED - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == CONFIG[CONF_NAME] assert result["data"][CONF_LATITUDE] == CONFIG[CONF_LATITUDE] assert result["data"][CONF_LONGITUDE] == CONFIG[CONF_LONGITUDE] @@ -92,14 +92,14 @@ async def test_form_options(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + 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_MODE: "daily"} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_MODE: "daily", CONF_LANGUAGE: DEFAULT_LANGUAGE, @@ -111,14 +111,14 @@ async def test_form_options(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + 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_MODE: "onecall_daily"} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_MODE: "onecall_daily", CONF_LANGUAGE: DEFAULT_LANGUAGE, diff --git a/tests/components/opower/test_config_flow.py b/tests/components/opower/test_config_flow.py index cb796d8e255..512a602a043 100644 --- a/tests/components/opower/test_config_flow.py +++ b/tests/components/opower/test_config_flow.py @@ -42,7 +42,7 @@ async def test_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] with patch( @@ -58,7 +58,7 @@ async def test_form( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Pacific Gas and Electric Company (PG&E) (test-username)" assert result2["data"] == { "utility": "Pacific Gas and Electric Company (PG&E)", @@ -76,7 +76,7 @@ async def test_form_with_mfa( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -87,7 +87,7 @@ async def test_form_with_mfa( "password": "test-password", }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert not result2["errors"] with patch( @@ -100,7 +100,7 @@ async def test_form_with_mfa( }, ) - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Consolidated Edison (ConEd) (test-username)" assert result3["data"] == { "utility": "Consolidated Edison (ConEd)", @@ -119,7 +119,7 @@ async def test_form_with_mfa_bad_secret( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -130,7 +130,7 @@ async def test_form_with_mfa_bad_secret( "password": "test-password", }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert not result2["errors"] with patch( @@ -144,7 +144,7 @@ async def test_form_with_mfa_bad_secret( }, ) - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == { "base": "invalid_auth", } @@ -161,7 +161,7 @@ async def test_form_with_mfa_bad_secret( }, ) - assert result4["type"] == FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == "Consolidated Edison (ConEd) (test-username)" assert result4["data"] == { "utility": "Consolidated Edison (ConEd)", @@ -201,7 +201,7 @@ async def test_form_exceptions( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": expected_error} assert mock_login.call_count == 1 @@ -228,7 +228,7 @@ async def test_form_already_configured( }, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" assert mock_login.call_count == 0 @@ -257,7 +257,7 @@ async def test_form_not_already_configured( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert ( result2["title"] == "Pacific Gas and Electric Company (PG&E) (test-username2)" ) @@ -297,7 +297,7 @@ async def test_form_valid_reauth( {"username": "test-username", "password": "test-password2"}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" await hass.async_block_till_done() @@ -347,7 +347,7 @@ async def test_form_valid_reauth_with_mfa( }, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" await hass.async_block_till_done() diff --git a/tests/components/oralb/test_config_flow.py b/tests/components/oralb/test_config_flow.py index 197da4264d1..dee16cd0632 100644 --- a/tests/components/oralb/test_config_flow.py +++ b/tests/components/oralb/test_config_flow.py @@ -19,13 +19,13 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=ORALB_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch("homeassistant.components.oralb.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Smart Series 7000 48BE" assert result2["data"] == {} assert result2["result"].unique_id == "78:DB:2F:C2:48:BE" @@ -40,13 +40,13 @@ async def test_async_step_bluetooth_valid_io_series4_device( context={"source": config_entries.SOURCE_BLUETOOTH}, data=ORALB_IO_SERIES_4_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch("homeassistant.components.oralb.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "IO Series 4 48BE" assert result2["data"] == {} assert result2["result"].unique_id == "78:DB:2F:C2:48:BE" @@ -59,7 +59,7 @@ async def test_async_step_bluetooth_not_oralb(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_ORALB_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -69,7 +69,7 @@ async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -83,14 +83,14 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch("homeassistant.components.oralb.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"address": "78:DB:2F:C2:48:BE"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Smart Series 7000 48BE" assert result2["data"] == {} assert result2["result"].unique_id == "78:DB:2F:C2:48:BE" @@ -106,7 +106,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -120,7 +120,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - result["flow_id"], user_input={"address": "78:DB:2F:C2:48:BE"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -142,7 +142,7 @@ async def test_async_step_user_with_found_devices_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -159,7 +159,7 @@ async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant) - context={"source": config_entries.SOURCE_BLUETOOTH}, data=ORALB_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -170,7 +170,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=ORALB_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -178,7 +178,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=ORALB_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -191,7 +191,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=ORALB_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -202,14 +202,14 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch("homeassistant.components.oralb.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"address": "78:DB:2F:C2:48:BE"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Smart Series 7000 48BE" assert result2["data"] == {} assert result2["result"].unique_id == "78:DB:2F:C2:48:BE" diff --git a/tests/components/osoenergy/test_config_flow.py b/tests/components/osoenergy/test_config_flow.py index c88035eef28..d9db5888cc3 100644 --- a/tests/components/osoenergy/test_config_flow.py +++ b/tests/components/osoenergy/test_config_flow.py @@ -4,10 +4,11 @@ from unittest.mock import patch from apyosoenergyapi.helper import osoenergy_exceptions -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.osoenergy.const import DOMAIN from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -23,7 +24,7 @@ async def test_user_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -41,7 +42,7 @@ async def test_user_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_USER_EMAIL assert result2["data"] == { CONF_API_KEY: SUBSCRIPTION_KEY, @@ -74,7 +75,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: data=mock_config.data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} with patch( @@ -90,7 +91,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert mock_config.data.get(CONF_API_KEY) == SUBSCRIPTION_KEY - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -116,7 +117,7 @@ async def test_abort_if_existing_entry(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -126,7 +127,7 @@ async def test_user_flow_invalid_subscription_key(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -138,7 +139,7 @@ async def test_user_flow_invalid_subscription_key(hass: HomeAssistant) -> None: {CONF_API_KEY: SUBSCRIPTION_KEY}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_auth"} @@ -151,7 +152,7 @@ async def test_user_flow_exception_on_subscription_key_check( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -163,6 +164,6 @@ async def test_user_flow_exception_on_subscription_key_check( {CONF_API_KEY: SUBSCRIPTION_KEY}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_auth"} diff --git a/tests/components/otbr/test_config_flow.py b/tests/components/otbr/test_config_flow.py index 81dcb894be6..224f77931e5 100644 --- a/tests/components/otbr/test_config_flow.py +++ b/tests/components/otbr/test_config_flow.py @@ -61,7 +61,7 @@ async def test_user_flow( expected_data = {"url": url} - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -74,7 +74,7 @@ async def test_user_flow( "url": url, }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Open Thread Border Router" assert result["data"] == expected_data assert result["options"] == {} @@ -102,7 +102,7 @@ async def test_user_flow_router_not_setup( result = await hass.config_entries.flow.async_init( otbr.DOMAIN, context={"source": "user"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -140,7 +140,7 @@ async def test_user_flow_router_not_setup( "url": "http://custom_url:1234", } - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Open Thread Border Router" assert result["data"] == expected_data assert result["options"] == {} @@ -163,7 +163,7 @@ async def test_user_flow_404( otbr.DOMAIN, context={"source": "user"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( @@ -172,7 +172,7 @@ async def test_user_flow_404( "url": url, }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -190,7 +190,7 @@ async def test_user_flow_connect_error(hass: HomeAssistant, error) -> None: otbr.DOMAIN, context={"source": "user"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch("python_otbr_api.OTBR.get_active_dataset_tlvs", side_effect=error): @@ -200,7 +200,7 @@ async def test_user_flow_connect_error(hass: HomeAssistant, error) -> None: "url": "http://custom_url:1234", }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -223,7 +223,7 @@ async def test_hassio_discovery_flow( "url": f"http://{HASSIO_DATA.config['host']}:{HASSIO_DATA.config['port']}", } - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Silicon Labs Multiprotocol" assert result["data"] == expected_data assert result["options"] == {} @@ -267,7 +267,7 @@ async def test_hassio_discovery_flow_yellow( "url": f"http://{HASSIO_DATA.config['host']}:{HASSIO_DATA.config['port']}", } - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Home Assistant Yellow (Silicon Labs Multiprotocol)" assert result["data"] == expected_data assert result["options"] == {} @@ -325,7 +325,7 @@ async def test_hassio_discovery_flow_sky_connect( "url": f"http://{HASSIO_DATA.config['host']}:{HASSIO_DATA.config['port']}", } - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == title assert result["data"] == expected_data assert result["options"] == {} @@ -396,13 +396,13 @@ async def test_hassio_discovery_flow_2x_addons( "url": f"http://{HASSIO_DATA.config['host']}:{HASSIO_DATA.config['port']}", } - assert results[0]["type"] == FlowResultType.CREATE_ENTRY + assert results[0]["type"] is FlowResultType.CREATE_ENTRY assert ( results[0]["title"] == "Home Assistant SkyConnect (Silicon Labs Multiprotocol)" ) assert results[0]["data"] == expected_data assert results[0]["options"] == {} - assert results[1]["type"] == FlowResultType.ABORT + assert results[1]["type"] is FlowResultType.ABORT assert results[1]["reason"] == "single_instance_allowed" assert len(hass.config_entries.async_entries(otbr.DOMAIN)) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -460,7 +460,7 @@ async def test_hassio_discovery_flow_router_not_setup( "url": f"http://{HASSIO_DATA.config['host']}:{HASSIO_DATA.config['port']}", } - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Silicon Labs Multiprotocol" assert result["data"] == expected_data assert result["options"] == {} @@ -512,7 +512,7 @@ async def test_hassio_discovery_flow_router_not_setup_has_preferred( "url": f"http://{HASSIO_DATA.config['host']}:{HASSIO_DATA.config['port']}", } - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Silicon Labs Multiprotocol" assert result["data"] == expected_data assert result["options"] == {} @@ -575,7 +575,7 @@ async def test_hassio_discovery_flow_router_not_setup_has_preferred_2( "url": f"http://{HASSIO_DATA.config['host']}:{HASSIO_DATA.config['port']}", } - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Silicon Labs Multiprotocol" assert result["data"] == expected_data assert result["options"] == {} @@ -598,7 +598,7 @@ async def test_hassio_discovery_flow_404( otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -624,7 +624,7 @@ async def test_hassio_discovery_flow_new_port_missing_unique_id( otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" expected_data = { @@ -655,7 +655,7 @@ async def test_hassio_discovery_flow_new_port(hass: HomeAssistant) -> None: otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" expected_data = { @@ -683,7 +683,7 @@ async def test_hassio_discovery_flow_new_port_other_addon(hass: HomeAssistant) - otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" # Make sure the data was not updated @@ -718,6 +718,6 @@ async def test_config_flow_single_entry( otbr.DOMAIN, context={"source": source}, data=data ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" mock_setup_entry.assert_not_called() diff --git a/tests/components/ourgroceries/test_config_flow.py b/tests/components/ourgroceries/test_config_flow.py index 0eb17cd4ff6..b18fd699c9a 100644 --- a/tests/components/ourgroceries/test_config_flow.py +++ b/tests/components/ourgroceries/test_config_flow.py @@ -19,7 +19,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -35,7 +35,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "test-username" assert result2["data"] == { "username": "test-username", @@ -73,7 +73,7 @@ async def test_form_error( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": error} with patch( "homeassistant.components.ourgroceries.config_flow.OurGroceries.login", @@ -87,7 +87,7 @@ async def test_form_error( }, ) - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "test-username" assert result3["data"] == { "username": "test-username", diff --git a/tests/components/overkiz/test_config_flow.py b/tests/components/overkiz/test_config_flow.py index dbe8c690bc4..d99fe57233c 100644 --- a/tests/components/overkiz/test_config_flow.py +++ b/tests/components/overkiz/test_config_flow.py @@ -16,11 +16,12 @@ from pyoverkiz.exceptions import ( ) import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.components.overkiz.const import DOMAIN from homeassistant.components.zeroconf import ZeroconfServiceInfo from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -243,7 +244,7 @@ async def test_form_invalid_auth_cloud( await hass.async_block_till_done() - assert result4["type"] == data_entry_flow.FlowResultType.FORM + assert result4["type"] is FlowResultType.FORM assert result4["errors"] == {"base": error} @@ -304,7 +305,7 @@ async def test_form_invalid_auth_local( await hass.async_block_till_done() - assert result4["type"] == data_entry_flow.FlowResultType.FORM + assert result4["type"] is FlowResultType.FORM assert result4["errors"] == {"base": error} @@ -350,7 +351,7 @@ async def test_form_local_developer_mode_disabled( }, ) - assert result4["type"] == data_entry_flow.FlowResultType.FORM + assert result4["type"] is FlowResultType.FORM assert result4["errors"] == {"base": "developer_mode_disabled"} @@ -386,7 +387,7 @@ async def test_form_invalid_cozytouch_auth( await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": error} assert result3["step_id"] == "cloud" @@ -436,7 +437,7 @@ async def test_cloud_abort_on_duplicate_entry( {"username": TEST_EMAIL, "password": TEST_PASSWORD}, ) - assert result4["type"] == data_entry_flow.FlowResultType.ABORT + assert result4["type"] is FlowResultType.ABORT assert result4["reason"] == "already_configured" @@ -496,7 +497,7 @@ async def test_local_abort_on_duplicate_entry( }, ) - assert result4["type"] == data_entry_flow.FlowResultType.ABORT + assert result4["type"] is FlowResultType.ABORT assert result4["reason"] == "already_configured" @@ -582,7 +583,7 @@ async def test_cloud_reauth_success(hass: HomeAssistant) -> None: data=mock_entry.data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" with ( @@ -600,7 +601,7 @@ async def test_cloud_reauth_success(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert mock_entry.data["username"] == TEST_EMAIL assert mock_entry.data["password"] == TEST_PASSWORD2 @@ -632,7 +633,7 @@ async def test_cloud_reauth_wrong_account(hass: HomeAssistant) -> None: data=mock_entry.data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" with ( @@ -650,7 +651,7 @@ async def test_cloud_reauth_wrong_account(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_wrong_account" @@ -681,7 +682,7 @@ async def test_local_reauth_success(hass: HomeAssistant) -> None: data=mock_entry.data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "local_or_cloud" result2 = await hass.config_entries.flow.async_configure( @@ -707,7 +708,7 @@ async def test_local_reauth_success(hass: HomeAssistant) -> None: }, ) - assert result3["type"] == data_entry_flow.FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" assert mock_entry.data["username"] == TEST_EMAIL assert mock_entry.data["password"] == TEST_PASSWORD2 @@ -739,7 +740,7 @@ async def test_local_reauth_wrong_account(hass: HomeAssistant) -> None: }, data=mock_entry.data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "local_or_cloud" result2 = await hass.config_entries.flow.async_configure( @@ -765,7 +766,7 @@ async def test_local_reauth_wrong_account(hass: HomeAssistant) -> None: }, ) - assert result3["type"] == data_entry_flow.FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_wrong_account" @@ -781,7 +782,7 @@ async def test_dhcp_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER result2 = await hass.config_entries.flow.async_configure( @@ -840,7 +841,7 @@ async def test_dhcp_flow_already_configured(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -852,7 +853,7 @@ async def test_zeroconf_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) - context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER result2 = await hass.config_entries.flow.async_configure( @@ -905,7 +906,7 @@ async def test_local_zeroconf_flow( context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER result2 = await hass.config_entries.flow.async_configure( @@ -967,5 +968,5 @@ async def test_zeroconf_flow_already_configured(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/ovo_energy/test_config_flow.py b/tests/components/ovo_energy/test_config_flow.py index 3975be7cf80..7575f1edb29 100644 --- a/tests/components/ovo_energy/test_config_flow.py +++ b/tests/components/ovo_energy/test_config_flow.py @@ -4,10 +4,11 @@ from unittest.mock import patch import aiohttp -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.ovo_energy.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -23,7 +24,7 @@ async def test_show_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -33,7 +34,7 @@ async def test_authorization_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -45,7 +46,7 @@ async def test_authorization_error(hass: HomeAssistant) -> None: FIXTURE_USER_INPUT, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_auth"} @@ -56,7 +57,7 @@ async def test_connection_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -68,7 +69,7 @@ async def test_connection_error(hass: HomeAssistant) -> None: FIXTURE_USER_INPUT, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -79,7 +80,7 @@ async def test_full_flow_implementation(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -101,7 +102,7 @@ async def test_full_flow_implementation(hass: HomeAssistant) -> None: FIXTURE_USER_INPUT, ) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME] assert result2["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] @@ -118,7 +119,7 @@ async def test_reauth_authorization_error(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth" result2 = await hass.config_entries.flow.async_configure( @@ -127,7 +128,7 @@ async def test_reauth_authorization_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "reauth" assert result2["errors"] == {"base": "authorization_error"} @@ -144,7 +145,7 @@ async def test_reauth_connection_error(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth" result2 = await hass.config_entries.flow.async_configure( @@ -153,7 +154,7 @@ async def test_reauth_connection_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "reauth" assert result2["errors"] == {"base": "connection_error"} @@ -175,7 +176,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth" assert result["errors"] == {"base": "authorization_error"} @@ -195,5 +196,5 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" diff --git a/tests/components/owntracks/test_config_flow.py b/tests/components/owntracks/test_config_flow.py index 8b353789c83..818524c1c50 100644 --- a/tests/components/owntracks/test_config_flow.py +++ b/tests/components/owntracks/test_config_flow.py @@ -4,13 +4,14 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.owntracks import config_flow from homeassistant.components.owntracks.config_flow import CONF_CLOUDHOOK, CONF_SECRET from homeassistant.components.owntracks.const import DOMAIN from homeassistant.config import async_process_ha_core_config from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -66,11 +67,11 @@ async def test_user(hass: HomeAssistant, webhook_id, secret) -> None: flow = await init_config_flow(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await flow.async_step_user({}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "OwnTracks" assert result["data"][CONF_WEBHOOK_ID] == WEBHOOK_ID assert result["data"][CONF_SECRET] == SECRET @@ -100,7 +101,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant) -> None: # Should fail, already setup (flow) result = await flow.async_step_user({}) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -111,7 +112,7 @@ async def test_user_not_supports_encryption( flow = await init_config_flow(hass) result = await flow.async_step_user({}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert ( result["description_placeholders"]["secret"] == "Encryption is not supported because nacl is not installed." @@ -169,7 +170,7 @@ async def test_with_cloud_sub(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data={} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY entry = result["result"] assert entry.data["cloudhook"] assert ( @@ -198,5 +199,5 @@ async def test_with_cloud_sub_not_connected(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data={} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cloud_not_connected" From ee66f6ec8c2352fe606f9b0acb9faf2fdec8399c Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 2 Apr 2024 23:21:50 +0200 Subject: [PATCH 191/967] Use is in enum comparison in config flow tests P-T (#114675) --- .../components/p1_monitor/test_config_flow.py | 6 +- tests/components/peco/test_config_flow.py | 26 ++-- .../pegel_online/test_config_flow.py | 28 ++-- tests/components/permobil/test_config_flow.py | 38 ++--- .../components/philips_js/test_config_flow.py | 11 +- tests/components/pi_hole/test_config_flow.py | 18 +-- tests/components/picnic/test_config_flow.py | 21 +-- tests/components/ping/test_config_flow.py | 12 +- tests/components/plaato/test_config_flow.py | 42 +++--- tests/components/plex/test_config_flow.py | 130 +++++++++--------- tests/components/plugwise/test_config_flow.py | 30 ++-- tests/components/point/test_config_flow.py | 26 ++-- .../components/poolsense/test_config_flow.py | 6 +- .../components/powerwall/test_config_flow.py | 16 +-- .../private_ble_device/test_config_flow.py | 6 +- .../progettihwsw/test_config_flow.py | 12 +- tests/components/prosegur/test_config_flow.py | 6 +- .../components/proximity/test_config_flow.py | 16 +-- .../components/prusalink/test_config_flow.py | 18 +-- tests/components/ps4/test_config_flow.py | 85 ++++++------ .../pure_energie/test_config_flow.py | 12 +- .../components/purpleair/test_config_flow.py | 52 +++---- .../components/pushbullet/test_config_flow.py | 10 +- tests/components/pushover/test_config_flow.py | 22 +-- tests/components/pvoutput/test_config_flow.py | 28 ++-- .../pvpc_hourly_pricing/test_config_flow.py | 37 ++--- .../qbittorrent/test_config_flow.py | 12 +- tests/components/qingping/test_config_flow.py | 34 ++--- tests/components/qnap/test_config_flow.py | 13 +- tests/components/qnap_qsw/test_config_flow.py | 15 +- .../components/rabbitair/test_config_flow.py | 18 +-- tests/components/radarr/test_config_flow.py | 24 ++-- .../radio_browser/test_config_flow.py | 8 +- .../components/radiotherm/test_config_flow.py | 21 +-- tests/components/rainbird/test_config_flow.py | 14 +- .../rainforest_eagle/test_config_flow.py | 8 +- .../rainforest_raven/test_config_flow.py | 30 ++-- .../rainmachine/test_config_flow.py | 25 ++-- tests/components/random/test_config_flow.py | 14 +- tests/components/rapt_ble/test_config_flow.py | 30 ++-- .../raspberry_pi/test_config_flow.py | 4 +- tests/components/rdw/test_config_flow.py | 12 +- .../recollect_waste/test_config_flow.py | 14 +- tests/components/refoss/test_config_flow.py | 11 +- tests/components/renault/test_config_flow.py | 29 ++-- tests/components/renson/test_config_flow.py | 8 +- tests/components/reolink/test_config_flow.py | 43 +++--- tests/components/rfxtrx/test_config_flow.py | 19 +-- tests/components/rhasspy/test_config_flow.py | 6 +- tests/components/ridwell/test_config_flow.py | 10 +- tests/components/ring/test_config_flow.py | 14 +- tests/components/risco/test_config_flow.py | 28 ++-- tests/components/roborock/test_config_flow.py | 26 ++-- tests/components/roku/test_config_flow.py | 32 ++--- tests/components/romy/test_config_flow.py | 29 ++-- tests/components/roon/test_config_flow.py | 5 +- tests/components/rova/test_config_flow.py | 24 ++-- .../components/rpi_power/test_config_flow.py | 10 +- .../ruckus_unleashed/test_config_flow.py | 39 +++--- .../ruuvi_gateway/test_config_flow.py | 16 +-- .../ruuvitag_ble/test_config_flow.py | 30 ++-- tests/components/rympro/test_config_flow.py | 10 +- tests/components/sabnzbd/test_config_flow.py | 8 +- .../components/samsungtv/test_config_flow.py | 28 ++-- tests/components/schlage/test_config_flow.py | 14 +- tests/components/scrape/test_config_flow.py | 54 ++++---- tests/components/season/test_config_flow.py | 6 +- tests/components/sensibo/test_config_flow.py | 24 ++-- .../sensirion_ble/test_config_flow.py | 30 ++-- .../components/sensorpro/test_config_flow.py | 30 ++-- .../components/sensorpush/test_config_flow.py | 30 ++-- tests/components/sentry/test_config_flow.py | 12 +- .../seventeentrack/test_config_flow.py | 18 +-- tests/components/sfr_box/test_config_flow.py | 33 ++--- tests/components/shelly/test_config_flow.py | 99 ++++++------- .../shopping_list/test_config_flow.py | 8 +- tests/components/sia/test_config_flow.py | 20 ++- .../components/simplepush/test_config_flow.py | 13 +- .../components/simplisafe/test_config_flow.py | 26 ++-- tests/components/skybell/test_config_flow.py | 22 +-- tests/components/slack/test_config_flow.py | 13 +- tests/components/sleepiq/test_config_flow.py | 11 +- .../components/slimproto/test_config_flow.py | 4 +- tests/components/sma/test_config_flow.py | 14 +- tests/components/smappee/test_config_flow.py | 67 ++++----- .../smartthings/test_config_flow.py | 105 +++++++------- tests/components/smarttub/test_config_flow.py | 11 +- tests/components/smhi/test_config_flow.py | 18 +-- tests/components/snapcast/test_config_flow.py | 12 +- tests/components/snooz/test_config_flow.py | 40 +++--- .../components/solaredge/test_config_flow.py | 18 +-- tests/components/solarlog/test_config_flow.py | 21 +-- tests/components/soma/test_config_flow.py | 16 +-- .../somfy_mylink/test_config_flow.py | 13 +- tests/components/sonarr/test_config_flow.py | 26 ++-- tests/components/songpal/test_config_flow.py | 22 +-- .../components/soundtouch/test_config_flow.py | 10 +- .../speedtestdotnet/test_config_flow.py | 14 +- tests/components/spider/test_config_flow.py | 13 +- tests/components/spotify/test_config_flow.py | 16 +-- tests/components/sql/test_config_flow.py | 82 +++++------ .../components/squeezebox/test_config_flow.py | 24 ++-- .../components/srp_energy/test_config_flow.py | 14 +- tests/components/starlink/test_config_flow.py | 11 +- .../steam_online/test_config_flow.py | 36 ++--- tests/components/steamist/test_config_flow.py | 46 +++---- .../components/stookalert/test_config_flow.py | 6 +- .../stookwijzer/test_config_flow.py | 4 +- .../streamlabswater/test_config_flow.py | 22 +-- .../components/suez_water/test_config_flow.py | 22 +-- tests/components/sun/test_config_flow.py | 8 +- tests/components/sunweg/test_config_flow.py | 25 ++-- .../surepetcare/test_config_flow.py | 12 +- .../test_config_flow.py | 8 +- .../switch_as_x/test_config_flow.py | 12 +- .../components/switchbee/test_config_flow.py | 10 +- .../components/switchbot/test_config_flow.py | 96 ++++++------- .../switchbot_cloud/test_config_flow.py | 6 +- .../switcher_kis/test_config_flow.py | 12 +- .../components/syncthing/test_config_flow.py | 13 +- tests/components/syncthru/test_config_flow.py | 15 +- .../synology_dsm/test_config_flow.py | 54 ++++---- .../system_bridge/test_config_flow.py | 61 ++++---- .../systemmonitor/test_config_flow.py | 28 ++-- tests/components/tado/test_config_flow.py | 24 ++-- .../components/tailscale/test_config_flow.py | 26 ++-- tests/components/tailwind/test_config_flow.py | 36 ++--- tests/components/tami4/test_config_flow.py | 24 ++-- .../tankerkoenig/test_config_flow.py | 32 ++--- tests/components/tautulli/test_config_flow.py | 30 ++-- tests/components/technove/test_config_flow.py | 28 ++-- tests/components/tedee/test_config_flow.py | 14 +- .../tellduslive/test_config_flow.py | 37 ++--- tests/components/template/test_config_flow.py | 38 ++--- .../tesla_wall_connector/test_config_flow.py | 10 +- .../components/teslemetry/test_config_flow.py | 8 +- tests/components/tessie/test_config_flow.py | 16 +-- .../thermobeacon/test_config_flow.py | 30 ++-- .../components/thermopro/test_config_flow.py | 30 ++-- tests/components/thread/test_config_flow.py | 18 +-- .../components/threshold/test_config_flow.py | 12 +- tests/components/tibber/test_config_flow.py | 8 +- tests/components/tile/test_config_flow.py | 16 +-- tests/components/tilt_ble/test_config_flow.py | 30 ++-- .../components/time_date/test_config_flow.py | 14 +- tests/components/tod/test_config_flow.py | 8 +- tests/components/todoist/test_config_flow.py | 12 +- tests/components/tolo/test_config_flow.py | 14 +- .../components/tomorrowio/test_config_flow.py | 24 ++-- tests/components/toon/test_config_flow.py | 20 +-- .../totalconnect/test_config_flow.py | 26 ++-- tests/components/tplink/test_config_flow.py | 8 +- .../tplink_omada/test_config_flow.py | 30 ++-- .../traccar_server/test_config_flow.py | 16 +-- tests/components/tradfri/test_config_flow.py | 21 +-- .../trafikverket_camera/test_config_flow.py | 22 +-- .../trafikverket_ferry/test_config_flow.py | 14 +- .../trafikverket_train/test_config_flow.py | 32 ++--- .../test_config_flow.py | 10 +- .../transmission/test_config_flow.py | 30 ++-- tests/components/trend/test_config_flow.py | 10 +- tests/components/tuya/test_config_flow.py | 34 ++--- .../twentemilieu/test_config_flow.py | 14 +- tests/components/twitch/test_config_flow.py | 18 +-- 164 files changed, 1919 insertions(+), 1890 deletions(-) diff --git a/tests/components/p1_monitor/test_config_flow.py b/tests/components/p1_monitor/test_config_flow.py index ec1af77a646..6f6c2c8f7ec 100644 --- a/tests/components/p1_monitor/test_config_flow.py +++ b/tests/components/p1_monitor/test_config_flow.py @@ -17,7 +17,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" with ( @@ -33,7 +33,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: user_input={CONF_HOST: "example.com"}, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "P1 Monitor" assert result2.get("data") == {CONF_HOST: "example.com"} @@ -53,5 +53,5 @@ async def test_api_error(hass: HomeAssistant) -> None: data={CONF_HOST: "example.com"}, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {"base": "cannot_connect"} diff --git a/tests/components/peco/test_config_flow.py b/tests/components/peco/test_config_flow.py index df178e125e1..112d160fa81 100644 --- a/tests/components/peco/test_config_flow.py +++ b/tests/components/peco/test_config_flow.py @@ -17,7 +17,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -33,7 +33,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Philadelphia Outage Count" assert result2["data"] == { "county": "PHILADELPHIA", @@ -46,7 +46,7 @@ async def test_invalid_county(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -70,7 +70,7 @@ async def test_meter_value_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -82,7 +82,7 @@ async def test_meter_value_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"phone_number": "invalid_phone_number"} @@ -92,7 +92,7 @@ async def test_incompatible_meter_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch("peco.PecoOutageApi.meter_check", side_effect=IncompatibleMeterError()): @@ -105,7 +105,7 @@ async def test_incompatible_meter_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "incompatible_meter" @@ -114,7 +114,7 @@ async def test_unresponsive_meter_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch("peco.PecoOutageApi.meter_check", side_effect=UnresponsiveMeterError()): @@ -127,7 +127,7 @@ async def test_unresponsive_meter_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"phone_number": "unresponsive_meter"} @@ -137,7 +137,7 @@ async def test_meter_http_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch("peco.PecoOutageApi.meter_check", side_effect=HttpError): @@ -150,7 +150,7 @@ async def test_meter_http_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"phone_number": "http_error"} @@ -160,7 +160,7 @@ async def test_smart_meter(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch("peco.PecoOutageApi.meter_check", return_value=True): @@ -173,7 +173,7 @@ async def test_smart_meter(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Philadelphia - 1234567890" assert result["data"]["phone_number"] == "1234567890" assert result["context"]["unique_id"] == "PHILADELPHIA-1234567890" diff --git a/tests/components/pegel_online/test_config_flow.py b/tests/components/pegel_online/test_config_flow.py index fedcba94616..45468917565 100644 --- a/tests/components/pegel_online/test_config_flow.py +++ b/tests/components/pegel_online/test_config_flow.py @@ -33,7 +33,7 @@ async def test_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -48,13 +48,13 @@ async def test_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA_STEP1 ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_station" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA_STEP2 ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_STATION] == "70272185-xxxx-xxxx-xxxx-43bea330dcae" assert result["title"] == "DRESDEN ELBE" @@ -75,7 +75,7 @@ async def test_user_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -85,13 +85,13 @@ async def test_user_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA_STEP1 ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_station" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA_STEP2 ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -100,7 +100,7 @@ async def test_connection_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -116,7 +116,7 @@ async def test_connection_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA_STEP1 ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "cannot_connect" @@ -125,13 +125,13 @@ async def test_connection_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA_STEP1 ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_station" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA_STEP2 ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_STATION] == "70272185-xxxx-xxxx-xxxx-43bea330dcae" assert result["title"] == "DRESDEN ELBE" @@ -145,7 +145,7 @@ async def test_user_no_stations(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -161,7 +161,7 @@ async def test_user_no_stations(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA_STEP1 ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"][CONF_RADIUS] == "no_stations" @@ -170,13 +170,13 @@ async def test_user_no_stations(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA_STEP1 ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_station" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA_STEP2 ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_STATION] == "70272185-xxxx-xxxx-xxxx-43bea330dcae" assert result["title"] == "DRESDEN ELBE" diff --git a/tests/components/permobil/test_config_flow.py b/tests/components/permobil/test_config_flow.py index 5968e247a95..ea39e678459 100644 --- a/tests/components/permobil/test_config_flow.py +++ b/tests/components/permobil/test_config_flow.py @@ -46,7 +46,7 @@ async def test_sucessful_config_flow(hass: HomeAssistant, my_permobil: Mock) -> data={CONF_EMAIL: MOCK_EMAIL}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "region" assert result["errors"] == {} @@ -56,7 +56,7 @@ async def test_sucessful_config_flow(hass: HomeAssistant, my_permobil: Mock) -> user_input={CONF_REGION: MOCK_REGION_NAME}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "email_code" assert result["errors"] == {} # request region code @@ -65,7 +65,7 @@ async def test_sucessful_config_flow(hass: HomeAssistant, my_permobil: Mock) -> user_input={CONF_CODE: MOCK_CODE}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == VALID_DATA @@ -89,7 +89,7 @@ async def test_config_flow_incorrect_code( data={CONF_EMAIL: MOCK_EMAIL}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "region" assert result["errors"] == {} @@ -99,7 +99,7 @@ async def test_config_flow_incorrect_code( user_input={CONF_REGION: MOCK_REGION_NAME}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "email_code" assert result["errors"] == {} @@ -109,7 +109,7 @@ async def test_config_flow_incorrect_code( result["flow_id"], user_input={CONF_CODE: MOCK_CODE}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "email_code" assert result["errors"]["base"] == "invalid_code" @@ -134,7 +134,7 @@ async def test_config_flow_unsigned_eula( data={CONF_EMAIL: MOCK_EMAIL}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "region" assert result["errors"] == {} @@ -144,7 +144,7 @@ async def test_config_flow_unsigned_eula( user_input={CONF_REGION: MOCK_REGION_NAME}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "email_code" assert result["errors"] == {} @@ -154,7 +154,7 @@ async def test_config_flow_unsigned_eula( result["flow_id"], user_input={CONF_CODE: MOCK_CODE}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "email_code" assert result["errors"]["base"] == "unsigned_eula" @@ -170,7 +170,7 @@ async def test_config_flow_unsigned_eula( ) # Now the method should not raise an exception, and you can proceed with your assertions - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == VALID_DATA @@ -195,7 +195,7 @@ async def test_config_flow_incorrect_region( data={CONF_EMAIL: MOCK_EMAIL}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "region" assert result["errors"] == {} @@ -206,7 +206,7 @@ async def test_config_flow_incorrect_region( user_input={CONF_REGION: MOCK_REGION_NAME}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "region" assert result["errors"]["base"] == "code_request_error" @@ -232,7 +232,7 @@ async def test_config_flow_region_request_error( data={CONF_EMAIL: MOCK_EMAIL}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "region" assert result["errors"]["base"] == "region_fetch_error" @@ -260,7 +260,7 @@ async def test_config_flow_invalid_email( data={CONF_EMAIL: INVALID_EMAIL}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER assert result["errors"]["base"] == "invalid_email" @@ -289,7 +289,7 @@ async def test_config_flow_reauth_success( context={"source": "reauth", "entry_id": mock_entry.entry_id}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "email_code" assert result["errors"] == {} @@ -299,7 +299,7 @@ async def test_config_flow_reauth_success( user_input={CONF_CODE: reauth_code}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_EMAIL: MOCK_EMAIL, CONF_REGION: MOCK_URL, @@ -331,7 +331,7 @@ async def test_config_flow_reauth_fail_invalid_code( context={"source": "reauth", "entry_id": mock_entry.entry_id}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "email_code" assert result["errors"] == {} @@ -341,7 +341,7 @@ async def test_config_flow_reauth_fail_invalid_code( user_input={CONF_CODE: reauth_invalid_code}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "email_code" assert result["errors"]["base"] == "invalid_code" @@ -368,5 +368,5 @@ async def test_config_flow_reauth_fail_code_request( context={"source": "reauth", "entry_id": reauth_entry.entry_id}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" diff --git a/tests/components/philips_js/test_config_flow.py b/tests/components/philips_js/test_config_flow.py index 07f1f2be933..fdf4825b116 100644 --- a/tests/components/philips_js/test_config_flow.py +++ b/tests/components/philips_js/test_config_flow.py @@ -5,9 +5,10 @@ from unittest.mock import ANY from haphilipsjs import PairingFailure import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.philips_js.const import CONF_ALLOW_NOTIFY, DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import ( MOCK_CONFIG, @@ -78,7 +79,7 @@ async def test_reauth( data=mock_config_entry.data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -88,7 +89,7 @@ async def test_reauth( ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert mock_config_entry.data == MOCK_CONFIG | {"system": mock_tv.system} assert len(mock_setup_entry.mock_calls) == 2 @@ -255,12 +256,12 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + 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_ALLOW_NOTIFY: True} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_ALLOW_NOTIFY: True} diff --git a/tests/components/pi_hole/test_config_flow.py b/tests/components/pi_hole/test_config_flow.py index 350b8b899d8..3b56305e0fc 100644 --- a/tests/components/pi_hole/test_config_flow.py +++ b/tests/components/pi_hole/test_config_flow.py @@ -32,7 +32,7 @@ async def test_flow_user_with_api_key(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -40,7 +40,7 @@ async def test_flow_user_with_api_key(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONFIG_FLOW_USER, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "api_key" assert result["errors"] == {} @@ -48,7 +48,7 @@ async def test_flow_user_with_api_key(hass: HomeAssistant) -> None: result["flow_id"], user_input={CONF_API_KEY: "some_key"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "api_key" assert result["errors"] == {CONF_API_KEY: "invalid_auth"} @@ -57,7 +57,7 @@ async def test_flow_user_with_api_key(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONFIG_FLOW_API_KEY, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"] == CONFIG_ENTRY_WITH_API_KEY mock_setup.assert_called_once() @@ -68,7 +68,7 @@ async def test_flow_user_with_api_key(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, data=CONFIG_FLOW_USER, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -80,7 +80,7 @@ async def test_flow_user_without_api_key(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -88,7 +88,7 @@ async def test_flow_user_without_api_key(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONFIG_FLOW_USER, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"] == CONFIG_ENTRY_WITHOUT_API_KEY mock_setup.assert_called_once() @@ -101,7 +101,7 @@ async def test_flow_user_invalid(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG_FLOW_USER ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -129,6 +129,6 @@ async def test_flow_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data[CONF_API_KEY] == "newkey" diff --git a/tests/components/picnic/test_config_flow.py b/tests/components/picnic/test_config_flow.py index ec103e9f3a3..694ca0df31f 100644 --- a/tests/components/picnic/test_config_flow.py +++ b/tests/components/picnic/test_config_flow.py @@ -6,10 +6,11 @@ import pytest from python_picnic_api.session import PicnicAuthError import requests -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.picnic.const import DOMAIN from homeassistant.const import CONF_ACCESS_TOKEN, CONF_COUNTRY_CODE from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -41,7 +42,7 @@ async def test_form(hass: HomeAssistant, picnic_api) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None @@ -87,7 +88,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -110,7 +111,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -133,7 +134,7 @@ async def test_form_exception(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -160,7 +161,7 @@ async def test_form_already_configured(hass: HomeAssistant, picnic_api) -> None: ) await hass.async_block_till_done() - assert result_configure["type"] == data_entry_flow.FlowResultType.ABORT + assert result_configure["type"] is FlowResultType.ABORT assert result_configure["reason"] == "already_configured" @@ -179,7 +180,7 @@ async def test_step_reauth(hass: HomeAssistant, picnic_api) -> None: result_init = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=conf ) - assert result_init["type"] == data_entry_flow.FlowResultType.FORM + assert result_init["type"] is FlowResultType.FORM assert result_init["step_id"] == "user" with patch( @@ -197,7 +198,7 @@ async def test_step_reauth(hass: HomeAssistant, picnic_api) -> None: await hass.async_block_till_done() # Check that the returned flow has type abort because of successful re-authentication - assert result_configure["type"] == data_entry_flow.FlowResultType.ABORT + assert result_configure["type"] is FlowResultType.ABORT assert result_configure["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 @@ -219,7 +220,7 @@ async def test_step_reauth_failed(hass: HomeAssistant) -> None: result_init = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=conf ) - assert result_init["type"] == data_entry_flow.FlowResultType.FORM + assert result_init["type"] is FlowResultType.FORM assert result_init["step_id"] == "user" with patch( @@ -258,7 +259,7 @@ async def test_step_reauth_different_account(hass: HomeAssistant, picnic_api) -> result_init = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=conf ) - assert result_init["type"] == data_entry_flow.FlowResultType.FORM + assert result_init["type"] is FlowResultType.FORM assert result_init["step_id"] == "user" with patch( diff --git a/tests/components/ping/test_config_flow.py b/tests/components/ping/test_config_flow.py index 541bdca8b1e..1f55957410d 100644 --- a/tests/components/ping/test_config_flow.py +++ b/tests/components/ping/test_config_flow.py @@ -27,7 +27,7 @@ async def test_form(hass: HomeAssistant, host, expected_title) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -37,7 +37,7 @@ async def test_form(hass: HomeAssistant, host, expected_title) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == expected_title assert result["data"] == {} assert result["options"] == { @@ -69,7 +69,7 @@ async def test_options(hass: HomeAssistant, host, count, expected_title) -> None 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["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -81,7 +81,7 @@ async def test_options(hass: HomeAssistant, host, count, expected_title) -> None ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "count": count, "host": "10.10.10.1", @@ -100,7 +100,7 @@ async def test_step_import(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "test2" assert result["data"] == {CONF_IMPORTED_BY: "binary_sensor"} assert result["options"] == { @@ -117,7 +117,7 @@ async def test_step_import(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + 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"] == { diff --git a/tests/components/plaato/test_config_flow.py b/tests/components/plaato/test_config_flow.py index 7088b672f69..f87b2db7fef 100644 --- a/tests/components/plaato/test_config_flow.py +++ b/tests/components/plaato/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import patch from pyplaato.models.device import PlaatoDeviceType import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.plaato.const import ( CONF_DEVICE_NAME, CONF_DEVICE_TYPE, @@ -62,7 +62,7 @@ async def test_show_config_form_device_type_airlock(hass: HomeAssistant) -> None }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "api_method" assert result["data_schema"].schema.get(CONF_TOKEN) == str assert result["data_schema"].schema.get(CONF_USE_WEBHOOK) == bool @@ -76,7 +76,7 @@ async def test_show_config_form_device_type_keg(hass: HomeAssistant) -> None: data={CONF_DEVICE_TYPE: PlaatoDeviceType.Keg, CONF_DEVICE_NAME: "device_name"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "api_method" assert result["data_schema"].schema.get(CONF_TOKEN) == str assert result["data_schema"].schema.get(CONF_USE_WEBHOOK) is None @@ -91,7 +91,7 @@ async def test_show_config_form_validate_webhook( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -102,7 +102,7 @@ async def test_show_config_form_validate_webhook( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "api_method" assert await async_setup_component(hass, "cloud", {}) @@ -126,7 +126,7 @@ async def test_show_config_form_validate_webhook( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "webhook" @@ -139,7 +139,7 @@ async def test_show_config_form_validate_webhook_not_connected( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -150,7 +150,7 @@ async def test_show_config_form_validate_webhook_not_connected( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "api_method" assert await async_setup_component(hass, "cloud", {}) @@ -174,7 +174,7 @@ async def test_show_config_form_validate_webhook_not_connected( }, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cloud_not_connected" @@ -193,7 +193,7 @@ async def test_show_config_form_validate_token(hass: HomeAssistant) -> None: }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "api_method" with patch("homeassistant.components.plaato.async_setup_entry", return_value=True): @@ -201,7 +201,7 @@ async def test_show_config_form_validate_token(hass: HomeAssistant) -> None: result["flow_id"], user_input={CONF_TOKEN: "valid_token"} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == PlaatoDeviceType.Keg.name assert result["data"] == { CONF_USE_WEBHOOK: False, @@ -228,7 +228,7 @@ async def test_show_config_form_no_cloud_webhook( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "api_method" result = await hass.config_entries.flow.async_configure( @@ -239,7 +239,7 @@ async def test_show_config_form_no_cloud_webhook( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "webhook" assert result["errors"] is None @@ -262,14 +262,14 @@ async def test_show_config_form_api_method_no_auth_token( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "api_method" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_TOKEN: ""} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "api_method" assert len(result["errors"]) == 1 assert result["errors"]["base"] == "no_auth_token" @@ -287,14 +287,14 @@ async def test_show_config_form_api_method_no_auth_token( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "api_method" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_TOKEN: ""} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "api_method" assert len(result["errors"]) == 1 assert result["errors"]["base"] == "no_api_method" @@ -318,7 +318,7 @@ async def test_options(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.options.async_configure( @@ -328,7 +328,7 @@ async def test_options(hass: HomeAssistant) -> None: await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_SCAN_INTERVAL] == 10 assert len(mock_setup_entry.mock_calls) == 1 @@ -352,7 +352,7 @@ async def test_options_webhook(hass: HomeAssistant, webhook_id) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "webhook" assert result["description_placeholders"] == {"webhook_url": ""} @@ -363,7 +363,7 @@ async def test_options_webhook(hass: HomeAssistant, webhook_id) -> None: await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_WEBHOOK_ID] == CONF_WEBHOOK_ID assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index 9cfcda1b29d..33e1b3637d8 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -56,7 +56,7 @@ async def test_bad_credentials( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -69,14 +69,14 @@ async def test_bad_credentials( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.EXTERNAL_STEP_DONE + assert result["type"] is FlowResultType.EXTERNAL_STEP_DONE result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"][CONF_TOKEN] == "faulty_credentials" @@ -88,7 +88,7 @@ async def test_bad_hostname( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -102,14 +102,14 @@ async def test_bad_hostname( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.EXTERNAL_STEP_DONE + assert result["type"] is FlowResultType.EXTERNAL_STEP_DONE result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"][CONF_HOST] == "not_found" @@ -121,7 +121,7 @@ async def test_unknown_exception( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -132,13 +132,13 @@ async def test_unknown_exception( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.EXTERNAL_STEP_DONE + assert result["type"] is FlowResultType.EXTERNAL_STEP_DONE result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -155,7 +155,7 @@ async def test_no_servers_found( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -165,13 +165,13 @@ async def test_no_servers_found( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.EXTERNAL_STEP_DONE + assert result["type"] is FlowResultType.EXTERNAL_STEP_DONE result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "no_servers" @@ -186,7 +186,7 @@ async def test_single_available_server( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -196,13 +196,13 @@ async def test_single_available_server( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.EXTERNAL_STEP_DONE + assert result["type"] is FlowResultType.EXTERNAL_STEP_DONE result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert ( result["title"] == "https://1-2-3-4.123456789001234567890.plex.direct:32400" @@ -230,7 +230,7 @@ async def test_multiple_servers_with_selection( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" requests_mock.get( @@ -244,13 +244,13 @@ async def test_multiple_servers_with_selection( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.EXTERNAL_STEP_DONE + assert result["type"] is FlowResultType.EXTERNAL_STEP_DONE result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_server" result = await hass.config_entries.flow.async_configure( @@ -259,7 +259,7 @@ async def test_multiple_servers_with_selection( CONF_SERVER_IDENTIFIER: MOCK_SERVERS[0][CONF_SERVER_IDENTIFIER] }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert ( result["title"] == "https://1-2-3-4.123456789001234567890.plex.direct:32400" @@ -295,7 +295,7 @@ async def test_adding_last_unconfigured_server( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" requests_mock.get( @@ -310,13 +310,13 @@ async def test_adding_last_unconfigured_server( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.EXTERNAL_STEP_DONE + assert result["type"] is FlowResultType.EXTERNAL_STEP_DONE result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert ( result["title"] == "https://1-2-3-4.123456789001234567890.plex.direct:32400" @@ -354,7 +354,7 @@ async def test_all_available_servers_configured( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" requests_mock.get("https://plex.tv/api/v2/user", text=plextv_account) @@ -370,13 +370,13 @@ async def test_all_available_servers_configured( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.EXTERNAL_STEP_DONE + assert result["type"] is FlowResultType.EXTERNAL_STEP_DONE result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "all_configured" @@ -388,7 +388,7 @@ async def test_option_flow(hass: HomeAssistant, entry, mock_plex_server) -> None result = await hass.config_entries.options.async_init( entry.entry_id, context={"source": "test"}, data=None ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "plex_mp_settings" result = await hass.config_entries.options.async_configure( @@ -399,7 +399,7 @@ async def test_option_flow(hass: HomeAssistant, entry, mock_plex_server) -> None CONF_MONITORED_USERS: list(mock_plex_server.accounts), }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { Platform.MEDIA_PLAYER: { CONF_USE_EPISODE_ART: True, @@ -422,7 +422,7 @@ async def test_missing_option_flow( result = await hass.config_entries.options.async_init( entry.entry_id, context={"source": "test"}, data=None ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "plex_mp_settings" result = await hass.config_entries.options.async_configure( @@ -433,7 +433,7 @@ async def test_missing_option_flow( CONF_MONITORED_USERS: list(mock_plex_server.accounts), }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { Platform.MEDIA_PLAYER: { CONF_USE_EPISODE_ART: True, @@ -470,7 +470,7 @@ async def test_option_flow_new_users_available( result = await hass.config_entries.options.async_init( entry.entry_id, context={"source": "test"}, data=None ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "plex_mp_settings" multiselect_defaults = result["data_schema"].schema["monitored_users"].options @@ -486,7 +486,7 @@ async def test_external_timed_out( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -496,13 +496,13 @@ async def test_external_timed_out( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.EXTERNAL_STEP_DONE + assert result["type"] is FlowResultType.EXTERNAL_STEP_DONE result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "token_request_timeout" @@ -515,7 +515,7 @@ async def test_callback_view( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -525,7 +525,7 @@ async def test_callback_view( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP client = await hass_client_no_auth() forward_url = f'{config_flow.AUTH_CALLBACK_PATH}?flow_id={result["flow_id"]}' @@ -552,7 +552,7 @@ async def test_manual_config( config_flow.DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["data_schema"] is None hass.config_entries.flow.async_abort(result["flow_id"]) @@ -564,7 +564,7 @@ async def test_manual_config( ) assert result["data_schema"] is not None - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user_advanced" with patch("plexauth.PlexAuth.initiate_auth"): @@ -572,7 +572,7 @@ async def test_manual_config( result["flow_id"], user_input={"setup_method": AUTOMATIC_SETUP_STRING} ) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP hass.config_entries.flow.async_abort(result["flow_id"]) # Advanced manual @@ -582,14 +582,14 @@ async def test_manual_config( ) assert result["data_schema"] is not None - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user_advanced" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"setup_method": MANUAL_SETUP_STRING} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual_setup" MANUAL_SERVER = { @@ -610,7 +610,7 @@ async def test_manual_config( result["flow_id"], user_input=MANUAL_SERVER_NO_HOST_OR_TOKEN ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual_setup" assert result["errors"]["base"] == "host_or_token" @@ -622,7 +622,7 @@ async def test_manual_config( result["flow_id"], user_input=MANUAL_SERVER ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual_setup" assert result["errors"]["base"] == "ssl_error" @@ -634,7 +634,7 @@ async def test_manual_config( result["flow_id"], user_input=MANUAL_SERVER ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual_setup" assert result["errors"]["base"] == "ssl_error" @@ -646,7 +646,7 @@ async def test_manual_config( result["flow_id"], user_input=MANUAL_SERVER ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual_setup" assert result["errors"]["base"] == "ssl_error" @@ -659,7 +659,7 @@ async def test_manual_config( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "http://1.2.3.4:32400" assert result["data"][CONF_SERVER] == "Plex Server 1" @@ -682,14 +682,14 @@ async def test_manual_config_with_token( context={"source": SOURCE_USER, "show_advanced_options": True}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user_advanced" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"setup_method": MANUAL_SETUP_STRING} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual_setup" with ( @@ -700,7 +700,7 @@ async def test_manual_config_with_token( result["flow_id"], user_input={CONF_TOKEN: MOCK_TOKEN} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY mock_url = "https://1-2-3-4.123456789001234567890.plex.direct:32400" @@ -761,13 +761,13 @@ async def test_reauth( patch("plexauth.PlexAuth.token", return_value="BRAND_NEW_TOKEN"), ): result = await hass.config_entries.flow.async_configure(flow_id, user_input={}) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.EXTERNAL_STEP_DONE + assert result["type"] is FlowResultType.EXTERNAL_STEP_DONE result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert result["flow_id"] == flow_id @@ -813,13 +813,13 @@ async def test_reauth_multiple_servers_available( patch("plexauth.PlexAuth.token", return_value="BRAND_NEW_TOKEN"), ): result = await hass.config_entries.flow.async_configure(flow_id, user_input={}) - assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.EXTERNAL_STEP_DONE + assert result["type"] is FlowResultType.EXTERNAL_STEP_DONE result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["flow_id"] == flow_id assert result["reason"] == "reauth_successful" @@ -840,7 +840,7 @@ async def test_client_request_missing(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -864,7 +864,7 @@ async def test_client_header_issues( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( diff --git a/tests/components/plugwise/test_config_flow.py b/tests/components/plugwise/test_config_flow.py index 6e2f4e63d85..4b7c567baa8 100644 --- a/tests/components/plugwise/test_config_flow.py +++ b/tests/components/plugwise/test_config_flow.py @@ -120,7 +120,7 @@ async def test_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {} assert result.get("step_id") == "user" @@ -133,7 +133,7 @@ async def test_form( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "Test Smile Name" assert result2.get("data") == { CONF_HOST: TEST_HOST, @@ -167,7 +167,7 @@ async def test_zeroconf_flow( context={CONF_SOURCE: SOURCE_ZEROCONF}, data=discovery, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {} assert result.get("step_id") == "user" @@ -177,7 +177,7 @@ async def test_zeroconf_flow( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "Test Smile Name" assert result2.get("data") == { CONF_HOST: TEST_HOST, @@ -202,7 +202,7 @@ async def test_zeroconf_flow_stretch( context={CONF_SOURCE: SOURCE_ZEROCONF}, data=TEST_DISCOVERY2, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {} assert result.get("step_id") == "user" @@ -212,7 +212,7 @@ async def test_zeroconf_flow_stretch( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "Test Smile Name" assert result2.get("data") == { CONF_HOST: TEST_HOST, @@ -253,7 +253,7 @@ async def test_zercoconf_discovery_update_configuration( context={CONF_SOURCE: SOURCE_ZEROCONF}, data=TEST_DISCOVERY, ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" assert entry.data[CONF_HOST] == "0.0.0.0" @@ -264,7 +264,7 @@ async def test_zercoconf_discovery_update_configuration( data=TEST_DISCOVERY, ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" assert entry.data[CONF_HOST] == "1.1.1.1" @@ -293,7 +293,7 @@ async def test_flow_errors( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {} assert result.get("step_id") == "user" @@ -303,7 +303,7 @@ async def test_flow_errors( user_input={CONF_HOST: TEST_HOST, CONF_PASSWORD: TEST_PASSWORD}, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("errors") == {"base": reason} assert result2.get("step_id") == "user" @@ -316,7 +316,7 @@ async def test_flow_errors( user_input={CONF_HOST: TEST_HOST, CONF_PASSWORD: TEST_PASSWORD}, ) - assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY assert result3.get("title") == "Test Smile Name" assert result3.get("data") == { CONF_HOST: TEST_HOST, @@ -341,7 +341,7 @@ async def test_zeroconf_abort_anna_with_existing_config_entries( context={CONF_SOURCE: SOURCE_ZEROCONF}, data=TEST_DISCOVERY_ANNA, ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "anna_with_adam" @@ -352,7 +352,7 @@ async def test_zeroconf_abort_anna_with_adam(hass: HomeAssistant) -> None: context={CONF_SOURCE: SOURCE_ZEROCONF}, data=TEST_DISCOVERY_ANNA, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" flows_in_progress = hass.config_entries.flow.async_progress() @@ -366,7 +366,7 @@ async def test_zeroconf_abort_anna_with_adam(hass: HomeAssistant) -> None: data=TEST_DISCOVERY_ADAM, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "user" flows_in_progress = hass.config_entries.flow.async_progress() @@ -379,7 +379,7 @@ async def test_zeroconf_abort_anna_with_adam(hass: HomeAssistant) -> None: context={CONF_SOURCE: SOURCE_ZEROCONF}, data=TEST_DISCOVERY_ANNA, ) - assert result3.get("type") == FlowResultType.ABORT + assert result3.get("type") is FlowResultType.ABORT assert result3.get("reason") == "anna_with_adam" # Adam should still be there diff --git a/tests/components/point/test_config_flow.py b/tests/components/point/test_config_flow.py index 67745251bf9..ec71b04b84b 100644 --- a/tests/components/point/test_config_flow.py +++ b/tests/components/point/test_config_flow.py @@ -4,10 +4,10 @@ from unittest.mock import AsyncMock, patch import pytest -from homeassistant import data_entry_flow from homeassistant.components.point import DOMAIN, config_flow from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType def init_config_flow(hass, side_effect=None): @@ -49,7 +49,7 @@ async def test_abort_if_no_implementation_registered(hass: HomeAssistant) -> Non flow.hass = hass result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_flows" @@ -59,12 +59,12 @@ async def test_abort_if_already_setup(hass: HomeAssistant) -> None: with patch.object(hass.config_entries, "async_entries", return_value=[{}]): result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_setup" with patch.object(hass.config_entries, "async_entries", return_value=[{}]): result = await flow.async_step_import() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_setup" @@ -74,18 +74,18 @@ async def test_full_flow_implementation(hass: HomeAssistant, mock_pypoint) -> No flow = init_config_flow(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await flow.async_step_user({"flow_impl": "test"}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["description_placeholders"] == { "authorization_url": "https://example.com" } result = await flow.async_step_code("123ABC") - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"]["refresh_args"] == { CONF_CLIENT_ID: "id", CONF_CLIENT_SECRET: "secret", @@ -99,7 +99,7 @@ async def test_step_import(hass: HomeAssistant, mock_pypoint) -> None: flow = init_config_flow(hass) result = await flow.async_step_import() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" @@ -111,7 +111,7 @@ async def test_wrong_code_flow_implementation( flow = init_config_flow(hass) result = await flow.async_step_code("123ABC") - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "auth_error" @@ -120,7 +120,7 @@ async def test_not_pick_implementation_if_only_one(hass: HomeAssistant) -> None: flow = init_config_flow(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" @@ -129,7 +129,7 @@ async def test_abort_if_timeout_generating_auth_url(hass: HomeAssistant) -> None flow = init_config_flow(hass, side_effect=TimeoutError) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "authorize_url_timeout" @@ -138,7 +138,7 @@ async def test_abort_if_exception_generating_auth_url(hass: HomeAssistant) -> No flow = init_config_flow(hass, side_effect=ValueError) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown_authorize_url_generation" @@ -147,5 +147,5 @@ async def test_abort_no_code(hass: HomeAssistant) -> None: flow = init_config_flow(hass) result = await flow.async_step_code() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_code" diff --git a/tests/components/poolsense/test_config_flow.py b/tests/components/poolsense/test_config_flow.py index 7a91e546a59..7fbb42ea106 100644 --- a/tests/components/poolsense/test_config_flow.py +++ b/tests/components/poolsense/test_config_flow.py @@ -2,11 +2,11 @@ from unittest.mock import patch -from homeassistant import data_entry_flow from homeassistant.components.poolsense.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_show_form(hass: HomeAssistant) -> None: @@ -15,7 +15,7 @@ async def test_show_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -50,7 +50,7 @@ async def test_valid_credentials(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "test-email" assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/powerwall/test_config_flow.py b/tests/components/powerwall/test_config_flow.py index 83156ffb170..207c9b32d2f 100644 --- a/tests/components/powerwall/test_config_flow.py +++ b/tests/components/powerwall/test_config_flow.py @@ -321,7 +321,7 @@ async def test_dhcp_discovery_cannot_connect(hass: HomeAssistant) -> None: hostname="00GGX", ), ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -399,7 +399,7 @@ async def test_dhcp_discovery_update_ip_address(hass: HomeAssistant) -> None: ), ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_IP_ADDRESS] == "1.1.1.1" @@ -436,7 +436,7 @@ async def test_dhcp_discovery_does_not_update_ip_when_auth_fails( ), ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_IP_ADDRESS] == "1.2.3.4" @@ -473,7 +473,7 @@ async def test_dhcp_discovery_does_not_update_ip_when_auth_successful( ), ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_IP_ADDRESS] == "1.2.3.4" @@ -508,7 +508,7 @@ async def test_dhcp_discovery_updates_unique_id(hass: HomeAssistant) -> None: ), ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_IP_ADDRESS] == "1.2.3.4" assert entry.unique_id == MOCK_GATEWAY_DIN @@ -547,7 +547,7 @@ async def test_dhcp_discovery_updates_unique_id_when_entry_is_failed( ), ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_IP_ADDRESS] == "1.2.3.4" assert entry.unique_id == MOCK_GATEWAY_DIN @@ -586,7 +586,7 @@ async def test_discovered_wifi_does_not_update_ip_if_is_still_online( ), ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_IP_ADDRESS] == "1.2.3.4" @@ -635,6 +635,6 @@ async def test_discovered_wifi_does_not_update_ip_online_but_access_denied( ), ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_IP_ADDRESS] == "1.2.3.4" diff --git a/tests/components/private_ble_device/test_config_flow.py b/tests/components/private_ble_device/test_config_flow.py index 2acb89240a1..64a3c9c1d2e 100644 --- a/tests/components/private_ble_device/test_config_flow.py +++ b/tests/components/private_ble_device/test_config_flow.py @@ -26,7 +26,7 @@ async def test_setup_user_no_bluetooth( const.DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "bluetooth_not_available" @@ -114,7 +114,7 @@ async def test_flow_works(hass: HomeAssistant, enable_bluetooth: None) -> None: user_input={"irk": "irk:00000000000000000000000000000000"}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Test Test Test" assert result["data"] == {"irk": "00000000000000000000000000000000"} assert result["result"].unique_id == "00000000000000000000000000000000" @@ -153,7 +153,7 @@ async def test_flow_works_by_base64( user_input={"irk": "AAAAAAAAAAAAAAAAAAAAAA=="}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Test Test Test" assert result["data"] == {"irk": "00000000000000000000000000000000"} assert result["result"].unique_id == "00000000000000000000000000000000" diff --git a/tests/components/progettihwsw/test_config_flow.py b/tests/components/progettihwsw/test_config_flow.py index 7774adb5208..8dcc6917346 100644 --- a/tests/components/progettihwsw/test_config_flow.py +++ b/tests/components/progettihwsw/test_config_flow.py @@ -24,7 +24,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -41,7 +41,7 @@ async def test_form(hass: HomeAssistant) -> None: {CONF_HOST: "", CONF_PORT: 80}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "relay_modes" assert result2["errors"] == {} @@ -54,7 +54,7 @@ async def test_form(hass: HomeAssistant) -> None: mock_value_step_rm, ) - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["data"] assert result3["data"]["title"] == "1R & 1IN Board" assert result3["data"]["is_old"] is False @@ -78,7 +78,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: {CONF_HOST: "", CONF_PORT: 80}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -105,7 +105,7 @@ async def test_form_existing_entry_exception(hass: HomeAssistant) -> None: {CONF_HOST: "", CONF_PORT: 80}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -126,6 +126,6 @@ async def test_form_user_exception(hass: HomeAssistant) -> None: {CONF_HOST: "", CONF_PORT: 80}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/prosegur/test_config_flow.py b/tests/components/prosegur/test_config_flow.py index 3c3c2468696..cd44c899824 100644 --- a/tests/components/prosegur/test_config_flow.py +++ b/tests/components/prosegur/test_config_flow.py @@ -153,7 +153,7 @@ async def test_reauth_flow(hass: HomeAssistant, mock_list_contracts) -> None: data=entry.data, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -175,7 +175,7 @@ async def test_reauth_flow(hass: HomeAssistant, mock_list_contracts) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == { "country": "PT", @@ -231,5 +231,5 @@ async def test_reauth_flow_error(hass: HomeAssistant, exception, base_error) -> ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"]["base"] == base_error diff --git a/tests/components/proximity/test_config_flow.py b/tests/components/proximity/test_config_flow.py index 1841c10873c..3ed9f5cba27 100644 --- a/tests/components/proximity/test_config_flow.py +++ b/tests/components/proximity/test_config_flow.py @@ -56,7 +56,7 @@ async def test_user_flow( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -66,7 +66,7 @@ async def test_user_flow( result["flow_id"], user_input=user_input, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == expected_result zone = hass.states.get(user_input[CONF_ZONE]) @@ -101,7 +101,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: assert mock_setup_entry.called result = await hass.config_entries.options.async_init(mock_config.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -111,7 +111,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: CONF_TOLERANCE: 1, }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert mock_config.data == { CONF_ZONE: "zone.home", CONF_TRACKED_ENTITIES: ["device_tracker.test2"], @@ -138,7 +138,7 @@ async def test_import_flow(hass: HomeAssistant) -> None: }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_NAME: "home", CONF_ZONE: "zone.home", @@ -182,7 +182,7 @@ async def test_abort_duplicated_entry(hass: HomeAssistant) -> None: result["flow_id"], user_input=DATA, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" await hass.async_block_till_done() @@ -229,7 +229,7 @@ async def test_avoid_duplicated_title(hass: HomeAssistant) -> None: CONF_TOLERANCE: 10, }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "home 2" await hass.async_block_till_done() @@ -246,7 +246,7 @@ async def test_avoid_duplicated_title(hass: HomeAssistant) -> None: CONF_TOLERANCE: 10, }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "home 4" await hass.async_block_till_done() diff --git a/tests/components/prusalink/test_config_flow.py b/tests/components/prusalink/test_config_flow.py index e7db5b54dac..cc66d25b35d 100644 --- a/tests/components/prusalink/test_config_flow.py +++ b/tests/components/prusalink/test_config_flow.py @@ -14,7 +14,7 @@ async def test_form(hass: HomeAssistant, mock_version_api) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -31,7 +31,7 @@ async def test_form(hass: HomeAssistant, mock_version_api) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "PrusaXL" assert result2["data"] == { "host": "http://1.1.1.1", @@ -65,7 +65,7 @@ async def test_form_mk3(hass: HomeAssistant, mock_version_api) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert len(mock_setup_entry.mock_calls) == 1 @@ -88,7 +88,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -111,7 +111,7 @@ async def test_form_unknown(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -132,7 +132,7 @@ async def test_form_too_low_version(hass: HomeAssistant, mock_version_api) -> No }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "not_supported"} @@ -153,7 +153,7 @@ async def test_form_invalid_version_2(hass: HomeAssistant, mock_version_api) -> }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "not_supported"} @@ -178,7 +178,7 @@ async def test_form_invalid_mk3_server_version( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "not_supported"} @@ -201,5 +201,5 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/ps4/test_config_flow.py b/tests/components/ps4/test_config_flow.py index db478903d1e..4e0505a8644 100644 --- a/tests/components/ps4/test_config_flow.py +++ b/tests/components/ps4/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import patch from pyps4_2ndscreen.errors import CredentialTimeout import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import ps4 from homeassistant.components.ps4.config_flow import LOCAL_UDP_PORT from homeassistant.components.ps4.const import ( @@ -23,6 +23,7 @@ from homeassistant.const import ( CONF_TOKEN, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.util import location from tests.common import MockConfigEntry @@ -105,7 +106,7 @@ async def test_full_flow_implementation(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "creds" # Step Creds results with form in Step Mode. @@ -113,7 +114,7 @@ async def test_full_flow_implementation(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "mode" # Step Mode with User Input which is not manual, results in Step Link. @@ -123,7 +124,7 @@ async def test_full_flow_implementation(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_AUTO ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" # User Input results in created entry. @@ -136,7 +137,7 @@ async def test_full_flow_implementation(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_TOKEN] == MOCK_CREDS assert result["data"]["devices"] == [MOCK_DEVICE] assert result["title"] == MOCK_TITLE @@ -149,7 +150,7 @@ async def test_multiple_flow_implementation(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "creds" # Step Creds results with form in Step Mode. @@ -157,7 +158,7 @@ async def test_multiple_flow_implementation(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "mode" # Step Mode with User Input which is not manual, results in Step Link. @@ -168,7 +169,7 @@ async def test_multiple_flow_implementation(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_AUTO ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" # User Input results in created entry. @@ -182,7 +183,7 @@ async def test_multiple_flow_implementation(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_TOKEN] == MOCK_CREDS assert result["data"]["devices"] == [MOCK_DEVICE] assert result["title"] == MOCK_TITLE @@ -207,7 +208,7 @@ async def test_multiple_flow_implementation(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "creds" # Step Creds results with form in Step Mode. @@ -215,7 +216,7 @@ async def test_multiple_flow_implementation(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "mode" # Step Mode with User Input which is not manual, results in Step Link. @@ -226,7 +227,7 @@ async def test_multiple_flow_implementation(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_AUTO ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" # Step Link @@ -240,7 +241,7 @@ async def test_multiple_flow_implementation(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_CONFIG_ADDITIONAL ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_TOKEN] == MOCK_CREDS assert len(result["data"]["devices"]) == 1 assert result["title"] == MOCK_TITLE @@ -263,7 +264,7 @@ async def test_port_bind_abort(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == reason with patch("pyps4_2ndscreen.Helper.port_bind", return_value=MOCK_TCP_PORT): @@ -271,7 +272,7 @@ async def test_port_bind_abort(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == reason @@ -283,14 +284,14 @@ async def test_duplicate_abort(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "mode" with patch( @@ -299,7 +300,7 @@ async def test_duplicate_abort(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_AUTO ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -313,14 +314,14 @@ async def test_additional_device(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "mode" with patch( @@ -336,7 +337,7 @@ async def test_additional_device(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_CONFIG_ADDITIONAL ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_TOKEN] == MOCK_CREDS assert len(result["data"]["devices"]) == 1 assert result["title"] == MOCK_TITLE @@ -350,7 +351,7 @@ async def test_0_pin(hass: HomeAssistant) -> None: context={"source": "creds"}, data={}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "mode" with ( @@ -365,7 +366,7 @@ async def test_0_pin(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_AUTO ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" mock_config = MOCK_CONFIG @@ -390,14 +391,14 @@ async def test_no_devices_found_abort(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "mode" with patch("pyps4_2ndscreen.Helper.has_devices", return_value=[]): @@ -405,7 +406,7 @@ async def test_no_devices_found_abort(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_AUTO ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -415,14 +416,14 @@ async def test_manual_mode(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "mode" # Step Mode with User Input: manual, results in Step Link. @@ -433,7 +434,7 @@ async def test_manual_mode(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_MANUAL ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" @@ -443,7 +444,7 @@ async def test_credential_abort(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", return_value=None): @@ -451,7 +452,7 @@ async def test_credential_abort(hass: HomeAssistant) -> None: result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "credential_error" @@ -461,7 +462,7 @@ async def test_credential_timeout(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", side_effect=CredentialTimeout): @@ -469,7 +470,7 @@ async def test_credential_timeout(hass: HomeAssistant) -> None: result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "creds" assert result["errors"] == {"base": "credential_timeout"} @@ -480,14 +481,14 @@ async def test_wrong_pin_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "mode" with patch( @@ -501,7 +502,7 @@ async def test_wrong_pin_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {"base": "login_failed"} @@ -512,14 +513,14 @@ async def test_device_connection_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "mode" with patch( @@ -533,7 +534,7 @@ async def test_device_connection_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {"base": "cannot_connect"} @@ -544,20 +545,20 @@ async def test_manual_mode_no_ip_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "mode" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"Config Mode": "Manual Entry"} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "mode" assert result["errors"] == {CONF_IP_ADDRESS: "no_ipaddress"} diff --git a/tests/components/pure_energie/test_config_flow.py b/tests/components/pure_energie/test_config_flow.py index 596853800aa..4305dab2236 100644 --- a/tests/components/pure_energie/test_config_flow.py +++ b/tests/components/pure_energie/test_config_flow.py @@ -25,14 +25,14 @@ async def test_full_user_flow_implementation( ) assert result.get("step_id") == "user" - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "192.168.1.123"} ) assert result.get("title") == "Pure Energie Meter" - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert "data" in result assert result["data"][CONF_HOST] == "192.168.1.123" assert "result" in result @@ -64,14 +64,14 @@ async def test_full_zeroconf_flow_implementationn( CONF_NAME: "Pure Energie Meter", } assert result.get("step_id") == "zeroconf_confirm" - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) assert result2.get("title") == "Pure Energie Meter" - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert "data" in result2 assert result2["data"][CONF_HOST] == "192.168.1.123" @@ -90,7 +90,7 @@ async def test_connection_error( data={CONF_HOST: "example.com"}, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" assert result.get("errors") == {"base": "cannot_connect"} @@ -115,5 +115,5 @@ async def test_zeroconf_connection_error( ), ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "cannot_connect" diff --git a/tests/components/purpleair/test_config_flow.py b/tests/components/purpleair/test_config_flow.py index efd0db6fd37..fbfc20fc632 100644 --- a/tests/components/purpleair/test_config_flow.py +++ b/tests/components/purpleair/test_config_flow.py @@ -5,10 +5,10 @@ from unittest.mock import AsyncMock, patch from aiopurpleair.errors import InvalidApiKeyError, PurpleAirError import pytest -from homeassistant import data_entry_flow from homeassistant.components.purpleair import DOMAIN 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 device_registry as dr from .conftest import TEST_API_KEY, TEST_SENSOR_INDEX1, TEST_SENSOR_INDEX2 @@ -46,7 +46,7 @@ async def test_create_entry_by_coordinates( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # Test errors that can arise when checking the API key: @@ -54,13 +54,13 @@ async def test_create_entry_by_coordinates( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"api_key": TEST_API_KEY} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == check_api_key_errors result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"api_key": TEST_API_KEY} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "by_coordinates" # Test errors that can arise when searching for nearby sensors: @@ -73,7 +73,7 @@ async def test_create_entry_by_coordinates( "distance": 5, }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == get_nearby_sensors_errors result = await hass.config_entries.flow.async_configure( @@ -84,7 +84,7 @@ async def test_create_entry_by_coordinates( "distance": 5, }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "choose_sensor" result = await hass.config_entries.flow.async_configure( @@ -93,7 +93,7 @@ async def test_create_entry_by_coordinates( "sensor_index": str(TEST_SENSOR_INDEX1), }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "abcde" assert result["data"] == { "api_key": TEST_API_KEY, @@ -110,7 +110,7 @@ async def test_duplicate_error( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={"api_key": TEST_API_KEY} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -140,7 +140,7 @@ async def test_reauth( }, data={"api_key": TEST_API_KEY}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" # Test errors that can arise when checking the API key: @@ -148,14 +148,14 @@ async def test_reauth( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"api_key": "new_api_key"} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == check_api_key_errors result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"api_key": "new_api_key"}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 # Unload to make sure the update does not run after the @@ -181,13 +181,13 @@ async def test_options_add_sensor( ) -> None: """Test adding a sensor via the options flow (including errors).""" result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"next_step_id": "add_sensor"} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "add_sensor" # Test errors that can arise when searching for nearby sensors: @@ -202,7 +202,7 @@ async def test_options_add_sensor( "distance": 5, }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "add_sensor" result = await hass.config_entries.options.async_configure( @@ -213,7 +213,7 @@ async def test_options_add_sensor( "distance": 5, }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "choose_sensor" result = await hass.config_entries.options.async_configure( @@ -222,7 +222,7 @@ async def test_options_add_sensor( "sensor_index": str(TEST_SENSOR_INDEX2), }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "sensor_indices": [TEST_SENSOR_INDEX1, TEST_SENSOR_INDEX2], } @@ -241,13 +241,13 @@ async def test_options_add_sensor_duplicate( ) -> None: """Test adding a duplicate sensor via the options flow.""" result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"next_step_id": "add_sensor"} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "add_sensor" result = await hass.config_entries.options.async_configure( @@ -258,7 +258,7 @@ async def test_options_add_sensor_duplicate( "distance": 5, }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "choose_sensor" result = await hass.config_entries.options.async_configure( @@ -267,7 +267,7 @@ async def test_options_add_sensor_duplicate( "sensor_index": str(TEST_SENSOR_INDEX1), }, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" # Unload to make sure the update does not run after the # mock is removed. @@ -279,13 +279,13 @@ async def test_options_remove_sensor( ) -> None: """Test removing a sensor via the options flow.""" result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"next_step_id": "remove_sensor"} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "remove_sensor" device_registry = dr.async_get(hass) @@ -296,7 +296,7 @@ async def test_options_remove_sensor( result["flow_id"], user_input={"sensor_device_id": device_entry.id}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "sensor_indices": [], } @@ -312,19 +312,19 @@ async def test_options_settings( ) -> None: """Test setting settings via the options flow.""" result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"next_step_id": "settings"} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "settings" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"show_on_map": True} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "sensor_indices": [TEST_SENSOR_INDEX1], "show_on_map": True, diff --git a/tests/components/pushbullet/test_config_flow.py b/tests/components/pushbullet/test_config_flow.py index 01c946286b4..0b2efa1d556 100644 --- a/tests/components/pushbullet/test_config_flow.py +++ b/tests/components/pushbullet/test_config_flow.py @@ -35,7 +35,7 @@ async def test_flow_user(hass: HomeAssistant, requests_mock_fixture) -> None: result["flow_id"], user_input=MOCK_CONFIG, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "pushbullet" assert result["data"] == MOCK_CONFIG @@ -60,7 +60,7 @@ async def test_flow_user_already_configured( result["flow_id"], user_input=MOCK_CONFIG, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -85,7 +85,7 @@ async def test_flow_name_already_configured(hass: HomeAssistant) -> None: result["flow_id"], user_input=new_config, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -101,7 +101,7 @@ async def test_flow_invalid_key(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, data=MOCK_CONFIG, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {CONF_API_KEY: "invalid_api_key"} @@ -118,6 +118,6 @@ async def test_flow_conn_error(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, data=MOCK_CONFIG, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/pushover/test_config_flow.py b/tests/components/pushover/test_config_flow.py index fcaedf2b5a6..9b92033414c 100644 --- a/tests/components/pushover/test_config_flow.py +++ b/tests/components/pushover/test_config_flow.py @@ -44,7 +44,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_CONFIG, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Pushover" assert result["data"] == MOCK_CONFIG @@ -66,7 +66,7 @@ async def test_flow_user_key_api_key_exists(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_CONFIG, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -91,7 +91,7 @@ async def test_flow_name_already_configured(hass: HomeAssistant) -> None: result["flow_id"], user_input=new_config, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -106,7 +106,7 @@ async def test_flow_invalid_user_key( context={"source": config_entries.SOURCE_USER}, data=MOCK_CONFIG, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {CONF_USER_KEY: "invalid_user_key"} @@ -122,7 +122,7 @@ async def test_flow_invalid_api_key( context={"source": config_entries.SOURCE_USER}, data=MOCK_CONFIG, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {CONF_API_KEY: "invalid_api_key"} @@ -136,7 +136,7 @@ async def test_flow_conn_err(hass: HomeAssistant, mock_pushover: MagicMock) -> N context={"source": config_entries.SOURCE_USER}, data=MOCK_CONFIG, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -158,7 +158,7 @@ async def test_reauth_success(hass: HomeAssistant) -> None: data=MOCK_CONFIG, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result2 = await hass.config_entries.flow.async_configure( @@ -168,7 +168,7 @@ async def test_reauth_success(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -200,7 +200,7 @@ async def test_reauth_failed(hass: HomeAssistant, mock_pushover: MagicMock) -> N }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == { CONF_API_KEY: "invalid_api_key", } @@ -232,7 +232,7 @@ async def test_reauth_with_existing_config(hass: HomeAssistant) -> None: data=MOCK_CONFIG, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result2 = await hass.config_entries.flow.async_configure( @@ -242,5 +242,5 @@ async def test_reauth_with_existing_config(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/pvoutput/test_config_flow.py b/tests/components/pvoutput/test_config_flow.py index 1839a7f51e0..20e99f8e497 100644 --- a/tests/components/pvoutput/test_config_flow.py +++ b/tests/components/pvoutput/test_config_flow.py @@ -23,7 +23,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( @@ -34,7 +34,7 @@ async def test_full_user_flow( }, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "12345" assert result2.get("data") == { CONF_SYSTEM_ID: 12345, @@ -59,7 +59,7 @@ async def test_full_flow_with_authentication_error( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" mock_pvoutput.system.side_effect = PVOutputAuthenticationError @@ -71,7 +71,7 @@ async def test_full_flow_with_authentication_error( }, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "user" assert result2.get("errors") == {"base": "invalid_auth"} @@ -87,7 +87,7 @@ async def test_full_flow_with_authentication_error( }, ) - assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY assert result3.get("title") == "12345" assert result3.get("data") == { CONF_SYSTEM_ID: 12345, @@ -111,7 +111,7 @@ async def test_connection_error(hass: HomeAssistant, mock_pvoutput: MagicMock) - }, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {"base": "cannot_connect"} assert len(mock_pvoutput.system.mock_calls) == 1 @@ -137,7 +137,7 @@ async def test_already_configured( }, ) - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "already_configured" @@ -159,7 +159,7 @@ async def test_reauth_flow( }, data=mock_config_entry.data, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" result2 = await hass.config_entries.flow.async_configure( @@ -168,7 +168,7 @@ async def test_reauth_flow( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "reauth_successful" assert mock_config_entry.data == { CONF_SYSTEM_ID: 12345, @@ -201,7 +201,7 @@ async def test_reauth_with_authentication_error( }, data=mock_config_entry.data, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" mock_pvoutput.system.side_effect = PVOutputAuthenticationError @@ -211,7 +211,7 @@ async def test_reauth_with_authentication_error( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "reauth_confirm" assert result2.get("errors") == {"base": "invalid_auth"} @@ -225,7 +225,7 @@ async def test_reauth_with_authentication_error( ) await hass.async_block_till_done() - assert result3.get("type") == FlowResultType.ABORT + assert result3.get("type") is FlowResultType.ABORT assert result3.get("reason") == "reauth_successful" assert mock_config_entry.data == { CONF_SYSTEM_ID: 12345, @@ -253,7 +253,7 @@ async def test_reauth_api_error( }, data=mock_config_entry.data, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" mock_pvoutput.system.side_effect = PVOutputConnectionError @@ -263,6 +263,6 @@ async def test_reauth_api_error( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "reauth_confirm" assert result2.get("errors") == {"base": "cannot_connect"} diff --git a/tests/components/pvpc_hourly_pricing/test_config_flow.py b/tests/components/pvpc_hourly_pricing/test_config_flow.py index 2a4c5688b5f..70e25392bb6 100644 --- a/tests/components/pvpc_hourly_pricing/test_config_flow.py +++ b/tests/components/pvpc_hourly_pricing/test_config_flow.py @@ -4,7 +4,7 @@ from datetime import datetime, timedelta from freezegun.api import FrozenDateTimeFactory -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.pvpc_hourly_pricing.const import ( ATTR_POWER, ATTR_POWER_P3, @@ -15,6 +15,7 @@ from homeassistant.components.pvpc_hourly_pricing.const import ( ) from homeassistant.const import CONF_API_TOKEN, CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import entity_registry as er from homeassistant.util import dt as dt_util @@ -53,12 +54,12 @@ async def test_config_flow( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], tst_config ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() state = hass.states.get("sensor.esios_pvpc") @@ -73,11 +74,11 @@ async def test_config_flow( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], tst_config ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert pvpc_aioclient_mock.call_count == 1 # Check removal @@ -89,12 +90,12 @@ async def test_config_flow( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], tst_config ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() state = hass.states.get("sensor.esios_pvpc") @@ -110,21 +111,21 @@ async def test_config_flow( config_entry = current_entries[0] result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ATTR_POWER: 3.0, ATTR_POWER_P3: 4.6, CONF_USE_API_TOKEN: True}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "api_token" assert pvpc_aioclient_mock.call_count == 2 result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_API_TOKEN: "test-token"} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert pvpc_aioclient_mock.call_count == 2 await hass.async_block_till_done() state = hass.states.get("sensor.esios_pvpc") @@ -154,14 +155,14 @@ async def test_config_flow( # disable api token in options result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ATTR_POWER: 3.0, ATTR_POWER_P3: 4.6, CONF_USE_API_TOKEN: False}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert pvpc_aioclient_mock.call_count == 6 await hass.async_block_till_done() assert pvpc_aioclient_mock.call_count == 7 @@ -195,19 +196,19 @@ async def test_reauth( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], tst_config ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "api_token" assert pvpc_aioclient_mock.call_count == 0 result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_API_TOKEN: "test-token"} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "api_token" assert result["errors"]["base"] == "invalid_auth" assert pvpc_aioclient_mock.call_count == 1 @@ -216,7 +217,7 @@ async def test_reauth( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_API_TOKEN: "test-token"} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY config_entry = result["result"] assert pvpc_aioclient_mock.call_count == 4 @@ -234,7 +235,7 @@ async def test_reauth( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_API_TOKEN: "test-token"} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert pvpc_aioclient_mock.call_count == 7 @@ -248,7 +249,7 @@ async def test_reauth( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_API_TOKEN: "test-token"} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert pvpc_aioclient_mock.call_count == 8 diff --git a/tests/components/qbittorrent/test_config_flow.py b/tests/components/qbittorrent/test_config_flow.py index 8a424f5c87b..c52762f24d3 100644 --- a/tests/components/qbittorrent/test_config_flow.py +++ b/tests/components/qbittorrent/test_config_flow.py @@ -40,7 +40,7 @@ async def test_flow_user(hass: HomeAssistant, mock_api: requests_mock.Mocker) -> result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # Test flow with connection failure, fail with cannot_connect @@ -53,7 +53,7 @@ async def test_flow_user(hass: HomeAssistant, mock_api: requests_mock.Mocker) -> result["flow_id"], USER_INPUT ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -69,7 +69,7 @@ async def test_flow_user(hass: HomeAssistant, mock_api: requests_mock.Mocker) -> result["flow_id"], USER_INPUT ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -78,7 +78,7 @@ async def test_flow_user(hass: HomeAssistant, mock_api: requests_mock.Mocker) -> result["flow_id"], USER_INPUT ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_URL: "http://localhost:8080", CONF_USERNAME: "user", @@ -96,12 +96,12 @@ async def test_flow_user_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # Test flow with duplicate config result = await hass.config_entries.flow.async_configure( result["flow_id"], USER_INPUT ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/qingping/test_config_flow.py b/tests/components/qingping/test_config_flow.py index c5b5dd94cc2..7bcd9c09e68 100644 --- a/tests/components/qingping/test_config_flow.py +++ b/tests/components/qingping/test_config_flow.py @@ -23,7 +23,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=LIGHT_AND_SIGNAL_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( "homeassistant.components.qingping.async_setup_entry", return_value=True @@ -31,7 +31,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Motion & Light EEFF" assert result2["data"] == {} assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" @@ -50,7 +50,7 @@ async def test_async_step_bluetooth_not_enough_info_at_start( context={"source": config_entries.SOURCE_BLUETOOTH}, data=NO_DATA_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( "homeassistant.components.qingping.async_setup_entry", return_value=True @@ -58,7 +58,7 @@ async def test_async_step_bluetooth_not_enough_info_at_start( result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Qingping Motion & Light" assert result2["data"] == {} assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" @@ -75,7 +75,7 @@ async def test_async_step_bluetooth_not_qingping(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_QINGPING_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -85,7 +85,7 @@ async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -99,7 +99,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( "homeassistant.components.qingping.async_setup_entry", return_value=True @@ -108,7 +108,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Motion & Light EEFF" assert result2["data"] == {} assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" @@ -124,7 +124,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -140,7 +140,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -162,7 +162,7 @@ async def test_async_step_user_with_found_devices_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -179,7 +179,7 @@ async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant) - context={"source": config_entries.SOURCE_BLUETOOTH}, data=LIGHT_AND_SIGNAL_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -190,7 +190,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=LIGHT_AND_SIGNAL_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -198,7 +198,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=LIGHT_AND_SIGNAL_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -211,7 +211,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=LIGHT_AND_SIGNAL_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -222,7 +222,7 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.qingping.async_setup_entry", return_value=True @@ -231,7 +231,7 @@ async def test_async_step_user_takes_precedence_over_discovery( result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Motion & Light EEFF" assert result2["data"] == {} assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" diff --git a/tests/components/qnap/test_config_flow.py b/tests/components/qnap/test_config_flow.py index 881086b9e10..57ac67525e2 100644 --- a/tests/components/qnap/test_config_flow.py +++ b/tests/components/qnap/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock import pytest from requests.exceptions import ConnectTimeout -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.qnap import const from homeassistant.const import ( CONF_HOST, @@ -16,6 +16,7 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .conftest import TEST_HOST, TEST_PASSWORD, TEST_USERNAME @@ -35,7 +36,7 @@ async def test_config_flow(hass: HomeAssistant, qnap_connect: MagicMock) -> None const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -45,7 +46,7 @@ async def test_config_flow(hass: HomeAssistant, qnap_connect: MagicMock) -> None STANDARD_CONFIG, ) - assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -55,7 +56,7 @@ async def test_config_flow(hass: HomeAssistant, qnap_connect: MagicMock) -> None STANDARD_CONFIG, ) - assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -65,7 +66,7 @@ async def test_config_flow(hass: HomeAssistant, qnap_connect: MagicMock) -> None STANDARD_CONFIG, ) - assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} @@ -75,7 +76,7 @@ async def test_config_flow(hass: HomeAssistant, qnap_connect: MagicMock) -> None STANDARD_CONFIG, ) - assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Test NAS name" assert result["data"] == { CONF_HOST: "1.2.3.4", diff --git a/tests/components/qnap_qsw/test_config_flow.py b/tests/components/qnap_qsw/test_config_flow.py index 26a6581b207..334f74ccf4b 100644 --- a/tests/components/qnap_qsw/test_config_flow.py +++ b/tests/components/qnap_qsw/test_config_flow.py @@ -5,12 +5,13 @@ from unittest.mock import MagicMock, patch from aioqsw.const import API_MAC_ADDR, API_PRODUCT, API_RESULT from aioqsw.exceptions import LoginError, QswError -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.components.qnap_qsw.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, ConfigEntryState from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers.device_registry import format_mac from .util import CONFIG, LIVE_MOCK, SYSTEM_BOARD_MOCK, USERS_LOGIN_MOCK @@ -53,7 +54,7 @@ async def test_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -67,7 +68,7 @@ async def test_form(hass: HomeAssistant) -> None: entry = conf_entries[0] assert entry.state is ConfigEntryState.LOADED - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert ( result["title"] == f"QNAP {SYSTEM_BOARD_MOCK[API_RESULT][API_PRODUCT]} {SYSTEM_BOARD_MOCK[API_RESULT][API_MAC_ADDR]}" @@ -164,7 +165,7 @@ async def test_dhcp_flow(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovered_connection" with ( @@ -216,7 +217,7 @@ async def test_dhcp_flow_error(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -233,7 +234,7 @@ async def test_dhcp_connection_error(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovered_connection" with patch( @@ -264,7 +265,7 @@ async def test_dhcp_login_error(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovered_connection" with patch( diff --git a/tests/components/rabbitair/test_config_flow.py b/tests/components/rabbitair/test_config_flow.py index 4b5867b441b..7ec411d6a48 100644 --- a/tests/components/rabbitair/test_config_flow.py +++ b/tests/components/rabbitair/test_config_flow.py @@ -84,7 +84,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] with patch( @@ -100,7 +100,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_TITLE assert result2["data"] == { CONF_HOST: TEST_HOST, @@ -127,7 +127,7 @@ async def test_form_cannot_connect( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] with patch( @@ -142,7 +142,7 @@ async def test_form_cannot_connect( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": base_value} @@ -151,7 +151,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] with patch( @@ -166,7 +166,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -177,7 +177,7 @@ async def test_zeroconf_discovery(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=ZEROCONF_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] with patch( @@ -193,7 +193,7 @@ async def test_zeroconf_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_TITLE assert result2["data"] == { CONF_HOST: TEST_NAME + ".local", @@ -207,5 +207,5 @@ async def test_zeroconf_discovery(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=ZEROCONF_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/radarr/test_config_flow.py b/tests/components/radarr/test_config_flow.py index 9733393836a..407b7b50c48 100644 --- a/tests/components/radarr/test_config_flow.py +++ b/tests/components/radarr/test_config_flow.py @@ -35,7 +35,7 @@ async def test_show_user_form(hass: HomeAssistant) -> None: ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM async def test_cannot_connect( @@ -50,7 +50,7 @@ async def test_cannot_connect( data=MOCK_USER_INPUT, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -64,7 +64,7 @@ async def test_invalid_auth( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=MOCK_USER_INPUT ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -81,7 +81,7 @@ async def test_wrong_app(hass: HomeAssistant) -> None: data={CONF_URL: URL, CONF_VERIFY_SSL: False}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "wrong_app" @@ -98,7 +98,7 @@ async def test_zero_conf_failure(hass: HomeAssistant) -> None: data={CONF_URL: URL, CONF_VERIFY_SSL: False}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "zeroconf_failed" @@ -115,7 +115,7 @@ async def test_unknown_error(hass: HomeAssistant) -> None: data=MOCK_USER_INPUT, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} @@ -132,7 +132,7 @@ async def test_zero_conf(hass: HomeAssistant) -> None: data={CONF_URL: URL, CONF_VERIFY_SSL: False}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == CONF_DATA @@ -153,14 +153,14 @@ async def test_full_reauth_flow_implementation( data=entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch_async_setup_entry() as mock_setup_entry: @@ -169,7 +169,7 @@ async def test_full_reauth_flow_implementation( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data == CONF_DATA | {CONF_API_KEY: "test-api-key-reauth"} @@ -188,7 +188,7 @@ async def test_full_user_flow_implementation( context={CONF_SOURCE: SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch_async_setup_entry(): @@ -197,7 +197,7 @@ async def test_full_user_flow_implementation( user_input=MOCK_USER_INPUT, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == CONF_DATA assert result["data"][CONF_URL] == "http://192.168.1.189:7887/test" diff --git a/tests/components/radio_browser/test_config_flow.py b/tests/components/radio_browser/test_config_flow.py index 0c0f2f479a8..be492e635ef 100644 --- a/tests/components/radio_browser/test_config_flow.py +++ b/tests/components/radio_browser/test_config_flow.py @@ -15,7 +15,7 @@ async def test_full_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") is None result2 = await hass.config_entries.flow.async_configure( @@ -23,7 +23,7 @@ async def test_full_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) user_input={}, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "Radio Browser" assert result2.get("data") == {} @@ -42,7 +42,7 @@ async def test_already_configured( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" @@ -54,7 +54,7 @@ async def test_onboarding_flow( DOMAIN, context={"source": "onboarding"} ) - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == "Radio Browser" assert result.get("data") == {} diff --git a/tests/components/radiotherm/test_config_flow.py b/tests/components/radiotherm/test_config_flow.py index 7729dfb86b7..002d6b71ae6 100644 --- a/tests/components/radiotherm/test_config_flow.py +++ b/tests/components/radiotherm/test_config_flow.py @@ -5,11 +5,12 @@ from unittest.mock import MagicMock, patch from radiotherm import CommonThermostat from radiotherm.validate import RadiothermTstatError -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.components.radiotherm.const import DOMAIN from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -51,7 +52,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "My Name" assert result2["data"] == { "host": "1.2.3.4", @@ -76,7 +77,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -97,7 +98,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {CONF_HOST: "cannot_connect"} @@ -119,7 +120,7 @@ async def test_dhcp_can_confirm(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["description_placeholders"] == { "host": "1.2.3.4", @@ -137,7 +138,7 @@ async def test_dhcp_can_confirm(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "My Name" assert result2["data"] == { "host": "1.2.3.4", @@ -163,7 +164,7 @@ async def test_dhcp_fails_to_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -192,7 +193,7 @@ async def test_dhcp_already_exists(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -209,7 +210,7 @@ async def test_user_unique_id_already_exists(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -230,5 +231,5 @@ async def test_user_unique_id_already_exists(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/rainbird/test_config_flow.py b/tests/components/rainbird/test_config_flow.py index 09db734f1ad..4768954850d 100644 --- a/tests/components/rainbird/test_config_flow.py +++ b/tests/components/rainbird/test_config_flow.py @@ -61,7 +61,7 @@ async def complete_flow(hass: HomeAssistant) -> FlowResult: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" assert not result.get("errors") assert "flow_id" in result @@ -165,7 +165,7 @@ async def test_multiple_config_entries( responses.extend(config_flow_responses) result = await complete_flow(hass) - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert dict(result.get("result").data) == expected_config_entry entries = hass.config_entries.async_entries(DOMAIN) @@ -241,7 +241,7 @@ async def test_duplicate_config_entries( result = await complete_flow(hass) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" assert dict(config_entry.data) == expected_config_entry_data @@ -261,7 +261,7 @@ async def test_controller_cannot_connect( ) result = await complete_flow(hass) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" assert result.get("errors") == {"base": "cannot_connect"} @@ -279,7 +279,7 @@ async def test_controller_timeout( side_effect=TimeoutError, ): result = await complete_flow(hass) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" assert result.get("errors") == {"base": "timeout_connect"} @@ -303,14 +303,14 @@ async def test_options_flow(hass: HomeAssistant, mock_setup: Mock) -> None: # Initiate the options flow result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "init" # Change the default duration result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ATTR_DURATION: 5} ) - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert config_entry.options == { ATTR_DURATION: 5, } diff --git a/tests/components/rainforest_eagle/test_config_flow.py b/tests/components/rainforest_eagle/test_config_flow.py index 191a7a4793e..d3df44fb4fe 100644 --- a/tests/components/rainforest_eagle/test_config_flow.py +++ b/tests/components/rainforest_eagle/test_config_flow.py @@ -22,7 +22,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -45,7 +45,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "abcdef" assert result2["data"] == { CONF_TYPE: TYPE_EAGLE_200, @@ -76,7 +76,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -99,5 +99,5 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/rainforest_raven/test_config_flow.py b/tests/components/rainforest_raven/test_config_flow.py index d7b188d6b14..d86dee6e0f6 100644 --- a/tests/components/rainforest_raven/test_config_flow.py +++ b/tests/components/rainforest_raven/test_config_flow.py @@ -6,11 +6,11 @@ from aioraven.device import RAVEnConnectionError import pytest import serial.tools.list_ports -from homeassistant import data_entry_flow from homeassistant.components.rainforest_raven.const import DOMAIN from homeassistant.config_entries import SOURCE_USB, SOURCE_USER from homeassistant.const import CONF_DEVICE, CONF_MAC, CONF_SOURCE from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import create_mock_device from .const import DEVICE_NAME, DISCOVERY_INFO, METER_LIST @@ -74,7 +74,7 @@ async def test_flow_usb(hass: HomeAssistant, mock_comports, mock_device): DOMAIN, context={CONF_SOURCE: SOURCE_USB}, data=DISCOVERY_INFO ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert not result.get("errors") assert result.get("flow_id") assert result.get("step_id") == "meters" @@ -83,7 +83,7 @@ async def test_flow_usb(hass: HomeAssistant, mock_comports, mock_device): result["flow_id"], user_input={CONF_MAC: [METER_LIST.meter_mac_ids[0].hex()]} ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY async def test_flow_usb_cannot_connect( @@ -94,7 +94,7 @@ async def test_flow_usb_cannot_connect( DOMAIN, context={CONF_SOURCE: SOURCE_USB}, data=DISCOVERY_INFO ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "cannot_connect" @@ -106,7 +106,7 @@ async def test_flow_usb_timeout_connect( DOMAIN, context={CONF_SOURCE: SOURCE_USB}, data=DISCOVERY_INFO ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "timeout_connect" @@ -118,7 +118,7 @@ async def test_flow_usb_comm_error( DOMAIN, context={CONF_SOURCE: SOURCE_USB}, data=DISCOVERY_INFO ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "cannot_connect" @@ -129,7 +129,7 @@ async def test_flow_user(hass: HomeAssistant, mock_comports, mock_device): context={CONF_SOURCE: SOURCE_USER}, ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert not result.get("errors") assert result.get("flow_id") assert result.get("step_id") == "user" @@ -141,7 +141,7 @@ async def test_flow_user(hass: HomeAssistant, mock_comports, mock_device): }, ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert not result.get("errors") assert result.get("flow_id") assert result.get("step_id") == "meters" @@ -150,7 +150,7 @@ async def test_flow_user(hass: HomeAssistant, mock_comports, mock_device): result["flow_id"], user_input={CONF_MAC: [METER_LIST.meter_mac_ids[0].hex()]} ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY async def test_flow_user_no_available_devices(hass: HomeAssistant, mock_comports): @@ -165,7 +165,7 @@ async def test_flow_user_no_available_devices(hass: HomeAssistant, mock_comports context={CONF_SOURCE: SOURCE_USER}, ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "no_devices_found" @@ -176,7 +176,7 @@ async def test_flow_user_in_progress(hass: HomeAssistant, mock_comports): context={CONF_SOURCE: SOURCE_USER}, ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert not result.get("errors") assert result.get("flow_id") assert result.get("step_id") == "user" @@ -186,7 +186,7 @@ async def test_flow_user_in_progress(hass: HomeAssistant, mock_comports): context={CONF_SOURCE: SOURCE_USER}, ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_in_progress" @@ -202,7 +202,7 @@ async def test_flow_user_cannot_connect( }, ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {CONF_DEVICE: "cannot_connect"} @@ -218,7 +218,7 @@ async def test_flow_user_timeout_connect( }, ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {CONF_DEVICE: "timeout_connect"} @@ -234,5 +234,5 @@ async def test_flow_user_comm_error( }, ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {CONF_DEVICE: "cannot_connect"} diff --git a/tests/components/rainmachine/test_config_flow.py b/tests/components/rainmachine/test_config_flow.py index 1c065a8f7ce..808c2f184a7 100644 --- a/tests/components/rainmachine/test_config_flow.py +++ b/tests/components/rainmachine/test_config_flow.py @@ -6,7 +6,7 @@ from unittest.mock import patch import pytest from regenmaschine.errors import RainMachineError -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, setup from homeassistant.components import zeroconf from homeassistant.components.rainmachine import ( CONF_ALLOW_INACTIVE_ZONES_TO_RUN, @@ -16,6 +16,7 @@ from homeassistant.components.rainmachine import ( ) from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SSL from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import entity_registry as er @@ -24,7 +25,7 @@ async def test_duplicate_error(hass: HomeAssistant, config, config_entry) -> Non result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -107,7 +108,7 @@ async def test_options_flow(hass: HomeAssistant, config, config_entry) -> None: ): await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -118,7 +119,7 @@ async def test_options_flow(hass: HomeAssistant, config, config_entry) -> None: CONF_ALLOW_INACTIVE_ZONES_TO_RUN: False, }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_DEFAULT_ZONE_RUN_TIME: 600, CONF_USE_APP_RUN_TIMES: False, @@ -133,7 +134,7 @@ async def test_show_form(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, data=None, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -144,7 +145,7 @@ async def test_step_user(hass: HomeAssistant, config, setup_rainmachine) -> None context={"source": config_entries.SOURCE_USER}, data=config, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "12345" assert result["data"] == { CONF_IP_ADDRESS: "192.168.1.100", @@ -179,7 +180,7 @@ async def test_step_homekit_zeroconf_ip_already_exists( ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -207,7 +208,7 @@ async def test_step_homekit_zeroconf_ip_change( ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_IP_ADDRESS] == "192.168.1.2" @@ -236,7 +237,7 @@ async def test_step_homekit_zeroconf_new_controller_when_some_exist( ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -258,7 +259,7 @@ async def test_step_homekit_zeroconf_new_controller_when_some_exist( ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "12345" assert result2["data"] == { CONF_IP_ADDRESS: "192.168.1.100", @@ -290,7 +291,7 @@ async def test_discovery_by_homekit_and_zeroconf_same_time( ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -310,5 +311,5 @@ async def test_discovery_by_homekit_and_zeroconf_same_time( ), ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_in_progress" diff --git a/tests/components/random/test_config_flow.py b/tests/components/random/test_config_flow.py index 4ffa59da4e4..b4eff5c966b 100644 --- a/tests/components/random/test_config_flow.py +++ b/tests/components/random/test_config_flow.py @@ -60,14 +60,14 @@ async def test_config_flow( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU result = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": entity_type}, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == entity_type with patch( @@ -82,7 +82,7 @@ async def test_config_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "My random entity" assert result["data"] == {} assert result["options"] == { @@ -108,14 +108,14 @@ async def test_wrong_uom( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU result = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": "sensor"}, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "sensor" with pytest.raises(Invalid, match="is not a valid unit for device class"): @@ -179,7 +179,7 @@ async def test_options( config_entry = hass.config_entries.async_entries(DOMAIN)[0] result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == entity_type assert "name" not in result["data_schema"].schema @@ -187,7 +187,7 @@ async def test_options( result["flow_id"], user_input=options_options, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "name": "My random", "entity_type": entity_type, diff --git a/tests/components/rapt_ble/test_config_flow.py b/tests/components/rapt_ble/test_config_flow.py index b71843bd44f..2189b8a610c 100644 --- a/tests/components/rapt_ble/test_config_flow.py +++ b/tests/components/rapt_ble/test_config_flow.py @@ -19,7 +19,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=COMPLETE_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( "homeassistant.components.rapt_ble.async_setup_entry", return_value=True @@ -27,7 +27,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "RAPT Pill 0666" assert result2["data"] == {} assert result2["result"].unique_id == RAPT_MAC @@ -40,7 +40,7 @@ async def test_async_step_bluetooth_not_rapt(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_RAPT_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -50,7 +50,7 @@ async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -64,7 +64,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( "homeassistant.components.rapt_ble.async_setup_entry", return_value=True @@ -73,7 +73,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: result["flow_id"], user_input={"address": RAPT_MAC}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "RAPT Pill 0666" assert result2["data"] == {} assert result2["result"].unique_id == RAPT_MAC @@ -89,7 +89,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -105,7 +105,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - result["flow_id"], user_input={"address": RAPT_MAC}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -127,7 +127,7 @@ async def test_async_step_user_with_found_devices_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -144,7 +144,7 @@ async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant) - context={"source": config_entries.SOURCE_BLUETOOTH}, data=COMPLETE_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -155,7 +155,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=COMPLETE_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -163,7 +163,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=COMPLETE_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -176,7 +176,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=COMPLETE_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -187,7 +187,7 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.rapt_ble.async_setup_entry", return_value=True @@ -196,7 +196,7 @@ async def test_async_step_user_takes_precedence_over_discovery( result["flow_id"], user_input={"address": RAPT_MAC}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "RAPT Pill 0666" assert result2["data"] == {} assert result2["result"].unique_id == RAPT_MAC diff --git a/tests/components/raspberry_pi/test_config_flow.py b/tests/components/raspberry_pi/test_config_flow.py index 05fea6ed3d3..19c23295493 100644 --- a/tests/components/raspberry_pi/test_config_flow.py +++ b/tests/components/raspberry_pi/test_config_flow.py @@ -21,7 +21,7 @@ async def test_config_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": "system"} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Raspberry Pi" assert result["data"] == {} assert result["options"] == {} @@ -54,6 +54,6 @@ async def test_config_flow_single_entry(hass: HomeAssistant) -> None: DOMAIN, context={"source": "system"} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" mock_setup_entry.assert_not_called() diff --git a/tests/components/rdw/test_config_flow.py b/tests/components/rdw/test_config_flow.py index b8c21be300e..2aa39f2c2d3 100644 --- a/tests/components/rdw/test_config_flow.py +++ b/tests/components/rdw/test_config_flow.py @@ -18,7 +18,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( @@ -28,7 +28,7 @@ async def test_full_user_flow( }, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "11-ZKZ-3" assert result2.get("data") == {CONF_LICENSE_PLATE: "11ZKZ3"} @@ -45,7 +45,7 @@ async def test_full_flow_with_authentication_error( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" mock_rdw_config_flow.vehicle.side_effect = RDWUnknownLicensePlateError @@ -56,7 +56,7 @@ async def test_full_flow_with_authentication_error( }, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "user" assert result2.get("errors") == {"base": "unknown_license_plate"} @@ -68,7 +68,7 @@ async def test_full_flow_with_authentication_error( }, ) - assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY assert result3.get("title") == "11-ZKZ-3" assert result3.get("data") == {CONF_LICENSE_PLATE: "11ZKZ3"} @@ -85,5 +85,5 @@ async def test_connection_error( data={CONF_LICENSE_PLATE: "0001TJ"}, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {"base": "cannot_connect"} diff --git a/tests/components/recollect_waste/test_config_flow.py b/tests/components/recollect_waste/test_config_flow.py index a65b0d27a74..aac829f00a3 100644 --- a/tests/components/recollect_waste/test_config_flow.py +++ b/tests/components/recollect_waste/test_config_flow.py @@ -5,7 +5,6 @@ from unittest.mock import AsyncMock, patch from aiorecollect.errors import RecollectError import pytest -from homeassistant import data_entry_flow from homeassistant.components.recollect_waste import ( CONF_PLACE_ID, CONF_SERVICE_ID, @@ -14,6 +13,7 @@ from homeassistant.components.recollect_waste import ( from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_FRIENDLY_NAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .conftest import TEST_PLACE_ID, TEST_SERVICE_ID @@ -39,7 +39,7 @@ async def test_create_entry( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # Test errors that can arise when checking the API key: @@ -47,13 +47,13 @@ async def test_create_entry( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == get_pickup_events_errors result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"{TEST_PLACE_ID}, {TEST_SERVICE_ID}" assert result["data"] == { CONF_PLACE_ID: TEST_PLACE_ID, @@ -66,7 +66,7 @@ async def test_duplicate_error(hass: HomeAssistant, config, setup_config_entry) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -75,11 +75,11 @@ async def test_options_flow( ) -> None: """Test config flow options.""" result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + 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_FRIENDLY_NAME: True} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_FRIENDLY_NAME: True} diff --git a/tests/components/refoss/test_config_flow.py b/tests/components/refoss/test_config_flow.py index f022c950635..ae40f6dd8b2 100644 --- a/tests/components/refoss/test_config_flow.py +++ b/tests/components/refoss/test_config_flow.py @@ -2,9 +2,10 @@ from unittest.mock import AsyncMock, patch -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.refoss.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import FakeDiscovery, build_base_device_mock @@ -33,11 +34,11 @@ async def test_creating_entry_sets_up( ) # Confirmation form - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -60,10 +61,10 @@ async def test_creating_entry_has_no_devices( ) # Confirmation form - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT await hass.async_block_till_done() diff --git a/tests/components/renault/test_config_flow.py b/tests/components/renault/test_config_flow.py index eca7991a27c..7d40cf69314 100644 --- a/tests/components/renault/test_config_flow.py +++ b/tests/components/renault/test_config_flow.py @@ -7,7 +7,7 @@ from renault_api.gigya.exceptions import InvalidCredentialsException from renault_api.kamereon import schemas from renault_api.renault_account import RenaultAccount -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.renault.const import ( CONF_KAMEREON_ACCOUNT_ID, CONF_LOCALE, @@ -16,6 +16,7 @@ from homeassistant.components.renault.const import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import aiohttp_client from .const import MOCK_CONFIG @@ -32,7 +33,7 @@ async def test_config_flow_single_account( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} # Failed credentials @@ -49,7 +50,7 @@ async def test_config_flow_single_account( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_credentials"} renault_account = AsyncMock() @@ -80,7 +81,7 @@ async def test_config_flow_single_account( }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "account_id_1" assert result["data"][CONF_USERNAME] == "email@test.com" assert result["data"][CONF_PASSWORD] == "test" @@ -97,7 +98,7 @@ async def test_config_flow_no_account( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} # Account list empty @@ -117,7 +118,7 @@ async def test_config_flow_no_account( }, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "kamereon_no_account" assert len(mock_setup_entry.mock_calls) == 0 @@ -130,7 +131,7 @@ async def test_config_flow_multiple_accounts( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} renault_account_1 = RenaultAccount( @@ -160,7 +161,7 @@ async def test_config_flow_multiple_accounts( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "kamereon" # Account selected @@ -168,7 +169,7 @@ async def test_config_flow_multiple_accounts( result["flow_id"], user_input={CONF_KAMEREON_ACCOUNT_ID: "account_id_2"}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "account_id_2" assert result["data"][CONF_USERNAME] == "email@test.com" assert result["data"][CONF_PASSWORD] == "test" @@ -188,7 +189,7 @@ async def test_config_flow_duplicate( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} renault_account = RenaultAccount( @@ -212,7 +213,7 @@ async def test_config_flow_duplicate( }, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" await hass.async_block_till_done() @@ -233,7 +234,7 @@ async def test_reauth(hass: HomeAssistant, config_entry: ConfigEntry) -> None: data=MOCK_CONFIG, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["description_placeholders"] == {CONF_USERNAME: "email@test.com"} assert result["errors"] == {} @@ -247,7 +248,7 @@ async def test_reauth(hass: HomeAssistant, config_entry: ConfigEntry) -> None: user_input={CONF_PASSWORD: "any"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["description_placeholders"] == {CONF_USERNAME: "email@test.com"} assert result2["errors"] == {"base": "invalid_credentials"} @@ -258,5 +259,5 @@ async def test_reauth(hass: HomeAssistant, config_entry: ConfigEntry) -> None: user_input={CONF_PASSWORD: "any"}, ) - assert result3["type"] == data_entry_flow.FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" diff --git a/tests/components/renson/test_config_flow.py b/tests/components/renson/test_config_flow.py index 6d51605824e..2f60149d14d 100644 --- a/tests/components/renson/test_config_flow.py +++ b/tests/components/renson/test_config_flow.py @@ -13,7 +13,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -34,7 +34,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Renson" assert result2["data"] == { "host": "1.1.1.1", @@ -59,7 +59,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -80,5 +80,5 @@ async def test_form_unknown(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/reolink/test_config_flow.py b/tests/components/reolink/test_config_flow.py index e8818c9e560..bcbf4fe45a7 100644 --- a/tests/components/reolink/test_config_flow.py +++ b/tests/components/reolink/test_config_flow.py @@ -8,7 +8,7 @@ from unittest.mock import AsyncMock, MagicMock, call import pytest from reolink_aio.exceptions import ApiError, CredentialsInvalidError, ReolinkError -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.components.reolink import DEVICE_UPDATE_INTERVAL, const from homeassistant.components.reolink.config_flow import DEFAULT_PROTOCOL @@ -23,6 +23,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers.device_registry import format_mac from homeassistant.util.dt import utcnow @@ -53,7 +54,7 @@ async def test_config_flow_manual_success( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -66,7 +67,7 @@ async def test_config_flow_manual_success( }, ) - assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_NVR_NAME assert result["data"] == { CONF_HOST: TEST_HOST, @@ -88,7 +89,7 @@ async def test_config_flow_errors( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -103,7 +104,7 @@ async def test_config_flow_errors( }, ) - assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {CONF_USERNAME: "not_admin"} @@ -119,7 +120,7 @@ async def test_config_flow_errors( }, ) - assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {CONF_HOST: "cannot_connect"} @@ -133,7 +134,7 @@ async def test_config_flow_errors( }, ) - assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "webhook_exception"} @@ -149,7 +150,7 @@ async def test_config_flow_errors( }, ) - assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {CONF_HOST: "unknown"} @@ -163,7 +164,7 @@ async def test_config_flow_errors( }, ) - assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {CONF_HOST: "invalid_auth"} @@ -177,7 +178,7 @@ async def test_config_flow_errors( }, ) - assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {CONF_HOST: "api_error"} @@ -193,7 +194,7 @@ async def test_config_flow_errors( }, ) - assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_NVR_NAME assert result["data"] == { CONF_HOST: TEST_HOST, @@ -231,7 +232,7 @@ async def test_options_flow(hass: HomeAssistant, mock_setup_entry: MagicMock) -> result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -239,7 +240,7 @@ async def test_options_flow(hass: HomeAssistant, mock_setup_entry: MagicMock) -> user_input={CONF_PROTOCOL: "rtmp"}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_PROTOCOL: "rtmp", } @@ -270,7 +271,7 @@ async def test_change_connection_settings( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -283,7 +284,7 @@ async def test_change_connection_settings( }, ) - assert result["type"] is data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == TEST_HOST2 assert config_entry.data[CONF_USERNAME] == TEST_USERNAME2 @@ -323,7 +324,7 @@ async def test_reauth(hass: HomeAssistant, mock_setup_entry: MagicMock) -> None: data=config_entry.data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -331,7 +332,7 @@ async def test_reauth(hass: HomeAssistant, mock_setup_entry: MagicMock) -> None: {}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -343,7 +344,7 @@ async def test_reauth(hass: HomeAssistant, mock_setup_entry: MagicMock) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert config_entry.data[CONF_HOST] == TEST_HOST assert config_entry.data[CONF_USERNAME] == TEST_USERNAME2 @@ -362,7 +363,7 @@ async def test_dhcp_flow(hass: HomeAssistant, mock_setup_entry: MagicMock) -> No const.DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=dhcp_data ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -374,7 +375,7 @@ async def test_dhcp_flow(hass: HomeAssistant, mock_setup_entry: MagicMock) -> No }, ) - assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_NVR_NAME assert result["data"] == { CONF_HOST: TEST_HOST, @@ -489,7 +490,7 @@ async def test_dhcp_ip_update( assert reolink_connect_class.call_args_list == expected_calls - assert result["type"] is data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" await hass.async_block_till_done() diff --git a/tests/components/rfxtrx/test_config_flow.py b/tests/components/rfxtrx/test_config_flow.py index d3c87885782..dad48a04290 100644 --- a/tests/components/rfxtrx/test_config_flow.py +++ b/tests/components/rfxtrx/test_config_flow.py @@ -6,10 +6,11 @@ from unittest.mock import MagicMock, patch, sentinel from RFXtrx import RFXtrxTransportError import serial.tools.list_ports -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.rfxtrx import DOMAIN, config_flow from homeassistant.const import STATE_UNKNOWN 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 tests.common import MockConfigEntry @@ -284,7 +285,7 @@ async def test_options_global(hass: HomeAssistant) -> None: user_input={"automatic_add": True, "protocols": SOME_PROTOCOLS}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -319,7 +320,7 @@ async def test_no_protocols(hass: HomeAssistant) -> None: user_input={"automatic_add": False, "protocols": []}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -374,7 +375,7 @@ async def test_options_add_device(hass: HomeAssistant) -> None: result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -528,7 +529,7 @@ async def test_options_replace_sensor_device(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -661,7 +662,7 @@ async def test_options_replace_control_device(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -742,7 +743,7 @@ async def test_options_add_and_configure_device(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -786,7 +787,7 @@ async def test_options_add_and_configure_device(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -869,7 +870,7 @@ async def test_options_configure_rfy_cover_device(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() diff --git a/tests/components/rhasspy/test_config_flow.py b/tests/components/rhasspy/test_config_flow.py index 1a53dd32e04..7f74143c67c 100644 --- a/tests/components/rhasspy/test_config_flow.py +++ b/tests/components/rhasspy/test_config_flow.py @@ -15,7 +15,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -28,7 +28,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Rhasspy" assert result2["data"] == {} assert len(mock_setup_entry.mock_calls) == 1 @@ -41,5 +41,5 @@ async def test_single_entry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/ridwell/test_config_flow.py b/tests/components/ridwell/test_config_flow.py index 15352929b4c..601ac182670 100644 --- a/tests/components/ridwell/test_config_flow.py +++ b/tests/components/ridwell/test_config_flow.py @@ -28,7 +28,7 @@ async def test_create_entry( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # Test errors that can arise: @@ -39,7 +39,7 @@ async def test_create_entry( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == errors @@ -47,7 +47,7 @@ async def test_create_entry( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_USERNAME assert result["data"] == { CONF_USERNAME: TEST_USERNAME, @@ -60,7 +60,7 @@ async def test_duplicate_error(hass: HomeAssistant, config, setup_config_entry) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=config ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -75,6 +75,6 @@ async def test_step_reauth( result["flow_id"], user_input={CONF_PASSWORD: "new_password"}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 diff --git a/tests/components/ring/test_config_flow.py b/tests/components/ring/test_config_flow.py index f9c24ad77c5..ae3301be3ed 100644 --- a/tests/components/ring/test_config_flow.py +++ b/tests/components/ring/test_config_flow.py @@ -76,7 +76,7 @@ async def test_form_2fa( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} mock_ring_auth.fetch_token.side_effect = ring_doorbell.Requires2FAError @@ -92,7 +92,7 @@ async def test_form_2fa( "foo@bar.com", "fake-password", None ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "2fa" mock_ring_auth.fetch_token.reset_mock(side_effect=True) mock_ring_auth.fetch_token.return_value = "new-foobar" @@ -104,7 +104,7 @@ async def test_form_2fa( mock_ring_auth.fetch_token.assert_called_once_with( "foo@bar.com", "fake-password", "123456" ) - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "foo@bar.com" assert result3["data"] == { "username": "foo@bar.com", @@ -139,7 +139,7 @@ async def test_reauth( mock_ring_auth.fetch_token.assert_called_once_with( "foo@bar.com", "other_fake_password", None ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "2fa" mock_ring_auth.fetch_token.reset_mock(side_effect=True) mock_ring_auth.fetch_token.return_value = "new-foobar" @@ -151,7 +151,7 @@ async def test_reauth( mock_ring_auth.fetch_token.assert_called_once_with( "foo@bar.com", "other_fake_password", "123456" ) - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" assert mock_added_config_entry.data == { "username": "foo@bar.com", @@ -197,7 +197,7 @@ async def test_reauth_error( mock_ring_auth.fetch_token.assert_called_once_with( "foo@bar.com", "error_fake_password", None ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": errors_msg} # Now test reauth can go on to succeed @@ -213,7 +213,7 @@ async def test_reauth_error( mock_ring_auth.fetch_token.assert_called_once_with( "foo@bar.com", "other_fake_password", None ) - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" assert mock_added_config_entry.data == { "username": "foo@bar.com", diff --git a/tests/components/risco/test_config_flow.py b/tests/components/risco/test_config_flow.py index d031f4e8542..3f7a166b465 100644 --- a/tests/components/risco/test_config_flow.py +++ b/tests/components/risco/test_config_flow.py @@ -57,13 +57,13 @@ async def test_cloud_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": "cloud"} ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {} with ( @@ -88,7 +88,7 @@ async def test_cloud_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == TEST_SITE_NAME assert result3["data"] == TEST_CLOUD_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -120,7 +120,7 @@ async def test_cloud_error(hass: HomeAssistant, login_with_error, error) -> None ) mock_close.assert_awaited_once() - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": error} @@ -146,7 +146,7 @@ async def test_form_cloud_already_exists(hass: HomeAssistant) -> None: result2["flow_id"], TEST_CLOUD_DATA ) - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "already_configured" @@ -236,13 +236,13 @@ async def test_local_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": "local"} ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {} with ( @@ -272,7 +272,7 @@ async def test_local_form(hass: HomeAssistant) -> None: "type": "local", CONF_COMMUNICATION_DELAY: 0, } - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == TEST_SITE_NAME assert result3["data"] == expected_data assert len(mock_setup_entry.mock_calls) == 1 @@ -300,7 +300,7 @@ async def test_local_error(hass: HomeAssistant, connect_with_error, error) -> No result2["flow_id"], TEST_LOCAL_DATA ) - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": error} @@ -339,7 +339,7 @@ async def test_form_local_already_exists(hass: HomeAssistant) -> None: result2["flow_id"], TEST_LOCAL_DATA ) - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "already_configured" @@ -355,14 +355,14 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input=TEST_OPTIONS, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "risco_to_ha" result = await hass.config_entries.options.async_configure( @@ -370,7 +370,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: user_input=TEST_RISCO_TO_HA, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "ha_to_risco" with patch("homeassistant.components.risco.async_setup_entry", return_value=True): @@ -379,7 +379,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: user_input=TEST_HA_TO_RISCO, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert entry.options == { **TEST_OPTIONS, "risco_states_to_ha": TEST_RISCO_TO_HA, diff --git a/tests/components/roborock/test_config_flow.py b/tests/components/roborock/test_config_flow.py index b5cff60cddb..fc097dd73ae 100644 --- a/tests/components/roborock/test_config_flow.py +++ b/tests/components/roborock/test_config_flow.py @@ -33,7 +33,7 @@ async def test_config_flow_success( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( "homeassistant.components.roborock.config_flow.RoborockApiClient.request_code" @@ -42,7 +42,7 @@ async def test_config_flow_success( result["flow_id"], {CONF_USERNAME: USER_EMAIL} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "code" assert result["errors"] == {} with patch( @@ -53,7 +53,7 @@ async def test_config_flow_success( result["flow_id"], user_input={CONF_ENTRY_CODE: "123456"} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == USER_EMAIL assert result["data"] == MOCK_CONFIG assert result["result"] @@ -86,7 +86,7 @@ async def test_config_flow_failures_request_code( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( "homeassistant.components.roborock.config_flow.RoborockApiClient.request_code", @@ -95,7 +95,7 @@ async def test_config_flow_failures_request_code( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_USERNAME: USER_EMAIL} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == request_code_errors # Recover from error with patch( @@ -105,7 +105,7 @@ async def test_config_flow_failures_request_code( result["flow_id"], {CONF_USERNAME: USER_EMAIL} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "code" assert result["errors"] == {} with patch( @@ -116,7 +116,7 @@ async def test_config_flow_failures_request_code( result["flow_id"], user_input={CONF_ENTRY_CODE: "123456"} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == USER_EMAIL assert result["data"] == MOCK_CONFIG assert result["result"] @@ -147,7 +147,7 @@ async def test_config_flow_failures_code_login( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( "homeassistant.components.roborock.config_flow.RoborockApiClient.request_code" @@ -156,7 +156,7 @@ async def test_config_flow_failures_code_login( result["flow_id"], {CONF_USERNAME: USER_EMAIL} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "code" assert result["errors"] == {} # Raise exception for invalid code @@ -167,7 +167,7 @@ async def test_config_flow_failures_code_login( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_ENTRY_CODE: "123456"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == code_login_errors with patch( "homeassistant.components.roborock.config_flow.RoborockApiClient.code_login", @@ -177,7 +177,7 @@ async def test_config_flow_failures_code_login( result["flow_id"], user_input={CONF_ENTRY_CODE: "123456"} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == USER_EMAIL assert result["data"] == MOCK_CONFIG assert result["result"] @@ -205,7 +205,7 @@ async def test_reauth_flow( ) # Enter a new code assert result["step_id"] == "code" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM new_user_data = deepcopy(USER_DATA) new_user_data.rriot.s = "new_password_hash" with patch( @@ -215,6 +215,6 @@ async def test_reauth_flow( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_ENTRY_CODE: "123456"} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert mock_roborock_entry.data["user_data"]["rriot"]["s"] == "new_password_hash" diff --git a/tests/components/roku/test_config_flow.py b/tests/components/roku/test_config_flow.py index 34640474bcd..3cf5627f342 100644 --- a/tests/components/roku/test_config_flow.py +++ b/tests/components/roku/test_config_flow.py @@ -37,7 +37,7 @@ async def test_duplicate_error( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=user_input ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" user_input = {CONF_HOST: mock_config_entry.data[CONF_HOST]} @@ -45,7 +45,7 @@ async def test_duplicate_error( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=user_input ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO) @@ -53,7 +53,7 @@ async def test_duplicate_error( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -66,7 +66,7 @@ async def test_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} user_input = {CONF_HOST: HOST} @@ -75,7 +75,7 @@ async def test_form( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "My Roku 3" assert "data" in result @@ -99,7 +99,7 @@ async def test_form_cannot_connect( flow_id=result["flow_id"], user_input={CONF_HOST: HOST} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -118,7 +118,7 @@ async def test_form_unknown_error( flow_id=result["flow_id"], user_input=user_input ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -135,7 +135,7 @@ async def test_homekit_cannot_connect( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -152,7 +152,7 @@ async def test_homekit_unknown_error( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -168,7 +168,7 @@ async def test_homekit_discovery( DOMAIN, context={CONF_SOURCE: SOURCE_HOMEKIT}, data=discovery_info ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" assert result["description_placeholders"] == {CONF_NAME: NAME_ROKUTV} @@ -177,7 +177,7 @@ async def test_homekit_discovery( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == NAME_ROKUTV assert "data" in result @@ -190,7 +190,7 @@ async def test_homekit_discovery( DOMAIN, context={CONF_SOURCE: SOURCE_HOMEKIT}, data=discovery_info ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -207,7 +207,7 @@ async def test_ssdp_cannot_connect( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -224,7 +224,7 @@ async def test_ssdp_unknown_error( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -239,7 +239,7 @@ async def test_ssdp_discovery( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" assert result["description_placeholders"] == {CONF_NAME: UPNP_FRIENDLY_NAME} @@ -248,7 +248,7 @@ async def test_ssdp_discovery( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == UPNP_FRIENDLY_NAME assert result["data"] diff --git a/tests/components/romy/test_config_flow.py b/tests/components/romy/test_config_flow.py index 480a37fa068..a29f899ee9d 100644 --- a/tests/components/romy/test_config_flow.py +++ b/tests/components/romy/test_config_flow.py @@ -5,11 +5,12 @@ from unittest.mock import Mock, PropertyMock, patch from romy import RomyRobot -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.romy.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_PASSWORD from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType def _create_mocked_romy( @@ -56,7 +57,7 @@ async def test_show_user_form_robot_is_offline_and_locked(hass: HomeAssistant) - assert result1["errors"].get("host") == "cannot_connect" assert result1["step_id"] == "user" - assert result1["type"] == data_entry_flow.FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM # Robot is locked with patch( @@ -68,7 +69,7 @@ async def test_show_user_form_robot_is_offline_and_locked(hass: HomeAssistant) - ) assert result2["step_id"] == "password" - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM # Robot is initialized and unlocked with patch( @@ -80,7 +81,7 @@ async def test_show_user_form_robot_is_offline_and_locked(hass: HomeAssistant) - ) assert "errors" not in result3 - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY async def test_show_user_form_robot_unlock_with_password(hass: HomeAssistant) -> None: @@ -106,7 +107,7 @@ async def test_show_user_form_robot_unlock_with_password(hass: HomeAssistant) -> assert result2["errors"] == {"password": "invalid_auth"} assert result2["step_id"] == "password" - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM with patch( "homeassistant.components.romy.config_flow.romy.create_romy", @@ -118,7 +119,7 @@ async def test_show_user_form_robot_unlock_with_password(hass: HomeAssistant) -> assert result3["errors"] == {"password": "cannot_connect"} assert result3["step_id"] == "password" - assert result3["type"] == data_entry_flow.FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM with patch( "homeassistant.components.romy.config_flow.romy.create_romy", @@ -129,7 +130,7 @@ async def test_show_user_form_robot_unlock_with_password(hass: HomeAssistant) -> ) assert "errors" not in result4 - assert result4["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY async def test_show_user_form_robot_reachable_again(hass: HomeAssistant) -> None: @@ -148,7 +149,7 @@ async def test_show_user_form_robot_reachable_again(hass: HomeAssistant) -> None assert result1["errors"].get("host") == "cannot_connect" assert result1["step_id"] == "user" - assert result1["type"] == data_entry_flow.FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM # Robot is locked with patch( @@ -160,7 +161,7 @@ async def test_show_user_form_robot_reachable_again(hass: HomeAssistant) -> None ) assert "errors" not in result2 - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( @@ -188,7 +189,7 @@ async def test_zero_conf_locked_interface_robot(hass: HomeAssistant) -> None: ) assert result1["step_id"] == "password" - assert result1["type"] == data_entry_flow.FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM with patch( "homeassistant.components.romy.config_flow.romy.create_romy", @@ -199,7 +200,7 @@ async def test_zero_conf_locked_interface_robot(hass: HomeAssistant) -> None: ) assert "errors" not in result2 - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY async def test_zero_conf_uninitialized_robot(hass: HomeAssistant) -> None: @@ -216,7 +217,7 @@ async def test_zero_conf_uninitialized_robot(hass: HomeAssistant) -> None: ) assert result["reason"] == "cannot_connect" - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT async def test_zero_conf_unlocked_interface_robot(hass: HomeAssistant) -> None: @@ -233,7 +234,7 @@ async def test_zero_conf_unlocked_interface_robot(hass: HomeAssistant) -> None: ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -246,4 +247,4 @@ async def test_zero_conf_unlocked_interface_robot(hass: HomeAssistant) -> None: assert result["result"] assert result["result"].unique_id == "aicu-aicgsbksisfapcjqmqjq" - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY diff --git a/tests/components/roon/test_config_flow.py b/tests/components/roon/test_config_flow.py index c31e689e05b..a9bae92f5e6 100644 --- a/tests/components/roon/test_config_flow.py +++ b/tests/components/roon/test_config_flow.py @@ -2,9 +2,10 @@ from unittest.mock import patch -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.roon.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -194,7 +195,7 @@ async def test_duplicate_config(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/rova/test_config_flow.py b/tests/components/rova/test_config_flow.py index 357cd9eb344..d9d1df3e188 100644 --- a/tests/components/rova/test_config_flow.py +++ b/tests/components/rova/test_config_flow.py @@ -5,7 +5,6 @@ from unittest.mock import MagicMock import pytest from requests.exceptions import ConnectTimeout, HTTPError -from homeassistant import data_entry_flow from homeassistant.components.rova.const import ( CONF_HOUSE_NUMBER, CONF_HOUSE_NUMBER_SUFFIX, @@ -14,6 +13,7 @@ from homeassistant.components.rova.const import ( ) from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -27,7 +27,7 @@ async def test_user(hass: HomeAssistant, mock_rova: MagicMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" # test with all information provided @@ -40,7 +40,7 @@ async def test_user(hass: HomeAssistant, mock_rova: MagicMock) -> None: CONF_HOUSE_NUMBER_SUFFIX: HOUSE_NUMBER_SUFFIX, }, ) - assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY data = result.get("data") assert data @@ -69,7 +69,7 @@ async def test_error_if_not_rova_area( }, ) - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {"base": "invalid_rova_area"} # now reset the return value and test if we can recover @@ -84,7 +84,7 @@ async def test_error_if_not_rova_area( }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"{ZIP_CODE} {HOUSE_NUMBER} {HOUSE_NUMBER_SUFFIX}" assert result["data"] == { CONF_ZIP_CODE: ZIP_CODE, @@ -114,7 +114,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant) -> None: CONF_HOUSE_NUMBER_SUFFIX: HOUSE_NUMBER_SUFFIX, }, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -145,7 +145,7 @@ async def test_abort_if_api_throws_exception( CONF_HOUSE_NUMBER_SUFFIX: HOUSE_NUMBER_SUFFIX, }, ) - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {"base": error} # now reset the side effect to see if we can recover @@ -160,7 +160,7 @@ async def test_abort_if_api_throws_exception( }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"{ZIP_CODE} {HOUSE_NUMBER} {HOUSE_NUMBER_SUFFIX}" assert result["data"] == { CONF_ZIP_CODE: ZIP_CODE, @@ -181,7 +181,7 @@ async def test_import(hass: HomeAssistant, mock_rova: MagicMock) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"{ZIP_CODE} {HOUSE_NUMBER} {HOUSE_NUMBER_SUFFIX}" assert result["data"] == { CONF_ZIP_CODE: ZIP_CODE, @@ -215,7 +215,7 @@ async def test_import_already_configured( }, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -237,7 +237,7 @@ async def test_import_if_not_rova_area( }, ) - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "invalid_rova_area" @@ -266,5 +266,5 @@ async def test_import_connection_errors( }, ) - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == error diff --git a/tests/components/rpi_power/test_config_flow.py b/tests/components/rpi_power/test_config_flow.py index 1cb9f772d70..1bce51830f0 100644 --- a/tests/components/rpi_power/test_config_flow.py +++ b/tests/components/rpi_power/test_config_flow.py @@ -18,13 +18,13 @@ async def test_setup(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert not result["errors"] with patch(MODULE, return_value=MagicMock()): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_not_supported(hass: HomeAssistant) -> None: @@ -36,7 +36,7 @@ async def test_not_supported(hass: HomeAssistant) -> None: with patch(MODULE, return_value=None): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -47,7 +47,7 @@ async def test_onboarding(hass: HomeAssistant) -> None: DOMAIN, context={"source": "onboarding"}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_onboarding_not_supported(hass: HomeAssistant) -> None: @@ -57,5 +57,5 @@ async def test_onboarding_not_supported(hass: HomeAssistant) -> None: DOMAIN, context={"source": "onboarding"}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" diff --git a/tests/components/ruckus_unleashed/test_config_flow.py b/tests/components/ruckus_unleashed/test_config_flow.py index ae0ccb0a9b1..5bfe2d941d5 100644 --- a/tests/components/ruckus_unleashed/test_config_flow.py +++ b/tests/components/ruckus_unleashed/test_config_flow.py @@ -11,7 +11,7 @@ from aioruckus.const import ( ) from aioruckus.exceptions import AuthenticationError -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.ruckus_unleashed.const import ( API_SYS_SYSINFO, API_SYS_SYSINFO_SERIAL, @@ -19,6 +19,7 @@ from homeassistant.components.ruckus_unleashed.const import ( ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.util import utcnow from . import ( @@ -37,7 +38,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -54,7 +55,7 @@ async def test_form(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == DEFAULT_TITLE assert result2["data"] == CONFIG @@ -73,7 +74,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: CONFIG, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -96,7 +97,7 @@ async def test_form_user_reauth(hass: HomeAssistant) -> None: assert len(flows) == 1 assert "flow_id" in flows[0] - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -111,7 +112,7 @@ async def test_form_user_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -134,7 +135,7 @@ async def test_form_user_reauth_different_unique_id(hass: HomeAssistant) -> None assert len(flows) == 1 assert "flow_id" in flows[0] - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -151,7 +152,7 @@ async def test_form_user_reauth_different_unique_id(hass: HomeAssistant) -> None ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_host"} @@ -174,7 +175,7 @@ async def test_form_user_reauth_invalid_auth(hass: HomeAssistant) -> None: assert len(flows) == 1 assert "flow_id" in flows[0] - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -191,7 +192,7 @@ async def test_form_user_reauth_invalid_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -214,7 +215,7 @@ async def test_form_user_reauth_cannot_connect(hass: HomeAssistant) -> None: assert len(flows) == 1 assert "flow_id" in flows[0] - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -231,7 +232,7 @@ async def test_form_user_reauth_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -254,7 +255,7 @@ async def test_form_user_reauth_general_exception(hass: HomeAssistant) -> None: assert len(flows) == 1 assert "flow_id" in flows[0] - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -269,7 +270,7 @@ async def test_form_user_reauth_general_exception(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "unknown"} @@ -288,7 +289,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: CONFIG, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -304,7 +305,7 @@ async def test_form_general_exception(hass: HomeAssistant) -> None: CONFIG, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "unknown"} @@ -325,7 +326,7 @@ async def test_form_unexpected_response(hass: HomeAssistant) -> None: CONFIG, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -348,7 +349,7 @@ async def test_form_duplicate_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -356,5 +357,5 @@ async def test_form_duplicate_error(hass: HomeAssistant) -> None: CONFIG, ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/ruuvi_gateway/test_config_flow.py b/tests/components/ruuvi_gateway/test_config_flow.py index e9e8446f8ac..c4ecf929f94 100644 --- a/tests/components/ruuvi_gateway/test_config_flow.py +++ b/tests/components/ruuvi_gateway/test_config_flow.py @@ -50,7 +50,7 @@ async def test_ok_setup(hass: HomeAssistant, init_data, init_context, entry) -> data=init_data, context=init_context, ) - assert init_result["type"] == FlowResultType.FORM + assert init_result["type"] is FlowResultType.FORM assert init_result["step_id"] == config_entries.SOURCE_USER assert init_result["errors"] is None @@ -61,7 +61,7 @@ async def test_ok_setup(hass: HomeAssistant, init_data, init_context, entry) -> entry, ) await hass.async_block_till_done() - assert config_result["type"] == FlowResultType.CREATE_ENTRY + assert config_result["type"] is FlowResultType.CREATE_ENTRY assert config_result["title"] == EXPECTED_TITLE assert config_result["data"] == entry assert config_result["context"]["unique_id"] == GATEWAY_MAC_LOWER @@ -80,7 +80,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: BASE_DATA, ) - assert config_result["type"] == FlowResultType.FORM + assert config_result["type"] is FlowResultType.FORM assert config_result["errors"] == {"base": "invalid_auth"} # Check that we still can finalize setup @@ -90,7 +90,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: BASE_DATA, ) await hass.async_block_till_done() - assert config_result["type"] == FlowResultType.CREATE_ENTRY + assert config_result["type"] is FlowResultType.CREATE_ENTRY assert config_result["title"] == EXPECTED_TITLE assert config_result["data"] == BASE_DATA assert config_result["context"]["unique_id"] == GATEWAY_MAC_LOWER @@ -109,7 +109,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: BASE_DATA, ) - assert config_result["type"] == FlowResultType.FORM + assert config_result["type"] is FlowResultType.FORM assert config_result["errors"] == {"base": "cannot_connect"} # Check that we still can finalize setup @@ -119,7 +119,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: BASE_DATA, ) await hass.async_block_till_done() - assert config_result["type"] == FlowResultType.CREATE_ENTRY + assert config_result["type"] is FlowResultType.CREATE_ENTRY assert config_result["title"] == EXPECTED_TITLE assert config_result["data"] == BASE_DATA assert config_result["context"]["unique_id"] == GATEWAY_MAC_LOWER @@ -138,7 +138,7 @@ async def test_form_unexpected(hass: HomeAssistant) -> None: BASE_DATA, ) - assert config_result["type"] == FlowResultType.FORM + assert config_result["type"] is FlowResultType.FORM assert config_result["errors"] == {"base": "unknown"} # Check that we still can finalize setup @@ -148,7 +148,7 @@ async def test_form_unexpected(hass: HomeAssistant) -> None: BASE_DATA, ) await hass.async_block_till_done() - assert config_result["type"] == FlowResultType.CREATE_ENTRY + assert config_result["type"] is FlowResultType.CREATE_ENTRY assert config_result["title"] == EXPECTED_TITLE assert config_result["data"] == BASE_DATA assert config_result["context"]["unique_id"] == GATEWAY_MAC_LOWER diff --git a/tests/components/ruuvitag_ble/test_config_flow.py b/tests/components/ruuvitag_ble/test_config_flow.py index 6f668b0168b..b6c79f1de0e 100644 --- a/tests/components/ruuvitag_ble/test_config_flow.py +++ b/tests/components/ruuvitag_ble/test_config_flow.py @@ -26,7 +26,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=RUUVITAG_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( "homeassistant.components.ruuvitag_ble.async_setup_entry", return_value=True @@ -34,7 +34,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == CONFIGURED_NAME assert result2["result"].unique_id == RUUVITAG_SERVICE_INFO.address @@ -46,7 +46,7 @@ async def test_async_step_bluetooth_not_ruuvitag(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_RUUVITAG_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -56,7 +56,7 @@ async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -70,7 +70,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( "homeassistant.components.ruuvitag_ble.async_setup_entry", return_value=True @@ -79,7 +79,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: result["flow_id"], user_input={"address": RUUVITAG_SERVICE_INFO.address}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == CONFIGURED_NAME assert result2["result"].unique_id == RUUVITAG_SERVICE_INFO.address @@ -94,7 +94,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -110,7 +110,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - result["flow_id"], user_input={"address": RUUVITAG_SERVICE_INFO.address}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -132,7 +132,7 @@ async def test_async_step_user_with_found_devices_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -149,7 +149,7 @@ async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant) - context={"source": config_entries.SOURCE_BLUETOOTH}, data=RUUVITAG_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -160,7 +160,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=RUUVITAG_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -168,7 +168,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=RUUVITAG_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -181,7 +181,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=RUUVITAG_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -192,7 +192,7 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.ruuvitag_ble.async_setup_entry", return_value=True @@ -201,7 +201,7 @@ async def test_async_step_user_takes_precedence_over_discovery( result["flow_id"], user_input={"address": RUUVITAG_SERVICE_INFO.address}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == CONFIGURED_NAME assert result2["data"] == {} assert result2["result"].unique_id == RUUVITAG_SERVICE_INFO.address diff --git a/tests/components/rympro/test_config_flow.py b/tests/components/rympro/test_config_flow.py index f5591d8e0c7..05078eb9a6c 100644 --- a/tests/components/rympro/test_config_flow.py +++ b/tests/components/rympro/test_config_flow.py @@ -41,7 +41,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -67,7 +67,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_DATA[CONF_EMAIL] assert result2["data"] == TEST_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -99,7 +99,7 @@ async def test_login_error(hass: HomeAssistant, exception, error) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": error} with ( @@ -125,7 +125,7 @@ async def test_login_error(hass: HomeAssistant, exception, error) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == TEST_DATA[CONF_EMAIL] assert result3["data"] == TEST_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -156,7 +156,7 @@ async def test_form_already_exists(hass: HomeAssistant, config_entry) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/sabnzbd/test_config_flow.py b/tests/components/sabnzbd/test_config_flow.py index 2da1c7c87db..7f5394902b4 100644 --- a/tests/components/sabnzbd/test_config_flow.py +++ b/tests/components/sabnzbd/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, patch from pysabnzbd import SabnzbdApiException import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.sabnzbd import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import ( @@ -41,7 +41,7 @@ async def test_create_entry(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -54,7 +54,7 @@ async def test_create_entry(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "edc3eee7330e" assert result2["data"] == { CONF_API_KEY: "edc3eee7330e4fdda04489e3fbc283d0", @@ -91,7 +91,7 @@ async def test_import_flow(hass: HomeAssistant) -> None: data=VALID_CONFIG_OLD, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "edc3eee7330e" assert result["data"][CONF_NAME] == "Sabnzbd" assert result["data"][CONF_API_KEY] == "edc3eee7330e4fdda04489e3fbc283d0" diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index a300c28b945..8ce1467b451 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -358,7 +358,7 @@ async def test_user_legacy_missing_auth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing" assert result["errors"] == {"base": "auth_missing"} @@ -370,7 +370,7 @@ async def test_user_legacy_missing_auth(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == RESULT_CANNOT_CONNECT @@ -451,7 +451,7 @@ async def test_user_websocket_auth_retry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing" assert result["errors"] == {"base": "auth_missing"} with ( @@ -466,7 +466,7 @@ async def test_user_websocket_auth_retry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Living Room (82GXARRS)" assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_NAME] == "Living Room" @@ -561,7 +561,7 @@ async def test_ssdp_legacy_not_remote_control_receiver_udn( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=data ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == RESULT_NOT_SUPPORTED @@ -607,7 +607,7 @@ async def test_ssdp_legacy_missing_auth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing" assert result["errors"] == {"base": "auth_missing"} @@ -616,7 +616,7 @@ async def test_ssdp_legacy_missing_auth(hass: HomeAssistant) -> None: result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "fake_model" assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_NAME] == "fake_model" @@ -636,7 +636,7 @@ async def test_ssdp_legacy_not_supported(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == RESULT_NOT_SUPPORTED @@ -768,7 +768,7 @@ async def test_ssdp_encrypted_websocket_not_supported( context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA_RENDERING_CONTROL_ST, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == RESULT_NOT_SUPPORTED @@ -1176,7 +1176,7 @@ async def test_autodetect_auth_missing(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing" assert result["errors"] == {"base": "auth_missing"} @@ -1191,7 +1191,7 @@ async def test_autodetect_auth_missing(hass: HomeAssistant) -> None: {}, ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == RESULT_CANNOT_CONNECT @@ -2042,7 +2042,7 @@ async def test_ssdp_update_mac(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY entry = result["result"] assert entry.data[CONF_MANUFACTURER] == DEFAULT_MANUFACTURER assert entry.data[CONF_MODEL] == "fake_model" @@ -2059,7 +2059,7 @@ async def test_ssdp_update_mac(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == RESULT_ALREADY_CONFIGURED # ensure mac wasn't updated with "none" @@ -2076,7 +2076,7 @@ async def test_ssdp_update_mac(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == RESULT_ALREADY_CONFIGURED # ensure mac was updated with new wifiMac value diff --git a/tests/components/schlage/test_config_flow.py b/tests/components/schlage/test_config_flow.py index 118ae44d15b..15ef3858c0c 100644 --- a/tests/components/schlage/test_config_flow.py +++ b/tests/components/schlage/test_config_flow.py @@ -22,7 +22,7 @@ async def test_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -35,7 +35,7 @@ async def test_form( await hass.async_block_till_done() mock_pyschlage_auth.authenticate.assert_called_once_with() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "test-username" assert result2["data"] == { "username": "test-username", @@ -60,7 +60,7 @@ async def test_form_invalid_auth( "password": "test-password", }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -79,7 +79,7 @@ async def test_form_unknown(hass: HomeAssistant, mock_pyschlage_auth: Mock) -> N }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -105,7 +105,7 @@ async def test_reauth( await hass.async_block_till_done() mock_pyschlage_auth.authenticate.assert_called_once_with() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert mock_added_config_entry.data == { "username": "asdf@asdf.com", @@ -138,7 +138,7 @@ async def test_reauth_invalid_auth( await hass.async_block_till_done() mock_pyschlage_auth.authenticate.assert_called_once_with() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -165,7 +165,7 @@ async def test_reauth_wrong_account( await hass.async_block_till_done() mock_pyschlage_auth.authenticate.assert_called_once_with() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "wrong_account" assert mock_added_config_entry.data == { "username": "asdf@asdf.com", diff --git a/tests/components/scrape/test_config_flow.py b/tests/components/scrape/test_config_flow.py index 8e281e148fc..17a527d2975 100644 --- a/tests/components/scrape/test_config_flow.py +++ b/tests/components/scrape/test_config_flow.py @@ -49,7 +49,7 @@ async def test_form( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.rest.RestData", @@ -75,7 +75,7 @@ async def test_form( ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["version"] == 1 assert result3["options"] == { CONF_RESOURCE: "https://www.home-assistant.io", @@ -106,7 +106,7 @@ async def test_form_with_post( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.rest.RestData", @@ -133,7 +133,7 @@ async def test_form_with_post( ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["version"] == 1 assert result3["options"] == { CONF_RESOURCE: "https://www.home-assistant.io", @@ -165,7 +165,7 @@ async def test_flow_fails( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER with patch( @@ -224,7 +224,7 @@ async def test_flow_fails( ) await hass.async_block_till_done() - assert result4["type"] == FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == "https://www.home-assistant.io" assert result4["options"] == { CONF_RESOURCE: "https://www.home-assistant.io", @@ -253,7 +253,7 @@ async def test_options_resource_flow( result = await hass.config_entries.options.async_init(loaded_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -261,7 +261,7 @@ async def test_options_resource_flow( {"next_step_id": "resource"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "resource" mocker = MockRestData("test_scrape_sensor2") @@ -280,7 +280,7 @@ async def test_options_resource_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_RESOURCE: "https://www.home-assistant.io", CONF_METHOD: "GET", @@ -319,7 +319,7 @@ async def test_options_add_remove_sensor_flow( result = await hass.config_entries.options.async_init(loaded_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -327,7 +327,7 @@ async def test_options_add_remove_sensor_flow( {"next_step_id": "add_sensor"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "add_sensor" mocker = MockRestData("test_scrape_sensor2") @@ -348,7 +348,7 @@ async def test_options_add_remove_sensor_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_RESOURCE: "https://www.home-assistant.io", CONF_METHOD: "GET", @@ -387,7 +387,7 @@ async def test_options_add_remove_sensor_flow( result = await hass.config_entries.options.async_init(loaded_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -395,7 +395,7 @@ async def test_options_add_remove_sensor_flow( {"next_step_id": "remove_sensor"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "remove_sensor" mocker = MockRestData("test_scrape_sensor2") @@ -408,7 +408,7 @@ async def test_options_add_remove_sensor_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_RESOURCE: "https://www.home-assistant.io", CONF_METHOD: "GET", @@ -445,7 +445,7 @@ async def test_options_edit_sensor_flow( result = await hass.config_entries.options.async_init(loaded_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -453,7 +453,7 @@ async def test_options_edit_sensor_flow( {"next_step_id": "select_edit_sensor"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_edit_sensor" result = await hass.config_entries.options.async_configure( @@ -461,7 +461,7 @@ async def test_options_edit_sensor_flow( {"index": "0"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "edit_sensor" mocker = MockRestData("test_scrape_sensor2") @@ -475,7 +475,7 @@ async def test_options_edit_sensor_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_RESOURCE: "https://www.home-assistant.io", CONF_METHOD: "GET", @@ -529,21 +529,21 @@ async def test_sensor_options_add_device_class( entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "select_edit_sensor"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_edit_sensor" result = await hass.config_entries.options.async_configure( result["flow_id"], {"index": "0"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "edit_sensor" result = await hass.config_entries.options.async_configure( @@ -559,7 +559,7 @@ async def test_sensor_options_add_device_class( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_RESOURCE: "https://www.home-assistant.io", CONF_METHOD: "GET", @@ -611,21 +611,21 @@ async def test_sensor_options_remove_device_class( entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "select_edit_sensor"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_edit_sensor" result = await hass.config_entries.options.async_configure( result["flow_id"], {"index": "0"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "edit_sensor" result = await hass.config_entries.options.async_configure( @@ -638,7 +638,7 @@ async def test_sensor_options_remove_device_class( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_RESOURCE: "https://www.home-assistant.io", CONF_METHOD: "GET", diff --git a/tests/components/season/test_config_flow.py b/tests/components/season/test_config_flow.py index e0a140f7136..2bc6a780c1b 100644 --- a/tests/components/season/test_config_flow.py +++ b/tests/components/season/test_config_flow.py @@ -20,7 +20,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( @@ -28,7 +28,7 @@ async def test_full_user_flow( user_input={CONF_TYPE: TYPE_ASTRONOMICAL}, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "Season" assert result2.get("data") == {CONF_TYPE: TYPE_ASTRONOMICAL} @@ -44,5 +44,5 @@ async def test_single_instance_allowed( DOMAIN, context={"source": SOURCE_USER}, data={CONF_TYPE: TYPE_ASTRONOMICAL} ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" diff --git a/tests/components/sensibo/test_config_flow.py b/tests/components/sensibo/test_config_flow.py index 3b1117f0908..e994402b09f 100644 --- a/tests/components/sensibo/test_config_flow.py +++ b/tests/components/sensibo/test_config_flow.py @@ -26,7 +26,7 @@ async def test_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -51,7 +51,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["version"] == 2 assert result2["data"] == { "api_key": "1234567890", @@ -78,7 +78,7 @@ async def test_flow_fails( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER with patch( @@ -115,7 +115,7 @@ async def test_flow_fails( }, ) - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Sensibo" assert result3["data"] == { "api_key": "1234567891", @@ -129,7 +129,7 @@ async def test_flow_get_no_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER with ( @@ -159,7 +159,7 @@ async def test_flow_get_no_username(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER with ( @@ -202,7 +202,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: data=entry.data, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -225,7 +225,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == {"api_key": "1234567891"} @@ -275,7 +275,7 @@ async def test_reauth_flow_error( await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": p_error} with ( @@ -298,7 +298,7 @@ async def test_reauth_flow_error( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == {"api_key": "1234567891"} @@ -348,7 +348,7 @@ async def test_flow_reauth_no_username_or_device( data=entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with ( @@ -370,5 +370,5 @@ async def test_flow_reauth_no_username_or_device( await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": p_error} diff --git a/tests/components/sensirion_ble/test_config_flow.py b/tests/components/sensirion_ble/test_config_flow.py index e93d060fd3e..00e92d37118 100644 --- a/tests/components/sensirion_ble/test_config_flow.py +++ b/tests/components/sensirion_ble/test_config_flow.py @@ -30,7 +30,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=SENSIRION_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( "homeassistant.components.sensirion_ble.async_setup_entry", return_value=True @@ -38,7 +38,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == CONFIGURED_NAME assert result2["result"].unique_id == SENSIRION_SERVICE_INFO.address @@ -50,7 +50,7 @@ async def test_async_step_bluetooth_not_sensirion(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_SENSIRION_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -60,7 +60,7 @@ async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -74,7 +74,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( "homeassistant.components.sensirion_ble.async_setup_entry", return_value=True @@ -83,7 +83,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: result["flow_id"], user_input={"address": SENSIRION_SERVICE_INFO.address}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == CONFIGURED_NAME assert result2["result"].unique_id == SENSIRION_SERVICE_INFO.address @@ -98,7 +98,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -114,7 +114,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - result["flow_id"], user_input={"address": SENSIRION_SERVICE_INFO.address}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -136,7 +136,7 @@ async def test_async_step_user_with_found_devices_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -153,7 +153,7 @@ async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant) - context={"source": config_entries.SOURCE_BLUETOOTH}, data=SENSIRION_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -164,7 +164,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=SENSIRION_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -172,7 +172,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=SENSIRION_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -185,7 +185,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=SENSIRION_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -196,7 +196,7 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.sensirion_ble.async_setup_entry", return_value=True @@ -205,7 +205,7 @@ async def test_async_step_user_takes_precedence_over_discovery( result["flow_id"], user_input={"address": SENSIRION_SERVICE_INFO.address}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == CONFIGURED_NAME assert result2["data"] == {} assert result2["result"].unique_id == SENSIRION_SERVICE_INFO.address diff --git a/tests/components/sensorpro/test_config_flow.py b/tests/components/sensorpro/test_config_flow.py index 1558e774f21..05be86d5209 100644 --- a/tests/components/sensorpro/test_config_flow.py +++ b/tests/components/sensorpro/test_config_flow.py @@ -19,7 +19,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=SENSORPRO_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( "homeassistant.components.sensorpro.async_setup_entry", return_value=True @@ -27,7 +27,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "T201 EEFF" assert result2["data"] == {} assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" @@ -40,7 +40,7 @@ async def test_async_step_bluetooth_not_sensorpro(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_SENSORPRO_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -50,7 +50,7 @@ async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -64,7 +64,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( "homeassistant.components.sensorpro.async_setup_entry", return_value=True @@ -73,7 +73,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "T201 EEFF" assert result2["data"] == {} assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" @@ -89,7 +89,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -105,7 +105,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -127,7 +127,7 @@ async def test_async_step_user_with_found_devices_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -144,7 +144,7 @@ async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant) - context={"source": config_entries.SOURCE_BLUETOOTH}, data=SENSORPRO_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -155,7 +155,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=SENSORPRO_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -163,7 +163,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=SENSORPRO_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -176,7 +176,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=SENSORPRO_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -187,7 +187,7 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.sensorpro.async_setup_entry", return_value=True @@ -196,7 +196,7 @@ async def test_async_step_user_takes_precedence_over_discovery( result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "T201 EEFF" assert result2["data"] == {} assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" diff --git a/tests/components/sensorpush/test_config_flow.py b/tests/components/sensorpush/test_config_flow.py index abbe04178c2..7e87dd1c6b8 100644 --- a/tests/components/sensorpush/test_config_flow.py +++ b/tests/components/sensorpush/test_config_flow.py @@ -19,7 +19,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=HTPWX_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( "homeassistant.components.sensorpush.async_setup_entry", return_value=True @@ -27,7 +27,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "HTP.xw F4D" assert result2["data"] == {} assert result2["result"].unique_id == "4125DDBA-2774-4851-9889-6AADDD4CAC3D" @@ -40,7 +40,7 @@ async def test_async_step_bluetooth_not_sensorpush(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_SENSOR_PUSH_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -50,7 +50,7 @@ async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -64,7 +64,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( "homeassistant.components.sensorpush.async_setup_entry", return_value=True @@ -73,7 +73,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: result["flow_id"], user_input={"address": "61DE521B-F0BF-9F44-64D4-75BBE1738105"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "HT.w 0CA1" assert result2["data"] == {} assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" @@ -89,7 +89,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -105,7 +105,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - result["flow_id"], user_input={"address": "61DE521B-F0BF-9F44-64D4-75BBE1738105"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -127,7 +127,7 @@ async def test_async_step_user_with_found_devices_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -144,7 +144,7 @@ async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant) - context={"source": config_entries.SOURCE_BLUETOOTH}, data=HTW_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -155,7 +155,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=HTW_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -163,7 +163,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=HTW_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -176,7 +176,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=HTW_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -187,7 +187,7 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.sensorpush.async_setup_entry", return_value=True @@ -196,7 +196,7 @@ async def test_async_step_user_takes_precedence_over_discovery( result["flow_id"], user_input={"address": "61DE521B-F0BF-9F44-64D4-75BBE1738105"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "HT.w 0CA1" assert result2["data"] == {} assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" diff --git a/tests/components/sentry/test_config_flow.py b/tests/components/sentry/test_config_flow.py index 0c3fc45b68b..f3136b639de 100644 --- a/tests/components/sentry/test_config_flow.py +++ b/tests/components/sentry/test_config_flow.py @@ -29,7 +29,7 @@ async def test_full_user_flow_implementation(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {} with ( @@ -61,7 +61,7 @@ async def test_integration_already_exists(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" @@ -80,7 +80,7 @@ async def test_user_flow_bad_dsn(hass: HomeAssistant) -> None: {"dsn": "foo"}, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("errors") == {"base": "bad_dsn"} @@ -99,7 +99,7 @@ async def test_user_flow_unknown_exception(hass: HomeAssistant) -> None: {"dsn": "foo"}, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("errors") == {"base": "unknown"} @@ -117,7 +117,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "init" result = await hass.config_entries.options.async_configure( @@ -134,7 +134,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: }, ) - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("data") == { CONF_ENVIRONMENT: "Test", CONF_EVENT_CUSTOM_COMPONENTS: True, diff --git a/tests/components/seventeentrack/test_config_flow.py b/tests/components/seventeentrack/test_config_flow.py index ae48fb6c792..380146ed276 100644 --- a/tests/components/seventeentrack/test_config_flow.py +++ b/tests/components/seventeentrack/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock from py17track.errors import SeventeenTrackError import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.seventeentrack import DOMAIN from homeassistant.components.seventeentrack.const import ( CONF_SHOW_ARCHIVED, @@ -38,7 +38,7 @@ async def test_create_entry( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -47,7 +47,7 @@ async def test_create_entry( ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "someemail@gmail.com" assert result2["data"] == { CONF_PASSWORD: "edc3eee7330e4fdda04489e3fbc283d0", @@ -97,7 +97,7 @@ async def test_flow_fails( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "someemail@gmail.com" assert result["data"] == { CONF_PASSWORD: "edc3eee7330e4fdda04489e3fbc283d0", @@ -113,7 +113,7 @@ async def test_import_flow(hass: HomeAssistant, mock_seventeentrack: AsyncMock) data=VALID_CONFIG_OLD, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "someemail@gmail.com" assert result["data"][CONF_USERNAME] == "someemail@gmail.com" assert result["data"][CONF_PASSWORD] == "edc3eee7330e4fdda04489e3fbc283d0" @@ -150,7 +150,7 @@ async def test_import_flow_cannot_connect_error( data=VALID_CONFIG_OLD, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == error @@ -170,7 +170,7 @@ async def test_option_flow(hass: HomeAssistant, mock_seventeentrack: AsyncMock) await hass.async_block_till_done() result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -178,7 +178,7 @@ async def test_option_flow(hass: HomeAssistant, mock_seventeentrack: AsyncMock) user_input={CONF_SHOW_ARCHIVED: True, CONF_SHOW_DELIVERED: False}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_SHOW_ARCHIVED] assert not result["data"][CONF_SHOW_DELIVERED] @@ -204,5 +204,5 @@ async def test_import_flow_already_configured( ) await hass.async_block_till_done() - assert result_aborted["type"] == data_entry_flow.FlowResultType.ABORT + assert result_aborted["type"] is FlowResultType.ABORT assert result_aborted["reason"] == "already_configured" diff --git a/tests/components/sfr_box/test_config_flow.py b/tests/components/sfr_box/test_config_flow.py index 282d7dbbb4c..08c12e9817b 100644 --- a/tests/components/sfr_box/test_config_flow.py +++ b/tests/components/sfr_box/test_config_flow.py @@ -7,11 +7,12 @@ import pytest from sfrbox_api.exceptions import SFRBoxAuthenticationError, SFRBoxError from sfrbox_api.models import SystemInfo -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.sfr_box.const import DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import load_fixture @@ -25,7 +26,7 @@ async def test_config_flow_skip_auth( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -39,7 +40,7 @@ async def test_config_flow_skip_auth( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} with patch( @@ -55,7 +56,7 @@ async def test_config_flow_skip_auth( }, ) - assert result["type"] == data_entry_flow.FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "choose_auth" result = await hass.config_entries.flow.async_configure( @@ -63,7 +64,7 @@ async def test_config_flow_skip_auth( {"next_step_id": "skip_auth"}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "SFR Box" assert result["data"] == {CONF_HOST: "192.168.0.1"} @@ -77,7 +78,7 @@ async def test_config_flow_with_auth( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -93,7 +94,7 @@ async def test_config_flow_with_auth( }, ) - assert result["type"] == data_entry_flow.FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "choose_auth" result = await hass.config_entries.flow.async_configure( @@ -113,7 +114,7 @@ async def test_config_flow_with_auth( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} with patch("homeassistant.components.sfr_box.config_flow.SFRBox.authenticate"): @@ -125,7 +126,7 @@ async def test_config_flow_with_auth( }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "SFR Box" assert result["data"] == { CONF_HOST: "192.168.0.1", @@ -146,7 +147,7 @@ async def test_config_flow_duplicate_host( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} system_info = SystemInfo(**json.loads(load_fixture("system_getInfo.json", DOMAIN))) @@ -163,7 +164,7 @@ async def test_config_flow_duplicate_host( }, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" await hass.async_block_till_done() @@ -180,7 +181,7 @@ async def test_config_flow_duplicate_mac( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} system_info = SystemInfo(**json.loads(load_fixture("system_getInfo.json", DOMAIN))) @@ -195,7 +196,7 @@ async def test_config_flow_duplicate_mac( }, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" await hass.async_block_till_done() @@ -216,7 +217,7 @@ async def test_reauth(hass: HomeAssistant, config_entry_with_auth: ConfigEntry) data=config_entry_with_auth.data, ) - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {} # Failed credentials @@ -232,7 +233,7 @@ async def test_reauth(hass: HomeAssistant, config_entry_with_auth: ConfigEntry) }, ) - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {"base": "invalid_auth"} # Valid credentials @@ -245,5 +246,5 @@ async def test_reauth(hass: HomeAssistant, config_entry_with_auth: ConfigEntry) }, ) - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "reauth_successful" diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index 1e7bbc01d6d..f2b0736f867 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -15,7 +15,7 @@ from aioshelly.exceptions import ( ) import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.shelly import config_flow from homeassistant.components.shelly.const import ( @@ -26,6 +26,7 @@ from homeassistant.components.shelly.const import ( from homeassistant.components.shelly.coordinator import ENTRY_RELOAD_COOLDOWN from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -74,7 +75,7 @@ async def test_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -102,7 +103,7 @@ async def test_form( ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Test name" assert result2["data"] == { "host": "1.1.1.1", @@ -123,7 +124,7 @@ async def test_form_gen1_custom_port( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -141,7 +142,7 @@ async def test_form_gen1_custom_port( {"host": "1.1.1.1", "port": "1100"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"]["base"] == "custom_port_not_supported" @@ -181,7 +182,7 @@ async def test_form_auth( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -193,7 +194,7 @@ async def test_form_auth( {"host": "1.1.1.1"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -210,7 +211,7 @@ async def test_form_auth( ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Test name" assert result3["data"] == { "host": "1.1.1.1", @@ -246,7 +247,7 @@ async def test_form_errors_get_info( {"host": "1.1.1.1"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": base_error} @@ -267,7 +268,7 @@ async def test_form_missing_model_key( {"host": "1.1.1.1"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "firmware_not_fully_provisioned"} @@ -278,7 +279,7 @@ async def test_form_missing_model_key_auth_enabled( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -290,14 +291,14 @@ async def test_form_missing_model_key_auth_enabled( {"host": "1.1.1.1"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result["errors"] == {} monkeypatch.setattr(mock_rpc_device, "shelly", {"gen": 2}) result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], {"password": "1234"} ) - assert result3["type"] == data_entry_flow.FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": "firmware_not_fully_provisioned"} @@ -317,14 +318,14 @@ async def test_form_missing_model_key_zeroconf( data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "firmware_not_fully_provisioned"} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "firmware_not_fully_provisioned"} @@ -357,7 +358,7 @@ async def test_form_errors_test_connection( {"host": "1.1.1.1"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": base_error} @@ -382,7 +383,7 @@ async def test_form_already_configured(hass: HomeAssistant) -> None: {"host": "1.1.1.1"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" # Test config entry got updated with latest IP @@ -424,7 +425,7 @@ async def test_user_setup_ignored_device( {"host": "1.1.1.1"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY # Test config entry got updated with latest IP assert entry.data["host"] == "1.1.1.1" @@ -447,7 +448,7 @@ async def test_form_firmware_unsupported(hass: HomeAssistant) -> None: {"host": "1.1.1.1"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "unsupported_firmware" @@ -484,7 +485,7 @@ async def test_form_auth_errors_test_connection_gen1( result2["flow_id"], {"username": "test username", "password": "test password"}, ) - assert result3["type"] == data_entry_flow.FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": base_error} @@ -520,7 +521,7 @@ async def test_form_auth_errors_test_connection_gen2( result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], {"password": "test password"} ) - assert result3["type"] == data_entry_flow.FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": base_error} @@ -562,7 +563,7 @@ async def test_zeroconf( data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} context = next( flow["context"] @@ -586,7 +587,7 @@ async def test_zeroconf( ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Test name" assert result2["data"] == { "host": "1.1.1.1", @@ -621,7 +622,7 @@ async def test_zeroconf_sleeping_device( data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} context = next( flow["context"] @@ -644,7 +645,7 @@ async def test_zeroconf_sleeping_device( ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Test name" assert result2["data"] == { "host": "1.1.1.1", @@ -678,7 +679,7 @@ async def test_zeroconf_sleeping_device_error(hass: HomeAssistant) -> None: data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -699,7 +700,7 @@ async def test_zeroconf_already_configured(hass: HomeAssistant) -> None: data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" # Test config entry got updated with latest IP @@ -726,7 +727,7 @@ async def test_zeroconf_ignored(hass: HomeAssistant) -> None: data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -749,7 +750,7 @@ async def test_zeroconf_with_wifi_ap_ip(hass: HomeAssistant) -> None: ), context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" # Test config entry was not updated with the wifi ap ip @@ -768,7 +769,7 @@ async def test_zeroconf_firmware_unsupported(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unsupported_firmware" @@ -783,7 +784,7 @@ async def test_zeroconf_cannot_connect(hass: HomeAssistant) -> None: data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -801,7 +802,7 @@ async def test_zeroconf_require_auth( data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -819,7 +820,7 @@ async def test_zeroconf_require_auth( ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Test name" assert result2["data"] == { "host": "1.1.1.1", @@ -865,7 +866,7 @@ async def test_reauth_successful( data=entry.data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -873,7 +874,7 @@ async def test_reauth_successful( user_input=user_input, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" @@ -914,7 +915,7 @@ async def test_reauth_unsuccessful( data=entry.data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -922,7 +923,7 @@ async def test_reauth_unsuccessful( user_input=user_input, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_unsuccessful" @@ -947,7 +948,7 @@ async def test_reauth_get_info_error(hass: HomeAssistant, error: Exception) -> N data=entry.data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -955,7 +956,7 @@ async def test_reauth_get_info_error(hass: HomeAssistant, error: Exception) -> N user_input={"password": "test2 password"}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_unsuccessful" @@ -1026,7 +1027,7 @@ async def test_options_flow_ble(hass: HomeAssistant, mock_rpc_device: Mock) -> N """Test setting ble options for gen2 devices.""" entry = await init_integration(hass, 2) result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] is None @@ -1038,11 +1039,11 @@ async def test_options_flow_ble(hass: HomeAssistant, mock_rpc_device: Mock) -> N ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_BLE_SCANNER_MODE] == BLEScannerMode.DISABLED result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] is None @@ -1054,11 +1055,11 @@ async def test_options_flow_ble(hass: HomeAssistant, mock_rpc_device: Mock) -> N ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_BLE_SCANNER_MODE] == BLEScannerMode.ACTIVE result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] is None @@ -1070,7 +1071,7 @@ async def test_options_flow_ble(hass: HomeAssistant, mock_rpc_device: Mock) -> N ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_BLE_SCANNER_MODE] == BLEScannerMode.PASSIVE await hass.config_entries.async_unload(entry.entry_id) @@ -1099,7 +1100,7 @@ async def test_zeroconf_already_configured_triggers_refresh_mac_in_name( data=DISCOVERY_INFO_WITH_MAC, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" monkeypatch.setattr(mock_rpc_device, "connected", False) @@ -1134,7 +1135,7 @@ async def test_zeroconf_already_configured_triggers_refresh( data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" monkeypatch.setattr(mock_rpc_device, "connected", False) @@ -1176,7 +1177,7 @@ async def test_zeroconf_sleeping_device_not_triggers_refresh( data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" monkeypatch.setattr(mock_rpc_device, "connected", False) @@ -1197,7 +1198,7 @@ async def test_sleeping_device_gen2_with_new_firmware( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( diff --git a/tests/components/shopping_list/test_config_flow.py b/tests/components/shopping_list/test_config_flow.py index 1d807e87ca2..4f6f5270c08 100644 --- a/tests/components/shopping_list/test_config_flow.py +++ b/tests/components/shopping_list/test_config_flow.py @@ -12,7 +12,7 @@ async def test_import(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data={} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_user(hass: HomeAssistant) -> None: @@ -22,7 +22,7 @@ async def test_user(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -33,7 +33,7 @@ async def test_user_confirm(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data={} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].data == {} @@ -43,6 +43,6 @@ async def test_onboarding_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": "onboarding"} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Shopping list" assert result["data"] == {} diff --git a/tests/components/sia/test_config_flow.py b/tests/components/sia/test_config_flow.py index 542c06da24f..c46a2ebbf46 100644 --- a/tests/components/sia/test_config_flow.py +++ b/tests/components/sia/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.sia.config_flow import ACCOUNT_SCHEMA, HUB_SCHEMA from homeassistant.components.sia.const import ( CONF_ACCOUNT, @@ -18,6 +18,7 @@ from homeassistant.components.sia.const import ( ) from homeassistant.const import CONF_PORT, CONF_PROTOCOL from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -164,9 +165,7 @@ async def test_form_start_account( async def test_create(hass: HomeAssistant, entry_with_basic_config) -> None: """Test we create a entry through the form.""" - assert ( - entry_with_basic_config["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - ) + assert entry_with_basic_config["type"] is FlowResultType.CREATE_ENTRY assert ( entry_with_basic_config["title"] == f"SIA Alarm on port {BASIC_CONFIG[CONF_PORT]}" @@ -179,10 +178,7 @@ async def test_create_additional_account( hass: HomeAssistant, entry_with_additional_account_config ) -> None: """Test we create a config with two accounts.""" - assert ( - entry_with_additional_account_config["type"] - == data_entry_flow.FlowResultType.CREATE_ENTRY - ) + assert entry_with_additional_account_config["type"] is FlowResultType.CREATE_ENTRY assert ( entry_with_additional_account_config["title"] == f"SIA Alarm on port {BASIC_CONFIG[CONF_PORT]}" @@ -322,7 +318,7 @@ async def test_options_basic(hass: HomeAssistant) -> None: ) await setup_sia(hass, config_entry) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options" assert result["last_step"] @@ -330,7 +326,7 @@ async def test_options_basic(hass: HomeAssistant) -> None: result["flow_id"], BASIC_OPTIONS ) await hass.async_block_till_done() - assert updated["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert updated["type"] is FlowResultType.CREATE_ENTRY assert updated["data"] == { CONF_ACCOUNTS: {BASIC_CONFIG[CONF_ACCOUNT]: BASIC_OPTIONS} } @@ -348,13 +344,13 @@ async def test_options_additional(hass: HomeAssistant) -> None: ) await setup_sia(hass, config_entry) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options" assert not result["last_step"] updated = await hass.config_entries.options.async_configure( result["flow_id"], BASIC_OPTIONS ) - assert updated["type"] == data_entry_flow.FlowResultType.FORM + assert updated["type"] is FlowResultType.FORM assert updated["step_id"] == "options" assert updated["last_step"] diff --git a/tests/components/simplepush/test_config_flow.py b/tests/components/simplepush/test_config_flow.py index 3905014747b..6718491f2a0 100644 --- a/tests/components/simplepush/test_config_flow.py +++ b/tests/components/simplepush/test_config_flow.py @@ -5,10 +5,11 @@ from unittest.mock import patch import pytest from simplepush import UnknownError -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.simplepush.const import CONF_DEVICE_KEY, CONF_SALT, DOMAIN from homeassistant.const import CONF_NAME, CONF_PASSWORD from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -44,7 +45,7 @@ async def test_flow_successful(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_CONFIG, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "simplepush" assert result["data"] == MOCK_CONFIG @@ -60,7 +61,7 @@ async def test_flow_with_password(hass: HomeAssistant) -> None: result["flow_id"], user_input=mock_config_pass, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "simplepush" assert result["data"] == mock_config_pass @@ -83,7 +84,7 @@ async def test_flow_user_device_key_already_configured(hass: HomeAssistant) -> N result["flow_id"], user_input=MOCK_CONFIG, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -108,7 +109,7 @@ async def test_flow_user_name_already_configured(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_CONFIG, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -126,5 +127,5 @@ async def test_error_on_connection_failure(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_CONFIG, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index af92833eb5b..dde7e37b891 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -27,12 +27,12 @@ async def test_duplicate_error( DOMAIN, context={"source": SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: VALID_AUTH_CODE} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -42,12 +42,12 @@ async def test_invalid_auth_code_length(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: "too_short_code"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_AUTH_CODE: "invalid_auth_code_length"} @@ -61,13 +61,13 @@ async def test_invalid_credentials(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: VALID_AUTH_CODE}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_AUTH_CODE: "invalid_auth"} @@ -79,14 +79,14 @@ async def test_options_flow(config_entry, hass: HomeAssistant) -> None: await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + 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_CODE: "4321"} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_CODE: "4321"} @@ -108,7 +108,7 @@ async def test_step_reauth(config_entry, hass: HomeAssistant, setup_simplisafe) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: VALID_AUTH_CODE} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 @@ -137,7 +137,7 @@ async def test_step_reauth_wrong_account( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: VALID_AUTH_CODE} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "wrong_account" @@ -178,7 +178,7 @@ async def test_step_user( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: auth_code} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY if log_statement: assert any(m for m in caplog.messages if log_statement in m) @@ -198,10 +198,10 @@ async def test_unknown_error(hass: HomeAssistant, setup_simplisafe) -> None: DOMAIN, context={"source": SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: VALID_AUTH_CODE} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "unknown"} diff --git a/tests/components/skybell/test_config_flow.py b/tests/components/skybell/test_config_flow.py index 535eb91f01b..cb62f808efc 100644 --- a/tests/components/skybell/test_config_flow.py +++ b/tests/components/skybell/test_config_flow.py @@ -33,7 +33,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -41,7 +41,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: user_input=CONF_DATA, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "user" assert result["data"] == CONF_DATA assert result["result"].unique_id == USER_ID @@ -59,7 +59,7 @@ async def test_flow_user_already_configured(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -69,7 +69,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant, skybell_mock) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -83,7 +83,7 @@ async def test_invalid_credentials(hass: HomeAssistant, skybell_mock) -> None: DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -94,7 +94,7 @@ async def test_flow_user_unknown_error(hass: HomeAssistant, skybell_mock) -> Non result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} @@ -114,14 +114,14 @@ async def test_step_reauth(hass: HomeAssistant) -> None: data=entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" @@ -140,7 +140,7 @@ async def test_step_reauth_failed(hass: HomeAssistant, skybell_mock) -> None: data=entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" skybell_mock.async_initialize.side_effect = ( @@ -151,7 +151,7 @@ async def test_step_reauth_failed(hass: HomeAssistant, skybell_mock) -> None: user_input={CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} skybell_mock.async_initialize.side_effect = None @@ -160,5 +160,5 @@ async def test_step_reauth_failed(hass: HomeAssistant, skybell_mock) -> None: result["flow_id"], user_input={CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" diff --git a/tests/components/slack/test_config_flow.py b/tests/components/slack/test_config_flow.py index c7b8d927c94..565b5ec2149 100644 --- a/tests/components/slack/test_config_flow.py +++ b/tests/components/slack/test_config_flow.py @@ -2,9 +2,10 @@ from unittest.mock import patch -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.slack.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import CONF_DATA, CONF_INPUT, TEAM_NAME, create_entry, mock_connection @@ -24,7 +25,7 @@ async def test_flow_user( result["flow_id"], user_input=CONF_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEAM_NAME assert result["data"] == CONF_DATA @@ -43,7 +44,7 @@ async def test_flow_user_already_configured( result["flow_id"], user_input=CONF_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -57,7 +58,7 @@ async def test_flow_user_invalid_auth( context={"source": config_entries.SOURCE_USER}, data=CONF_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -72,7 +73,7 @@ async def test_flow_user_cannot_connect( context={"source": config_entries.SOURCE_USER}, data=CONF_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -88,6 +89,6 @@ async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, data=CONF_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} diff --git a/tests/components/sleepiq/test_config_flow.py b/tests/components/sleepiq/test_config_flow.py index b623252cec4..fff483d2f15 100644 --- a/tests/components/sleepiq/test_config_flow.py +++ b/tests/components/sleepiq/test_config_flow.py @@ -5,10 +5,11 @@ from unittest.mock import AsyncMock, patch from asyncsleepiq import SleepIQLoginException, SleepIQTimeoutException import pytest -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, setup from homeassistant.components.sleepiq.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .conftest import SLEEPIQ_CONFIG, setup_platform @@ -49,7 +50,7 @@ async def test_show_set_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -70,7 +71,7 @@ async def test_login_failure(hass: HomeAssistant, side_effect, error) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=SLEEPIQ_CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": error} @@ -89,7 +90,7 @@ async def test_success(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"][CONF_USERNAME] == SLEEPIQ_CONFIG[CONF_USERNAME] assert result2["data"][CONF_PASSWORD] == SLEEPIQ_CONFIG[CONF_PASSWORD] assert len(mock_setup_entry.mock_calls) == 1 @@ -124,5 +125,5 @@ async def test_reauth_password(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" diff --git a/tests/components/slimproto/test_config_flow.py b/tests/components/slimproto/test_config_flow.py index 686768c6eb6..97da39517bd 100644 --- a/tests/components/slimproto/test_config_flow.py +++ b/tests/components/slimproto/test_config_flow.py @@ -16,7 +16,7 @@ async def test_full_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == DEFAULT_NAME assert result.get("data") == {} @@ -35,5 +35,5 @@ async def test_already_configured( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" diff --git a/tests/components/sma/test_config_flow.py b/tests/components/sma/test_config_flow.py index d73d8eb9728..93ac1783e09 100644 --- a/tests/components/sma/test_config_flow.py +++ b/tests/components/sma/test_config_flow.py @@ -22,7 +22,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -36,7 +36,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_USER_INPUT["host"] assert result["data"] == MOCK_USER_INPUT @@ -58,7 +58,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: MOCK_USER_INPUT, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} assert len(mock_setup_entry.mock_calls) == 0 @@ -78,7 +78,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: MOCK_USER_INPUT, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} assert len(mock_setup_entry.mock_calls) == 0 @@ -99,7 +99,7 @@ async def test_form_cannot_retrieve_device_info(hass: HomeAssistant) -> None: MOCK_USER_INPUT, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_retrieve_device_info"} assert len(mock_setup_entry.mock_calls) == 0 @@ -119,7 +119,7 @@ async def test_form_unexpected_exception(hass: HomeAssistant) -> None: MOCK_USER_INPUT, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "unknown"} assert len(mock_setup_entry.mock_calls) == 0 @@ -143,6 +143,6 @@ async def test_form_already_configured(hass: HomeAssistant, mock_config_entry) - MOCK_USER_INPUT, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/smappee/test_config_flow.py b/tests/components/smappee/test_config_flow.py index b5551c03c77..82f5baf952f 100644 --- a/tests/components/smappee/test_config_flow.py +++ b/tests/components/smappee/test_config_flow.py @@ -4,7 +4,7 @@ from http import HTTPStatus from ipaddress import ip_address from unittest.mock import patch -from homeassistant import data_entry_flow, setup +from homeassistant import setup from homeassistant.components import zeroconf from homeassistant.components.smappee.const import ( CONF_SERIALNUMBER, @@ -16,6 +16,7 @@ from homeassistant.components.smappee.const import ( from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow from tests.common import MockConfigEntry @@ -34,7 +35,7 @@ async def test_show_user_form(hass: HomeAssistant) -> None: ) assert result["step_id"] == "environment" - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM async def test_show_user_host_form(hass: HomeAssistant) -> None: @@ -44,14 +45,14 @@ async def test_show_user_host_form(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, ) assert result["step_id"] == "environment" - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"environment": ENV_LOCAL} ) assert result["step_id"] == ENV_LOCAL - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM async def test_show_zeroconf_connection_error_form(hass: HomeAssistant) -> None: @@ -72,14 +73,14 @@ async def test_show_zeroconf_connection_error_form(hass: HomeAssistant) -> None: ) assert result["description_placeholders"] == {CONF_SERIALNUMBER: "1006000212"} - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zeroconf_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.2.3.4"} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" assert len(hass.config_entries.async_entries(DOMAIN)) == 0 @@ -104,14 +105,14 @@ async def test_show_zeroconf_connection_error_form_next_generation( ) assert result["description_placeholders"] == {CONF_SERIALNUMBER: "5001000212"} - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zeroconf_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.2.3.4"} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" assert len(hass.config_entries.async_entries(DOMAIN)) == 0 @@ -127,19 +128,19 @@ async def test_connection_error(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, ) assert result["step_id"] == "environment" - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"environment": ENV_LOCAL} ) assert result["step_id"] == ENV_LOCAL - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.2.3.4"} ) assert result["reason"] == "cannot_connect" - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT async def test_user_local_connection_error(hass: HomeAssistant) -> None: @@ -156,19 +157,19 @@ async def test_user_local_connection_error(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, ) assert result["step_id"] == "environment" - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"environment": ENV_LOCAL} ) assert result["step_id"] == ENV_LOCAL - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.2.3.4"} ) assert result["reason"] == "cannot_connect" - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT async def test_zeroconf_wrong_mdns(hass: HomeAssistant) -> None: @@ -188,7 +189,7 @@ async def test_zeroconf_wrong_mdns(hass: HomeAssistant) -> None: ) assert result["reason"] == "invalid_mdns" - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT async def test_full_user_wrong_mdns(hass: HomeAssistant) -> None: @@ -212,18 +213,18 @@ async def test_full_user_wrong_mdns(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, ) assert result["step_id"] == "environment" - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"environment": ENV_LOCAL} ) assert result["step_id"] == ENV_LOCAL - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.2.3.4"} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_mdns" @@ -257,18 +258,18 @@ async def test_user_device_exists_abort(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, ) assert result["step_id"] == "environment" - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"environment": ENV_LOCAL} ) assert result["step_id"] == ENV_LOCAL - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.2.3.4"} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -312,7 +313,7 @@ async def test_zeroconf_device_exists_abort(hass: HomeAssistant) -> None: properties={"_raw": {}}, ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -333,7 +334,7 @@ async def test_cloud_device_exists_abort(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured_device" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -362,7 +363,7 @@ async def test_zeroconf_abort_if_cloud_device_exists(hass: HomeAssistant) -> Non properties={"_raw": {}}, ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured_device" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -396,7 +397,7 @@ async def test_zeroconf_confirm_abort_if_cloud_device_exists( result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured_device" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -421,7 +422,7 @@ async def test_abort_cloud_flow_if_local_device_exists(hass: HomeAssistant) -> N result["flow_id"], {"environment": ENV_CLOUD} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured_local_device" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -511,7 +512,7 @@ async def test_full_zeroconf_flow(hass: HomeAssistant) -> None: properties={"_raw": {}}, ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zeroconf_confirm" assert result["description_placeholders"] == {CONF_SERIALNUMBER: "1006000212"} @@ -519,7 +520,7 @@ async def test_full_zeroconf_flow(hass: HomeAssistant) -> None: result["flow_id"], {"host": "1.2.3.4"} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "smappee1006000212" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -549,7 +550,7 @@ async def test_full_user_local_flow(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, ) assert result["step_id"] == "environment" - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["description_placeholders"] is None result = await hass.config_entries.flow.async_configure( @@ -557,12 +558,12 @@ async def test_full_user_local_flow(hass: HomeAssistant) -> None: {"environment": ENV_LOCAL}, ) assert result["step_id"] == ENV_LOCAL - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.2.3.4"} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "smappee1006000212" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -596,7 +597,7 @@ async def test_full_zeroconf_flow_next_generation(hass: HomeAssistant) -> None: properties={"_raw": {}}, ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zeroconf_confirm" assert result["description_placeholders"] == {CONF_SERIALNUMBER: "5001000212"} @@ -604,7 +605,7 @@ async def test_full_zeroconf_flow_next_generation(hass: HomeAssistant) -> None: result["flow_id"], {"host": "1.2.3.4"} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "smappee5001000212" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 diff --git a/tests/components/smartthings/test_config_flow.py b/tests/components/smartthings/test_config_flow.py index e3dcf76bbaf..49444e47780 100644 --- a/tests/components/smartthings/test_config_flow.py +++ b/tests/components/smartthings/test_config_flow.py @@ -8,7 +8,7 @@ from aiohttp import ClientResponseError from pysmartthings import APIResponseError from pysmartthings.installedapp import format_install_url -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.smartthings import smartapp from homeassistant.components.smartthings.const import ( CONF_APP_ID, @@ -19,6 +19,7 @@ from homeassistant.components.smartthings.const import ( from homeassistant.config import async_process_ha_core_config from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -29,7 +30,7 @@ async def test_import_shows_user_step(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -56,7 +57,7 @@ async def test_entry_created( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -64,7 +65,7 @@ async def test_entry_created( # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -73,14 +74,14 @@ async def test_entry_created( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_location" # Select location and advance to external auth result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_LOCATION_ID: location.location_id} ) - assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["step_id"] == "authorize" assert result["url"] == format_install_url(app.app_id, location.location_id) @@ -89,7 +90,7 @@ async def test_entry_created( # Finish result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"]["app_id"] == app.app_id assert result["data"]["installed_app_id"] == installed_app_id assert result["data"]["location_id"] == location.location_id @@ -127,7 +128,7 @@ async def test_entry_created_from_update_event( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -135,7 +136,7 @@ async def test_entry_created_from_update_event( # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -144,14 +145,14 @@ async def test_entry_created_from_update_event( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_location" # Select location and advance to external auth result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_LOCATION_ID: location.location_id} ) - assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["step_id"] == "authorize" assert result["url"] == format_install_url(app.app_id, location.location_id) @@ -160,7 +161,7 @@ async def test_entry_created_from_update_event( # Finish result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"]["app_id"] == app.app_id assert result["data"]["installed_app_id"] == installed_app_id assert result["data"]["location_id"] == location.location_id @@ -199,7 +200,7 @@ async def test_entry_created_existing_app_new_oauth_client( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -207,7 +208,7 @@ async def test_entry_created_existing_app_new_oauth_client( # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -216,14 +217,14 @@ async def test_entry_created_existing_app_new_oauth_client( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_location" # Select location and advance to external auth result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_LOCATION_ID: location.location_id} ) - assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["step_id"] == "authorize" assert result["url"] == format_install_url(app.app_id, location.location_id) @@ -232,7 +233,7 @@ async def test_entry_created_existing_app_new_oauth_client( # Finish result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"]["app_id"] == app.app_id assert result["data"]["installed_app_id"] == installed_app_id assert result["data"]["location_id"] == location.location_id @@ -283,7 +284,7 @@ async def test_entry_created_existing_app_copies_oauth_client( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -291,7 +292,7 @@ async def test_entry_created_existing_app_copies_oauth_client( # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -302,14 +303,14 @@ async def test_entry_created_existing_app_copies_oauth_client( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_location" # Select location and advance to external auth result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_LOCATION_ID: location.location_id} ) - assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["step_id"] == "authorize" assert result["url"] == format_install_url(app.app_id, location.location_id) @@ -318,7 +319,7 @@ async def test_entry_created_existing_app_copies_oauth_client( # Finish result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"]["app_id"] == app.app_id assert result["data"]["installed_app_id"] == installed_app_id assert result["data"]["location_id"] == location.location_id @@ -377,7 +378,7 @@ async def test_entry_created_with_cloudhook( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -387,7 +388,7 @@ async def test_entry_created_with_cloudhook( # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -396,14 +397,14 @@ async def test_entry_created_with_cloudhook( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_location" # Select location and advance to external auth result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_LOCATION_ID: location.location_id} ) - assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["step_id"] == "authorize" assert result["url"] == format_install_url(app.app_id, location.location_id) @@ -412,7 +413,7 @@ async def test_entry_created_with_cloudhook( # Finish result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"]["app_id"] == app.app_id assert result["data"]["installed_app_id"] == installed_app_id assert result["data"]["location_id"] == location.location_id @@ -440,7 +441,7 @@ async def test_invalid_webhook_aborts(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_webhook_url" assert result["description_placeholders"][ "webhook_url" @@ -456,7 +457,7 @@ async def test_invalid_token_shows_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -464,7 +465,7 @@ async def test_invalid_token_shows_error(hass: HomeAssistant) -> None: # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -473,7 +474,7 @@ async def test_invalid_token_shows_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pat" assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token} assert result["errors"] == {CONF_ACCESS_TOKEN: "token_invalid_format"} @@ -495,7 +496,7 @@ async def test_unauthorized_token_shows_error( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -503,7 +504,7 @@ async def test_unauthorized_token_shows_error( # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -512,7 +513,7 @@ async def test_unauthorized_token_shows_error( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pat" assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token} assert result["errors"] == {CONF_ACCESS_TOKEN: "token_unauthorized"} @@ -534,7 +535,7 @@ async def test_forbidden_token_shows_error( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -542,7 +543,7 @@ async def test_forbidden_token_shows_error( # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -551,7 +552,7 @@ async def test_forbidden_token_shows_error( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pat" assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token} assert result["errors"] == {CONF_ACCESS_TOKEN: "token_forbidden"} @@ -579,7 +580,7 @@ async def test_webhook_problem_shows_error( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -587,7 +588,7 @@ async def test_webhook_problem_shows_error( # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -596,7 +597,7 @@ async def test_webhook_problem_shows_error( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pat" assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token} assert result["errors"] == {"base": "webhook_error"} @@ -621,7 +622,7 @@ async def test_api_error_shows_error(hass: HomeAssistant, smartthings_mock) -> N result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -629,7 +630,7 @@ async def test_api_error_shows_error(hass: HomeAssistant, smartthings_mock) -> N # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -638,7 +639,7 @@ async def test_api_error_shows_error(hass: HomeAssistant, smartthings_mock) -> N result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pat" assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token} assert result["errors"] == {"base": "app_setup_error"} @@ -661,7 +662,7 @@ async def test_unknown_response_error_shows_error( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -669,7 +670,7 @@ async def test_unknown_response_error_shows_error( # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -678,7 +679,7 @@ async def test_unknown_response_error_shows_error( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pat" assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token} assert result["errors"] == {"base": "app_setup_error"} @@ -695,7 +696,7 @@ async def test_unknown_error_shows_error(hass: HomeAssistant, smartthings_mock) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -703,7 +704,7 @@ async def test_unknown_error_shows_error(hass: HomeAssistant, smartthings_mock) # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -712,7 +713,7 @@ async def test_unknown_error_shows_error(hass: HomeAssistant, smartthings_mock) result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pat" assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token} assert result["errors"] == {"base": "app_setup_error"} @@ -737,7 +738,7 @@ async def test_no_available_locations_aborts( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -745,7 +746,7 @@ async def test_no_available_locations_aborts( # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -754,5 +755,5 @@ async def test_no_available_locations_aborts( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_available_locations" diff --git a/tests/components/smarttub/test_config_flow.py b/tests/components/smarttub/test_config_flow.py index df3695f31af..47204e2154e 100644 --- a/tests/components/smarttub/test_config_flow.py +++ b/tests/components/smarttub/test_config_flow.py @@ -4,10 +4,11 @@ from unittest.mock import patch from smarttub import LoginFailed -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.smarttub.const import DOMAIN 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 @@ -75,14 +76,14 @@ async def test_reauth_success(hass: HomeAssistant, smarttub_api, account) -> Non data=mock_entry.data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + 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: "test-email3", CONF_PASSWORD: "test-password3"} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert mock_entry.data[CONF_EMAIL] == "test-email3" assert mock_entry.data[CONF_PASSWORD] == "test-password3" @@ -116,12 +117,12 @@ async def test_reauth_wrong_account(hass: HomeAssistant, smarttub_api, account) data=mock_entry2.data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + 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: "test-email1", CONF_PASSWORD: "test-password1"} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/smhi/test_config_flow.py b/tests/components/smhi/test_config_flow.py index 7d8701eca45..a771bcc1e1d 100644 --- a/tests/components/smhi/test_config_flow.py +++ b/tests/components/smhi/test_config_flow.py @@ -26,7 +26,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -50,7 +50,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Home" assert result2["data"] == { "location": { @@ -86,7 +86,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result4["type"] == FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == "Weather 1.0 1.0" assert result4["data"] == { "location": { @@ -118,7 +118,7 @@ async def test_form_invalid_coordinates(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "wrong_location"} # Continue flow with new coordinates @@ -143,7 +143,7 @@ async def test_form_invalid_coordinates(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Weather 2.0 2.0" assert result3["data"] == { "location": { @@ -187,7 +187,7 @@ async def test_form_unique_id_exist(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -224,7 +224,7 @@ async def test_reconfigure_flow( "entry_id": entry.entry_id, }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.smhi.config_flow.Smhi.async_get_forecast", @@ -241,7 +241,7 @@ async def test_reconfigure_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "wrong_location"} with ( @@ -265,7 +265,7 @@ async def test_reconfigure_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" entry = hass.config_entries.async_get_entry(entry.entry_id) assert entry.title == "Home" diff --git a/tests/components/snapcast/test_config_flow.py b/tests/components/snapcast/test_config_flow.py index bb07eae2140..3bdba8b4c58 100644 --- a/tests/components/snapcast/test_config_flow.py +++ b/tests/components/snapcast/test_config_flow.py @@ -26,7 +26,7 @@ async def test_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -38,7 +38,7 @@ async def test_form( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_host"} @@ -50,7 +50,7 @@ async def test_form( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -60,7 +60,7 @@ async def test_form( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Snapcast" assert result["data"] == {CONF_HOST: "snapserver.test", CONF_PORT: 1705} assert len(mock_create_server.mock_calls) == 1 @@ -80,7 +80,7 @@ async def test_abort( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -91,5 +91,5 @@ async def test_abort( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/snooz/test_config_flow.py b/tests/components/snooz/test_config_flow.py index 172ca3cd143..209bd50512a 100644 --- a/tests/components/snooz/test_config_flow.py +++ b/tests/components/snooz/test_config_flow.py @@ -30,7 +30,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=SNOOZ_SERVICE_INFO_PAIRING, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" await _test_setup_entry(hass, result["flow_id"]) @@ -44,7 +44,7 @@ async def test_async_step_bluetooth_waits_to_pair(hass: HomeAssistant) -> None: data=SNOOZ_SERVICE_INFO_NOT_PAIRING, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" await _test_pairs(hass, result["flow_id"]) @@ -59,7 +59,7 @@ async def test_async_step_bluetooth_retries_pairing(hass: HomeAssistant) -> None data=SNOOZ_SERVICE_INFO_NOT_PAIRING, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" retry_id = await _test_pairs_timeout(hass, result["flow_id"]) @@ -73,7 +73,7 @@ async def test_async_step_bluetooth_not_snooz(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_SNOOZ_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -83,7 +83,7 @@ async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -97,7 +97,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["data_schema"] # ensure discovered devices are listed as options @@ -119,7 +119,7 @@ async def test_async_step_user_with_found_devices_waits_to_pair( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" await _test_pairs(hass, result["flow_id"], {CONF_NAME: TEST_SNOOZ_DISPLAY_NAME}) @@ -137,7 +137,7 @@ async def test_async_step_user_with_found_devices_retries_pairing( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" user_input = {CONF_NAME: TEST_SNOOZ_DISPLAY_NAME} @@ -156,7 +156,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -171,7 +171,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - result["flow_id"], user_input={CONF_NAME: TEST_SNOOZ_DISPLAY_NAME}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -194,7 +194,7 @@ async def test_async_step_user_with_found_devices_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -212,7 +212,7 @@ async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant) - context={"source": config_entries.SOURCE_BLUETOOTH}, data=SNOOZ_SERVICE_INFO_PAIRING, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -223,7 +223,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=SNOOZ_SERVICE_INFO_PAIRING, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -231,7 +231,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=SNOOZ_SERVICE_INFO_PAIRING, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -244,7 +244,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=SNOOZ_SERVICE_INFO_PAIRING, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -255,7 +255,7 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM await _test_setup_entry( hass, result["flow_id"], {CONF_NAME: TEST_SNOOZ_DISPLAY_NAME} @@ -286,7 +286,7 @@ async def _test_pairs( flow_id, user_input=user_input or {}, ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "wait_for_pairing_mode" pairing_mode_entered.set() @@ -305,12 +305,12 @@ async def _test_pairs_timeout( result = await hass.config_entries.flow.async_configure( flow_id, user_input=user_input or {} ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "wait_for_pairing_mode" await hass.async_block_till_done() result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pairing_timeout" return result2["flow_id"] @@ -325,7 +325,7 @@ async def _test_setup_entry( user_input=user_input or {}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_ADDRESS: TEST_ADDRESS, CONF_TOKEN: TEST_PAIRING_TOKEN, diff --git a/tests/components/solaredge/test_config_flow.py b/tests/components/solaredge/test_config_flow.py index 81b97c071fd..9ff605a871d 100644 --- a/tests/components/solaredge/test_config_flow.py +++ b/tests/components/solaredge/test_config_flow.py @@ -5,11 +5,11 @@ from unittest.mock import Mock, patch import pytest from requests.exceptions import ConnectTimeout, HTTPError -from homeassistant import data_entry_flow from homeassistant.components.solaredge.const import CONF_SITE_ID, DEFAULT_NAME, DOMAIN from homeassistant.config_entries import SOURCE_IGNORE, SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -32,7 +32,7 @@ async def test_user(hass: HomeAssistant, test_api: Mock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" # test with all provided @@ -41,7 +41,7 @@ async def test_user(hass: HomeAssistant, test_api: Mock) -> None: context={"source": SOURCE_USER}, data={CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}, ) - assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == "solaredge_site_1_2_3" data = result.get("data") @@ -63,7 +63,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant, test_api: str) -> Non context={"source": SOURCE_USER}, data={CONF_NAME: "test", CONF_SITE_ID: SITE_ID, CONF_API_KEY: "test"}, ) - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {CONF_SITE_ID: "already_configured"} @@ -83,7 +83,7 @@ async def test_ignored_entry_does_not_cause_error( context={"source": SOURCE_USER}, data={CONF_NAME: "test", CONF_SITE_ID: SITE_ID, CONF_API_KEY: "test"}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "test" data = result["data"] @@ -103,7 +103,7 @@ async def test_asserts(hass: HomeAssistant, test_api: Mock) -> None: context={"source": SOURCE_USER}, data={CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}, ) - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {CONF_SITE_ID: "site_not_active"} # test with api_failure @@ -113,7 +113,7 @@ async def test_asserts(hass: HomeAssistant, test_api: Mock) -> None: context={"source": SOURCE_USER}, data={CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}, ) - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {CONF_SITE_ID: "invalid_api_key"} # test with ConnectionTimeout @@ -123,7 +123,7 @@ async def test_asserts(hass: HomeAssistant, test_api: Mock) -> None: context={"source": SOURCE_USER}, data={CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}, ) - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {CONF_SITE_ID: "could_not_connect"} # test with HTTPError @@ -133,5 +133,5 @@ async def test_asserts(hass: HomeAssistant, test_api: Mock) -> None: context={"source": SOURCE_USER}, data={CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}, ) - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {CONF_SITE_ID: "could_not_connect"} diff --git a/tests/components/solarlog/test_config_flow.py b/tests/components/solarlog/test_config_flow.py index 16f25264b9d..4b840dd0cf9 100644 --- a/tests/components/solarlog/test_config_flow.py +++ b/tests/components/solarlog/test_config_flow.py @@ -4,11 +4,12 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.solarlog import config_flow from homeassistant.components.solarlog.const import DEFAULT_HOST, DOMAIN 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 @@ -68,12 +69,12 @@ async def test_user(hass: HomeAssistant, test_connect) -> None: flow = init_config_flow(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.FORM + 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}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "solarlog_test_1_2_3" assert result["data"][CONF_HOST] == HOST @@ -84,19 +85,19 @@ async def test_import(hass: HomeAssistant, test_connect) -> None: # import with only host result = await flow.async_step_import({CONF_HOST: HOST}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + 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}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + 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}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "solarlog_test_1_2_3" assert result["data"][CONF_HOST] == HOST @@ -112,19 +113,19 @@ async def test_abort_if_already_setup(hass: HomeAssistant, test_connect) -> None result = await flow.async_step_import( {CONF_HOST: HOST, CONF_NAME: "solarlog_test_7_8_9"} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" # Should fail, same HOST and NAME result = await flow.async_step_user({CONF_HOST: HOST, CONF_NAME: NAME}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_HOST: "already_configured"} # 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"} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "solarlog_test_7_8_9" assert result["data"][CONF_HOST] == "http://2.2.2.2" @@ -132,6 +133,6 @@ async def test_abort_if_already_setup(hass: HomeAssistant, test_connect) -> None result = await flow.async_step_import( {CONF_HOST: "http://2.2.2.2", CONF_NAME: NAME} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + 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/soma/test_config_flow.py b/tests/components/soma/test_config_flow.py index 04a93bb5a58..8b8548bfe3e 100644 --- a/tests/components/soma/test_config_flow.py +++ b/tests/components/soma/test_config_flow.py @@ -5,9 +5,9 @@ from unittest.mock import patch from api.soma_api import SomaApi from requests import RequestException -from homeassistant import data_entry_flow from homeassistant.components.soma import DOMAIN, config_flow from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -20,7 +20,7 @@ async def test_form(hass: HomeAssistant) -> None: flow = config_flow.SomaFlowHandler() flow.hass = hass result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM async def test_import_abort(hass: HomeAssistant) -> None: @@ -29,7 +29,7 @@ async def test_import_abort(hass: HomeAssistant) -> None: flow.hass = hass MockConfigEntry(domain=DOMAIN).add_to_hass(hass) result = await flow.async_step_import() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_setup" @@ -39,7 +39,7 @@ async def test_import_create(hass: HomeAssistant) -> None: flow.hass = hass with patch.object(SomaApi, "list_devices", return_value={"result": "success"}): result = await flow.async_step_import({"host": MOCK_HOST, "port": MOCK_PORT}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_error_status(hass: HomeAssistant) -> None: @@ -48,7 +48,7 @@ async def test_error_status(hass: HomeAssistant) -> None: flow.hass = hass with patch.object(SomaApi, "list_devices", return_value={"result": "error"}): result = await flow.async_step_import({"host": MOCK_HOST, "port": MOCK_PORT}) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "result_error" @@ -58,7 +58,7 @@ async def test_key_error(hass: HomeAssistant) -> None: flow.hass = hass with patch.object(SomaApi, "list_devices", return_value={}): result = await flow.async_step_import({"host": MOCK_HOST, "port": MOCK_PORT}) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "connection_error" @@ -68,7 +68,7 @@ async def test_exception(hass: HomeAssistant) -> None: flow.hass = hass with patch.object(SomaApi, "list_devices", side_effect=RequestException()): result = await flow.async_step_import({"host": MOCK_HOST, "port": MOCK_PORT}) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "connection_error" @@ -79,4 +79,4 @@ async def test_full_flow(hass: HomeAssistant) -> None: flow.hass = hass with patch.object(SomaApi, "list_devices", return_value={"result": "success"}): result = await flow.async_step_user({"host": MOCK_HOST, "port": MOCK_PORT}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY diff --git a/tests/components/somfy_mylink/test_config_flow.py b/tests/components/somfy_mylink/test_config_flow.py index a01f4d640a1..175fcd68477 100644 --- a/tests/components/somfy_mylink/test_config_flow.py +++ b/tests/components/somfy_mylink/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.components.somfy_mylink.const import ( CONF_REVERSED_TARGET_IDS, @@ -13,6 +13,7 @@ from homeassistant.components.somfy_mylink.const import ( ) from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -182,7 +183,7 @@ async def test_options_not_loaded(hass: HomeAssistant) -> None: ): result = await hass.config_entries.options.async_init(config_entry.entry_id) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT @pytest.mark.parametrize("reversed", [True, False]) @@ -211,7 +212,7 @@ async def test_options_with_targets(hass: HomeAssistant, reversed) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result2 = await hass.config_entries.options.async_configure( @@ -219,19 +220,19 @@ async def test_options_with_targets(hass: HomeAssistant, reversed) -> None: user_input={"target_id": "a"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM result3 = await hass.config_entries.options.async_configure( result2["flow_id"], user_input={"reverse": reversed}, ) - assert result3["type"] == data_entry_flow.FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM result4 = await hass.config_entries.options.async_configure( result3["flow_id"], user_input={"target_id": None}, ) - assert result4["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_REVERSED_TARGET_IDS: {"a": reversed}, diff --git a/tests/components/sonarr/test_config_flow.py b/tests/components/sonarr/test_config_flow.py index 3e48a4b25a8..6bd14e8b581 100644 --- a/tests/components/sonarr/test_config_flow.py +++ b/tests/components/sonarr/test_config_flow.py @@ -29,7 +29,7 @@ async def test_show_user_form(hass: HomeAssistant) -> None: ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM async def test_cannot_connect( @@ -45,7 +45,7 @@ async def test_cannot_connect( data=user_input, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -65,7 +65,7 @@ async def test_invalid_auth( data=user_input, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -83,7 +83,7 @@ async def test_unknown_error( data=user_input, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -106,14 +106,14 @@ async def test_full_reauth_flow_implementation( data=entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" user_input = MOCK_REAUTH_INPUT.copy() @@ -122,7 +122,7 @@ async def test_full_reauth_flow_implementation( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data[CONF_API_KEY] == "test-api-key-reauth" @@ -139,7 +139,7 @@ async def test_full_user_flow_implementation( context={CONF_SOURCE: SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" user_input = MOCK_USER_INPUT.copy() @@ -149,7 +149,7 @@ async def test_full_user_flow_implementation( user_input=user_input, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "192.168.1.189" assert result["data"] @@ -166,7 +166,7 @@ async def test_full_user_flow_advanced_options( DOMAIN, context={CONF_SOURCE: SOURCE_USER, "show_advanced_options": True} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" user_input = { @@ -179,7 +179,7 @@ async def test_full_user_flow_advanced_options( user_input=user_input, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "192.168.1.189" assert result["data"] @@ -201,7 +201,7 @@ async def test_options_flow( result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -210,6 +210,6 @@ async def test_options_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_UPCOMING_DAYS] == 2 assert result["data"][CONF_WANTED_MAX_ITEMS] == 100 diff --git a/tests/components/songpal/test_config_flow.py b/tests/components/songpal/test_config_flow.py index 338207a5d13..12c1ef3ec70 100644 --- a/tests/components/songpal/test_config_flow.py +++ b/tests/components/songpal/test_config_flow.py @@ -77,7 +77,7 @@ async def test_flow_ssdp(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == FRIENDLY_NAME assert result["data"] == CONF_DATA @@ -91,7 +91,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None _flow_next(hass, result["flow_id"]) @@ -100,7 +100,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: result["flow_id"], user_input={CONF_ENDPOINT: ENDPOINT}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MODEL assert result["data"] == { CONF_NAME: MODEL, @@ -119,7 +119,7 @@ async def test_flow_import(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=CONF_DATA ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == FRIENDLY_NAME assert result["data"] == CONF_DATA @@ -135,7 +135,7 @@ async def test_flow_import_without_name(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data={CONF_ENDPOINT: ENDPOINT} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MODEL assert result["data"] == {CONF_NAME: MODEL, CONF_ENDPOINT: ENDPOINT} @@ -163,7 +163,7 @@ async def test_ssdp_bravia(hass: HomeAssistant) -> None: context={"source": SOURCE_SSDP}, data=ssdp_data, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_songpal_device" @@ -175,7 +175,7 @@ async def test_sddp_exist(hass: HomeAssistant) -> None: context={"source": SOURCE_SSDP}, data=SSDP_DATA, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -188,7 +188,7 @@ async def test_user_exist(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" mocked_device.get_supported_methods.assert_called_once() @@ -204,7 +204,7 @@ async def test_import_exist(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=CONF_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" mocked_device.get_supported_methods.assert_called_once() @@ -220,7 +220,7 @@ async def test_user_invalid(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -237,7 +237,7 @@ async def test_import_invalid(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=CONF_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" mocked_device.get_supported_methods.assert_called_once() diff --git a/tests/components/soundtouch/test_config_flow.py b/tests/components/soundtouch/test_config_flow.py index bc7de5b7fda..264049ab5fc 100644 --- a/tests/components/soundtouch/test_config_flow.py +++ b/tests/components/soundtouch/test_config_flow.py @@ -26,7 +26,7 @@ async def test_user_flow_create_entry( context={CONF_SOURCE: SOURCE_USER}, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" with patch( @@ -41,7 +41,7 @@ async def test_user_flow_create_entry( assert len(mock_setup_entry.mock_calls) == 1 - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == DEVICE_1_NAME assert result.get("data") == { CONF_HOST: DEVICE_1_IP, @@ -65,7 +65,7 @@ async def test_user_flow_cannot_connect( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -92,7 +92,7 @@ async def test_zeroconf_flow_create_entry( ), ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "zeroconf_confirm" assert result.get("description_placeholders") == {"name": DEVICE_1_NAME} @@ -105,7 +105,7 @@ async def test_zeroconf_flow_create_entry( assert len(mock_setup_entry.mock_calls) == 1 - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == DEVICE_1_NAME assert result.get("data") == { CONF_HOST: DEVICE_1_IP, diff --git a/tests/components/speedtestdotnet/test_config_flow.py b/tests/components/speedtestdotnet/test_config_flow.py index f412c71a6ed..f509c91ad20 100644 --- a/tests/components/speedtestdotnet/test_config_flow.py +++ b/tests/components/speedtestdotnet/test_config_flow.py @@ -20,13 +20,13 @@ async def test_flow_works(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( speedtestdotnet.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_options(hass: HomeAssistant, mock_api: MagicMock) -> None: @@ -41,7 +41,7 @@ async def test_options(hass: HomeAssistant, mock_api: MagicMock) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -51,7 +51,7 @@ async def test_options(hass: HomeAssistant, mock_api: MagicMock) -> None: }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_SERVER_NAME: "Country1 - Sponsor1 - Server1", CONF_SERVER_ID: "1", @@ -60,7 +60,7 @@ async def test_options(hass: HomeAssistant, mock_api: MagicMock) -> None: # test setting server name to "*Auto Detect" result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -70,7 +70,7 @@ async def test_options(hass: HomeAssistant, mock_api: MagicMock) -> None: }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_SERVER_NAME: "*Auto Detect", CONF_SERVER_ID: None, @@ -86,5 +86,5 @@ async def test_integration_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( speedtestdotnet.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/spider/test_config_flow.py b/tests/components/spider/test_config_flow.py index 1fb61573216..69f97130f8c 100644 --- a/tests/components/spider/test_config_flow.py +++ b/tests/components/spider/test_config_flow.py @@ -4,10 +4,11 @@ from unittest.mock import Mock, patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.spider.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -34,7 +35,7 @@ async def test_user(hass: HomeAssistant, spider) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -50,7 +51,7 @@ async def test_user(hass: HomeAssistant, spider) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DOMAIN assert result["data"][CONF_USERNAME] == USERNAME assert result["data"][CONF_PASSWORD] == PASSWORD @@ -80,7 +81,7 @@ async def test_import(hass: HomeAssistant, spider) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DOMAIN assert result["data"][CONF_USERNAME] == USERNAME assert result["data"][CONF_PASSWORD] == PASSWORD @@ -99,7 +100,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant, spider) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=SPIDER_USER_DATA ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" # Should fail, config exist (flow) @@ -107,5 +108,5 @@ async def test_abort_if_already_setup(hass: HomeAssistant, spider) -> None: DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=SPIDER_USER_DATA ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/spotify/test_config_flow.py b/tests/components/spotify/test_config_flow.py index 1ab4e46bd55..6de549c8bc7 100644 --- a/tests/components/spotify/test_config_flow.py +++ b/tests/components/spotify/test_config_flow.py @@ -7,7 +7,6 @@ from unittest.mock import patch import pytest from spotipy import SpotifyException -from homeassistant import data_entry_flow from homeassistant.components import zeroconf from homeassistant.components.application_credentials import ( ClientCredential, @@ -16,6 +15,7 @@ from homeassistant.components.application_credentials import ( from homeassistant.components.spotify.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER, SOURCE_ZEROCONF 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 @@ -53,14 +53,14 @@ async def test_abort_if_no_configuration(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "missing_credentials" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=BLANK_ZEROCONF_INFO ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "missing_credentials" @@ -72,7 +72,7 @@ async def test_zeroconf_abort_if_existing_entry(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_ZEROCONF}, data=BLANK_ZEROCONF_INFO ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -96,7 +96,7 @@ async def test_full_flow( }, ) - assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["url"] == ( "https://accounts.spotify.com/authorize" "?response_type=code&client_id=client" @@ -181,7 +181,7 @@ async def test_abort_if_spotify_error( ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "connection_error" @@ -306,7 +306,7 @@ async def test_reauth_account_mismatch( spotify_mock.return_value.current_user.return_value = {"id": "fake_id"} result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_account_mismatch" @@ -316,5 +316,5 @@ async def test_abort_if_no_reauth_entry(hass: HomeAssistant) -> None: DOMAIN, context={"source": "reauth_confirm"} ) - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "reauth_account_mismatch" diff --git a/tests/components/sql/test_config_flow.py b/tests/components/sql/test_config_flow.py index 7b3b0aaf350..93cde0bccdd 100644 --- a/tests/components/sql/test_config_flow.py +++ b/tests/components/sql/test_config_flow.py @@ -42,7 +42,7 @@ async def test_form(recorder_mock: Recorder, hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -55,7 +55,7 @@ async def test_form(recorder_mock: Recorder, hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Get Value" assert result2["options"] == { "name": "Get Value", @@ -76,7 +76,7 @@ async def test_form_with_value_template( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -89,7 +89,7 @@ async def test_form_with_value_template( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Get Value" assert result2["options"] == { "name": "Get Value", @@ -107,7 +107,7 @@ async def test_flow_fails_db_url(recorder_mock: Recorder, hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result4["type"] == FlowResultType.FORM + assert result4["type"] is FlowResultType.FORM assert result4["step_id"] == config_entries.SOURCE_USER with patch( @@ -130,7 +130,7 @@ async def test_flow_fails_invalid_query( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result4["type"] == FlowResultType.FORM + assert result4["type"] is FlowResultType.FORM assert result4["step_id"] == config_entries.SOURCE_USER result5 = await hass.config_entries.flow.async_configure( @@ -138,7 +138,7 @@ async def test_flow_fails_invalid_query( user_input=ENTRY_CONFIG_INVALID_QUERY, ) - assert result5["type"] == FlowResultType.FORM + assert result5["type"] is FlowResultType.FORM assert result5["errors"] == { "query": "query_invalid", } @@ -148,7 +148,7 @@ async def test_flow_fails_invalid_query( user_input=ENTRY_CONFIG_INVALID_QUERY_2, ) - assert result6["type"] == FlowResultType.FORM + assert result6["type"] is FlowResultType.FORM assert result6["errors"] == { "query": "query_invalid", } @@ -158,7 +158,7 @@ async def test_flow_fails_invalid_query( user_input=ENTRY_CONFIG_INVALID_QUERY_3, ) - assert result6["type"] == FlowResultType.FORM + assert result6["type"] is FlowResultType.FORM assert result6["errors"] == { "query": "query_invalid", } @@ -168,7 +168,7 @@ async def test_flow_fails_invalid_query( user_input=ENTRY_CONFIG_QUERY_NO_READ_ONLY, ) - assert result5["type"] == FlowResultType.FORM + assert result5["type"] is FlowResultType.FORM assert result5["errors"] == { "query": "query_no_read_only", } @@ -178,7 +178,7 @@ async def test_flow_fails_invalid_query( user_input=ENTRY_CONFIG_QUERY_NO_READ_ONLY_CTE, ) - assert result6["type"] == FlowResultType.FORM + assert result6["type"] is FlowResultType.FORM assert result6["errors"] == { "query": "query_no_read_only", } @@ -188,7 +188,7 @@ async def test_flow_fails_invalid_query( user_input=ENTRY_CONFIG_MULTIPLE_QUERIES, ) - assert result6["type"] == FlowResultType.FORM + assert result6["type"] is FlowResultType.FORM assert result6["errors"] == { "query": "multiple_queries", } @@ -198,7 +198,7 @@ async def test_flow_fails_invalid_query( user_input=ENTRY_CONFIG_NO_RESULTS, ) - assert result5["type"] == FlowResultType.FORM + assert result5["type"] is FlowResultType.FORM assert result5["errors"] == { "query": "query_invalid", } @@ -208,7 +208,7 @@ async def test_flow_fails_invalid_query( user_input=ENTRY_CONFIG, ) - assert result5["type"] == FlowResultType.CREATE_ENTRY + assert result5["type"] is FlowResultType.CREATE_ENTRY assert result5["title"] == "Get Value" assert result5["options"] == { "name": "Get Value", @@ -228,7 +228,7 @@ async def test_flow_fails_invalid_column_name( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result4["type"] == FlowResultType.FORM + assert result4["type"] is FlowResultType.FORM assert result4["step_id"] == "user" result5 = await hass.config_entries.flow.async_configure( @@ -236,7 +236,7 @@ async def test_flow_fails_invalid_column_name( user_input=ENTRY_CONFIG_INVALID_COLUMN_NAME, ) - assert result5["type"] == FlowResultType.FORM + assert result5["type"] is FlowResultType.FORM assert result5["errors"] == { "column": "column_invalid", } @@ -246,7 +246,7 @@ async def test_flow_fails_invalid_column_name( user_input=ENTRY_CONFIG, ) - assert result5["type"] == FlowResultType.CREATE_ENTRY + assert result5["type"] is FlowResultType.CREATE_ENTRY assert result5["title"] == "Get Value" assert result5["options"] == { "name": "Get Value", @@ -284,7 +284,7 @@ async def test_options_flow(recorder_mock: Recorder, hass: HomeAssistant) -> Non result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -300,7 +300,7 @@ async def test_options_flow(recorder_mock: Recorder, hass: HomeAssistant) -> Non }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "name": "Get Value", "query": "SELECT 5 as size", @@ -334,7 +334,7 @@ async def test_options_flow_name_previously_removed( result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" with patch( @@ -353,7 +353,7 @@ async def test_options_flow_name_previously_removed( await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "name": "Get Value Title", "query": "SELECT 5 as size", @@ -436,7 +436,7 @@ async def test_options_flow_fails_invalid_query( user_input=ENTRY_CONFIG_INVALID_QUERY_OPT, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == { "query": "query_invalid", } @@ -446,7 +446,7 @@ async def test_options_flow_fails_invalid_query( user_input=ENTRY_CONFIG_INVALID_QUERY_2_OPT, ) - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == { "query": "query_invalid", } @@ -456,7 +456,7 @@ async def test_options_flow_fails_invalid_query( user_input=ENTRY_CONFIG_INVALID_QUERY_3_OPT, ) - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == { "query": "query_invalid", } @@ -466,7 +466,7 @@ async def test_options_flow_fails_invalid_query( user_input=ENTRY_CONFIG_QUERY_NO_READ_ONLY_OPT, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == { "query": "query_no_read_only", } @@ -476,7 +476,7 @@ async def test_options_flow_fails_invalid_query( user_input=ENTRY_CONFIG_QUERY_NO_READ_ONLY_CTE_OPT, ) - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == { "query": "query_no_read_only", } @@ -486,7 +486,7 @@ async def test_options_flow_fails_invalid_query( user_input=ENTRY_CONFIG_MULTIPLE_QUERIES_OPT, ) - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == { "query": "multiple_queries", } @@ -501,7 +501,7 @@ async def test_options_flow_fails_invalid_query( }, ) - assert result4["type"] == FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["data"] == { "name": "Get Value", "query": "SELECT 5 as size", @@ -540,7 +540,7 @@ async def test_options_flow_fails_invalid_column_name( user_input=ENTRY_CONFIG_INVALID_COLUMN_NAME_OPT, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == { "column": "column_invalid", } @@ -554,7 +554,7 @@ async def test_options_flow_fails_invalid_column_name( }, ) - assert result4["type"] == FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["data"] == { "name": "Get Value", "query": "SELECT 5 as value", @@ -589,7 +589,7 @@ async def test_options_flow_db_url_empty( result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" with ( @@ -611,7 +611,7 @@ async def test_options_flow_db_url_empty( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "name": "Get Value", "query": "SELECT 5 as size", @@ -627,7 +627,7 @@ async def test_full_flow_not_recorder_db( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -650,7 +650,7 @@ async def test_full_flow_not_recorder_db( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Get Value" assert result2["options"] == { "name": "Get Value", @@ -663,7 +663,7 @@ async def test_full_flow_not_recorder_db( result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" with ( @@ -686,7 +686,7 @@ async def test_full_flow_not_recorder_db( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "name": "Get Value", "db_url": "sqlite://path/to/db.db", @@ -711,7 +711,7 @@ async def test_full_flow_not_recorder_db( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "name": "Get Value", "db_url": "sqlite://path/to/db.db", @@ -745,7 +745,7 @@ async def test_device_state_class(recorder_mock: Recorder, hass: HomeAssistant) entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" with patch( @@ -764,7 +764,7 @@ async def test_device_state_class(recorder_mock: Recorder, hass: HomeAssistant) ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { "name": "Get Value", "query": "SELECT 5 as value", @@ -775,7 +775,7 @@ async def test_device_state_class(recorder_mock: Recorder, hass: HomeAssistant) } result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" with patch( @@ -792,7 +792,7 @@ async def test_device_state_class(recorder_mock: Recorder, hass: HomeAssistant) ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert "device_class" not in result3["data"] assert "state_class" not in result3["data"] assert result3["data"] == { diff --git a/tests/components/squeezebox/test_config_flow.py b/tests/components/squeezebox/test_config_flow.py index dc82b658163..0a03bcc291c 100644 --- a/tests/components/squeezebox/test_config_flow.py +++ b/tests/components/squeezebox/test_config_flow.py @@ -55,7 +55,7 @@ async def test_user_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "edit" assert CONF_HOST in result["data_schema"].schema for key in result["data_schema"].schema: @@ -73,7 +73,7 @@ async def test_user_form(hass: HomeAssistant) -> None: CONF_HTTPS: False, }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"] == { CONF_HOST: HOST, @@ -99,14 +99,14 @@ async def test_user_form_timeout(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "no_server_found"} # simulate manual input of host result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: HOST2} ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "edit" assert CONF_HOST in result2["data_schema"].schema for key in result2["data_schema"].schema: @@ -137,7 +137,7 @@ async def test_user_form_duplicate(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "no_server_found"} @@ -162,7 +162,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -186,7 +186,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -201,7 +201,7 @@ async def test_discovery(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={CONF_HOST: HOST, CONF_PORT: PORT, "uuid": UUID}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "edit" @@ -213,7 +213,7 @@ async def test_discovery_no_uuid(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={CONF_HOST: HOST, CONF_PORT: PORT, CONF_HTTPS: False}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "edit" @@ -238,7 +238,7 @@ async def test_dhcp_discovery(hass: HomeAssistant) -> None: hostname="any", ), ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "edit" @@ -260,7 +260,7 @@ async def test_dhcp_discovery_no_server_found(hass: HomeAssistant) -> None: hostname="any", ), ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -279,4 +279,4 @@ async def test_dhcp_discovery_existing_player(hass: HomeAssistant) -> None: hostname="any", ), ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT diff --git a/tests/components/srp_energy/test_config_flow.py b/tests/components/srp_energy/test_config_flow.py index 8d4904bf00d..bef1acab855 100644 --- a/tests/components/srp_energy/test_config_flow.py +++ b/tests/components/srp_energy/test_config_flow.py @@ -31,7 +31,7 @@ async def test_show_form( DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -44,7 +44,7 @@ async def test_show_form( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == ACCNT_NAME assert "data" in result @@ -74,7 +74,7 @@ async def test_form_invalid_account( flow_id=result["flow_id"], user_input=TEST_CONFIG_HOME ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_account"} @@ -93,7 +93,7 @@ async def test_form_invalid_auth( flow_id=result["flow_id"], user_input=TEST_CONFIG_HOME ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -112,7 +112,7 @@ async def test_form_unknown_error( flow_id=result["flow_id"], user_input=TEST_CONFIG_HOME ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -135,7 +135,7 @@ async def test_flow_entry_already_configured( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=user_input_second ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -156,7 +156,7 @@ async def test_flow_multiple_configs( ) # Verify created - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == ACCNT_NAME_2 assert "data" in result diff --git a/tests/components/starlink/test_config_flow.py b/tests/components/starlink/test_config_flow.py index 5b0e122ad5d..613e9b0fc7a 100644 --- a/tests/components/starlink/test_config_flow.py +++ b/tests/components/starlink/test_config_flow.py @@ -1,10 +1,11 @@ """Test the Starlink config flow.""" -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.starlink.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_IP_ADDRESS from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .patchers import DEVICE_FOUND_PATCHER, NO_DEVICE_PATCHER, SETUP_ENTRY_PATCHER @@ -27,7 +28,7 @@ async def test_flow_user_fails_can_succeed(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] with DEVICE_FOUND_PATCHER, SETUP_ENTRY_PATCHER: @@ -37,7 +38,7 @@ async def test_flow_user_fails_can_succeed(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == user_input @@ -57,7 +58,7 @@ async def test_flow_user_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == user_input @@ -85,5 +86,5 @@ async def test_flow_user_duplicate_abort(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/steam_online/test_config_flow.py b/tests/components/steam_online/test_config_flow.py index 00b47ea48bd..9292f58d231 100644 --- a/tests/components/steam_online/test_config_flow.py +++ b/tests/components/steam_online/test_config_flow.py @@ -4,11 +4,11 @@ from unittest.mock import patch import steam -from homeassistant import data_entry_flow from homeassistant.components.steam_online.const import CONF_ACCOUNTS, DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_SOURCE from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import entity_registry as er from . import ( @@ -42,7 +42,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == ACCOUNT_NAME_1 assert result["data"] == CONF_DATA assert result["options"] == CONF_OPTIONS @@ -56,7 +56,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "cannot_connect" @@ -68,7 +68,7 @@ async def test_flow_user_invalid_auth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "invalid_auth" @@ -79,7 +79,7 @@ async def test_flow_user_invalid_account(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "invalid_account" @@ -91,7 +91,7 @@ async def test_flow_user_unknown(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "unknown" @@ -104,7 +104,7 @@ async def test_flow_user_already_configured(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -121,20 +121,20 @@ async def test_flow_reauth(hass: HomeAssistant) -> None: }, data=CONF_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" new_conf = CONF_DATA | {CONF_API_KEY: "1234567890"} result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=new_conf, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data == new_conf @@ -153,7 +153,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -162,7 +162,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == CONF_OPTIONS_2 @@ -187,7 +187,7 @@ async def test_options_flow_deselect(hass: HomeAssistant) -> None: return_value=True, ), ): - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -196,7 +196,7 @@ async def test_options_flow_deselect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_ACCOUNTS: {}} assert len(er.async_get(hass).entities) == 0 @@ -208,7 +208,7 @@ async def test_options_flow_timeout(hass: HomeAssistant) -> None: servicemock.side_effect = steam.api.HTTPTimeoutError result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -217,7 +217,7 @@ async def test_options_flow_timeout(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == CONF_OPTIONS @@ -227,7 +227,7 @@ async def test_options_flow_unauthorized(hass: HomeAssistant) -> None: with patch_interface_private(): result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -236,5 +236,5 @@ async def test_options_flow_unauthorized(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == CONF_OPTIONS diff --git a/tests/components/steamist/test_config_flow.py b/tests/components/steamist/test_config_flow.py index 9480703af9f..6152721ed0a 100644 --- a/tests/components/steamist/test_config_flow.py +++ b/tests/components/steamist/test_config_flow.py @@ -42,7 +42,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -63,7 +63,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "127.0.0.1" assert result2["data"] == { "host": "127.0.0.1", @@ -76,7 +76,7 @@ async def test_form_with_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -97,7 +97,7 @@ async def test_form_with_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == DEVICE_NAME assert result2["data"] == DEFAULT_ENTRY_DATA assert result2["context"]["unique_id"] == FORMATTED_MAC_ADDRESS @@ -121,7 +121,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -142,7 +142,7 @@ async def test_form_unknown_exception(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -153,13 +153,13 @@ async def test_discovery(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -167,7 +167,7 @@ async def test_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -189,7 +189,7 @@ async def test_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == DEVICE_NAME assert result3["data"] == DEFAULT_ENTRY_DATA mock_setup.assert_called_once() @@ -199,7 +199,7 @@ async def test_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -207,7 +207,7 @@ async def test_discovery(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "no_devices_found" @@ -221,7 +221,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: data=DISCOVERY_30303, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_status(MOCK_ASYNC_GET_STATUS_INACTIVE): @@ -231,7 +231,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: data=DHCP_DISCOVERY, ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_in_progress" with _patch_discovery(), _patch_status(MOCK_ASYNC_GET_STATUS_INACTIVE): @@ -245,7 +245,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: ), ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "already_in_progress" @@ -260,7 +260,7 @@ async def test_discovered_by_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -274,7 +274,7 @@ async def test_discovered_by_discovery(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == DEFAULT_ENTRY_DATA assert mock_async_setup.called assert mock_async_setup_entry.called @@ -291,7 +291,7 @@ async def test_discovered_by_dhcp(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -305,7 +305,7 @@ async def test_discovered_by_dhcp(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == DEFAULT_ENTRY_DATA assert mock_async_setup.called assert mock_async_setup_entry.called @@ -325,7 +325,7 @@ async def test_discovered_by_dhcp_discovery_fails(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -345,7 +345,7 @@ async def test_discovered_by_dhcp_discovery_finds_non_steamist_device( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_steamist_device" @@ -374,7 +374,7 @@ async def test_discovered_by_dhcp_or_discovery_adds_missing_unique_id( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.unique_id == FORMATTED_MAC_ADDRESS @@ -409,7 +409,7 @@ async def test_discovered_by_dhcp_or_discovery_existing_unique_id_does_not_reloa ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert not mock_setup.called assert not mock_setup_entry.called diff --git a/tests/components/stookalert/test_config_flow.py b/tests/components/stookalert/test_config_flow.py index 9830022203a..3664527cbcf 100644 --- a/tests/components/stookalert/test_config_flow.py +++ b/tests/components/stookalert/test_config_flow.py @@ -16,7 +16,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" with patch( @@ -29,7 +29,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: }, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "Overijssel" assert result2.get("data") == { CONF_PROVINCE: "Overijssel", @@ -55,5 +55,5 @@ async def test_already_configured(hass: HomeAssistant) -> None: }, ) - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "already_configured" diff --git a/tests/components/stookwijzer/test_config_flow.py b/tests/components/stookwijzer/test_config_flow.py index 590c93bb3c1..732e8abfc98 100644 --- a/tests/components/stookwijzer/test_config_flow.py +++ b/tests/components/stookwijzer/test_config_flow.py @@ -15,7 +15,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" assert "flow_id" in result @@ -32,7 +32,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: }, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("data") == { "location": { "latitude": 1.0, diff --git a/tests/components/streamlabswater/test_config_flow.py b/tests/components/streamlabswater/test_config_flow.py index 4efe80b31e5..0cee3b8b088 100644 --- a/tests/components/streamlabswater/test_config_flow.py +++ b/tests/components/streamlabswater/test_config_flow.py @@ -16,7 +16,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch("homeassistant.components.streamlabswater.config_flow.StreamlabsClient"): @@ -26,7 +26,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + 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 @@ -49,7 +49,7 @@ async def test_form_cannot_connect( {CONF_API_KEY: "abc"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} with patch("homeassistant.components.streamlabswater.config_flow.StreamlabsClient"): @@ -59,7 +59,7 @@ async def test_form_cannot_connect( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + 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 @@ -80,7 +80,7 @@ async def test_form_unknown(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> {CONF_API_KEY: "abc"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "unknown"} with patch("homeassistant.components.streamlabswater.config_flow.StreamlabsClient"): @@ -90,7 +90,7 @@ async def test_form_unknown(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + 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 @@ -118,7 +118,7 @@ async def test_form_entry_already_exists(hass: HomeAssistant) -> None: {CONF_API_KEY: "abc"}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -132,7 +132,7 @@ async def test_import(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + 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 @@ -153,7 +153,7 @@ async def test_import_cannot_connect( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -170,7 +170,7 @@ async def test_import_unknown(hass: HomeAssistant, mock_setup_entry: AsyncMock) ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -190,5 +190,5 @@ async def test_import_entry_already_exists(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/suez_water/test_config_flow.py b/tests/components/suez_water/test_config_flow.py index a4ab52151d1..1d689ffe0d6 100644 --- a/tests/components/suez_water/test_config_flow.py +++ b/tests/components/suez_water/test_config_flow.py @@ -24,7 +24,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch("homeassistant.components.suez_water.config_flow.SuezClient"): @@ -34,7 +34,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "test-username" assert result["result"].unique_id == "test-username" assert result["data"] == MOCK_DATA @@ -64,7 +64,7 @@ async def test_form_invalid_auth( MOCK_DATA, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} with patch("homeassistant.components.suez_water.config_flow.SuezClient"): @@ -74,7 +74,7 @@ async def test_form_invalid_auth( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "test-username" assert result["result"].unique_id == "test-username" assert result["data"] == MOCK_DATA @@ -100,7 +100,7 @@ async def test_form_already_configured(hass: HomeAssistant) -> None: MOCK_DATA, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -124,7 +124,7 @@ async def test_form_error( MOCK_DATA, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": error} with patch( @@ -135,7 +135,7 @@ async def test_form_error( MOCK_DATA, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "test-username" assert result["data"] == MOCK_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -149,7 +149,7 @@ async def test_import(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "test-username" assert result["result"].unique_id == "test-username" assert result["data"] == MOCK_DATA @@ -175,7 +175,7 @@ async def test_import_error( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=MOCK_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == reason @@ -196,7 +196,7 @@ async def test_importing_invalid_auth(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=MOCK_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_auth" @@ -214,5 +214,5 @@ async def test_import_already_configured(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=MOCK_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/sun/test_config_flow.py b/tests/components/sun/test_config_flow.py index ef13595ed59..c5b5b29976c 100644 --- a/tests/components/sun/test_config_flow.py +++ b/tests/components/sun/test_config_flow.py @@ -18,7 +18,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" with patch( @@ -30,7 +30,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: user_input={}, ) - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == "Sun" assert result.get("data") == {} assert result.get("options") == {} @@ -51,7 +51,7 @@ async def test_single_instance_allowed( DOMAIN, context={"source": source} ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" @@ -65,7 +65,7 @@ async def test_import_flow( data={}, ) - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == "Sun" assert result.get("data") == {} assert result.get("options") == {} diff --git a/tests/components/sunweg/test_config_flow.py b/tests/components/sunweg/test_config_flow.py index 54ad4f3f234..3f250ebc994 100644 --- a/tests/components/sunweg/test_config_flow.py +++ b/tests/components/sunweg/test_config_flow.py @@ -4,10 +4,11 @@ from unittest.mock import patch from sunweg.api import APIHelper, SunWegApiError -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.sunweg.const import CONF_PLANT_ID, DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .common import SUNWEG_MOCK_ENTRY, SUNWEG_USER_INPUT @@ -20,7 +21,7 @@ async def test_show_authenticate_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -35,7 +36,7 @@ async def test_incorrect_login(hass: HomeAssistant) -> None: result["flow_id"], SUNWEG_USER_INPUT ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -53,7 +54,7 @@ async def test_server_unavailable(hass: HomeAssistant) -> None: result["flow_id"], SUNWEG_USER_INPUT ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "timeout_connect"} @@ -77,7 +78,7 @@ async def test_reauth(hass: HomeAssistant) -> None: data=mock_entry.data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch.object(APIHelper, "authenticate", return_value=False): @@ -86,7 +87,7 @@ async def test_reauth(hass: HomeAssistant) -> None: user_input=SUNWEG_USER_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "invalid_auth"} @@ -98,7 +99,7 @@ async def test_reauth(hass: HomeAssistant) -> None: user_input=SUNWEG_USER_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "timeout_connect"} @@ -108,7 +109,7 @@ async def test_reauth(hass: HomeAssistant) -> None: user_input=SUNWEG_USER_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" entries = hass.config_entries.async_entries() @@ -129,7 +130,7 @@ async def test_no_plants_on_account(hass: HomeAssistant) -> None: result["flow_id"], SUNWEG_USER_INPUT ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -160,7 +161,7 @@ async def test_multiple_plant_ids(hass: HomeAssistant, plant_fixture) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], SUNWEG_USER_INPUT ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "plant" result = await hass.config_entries.flow.async_configure( @@ -168,7 +169,7 @@ async def test_multiple_plant_ids(hass: HomeAssistant, plant_fixture) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_USERNAME] == SUNWEG_USER_INPUT[CONF_USERNAME] assert result["data"][CONF_PASSWORD] == SUNWEG_USER_INPUT[CONF_PASSWORD] assert result["data"][CONF_PLANT_ID] == 123456 @@ -192,7 +193,7 @@ async def test_one_plant_on_account(hass: HomeAssistant, plant_fixture) -> None: result["flow_id"], SUNWEG_USER_INPUT ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_USERNAME] == SUNWEG_USER_INPUT[CONF_USERNAME] assert result["data"][CONF_PASSWORD] == SUNWEG_USER_INPUT[CONF_PASSWORD] assert result["data"][CONF_PLANT_ID] == 123456 diff --git a/tests/components/surepetcare/test_config_flow.py b/tests/components/surepetcare/test_config_flow.py index 67ee5d81247..c8f77012502 100644 --- a/tests/components/surepetcare/test_config_flow.py +++ b/tests/components/surepetcare/test_config_flow.py @@ -23,7 +23,7 @@ async def test_form(hass: HomeAssistant, surepetcare: NonCallableMagicMock) -> N result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -39,7 +39,7 @@ async def test_form(hass: HomeAssistant, surepetcare: NonCallableMagicMock) -> N ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Sure Petcare" assert result2["data"] == { "username": "test-username", @@ -67,7 +67,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -89,7 +89,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -111,7 +111,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -142,7 +142,7 @@ async def test_flow_entry_already_exists( }, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/swiss_public_transport/test_config_flow.py b/tests/components/swiss_public_transport/test_config_flow.py index 9400423ff98..a18db9bf2de 100644 --- a/tests/components/swiss_public_transport/test_config_flow.py +++ b/tests/components/swiss_public_transport/test_config_flow.py @@ -124,7 +124,7 @@ async def test_flow_user_init_data_already_configured(hass: HomeAssistant) -> No user_input=MOCK_DATA_STEP, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -152,7 +152,7 @@ async def test_import( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == MOCK_DATA_IMPORT assert len(mock_setup_entry.mock_calls) == 1 @@ -179,7 +179,7 @@ async def test_import_error(hass: HomeAssistant, raise_error, text_error) -> Non ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == text_error @@ -199,5 +199,5 @@ async def test_import_already_configured(hass: HomeAssistant) -> None: data=MOCK_DATA_IMPORT, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/switch_as_x/test_config_flow.py b/tests/components/switch_as_x/test_config_flow.py index 59b7d7fadcd..206ae232d56 100644 --- a/tests/components/switch_as_x/test_config_flow.py +++ b/tests/components/switch_as_x/test_config_flow.py @@ -33,7 +33,7 @@ async def test_config_flow( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -46,7 +46,7 @@ async def test_config_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "ceiling" assert result["data"] == {} assert result["options"] == { @@ -91,7 +91,7 @@ async def test_config_flow_registered_entity( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -104,7 +104,7 @@ async def test_config_flow_registered_entity( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "ceiling" assert result["data"] == {} assert result["options"] == { @@ -158,7 +158,7 @@ async def test_options( assert config_entry result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema schema_key = next(k for k in schema if k == CONF_INVERT) @@ -170,7 +170,7 @@ async def test_options( CONF_INVERT: False, }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_ENTITY_ID: "switch.ceiling", CONF_INVERT: False, diff --git a/tests/components/switchbee/test_config_flow.py b/tests/components/switchbee/test_config_flow.py index 99c44365353..50ad6d22cd1 100644 --- a/tests/components/switchbee/test_config_flow.py +++ b/tests/components/switchbee/test_config_flow.py @@ -56,7 +56,7 @@ async def test_form(hass: HomeAssistant, test_cucode_in_coordinator_data) -> Non ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "1.1.1.1" assert result2["data"] == { CONF_HOST: "1.1.1.1", @@ -84,7 +84,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -108,7 +108,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -131,7 +131,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: }, ) - assert form_result["type"] == FlowResultType.FORM + assert form_result["type"] is FlowResultType.FORM assert form_result["errors"] == {"base": "unknown"} @@ -177,5 +177,5 @@ async def test_form_entry_exists(hass: HomeAssistant) -> None: }, ) - assert form_result["type"] == FlowResultType.ABORT + assert form_result["type"] is FlowResultType.ABORT assert form_result["reason"] == "already_configured" diff --git a/tests/components/switchbot/test_config_flow.py b/tests/components/switchbot/test_config_flow.py index 3d53dd2848e..a62a100f55a 100644 --- a/tests/components/switchbot/test_config_flow.py +++ b/tests/components/switchbot/test_config_flow.py @@ -46,7 +46,7 @@ async def test_bluetooth_discovery(hass: HomeAssistant) -> None: context={"source": SOURCE_BLUETOOTH}, data=WOHAND_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" with patch_async_setup_entry() as mock_setup_entry: @@ -56,7 +56,7 @@ async def test_bluetooth_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Bot EEFF" assert result["data"] == { CONF_ADDRESS: "AA:BB:CC:DD:EE:FF", @@ -73,7 +73,7 @@ async def test_bluetooth_discovery_requires_password(hass: HomeAssistant) -> Non context={"source": SOURCE_BLUETOOTH}, data=WOHAND_ENCRYPTED_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "password" with patch_async_setup_entry() as mock_setup_entry: @@ -83,7 +83,7 @@ async def test_bluetooth_discovery_requires_password(hass: HomeAssistant) -> Non ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Bot 923B" assert result["data"] == { CONF_ADDRESS: "798A8547-2A3D-C609-55FF-73FA824B923B", @@ -101,14 +101,14 @@ async def test_bluetooth_discovery_lock_key(hass: HomeAssistant) -> None: context={"source": SOURCE_BLUETOOTH}, data=WOLOCK_SERVICE_INFO, ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "lock_choose_method" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"next_step_id": "lock_key"} ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "lock_key" assert result["errors"] == {} @@ -125,7 +125,7 @@ async def test_bluetooth_discovery_lock_key(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "lock_key" assert result["errors"] == {"base": "encryption_key_invalid"} @@ -145,7 +145,7 @@ async def test_bluetooth_discovery_lock_key(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Lock EEFF" assert result["data"] == { CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", @@ -175,7 +175,7 @@ async def test_bluetooth_discovery_already_setup(hass: HomeAssistant) -> None: context={"source": SOURCE_BLUETOOTH}, data=WOHAND_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -186,7 +186,7 @@ async def test_async_step_bluetooth_not_switchbot(hass: HomeAssistant) -> None: context={"source": SOURCE_BLUETOOTH}, data=NOT_SWITCHBOT_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -197,7 +197,7 @@ async def test_async_step_bluetooth_not_connectable(hass: HomeAssistant) -> None context={"source": SOURCE_BLUETOOTH}, data=WOHAND_SERVICE_INFO_NOT_CONNECTABLE, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -211,7 +211,7 @@ async def test_user_setup_wohand(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] is None @@ -222,7 +222,7 @@ async def test_user_setup_wohand(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Bot EEFF" assert result["data"] == { CONF_ADDRESS: "AA:BB:CC:DD:EE:FF", @@ -252,7 +252,7 @@ async def test_user_setup_wohand_already_configured(hass: HomeAssistant) -> None result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -266,7 +266,7 @@ async def test_user_setup_wocurtain(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] is None @@ -277,7 +277,7 @@ async def test_user_setup_wocurtain(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Curtain EEFF" assert result["data"] == { CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", @@ -302,7 +302,7 @@ async def test_user_setup_wocurtain_or_bot(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -313,7 +313,7 @@ async def test_user_setup_wocurtain_or_bot(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Curtain EEFF" assert result["data"] == { CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", @@ -337,7 +337,7 @@ async def test_user_setup_wocurtain_or_bot_with_password(hass: HomeAssistant) -> result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -345,7 +345,7 @@ async def test_user_setup_wocurtain_or_bot_with_password(hass: HomeAssistant) -> result["flow_id"], {CONF_ADDRESS: "798A8547-2A3D-C609-55FF-73FA824B923B"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "password" assert result2["errors"] is None @@ -356,7 +356,7 @@ async def test_user_setup_wocurtain_or_bot_with_password(hass: HomeAssistant) -> ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Bot 923B" assert result3["data"] == { CONF_ADDRESS: "798A8547-2A3D-C609-55FF-73FA824B923B", @@ -377,7 +377,7 @@ async def test_user_setup_single_bot_with_password(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "password" assert result["errors"] is None @@ -388,7 +388,7 @@ async def test_user_setup_single_bot_with_password(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Bot 923B" assert result2["data"] == { CONF_ADDRESS: "798A8547-2A3D-C609-55FF-73FA824B923B", @@ -409,14 +409,14 @@ async def test_user_setup_wolock_key(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "lock_choose_method" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"next_step_id": "lock_key"} ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "lock_key" assert result["errors"] == {} @@ -433,7 +433,7 @@ async def test_user_setup_wolock_key(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "lock_key" assert result["errors"] == {"base": "encryption_key_invalid"} @@ -453,7 +453,7 @@ async def test_user_setup_wolock_key(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Lock EEFF" assert result["data"] == { CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", @@ -475,14 +475,14 @@ async def test_user_setup_wolock_auth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "lock_choose_method" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"next_step_id": "lock_auth"} ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "lock_auth" assert result["errors"] == {} @@ -498,7 +498,7 @@ async def test_user_setup_wolock_auth(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "lock_auth" assert result["errors"] == {"base": "auth_failed"} assert "error from api" in result["description_placeholders"]["error_detail"] @@ -526,7 +526,7 @@ async def test_user_setup_wolock_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Lock EEFF" assert result["data"] == { CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", @@ -548,14 +548,14 @@ async def test_user_setup_wolock_auth_switchbot_api_down(hass: HomeAssistant) -> result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "lock_choose_method" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"next_step_id": "lock_auth"} ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "lock_auth" assert result["errors"] == {} @@ -571,7 +571,7 @@ async def test_user_setup_wolock_auth_switchbot_api_down(hass: HomeAssistant) -> }, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -588,7 +588,7 @@ async def test_user_setup_wolock_or_bot(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -597,14 +597,14 @@ async def test_user_setup_wolock_or_bot(hass: HomeAssistant) -> None: USER_INPUT, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "lock_choose_method" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"next_step_id": "lock_key"} ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "lock_key" assert result["errors"] == {} @@ -624,7 +624,7 @@ async def test_user_setup_wolock_or_bot(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Lock EEFF" assert result["data"] == { CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", @@ -645,7 +645,7 @@ async def test_user_setup_wosensor(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] is None @@ -656,7 +656,7 @@ async def test_user_setup_wosensor(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Meter EEFF" assert result["data"] == { CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", @@ -675,7 +675,7 @@ async def test_user_no_devices(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -688,7 +688,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": SOURCE_BLUETOOTH}, data=WOCURTAIN_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" with patch( @@ -699,7 +699,7 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch_async_setup_entry() as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( @@ -707,7 +707,7 @@ async def test_async_step_user_takes_precedence_over_discovery( user_input={}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Curtain EEFF" assert result2["data"] == { CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", @@ -740,7 +740,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: entry = await init_integration(hass) result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] is None @@ -752,7 +752,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_RETRY_COUNT] == 3 assert len(mock_setup_entry.mock_calls) == 2 @@ -763,7 +763,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: entry = await init_integration(hass) result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] is None @@ -775,7 +775,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_RETRY_COUNT] == 6 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/switchbot_cloud/test_config_flow.py b/tests/components/switchbot_cloud/test_config_flow.py index 47758d50582..1d49b503ef2 100644 --- a/tests/components/switchbot_cloud/test_config_flow.py +++ b/tests/components/switchbot_cloud/test_config_flow.py @@ -32,7 +32,7 @@ async def _fill_out_form_and_assert_entry_created( ) await hass.async_block_till_done() - assert result_configure["type"] == FlowResultType.CREATE_ENTRY + assert result_configure["type"] is FlowResultType.CREATE_ENTRY assert result_configure["title"] == ENTRY_TITLE assert result_configure["data"] == { CONF_API_TOKEN: "test-token", @@ -46,7 +46,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: result_init = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result_init["type"] == FlowResultType.FORM + assert result_init["type"] is FlowResultType.FORM assert not result_init["errors"] await _fill_out_form_and_assert_entry_created( @@ -82,7 +82,7 @@ async def test_form_fails( }, ) - assert result_configure["type"] == FlowResultType.FORM + assert result_configure["type"] is FlowResultType.FORM assert result_configure["errors"] == {"base": message} await hass.async_block_till_done() diff --git a/tests/components/switcher_kis/test_config_flow.py b/tests/components/switcher_kis/test_config_flow.py index e03c8eb645f..913424abae5 100644 --- a/tests/components/switcher_kis/test_config_flow.py +++ b/tests/components/switcher_kis/test_config_flow.py @@ -23,7 +23,7 @@ async def test_import(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_IMPORT} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Switcher" assert result["data"] == {} @@ -51,7 +51,7 @@ async def test_user_setup(hass: HomeAssistant, mock_bridge) -> None: assert mock_bridge.is_running is False assert len(hass.data[DOMAIN][DATA_DISCOVERY].result()) == 2 - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] is None @@ -60,7 +60,7 @@ async def test_user_setup(hass: HomeAssistant, mock_bridge) -> None: ): result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Switcher" assert result2["result"].data == {} @@ -78,13 +78,13 @@ async def test_user_setup_abort_no_devices_found( assert mock_bridge.is_running is False assert len(hass.data[DOMAIN][DATA_DISCOVERY].result()) == 0 - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "no_devices_found" @@ -104,5 +104,5 @@ async def test_single_instance(hass: HomeAssistant, source) -> None: DOMAIN, context={"source": source} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/syncthing/test_config_flow.py b/tests/components/syncthing/test_config_flow.py index d97226e422c..82cbd85ffaa 100644 --- a/tests/components/syncthing/test_config_flow.py +++ b/tests/components/syncthing/test_config_flow.py @@ -4,10 +4,11 @@ from unittest.mock import patch from aiosyncthing.exceptions import UnauthorizedError -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.syncthing.const import DOMAIN from homeassistant.const import CONF_NAME, CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -30,7 +31,7 @@ async def test_show_setup_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "user" @@ -54,7 +55,7 @@ async def test_flow_successful(hass: HomeAssistant) -> None: CONF_VERIFY_SSL: VERIFY_SSL, }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "http://127.0.0.1:8384" assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_URL] == URL @@ -76,7 +77,7 @@ async def test_flow_already_configured(hass: HomeAssistant) -> None: data=MOCK_ENTRY, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -90,7 +91,7 @@ async def test_flow_invalid_auth(hass: HomeAssistant) -> None: data=MOCK_ENTRY, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"]["token"] == "invalid_auth" @@ -104,5 +105,5 @@ async def test_flow_cannot_connect(hass: HomeAssistant) -> None: data=MOCK_ENTRY, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == "cannot_connect" diff --git a/tests/components/syncthru/test_config_flow.py b/tests/components/syncthru/test_config_flow.py index 90470431ade..b79e63e1ce7 100644 --- a/tests/components/syncthru/test_config_flow.py +++ b/tests/components/syncthru/test_config_flow.py @@ -5,12 +5,13 @@ from unittest.mock import patch from pysyncthru import SyncThruAPINotSupported -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.components.syncthru.config_flow import SyncThru from homeassistant.components.syncthru.const import DOMAIN from homeassistant.const import CONF_NAME, CONF_URL from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker @@ -45,7 +46,7 @@ async def test_show_setup_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -69,7 +70,7 @@ async def test_already_configured_by_url( data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_URL] == FIXTURE_USER_INPUT[CONF_URL] assert result["data"][CONF_NAME] == FIXTURE_USER_INPUT[CONF_NAME] assert result["result"].unique_id == udn @@ -84,7 +85,7 @@ async def test_syncthru_not_supported(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {CONF_URL: "syncthru_not_supported"} @@ -101,7 +102,7 @@ async def test_unknown_state(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {CONF_URL: "unknown_state"} @@ -123,7 +124,7 @@ async def test_success( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_URL] == FIXTURE_USER_INPUT[CONF_URL] assert len(mock_setup_entry.mock_calls) == 1 @@ -151,7 +152,7 @@ async def test_ssdp(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -> ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert CONF_URL in result["data_schema"].schema for k in result["data_schema"].schema: diff --git a/tests/components/synology_dsm/test_config_flow.py b/tests/components/synology_dsm/test_config_flow.py index 67da3712983..483e22f2359 100644 --- a/tests/components/synology_dsm/test_config_flow.py +++ b/tests/components/synology_dsm/test_config_flow.py @@ -12,7 +12,6 @@ from synology_dsm.exceptions import ( SynologyDSMRequestException, ) -from homeassistant import data_entry_flow from homeassistant.components import ssdp, zeroconf from homeassistant.components.synology_dsm.config_flow import CONF_OTP_CODE from homeassistant.components.synology_dsm.const import ( @@ -46,6 +45,7 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .consts import ( DEVICE_TOKEN, @@ -154,7 +154,7 @@ async def test_user(hass: HomeAssistant, service: MagicMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=None ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -174,7 +174,7 @@ async def test_user(hass: HomeAssistant, service: MagicMock) -> None: CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST @@ -205,7 +205,7 @@ async def test_user(hass: HomeAssistant, service: MagicMock) -> None: CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL_2 assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST @@ -232,7 +232,7 @@ async def test_user_2sa(hass: HomeAssistant, service_2sa: MagicMock) -> None: context={"source": SOURCE_USER}, data={CONF_HOST: HOST, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "2sa" # Failed the first time because was too slow to enter the code @@ -242,7 +242,7 @@ async def test_user_2sa(hass: HomeAssistant, service_2sa: MagicMock) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_OTP_CODE: "000000"} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "2sa" assert result["errors"] == {CONF_OTP_CODE: "otp_failed"} @@ -258,7 +258,7 @@ async def test_user_2sa(hass: HomeAssistant, service_2sa: MagicMock) -> None: result["flow_id"], {CONF_OTP_CODE: "123456"} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST @@ -283,7 +283,7 @@ async def test_user_vdsm(hass: HomeAssistant, service_vdsm: MagicMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=None ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -303,7 +303,7 @@ async def test_user_vdsm(hass: HomeAssistant, service_vdsm: MagicMock) -> None: CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST @@ -350,7 +350,7 @@ async def test_reauth(hass: HomeAssistant, service: MagicMock) -> None: CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( @@ -364,7 +364,7 @@ async def test_reauth(hass: HomeAssistant, service: MagicMock) -> None: CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" @@ -396,7 +396,7 @@ async def test_reconfig_user(hass: HomeAssistant, service: MagicMock) -> None: context={"source": SOURCE_USER}, data={CONF_HOST: HOST, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" @@ -412,7 +412,7 @@ async def test_login_failed(hass: HomeAssistant, service: MagicMock) -> None: context={"source": SOURCE_USER}, data={CONF_HOST: HOST, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_USERNAME: "invalid_auth"} @@ -429,7 +429,7 @@ async def test_connection_failed(hass: HomeAssistant, service: MagicMock) -> Non data={CONF_HOST: HOST, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_HOST: "cannot_connect"} @@ -444,7 +444,7 @@ async def test_unknown_failed(hass: HomeAssistant, service: MagicMock) -> None: data={CONF_HOST: HOST, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "unknown"} @@ -462,7 +462,7 @@ async def test_missing_data_after_login( context={"source": SOURCE_USER}, data={CONF_HOST: HOST, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "missing_data"} @@ -483,7 +483,7 @@ async def test_form_ssdp(hass: HomeAssistant, service: MagicMock) -> None: }, ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {} @@ -495,7 +495,7 @@ async def test_form_ssdp(hass: HomeAssistant, service: MagicMock) -> None: result["flow_id"], {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == "mydsm" assert result["data"][CONF_HOST] == "192.168.1.5" @@ -539,7 +539,7 @@ async def test_reconfig_ssdp(hass: HomeAssistant, service: MagicMock) -> None: }, ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" @@ -582,7 +582,7 @@ async def test_skip_reconfig_ssdp( }, ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -615,7 +615,7 @@ async def test_existing_ssdp(hass: HomeAssistant, service: MagicMock) -> None: }, ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -637,7 +637,7 @@ async def test_options_flow(hass: HomeAssistant, service: MagicMock) -> None: assert config_entry.options == {} result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" # Scan interval @@ -646,7 +646,7 @@ async def test_options_flow(hass: HomeAssistant, service: MagicMock) -> None: result["flow_id"], user_input={}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options[CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL assert config_entry.options[CONF_TIMEOUT] == DEFAULT_TIMEOUT assert config_entry.options[CONF_SNAPSHOT_QUALITY] == DEFAULT_SNAPSHOT_QUALITY @@ -657,7 +657,7 @@ async def test_options_flow(hass: HomeAssistant, service: MagicMock) -> None: result["flow_id"], user_input={CONF_SCAN_INTERVAL: 2, CONF_TIMEOUT: 30, CONF_SNAPSHOT_QUALITY: 0}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options[CONF_SCAN_INTERVAL] == 2 assert config_entry.options[CONF_TIMEOUT] == 30 assert config_entry.options[CONF_SNAPSHOT_QUALITY] == 0 @@ -682,7 +682,7 @@ async def test_discovered_via_zeroconf(hass: HomeAssistant, service: MagicMock) }, ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {} @@ -694,7 +694,7 @@ async def test_discovered_via_zeroconf(hass: HomeAssistant, service: MagicMock) result["flow_id"], {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == "mydsm" assert result["data"][CONF_HOST] == "192.168.1.5" @@ -728,5 +728,5 @@ async def test_discovered_via_zeroconf_missing_mac( properties={}, ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_mac_address" diff --git a/tests/components/system_bridge/test_config_flow.py b/tests/components/system_bridge/test_config_flow.py index 0047cc62365..16a6f5d0f56 100644 --- a/tests/components/system_bridge/test_config_flow.py +++ b/tests/components/system_bridge/test_config_flow.py @@ -8,9 +8,10 @@ from systembridgeconnector.exceptions import ( ConnectionErrorException, ) -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.system_bridge.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import ( FIXTURE_AUTH_INPUT, @@ -32,7 +33,7 @@ async def test_show_user_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -42,7 +43,7 @@ async def test_user_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -67,7 +68,7 @@ async def test_user_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "test-bridge" assert result2["data"] == FIXTURE_USER_INPUT assert len(mock_setup_entry.mock_calls) == 1 @@ -79,7 +80,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -91,7 +92,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -102,7 +103,7 @@ async def test_form_connection_closed_cannot_connect(hass: HomeAssistant) -> Non DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -123,7 +124,7 @@ async def test_form_connection_closed_cannot_connect(hass: HomeAssistant) -> Non ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -134,7 +135,7 @@ async def test_form_timeout_cannot_connect(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -155,7 +156,7 @@ async def test_form_timeout_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -166,7 +167,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -187,7 +188,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_auth"} @@ -198,7 +199,7 @@ async def test_form_uuid_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -219,7 +220,7 @@ async def test_form_uuid_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -230,7 +231,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -251,7 +252,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "unknown"} @@ -262,7 +263,7 @@ async def test_reauth_authorization_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "authenticate" with ( @@ -283,7 +284,7 @@ async def test_reauth_authorization_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "authenticate" assert result2["errors"] == {"base": "invalid_auth"} @@ -294,7 +295,7 @@ async def test_reauth_connection_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "authenticate" with patch( @@ -306,7 +307,7 @@ async def test_reauth_connection_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "authenticate" assert result2["errors"] == {"base": "cannot_connect"} @@ -328,7 +329,7 @@ async def test_reauth_connection_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "authenticate" assert result3["errors"] == {"base": "cannot_connect"} @@ -339,7 +340,7 @@ async def test_reauth_connection_closed_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "authenticate" with ( @@ -360,7 +361,7 @@ async def test_reauth_connection_closed_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "authenticate" assert result2["errors"] == {"base": "cannot_connect"} @@ -376,7 +377,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "authenticate" with ( @@ -401,7 +402,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -414,7 +415,7 @@ async def test_zeroconf_flow(hass: HomeAssistant) -> None: data=FIXTURE_ZEROCONF, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] with ( @@ -439,7 +440,7 @@ async def test_zeroconf_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "1.1.1.1" assert result2["data"] == FIXTURE_ZEROCONF_INPUT assert len(mock_setup_entry.mock_calls) == 1 @@ -454,7 +455,7 @@ async def test_zeroconf_cannot_connect(hass: HomeAssistant) -> None: data=FIXTURE_ZEROCONF, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert not result["errors"] with patch( @@ -466,7 +467,7 @@ async def test_zeroconf_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "authenticate" assert result2["errors"] == {"base": "cannot_connect"} @@ -480,5 +481,5 @@ async def test_zeroconf_bad_zeroconf_info(hass: HomeAssistant) -> None: data=FIXTURE_ZEROCONF_BAD, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" diff --git a/tests/components/systemmonitor/test_config_flow.py b/tests/components/systemmonitor/test_config_flow.py index eb6f5778805..bd98099accc 100644 --- a/tests/components/systemmonitor/test_config_flow.py +++ b/tests/components/systemmonitor/test_config_flow.py @@ -25,7 +25,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -33,7 +33,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["options"] == {} assert len(mock_setup_entry.mock_calls) == 1 @@ -60,7 +60,7 @@ async def test_import( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["options"] == { "binary_sensor": {"process": ["systemd", "octave-cli"]}, "resources": [ @@ -99,7 +99,7 @@ async def test_form_already_configured( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -107,7 +107,7 @@ async def test_form_already_configured( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -147,7 +147,7 @@ async def test_import_already_configured( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" issue = issue_registry.async_get_issue( @@ -179,7 +179,7 @@ async def test_add_and_remove_processes( result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -190,7 +190,7 @@ async def test_add_and_remove_processes( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "binary_sensor": { CONF_PROCESS: ["systemd"], @@ -200,7 +200,7 @@ async def test_add_and_remove_processes( # Add another result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -211,7 +211,7 @@ async def test_add_and_remove_processes( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "binary_sensor": { CONF_PROCESS: ["systemd", "octave-cli"], @@ -230,7 +230,7 @@ async def test_add_and_remove_processes( # Remove one result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -241,7 +241,7 @@ async def test_add_and_remove_processes( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "binary_sensor": { CONF_PROCESS: ["systemd"], @@ -251,7 +251,7 @@ async def test_add_and_remove_processes( # Remove last result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -262,7 +262,7 @@ async def test_add_and_remove_processes( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "binary_sensor": {CONF_PROCESS: []}, } diff --git a/tests/components/tado/test_config_flow.py b/tests/components/tado/test_config_flow.py index c2bbe4f37de..c954a4b79af 100644 --- a/tests/components/tado/test_config_flow.py +++ b/tests/components/tado/test_config_flow.py @@ -56,7 +56,7 @@ async def test_form_exceptions( {"username": "test-username", "password": "test-password"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": error} # Test a retry to recover, upon failure @@ -78,7 +78,7 @@ async def test_form_exceptions( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "myhome" assert result["data"] == { "username": "test-username", @@ -95,7 +95,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -108,7 +108,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init( entry.entry_id, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -117,7 +117,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_FALLBACK: CONST_OVERLAY_TADO_DEFAULT} @@ -127,7 +127,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} mock_tado_api = _get_mock_tado_api(getMe={"homes": [{"id": 1, "name": "myhome"}]}) @@ -148,7 +148,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "myhome" assert result["data"] == { "username": "test-username", @@ -295,7 +295,7 @@ async def test_import_step(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "username": "test-username", "password": "test-password", @@ -331,7 +331,7 @@ async def test_import_step_existing_entry(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_setup_entry.call_count == 0 @@ -353,7 +353,7 @@ async def test_import_step_validation_failed(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "import_failed" @@ -374,7 +374,7 @@ async def test_import_step_device_authentication_failed(hass: HomeAssistant) -> ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "import_failed_invalid_auth" @@ -406,6 +406,6 @@ async def test_import_step_unique_id_configured(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_setup_entry.call_count == 0 diff --git a/tests/components/tailscale/test_config_flow.py b/tests/components/tailscale/test_config_flow.py index 5bf814a56d6..86daa40d8dc 100644 --- a/tests/components/tailscale/test_config_flow.py +++ b/tests/components/tailscale/test_config_flow.py @@ -23,7 +23,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( @@ -34,7 +34,7 @@ async def test_full_user_flow( }, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "homeassistant.github" assert result2.get("data") == { CONF_TAILNET: "homeassistant.github", @@ -59,7 +59,7 @@ async def test_full_flow_with_authentication_error( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" mock_tailscale_config_flow.devices.side_effect = TailscaleAuthenticationError @@ -71,7 +71,7 @@ async def test_full_flow_with_authentication_error( }, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "user" assert result2.get("errors") == {"base": "invalid_auth"} @@ -87,7 +87,7 @@ async def test_full_flow_with_authentication_error( }, ) - assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY assert result3.get("title") == "homeassistant.github" assert result3.get("data") == { CONF_TAILNET: "homeassistant.github", @@ -113,7 +113,7 @@ async def test_connection_error( }, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {"base": "cannot_connect"} assert len(mock_tailscale_config_flow.devices.mock_calls) == 1 @@ -137,7 +137,7 @@ async def test_reauth_flow( }, data=mock_config_entry.data, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" result2 = await hass.config_entries.flow.async_configure( @@ -146,7 +146,7 @@ async def test_reauth_flow( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "reauth_successful" assert mock_config_entry.data == { CONF_TAILNET: "homeassistant.github", @@ -179,7 +179,7 @@ async def test_reauth_with_authentication_error( }, data=mock_config_entry.data, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" mock_tailscale_config_flow.devices.side_effect = TailscaleAuthenticationError @@ -189,7 +189,7 @@ async def test_reauth_with_authentication_error( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "reauth_confirm" assert result2.get("errors") == {"base": "invalid_auth"} @@ -203,7 +203,7 @@ async def test_reauth_with_authentication_error( ) await hass.async_block_till_done() - assert result3.get("type") == FlowResultType.ABORT + assert result3.get("type") is FlowResultType.ABORT assert result3.get("reason") == "reauth_successful" assert mock_config_entry.data == { CONF_TAILNET: "homeassistant.github", @@ -231,7 +231,7 @@ async def test_reauth_api_error( }, data=mock_config_entry.data, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" mock_tailscale_config_flow.devices.side_effect = TailscaleConnectionError @@ -241,6 +241,6 @@ async def test_reauth_api_error( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "reauth_confirm" assert result2.get("errors") == {"base": "cannot_connect"} diff --git a/tests/components/tailwind/test_config_flow.py b/tests/components/tailwind/test_config_flow.py index efd828dcbde..f70ab6e27ff 100644 --- a/tests/components/tailwind/test_config_flow.py +++ b/tests/components/tailwind/test_config_flow.py @@ -40,7 +40,7 @@ async def test_user_flow( context={"source": SOURCE_USER}, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( @@ -51,7 +51,7 @@ async def test_user_flow( }, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2 == snapshot @@ -81,7 +81,7 @@ async def test_user_flow_errors( }, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" assert result.get("errors") == expected_error @@ -93,7 +93,7 @@ async def test_user_flow_errors( CONF_TOKEN: "123456", }, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY async def test_user_flow_unsupported_firmware_version( @@ -110,7 +110,7 @@ async def test_user_flow_unsupported_firmware_version( }, ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "unsupported_firmware" @@ -134,7 +134,7 @@ async def test_user_flow_already_configured( }, ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" assert mock_config_entry.data[CONF_HOST] == "127.0.0.1" assert mock_config_entry.data[CONF_TOKEN] == "987654" @@ -166,7 +166,7 @@ async def test_zeroconf_flow( ) assert result.get("step_id") == "zeroconf_confirm" - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM progress = hass.config_entries.flow.async_progress() assert len(progress) == 1 @@ -176,7 +176,7 @@ async def test_zeroconf_flow( result["flow_id"], user_input={CONF_TOKEN: "987654"} ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2 == snapshot @@ -205,7 +205,7 @@ async def test_zeroconf_flow_abort_incompatible_properties( ), ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == expected_reason @@ -252,7 +252,7 @@ async def test_zeroconf_flow_errors( }, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "zeroconf_confirm" assert result2.get("errors") == expected_error @@ -263,7 +263,7 @@ async def test_zeroconf_flow_errors( CONF_TOKEN: "123456", }, ) - assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY @pytest.mark.usefixtures("mock_tailwind") @@ -297,7 +297,7 @@ async def test_zeroconf_flow_not_discovered_again( ), ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" assert mock_config_entry.data[CONF_HOST] == "127.0.0.1" @@ -320,7 +320,7 @@ async def test_reauth_flow( }, data=mock_config_entry.data, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" result2 = await hass.config_entries.flow.async_configure( @@ -329,7 +329,7 @@ async def test_reauth_flow( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "reauth_successful" assert mock_config_entry.data[CONF_TOKEN] == "987654" @@ -371,7 +371,7 @@ async def test_reauth_flow_errors( }, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "reauth_confirm" assert result2.get("errors") == expected_error @@ -383,7 +383,7 @@ async def test_reauth_flow_errors( }, ) - assert result3.get("type") == FlowResultType.ABORT + assert result3.get("type") is FlowResultType.ABORT assert result3.get("reason") == "reauth_successful" @@ -405,7 +405,7 @@ async def test_dhcp_discovery_updates_entry( ), ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" assert mock_config_entry.data[CONF_HOST] == "127.0.0.1" @@ -425,5 +425,5 @@ async def test_dhcp_discovery_ignores_unknown(hass: HomeAssistant) -> None: ), ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "unknown" diff --git a/tests/components/tami4/test_config_flow.py b/tests/components/tami4/test_config_flow.py index 341e56bec84..cf81b015254 100644 --- a/tests/components/tami4/test_config_flow.py +++ b/tests/components/tami4/test_config_flow.py @@ -20,7 +20,7 @@ async def test_step_user_valid_number( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -28,7 +28,7 @@ async def test_step_user_valid_number( result["flow_id"], user_input={CONF_PHONE: "+972555555555"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "otp" assert result["errors"] == {} @@ -44,7 +44,7 @@ async def test_step_user_invalid_number( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -52,7 +52,7 @@ async def test_step_user_invalid_number( result["flow_id"], user_input={CONF_PHONE: "+275123"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_phone"} @@ -74,7 +74,7 @@ async def test_step_user_exception( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -82,7 +82,7 @@ async def test_step_user_exception( result["flow_id"], user_input={CONF_PHONE: "+972555555555"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": expected_error} @@ -99,7 +99,7 @@ async def test_step_otp_valid( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -107,7 +107,7 @@ async def test_step_otp_valid( result["flow_id"], user_input={CONF_PHONE: "+972555555555"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "otp" assert result["errors"] == {} @@ -115,7 +115,7 @@ async def test_step_otp_valid( result["flow_id"], user_input={"otp": "123456"}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Drink Water" assert "refresh_token" in result["data"] @@ -142,7 +142,7 @@ async def test_step_otp_exception( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -150,7 +150,7 @@ async def test_step_otp_exception( result["flow_id"], user_input={CONF_PHONE: "+972555555555"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "otp" assert result["errors"] == {} @@ -158,6 +158,6 @@ async def test_step_otp_exception( result["flow_id"], user_input={"otp": "123456"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "otp" assert result["errors"] == {"base": expected_error} diff --git a/tests/components/tankerkoenig/test_config_flow.py b/tests/components/tankerkoenig/test_config_flow.py index b954598c12a..b255491cb31 100644 --- a/tests/components/tankerkoenig/test_config_flow.py +++ b/tests/components/tankerkoenig/test_config_flow.py @@ -56,7 +56,7 @@ async def test_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -71,13 +71,13 @@ async def test_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_station" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_STATIONS_DATA ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_NAME] == "Home" assert result["data"][CONF_API_KEY] == "269534f6-xxxx-xxxx-xxxx-yyyyzzzzxxxx" assert result["data"][CONF_FUEL_TYPES] == ["e5"] @@ -107,14 +107,14 @@ async def test_user_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -123,7 +123,7 @@ async def test_exception_security(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -133,7 +133,7 @@ async def test_exception_security(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"][CONF_API_KEY] == "invalid_auth" @@ -143,7 +143,7 @@ async def test_user_no_stations(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -153,7 +153,7 @@ async def test_user_no_stations(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"][CONF_RADIUS] == "no_stations" @@ -176,7 +176,7 @@ async def test_reauth(hass: HomeAssistant, config_entry: MockConfigEntry) -> Non context={"source": SOURCE_REAUTH, "entry_id": config_entry.entry_id}, data=config_entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" # re-auth unsuccessful @@ -187,7 +187,7 @@ async def test_reauth(hass: HomeAssistant, config_entry: MockConfigEntry) -> Non CONF_API_KEY: "269534f6-aaaa-bbbb-cccc-yyyyzzzzxxxx", }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {CONF_API_KEY: "invalid_auth"} @@ -199,7 +199,7 @@ async def test_reauth(hass: HomeAssistant, config_entry: MockConfigEntry) -> Non CONF_API_KEY: "269534f6-aaaa-bbbb-cccc-yyyyzzzzxxxx", }, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" mock_setup_entry.assert_called() @@ -224,7 +224,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: return_value=NEARBY_STATIONS, ): result = await hass.config_entries.options.async_init(mock_config.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -234,7 +234,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: CONF_STATIONS: MOCK_OPTIONS_DATA[CONF_STATIONS], }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert not mock_config.options[CONF_SHOW_ON_MAP] @@ -254,7 +254,7 @@ async def test_options_flow_error(hass: HomeAssistant) -> None: side_effect=TankerkoenigInvalidKeyError("Booom!"), ) as mock_nearby_stations: result = await hass.config_entries.options.async_init(mock_config.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {"base": "invalid_auth"} @@ -267,5 +267,5 @@ async def test_options_flow_error(hass: HomeAssistant) -> None: CONF_STATIONS: MOCK_OPTIONS_DATA[CONF_STATIONS], }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert not mock_config.options[CONF_SHOW_ON_MAP] diff --git a/tests/components/tautulli/test_config_flow.py b/tests/components/tautulli/test_config_flow.py index e51fbfbad0d..b731067cd72 100644 --- a/tests/components/tautulli/test_config_flow.py +++ b/tests/components/tautulli/test_config_flow.py @@ -21,7 +21,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -32,7 +32,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == NAME assert result2["data"] == CONF_DATA @@ -44,7 +44,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "cannot_connect" @@ -55,7 +55,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == NAME assert result2["data"] == CONF_DATA @@ -67,7 +67,7 @@ async def test_flow_user_invalid_auth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "invalid_auth" @@ -78,7 +78,7 @@ async def test_flow_user_invalid_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == NAME assert result2["data"] == CONF_DATA @@ -90,7 +90,7 @@ async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "unknown" @@ -101,7 +101,7 @@ async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == NAME assert result2["data"] == CONF_DATA @@ -117,7 +117,7 @@ async def test_flow_user_already_configured(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, data=CONF_DATA, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -129,7 +129,7 @@ async def test_flow_user_multiple_entries_allowed(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -145,7 +145,7 @@ async def test_flow_user_multiple_entries_allowed(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == NAME assert result2["data"] == input @@ -165,7 +165,7 @@ async def test_flow_reauth( }, data=CONF_DATA, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -181,7 +181,7 @@ async def test_flow_reauth( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == CONF_DATA assert len(mock_entry.mock_calls) == 1 @@ -207,7 +207,7 @@ async def test_flow_reauth_error( result["flow_id"], user_input={CONF_API_KEY: "efgh"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"]["base"] == "invalid_auth" @@ -216,5 +216,5 @@ async def test_flow_reauth_error( result["flow_id"], user_input={CONF_API_KEY: "efgh"}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" diff --git a/tests/components/technove/test_config_flow.py b/tests/components/technove/test_config_flow.py index 72b9b358c89..81e0b32b55b 100644 --- a/tests/components/technove/test_config_flow.py +++ b/tests/components/technove/test_config_flow.py @@ -25,14 +25,14 @@ async def test_full_user_flow_implementation(hass: HomeAssistant) -> None: ) assert result.get("step_id") == "user" - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "192.168.1.123"} ) assert result.get("title") == "TechnoVE Station" - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert "data" in result assert result["data"][CONF_HOST] == "192.168.1.123" assert "result" in result @@ -53,7 +53,7 @@ async def test_user_device_exists_abort( data={CONF_HOST: "192.168.1.123"}, ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -66,7 +66,7 @@ async def test_connection_error(hass: HomeAssistant, mock_technove: MagicMock) - data={CONF_HOST: "example.com"}, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" assert result.get("errors") == {"base": "cannot_connect"} @@ -83,13 +83,13 @@ async def test_full_user_flow_with_error( ) assert result.get("step_id") == "user" - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "192.168.1.123"} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" assert result.get("errors") == {"base": "cannot_connect"} @@ -99,7 +99,7 @@ async def test_full_user_flow_with_error( ) assert result.get("title") == "TechnoVE Station" - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert "data" in result assert result["data"][CONF_HOST] == "192.168.1.123" assert "result" in result @@ -128,14 +128,14 @@ async def test_full_zeroconf_flow_implementation(hass: HomeAssistant) -> None: assert result.get("description_placeholders") == {CONF_NAME: "TechnoVE Station"} assert result.get("step_id") == "zeroconf_confirm" - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) assert result2.get("title") == "TechnoVE Station" - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert "data" in result2 assert result2["data"][CONF_HOST] == "192.168.1.123" @@ -165,7 +165,7 @@ async def test_zeroconf_during_onboarding( ) assert result.get("title") == "TechnoVE Station" - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("data") == {CONF_HOST: "192.168.1.123"} assert "result" in result @@ -195,7 +195,7 @@ async def test_zeroconf_connection_error( ), ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "cannot_connect" @@ -211,7 +211,7 @@ async def test_user_station_exists_abort( data={CONF_HOST: "192.168.1.123"}, ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -236,7 +236,7 @@ async def test_zeroconf_without_mac_station_exists_abort( ), ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -262,5 +262,5 @@ async def test_zeroconf_with_mac_station_exists_abort( ) mock_technove.update.assert_not_called() - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" diff --git a/tests/components/tedee/test_config_flow.py b/tests/components/tedee/test_config_flow.py index 6e8f02d04bc..1da1e392bf3 100644 --- a/tests/components/tedee/test_config_flow.py +++ b/tests/components/tedee/test_config_flow.py @@ -27,7 +27,7 @@ async def test_flow(hass: HomeAssistant, mock_tedee: MagicMock) -> None: DOMAIN, context={"source": SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -37,7 +37,7 @@ async def test_flow(hass: HomeAssistant, mock_tedee: MagicMock) -> None: }, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { CONF_HOST: "192.168.1.62", CONF_LOCAL_ACCESS_TOKEN: "token", @@ -56,7 +56,7 @@ async def test_flow_already_configured( DOMAIN, context={"source": SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -65,7 +65,7 @@ async def test_flow_already_configured( CONF_LOCAL_ACCESS_TOKEN: "token", }, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -91,7 +91,7 @@ async def test_config_flow_errors( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM mock_tedee.get_local_bridge.side_effect = side_effect @@ -103,7 +103,7 @@ async def test_config_flow_errors( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == error assert len(mock_tedee.get_local_bridge.mock_calls) == 1 @@ -134,5 +134,5 @@ async def test_reauth_flow( CONF_LOCAL_ACCESS_TOKEN: LOCAL_ACCESS_TOKEN, }, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" diff --git a/tests/components/tellduslive/test_config_flow.py b/tests/components/tellduslive/test_config_flow.py index 3cd157fd8b5..c575e7fb5c1 100644 --- a/tests/components/tellduslive/test_config_flow.py +++ b/tests/components/tellduslive/test_config_flow.py @@ -15,6 +15,7 @@ from homeassistant.components.tellduslive import ( from homeassistant.config_entries import SOURCE_DISCOVERY from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -63,12 +64,12 @@ async def test_abort_if_already_setup(hass: HomeAssistant) -> None: with patch.object(hass.config_entries, "async_entries", return_value=[{}]): result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_setup" with patch.object(hass.config_entries, "async_entries", return_value=[{}]): result = await flow.async_step_import(None) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_setup" @@ -77,16 +78,16 @@ async def test_full_flow_implementation(hass: HomeAssistant, mock_tellduslive) - flow = init_config_flow(hass) flow.context = {"source": SOURCE_DISCOVERY} result = await flow.async_step_discovery(["localhost", "tellstick"]) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert len(flow._hosts) == 2 result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await flow.async_step_user({"host": "localhost"}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["description_placeholders"] == { "auth_url": "https://example.com", @@ -94,7 +95,7 @@ async def test_full_flow_implementation(hass: HomeAssistant, mock_tellduslive) - } result = await flow.async_step_auth("") - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "localhost" assert result["data"]["host"] == "localhost" assert result["data"]["scan_interval"] == 60 @@ -106,7 +107,7 @@ async def test_step_import(hass: HomeAssistant, mock_tellduslive) -> None: flow = init_config_flow(hass) result = await flow.async_step_import({CONF_HOST: DOMAIN, KEY_SCAN_INTERVAL: 0}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" @@ -117,7 +118,7 @@ async def test_step_import_add_host(hass: HomeAssistant, mock_tellduslive) -> No result = await flow.async_step_import( {CONF_HOST: "localhost", KEY_SCAN_INTERVAL: 0} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -130,7 +131,7 @@ async def test_step_import_no_config_file( result = await flow.async_step_import( {CONF_HOST: "localhost", KEY_SCAN_INTERVAL: 0} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -150,7 +151,7 @@ async def test_step_import_load_json_matching_host( result = await flow.async_step_import( {CONF_HOST: "Cloud API", KEY_SCAN_INTERVAL: 0} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -168,7 +169,7 @@ async def test_step_import_load_json(hass: HomeAssistant, mock_tellduslive) -> N result = await flow.async_step_import( {CONF_HOST: "localhost", KEY_SCAN_INTERVAL: SCAN_INTERVAL} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "localhost" assert result["data"]["host"] == "localhost" assert result["data"]["scan_interval"] == 60 @@ -182,7 +183,7 @@ async def test_step_disco_no_local_api(hass: HomeAssistant, mock_tellduslive) -> flow.context = {"source": SOURCE_DISCOVERY} result = await flow.async_step_discovery(["localhost", "tellstick"]) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert len(flow._hosts) == 1 @@ -193,7 +194,7 @@ async def test_step_auth(hass: HomeAssistant, mock_tellduslive) -> None: await flow.async_step_auth() result = await flow.async_step_auth(["localhost", "tellstick"]) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Cloud API" assert result["data"]["host"] == "Cloud API" assert result["data"]["scan_interval"] == 60 @@ -212,7 +213,7 @@ async def test_wrong_auth_flow_implementation( await flow.async_step_auth() result = await flow.async_step_auth("") - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"]["base"] == "invalid_auth" @@ -222,7 +223,7 @@ async def test_not_pick_host_if_only_one(hass: HomeAssistant, mock_tellduslive) flow = init_config_flow(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" @@ -233,7 +234,7 @@ async def test_abort_if_timeout_generating_auth_url( flow = init_config_flow(hass, side_effect=TimeoutError) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "authorize_url_timeout" @@ -243,7 +244,7 @@ async def test_abort_no_auth_url(hass: HomeAssistant, mock_tellduslive) -> None: flow._get_auth_url = Mock(return_value=False) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown_authorize_url_generation" @@ -254,7 +255,7 @@ async def test_abort_if_exception_generating_auth_url( flow = init_config_flow(hass, side_effect=ValueError) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown_authorize_url_generation" diff --git a/tests/components/template/test_config_flow.py b/tests/components/template/test_config_flow.py index 0a34dff9776..59a2c4f38a3 100644 --- a/tests/components/template/test_config_flow.py +++ b/tests/components/template/test_config_flow.py @@ -72,14 +72,14 @@ async def test_config_flow( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.MENU + 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"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == template_type with patch( @@ -95,7 +95,7 @@ async def test_config_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "My template" assert result["data"] == {} assert result["options"] == { @@ -203,7 +203,7 @@ async def test_options( config_entry = hass.config_entries.async_entries(DOMAIN)[0] result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == template_type assert get_suggested(result["data_schema"].schema, "state") == old_state_template assert "name" not in result["data_schema"].schema @@ -212,7 +212,7 @@ async def test_options( result["flow_id"], user_input={"state": new_state_template, **options_options}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "name": "My template", "state": new_state_template, @@ -237,14 +237,14 @@ async def test_options( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.MENU + 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"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == template_type assert get_suggested(result["data_schema"].schema, "name") is None @@ -301,14 +301,14 @@ async def test_config_flow_preview( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.MENU + 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"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == template_type assert result["errors"] is None assert result["preview"] == "template" @@ -442,14 +442,14 @@ async def test_config_flow_preview_bad_input( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.MENU + 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"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == template_type assert result["errors"] is None assert result["preview"] == "template" @@ -512,14 +512,14 @@ async def test_config_flow_preview_template_startup_error( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.MENU + 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"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == template_type assert result["errors"] is None assert result["preview"] == "template" @@ -595,14 +595,14 @@ async def test_config_flow_preview_template_error( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.MENU + 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"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == template_type assert result["errors"] is None assert result["preview"] == "template" @@ -665,14 +665,14 @@ async def test_config_flow_preview_bad_state( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.MENU + 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"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == template_type assert result["errors"] is None assert result["preview"] == "template" @@ -773,7 +773,7 @@ async def test_option_flow_preview( 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["type"] is FlowResultType.FORM assert result["errors"] is None assert result["preview"] == "template" @@ -830,7 +830,7 @@ async def test_option_flow_sensor_preview_config_entry_removed( 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["type"] is FlowResultType.FORM assert result["errors"] is None assert result["preview"] == "template" diff --git a/tests/components/tesla_wall_connector/test_config_flow.py b/tests/components/tesla_wall_connector/test_config_flow.py index 198dcccfe00..84d655a629e 100644 --- a/tests/components/tesla_wall_connector/test_config_flow.py +++ b/tests/components/tesla_wall_connector/test_config_flow.py @@ -19,7 +19,7 @@ async def test_form(mock_wall_connector_version, hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -32,7 +32,7 @@ async def test_form(mock_wall_connector_version, hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Tesla Wall Connector" assert result2["data"] == {CONF_HOST: "1.1.1.1"} assert len(mock_setup_entry.mock_calls) == 1 @@ -53,7 +53,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: {CONF_HOST: "1.1.1.1"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -74,7 +74,7 @@ async def test_form_other_error( {CONF_HOST: "1.1.1.1"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -129,7 +129,7 @@ async def test_dhcp_can_finish( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_HOST: "1.2.3.4"} diff --git a/tests/components/teslemetry/test_config_flow.py b/tests/components/teslemetry/test_config_flow.py index 3757c331996..f2894c695fa 100644 --- a/tests/components/teslemetry/test_config_flow.py +++ b/tests/components/teslemetry/test_config_flow.py @@ -36,7 +36,7 @@ async def test_form( result1 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert not result1["errors"] with patch( @@ -50,7 +50,7 @@ async def test_form( await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == CONFIG @@ -76,7 +76,7 @@ async def test_form_errors(hass: HomeAssistant, side_effect, error, mock_test) - CONFIG, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == error # Complete the flow @@ -85,4 +85,4 @@ async def test_form_errors(hass: HomeAssistant, side_effect, error, mock_test) - result2["flow_id"], CONFIG, ) - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY diff --git a/tests/components/tessie/test_config_flow.py b/tests/components/tessie/test_config_flow.py index e5bcf11efd1..ac3217f864b 100644 --- a/tests/components/tessie/test_config_flow.py +++ b/tests/components/tessie/test_config_flow.py @@ -27,7 +27,7 @@ async def test_form(hass: HomeAssistant, mock_get_state_of_all_vehicles) -> None result1 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert not result1["errors"] with patch( @@ -42,7 +42,7 @@ async def test_form(hass: HomeAssistant, mock_get_state_of_all_vehicles) -> None assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_get_state_of_all_vehicles.mock_calls) == 1 - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Tessie" assert result2["data"] == TEST_CONFIG @@ -70,7 +70,7 @@ async def test_form_errors( TEST_CONFIG, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == error # Complete the flow @@ -79,7 +79,7 @@ async def test_form_errors( result2["flow_id"], TEST_CONFIG, ) - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY async def test_reauth(hass: HomeAssistant, mock_get_state_of_all_vehicles) -> None: @@ -100,7 +100,7 @@ async def test_reauth(hass: HomeAssistant, mock_get_state_of_all_vehicles) -> No data=TEST_CONFIG, ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "reauth_confirm" assert not result1["errors"] @@ -116,7 +116,7 @@ async def test_reauth(hass: HomeAssistant, mock_get_state_of_all_vehicles) -> No assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_get_state_of_all_vehicles.mock_calls) == 1 - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert mock_entry.data == TEST_CONFIG @@ -153,7 +153,7 @@ async def test_reauth_errors( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == error # Complete the flow @@ -163,6 +163,6 @@ async def test_reauth_errors( TEST_CONFIG, ) assert "errors" not in result3 - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" assert mock_entry.data == TEST_CONFIG diff --git a/tests/components/thermobeacon/test_config_flow.py b/tests/components/thermobeacon/test_config_flow.py index a63ccf08963..a26a2b70c5e 100644 --- a/tests/components/thermobeacon/test_config_flow.py +++ b/tests/components/thermobeacon/test_config_flow.py @@ -19,7 +19,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=THERMOBEACON_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( "homeassistant.components.thermobeacon.async_setup_entry", return_value=True @@ -27,7 +27,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Lanyard/mini hygrometer EEFF" assert result2["data"] == {} assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" @@ -40,7 +40,7 @@ async def test_async_step_bluetooth_not_thermobeacon(hass: HomeAssistant) -> Non context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_THERMOBEACON_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -50,7 +50,7 @@ async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -64,7 +64,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( "homeassistant.components.thermobeacon.async_setup_entry", return_value=True @@ -73,7 +73,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Lanyard/mini hygrometer EEFF" assert result2["data"] == {} assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" @@ -89,7 +89,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -105,7 +105,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -127,7 +127,7 @@ async def test_async_step_user_with_found_devices_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -144,7 +144,7 @@ async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant) - context={"source": config_entries.SOURCE_BLUETOOTH}, data=THERMOBEACON_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -155,7 +155,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=THERMOBEACON_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -163,7 +163,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=THERMOBEACON_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -176,7 +176,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=THERMOBEACON_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -187,7 +187,7 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.thermobeacon.async_setup_entry", return_value=True @@ -196,7 +196,7 @@ async def test_async_step_user_takes_precedence_over_discovery( result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Lanyard/mini hygrometer EEFF" assert result2["data"] == {} assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" diff --git a/tests/components/thermopro/test_config_flow.py b/tests/components/thermopro/test_config_flow.py index 0ee86cd5067..9b9fdd67334 100644 --- a/tests/components/thermopro/test_config_flow.py +++ b/tests/components/thermopro/test_config_flow.py @@ -19,7 +19,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=TP357_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( "homeassistant.components.thermopro.async_setup_entry", return_value=True @@ -27,7 +27,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "TP357 (2142) AC3D" assert result2["data"] == {} assert result2["result"].unique_id == "4125DDBA-2774-4851-9889-6AADDD4CAC3D" @@ -40,7 +40,7 @@ async def test_async_step_bluetooth_not_thermopro(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_THERMOPRO_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -50,7 +50,7 @@ async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -64,7 +64,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( "homeassistant.components.thermopro.async_setup_entry", return_value=True @@ -73,7 +73,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: result["flow_id"], user_input={"address": "4125DDBA-2774-4851-9889-6AADDD4CAC3D"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "TP357 (2142) AC3D" assert result2["data"] == {} assert result2["result"].unique_id == "4125DDBA-2774-4851-9889-6AADDD4CAC3D" @@ -89,7 +89,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -105,7 +105,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - result["flow_id"], user_input={"address": "4125DDBA-2774-4851-9889-6AADDD4CAC3D"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -127,7 +127,7 @@ async def test_async_step_user_with_found_devices_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -144,7 +144,7 @@ async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant) - context={"source": config_entries.SOURCE_BLUETOOTH}, data=TP357_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -155,7 +155,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=TP357_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -163,7 +163,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=TP357_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -176,7 +176,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=TP357_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -187,7 +187,7 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.thermopro.async_setup_entry", return_value=True @@ -196,7 +196,7 @@ async def test_async_step_user_takes_precedence_over_discovery( result["flow_id"], user_input={"address": "4125DDBA-2774-4851-9889-6AADDD4CAC3D"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "TP357 (2142) AC3D" assert result2["data"] == {} assert result2["result"].unique_id == "4125DDBA-2774-4851-9889-6AADDD4CAC3D" diff --git a/tests/components/thread/test_config_flow.py b/tests/components/thread/test_config_flow.py index 9f4930947ef..c31a1937d45 100644 --- a/tests/components/thread/test_config_flow.py +++ b/tests/components/thread/test_config_flow.py @@ -42,7 +42,7 @@ async def test_import(hass: HomeAssistant) -> None: thread.DOMAIN, context={"source": "import"} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Thread" assert result["data"] == {} assert result["options"] == {} @@ -65,7 +65,7 @@ async def test_import_then_zeroconf(hass: HomeAssistant) -> None: thread.DOMAIN, context={"source": "import"} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY with patch( "homeassistant.components.thread.async_setup_entry", @@ -75,7 +75,7 @@ async def test_import_then_zeroconf(hass: HomeAssistant) -> None: thread.DOMAIN, context={"source": "zeroconf"}, data=TEST_ZEROCONF_RECORD ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(mock_setup_entry.mock_calls) == 0 @@ -90,7 +90,7 @@ async def test_user(hass: HomeAssistant) -> None: thread.DOMAIN, context={"source": "user"} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Thread" assert result["data"] == {} assert result["options"] == {} @@ -108,7 +108,7 @@ async def test_zeroconf(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( thread.DOMAIN, context={"source": "zeroconf"}, data=TEST_ZEROCONF_RECORD ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "confirm" @@ -117,7 +117,7 @@ async def test_zeroconf(hass: HomeAssistant) -> None: return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Thread" assert result["data"] == {} assert result["options"] == {} @@ -144,7 +144,7 @@ async def test_zeroconf_setup_onboarding(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( thread.DOMAIN, context={"source": "zeroconf"}, data=TEST_ZEROCONF_RECORD ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Thread" assert result["data"] == {} assert result["options"] == {} @@ -161,7 +161,7 @@ async def test_zeroconf_then_import(hass: HomeAssistant) -> None: return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY with patch( "homeassistant.components.thread.async_setup_entry", @@ -171,6 +171,6 @@ async def test_zeroconf_then_import(hass: HomeAssistant) -> None: thread.DOMAIN, context={"source": "import"} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/threshold/test_config_flow.py b/tests/components/threshold/test_config_flow.py index 726fa04cef0..88c970d5c2c 100644 --- a/tests/components/threshold/test_config_flow.py +++ b/tests/components/threshold/test_config_flow.py @@ -19,7 +19,7 @@ async def test_config_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -37,7 +37,7 @@ async def test_config_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "My threshold sensor" assert result["data"] == {} assert result["options"] == { @@ -69,7 +69,7 @@ async def test_fail(hass: HomeAssistant, extra_input_data, error) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -81,7 +81,7 @@ async def test_fail(hass: HomeAssistant, extra_input_data, error) -> None: }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": error} @@ -119,7 +119,7 @@ async def test_options(hass: HomeAssistant) -> None: 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["type"] is FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema assert get_suggested(schema, "hysteresis") == 0.0 @@ -133,7 +133,7 @@ async def test_options(hass: HomeAssistant) -> None: "upper": 20.0, }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "entity_id": input_sensor, "hysteresis": 0.0, diff --git a/tests/components/tibber/test_config_flow.py b/tests/components/tibber/test_config_flow.py index b6c616c5cf0..28b590a29d2 100644 --- a/tests/components/tibber/test_config_flow.py +++ b/tests/components/tibber/test_config_flow.py @@ -33,7 +33,7 @@ async def test_show_config_form(recorder_mock: Recorder, hass: HomeAssistant) -> DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -56,7 +56,7 @@ async def test_create_entry(recorder_mock: Recorder, hass: HomeAssistant) -> Non DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == title assert result["data"] == test_data @@ -92,7 +92,7 @@ async def test_create_entry_exceptions( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"][CONF_ACCESS_TOKEN] == expected_error @@ -109,5 +109,5 @@ async def test_flow_entry_already_exists( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/tile/test_config_flow.py b/tests/components/tile/test_config_flow.py index 5d269bfee5d..87fe976ca3f 100644 --- a/tests/components/tile/test_config_flow.py +++ b/tests/components/tile/test_config_flow.py @@ -5,11 +5,11 @@ from unittest.mock import AsyncMock, patch import pytest from pytile.errors import InvalidAuthError, TileError -from homeassistant import data_entry_flow from homeassistant.components.tile import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .conftest import TEST_PASSWORD, TEST_USERNAME @@ -28,7 +28,7 @@ async def test_create_entry( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # Test errors that can arise: @@ -38,7 +38,7 @@ async def test_create_entry( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == errors @@ -46,7 +46,7 @@ async def test_create_entry( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_USERNAME assert result["data"] == { CONF_USERNAME: TEST_USERNAME, @@ -59,7 +59,7 @@ async def test_duplicate_error(hass: HomeAssistant, config, setup_config_entry) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -68,7 +68,7 @@ async def test_import_entry(hass: HomeAssistant, config, mock_pytile) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=config ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_USERNAME assert result["data"] == { CONF_USERNAME: TEST_USERNAME, @@ -86,12 +86,12 @@ async def test_step_reauth( assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "password"} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 diff --git a/tests/components/tilt_ble/test_config_flow.py b/tests/components/tilt_ble/test_config_flow.py index b9623f9700d..fd996228034 100644 --- a/tests/components/tilt_ble/test_config_flow.py +++ b/tests/components/tilt_ble/test_config_flow.py @@ -19,7 +19,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=TILT_GREEN_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( "homeassistant.components.tilt_ble.async_setup_entry", return_value=True @@ -27,7 +27,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Tilt Green" assert result2["data"] == {} assert result2["result"].unique_id == "F6:0F:28:F2:1F:CB" @@ -40,7 +40,7 @@ async def test_async_step_bluetooth_not_tilt(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_TILT_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -50,7 +50,7 @@ async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -64,7 +64,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( "homeassistant.components.tilt_ble.async_setup_entry", return_value=True @@ -73,7 +73,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: result["flow_id"], user_input={"address": "F6:0F:28:F2:1F:CB"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Tilt Green" assert result2["data"] == {} assert result2["result"].unique_id == "F6:0F:28:F2:1F:CB" @@ -89,7 +89,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -105,7 +105,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - result["flow_id"], user_input={"address": "F6:0F:28:F2:1F:CB"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -127,7 +127,7 @@ async def test_async_step_user_with_found_devices_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -144,7 +144,7 @@ async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant) - context={"source": config_entries.SOURCE_BLUETOOTH}, data=TILT_GREEN_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -155,7 +155,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=TILT_GREEN_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -163,7 +163,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=TILT_GREEN_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -176,7 +176,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=TILT_GREEN_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -187,7 +187,7 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.tilt_ble.async_setup_entry", return_value=True @@ -196,7 +196,7 @@ async def test_async_step_user_takes_precedence_over_discovery( result["flow_id"], user_input={"address": "F6:0F:28:F2:1F:CB"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Tilt Green" assert result2["data"] == {} assert result2["result"].unique_id == "F6:0F:28:F2:1F:CB" diff --git a/tests/components/time_date/test_config_flow.py b/tests/components/time_date/test_config_flow.py index 7402fc529d1..9f25b572014 100644 --- a/tests/components/time_date/test_config_flow.py +++ b/tests/components/time_date/test_config_flow.py @@ -25,7 +25,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -34,7 +34,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_user_flow_does_not_allow_beat( @@ -45,7 +45,7 @@ async def test_user_flow_does_not_allow_beat( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with pytest.raises(vol.Invalid): await hass.config_entries.flow.async_configure( @@ -65,13 +65,13 @@ async def test_single_instance(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"display_options": "time"}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -88,7 +88,7 @@ async def test_timezone_not_set(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "timezone_not_exist"} @@ -104,7 +104,7 @@ async def test_config_flow_preview( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None assert result["preview"] == "time_date" diff --git a/tests/components/tod/test_config_flow.py b/tests/components/tod/test_config_flow.py index c56accf103c..15c0229c653 100644 --- a/tests/components/tod/test_config_flow.py +++ b/tests/components/tod/test_config_flow.py @@ -18,7 +18,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -35,7 +35,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "My tod" assert result["data"] == {} assert result["options"] == { @@ -85,7 +85,7 @@ async def test_options(hass: HomeAssistant) -> None: 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["type"] is FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema assert get_suggested(schema, "after_time") == "10:00" @@ -98,7 +98,7 @@ async def test_options(hass: HomeAssistant) -> None: "before_time": "17:05", }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "after_time": "10:00", "before_time": "17:05", diff --git a/tests/components/todoist/test_config_flow.py b/tests/components/todoist/test_config_flow.py index 141f12269de..46ae0e24fba 100644 --- a/tests/components/todoist/test_config_flow.py +++ b/tests/components/todoist/test_config_flow.py @@ -35,7 +35,7 @@ async def test_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert not result.get("errors") result2 = await hass.config_entries.flow.async_configure( @@ -46,7 +46,7 @@ async def test_form( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "Todoist" assert result2.get("data") == { CONF_TOKEN: TOKEN, @@ -68,7 +68,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("errors") == {"base": "invalid_api_key"} @@ -86,7 +86,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("errors") == {"base": "cannot_connect"} @@ -106,7 +106,7 @@ async def test_unknown_error(hass: HomeAssistant, api: AsyncMock) -> None: }, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("errors") == {"base": "unknown"} @@ -119,5 +119,5 @@ async def test_already_configured(hass: HomeAssistant, setup_integration: None) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" diff --git a/tests/components/tolo/test_config_flow.py b/tests/components/tolo/test_config_flow.py index 711ded3880b..9dcca4b704f 100644 --- a/tests/components/tolo/test_config_flow.py +++ b/tests/components/tolo/test_config_flow.py @@ -46,7 +46,7 @@ async def test_user_with_timed_out_host(hass: HomeAssistant, toloclient: Mock) - data={CONF_HOST: "127.0.0.1"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -59,7 +59,7 @@ async def test_user_walkthrough( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" toloclient().get_status.side_effect = lambda *args, **kwargs: None @@ -69,7 +69,7 @@ async def test_user_walkthrough( user_input={CONF_HOST: "127.0.0.2"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -80,7 +80,7 @@ async def test_user_walkthrough( user_input={CONF_HOST: "127.0.0.1"}, ) - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "TOLO Sauna" assert result3["data"][CONF_HOST] == "127.0.0.1" @@ -94,7 +94,7 @@ async def test_dhcp( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, data=MOCK_DHCP_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -102,7 +102,7 @@ async def test_dhcp( user_input={}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "TOLO Sauna" assert result["data"][CONF_HOST] == "127.0.0.2" assert result["result"].unique_id == "00:11:22:33:44:55" @@ -115,4 +115,4 @@ async def test_dhcp_invalid_device(hass: HomeAssistant, toloclient: Mock) -> Non result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, data=MOCK_DHCP_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT diff --git a/tests/components/tomorrowio/test_config_flow.py b/tests/components/tomorrowio/test_config_flow.py index 5d4d2e3b43b..d280e8a5182 100644 --- a/tests/components/tomorrowio/test_config_flow.py +++ b/tests/components/tomorrowio/test_config_flow.py @@ -9,7 +9,6 @@ from pytomorrowio.exceptions import ( UnknownException, ) -from homeassistant import data_entry_flow from homeassistant.components.tomorrowio.config_flow import ( _get_config_schema, _get_unique_id, @@ -30,6 +29,7 @@ from homeassistant.const import ( CONF_RADIUS, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.setup import async_setup_component from .const import API_KEY, MIN_CONFIG @@ -42,7 +42,7 @@ async def test_user_flow_minimum_fields(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -50,7 +50,7 @@ async def test_user_flow_minimum_fields(hass: HomeAssistant) -> None: user_input=_get_config_schema(hass, SOURCE_USER, MIN_CONFIG)(MIN_CONFIG), ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"][CONF_NAME] == DEFAULT_NAME assert result["data"][CONF_API_KEY] == API_KEY @@ -75,7 +75,7 @@ async def test_user_flow_minimum_fields_in_zone(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -83,7 +83,7 @@ async def test_user_flow_minimum_fields_in_zone(hass: HomeAssistant) -> None: user_input=_get_config_schema(hass, SOURCE_USER, MIN_CONFIG)(MIN_CONFIG), ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"{DEFAULT_NAME} - Home" assert result["data"][CONF_NAME] == f"{DEFAULT_NAME} - Home" assert result["data"][CONF_API_KEY] == API_KEY @@ -109,7 +109,7 @@ async def test_user_flow_same_unique_ids(hass: HomeAssistant) -> None: data=user_input, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -125,7 +125,7 @@ async def test_user_flow_cannot_connect(hass: HomeAssistant) -> None: data=_get_config_schema(hass, SOURCE_USER, MIN_CONFIG)(MIN_CONFIG), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -141,7 +141,7 @@ async def test_user_flow_invalid_api(hass: HomeAssistant) -> None: data=_get_config_schema(hass, SOURCE_USER, MIN_CONFIG)(MIN_CONFIG), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_API_KEY: "invalid_api_key"} @@ -157,7 +157,7 @@ async def test_user_flow_rate_limited(hass: HomeAssistant) -> None: data=_get_config_schema(hass, SOURCE_USER, MIN_CONFIG)(MIN_CONFIG), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_API_KEY: "rate_limited"} @@ -173,7 +173,7 @@ async def test_user_flow_unknown_exception(hass: HomeAssistant) -> None: data=_get_config_schema(hass, SOURCE_USER, MIN_CONFIG)(MIN_CONFIG), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "unknown"} @@ -197,14 +197,14 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id, data=None) - assert result["type"] == data_entry_flow.FlowResultType.FORM + 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_TIMESTEP: 1} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"][CONF_TIMESTEP] == 1 assert entry.options[CONF_TIMESTEP] == 1 diff --git a/tests/components/toon/test_config_flow.py b/tests/components/toon/test_config_flow.py index 3e8f7fa2624..7bda813e447 100644 --- a/tests/components/toon/test_config_flow.py +++ b/tests/components/toon/test_config_flow.py @@ -5,12 +5,12 @@ from unittest.mock import patch from toonapi import Agreement, ToonError -from homeassistant import data_entry_flow from homeassistant.components.toon.const import CONF_AGREEMENT, CONF_MIGRATE, DOMAIN from homeassistant.config import async_process_ha_core_config from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET 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 @@ -41,7 +41,7 @@ async def test_abort_if_no_configuration(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "missing_configuration" @@ -58,7 +58,7 @@ async def test_full_flow_implementation( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pick_implementation" state = config_entry_oauth2_flow._encode_jwt( @@ -73,7 +73,7 @@ async def test_full_flow_implementation( result["flow_id"], {"implementation": "eneco"} ) - assert result2["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP + assert result2["type"] is FlowResultType.EXTERNAL_STEP assert result2["url"] == ( "https://api.toon.eu/authorize" "?response_type=code&client_id=client" @@ -149,7 +149,7 @@ async def test_no_agreements( with patch("toonapi.Toon.agreements", return_value=[]): result3 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result3["type"] == data_entry_flow.FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "no_agreements" @@ -195,7 +195,7 @@ async def test_multiple_agreements( ): result3 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result3["type"] == data_entry_flow.FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "agreement" result4 = await hass.config_entries.flow.async_configure( @@ -244,7 +244,7 @@ async def test_agreement_already_set_up( with patch("toonapi.Toon.agreements", return_value=[Agreement(agreement_id=123)]): result3 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result3["type"] == data_entry_flow.FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "already_configured" @@ -286,7 +286,7 @@ async def test_toon_abort( with patch("toonapi.Toon.agreements", side_effect=ToonError): result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "connection_error" @@ -300,7 +300,7 @@ async def test_import(hass: HomeAssistant, current_request_with_host: None) -> N DOMAIN, context={"source": SOURCE_IMPORT} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -350,7 +350,7 @@ async def test_import_migration( with patch("toonapi.Toon.agreements", return_value=[Agreement(agreement_id=123)]): result = await hass.config_entries.flow.async_configure(flows[0]["flow_id"]) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 diff --git a/tests/components/totalconnect/test_config_flow.py b/tests/components/totalconnect/test_config_flow.py index 940542bf3ad..98de748faea 100644 --- a/tests/components/totalconnect/test_config_flow.py +++ b/tests/components/totalconnect/test_config_flow.py @@ -4,7 +4,6 @@ from unittest.mock import patch from total_connect_client.exceptions import AuthenticationError -from homeassistant import data_entry_flow from homeassistant.components.totalconnect.const import ( AUTO_BYPASS, CONF_USERCODES, @@ -13,6 +12,7 @@ from homeassistant.components.totalconnect.const import ( from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_PASSWORD from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .common import ( CONFIG_DATA, @@ -39,7 +39,7 @@ async def test_user(hass: HomeAssistant) -> None: data=None, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -71,7 +71,7 @@ async def test_user_show_locations(hass: HomeAssistant) -> None: ) # first it should show the locations form - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "locations" # client should have sent four requests for init assert mock_request.call_count == 4 @@ -81,7 +81,7 @@ async def test_user_show_locations(hass: HomeAssistant) -> None: result["flow_id"], user_input={CONF_USERCODES: "bad"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "locations" # client should have sent 5th request to validate usercode assert mock_request.call_count == 5 @@ -91,7 +91,7 @@ async def test_user_show_locations(hass: HomeAssistant) -> None: result2["flow_id"], user_input={CONF_USERCODES: "7890"}, ) - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY # client should have sent another request to validate usercode assert mock_request.call_count == 6 @@ -112,7 +112,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant) -> None: data=CONFIG_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -128,7 +128,7 @@ async def test_login_failed(hass: HomeAssistant) -> None: data=CONFIG_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -144,7 +144,7 @@ async def test_reauth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_REAUTH}, data=entry.data ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with ( @@ -161,7 +161,7 @@ async def test_reauth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "password"} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "invalid_auth"} @@ -171,7 +171,7 @@ async def test_reauth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "password"} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" await hass.async_block_till_done() @@ -205,7 +205,7 @@ async def test_no_locations(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, data=CONFIG_DATA_NO_USERCODES, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_locations" await hass.async_block_till_done() @@ -236,14 +236,14 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={AUTO_BYPASS: True} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == {AUTO_BYPASS: True} await hass.async_block_till_done() diff --git a/tests/components/tplink/test_config_flow.py b/tests/components/tplink/test_config_flow.py index 4c1cc999f16..4e80ce3e890 100644 --- a/tests/components/tplink/test_config_flow.py +++ b/tests/components/tplink/test_config_flow.py @@ -102,7 +102,7 @@ async def test_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -110,7 +110,7 @@ async def test_discovery(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "no_devices_found" @@ -661,7 +661,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: ), ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "cannot_connect" @@ -1104,7 +1104,7 @@ async def test_pick_device_errors( CONF_PASSWORD: "fake_password", }, ) - assert result4["type"] == FlowResultType.CREATE_ENTRY + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["context"]["unique_id"] == MAC_ADDRESS diff --git a/tests/components/tplink_omada/test_config_flow.py b/tests/components/tplink_omada/test_config_flow.py index 230f0d2a68e..08606fe126c 100644 --- a/tests/components/tplink_omada/test_config_flow.py +++ b/tests/components/tplink_omada/test_config_flow.py @@ -43,7 +43,7 @@ async def test_form_single_site(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -64,7 +64,7 @@ async def test_form_single_site(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "OC200 (Display Name)" assert result2["data"] == MOCK_ENTRY_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -76,7 +76,7 @@ async def test_form_multiple_sites(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -100,7 +100,7 @@ async def test_form_multiple_sites(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "site" with patch( @@ -115,7 +115,7 @@ async def test_form_multiple_sites(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "OC200 (Site 2)" assert result3["data"] == { "host": "https://fake.omada.host", @@ -142,7 +142,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: MOCK_USER_DATA, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -161,7 +161,7 @@ async def test_form_api_error(hass: HomeAssistant) -> None: MOCK_USER_DATA, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -180,7 +180,7 @@ async def test_form_generic_exception(hass: HomeAssistant) -> None: MOCK_USER_DATA, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -199,7 +199,7 @@ async def test_form_unsupported_controller(hass: HomeAssistant) -> None: MOCK_USER_DATA, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unsupported_controller"} @@ -218,7 +218,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: MOCK_USER_DATA, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -237,7 +237,7 @@ async def test_form_no_sites(hass: HomeAssistant) -> None: MOCK_USER_DATA, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "no_sites_found"} @@ -260,7 +260,7 @@ async def test_async_step_reauth_success(hass: HomeAssistant) -> None: data=mock_entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( @@ -274,7 +274,7 @@ async def test_async_step_reauth_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" mocked_validate.assert_called_once_with( hass, @@ -307,7 +307,7 @@ async def test_async_step_reauth_invalid_auth(hass: HomeAssistant) -> None: data=mock_entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( @@ -319,7 +319,7 @@ async def test_async_step_reauth_invalid_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result2["errors"] == {"base": "invalid_auth"} diff --git a/tests/components/traccar_server/test_config_flow.py b/tests/components/traccar_server/test_config_flow.py index c412830066d..5652d2c77be 100644 --- a/tests/components/traccar_server/test_config_flow.py +++ b/tests/components/traccar_server/test_config_flow.py @@ -39,7 +39,7 @@ async def test_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( @@ -52,7 +52,7 @@ async def test_form( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "1.1.1.1:8082" assert result["data"] == { CONF_HOST: "1.1.1.1", @@ -94,7 +94,7 @@ async def test_form_cannot_connect( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": error} mock_traccar_api_client.get_server.side_effect = None @@ -109,7 +109,7 @@ async def test_form_cannot_connect( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "1.1.1.1:8082" assert result["data"] == { CONF_HOST: "1.1.1.1", @@ -143,7 +143,7 @@ async def test_options( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert mock_config_entry.options == { CONF_MAX_ACCURACY: 2.0, CONF_EVENTS: [], @@ -238,7 +238,7 @@ async def test_import_from_yaml( context={"source": config_entries.SOURCE_IMPORT}, data=PLATFORM_SCHEMA({"platform": "traccar", **imported}), ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"{data[CONF_HOST]}:{data[CONF_PORT]}" assert result["data"] == data assert result["options"] == options @@ -269,7 +269,7 @@ async def test_abort_import_already_configured(hass: HomeAssistant) -> None: ), ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -296,5 +296,5 @@ async def test_abort_already_configured( }, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/tradfri/test_config_flow.py b/tests/components/tradfri/test_config_flow.py index fd3d85461b1..af2fdc22d2a 100644 --- a/tests/components/tradfri/test_config_flow.py +++ b/tests/components/tradfri/test_config_flow.py @@ -5,10 +5,11 @@ from unittest.mock import AsyncMock, patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.tradfri import config_flow from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import TRADFRI_PATH @@ -38,7 +39,7 @@ async def test_already_paired(hass: HomeAssistant, mock_entry_setup) -> None: result["flow_id"], {"host": "123.123.123.123", "security_code": "abcd"} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_authenticate"} @@ -58,7 +59,7 @@ async def test_user_connection_successful( assert len(mock_entry_setup.mock_calls) == 1 - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].data == { "host": "123.123.123.123", "gateway_id": "bla", @@ -81,7 +82,7 @@ async def test_user_connection_timeout( assert len(mock_entry_setup.mock_calls) == 0 - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "timeout"} @@ -101,7 +102,7 @@ async def test_user_connection_bad_key( assert len(mock_entry_setup.mock_calls) == 0 - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"security_code": "invalid_security_code"} @@ -131,7 +132,7 @@ async def test_discovery_connection( assert len(mock_entry_setup.mock_calls) == 1 - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == "homekit-id" assert result["result"].data == { "host": "123.123.123.123", @@ -160,7 +161,7 @@ async def test_discovery_duplicate_aborted(hass: HomeAssistant) -> None: ), ) - assert flow["type"] == data_entry_flow.FlowResultType.ABORT + assert flow["type"] is FlowResultType.ABORT assert flow["reason"] == "already_configured" assert entry.data["host"] == "123.123.123.124" @@ -184,7 +185,7 @@ async def test_duplicate_discovery( ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_init( "tradfri", @@ -200,7 +201,7 @@ async def test_duplicate_discovery( ), ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT async def test_discovery_updates_unique_id(hass: HomeAssistant) -> None: @@ -225,7 +226,7 @@ async def test_discovery_updates_unique_id(hass: HomeAssistant) -> None: ), ) - assert flow["type"] == data_entry_flow.FlowResultType.ABORT + assert flow["type"] is FlowResultType.ABORT assert flow["reason"] == "already_configured" assert entry.unique_id == "homekit-id" diff --git a/tests/components/trafikverket_camera/test_config_flow.py b/tests/components/trafikverket_camera/test_config_flow.py index eb14636d6c9..8162db076fa 100644 --- a/tests/components/trafikverket_camera/test_config_flow.py +++ b/tests/components/trafikverket_camera/test_config_flow.py @@ -23,7 +23,7 @@ async def test_form(hass: HomeAssistant, get_camera: CameraInfo) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -45,7 +45,7 @@ async def test_form(hass: HomeAssistant, get_camera: CameraInfo) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Test Camera" assert result2["data"] == { "api_key": "1234567890", @@ -63,7 +63,7 @@ async def test_form_multiple_cameras( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -97,7 +97,7 @@ async def test_form_multiple_cameras( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Test Camera2" assert result["data"] == { "api_key": "1234567890", @@ -115,7 +115,7 @@ async def test_form_no_location_data( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -137,7 +137,7 @@ async def test_form_no_location_data( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Test Camera" assert result2["data"] == { "api_key": "1234567890", @@ -175,7 +175,7 @@ async def test_flow_fails( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result4["type"] == FlowResultType.FORM + assert result4["type"] is FlowResultType.FORM assert result4["step_id"] == config_entries.SOURCE_USER with patch( @@ -216,7 +216,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: data=entry.data, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -234,7 +234,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == { "api_key": "1234567891", @@ -299,7 +299,7 @@ async def test_reauth_flow_error( await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {error_key: p_error} with ( @@ -317,7 +317,7 @@ async def test_reauth_flow_error( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == { "api_key": "1234567891", diff --git a/tests/components/trafikverket_ferry/test_config_flow.py b/tests/components/trafikverket_ferry/test_config_flow.py index 2a0a0ae6cd6..1c170a917cc 100644 --- a/tests/components/trafikverket_ferry/test_config_flow.py +++ b/tests/components/trafikverket_ferry/test_config_flow.py @@ -27,7 +27,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -51,7 +51,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Ekerö to Slagsta at 10:00" assert result2["data"] == { "api_key": "1234567890", @@ -92,7 +92,7 @@ async def test_flow_fails( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result4["type"] == FlowResultType.FORM + assert result4["type"] is FlowResultType.FORM assert result4["step_id"] == config_entries.SOURCE_USER with patch( @@ -138,7 +138,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: data=entry.data, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -156,7 +156,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == { "api_key": "1234567891", @@ -224,7 +224,7 @@ async def test_reauth_flow_error( await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": p_error} with ( @@ -242,7 +242,7 @@ async def test_reauth_flow_error( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == { "api_key": "1234567891", diff --git a/tests/components/trafikverket_train/test_config_flow.py b/tests/components/trafikverket_train/test_config_flow.py index 3a5afa7431c..a6ba82a85bc 100644 --- a/tests/components/trafikverket_train/test_config_flow.py +++ b/tests/components/trafikverket_train/test_config_flow.py @@ -34,7 +34,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -61,7 +61,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Stockholm C to Uppsala C at 10:00" assert result["data"] == { "api_key": "1234567890", @@ -98,7 +98,7 @@ async def test_form_entry_already_exist(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -125,7 +125,7 @@ async def test_form_entry_already_exist(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -158,7 +158,7 @@ async def test_flow_fails( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER with ( @@ -203,7 +203,7 @@ async def test_flow_fails_departures( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER with ( @@ -256,7 +256,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: data=entry.data, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -277,7 +277,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data == { "api_key": "1234567891", @@ -354,7 +354,7 @@ async def test_reauth_flow_error( await hass.async_block_till_done() assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": p_error} with ( @@ -375,7 +375,7 @@ async def test_reauth_flow_error( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data == { "api_key": "1234567891", @@ -444,7 +444,7 @@ async def test_reauth_flow_error_departures( await hass.async_block_till_done() assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": p_error} with ( @@ -465,7 +465,7 @@ async def test_reauth_flow_error_departures( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data == { "api_key": "1234567891", @@ -515,7 +515,7 @@ async def test_options_flow( result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -524,12 +524,12 @@ async def test_options_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {"filter_product": "SJ Regionaltåg"} result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -538,5 +538,5 @@ async def test_options_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {"filter_product": None} diff --git a/tests/components/trafikverket_weatherstation/test_config_flow.py b/tests/components/trafikverket_weatherstation/test_config_flow.py index 4a1c50cbaf1..d2bd794daf7 100644 --- a/tests/components/trafikverket_weatherstation/test_config_flow.py +++ b/tests/components/trafikverket_weatherstation/test_config_flow.py @@ -87,7 +87,7 @@ async def test_flow_fails( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result4["type"] == FlowResultType.FORM + assert result4["type"] is FlowResultType.FORM assert result4["step_id"] == config_entries.SOURCE_USER with patch( @@ -125,7 +125,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: data=entry.data, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -143,7 +143,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data == {"api_key": "1234567891", "station": "Vallby"} @@ -191,7 +191,7 @@ async def test_reauth_flow_fails( data=entry.data, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -204,5 +204,5 @@ async def test_reauth_flow_fails( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": base_error} diff --git a/tests/components/transmission/test_config_flow.py b/tests/components/transmission/test_config_flow.py index 0e184ffc96b..e6c523bf1f6 100644 --- a/tests/components/transmission/test_config_flow.py +++ b/tests/components/transmission/test_config_flow.py @@ -32,7 +32,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.transmission.async_setup_entry", @@ -44,7 +44,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Transmission" assert result2["data"] == MOCK_CONFIG_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -60,7 +60,7 @@ async def test_device_already_configured( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -68,7 +68,7 @@ async def test_device_already_configured( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -90,14 +90,14 @@ async def test_options(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"limit": 20} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"]["limit"] == 20 assert result["data"]["order"] == "oldest_first" @@ -115,7 +115,7 @@ async def test_error_on_wrong_credentials( result["flow_id"], MOCK_CONFIG_DATA, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == { "username": "invalid_auth", "password": "invalid_auth", @@ -133,7 +133,7 @@ async def test_unexpected_error(hass: HomeAssistant, mock_api: MagicMock) -> Non result["flow_id"], MOCK_CONFIG_DATA, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -151,7 +151,7 @@ async def test_error_on_connection_failure( MOCK_CONFIG_DATA, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -169,7 +169,7 @@ async def test_reauth_success(hass: HomeAssistant) -> None: data=MOCK_CONFIG_DATA, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["description_placeholders"] == {"username": "user"} @@ -184,7 +184,7 @@ async def test_reauth_success(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 @@ -206,7 +206,7 @@ async def test_reauth_failed(hass: HomeAssistant, mock_api: MagicMock) -> None: data=MOCK_CONFIG_DATA, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["description_placeholders"] == {"username": "user"} @@ -218,7 +218,7 @@ async def test_reauth_failed(hass: HomeAssistant, mock_api: MagicMock) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"password": "invalid_auth"} @@ -241,7 +241,7 @@ async def test_reauth_failed_connection_error( data=MOCK_CONFIG_DATA, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["description_placeholders"] == {"username": "user"} @@ -253,5 +253,5 @@ async def test_reauth_failed_connection_error( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/trend/test_config_flow.py b/tests/components/trend/test_config_flow.py index baccc396bf1..fb82589b3ce 100644 --- a/tests/components/trend/test_config_flow.py +++ b/tests/components/trend/test_config_flow.py @@ -20,7 +20,7 @@ async def test_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -28,7 +28,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM # test step 2 of config flow: settings of trend sensor with patch( @@ -42,7 +42,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "CPU Temperature rising" assert result["data"] == {} assert result["options"] == { @@ -57,7 +57,7 @@ async def test_options(hass: HomeAssistant, config_entry: MockConfigEntry) -> No config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -69,7 +69,7 @@ async def test_options(hass: HomeAssistant, config_entry: MockConfigEntry) -> No ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "min_samples": 30, "max_samples": 50, diff --git a/tests/components/tuya/test_config_flow.py b/tests/components/tuya/test_config_flow.py index 646d6a09f12..6e971262bc8 100644 --- a/tests/components/tuya/test_config_flow.py +++ b/tests/components/tuya/test_config_flow.py @@ -28,7 +28,7 @@ async def test_user_flow( context={"source": SOURCE_USER}, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( @@ -36,7 +36,7 @@ async def test_user_flow( user_input={CONF_USER_CODE: "12345"}, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "scan" result3 = await hass.config_entries.flow.async_configure( @@ -44,7 +44,7 @@ async def test_user_flow( user_input={}, ) - assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY assert result3 == snapshot @@ -58,7 +58,7 @@ async def test_user_flow_failed_qr_code( context={"source": SOURCE_USER}, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" # Something went wrong getting the QR code (like an invalid user code) @@ -69,7 +69,7 @@ async def test_user_flow_failed_qr_code( user_input={CONF_USER_CODE: "12345"}, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("errors") == {"base": "login_error"} # This time it worked out @@ -86,7 +86,7 @@ async def test_user_flow_failed_qr_code( user_input={}, ) - assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY async def test_user_flow_failed_scan( @@ -99,7 +99,7 @@ async def test_user_flow_failed_scan( context={"source": SOURCE_USER}, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( @@ -107,7 +107,7 @@ async def test_user_flow_failed_scan( user_input={CONF_USER_CODE: "12345"}, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "scan" # Access has been denied, or the code hasn't been scanned yet @@ -122,7 +122,7 @@ async def test_user_flow_failed_scan( user_input={}, ) - assert result3.get("type") == FlowResultType.FORM + assert result3.get("type") is FlowResultType.FORM assert result3.get("errors") == {"base": "login_error"} # This time it worked out @@ -133,7 +133,7 @@ async def test_user_flow_failed_scan( user_input={}, ) - assert result4.get("type") == FlowResultType.CREATE_ENTRY + assert result4.get("type") is FlowResultType.CREATE_ENTRY @pytest.mark.usefixtures("mock_tuya_login_control") @@ -155,7 +155,7 @@ async def test_reauth_flow( data=mock_config_entry.data, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "scan" result2 = await hass.config_entries.flow.async_configure( @@ -163,7 +163,7 @@ async def test_reauth_flow( user_input={}, ) - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "reauth_successful" assert mock_config_entry == snapshot @@ -195,7 +195,7 @@ async def test_reauth_flow_migration( data=mock_old_config_entry.data, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "reauth_user_code" result2 = await hass.config_entries.flow.async_configure( @@ -203,7 +203,7 @@ async def test_reauth_flow_migration( user_input={CONF_USER_CODE: "12345"}, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "scan" result3 = await hass.config_entries.flow.async_configure( @@ -211,7 +211,7 @@ async def test_reauth_flow_migration( user_input={}, ) - assert result3.get("type") == FlowResultType.ABORT + assert result3.get("type") is FlowResultType.ABORT assert result3.get("reason") == "reauth_successful" # Ensure the old data is gone, new data is present @@ -247,7 +247,7 @@ async def test_reauth_flow_failed_qr_code( user_input={CONF_USER_CODE: "12345"}, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("errors") == {"base": "login_error"} # This time it worked out @@ -264,5 +264,5 @@ async def test_reauth_flow_failed_qr_code( user_input={}, ) - assert result3.get("type") == FlowResultType.ABORT + assert result3.get("type") is FlowResultType.ABORT assert result3.get("reason") == "reauth_successful" diff --git a/tests/components/twentemilieu/test_config_flow.py b/tests/components/twentemilieu/test_config_flow.py index e272ce38bee..dbc01c69acb 100644 --- a/tests/components/twentemilieu/test_config_flow.py +++ b/tests/components/twentemilieu/test_config_flow.py @@ -30,7 +30,7 @@ async def test_full_user_flow(hass: HomeAssistant, snapshot: SnapshotAssertion) DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( @@ -42,7 +42,7 @@ async def test_full_user_flow(hass: HomeAssistant, snapshot: SnapshotAssertion) }, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2 == snapshot @@ -60,7 +60,7 @@ async def test_invalid_address( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" mock_twentemilieu.unique_id.side_effect = TwenteMilieuAddressError @@ -72,7 +72,7 @@ async def test_invalid_address( }, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "user" assert result2.get("errors") == {"base": "invalid_address"} @@ -85,7 +85,7 @@ async def test_invalid_address( }, ) - assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("type") is FlowResultType.CREATE_ENTRY assert result3 == snapshot @@ -106,7 +106,7 @@ async def test_connection_error( }, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" assert result.get("errors") == {"base": "cannot_connect"} @@ -128,5 +128,5 @@ async def test_address_already_set_up( }, ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" diff --git a/tests/components/twitch/test_config_flow.py b/tests/components/twitch/test_config_flow.py index 4b6834ba544..f9d8be4a5d2 100644 --- a/tests/components/twitch/test_config_flow.py +++ b/tests/components/twitch/test_config_flow.py @@ -67,7 +67,7 @@ async def test_full_flow( assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "channel123" assert "result" in result assert "token" in result["result"].data @@ -98,7 +98,7 @@ async def test_already_configured( ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -121,7 +121,7 @@ async def test_reauth( }, data=config_entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) @@ -130,7 +130,7 @@ async def test_reauth( result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" @@ -195,7 +195,7 @@ async def test_reauth_wrong_account( }, data=config_entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) @@ -204,7 +204,7 @@ async def test_reauth_wrong_account( result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "wrong_account" @@ -229,7 +229,7 @@ async def test_import( "channels": ["channel123"], }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "channel123" assert "result" in result assert "token" in result["result"].data @@ -261,7 +261,7 @@ async def test_import_invalid_token( "channels": ["channel123"], }, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_token" issue_registry = ir.async_get(hass) assert len(issue_registry.issues) == 1 @@ -290,7 +290,7 @@ async def test_import_already_imported( "channels": ["channel123"], }, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" issue_registry = ir.async_get(hass) assert len(issue_registry.issues) == 1 From 9b41e3d1249b454d89f7e5980f69641cc54feedf Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 2 Apr 2024 23:22:05 +0200 Subject: [PATCH 192/967] Use is in enum comparison in config flow tests A-E (#114669) --- tests/components/abode/test_config_flow.py | 16 +-- .../accuweather/test_config_flow.py | 10 +- tests/components/acmeda/test_config_flow.py | 10 +- tests/components/adax/test_config_flow.py | 42 +++--- tests/components/adguard/test_config_flow.py | 21 +-- .../advantage_air/test_config_flow.py | 11 +- tests/components/aemet/test_config_flow.py | 10 +- .../components/aftership/test_config_flow.py | 6 +- .../components/agent_dvr/test_config_flow.py | 12 +- tests/components/airly/test_config_flow.py | 10 +- tests/components/airnow/test_config_flow.py | 11 +- tests/components/airq/test_config_flow.py | 10 +- .../components/airthings/test_config_flow.py | 10 +- .../airthings_ble/test_config_flow.py | 24 ++-- .../components/airtouch5/test_config_flow.py | 6 +- .../components/airvisual/test_config_flow.py | 26 ++-- .../airvisual_pro/test_config_flow.py | 20 +-- tests/components/airzone/test_config_flow.py | 26 ++-- .../airzone_cloud/test_config_flow.py | 12 +- .../aladdin_connect/test_config_flow.py | 20 +-- .../alarmdecoder/test_config_flow.py | 63 ++++----- .../amberelectric/test_config_flow.py | 30 ++--- .../ambiclimate/test_config_flow.py | 23 ++-- .../ambient_station/test_config_flow.py | 10 +- .../analytics_insights/test_config_flow.py | 26 ++-- .../android_ip_webcam/test_config_flow.py | 12 +- .../components/androidtv/test_config_flow.py | 60 ++++----- .../androidtv_remote/test_config_flow.py | 64 ++++----- tests/components/anova/test_config_flow.py | 13 +- tests/components/anthemav/test_config_flow.py | 10 +- tests/components/aosmith/test_config_flow.py | 16 +-- tests/components/apcupsd/test_config_flow.py | 14 +- tests/components/apple_tv/test_config_flow.py | 113 ++++++++-------- tests/components/aranet/test_config_flow.py | 40 +++--- .../components/arcam_fmj/test_config_flow.py | 24 ++-- .../aseko_pool_live/test_config_flow.py | 14 +- tests/components/asuswrt/test_config_flow.py | 42 +++--- tests/components/atag/test_config_flow.py | 15 ++- tests/components/aurora/test_config_flow.py | 7 +- .../aurora_abb_powerone/test_config_flow.py | 5 +- .../aussie_broadband/test_config_flow.py | 16 +-- tests/components/awair/test_config_flow.py | 36 ++--- tests/components/axis/test_config_flow.py | 40 +++--- .../azure_devops/test_config_flow.py | 37 +++--- .../azure_event_hub/test_config_flow.py | 17 +-- tests/components/baf/test_config_flow.py | 16 +-- tests/components/balboa/test_config_flow.py | 20 +-- .../bang_olufsen/test_config_flow.py | 20 +-- tests/components/blebox/test_config_flow.py | 14 +- tests/components/blink/test_config_flow.py | 5 +- .../blue_current/test_config_flow.py | 14 +- .../bluemaestro/test_config_flow.py | 30 ++--- .../components/bluetooth/test_config_flow.py | 38 +++--- .../bmw_connected_drive/test_config_flow.py | 21 +-- tests/components/braviatv/test_config_flow.py | 36 ++--- tests/components/bring/test_config_flow.py | 8 +- tests/components/brother/test_config_flow.py | 26 ++-- .../brottsplatskartan/test_config_flow.py | 12 +- tests/components/brunt/test_config_flow.py | 15 ++- tests/components/bsblan/test_config_flow.py | 10 +- tests/components/bthome/test_config_flow.py | 80 +++++------ .../components/buienradar/test_config_flow.py | 5 +- tests/components/caldav/test_config_flow.py | 20 +-- tests/components/canary/test_config_flow.py | 16 +-- tests/components/cast/test_config_flow.py | 17 +-- tests/components/ccm15/test_config_flow.py | 20 +-- .../cert_expiry/test_config_flow.py | 31 ++--- .../components/cloudflare/test_config_flow.py | 20 +-- .../components/co2signal/test_config_flow.py | 24 ++-- .../color_extractor/test_config_flow.py | 8 +- tests/components/comelit/test_config_flow.py | 16 +-- tests/components/cpuspeed/test_config_flow.py | 10 +- .../components/crownstone/test_config_flow.py | 52 ++++---- tests/components/daikin/test_config_flow.py | 16 +-- tests/components/deconz/test_config_flow.py | 70 +++++----- tests/components/deluge/test_config_flow.py | 12 +- tests/components/denonavr/test_config_flow.py | 7 +- .../components/derivative/test_config_flow.py | 8 +- tests/components/devialet/test_config_flow.py | 12 +- .../devolo_home_control/test_config_flow.py | 24 ++-- .../devolo_home_network/test_config_flow.py | 18 +-- tests/components/dexcom/test_config_flow.py | 21 +-- tests/components/directv/test_config_flow.py | 30 ++--- tests/components/discord/test_config_flow.py | 25 ++-- .../components/discovergy/test_config_flow.py | 14 +- tests/components/dlink/test_config_flow.py | 32 ++--- tests/components/dlna_dmr/test_config_flow.py | 86 ++++++------ tests/components/dlna_dms/test_config_flow.py | 39 +++--- tests/components/dnsip/test_config_flow.py | 18 +-- tests/components/doorbird/test_config_flow.py | 15 ++- .../dormakaba_dkey/test_config_flow.py | 54 ++++---- .../components/downloader/test_config_flow.py | 10 +- .../dremel_3d_printer/test_config_flow.py | 14 +- .../drop_connect/test_config_flow.py | 4 +- tests/components/dsmr/test_config_flow.py | 10 +- .../dsmr_reader/test_config_flow.py | 6 +- tests/components/dunehd/test_config_flow.py | 6 +- tests/components/duotecno/test_config_flow.py | 10 +- .../dwd_weather_warnings/test_config_flow.py | 10 +- .../components/easyenergy/test_config_flow.py | 4 +- tests/components/ecobee/test_config_flow.py | 18 +-- .../components/ecoforest/test_config_flow.py | 12 +- tests/components/econet/test_config_flow.py | 16 +-- tests/components/ecovacs/test_config_flow.py | 26 ++-- tests/components/ecowitt/test_config_flow.py | 4 +- tests/components/edl21/test_config_flow.py | 6 +- tests/components/efergy/test_config_flow.py | 14 +- .../electrasmart/test_config_flow.py | 12 +- .../electric_kiwi/test_config_flow.py | 2 +- tests/components/elgato/test_config_flow.py | 20 +-- tests/components/elkm1/test_config_flow.py | 32 ++--- tests/components/elmax/test_config_flow.py | 49 +++---- tests/components/elvia/test_config_flow.py | 24 ++-- .../test_config_flow.py | 16 +-- .../components/energyzero/test_config_flow.py | 4 +- tests/components/enocean/test_config_flow.py | 23 ++-- .../environment_canada/test_config_flow.py | 9 +- tests/components/epion/test_config_flow.py | 8 +- tests/components/escea/test_config_flow.py | 10 +- tests/components/esphome/test_config_flow.py | 124 +++++++++--------- .../eufylife_ble/test_config_flow.py | 30 ++--- .../evil_genius_labs/test_config_flow.py | 10 +- tests/components/ezviz/test_config_flow.py | 92 ++++++------- 123 files changed, 1413 insertions(+), 1387 deletions(-) diff --git a/tests/components/abode/test_config_flow.py b/tests/components/abode/test_config_flow.py index 2f73ee052c1..265a77560f7 100644 --- a/tests/components/abode/test_config_flow.py +++ b/tests/components/abode/test_config_flow.py @@ -10,12 +10,12 @@ from jaraco.abode.helpers.errors import MFA_CODE_REQUIRED import pytest from requests.exceptions import ConnectTimeout -from homeassistant import data_entry_flow from homeassistant.components.abode import config_flow from homeassistant.components.abode.const import CONF_POLLING, DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -29,7 +29,7 @@ async def test_show_form(hass: HomeAssistant) -> None: result = await flow.async_step_user(user_input=None) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -45,7 +45,7 @@ async def test_one_config_allowed(hass: HomeAssistant) -> None: step_user_result = await flow.async_step_user() - assert step_user_result["type"] == data_entry_flow.FlowResultType.ABORT + assert step_user_result["type"] is FlowResultType.ABORT assert step_user_result["reason"] == "single_instance_allowed" @@ -107,7 +107,7 @@ async def test_step_user(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=conf ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "user@email.com" assert result["data"] == { CONF_USERNAME: "user@email.com", @@ -128,7 +128,7 @@ async def test_step_mfa(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=conf ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "mfa" with patch( @@ -148,7 +148,7 @@ async def test_step_mfa(hass: HomeAssistant) -> None: result["flow_id"], user_input={"mfa_code": "123456"} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "user@email.com" assert result["data"] == { CONF_USERNAME: "user@email.com", @@ -174,7 +174,7 @@ async def test_step_reauth(hass: HomeAssistant) -> None: data=conf, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch("homeassistant.config_entries.ConfigEntries.async_reload"): @@ -183,7 +183,7 @@ async def test_step_reauth(hass: HomeAssistant) -> None: user_input=conf, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 diff --git a/tests/components/accuweather/test_config_flow.py b/tests/components/accuweather/test_config_flow.py index c9d95c34b7c..acac15204f9 100644 --- a/tests/components/accuweather/test_config_flow.py +++ b/tests/components/accuweather/test_config_flow.py @@ -4,11 +4,11 @@ from unittest.mock import PropertyMock, patch from accuweather import ApiError, InvalidApiKeyError, RequestsExceededError -from homeassistant import data_entry_flow from homeassistant.components.accuweather.const import CONF_FORECAST, DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry, load_json_object_fixture @@ -26,7 +26,7 @@ async def test_show_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -134,7 +134,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: data=VALID_CONFIG, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "abcd" assert result["data"][CONF_NAME] == "abcd" assert result["data"][CONF_LATITUDE] == 55.55 @@ -176,14 +176,14 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + 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_FORECAST: True} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_FORECAST: True} await hass.async_block_till_done() diff --git a/tests/components/acmeda/test_config_flow.py b/tests/components/acmeda/test_config_flow.py index c39470ebbb6..1b79e37cab3 100644 --- a/tests/components/acmeda/test_config_flow.py +++ b/tests/components/acmeda/test_config_flow.py @@ -5,11 +5,11 @@ from unittest.mock import patch import aiopulse import pytest -from homeassistant import data_entry_flow from homeassistant.components.acmeda.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -49,7 +49,7 @@ async def test_show_form_no_hubs(hass: HomeAssistant, mock_hub_discover) -> None DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" # Check we performed the discovery @@ -70,7 +70,7 @@ async def test_show_form_one_hub( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == dummy_hub_1.id assert result["result"].data == { CONF_HOST: DUMMY_HOST1, @@ -95,7 +95,7 @@ async def test_show_form_two_hubs(hass: HomeAssistant, mock_hub_discover) -> Non DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # Check we performed the discovery @@ -123,7 +123,7 @@ async def test_create_second_entry( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == dummy_hub_2.id assert result["result"].data == { CONF_HOST: DUMMY_HOST2, diff --git a/tests/components/adax/test_config_flow.py b/tests/components/adax/test_config_flow.py index b2342b7c2a7..40640c66143 100644 --- a/tests/components/adax/test_config_flow.py +++ b/tests/components/adax/test_config_flow.py @@ -31,7 +31,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -40,7 +40,7 @@ async def test_form(hass: HomeAssistant) -> None: CONNECTION_TYPE: CLOUD, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM with ( patch( @@ -80,7 +80,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: CONNECTION_TYPE: CLOUD, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM with patch( "adax.get_adax_token", @@ -90,7 +90,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: result2["flow_id"], TEST_DATA, ) - assert result3["type"] == FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": "cannot_connect"} @@ -115,7 +115,7 @@ async def test_flow_entry_already_exists(hass: HomeAssistant) -> None: CONNECTION_TYPE: CLOUD, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM with patch("adax.get_adax_token", return_value="token"): result3 = await hass.config_entries.flow.async_configure( @@ -136,7 +136,7 @@ async def test_local_create_entry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -145,7 +145,7 @@ async def test_local_create_entry(hass: HomeAssistant) -> None: CONNECTION_TYPE: LOCAL, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM test_data = { WIFI_SSID: "ssid", @@ -201,7 +201,7 @@ async def test_local_flow_entry_already_exists(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -210,7 +210,7 @@ async def test_local_flow_entry_already_exists(hass: HomeAssistant) -> None: CONNECTION_TYPE: LOCAL, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM test_data = { WIFI_SSID: "ssid", @@ -239,7 +239,7 @@ async def test_local_connection_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -248,7 +248,7 @@ async def test_local_connection_error(hass: HomeAssistant) -> None: CONNECTION_TYPE: LOCAL, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM test_data = { WIFI_SSID: "ssid", @@ -264,7 +264,7 @@ async def test_local_connection_error(hass: HomeAssistant) -> None: test_data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -274,7 +274,7 @@ async def test_local_heater_not_available(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -283,7 +283,7 @@ async def test_local_heater_not_available(hass: HomeAssistant) -> None: CONNECTION_TYPE: LOCAL, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM test_data = { WIFI_SSID: "ssid", @@ -299,7 +299,7 @@ async def test_local_heater_not_available(hass: HomeAssistant) -> None: test_data, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "heater_not_available" @@ -309,7 +309,7 @@ async def test_local_heater_not_found(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -318,7 +318,7 @@ async def test_local_heater_not_found(hass: HomeAssistant) -> None: CONNECTION_TYPE: LOCAL, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM test_data = { WIFI_SSID: "ssid", @@ -334,7 +334,7 @@ async def test_local_heater_not_found(hass: HomeAssistant) -> None: test_data, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "heater_not_found" @@ -344,7 +344,7 @@ async def test_local_invalid_wifi_cred(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -353,7 +353,7 @@ async def test_local_invalid_wifi_cred(hass: HomeAssistant) -> None: CONNECTION_TYPE: LOCAL, }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM test_data = { WIFI_SSID: "ssid", @@ -369,5 +369,5 @@ async def test_local_invalid_wifi_cred(hass: HomeAssistant) -> None: test_data, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_auth" diff --git a/tests/components/adguard/test_config_flow.py b/tests/components/adguard/test_config_flow.py index 3f12dd1508a..3229a753699 100644 --- a/tests/components/adguard/test_config_flow.py +++ b/tests/components/adguard/test_config_flow.py @@ -2,7 +2,7 @@ import aiohttp -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.adguard.const import DOMAIN from homeassistant.components.hassio import HassioServiceInfo from homeassistant.config_entries import SOURCE_USER @@ -16,6 +16,7 @@ from homeassistant.const import ( CONTENT_TYPE_JSON, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker @@ -36,7 +37,7 @@ async def test_show_authenticate_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -58,7 +59,7 @@ async def test_connection_error( ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" assert result.get("errors") == {"base": "cannot_connect"} @@ -83,14 +84,14 @@ async def test_full_flow_implementation( assert result assert result.get("flow_id") - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=FIXTURE_USER_INPUT ) assert result2 - assert result2.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == FIXTURE_USER_INPUT[CONF_HOST] data = result2.get("data") @@ -140,7 +141,7 @@ async def test_hassio_already_configured(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_HASSIO}, ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -165,7 +166,7 @@ async def test_hassio_ignored(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_HASSIO}, ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -194,14 +195,14 @@ async def test_hassio_confirm( context={"source": config_entries.SOURCE_HASSIO}, ) assert result - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "hassio_confirm" assert result.get("description_placeholders") == {"addon": "AdGuard Home Addon"} result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result2 - assert result2.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "AdGuard Home Addon" data = result2.get("data") @@ -240,6 +241,6 @@ async def test_hassio_connection_error( result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "hassio_confirm" assert result.get("errors") == {"base": "cannot_connect"} diff --git a/tests/components/advantage_air/test_config_flow.py b/tests/components/advantage_air/test_config_flow.py index 134cfee9f68..d0f200a9ca5 100644 --- a/tests/components/advantage_air/test_config_flow.py +++ b/tests/components/advantage_air/test_config_flow.py @@ -4,9 +4,10 @@ from unittest.mock import AsyncMock, patch from advantage_air import ApiError -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.advantage_air.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import TEST_SYSTEM_DATA, USER_INPUT @@ -17,7 +18,7 @@ async def test_form(hass: HomeAssistant) -> None: result1 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result1["type"] == data_entry_flow.FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "user" assert result1["errors"] == {} @@ -39,7 +40,7 @@ async def test_form(hass: HomeAssistant) -> None: mock_setup_entry.assert_called_once() mock_get.assert_called_once() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "testname" assert result2["data"] == USER_INPUT @@ -55,7 +56,7 @@ async def test_form(hass: HomeAssistant) -> None: result3["flow_id"], USER_INPUT, ) - assert result4["type"] == data_entry_flow.FlowResultType.ABORT + assert result4["type"] is FlowResultType.ABORT async def test_form_cannot_connect(hass: HomeAssistant) -> None: @@ -74,6 +75,6 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: ) mock_get.assert_called_once() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/aemet/test_config_flow.py b/tests/components/aemet/test_config_flow.py index a7a689381e0..a9a2d45f618 100644 --- a/tests/components/aemet/test_config_flow.py +++ b/tests/components/aemet/test_config_flow.py @@ -6,11 +6,11 @@ from aemet_opendata.exceptions import AuthError from freezegun.api import FrozenDateTimeFactory import pytest -from homeassistant import data_entry_flow from homeassistant.components.aemet.const import CONF_STATION_UPDATES, DOMAIN from homeassistant.config_entries import SOURCE_USER, ConfigEntryState from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .util import mock_api_call @@ -37,7 +37,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -51,7 +51,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: entry = conf_entries[0] assert entry.state is ConfigEntryState.LOADED - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == CONFIG[CONF_NAME] assert result["data"][CONF_LATITUDE] == CONFIG[CONF_LATITUDE] assert result["data"][CONF_LONGITUDE] == CONFIG[CONF_LONGITUDE] @@ -89,14 +89,14 @@ async def test_form_options( result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input=user_input ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert entry.options == { CONF_STATION_UPDATES: expected, } diff --git a/tests/components/aftership/test_config_flow.py b/tests/components/aftership/test_config_flow.py index d5e34ac0ae2..34a23e31918 100644 --- a/tests/components/aftership/test_config_flow.py +++ b/tests/components/aftership/test_config_flow.py @@ -29,7 +29,7 @@ async def test_full_user_flow(hass: HomeAssistant, mock_setup_entry) -> None: CONF_API_KEY: "mock-api-key", }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "AfterShip" assert result["data"] == { CONF_API_KEY: "mock-api-key", @@ -54,7 +54,7 @@ async def test_flow_cannot_connect(hass: HomeAssistant, mock_setup_entry) -> Non CONF_API_KEY: "mock-api-key", }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -68,7 +68,7 @@ async def test_flow_cannot_connect(hass: HomeAssistant, mock_setup_entry) -> Non CONF_API_KEY: "mock-api-key", }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "AfterShip" assert result["data"] == { CONF_API_KEY: "mock-api-key", diff --git a/tests/components/agent_dvr/test_config_flow.py b/tests/components/agent_dvr/test_config_flow.py index 958ec97a3ca..fee8a40f4f7 100644 --- a/tests/components/agent_dvr/test_config_flow.py +++ b/tests/components/agent_dvr/test_config_flow.py @@ -2,12 +2,12 @@ import pytest -from homeassistant import data_entry_flow from homeassistant.components.agent_dvr import config_flow from homeassistant.components.agent_dvr.const import SERVER_URL from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_HOST, CONF_PORT, CONTENT_TYPE_JSON from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import init_integration @@ -25,7 +25,7 @@ async def test_show_user_form(hass: HomeAssistant) -> None: ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM async def test_user_device_exists_abort( @@ -40,7 +40,7 @@ async def test_user_device_exists_abort( data={CONF_HOST: "example.local", CONF_PORT: 8090}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT async def test_connection_error( @@ -58,7 +58,7 @@ async def test_connection_error( assert result["errors"]["base"] == "cannot_connect" assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM async def test_full_user_flow_implementation( @@ -83,7 +83,7 @@ async def test_full_user_flow_implementation( ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "example.local", CONF_PORT: 8090} @@ -93,7 +93,7 @@ async def test_full_user_flow_implementation( assert result["data"][CONF_PORT] == 8090 assert result["data"][SERVER_URL] == "http://example.local:8090/" assert result["title"] == "DESKTOP" - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY entries = hass.config_entries.async_entries(config_flow.DOMAIN) assert entries[0].unique_id == "c0715bba-c2d0-48ef-9e3e-bc81c9ea4447" diff --git a/tests/components/airly/test_config_flow.py b/tests/components/airly/test_config_flow.py index fcc024f7cee..96f4d95995b 100644 --- a/tests/components/airly/test_config_flow.py +++ b/tests/components/airly/test_config_flow.py @@ -4,11 +4,11 @@ from http import HTTPStatus from airly.exceptions import AirlyError -from homeassistant import data_entry_flow from homeassistant.components.airly.const import CONF_USE_NEAREST, DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import API_NEAREST_URL, API_POINT_URL @@ -29,7 +29,7 @@ async def test_show_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -83,7 +83,7 @@ async def test_invalid_location_for_point_and_nearest( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "wrong_location" @@ -113,7 +113,7 @@ async def test_create_entry( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == CONFIG[CONF_NAME] assert result["data"][CONF_LATITUDE] == CONFIG[CONF_LATITUDE] assert result["data"][CONF_LONGITUDE] == CONFIG[CONF_LONGITUDE] @@ -137,7 +137,7 @@ async def test_create_entry_with_nearest_method( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == CONFIG[CONF_NAME] assert result["data"][CONF_LATITUDE] == CONFIG[CONF_LATITUDE] assert result["data"][CONF_LONGITUDE] == CONFIG[CONF_LONGITUDE] diff --git a/tests/components/airnow/test_config_flow.py b/tests/components/airnow/test_config_flow.py index ece28a77a87..740adec4b00 100644 --- a/tests/components/airnow/test_config_flow.py +++ b/tests/components/airnow/test_config_flow.py @@ -5,10 +5,11 @@ from unittest.mock import AsyncMock, patch from pyairnow.errors import AirNowError, EmptyResponseError, InvalidKeyError import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.airnow.const import DOMAIN from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -18,11 +19,11 @@ async def test_form(hass: HomeAssistant, config, options, setup_airnow) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure(result["flow_id"], config) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == config assert result2["options"] == options @@ -140,7 +141,7 @@ async def test_options_flow(hass: HomeAssistant, setup_airnow) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" with patch( @@ -153,7 +154,7 @@ async def test_options_flow(hass: HomeAssistant, setup_airnow) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_RADIUS: 25, } diff --git a/tests/components/airq/test_config_flow.py b/tests/components/airq/test_config_flow.py index 9c5492eaa20..8c85e017367 100644 --- a/tests/components/airq/test_config_flow.py +++ b/tests/components/airq/test_config_flow.py @@ -34,7 +34,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -47,7 +47,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_DEVICE_INFO["name"] assert result2["data"] == TEST_USER_DATA @@ -63,7 +63,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: result["flow_id"], TEST_USER_DATA | {CONF_PASSWORD: "wrong_password"} ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -78,7 +78,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: result["flow_id"], TEST_USER_DATA ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -101,5 +101,5 @@ async def test_duplicate_error(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], TEST_USER_DATA ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/airthings/test_config_flow.py b/tests/components/airthings/test_config_flow.py index 2ea157f09b1..ff4cdfa30d2 100644 --- a/tests/components/airthings/test_config_flow.py +++ b/tests/components/airthings/test_config_flow.py @@ -24,7 +24,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -43,7 +43,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Airthings" assert result2["data"] == TEST_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -64,7 +64,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: TEST_DATA, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -83,7 +83,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: TEST_DATA, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -102,7 +102,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: TEST_DATA, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/airthings_ble/test_config_flow.py b/tests/components/airthings_ble/test_config_flow.py index edeb08abb74..f6a7098785b 100644 --- a/tests/components/airthings_ble/test_config_flow.py +++ b/tests/components/airthings_ble/test_config_flow.py @@ -43,7 +43,7 @@ async def test_bluetooth_discovery(hass: HomeAssistant) -> None: data=WAVE_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" assert result["description_placeholders"] == { "name": "Airthings Wave Plus (123456)" @@ -54,7 +54,7 @@ async def test_bluetooth_discovery(hass: HomeAssistant) -> None: result["flow_id"], user_input={"not": "empty"} ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Airthings Wave Plus (123456)" assert result["result"].unique_id == "cc:cc:cc:cc:cc:cc" @@ -67,7 +67,7 @@ async def test_bluetooth_discovery_no_BLEDevice(hass: HomeAssistant) -> None: context={"source": SOURCE_BLUETOOTH}, data=WAVE_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -87,7 +87,7 @@ async def test_bluetooth_discovery_airthings_ble_update_failed( data=WAVE_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == reason @@ -103,7 +103,7 @@ async def test_bluetooth_discovery_already_setup(hass: HomeAssistant) -> None: context={"source": SOURCE_BLUETOOTH}, data=WAVE_DEVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -127,7 +127,7 @@ async def test_user_setup(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None assert result["data_schema"] is not None @@ -146,7 +146,7 @@ async def test_user_setup(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Airthings Wave Plus (123456)" assert result["result"].unique_id == "cc:cc:cc:cc:cc:cc" @@ -160,7 +160,7 @@ async def test_user_setup_no_device(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -178,7 +178,7 @@ async def test_user_setup_existing_and_unknown_device(hass: HomeAssistant) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -196,7 +196,7 @@ async def test_user_setup_unknown_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -214,7 +214,7 @@ async def test_user_setup_unable_to_connect(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -227,5 +227,5 @@ async def test_unsupported_device(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" diff --git a/tests/components/airtouch5/test_config_flow.py b/tests/components/airtouch5/test_config_flow.py index 8b4b9890e57..9a294d5a4f5 100644 --- a/tests/components/airtouch5/test_config_flow.py +++ b/tests/components/airtouch5/test_config_flow.py @@ -17,7 +17,7 @@ async def test_success(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None host = "1.1.1.1" @@ -34,7 +34,7 @@ async def test_success(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == host assert result2["data"] == { "host": host, @@ -59,5 +59,5 @@ async def test_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/airvisual/test_config_flow.py b/tests/components/airvisual/test_config_flow.py index 8c5bbded662..b9643b17c07 100644 --- a/tests/components/airvisual/test_config_flow.py +++ b/tests/components/airvisual/test_config_flow.py @@ -11,7 +11,6 @@ from pyairvisual.cloud_api import ( from pyairvisual.errors import AirVisualError import pytest -from homeassistant import data_entry_flow from homeassistant.components.airvisual import ( CONF_CITY, CONF_INTEGRATION_TYPE, @@ -22,6 +21,7 @@ from homeassistant.components.airvisual import ( from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_SHOW_ON_MAP from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .conftest import ( COORDS_CONFIG, @@ -81,13 +81,13 @@ async def test_create_entry( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={"type": integration_type} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == input_form_step # Test errors that can arise: @@ -95,14 +95,14 @@ async def test_create_entry( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == errors # Test that we can recover and finish the flow after errors occur: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == entry_title assert result["data"] == {**config, CONF_INTEGRATION_TYPE: integration_type} @@ -112,7 +112,7 @@ async def test_duplicate_error(hass: HomeAssistant, config, setup_config_entry) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_init( @@ -120,13 +120,13 @@ async def test_duplicate_error(hass: HomeAssistant, config, setup_config_entry) context={"source": SOURCE_USER}, data={"type": INTEGRATION_TYPE_GEOGRAPHY_COORDS}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "geography_by_coords" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -135,13 +135,13 @@ async def test_options_flow( ) -> None: """Test config flow options.""" result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + 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_SHOW_ON_MAP: False} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_SHOW_ON_MAP: False} @@ -152,11 +152,11 @@ async def test_step_reauth( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_REAUTH}, data=config_entry.data ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" new_api_key = "defgh67890" @@ -164,7 +164,7 @@ async def test_step_reauth( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_API_KEY: new_api_key} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 diff --git a/tests/components/airvisual_pro/test_config_flow.py b/tests/components/airvisual_pro/test_config_flow.py index b0469b5288b..803a335f52c 100644 --- a/tests/components/airvisual_pro/test_config_flow.py +++ b/tests/components/airvisual_pro/test_config_flow.py @@ -9,11 +9,11 @@ from pyairvisual.node import ( ) import pytest -from homeassistant import data_entry_flow from homeassistant.components.airvisual_pro.const import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType pytestmark = pytest.mark.usefixtures("mock_setup_entry") @@ -34,7 +34,7 @@ async def test_create_entry( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # Test errors that can arise when connecting to a Pro: @@ -42,13 +42,13 @@ async def test_create_entry( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == connect_errors result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "192.168.1.101" assert result["data"] == { CONF_IP_ADDRESS: "192.168.1.101", @@ -63,13 +63,13 @@ async def test_duplicate_error( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -78,7 +78,7 @@ async def test_step_import(hass: HomeAssistant, config, setup_airvisual_pro) -> result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=config ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "192.168.1.101" assert result["data"] == { CONF_IP_ADDRESS: "192.168.1.101", @@ -114,7 +114,7 @@ async def test_reauth( }, data=config, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" # Test errors that can arise when connecting to a Pro: @@ -122,7 +122,7 @@ async def test_reauth( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "new_password"} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == connect_errors result = await hass.config_entries.flow.async_configure( @@ -132,6 +132,6 @@ async def test_reauth( # Allow reload to finish: await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 diff --git a/tests/components/airzone/test_config_flow.py b/tests/components/airzone/test_config_flow.py index c47e2b1a3dd..090674d5fd2 100644 --- a/tests/components/airzone/test_config_flow.py +++ b/tests/components/airzone/test_config_flow.py @@ -76,7 +76,7 @@ async def test_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -90,7 +90,7 @@ async def test_form(hass: HomeAssistant) -> None: entry = conf_entries[0] assert entry.state is ConfigEntryState.LOADED - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"Airzone {CONFIG[CONF_HOST]}:{CONFIG[CONF_PORT]}" assert result["data"][CONF_HOST] == CONFIG[CONF_HOST] assert result["data"][CONF_PORT] == CONFIG[CONF_PORT] @@ -132,7 +132,7 @@ async def test_form_invalid_system_id(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {CONF_ID: "invalid_system_id"} @@ -143,7 +143,7 @@ async def test_form_invalid_system_id(hass: HomeAssistant) -> None: result["flow_id"], CONFIG_ID1 ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -151,7 +151,7 @@ async def test_form_invalid_system_id(hass: HomeAssistant) -> None: entry = conf_entries[0] assert entry.state is ConfigEntryState.LOADED - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert ( result["title"] == f"Airzone {CONFIG_ID1[CONF_HOST]}:{CONFIG_ID1[CONF_PORT]}" @@ -208,7 +208,7 @@ async def test_dhcp_flow(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovered_connection" with ( @@ -244,7 +244,7 @@ async def test_dhcp_flow(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { CONF_HOST: TEST_IP, CONF_PORT: TEST_PORT, @@ -266,7 +266,7 @@ async def test_dhcp_flow_error(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -283,7 +283,7 @@ async def test_dhcp_connection_error(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovered_connection" with patch( @@ -338,7 +338,7 @@ async def test_dhcp_connection_error(hass: HomeAssistant) -> None: entry = conf_entries[0] assert entry.state is ConfigEntryState.LOADED - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"Airzone {short_mac(HVAC_WEBSERVER_MOCK[API_MAC])}" assert result["data"][CONF_HOST] == TEST_IP assert result["data"][CONF_PORT] == TEST_PORT @@ -359,7 +359,7 @@ async def test_dhcp_invalid_system_id(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovered_connection" with ( @@ -395,7 +395,7 @@ async def test_dhcp_invalid_system_id(hass: HomeAssistant) -> None: }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovered_connection" assert result["errors"] == {CONF_ID: "invalid_system_id"} @@ -416,7 +416,7 @@ async def test_dhcp_invalid_system_id(hass: HomeAssistant) -> None: entry = conf_entries[0] assert entry.state is ConfigEntryState.LOADED - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"Airzone {short_mac(DHCP_SERVICE_INFO.macaddress)}" assert result["data"][CONF_HOST] == TEST_IP assert result["data"][CONF_PORT] == TEST_PORT diff --git a/tests/components/airzone_cloud/test_config_flow.py b/tests/components/airzone_cloud/test_config_flow.py index e1d31e28d4b..86a70ced51a 100644 --- a/tests/components/airzone_cloud/test_config_flow.py +++ b/tests/components/airzone_cloud/test_config_flow.py @@ -4,11 +4,11 @@ from unittest.mock import patch from aioairzone_cloud.exceptions import AirzoneCloudError, LoginError -from homeassistant import data_entry_flow from homeassistant.components.airzone_cloud.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, ConfigEntryState from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .util import ( CONFIG, @@ -53,7 +53,7 @@ async def test_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -65,7 +65,7 @@ async def test_form(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -82,7 +82,7 @@ async def test_form(hass: HomeAssistant) -> None: entry = conf_entries[0] assert entry.state is ConfigEntryState.LOADED - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"House {WS_ID} ({CONFIG[CONF_ID]})" assert result["data"][CONF_ID] == CONFIG[CONF_ID] assert result["data"][CONF_USERNAME] == CONFIG[CONF_USERNAME] @@ -120,7 +120,7 @@ async def test_installations_list_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -132,7 +132,7 @@ async def test_installations_list_error(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/aladdin_connect/test_config_flow.py b/tests/components/aladdin_connect/test_config_flow.py index 90cf269b3f8..d41b88de8e4 100644 --- a/tests/components/aladdin_connect/test_config_flow.py +++ b/tests/components/aladdin_connect/test_config_flow.py @@ -20,7 +20,7 @@ async def test_form(hass: HomeAssistant, mock_aladdinconnect_api: MagicMock) -> result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -42,7 +42,7 @@ async def test_form(hass: HomeAssistant, mock_aladdinconnect_api: MagicMock) -> ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Aladdin Connect" assert result2["data"] == { CONF_USERNAME: "test-username", @@ -73,7 +73,7 @@ async def test_form_failed_auth( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -97,7 +97,7 @@ async def test_form_connection_timeout( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -159,7 +159,7 @@ async def test_reauth_flow( ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -178,7 +178,7 @@ async def test_reauth_flow( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert mock_entry.data == { CONF_USERNAME: "test-username", @@ -209,7 +209,7 @@ async def test_reauth_flow_auth_error( ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} mock_aladdinconnect_api.login.return_value = False mock_aladdinconnect_api.login.side_effect = InvalidPasswordError @@ -233,7 +233,7 @@ async def test_reauth_flow_auth_error( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -260,7 +260,7 @@ async def test_reauth_flow_connnection_error( ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} mock_aladdinconnect_api.login.side_effect = ClientConnectionError @@ -274,5 +274,5 @@ async def test_reauth_flow_connnection_error( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/alarmdecoder/test_config_flow.py b/tests/components/alarmdecoder/test_config_flow.py index 614d055405e..bd71795b4c9 100644 --- a/tests/components/alarmdecoder/test_config_flow.py +++ b/tests/components/alarmdecoder/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import patch from alarmdecoder.util import NoDeviceError import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.alarmdecoder import config_flow from homeassistant.components.alarmdecoder.const import ( CONF_ALT_NIGHT_MODE, @@ -31,6 +31,7 @@ from homeassistant.components.alarmdecoder.const import ( from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.const import CONF_HOST, CONF_PORT, CONF_PROTOCOL from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -63,7 +64,7 @@ async def test_setups(hass: HomeAssistant, protocol, connection, title) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -71,7 +72,7 @@ async def test_setups(hass: HomeAssistant, protocol, connection, title) -> None: {CONF_PROTOCOL: protocol}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "protocol" with ( @@ -85,7 +86,7 @@ async def test_setups(hass: HomeAssistant, protocol, connection, title) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], connection ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == title assert result["data"] == { **connection, @@ -108,7 +109,7 @@ async def test_setup_connection_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -116,7 +117,7 @@ async def test_setup_connection_error(hass: HomeAssistant) -> None: {CONF_PROTOCOL: protocol}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "protocol" with ( @@ -129,7 +130,7 @@ async def test_setup_connection_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], connection_settings ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} with ( @@ -142,7 +143,7 @@ async def test_setup_connection_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], connection_settings ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "unknown"} @@ -161,7 +162,7 @@ async def test_options_arm_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -169,7 +170,7 @@ async def test_options_arm_flow(hass: HomeAssistant) -> None: user_input={"edit_selection": "Arming Settings"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "arm_settings" with patch( @@ -180,7 +181,7 @@ async def test_options_arm_flow(hass: HomeAssistant) -> None: user_input=user_input, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert entry.options == { OPTIONS_ARM: user_input, OPTIONS_ZONES: DEFAULT_ZONE_OPTIONS, @@ -202,7 +203,7 @@ async def test_options_zone_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -210,7 +211,7 @@ async def test_options_zone_flow(hass: HomeAssistant) -> None: user_input={"edit_selection": "Zones"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zone_select" result = await hass.config_entries.options.async_configure( @@ -226,7 +227,7 @@ async def test_options_zone_flow(hass: HomeAssistant) -> None: user_input=zone_settings, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert entry.options == { OPTIONS_ARM: DEFAULT_ARM_OPTIONS, OPTIONS_ZONES: {zone_number: zone_settings}, @@ -235,7 +236,7 @@ async def test_options_zone_flow(hass: HomeAssistant) -> None: # Make sure zone can be removed... result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -243,7 +244,7 @@ async def test_options_zone_flow(hass: HomeAssistant) -> None: user_input={"edit_selection": "Zones"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zone_select" result = await hass.config_entries.options.async_configure( @@ -259,7 +260,7 @@ async def test_options_zone_flow(hass: HomeAssistant) -> None: user_input={}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert entry.options == { OPTIONS_ARM: DEFAULT_ARM_OPTIONS, OPTIONS_ZONES: {}, @@ -281,7 +282,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -289,7 +290,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant) -> None: user_input={"edit_selection": "Zones"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zone_select" # Zone Number must be int @@ -298,7 +299,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant) -> None: user_input={CONF_ZONE_NUMBER: "asd"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zone_select" assert result["errors"] == {CONF_ZONE_NUMBER: "int"} @@ -307,7 +308,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant) -> None: user_input={CONF_ZONE_NUMBER: zone_number}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zone_details" # CONF_RELAY_ADDR & CONF_RELAY_CHAN are inclusive @@ -316,7 +317,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant) -> None: user_input={**zone_settings, CONF_RELAY_ADDR: "1"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zone_details" assert result["errors"] == {"base": "relay_inclusive"} @@ -325,7 +326,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant) -> None: user_input={**zone_settings, CONF_RELAY_CHAN: "1"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zone_details" assert result["errors"] == {"base": "relay_inclusive"} @@ -335,7 +336,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant) -> None: user_input={**zone_settings, CONF_RELAY_ADDR: "abc", CONF_RELAY_CHAN: "abc"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zone_details" assert result["errors"] == { CONF_RELAY_ADDR: "int", @@ -348,7 +349,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant) -> None: user_input={**zone_settings, CONF_ZONE_LOOP: "1"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zone_details" assert result["errors"] == {CONF_ZONE_LOOP: "loop_rfid"} @@ -358,7 +359,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant) -> None: user_input={**zone_settings, CONF_ZONE_RFID: "rfid123", CONF_ZONE_LOOP: "ab"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zone_details" assert result["errors"] == {CONF_ZONE_LOOP: "int"} @@ -368,7 +369,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant) -> None: user_input={**zone_settings, CONF_ZONE_RFID: "rfid123", CONF_ZONE_LOOP: "5"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zone_details" assert result["errors"] == {CONF_ZONE_LOOP: "loop_range"} @@ -387,7 +388,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert entry.options == { OPTIONS_ARM: DEFAULT_ARM_OPTIONS, OPTIONS_ZONES: { @@ -435,7 +436,7 @@ async def test_one_device_allowed(hass: HomeAssistant, protocol, connection) -> DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -443,11 +444,11 @@ async def test_one_device_allowed(hass: HomeAssistant, protocol, connection) -> {CONF_PROTOCOL: protocol}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "protocol" result = await hass.config_entries.flow.async_configure( result["flow_id"], connection ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/amberelectric/test_config_flow.py b/tests/components/amberelectric/test_config_flow.py index 2624bd96d31..030b82d3596 100644 --- a/tests/components/amberelectric/test_config_flow.py +++ b/tests/components/amberelectric/test_config_flow.py @@ -8,7 +8,6 @@ from amberelectric import ApiException from amberelectric.model.site import Site, SiteStatus import pytest -from homeassistant import data_entry_flow from homeassistant.components.amberelectric.config_flow import filter_sites from homeassistant.components.amberelectric.const import ( CONF_SITE_ID, @@ -18,6 +17,7 @@ from homeassistant.components.amberelectric.const import ( from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_API_TOKEN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType API_KEY = "psk_123456789" @@ -131,7 +131,7 @@ async def test_single_pending_site( initial_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert initial_result.get("type") == data_entry_flow.FlowResultType.FORM + assert initial_result.get("type") is FlowResultType.FORM assert initial_result.get("step_id") == "user" # Test filling in API key @@ -140,7 +140,7 @@ async def test_single_pending_site( context={"source": SOURCE_USER}, data={CONF_API_TOKEN: API_KEY}, ) - assert enter_api_key_result.get("type") == data_entry_flow.FlowResultType.FORM + assert enter_api_key_result.get("type") is FlowResultType.FORM assert enter_api_key_result.get("step_id") == "site" select_site_result = await hass.config_entries.flow.async_configure( @@ -149,7 +149,7 @@ async def test_single_pending_site( ) # Show available sites - assert select_site_result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY + assert select_site_result.get("type") is FlowResultType.CREATE_ENTRY assert select_site_result.get("title") == "Home" data = select_site_result.get("data") assert data @@ -162,7 +162,7 @@ async def test_single_site(hass: HomeAssistant, single_site_api: Mock) -> None: initial_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert initial_result.get("type") == data_entry_flow.FlowResultType.FORM + assert initial_result.get("type") is FlowResultType.FORM assert initial_result.get("step_id") == "user" # Test filling in API key @@ -171,7 +171,7 @@ async def test_single_site(hass: HomeAssistant, single_site_api: Mock) -> None: context={"source": SOURCE_USER}, data={CONF_API_TOKEN: API_KEY}, ) - assert enter_api_key_result.get("type") == data_entry_flow.FlowResultType.FORM + assert enter_api_key_result.get("type") is FlowResultType.FORM assert enter_api_key_result.get("step_id") == "site" select_site_result = await hass.config_entries.flow.async_configure( @@ -180,7 +180,7 @@ async def test_single_site(hass: HomeAssistant, single_site_api: Mock) -> None: ) # Show available sites - assert select_site_result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY + assert select_site_result.get("type") is FlowResultType.CREATE_ENTRY assert select_site_result.get("title") == "Home" data = select_site_result.get("data") assert data @@ -195,7 +195,7 @@ async def test_single_site_rejoin( initial_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert initial_result.get("type") == data_entry_flow.FlowResultType.FORM + assert initial_result.get("type") is FlowResultType.FORM assert initial_result.get("step_id") == "user" # Test filling in API key @@ -204,7 +204,7 @@ async def test_single_site_rejoin( context={"source": SOURCE_USER}, data={CONF_API_TOKEN: API_KEY}, ) - assert enter_api_key_result.get("type") == data_entry_flow.FlowResultType.FORM + assert enter_api_key_result.get("type") is FlowResultType.FORM assert enter_api_key_result.get("step_id") == "site" select_site_result = await hass.config_entries.flow.async_configure( @@ -213,7 +213,7 @@ async def test_single_site_rejoin( ) # Show available sites - assert select_site_result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY + assert select_site_result.get("type") is FlowResultType.CREATE_ENTRY assert select_site_result.get("title") == "Home" data = select_site_result.get("data") assert data @@ -229,7 +229,7 @@ async def test_no_site(hass: HomeAssistant, no_site_api: Mock) -> None: data={CONF_API_TOKEN: "psk_123456789"}, ) - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM # Goes back to the user step assert result.get("step_id") == "user" assert result.get("errors") == {"api_token": "no_site"} @@ -240,7 +240,7 @@ async def test_invalid_key(hass: HomeAssistant, invalid_key_api: Mock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" # Test filling in API key @@ -249,7 +249,7 @@ async def test_invalid_key(hass: HomeAssistant, invalid_key_api: Mock) -> None: context={"source": SOURCE_USER}, data={CONF_API_TOKEN: "psk_123456789"}, ) - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM # Goes back to the user step assert result.get("step_id") == "user" assert result.get("errors") == {"api_token": "invalid_api_token"} @@ -260,7 +260,7 @@ async def test_unknown_error(hass: HomeAssistant, api_error: Mock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" # Test filling in API key @@ -269,7 +269,7 @@ async def test_unknown_error(hass: HomeAssistant, api_error: Mock) -> None: context={"source": SOURCE_USER}, data={CONF_API_TOKEN: "psk_123456789"}, ) - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM # Goes back to the user step assert result.get("step_id") == "user" assert result.get("errors") == {"api_token": "unknown_error"} diff --git a/tests/components/ambiclimate/test_config_flow.py b/tests/components/ambiclimate/test_config_flow.py index e9b85eaaa40..67c67aba4a8 100644 --- a/tests/components/ambiclimate/test_config_flow.py +++ b/tests/components/ambiclimate/test_config_flow.py @@ -5,12 +5,13 @@ from unittest.mock import AsyncMock, patch import ambiclimate import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.ambiclimate import config_flow from homeassistant.components.http import KEY_HASS from homeassistant.config import async_process_ha_core_config from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import AbortFlow, FlowResultType from homeassistant.setup import async_setup_component from homeassistant.util import aiohttp @@ -38,7 +39,7 @@ async def test_abort_if_no_implementation_registered(hass: HomeAssistant) -> Non flow.hass = hass result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "missing_configuration" @@ -51,12 +52,12 @@ async def test_abort_if_already_setup(hass: HomeAssistant) -> None: config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" - with pytest.raises(data_entry_flow.AbortFlow): + with pytest.raises(AbortFlow): result = await flow.async_step_code() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -66,7 +67,7 @@ async def test_full_flow_implementation(hass: HomeAssistant) -> None: flow = await init_config_flow(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert ( result["description_placeholders"]["cb_url"] @@ -81,7 +82,7 @@ async def test_full_flow_implementation(hass: HomeAssistant) -> None: with patch("ambiclimate.AmbiclimateOAuth.get_access_token", return_value="test"): result = await flow.async_step_code("123ABC") - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Ambiclimate" assert result["data"]["callback_url"] == "https://example.com/api/ambiclimate" assert result["data"][CONF_CLIENT_SECRET] == "secret" @@ -89,14 +90,14 @@ async def test_full_flow_implementation(hass: HomeAssistant) -> None: with patch("ambiclimate.AmbiclimateOAuth.get_access_token", return_value=None): result = await flow.async_step_code("123ABC") - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT with patch( "ambiclimate.AmbiclimateOAuth.get_access_token", side_effect=ambiclimate.AmbiclimateOauthError(), ): result = await flow.async_step_code("123ABC") - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT async def test_abort_invalid_code(hass: HomeAssistant) -> None: @@ -106,7 +107,7 @@ async def test_abort_invalid_code(hass: HomeAssistant) -> None: with patch("ambiclimate.AmbiclimateOAuth.get_access_token", return_value=None): result = await flow.async_step_code("invalid") - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "access_token" @@ -118,7 +119,7 @@ async def test_already_setup(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/ambient_station/test_config_flow.py b/tests/components/ambient_station/test_config_flow.py index ae3af962b0a..19ae9828c22 100644 --- a/tests/components/ambient_station/test_config_flow.py +++ b/tests/components/ambient_station/test_config_flow.py @@ -5,11 +5,11 @@ from unittest.mock import AsyncMock, patch from aioambient.errors import AmbientError import pytest -from homeassistant import data_entry_flow from homeassistant.components.ambient_station import CONF_APP_KEY, DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType @pytest.mark.parametrize( @@ -26,7 +26,7 @@ async def test_create_entry( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # Test errors that can arise: @@ -34,14 +34,14 @@ async def test_create_entry( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == errors # Test that we can recover and finish the flow after errors occur: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "67890fghij67" assert result["data"] == { CONF_API_KEY: "12345abcde12345abcde", @@ -56,5 +56,5 @@ async def test_duplicate_error( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/analytics_insights/test_config_flow.py b/tests/components/analytics_insights/test_config_flow.py index 16ca0812d7d..77264eb2439 100644 --- a/tests/components/analytics_insights/test_config_flow.py +++ b/tests/components/analytics_insights/test_config_flow.py @@ -63,7 +63,7 @@ async def test_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -71,7 +71,7 @@ async def test_form( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Home Assistant Analytics Insights" assert result["data"] == {} assert result["options"] == expected_options @@ -98,7 +98,7 @@ async def test_submitting_empty_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -106,7 +106,7 @@ async def test_submitting_empty_form( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "no_integrations_selected"} result = await hass.config_entries.flow.async_configure( @@ -118,7 +118,7 @@ async def test_submitting_empty_form( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Home Assistant Analytics Insights" assert result["data"] == {} assert result["options"] == { @@ -140,7 +140,7 @@ async def test_form_cannot_connect( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -161,7 +161,7 @@ async def test_form_already_configured( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -209,7 +209,7 @@ async def test_options_flow( await setup_integration(hass, mock_config_entry) result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM mock_analytics_client.get_integrations.reset_mock() result = await hass.config_entries.options.async_configure( @@ -218,7 +218,7 @@ async def test_options_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == expected_options await hass.async_block_till_done() mock_analytics_client.get_integrations.assert_called_once() @@ -244,7 +244,7 @@ async def test_submitting_empty_options_flow( await setup_integration(hass, mock_config_entry) result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -252,7 +252,7 @@ async def test_submitting_empty_options_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "no_integrations_selected"} result = await hass.config_entries.options.async_configure( @@ -264,7 +264,7 @@ async def test_submitting_empty_options_flow( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_TRACKED_INTEGRATIONS: ["youtube", "hue"], CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"], @@ -285,5 +285,5 @@ async def test_options_flow_cannot_connect( mock_config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" diff --git a/tests/components/android_ip_webcam/test_config_flow.py b/tests/components/android_ip_webcam/test_config_flow.py index 2e4522188eb..6e6e34fb9f8 100644 --- a/tests/components/android_ip_webcam/test_config_flow.py +++ b/tests/components/android_ip_webcam/test_config_flow.py @@ -20,7 +20,7 @@ async def test_form(hass: HomeAssistant, aioclient_mock_fixture) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -36,7 +36,7 @@ async def test_form(hass: HomeAssistant, aioclient_mock_fixture) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "1.1.1.1" assert result2["data"] == { "host": "1.1.1.1", @@ -55,7 +55,7 @@ async def test_device_already_configured( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -66,7 +66,7 @@ async def test_device_already_configured( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -87,7 +87,7 @@ async def test_form_invalid_auth( {"host": "1.1.1.1", "port": 8080, "username": "user", "password": "wrong-pass"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"username": "invalid_auth", "password": "invalid_auth"} @@ -110,5 +110,5 @@ async def test_form_cannot_connect( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/androidtv/test_config_flow.py b/tests/components/androidtv/test_config_flow.py index afebe9903ce..e2b5207c590 100644 --- a/tests/components/androidtv/test_config_flow.py +++ b/tests/components/androidtv/test_config_flow.py @@ -5,7 +5,6 @@ from unittest.mock import patch import pytest -from homeassistant import data_entry_flow from homeassistant.components.androidtv.config_flow import ( APPS_NEW_ID, CONF_APP_DELETE, @@ -37,6 +36,7 @@ from homeassistant.components.androidtv.const import ( from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_DEVICE_CLASS, CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .patchers import PATCH_ACCESS, PATCH_ISFILE, PATCH_SETUP_ENTRY @@ -104,7 +104,7 @@ async def test_user( flow_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER, "show_advanced_options": True} ) - assert flow_result["type"] == data_entry_flow.FlowResultType.FORM + assert flow_result["type"] is FlowResultType.FORM assert flow_result["step_id"] == "user" # test with all provided @@ -120,7 +120,7 @@ async def test_user( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"] == config @@ -148,7 +148,7 @@ async def test_user_adbkey(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"] == config_data @@ -166,7 +166,7 @@ async def test_error_both_key_server(hass: HomeAssistant) -> None: data=config_data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "key_and_server"} with ( @@ -181,7 +181,7 @@ async def test_error_both_key_server(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == HOST assert result2["data"] == CONFIG_ADB_SERVER @@ -196,7 +196,7 @@ async def test_error_invalid_key(hass: HomeAssistant) -> None: data=config_data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "adbkey_not_file"} with ( @@ -211,7 +211,7 @@ async def test_error_invalid_key(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == HOST assert result2["data"] == CONFIG_ADB_SERVER @@ -244,7 +244,7 @@ async def test_invalid_mac( data=config, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_unique_id" @@ -262,7 +262,7 @@ async def test_abort_if_host_exist(hass: HomeAssistant) -> None: data=config_data, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -285,7 +285,7 @@ async def test_abort_if_unique_exist(hass: HomeAssistant) -> None: data=CONFIG_ADB_SERVER, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -300,7 +300,7 @@ async def test_on_connect_failed(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( flow_result["flow_id"], user_input=CONFIG_ADB_SERVER ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} with patch( @@ -310,7 +310,7 @@ async def test_on_connect_failed(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONFIG_ADB_SERVER ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} with ( @@ -325,7 +325,7 @@ async def test_on_connect_failed(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == HOST assert result3["data"] == CONFIG_ADB_SERVER @@ -348,7 +348,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" # test app form with existing app @@ -358,7 +358,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: CONF_APPS: "app1", }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "apps" # test change value in apps form @@ -368,7 +368,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: CONF_APP_NAME: "Appl1", }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" # test app form with new app @@ -378,7 +378,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: CONF_APPS: APPS_NEW_ID, }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "apps" # test save value for new app @@ -389,7 +389,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: CONF_APP_NAME: "Appl2", }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" # test app form for delete @@ -399,7 +399,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: CONF_APPS: "app1", }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "apps" # test delete app1 @@ -410,7 +410,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: CONF_APP_DELETE: True, }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" # test rules form with existing rule @@ -420,7 +420,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: CONF_STATE_DETECTION_RULES: "com.plexapp.android", }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "rules" # test change value in rule form with invalid json rule @@ -430,7 +430,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: CONF_RULE_VALUES: "a", }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "rules" assert result["errors"] == {"base": "invalid_det_rules"} @@ -441,7 +441,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: CONF_RULE_VALUES: {"a": "b"}, }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "rules" assert result["errors"] == {"base": "invalid_det_rules"} @@ -452,7 +452,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: CONF_RULE_VALUES: ["standby"], }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" # test rule form with new rule @@ -462,7 +462,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: CONF_STATE_DETECTION_RULES: RULES_NEW_ID, }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "rules" # test save value for new rule @@ -473,7 +473,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: CONF_RULE_VALUES: VALID_DETECT_RULE, }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" # test rules form with delete existing rule @@ -483,7 +483,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: CONF_STATE_DETECTION_RULES: "com.plexapp.android", }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "rules" # test delete rule @@ -493,7 +493,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: CONF_RULE_DELETE: True, }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -507,7 +507,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY apps_options = config_entry.options[CONF_APPS] assert apps_options.get("app1") is None diff --git a/tests/components/androidtv_remote/test_config_flow.py b/tests/components/androidtv_remote/test_config_flow.py index f4e141ce952..eb51f9465c3 100644 --- a/tests/components/androidtv_remote/test_config_flow.py +++ b/tests/components/androidtv_remote/test_config_flow.py @@ -25,7 +25,7 @@ async def test_user_flow_success( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert "host" in result["data_schema"].schema assert not result["errors"] @@ -44,7 +44,7 @@ async def test_user_flow_success( result["flow_id"], {"host": host} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair" assert "pin" in result["data_schema"].schema assert not result["errors"] @@ -58,7 +58,7 @@ async def test_user_flow_success( result["flow_id"], {"pin": pin} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == name assert result["data"] == {"host": host, "name": name, "mac": mac} assert result["context"]["source"] == "user" @@ -85,7 +85,7 @@ async def test_user_flow_cannot_connect( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert "host" in result["data_schema"].schema assert not result["errors"] @@ -99,7 +99,7 @@ async def test_user_flow_cannot_connect( result["flow_id"], {"host": host} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert "host" in result["data_schema"].schema assert result["errors"] == {"base": "cannot_connect"} @@ -127,7 +127,7 @@ async def test_user_flow_pairing_invalid_auth( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert "host" in result["data_schema"].schema assert not result["errors"] @@ -145,7 +145,7 @@ async def test_user_flow_pairing_invalid_auth( result["flow_id"], {"host": host} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair" assert "pin" in result["data_schema"].schema assert not result["errors"] @@ -159,7 +159,7 @@ async def test_user_flow_pairing_invalid_auth( result["flow_id"], {"pin": pin} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair" assert "pin" in result["data_schema"].schema assert result["errors"] == {"base": "invalid_auth"} @@ -189,7 +189,7 @@ async def test_user_flow_pairing_connection_closed( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert "host" in result["data_schema"].schema assert not result["errors"] @@ -207,7 +207,7 @@ async def test_user_flow_pairing_connection_closed( result["flow_id"], {"host": host} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair" assert "pin" in result["data_schema"].schema assert not result["errors"] @@ -221,7 +221,7 @@ async def test_user_flow_pairing_connection_closed( result["flow_id"], {"pin": pin} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair" assert "pin" in result["data_schema"].schema assert not result["errors"] @@ -251,7 +251,7 @@ async def test_user_flow_pairing_connection_closed_followed_by_cannot_connect( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert "host" in result["data_schema"].schema assert not result["errors"] @@ -269,7 +269,7 @@ async def test_user_flow_pairing_connection_closed_followed_by_cannot_connect( result["flow_id"], {"host": host} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair" assert "pin" in result["data_schema"].schema assert not result["errors"] @@ -283,7 +283,7 @@ async def test_user_flow_pairing_connection_closed_followed_by_cannot_connect( result["flow_id"], {"pin": pin} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" mock_api.async_finish_pairing.assert_called_with(pin) @@ -328,7 +328,7 @@ async def test_user_flow_already_configured_host_changed_reloads_entry( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert "host" in result["data_schema"].schema assert not result["errors"] @@ -340,7 +340,7 @@ async def test_user_flow_already_configured_host_changed_reloads_entry( result["flow_id"], {"host": host} ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" mock_api.async_generate_cert_if_missing.assert_called() @@ -387,7 +387,7 @@ async def test_user_flow_already_configured_host_not_changed_no_reload_entry( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert "host" in result["data_schema"].schema assert not result["errors"] @@ -399,7 +399,7 @@ async def test_user_flow_already_configured_host_not_changed_no_reload_entry( result["flow_id"], {"host": host} ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" mock_api.async_generate_cert_if_missing.assert_called() @@ -442,7 +442,7 @@ async def test_zeroconf_flow_success( properties={"bt": mac}, ), ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zeroconf_confirm" assert not result["data_schema"] @@ -461,7 +461,7 @@ async def test_zeroconf_flow_success( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair" assert "pin" in result["data_schema"].schema assert not result["errors"] @@ -475,7 +475,7 @@ async def test_zeroconf_flow_success( result["flow_id"], {"pin": pin} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == name assert result["data"] == { "host": host, @@ -520,7 +520,7 @@ async def test_zeroconf_flow_cannot_connect( properties={"bt": mac}, ), ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zeroconf_confirm" assert not result["data_schema"] @@ -531,7 +531,7 @@ async def test_zeroconf_flow_cannot_connect( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" mock_api.async_generate_cert_if_missing.assert_called() @@ -571,7 +571,7 @@ async def test_zeroconf_flow_pairing_invalid_auth( properties={"bt": mac}, ), ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zeroconf_confirm" assert not result["data_schema"] @@ -582,7 +582,7 @@ async def test_zeroconf_flow_pairing_invalid_auth( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair" assert "pin" in result["data_schema"].schema assert not result["errors"] @@ -596,7 +596,7 @@ async def test_zeroconf_flow_pairing_invalid_auth( result["flow_id"], {"pin": pin} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair" assert "pin" in result["data_schema"].schema assert result["errors"] == {"base": "invalid_auth"} @@ -654,7 +654,7 @@ async def test_zeroconf_flow_already_configured_host_changed_reloads_entry( properties={"bt": mac}, ), ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" await hass.async_block_till_done() @@ -707,7 +707,7 @@ async def test_zeroconf_flow_already_configured_host_not_changed_no_reload_entry properties={"bt": mac}, ), ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" await hass.async_block_till_done() @@ -740,7 +740,7 @@ async def test_zeroconf_flow_abort_if_mac_is_missing( properties={}, ), ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -785,7 +785,7 @@ async def test_reauth_flow_success( mock_api.async_start_pairing = AsyncMock(return_value=None) result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair" assert "pin" in result["data_schema"].schema assert not result["errors"] @@ -799,7 +799,7 @@ async def test_reauth_flow_success( result = await hass.config_entries.flow.async_configure( result["flow_id"], {"pin": pin} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" mock_api.async_finish_pairing.assert_called_with(pin) @@ -854,7 +854,7 @@ async def test_reauth_flow_cannot_connect( mock_api.async_start_pairing = AsyncMock(side_effect=CannotConnect()) result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/anova/test_config_flow.py b/tests/components/anova/test_config_flow.py index 6ea988dc53a..b92c50c40b0 100644 --- a/tests/components/anova/test_config_flow.py +++ b/tests/components/anova/test_config_flow.py @@ -4,10 +4,11 @@ from unittest.mock import patch from anova_wifi import AnovaPrecisionCooker, InvalidLogin, NoDevicesFound -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.anova.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import CONF_INPUT, DEVICE_UNIQUE_ID, create_entry @@ -41,7 +42,7 @@ async def test_flow_user( result["flow_id"], user_input=CONF_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_USERNAME: "sample@gmail.com", CONF_PASSWORD: "sample", @@ -76,7 +77,7 @@ async def test_flow_user_already_configured(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -94,7 +95,7 @@ async def test_flow_wrong_login(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -112,7 +113,7 @@ async def test_flow_unknown_error(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "unknown"} @@ -133,5 +134,5 @@ async def test_flow_no_devices(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "no_devices_found"} diff --git a/tests/components/anthemav/test_config_flow.py b/tests/components/anthemav/test_config_flow.py index a0a6bf82762..ee2f1da00e9 100644 --- a/tests/components/anthemav/test_config_flow.py +++ b/tests/components/anthemav/test_config_flow.py @@ -19,7 +19,7 @@ async def test_form_with_valid_connection( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -36,7 +36,7 @@ async def test_form_with_valid_connection( await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Anthem AV" assert result2["data"] == { "host": "1.1.1.1", @@ -67,7 +67,7 @@ async def test_form_device_info_error(hass: HomeAssistant) -> None: await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_receive_deviceinfo"} @@ -91,7 +91,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -112,5 +112,5 @@ async def test_device_already_configured( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" diff --git a/tests/components/aosmith/test_config_flow.py b/tests/components/aosmith/test_config_flow.py index 32f259f552c..991d4129392 100644 --- a/tests/components/aosmith/test_config_flow.py +++ b/tests/components/aosmith/test_config_flow.py @@ -27,7 +27,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -40,7 +40,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == FIXTURE_USER_INPUT[CONF_EMAIL] assert result2["data"] == FIXTURE_USER_INPUT assert len(mock_setup_entry.mock_calls) == 1 @@ -63,7 +63,7 @@ async def test_form_exception( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -74,7 +74,7 @@ async def test_form_exception( result["flow_id"], FIXTURE_USER_INPUT, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": expected_error_key} with patch( @@ -87,7 +87,7 @@ async def test_form_exception( ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == FIXTURE_USER_INPUT[CONF_EMAIL] assert result3["data"] == FIXTURE_USER_INPUT assert len(mock_setup_entry.mock_calls) == 1 @@ -141,7 +141,7 @@ async def test_reauth_flow( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -178,7 +178,7 @@ async def test_reauth_flow_retry( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} # Second attempt at reauth - authentication succeeds @@ -195,5 +195,5 @@ async def test_reauth_flow_retry( ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" diff --git a/tests/components/apcupsd/test_config_flow.py b/tests/components/apcupsd/test_config_flow.py index bed0e78ad55..2888771eb01 100644 --- a/tests/components/apcupsd/test_config_flow.py +++ b/tests/components/apcupsd/test_config_flow.py @@ -33,7 +33,7 @@ async def test_config_flow_cannot_connect(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, data=CONF_DATA, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == "cannot_connect" @@ -63,7 +63,7 @@ async def test_config_flow_duplicate(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, data=CONF_DATA, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" # Then, we create the integration once again using a different port. However, @@ -78,7 +78,7 @@ async def test_config_flow_duplicate(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, data=another_host, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" # Now we change the serial number and add it again. This should be successful. @@ -91,7 +91,7 @@ async def test_config_flow_duplicate(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, data=another_host, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == another_host @@ -105,14 +105,14 @@ async def test_flow_works(hass: HomeAssistant) -> None: DOMAIN, context={CONF_SOURCE: SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONF_DATA ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_STATUS["UPSNAME"] assert result["data"] == CONF_DATA @@ -147,7 +147,7 @@ async def test_flow_minimal_status( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == CONF_DATA assert result["title"] == expected_title mock_setup.assert_called_once() diff --git a/tests/components/apple_tv/test_config_flow.py b/tests/components/apple_tv/test_config_flow.py index 28d87ef1b03..e7025890ec4 100644 --- a/tests/components/apple_tv/test_config_flow.py +++ b/tests/components/apple_tv/test_config_flow.py @@ -7,7 +7,7 @@ from pyatv import exceptions from pyatv.const import PairingRequirement, Protocol import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.apple_tv import CONF_ADDRESS, config_flow from homeassistant.components.apple_tv.const import ( @@ -16,6 +16,7 @@ from homeassistant.components.apple_tv.const import ( DOMAIN, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .common import airplay_service, create_conf, mrp_service, raop_service @@ -72,7 +73,7 @@ async def test_user_input_device_not_found(hass: HomeAssistant, mrp_device) -> N result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( @@ -80,7 +81,7 @@ async def test_user_input_device_not_found(hass: HomeAssistant, mrp_device) -> N {"device_input": "none"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "no_devices_found"} @@ -96,7 +97,7 @@ async def test_user_input_unexpected_error(hass: HomeAssistant, mock_scan) -> No {"device_input": "dummy"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -105,31 +106,31 @@ async def test_user_adds_full_device(hass: HomeAssistant, full_device, pairing) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"device_input": "MRP Device"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["description_placeholders"] == { "name": "MRP Device", "type": "Unknown", } result3 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result3["type"] == data_entry_flow.FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["description_placeholders"] == {"protocol": "MRP"} result4 = await hass.config_entries.flow.async_configure( result["flow_id"], {"pin": 1111} ) - assert result4["type"] == data_entry_flow.FlowResultType.FORM + assert result4["type"] is FlowResultType.FORM assert result4["description_placeholders"] == {"protocol": "DMAP", "pin": "1111"} result5 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result5["type"] == data_entry_flow.FlowResultType.FORM + assert result5["type"] is FlowResultType.FORM assert result5["description_placeholders"] == {"protocol": "AirPlay"} result6 = await hass.config_entries.flow.async_configure( @@ -160,14 +161,14 @@ async def test_user_adds_dmap_device( result["flow_id"], {"device_input": "DMAP Device"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["description_placeholders"] == { "name": "DMAP Device", "type": "Unknown", } result3 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result3["type"] == data_entry_flow.FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["description_placeholders"] == {"pin": "1111", "protocol": "DMAP"} result6 = await hass.config_entries.flow.async_configure( @@ -200,7 +201,7 @@ async def test_user_adds_dmap_device_failed( await hass.config_entries.flow.async_configure(result["flow_id"], {}) result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "device_did_not_pair" @@ -216,7 +217,7 @@ async def test_user_adds_device_with_ip_filter( result["flow_id"], {"device_input": "127.0.0.1"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["description_placeholders"] == { "name": "DMAP Device", "type": "Unknown", @@ -277,7 +278,7 @@ async def test_user_adds_existing_device(hass: HomeAssistant, mrp_device) -> Non result["flow_id"], {"device_input": "127.0.0.1"}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "already_configured"} @@ -305,7 +306,7 @@ async def test_user_connection_failed( result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "setup_failed" @@ -328,7 +329,7 @@ async def test_user_start_pair_error_failed( result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "invalid_auth" @@ -349,14 +350,14 @@ async def test_user_pair_service_with_password( result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "password" result3 = await hass.config_entries.flow.async_configure( result["flow_id"], {}, ) - assert result3["type"] == data_entry_flow.FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "setup_failed" @@ -378,14 +379,14 @@ async def test_user_pair_disabled_service( result["flow_id"], {}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "protocol_disabled" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "setup_failed" @@ -407,7 +408,7 @@ async def test_user_pair_ignore_unsupported( result["flow_id"], {}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "setup_failed" @@ -435,7 +436,7 @@ async def test_user_pair_invalid_pin( result["flow_id"], {"pin": 1111}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -463,7 +464,7 @@ async def test_user_pair_unexpected_error( result["flow_id"], {"pin": 1111}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -486,7 +487,7 @@ async def test_user_pair_backoff_error( result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "backoff" @@ -509,7 +510,7 @@ async def test_user_pair_begin_unexpected_error( result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "unknown" @@ -526,14 +527,14 @@ async def test_ignores_disabled_service( result["flow_id"], {"device_input": "mrpid"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["description_placeholders"] == { "name": "AirPlay Device", "type": "Unknown", } result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["description_placeholders"] == {"protocol": "AirPlay"} result3 = await hass.config_entries.flow.async_configure( @@ -568,7 +569,7 @@ async def test_zeroconf_unsupported_service_aborts(hass: HomeAssistant) -> None: properties={}, ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -589,7 +590,7 @@ async def test_zeroconf_add_mrp_device( type="_mediaremotetv._tcp.local.", ), ) - assert unrelated_result["type"] == data_entry_flow.FlowResultType.FORM + assert unrelated_result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_init( DOMAIN, @@ -604,7 +605,7 @@ async def test_zeroconf_add_mrp_device( type="_mediaremotetv._tcp.local.", ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["description_placeholders"] == { "name": "MRP Device", "type": "Unknown", @@ -614,7 +615,7 @@ async def test_zeroconf_add_mrp_device( result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["description_placeholders"] == {"protocol": "MRP"} result3 = await hass.config_entries.flow.async_configure( @@ -636,7 +637,7 @@ async def test_zeroconf_add_dmap_device( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=DMAP_SERVICE ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["description_placeholders"] == { "name": "DMAP Device", "type": "Unknown", @@ -646,7 +647,7 @@ async def test_zeroconf_add_dmap_device( result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["description_placeholders"] == {"protocol": "DMAP", "pin": "1111"} result3 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) @@ -685,7 +686,7 @@ async def test_zeroconf_ip_change(hass: HomeAssistant, mock_scan) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(mock_async_setup.mock_calls) == 2 assert entry.data[CONF_ADDRESS] == "127.0.0.1" @@ -723,7 +724,7 @@ async def test_zeroconf_ip_change_after_ip_conflict_with_ignored_entry( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(mock_async_setup.mock_calls) == 1 assert entry.data[CONF_ADDRESS] == "127.0.0.1" @@ -764,7 +765,7 @@ async def test_zeroconf_ip_change_via_secondary_identifier( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(mock_async_setup.mock_calls) == 2 assert entry.data[CONF_ADDRESS] == "127.0.0.1" @@ -807,7 +808,7 @@ async def test_zeroconf_updates_identifiers_for_ignored_entries( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert ( len(mock_async_setup.mock_calls) == 0 @@ -826,7 +827,7 @@ async def test_zeroconf_add_existing_aborts(hass: HomeAssistant, dmap_device) -> result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=DMAP_SERVICE ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -837,7 +838,7 @@ async def test_zeroconf_add_but_device_not_found( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=DMAP_SERVICE ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -848,7 +849,7 @@ async def test_zeroconf_add_existing_device(hass: HomeAssistant, dmap_device) -> result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=DMAP_SERVICE ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -859,7 +860,7 @@ async def test_zeroconf_unexpected_error(hass: HomeAssistant, mock_scan) -> None result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=DMAP_SERVICE ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -885,7 +886,7 @@ async def test_zeroconf_abort_if_other_in_progress( ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" mock_scan.result = [ @@ -907,7 +908,7 @@ async def test_zeroconf_abort_if_other_in_progress( properties={"UniqueIdentifier": "mrpid", "Name": "Kitchen"}, ), ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_in_progress" @@ -965,7 +966,7 @@ async def test_zeroconf_missing_device_during_protocol_resolve( result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "device_not_found" @@ -1025,7 +1026,7 @@ async def test_zeroconf_additional_protocol_resolve_failure( result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "inconsistent_device" @@ -1051,7 +1052,7 @@ async def test_zeroconf_pair_additionally_found_protocols( properties={"deviceid": "airplayid"}, ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM await hass.async_block_till_done() mock_scan.result = [ @@ -1102,7 +1103,7 @@ async def test_zeroconf_pair_additionally_found_protocols( {}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pair_no_pin" assert result2["description_placeholders"] == {"pin": ANY, "protocol": "RAOP"} @@ -1112,7 +1113,7 @@ async def test_zeroconf_pair_additionally_found_protocols( {}, ) - assert result3["type"] == data_entry_flow.FlowResultType.FORM + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "pair_with_pin" assert result3["description_placeholders"] == {"protocol": "MRP"} @@ -1120,7 +1121,7 @@ async def test_zeroconf_pair_additionally_found_protocols( result["flow_id"], {"pin": 1234}, ) - assert result4["type"] == data_entry_flow.FlowResultType.FORM + assert result4["type"] is FlowResultType.FORM assert result4["step_id"] == "pair_with_pin" assert result4["description_placeholders"] == {"protocol": "AirPlay"} @@ -1128,7 +1129,7 @@ async def test_zeroconf_pair_additionally_found_protocols( result["flow_id"], {"pin": 1234}, ) - assert result5["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result5["type"] is FlowResultType.CREATE_ENTRY async def test_zeroconf_mismatch( @@ -1157,14 +1158,14 @@ async def test_zeroconf_mismatch( properties={"deviceid": "airplayid"}, ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM await hass.async_block_till_done() result = await hass.config_entries.flow.async_configure( result["flow_id"], {}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "setup_failed" @@ -1190,13 +1191,13 @@ async def test_reconfigure_update_credentials( result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["description_placeholders"] == {"protocol": "MRP"} result3 = await hass.config_entries.flow.async_configure( result["flow_id"], {"pin": 1111} ) - assert result3["type"] == data_entry_flow.FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" assert config_entry.data == { @@ -1218,7 +1219,7 @@ async def test_option_start_off(hass: HomeAssistant) -> None: config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_START_OFF: True} @@ -1243,5 +1244,5 @@ async def test_zeroconf_rejects_ipv6(hass: HomeAssistant) -> None: properties={"CtlN": "Apple TV"}, ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "ipv6_not_supported" diff --git a/tests/components/aranet/test_config_flow.py b/tests/components/aranet/test_config_flow.py index f3558c66daf..d278e98be0c 100644 --- a/tests/components/aranet/test_config_flow.py +++ b/tests/components/aranet/test_config_flow.py @@ -24,13 +24,13 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=VALID_DATA_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch("homeassistant.components.aranet.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Aranet4 12345" assert result2["data"] == {} assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" @@ -43,7 +43,7 @@ async def test_async_step_bluetooth_not_aranet4(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_ARANET4_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant) -> None: @@ -59,7 +59,7 @@ async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant) - context={"source": config_entries.SOURCE_BLUETOOTH}, data=VALID_DATA_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -70,7 +70,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=VALID_DATA_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -78,7 +78,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=VALID_DATA_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -91,7 +91,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=VALID_DATA_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -102,14 +102,14 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch("homeassistant.components.aranet.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Aranet4 12345" assert result2["data"] == {} assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" @@ -124,7 +124,7 @@ async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -138,7 +138,7 @@ async def test_async_step_user_only_other_devices_found(hass: HomeAssistant) -> DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -152,14 +152,14 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch("homeassistant.components.aranet.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Aranet4 12345" assert result2["data"] == {} assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" @@ -175,7 +175,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -189,7 +189,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -211,7 +211,7 @@ async def test_async_step_user_with_found_devices_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -225,14 +225,14 @@ async def test_async_step_user_old_firmware(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch("homeassistant.components.aranet.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "outdated_version" @@ -246,12 +246,12 @@ async def test_async_step_user_integrations_disabled(hass: HomeAssistant) -> Non DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch("homeassistant.components.aranet.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "integrations_disabled" diff --git a/tests/components/arcam_fmj/test_config_flow.py b/tests/components/arcam_fmj/test_config_flow.py index 470a91feb3b..65991c313ee 100644 --- a/tests/components/arcam_fmj/test_config_flow.py +++ b/tests/components/arcam_fmj/test_config_flow.py @@ -6,13 +6,13 @@ from unittest.mock import AsyncMock, patch from arcam.fmj.client import ConnectionFailed import pytest -from homeassistant import data_entry_flow from homeassistant.components import ssdp from homeassistant.components.arcam_fmj.config_flow import get_entry_client from homeassistant.components.arcam_fmj.const import DOMAIN, DOMAIN_DATA_ENTRIES from homeassistant.config_entries import SOURCE_SSDP, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SOURCE from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .conftest import ( MOCK_CONFIG_ENTRY, @@ -68,11 +68,11 @@ async def test_ssdp(hass: HomeAssistant, dummy_client) -> None: context={CONF_SOURCE: SOURCE_SSDP}, data=MOCK_DISCOVER, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"Arcam FMJ ({MOCK_HOST})" assert result["data"] == MOCK_CONFIG_ENTRY @@ -89,7 +89,7 @@ async def test_ssdp_abort(hass: HomeAssistant) -> None: context={CONF_SOURCE: SOURCE_SSDP}, data=MOCK_DISCOVER, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -102,11 +102,11 @@ async def test_ssdp_unable_to_connect(hass: HomeAssistant, dummy_client) -> None context={CONF_SOURCE: SOURCE_SSDP}, data=MOCK_DISCOVER, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -121,7 +121,7 @@ async def test_ssdp_invalid_id(hass: HomeAssistant, dummy_client) -> None: context={CONF_SOURCE: SOURCE_SSDP}, data=discover, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -140,7 +140,7 @@ async def test_ssdp_update(hass: HomeAssistant) -> None: context={CONF_SOURCE: SOURCE_SSDP}, data=MOCK_DISCOVER, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == MOCK_HOST @@ -155,7 +155,7 @@ async def test_user(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -> data=None, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" user_input = { @@ -167,7 +167,7 @@ async def test_user(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -> result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"Arcam FMJ ({MOCK_HOST})" assert result["data"] == MOCK_CONFIG_ENTRY assert result["result"].unique_id == MOCK_UUID @@ -188,7 +188,7 @@ async def test_invalid_ssdp( context={CONF_SOURCE: SOURCE_USER}, data=user_input, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"Arcam FMJ ({MOCK_HOST})" assert result["data"] == MOCK_CONFIG_ENTRY assert result["result"].unique_id is None @@ -209,7 +209,7 @@ async def test_user_wrong( context={CONF_SOURCE: SOURCE_USER}, data=user_input, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"Arcam FMJ ({MOCK_HOST})" assert result["result"].unique_id is None diff --git a/tests/components/aseko_pool_live/test_config_flow.py b/tests/components/aseko_pool_live/test_config_flow.py index 2566dfd61e6..4307e527cee 100644 --- a/tests/components/aseko_pool_live/test_config_flow.py +++ b/tests/components/aseko_pool_live/test_config_flow.py @@ -19,7 +19,7 @@ async def test_async_step_user_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} @@ -48,7 +48,7 @@ async def test_async_step_user_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "aseko@example.com" assert result2["data"] == { CONF_EMAIL: "aseko@example.com", @@ -86,7 +86,7 @@ async def test_async_step_user_exception( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": reason} @@ -119,7 +119,7 @@ async def test_get_account_info_exceptions( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": reason} @@ -141,7 +141,7 @@ async def test_async_step_reauth_success(hass: HomeAssistant) -> None: }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -154,7 +154,7 @@ async def test_async_step_reauth_success(hass: HomeAssistant) -> None: {CONF_EMAIL: "aseko@example.com", CONF_PASSWORD: "passw0rd"}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 @@ -200,5 +200,5 @@ async def test_async_step_reauth_exception( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": reason} diff --git a/tests/components/asuswrt/test_config_flow.py b/tests/components/asuswrt/test_config_flow.py index 08ab2ae6c98..14b70811cde 100644 --- a/tests/components/asuswrt/test_config_flow.py +++ b/tests/components/asuswrt/test_config_flow.py @@ -6,7 +6,6 @@ from unittest.mock import patch from pyasuswrt import AsusWrtError import pytest -from homeassistant import data_entry_flow from homeassistant.components.asuswrt.const import ( CONF_DNSMASQ, CONF_INTERFACE, @@ -32,6 +31,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .common import ASUSWRT_BASE, HOST, ROUTER_MAC_ADDR @@ -90,7 +90,7 @@ async def test_user_legacy( flow_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER, "show_advanced_options": True} ) - assert flow_result["type"] == data_entry_flow.FlowResultType.FORM + assert flow_result["type"] is FlowResultType.FORM assert flow_result["step_id"] == "user" connect_legacy.return_value.async_get_nvram.return_value = unique_id @@ -101,7 +101,7 @@ async def test_user_legacy( ) await hass.async_block_till_done() - assert legacy_result["type"] == data_entry_flow.FlowResultType.FORM + assert legacy_result["type"] is FlowResultType.FORM assert legacy_result["step_id"] == "legacy" # complete configuration @@ -110,7 +110,7 @@ async def test_user_legacy( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"] == {**CONFIG_DATA_TELNET, CONF_MODE: MODE_AP} @@ -125,7 +125,7 @@ async def test_user_http( flow_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER, "show_advanced_options": True} ) - assert flow_result["type"] == data_entry_flow.FlowResultType.FORM + assert flow_result["type"] is FlowResultType.FORM assert flow_result["step_id"] == "user" connect_http.return_value.mac = unique_id @@ -136,7 +136,7 @@ async def test_user_http( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"] == CONFIG_DATA_HTTP @@ -153,7 +153,7 @@ async def test_error_pwd_required(hass: HomeAssistant, config) -> None: data=config_data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_BASE: "pwd_required"} @@ -166,7 +166,7 @@ async def test_error_no_password_ssh(hass: HomeAssistant) -> None: data=config_data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_BASE: "pwd_or_ssh"} @@ -182,7 +182,7 @@ async def test_error_invalid_ssh(hass: HomeAssistant, patch_is_file) -> None: data=config_data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_BASE: "ssh_not_file"} @@ -195,7 +195,7 @@ async def test_error_invalid_host(hass: HomeAssistant, patch_get_host) -> None: data=CONFIG_DATA_TELNET, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_BASE: "invalid_host"} @@ -211,7 +211,7 @@ async def test_abort_if_not_unique_id_setup(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, data=CONFIG_DATA_TELNET, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_unique_id" @@ -233,7 +233,7 @@ async def test_update_uniqueid_exist( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"] == CONFIG_DATA_HTTP prev_entry = hass.config_entries.async_get_entry(existing_entry.entry_id) @@ -255,7 +255,7 @@ async def test_abort_invalid_unique_id(hass: HomeAssistant, connect_legacy) -> N context={"source": SOURCE_USER}, data=CONFIG_DATA_TELNET, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_unique_id" @@ -285,7 +285,7 @@ async def test_on_connect_legacy_failed( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_BASE: error} @@ -314,7 +314,7 @@ async def test_on_connect_http_failed( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_BASE: error} @@ -331,7 +331,7 @@ async def test_options_flow_ap(hass: HomeAssistant, patch_setup_entry) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert CONF_REQUIRE_IP in result["data_schema"].schema @@ -346,7 +346,7 @@ async def test_options_flow_ap(hass: HomeAssistant, patch_setup_entry) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_CONSIDER_HOME: 20, CONF_TRACK_UNKNOWN: True, @@ -368,7 +368,7 @@ async def test_options_flow_router(hass: HomeAssistant, patch_setup_entry) -> No await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert CONF_REQUIRE_IP not in result["data_schema"].schema @@ -382,7 +382,7 @@ async def test_options_flow_router(hass: HomeAssistant, patch_setup_entry) -> No }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_CONSIDER_HOME: 20, CONF_TRACK_UNKNOWN: True, @@ -403,7 +403,7 @@ async def test_options_flow_http(hass: HomeAssistant, patch_setup_entry) -> None await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert CONF_INTERFACE not in result["data_schema"].schema assert CONF_DNSMASQ not in result["data_schema"].schema @@ -417,7 +417,7 @@ async def test_options_flow_http(hass: HomeAssistant, patch_setup_entry) -> None }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_CONSIDER_HOME: 20, CONF_TRACK_UNKNOWN: True, diff --git a/tests/components/atag/test_config_flow.py b/tests/components/atag/test_config_flow.py index 138790e77e8..59dd7fe8b48 100644 --- a/tests/components/atag/test_config_flow.py +++ b/tests/components/atag/test_config_flow.py @@ -4,9 +4,10 @@ from unittest.mock import PropertyMock, patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.atag import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import UID, USER_INPUT, init_integration, mock_connection @@ -24,7 +25,7 @@ async def test_show_form( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -38,7 +39,7 @@ async def test_adding_second_device( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=USER_INPUT ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" with patch( "pyatag.AtagOne.id", @@ -47,7 +48,7 @@ async def test_adding_second_device( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=USER_INPUT ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_connection_error( @@ -61,7 +62,7 @@ async def test_connection_error( data=USER_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -76,7 +77,7 @@ async def test_unauthorized( context={"source": config_entries.SOURCE_USER}, data=USER_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unauthorized"} @@ -91,6 +92,6 @@ async def test_full_flow_implementation( context={"source": config_entries.SOURCE_USER}, data=USER_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == UID assert result["result"].unique_id == UID diff --git a/tests/components/aurora/test_config_flow.py b/tests/components/aurora/test_config_flow.py index d8f42e48842..a6d6a67cf30 100644 --- a/tests/components/aurora/test_config_flow.py +++ b/tests/components/aurora/test_config_flow.py @@ -4,9 +4,10 @@ from unittest.mock import patch from aiohttp import ClientError -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.aurora.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -104,7 +105,7 @@ async def test_option_flow(hass: HomeAssistant) -> None: data=None, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -112,5 +113,5 @@ async def test_option_flow(hass: HomeAssistant) -> None: user_input={"forecast_threshold": 65}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"]["forecast_threshold"] == 65 diff --git a/tests/components/aurora_abb_powerone/test_config_flow.py b/tests/components/aurora_abb_powerone/test_config_flow.py index fbeaff2f4f8..91ab362b5bc 100644 --- a/tests/components/aurora_abb_powerone/test_config_flow.py +++ b/tests/components/aurora_abb_powerone/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import patch from aurorapy.client import AuroraError, AuroraTimeoutError from serial.tools import list_ports_common -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, setup from homeassistant.components.aurora_abb_powerone.const import ( ATTR_FIRMWARE, ATTR_MODEL, @@ -13,6 +13,7 @@ from homeassistant.components.aurora_abb_powerone.const import ( ) from homeassistant.const import ATTR_SERIAL_NUMBER, CONF_ADDRESS, CONF_PORT from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType TEST_DATA = {"device": "/dev/ttyUSB7", "address": 3, "name": "MyAuroraPV"} @@ -64,7 +65,7 @@ async def test_form(hass: HomeAssistant) -> None: {CONF_PORT: "/dev/ttyUSB7", CONF_ADDRESS: 7}, ) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { CONF_PORT: "/dev/ttyUSB7", diff --git a/tests/components/aussie_broadband/test_config_flow.py b/tests/components/aussie_broadband/test_config_flow.py index f08b56502b8..bc7ed8b8167 100644 --- a/tests/components/aussie_broadband/test_config_flow.py +++ b/tests/components/aussie_broadband/test_config_flow.py @@ -22,7 +22,7 @@ async def test_form(hass: HomeAssistant) -> None: result1 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["errors"] is None with ( @@ -40,7 +40,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_USERNAME assert result2["data"] == FAKE_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -91,7 +91,7 @@ async def test_already_configured(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result4["type"] == FlowResultType.ABORT + assert result4["type"] is FlowResultType.ABORT assert len(mock_setup_entry.mock_calls) == 0 @@ -100,7 +100,7 @@ async def test_no_services(hass: HomeAssistant) -> None: result1 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["errors"] is None with ( @@ -118,7 +118,7 @@ async def test_no_services(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "no_services_found" assert len(mock_setup_entry.mock_calls) == 0 @@ -138,7 +138,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: FAKE_DATA, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -157,7 +157,7 @@ async def test_form_network_issue(hass: HomeAssistant) -> None: FAKE_DATA, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -188,7 +188,7 @@ async def test_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_USERNAME assert result2["data"] == FAKE_DATA diff --git a/tests/components/awair/test_config_flow.py b/tests/components/awair/test_config_flow.py index c8b9ea262a8..ab9f5faa425 100644 --- a/tests/components/awair/test_config_flow.py +++ b/tests/components/awair/test_config_flow.py @@ -6,11 +6,11 @@ from unittest.mock import Mock, patch from aiohttp.client_exceptions import ClientConnectorError from python_awair.exceptions import AuthError, AwairError -from homeassistant import data_entry_flow from homeassistant.components.awair.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .const import ( CLOUD_CONFIG, @@ -29,7 +29,7 @@ async def test_show_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "user" @@ -72,7 +72,7 @@ async def test_unexpected_api_error(hass: HomeAssistant) -> None: CLOUD_CONFIG, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -101,7 +101,7 @@ async def test_duplicate_error(hass: HomeAssistant, user, cloud_devices) -> None CLOUD_CONFIG, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured_account" @@ -123,7 +123,7 @@ async def test_no_devices_error(hass: HomeAssistant, user, no_devices) -> None: CLOUD_CONFIG, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -141,7 +141,7 @@ async def test_reauth(hass: HomeAssistant, user, cloud_devices) -> None: context={"source": SOURCE_REAUTH, "unique_id": CLOUD_UNIQUE_ID}, data={**CLOUD_CONFIG, CONF_ACCESS_TOKEN: "blah"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -151,7 +151,7 @@ async def test_reauth(hass: HomeAssistant, user, cloud_devices) -> None: user_input=CLOUD_CONFIG, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {CONF_ACCESS_TOKEN: "invalid_access_token"} @@ -167,7 +167,7 @@ async def test_reauth(hass: HomeAssistant, user, cloud_devices) -> None: user_input=CLOUD_CONFIG, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" @@ -185,7 +185,7 @@ async def test_reauth_error(hass: HomeAssistant) -> None: context={"source": SOURCE_REAUTH, "unique_id": CLOUD_UNIQUE_ID}, data={**CLOUD_CONFIG, CONF_ACCESS_TOKEN: "blah"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -195,7 +195,7 @@ async def test_reauth_error(hass: HomeAssistant) -> None: user_input=CLOUD_CONFIG, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -226,7 +226,7 @@ async def test_create_cloud_entry(hass: HomeAssistant, user, cloud_devices) -> N CLOUD_CONFIG, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "foo@bar.com" assert result["data"][CONF_ACCESS_TOKEN] == CLOUD_CONFIG[CONF_ACCESS_TOKEN] assert result["result"].unique_id == CLOUD_UNIQUE_ID @@ -262,7 +262,7 @@ async def test_create_local_entry(hass: HomeAssistant, local_devices) -> None: LOCAL_CONFIG, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Awair Element (24947)" assert result["data"][CONF_HOST] == LOCAL_CONFIG[CONF_HOST] assert result["result"].unique_id == LOCAL_UNIQUE_ID @@ -308,7 +308,7 @@ async def test_create_local_entry_from_discovery( {"device": LOCAL_CONFIG[CONF_HOST]}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Awair Element (24947)" assert result["data"][CONF_HOST] == LOCAL_CONFIG[CONF_HOST] assert result["result"].unique_id == LOCAL_UNIQUE_ID @@ -342,7 +342,7 @@ async def test_create_local_entry_awair_error(hass: HomeAssistant) -> None: ) # User is returned to form to try again - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "local_pick" @@ -365,7 +365,7 @@ async def test_create_zeroconf_entry(hass: HomeAssistant, local_devices) -> None {}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Awair Element (24947)" assert result["data"][CONF_HOST] == ZEROCONF_DISCOVERY.host assert result["result"].unique_id == LOCAL_UNIQUE_ID @@ -382,7 +382,7 @@ async def test_unsuccessful_create_zeroconf_entry(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_ZEROCONF}, data=ZEROCONF_DISCOVERY ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT async def test_zeroconf_discovery_update_configuration( @@ -414,7 +414,7 @@ async def test_zeroconf_discovery_update_configuration( data=ZEROCONF_DISCOVERY, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured_device" assert config_entry.data[CONF_HOST] == ZEROCONF_DISCOVERY.host @@ -440,7 +440,7 @@ async def test_zeroconf_during_onboarding( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=ZEROCONF_DISCOVERY ) - assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == "Awair Element (24947)" assert "data" in result assert result["data"][CONF_HOST] == ZEROCONF_DISCOVERY.host diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index a6a0235b118..68dca3539c5 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -61,7 +61,7 @@ async def test_flow_manual_configuration( AXIS_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -75,7 +75,7 @@ async def test_flow_manual_configuration( }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"M1065-LW - {MAC}" assert result["data"] == { CONF_PROTOCOL: "http", @@ -98,7 +98,7 @@ async def test_manual_configuration_update_configuration( AXIS_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" mock_vapix_requests("2.3.4.5") @@ -114,7 +114,7 @@ async def test_manual_configuration_update_configuration( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_config_entry.data[CONF_HOST] == "2.3.4.5" @@ -125,7 +125,7 @@ async def test_flow_fails_faulty_credentials(hass: HomeAssistant) -> None: AXIS_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -152,7 +152,7 @@ async def test_flow_fails_cannot_connect(hass: HomeAssistant) -> None: AXIS_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -192,7 +192,7 @@ async def test_flow_create_entry_multiple_existing_entries_of_same_model( AXIS_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -206,7 +206,7 @@ async def test_flow_create_entry_multiple_existing_entries_of_same_model( }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"M1065-LW - {MAC}" assert result["data"] == { CONF_PROTOCOL: "http", @@ -235,7 +235,7 @@ async def test_reauth_flow_update_configuration( data=mock_config_entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" mock_vapix_requests("2.3.4.5") @@ -251,7 +251,7 @@ async def test_reauth_flow_update_configuration( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_config_entry.data[CONF_PROTOCOL] == "https" assert mock_config_entry.data[CONF_HOST] == "2.3.4.5" @@ -276,7 +276,7 @@ async def test_reconfiguration_flow_update_configuration( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" mock_vapix_requests("2.3.4.5") @@ -289,7 +289,7 @@ async def test_reconfiguration_flow_update_configuration( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_config_entry.data[CONF_PROTOCOL] == "http" assert mock_config_entry.data[CONF_HOST] == "2.3.4.5" @@ -374,7 +374,7 @@ async def test_discovery_flow( AXIS_DOMAIN, data=discovery_info, context={"source": source} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" flows = hass.config_entries.flow.async_progress() @@ -392,7 +392,7 @@ async def test_discovery_flow( }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"M1065-LW - {MAC}" assert result["data"] == { CONF_PROTOCOL: "http", @@ -454,7 +454,7 @@ async def test_discovered_device_already_configured( AXIS_DOMAIN, data=discovery_info, context={"source": source} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_config_entry.data[CONF_HOST] == DEFAULT_HOST @@ -523,7 +523,7 @@ async def test_discovery_flow_updated_configuration( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_config_entry.data == { CONF_HOST: "2.3.4.5", @@ -580,7 +580,7 @@ async def test_discovery_flow_ignore_non_axis_device( AXIS_DOMAIN, data=discovery_info, context={"source": source} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_axis_device" @@ -629,7 +629,7 @@ async def test_discovery_flow_ignore_link_local_address( AXIS_DOMAIN, data=discovery_info, context={"source": source} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "link_local_address" @@ -640,7 +640,7 @@ async def test_option_flow(hass: HomeAssistant, setup_config_entry) -> None: result = await hass.config_entries.options.async_init(setup_config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure_stream" assert set(result["data_schema"].schema[CONF_STREAM_PROFILE].container) == { DEFAULT_STREAM_PROFILE, @@ -657,7 +657,7 @@ async def test_option_flow(hass: HomeAssistant, setup_config_entry) -> None: user_input={CONF_STREAM_PROFILE: "profile_1", CONF_VIDEO_SOURCE: 1}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_STREAM_PROFILE: "profile_1", CONF_VIDEO_SOURCE: 1, diff --git a/tests/components/azure_devops/test_config_flow.py b/tests/components/azure_devops/test_config_flow.py index 8d36b731ff2..acb610a78be 100644 --- a/tests/components/azure_devops/test_config_flow.py +++ b/tests/components/azure_devops/test_config_flow.py @@ -5,9 +5,10 @@ from unittest.mock import AsyncMock from aioazuredevops.core import DevOpsProject import aiohttp -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.azure_devops.const import CONF_ORG, CONF_PROJECT, DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import FIXTURE_REAUTH_INPUT, FIXTURE_USER_INPUT @@ -20,7 +21,7 @@ async def test_show_user_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -37,7 +38,7 @@ async def test_authorization_error( context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( @@ -46,7 +47,7 @@ async def test_authorization_error( ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_auth"} @@ -65,7 +66,7 @@ async def test_reauth_authorization_error( data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth" result2 = await hass.config_entries.flow.async_configure( @@ -74,7 +75,7 @@ async def test_reauth_authorization_error( ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "reauth" assert result2["errors"] == {"base": "invalid_auth"} @@ -92,7 +93,7 @@ async def test_connection_error( context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( @@ -101,7 +102,7 @@ async def test_connection_error( ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -120,7 +121,7 @@ async def test_reauth_connection_error( data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth" result2 = await hass.config_entries.flow.async_configure( @@ -129,7 +130,7 @@ async def test_reauth_connection_error( ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "reauth" assert result2["errors"] == {"base": "cannot_connect"} @@ -148,7 +149,7 @@ async def test_project_error( context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( @@ -157,7 +158,7 @@ async def test_project_error( ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "project_error"} @@ -180,7 +181,7 @@ async def test_reauth_project_error( data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth" result2 = await hass.config_entries.flow.async_configure( @@ -189,7 +190,7 @@ async def test_reauth_project_error( ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "reauth" assert result2["errors"] == {"base": "project_error"} @@ -211,7 +212,7 @@ async def test_reauth_flow( data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth" assert result["errors"] == {"base": "invalid_auth"} @@ -227,7 +228,7 @@ async def test_reauth_flow( ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -242,7 +243,7 @@ async def test_full_flow_implementation( context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( @@ -252,7 +253,7 @@ async def test_full_flow_implementation( await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert ( result2["title"] == f"{FIXTURE_USER_INPUT[CONF_ORG]}/{FIXTURE_USER_INPUT[CONF_PROJECT]}" diff --git a/tests/components/azure_event_hub/test_config_flow.py b/tests/components/azure_event_hub/test_config_flow.py index 38454e46dd1..70914f0ee83 100644 --- a/tests/components/azure_event_hub/test_config_flow.py +++ b/tests/components/azure_event_hub/test_config_flow.py @@ -6,7 +6,7 @@ from unittest.mock import AsyncMock from azure.eventhub.exceptions import EventHubError import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.azure_event_hub.const import ( CONF_MAX_DELAY, CONF_SEND_INTERVAL, @@ -15,6 +15,7 @@ from homeassistant.components.azure_event_hub.const import ( STEP_SAS, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .const import ( BASE_CONFIG_CS, @@ -68,7 +69,7 @@ async def test_form( result2["flow_id"], step2_config.copy(), ) - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "test-instance" assert result3["data"] == data_config mock_setup_entry.assert_called_once() @@ -84,7 +85,7 @@ async def test_import(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: data=IMPORT_CONFIG.copy(), ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "test-instance" options = { CONF_SEND_INTERVAL: import_config.pop(CONF_SEND_INTERVAL), @@ -114,7 +115,7 @@ async def test_single_instance(hass: HomeAssistant, source) -> None: context={"source": source}, data=BASE_CONFIG_CS.copy(), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -143,7 +144,7 @@ async def test_connection_error_sas( result["flow_id"], SAS_CONFIG.copy(), ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": error_message} @@ -173,7 +174,7 @@ async def test_connection_error_cs( result["flow_id"], CS_CONFIG.copy(), ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": error_message} @@ -181,13 +182,13 @@ async def test_options_flow(hass: HomeAssistant, entry) -> None: """Test options flow.""" result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["last_step"] updated = await hass.config_entries.options.async_configure( result["flow_id"], UPDATE_OPTIONS ) - assert updated["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert updated["type"] is FlowResultType.CREATE_ENTRY assert updated["data"] == UPDATE_OPTIONS await hass.async_block_till_done() diff --git a/tests/components/baf/test_config_flow.py b/tests/components/baf/test_config_flow.py index c7c56179839..cf91be0d400 100644 --- a/tests/components/baf/test_config_flow.py +++ b/tests/components/baf/test_config_flow.py @@ -46,7 +46,7 @@ async def test_form_user(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == MOCK_NAME assert result2["data"] == {CONF_IP_ADDRESS: "127.0.0.1"} assert len(mock_setup_entry.mock_calls) == 1 @@ -64,7 +64,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: {CONF_IP_ADDRESS: "127.0.0.1"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {CONF_IP_ADDRESS: "cannot_connect"} @@ -80,7 +80,7 @@ async def test_form_unknown_exception(hass: HomeAssistant) -> None: {CONF_IP_ADDRESS: "127.0.0.1"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -100,7 +100,7 @@ async def test_zeroconf_discovery(hass: HomeAssistant) -> None: type="mock_type", ), ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -138,7 +138,7 @@ async def test_zeroconf_updates_existing_ip(hass: HomeAssistant) -> None: type="mock_type", ), ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_IP_ADDRESS] == "127.0.0.1" @@ -158,7 +158,7 @@ async def test_zeroconf_rejects_ipv6(hass: HomeAssistant) -> None: type="mock_type", ), ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "ipv6_not_supported" @@ -177,7 +177,7 @@ async def test_user_flow_is_not_blocked_by_discovery(hass: HomeAssistant) -> Non type="mock_type", ), ) - assert discovery_result["type"] == FlowResultType.FORM + assert discovery_result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -198,7 +198,7 @@ async def test_user_flow_is_not_blocked_by_discovery(hass: HomeAssistant) -> Non ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == MOCK_NAME assert result2["data"] == {CONF_IP_ADDRESS: "127.0.0.1"} assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/balboa/test_config_flow.py b/tests/components/balboa/test_config_flow.py index 66bc47d23f0..afa170577df 100644 --- a/tests/components/balboa/test_config_flow.py +++ b/tests/components/balboa/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock, patch from pybalboa.exceptions import SpaConnectionError -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.balboa.const import CONF_SYNC_TIME, DOMAIN from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant @@ -23,7 +23,7 @@ async def test_form(hass: HomeAssistant, client: MagicMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -42,7 +42,7 @@ async def test_form(hass: HomeAssistant, client: MagicMock) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == TEST_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -62,7 +62,7 @@ async def test_form_cannot_connect(hass: HomeAssistant, client: MagicMock) -> No result["flow_id"], TEST_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -81,7 +81,7 @@ async def test_form_spa_not_configured(hass: HomeAssistant, client: MagicMock) - result["flow_id"], TEST_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -101,7 +101,7 @@ async def test_unknown_error(hass: HomeAssistant, client: MagicMock) -> None: TEST_DATA, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -113,7 +113,7 @@ async def test_already_configured(hass: HomeAssistant, client: MagicMock) -> Non DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -132,7 +132,7 @@ async def test_already_configured(hass: HomeAssistant, client: MagicMock) -> Non ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -146,7 +146,7 @@ async def test_options_flow(hass: HomeAssistant, client: MagicMock) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" with patch( @@ -159,5 +159,5 @@ async def test_options_flow(hass: HomeAssistant, client: MagicMock) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert dict(config_entry.options) == {CONF_SYNC_TIME: True} diff --git a/tests/components/bang_olufsen/test_config_flow.py b/tests/components/bang_olufsen/test_config_flow.py index d813ddf185b..ad513905f16 100644 --- a/tests/components/bang_olufsen/test_config_flow.py +++ b/tests/components/bang_olufsen/test_config_flow.py @@ -35,7 +35,7 @@ async def test_config_flow_timeout_error( context={CONF_SOURCE: SOURCE_USER}, data=TEST_DATA_USER, ) - assert result_user["type"] == FlowResultType.FORM + assert result_user["type"] is FlowResultType.FORM assert result_user["errors"] == {"base": "timeout_error"} assert mock_mozart_client.get_beolink_self.call_count == 1 @@ -54,7 +54,7 @@ async def test_config_flow_client_connector_error( context={CONF_SOURCE: SOURCE_USER}, data=TEST_DATA_USER, ) - assert result_user["type"] == FlowResultType.FORM + assert result_user["type"] is FlowResultType.FORM assert result_user["errors"] == {"base": "client_connector_error"} assert mock_mozart_client.get_beolink_self.call_count == 1 @@ -68,7 +68,7 @@ async def test_config_flow_invalid_ip(hass: HomeAssistant) -> None: context={CONF_SOURCE: SOURCE_USER}, data=TEST_DATA_USER_INVALID, ) - assert result_user["type"] == FlowResultType.FORM + assert result_user["type"] is FlowResultType.FORM assert result_user["errors"] == {"base": "invalid_ip"} @@ -83,7 +83,7 @@ async def test_config_flow_api_exception( context={CONF_SOURCE: SOURCE_USER}, data=TEST_DATA_USER, ) - assert result_user["type"] == FlowResultType.FORM + assert result_user["type"] is FlowResultType.FORM assert result_user["errors"] == {"base": "api_exception"} assert mock_mozart_client.get_beolink_self.call_count == 1 @@ -98,7 +98,7 @@ async def test_config_flow(hass: HomeAssistant, mock_mozart_client) -> None: data=None, ) - assert result_init["type"] == FlowResultType.FORM + assert result_init["type"] is FlowResultType.FORM assert result_init["step_id"] == "user" result_user = await hass.config_entries.flow.async_configure( @@ -106,7 +106,7 @@ async def test_config_flow(hass: HomeAssistant, mock_mozart_client) -> None: user_input=TEST_DATA_USER, ) - assert result_user["type"] == FlowResultType.CREATE_ENTRY + assert result_user["type"] is FlowResultType.CREATE_ENTRY assert result_user["data"] == TEST_DATA_CREATE_ENTRY assert mock_mozart_client.get_beolink_self.call_count == 1 @@ -121,7 +121,7 @@ async def test_config_flow_zeroconf(hass: HomeAssistant, mock_mozart_client) -> data=TEST_DATA_ZEROCONF, ) - assert result_zeroconf["type"] == FlowResultType.FORM + assert result_zeroconf["type"] is FlowResultType.FORM assert result_zeroconf["step_id"] == "zeroconf_confirm" result_confirm = await hass.config_entries.flow.async_configure( @@ -129,7 +129,7 @@ async def test_config_flow_zeroconf(hass: HomeAssistant, mock_mozart_client) -> user_input=TEST_DATA_USER, ) - assert result_confirm["type"] == FlowResultType.CREATE_ENTRY + assert result_confirm["type"] is FlowResultType.CREATE_ENTRY assert result_confirm["data"] == TEST_DATA_CREATE_ENTRY assert mock_mozart_client.get_beolink_self.call_count == 0 @@ -144,7 +144,7 @@ async def test_config_flow_zeroconf_not_mozart_device(hass: HomeAssistant) -> No data=TEST_DATA_ZEROCONF_NOT_MOZART, ) - assert result_user["type"] == FlowResultType.ABORT + assert result_user["type"] is FlowResultType.ABORT assert result_user["reason"] == "not_mozart_device" @@ -157,5 +157,5 @@ async def test_config_flow_zeroconf_ipv6(hass: HomeAssistant) -> None: data=TEST_DATA_ZEROCONF_IPV6, ) - assert result_user["type"] == FlowResultType.ABORT + assert result_user["type"] is FlowResultType.ABORT assert result_user["reason"] == "ipv6_address" diff --git a/tests/components/blebox/test_config_flow.py b/tests/components/blebox/test_config_flow.py index 3f59ed022fd..e94553e10cf 100644 --- a/tests/components/blebox/test_config_flow.py +++ b/tests/components/blebox/test_config_flow.py @@ -6,7 +6,7 @@ from unittest.mock import DEFAULT, AsyncMock, PropertyMock, patch import blebox_uniapi import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.blebox import config_flow from homeassistant.const import CONF_IP_ADDRESS @@ -189,7 +189,7 @@ async def test_already_configured(hass: HomeAssistant, valid_feature_mock) -> No context={"source": config_entries.SOURCE_USER}, data={config_flow.CONF_HOST: "172.2.3.4", config_flow.CONF_PORT: 80}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "address_already_configured" @@ -238,13 +238,13 @@ async def test_flow_with_zeroconf(hass: HomeAssistant) -> None: ), ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch("homeassistant.components.blebox.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == {"host": "172.100.123.4", "port": 80} @@ -278,7 +278,7 @@ async def test_flow_with_zeroconf_when_already_configured(hass: HomeAssistant) - ), ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -301,7 +301,7 @@ async def test_flow_with_zeroconf_when_device_unsupported(hass: HomeAssistant) - properties={"_raw": {}}, ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unsupported_device_version" @@ -327,5 +327,5 @@ async def test_flow_with_zeroconf_when_device_response_unsupported( properties={"_raw": {}}, ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unsupported_device_response" diff --git a/tests/components/blink/test_config_flow.py b/tests/components/blink/test_config_flow.py index b5fbf19ef9b..10b34fa532c 100644 --- a/tests/components/blink/test_config_flow.py +++ b/tests/components/blink/test_config_flow.py @@ -5,9 +5,10 @@ from unittest.mock import patch from blinkpy.auth import LoginError from blinkpy.blinkpy import BlinkSetupError -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.blink import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType async def test_form(hass: HomeAssistant) -> None: @@ -295,5 +296,5 @@ async def test_reauth_shows_user_step(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_REAUTH}, data={"username": "blink@example.com", "password": "invalid_password"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" diff --git a/tests/components/blue_current/test_config_flow.py b/tests/components/blue_current/test_config_flow.py index b5dad155618..33346990425 100644 --- a/tests/components/blue_current/test_config_flow.py +++ b/tests/components/blue_current/test_config_flow.py @@ -24,7 +24,7 @@ async def test_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["errors"] == {} - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM async def test_user(hass: HomeAssistant) -> None: @@ -34,7 +34,7 @@ async def test_user(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["errors"] == {} - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with ( patch( @@ -60,7 +60,7 @@ async def test_user(hass: HomeAssistant) -> None: assert result2["title"] == "test@email.com" assert result2["data"] == {"api_token": "123"} - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY @pytest.mark.parametrize( @@ -85,7 +85,7 @@ async def test_flow_fails(hass: HomeAssistant, error: Exception, message: str) - data={"api_token": "123"}, ) assert result["errors"]["base"] == message - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with ( patch( @@ -111,7 +111,7 @@ async def test_flow_fails(hass: HomeAssistant, error: Exception, message: str) - assert result2["title"] == "test@email.com" assert result2["data"] == {"api_token": "123"} - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY @pytest.mark.parametrize( @@ -157,14 +157,14 @@ async def test_reauth( data={"api_token": "123"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"api_token": "1234567890"}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == reason assert config_entry.data["api_token"] == expected_api_token diff --git a/tests/components/bluemaestro/test_config_flow.py b/tests/components/bluemaestro/test_config_flow.py index f87ea053ffe..819541c3b7f 100644 --- a/tests/components/bluemaestro/test_config_flow.py +++ b/tests/components/bluemaestro/test_config_flow.py @@ -19,7 +19,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=BLUEMAESTRO_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( "homeassistant.components.bluemaestro.async_setup_entry", return_value=True @@ -27,7 +27,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Tempo Disc THD EEFF" assert result2["data"] == {} assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" @@ -40,7 +40,7 @@ async def test_async_step_bluetooth_not_bluemaestro(hass: HomeAssistant) -> None context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_BLUEMAESTRO_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -50,7 +50,7 @@ async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -64,7 +64,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( "homeassistant.components.bluemaestro.async_setup_entry", return_value=True @@ -73,7 +73,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Tempo Disc THD EEFF" assert result2["data"] == {} assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" @@ -89,7 +89,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -105,7 +105,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -127,7 +127,7 @@ async def test_async_step_user_with_found_devices_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -144,7 +144,7 @@ async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant) - context={"source": config_entries.SOURCE_BLUETOOTH}, data=BLUEMAESTRO_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -155,7 +155,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=BLUEMAESTRO_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -163,7 +163,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=BLUEMAESTRO_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -176,7 +176,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=BLUEMAESTRO_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -187,7 +187,7 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.bluemaestro.async_setup_entry", return_value=True @@ -196,7 +196,7 @@ async def test_async_step_user_takes_precedence_over_discovery( result["flow_id"], user_input={"address": "aa:bb:cc:dd:ee:ff"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Tempo Disc THD EEFF" assert result2["data"] == {} assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" diff --git a/tests/components/bluetooth/test_config_flow.py b/tests/components/bluetooth/test_config_flow.py index 9ca674e2d32..89243223129 100644 --- a/tests/components/bluetooth/test_config_flow.py +++ b/tests/components/bluetooth/test_config_flow.py @@ -53,7 +53,7 @@ async def test_async_step_user_macos(hass: HomeAssistant, macos_adapter: None) - context={"source": config_entries.SOURCE_USER}, data={}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "single_adapter" with ( patch("homeassistant.components.bluetooth.async_setup", return_value=True), @@ -64,7 +64,7 @@ async def test_async_step_user_macos(hass: HomeAssistant, macos_adapter: None) - result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Core Bluetooth" assert result2["data"] == {} assert len(mock_setup_entry.mock_calls) == 1 @@ -79,7 +79,7 @@ async def test_async_step_user_linux_one_adapter( context={"source": config_entries.SOURCE_USER}, data={}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "single_adapter" with ( patch("homeassistant.components.bluetooth.async_setup", return_value=True), @@ -90,7 +90,7 @@ async def test_async_step_user_linux_one_adapter( result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "00:00:00:00:00:01" assert result2["data"] == {} assert len(mock_setup_entry.mock_calls) == 1 @@ -105,7 +105,7 @@ async def test_async_step_user_linux_two_adapters( context={"source": config_entries.SOURCE_USER}, data={}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "multiple_adapters" with ( patch("homeassistant.components.bluetooth.async_setup", return_value=True), @@ -116,7 +116,7 @@ async def test_async_step_user_linux_two_adapters( result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_ADAPTER: "hci1"} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "00:00:00:00:00:02" assert result2["data"] == {} assert len(mock_setup_entry.mock_calls) == 1 @@ -133,7 +133,7 @@ async def test_async_step_user_only_allows_one( context={"source": config_entries.SOURCE_USER}, data={}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_adapters" @@ -152,7 +152,7 @@ async def test_async_step_integration_discovery(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={CONF_ADAPTER: "hci0", CONF_DETAILS: details}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "single_adapter" with ( patch("homeassistant.components.bluetooth.async_setup", return_value=True), @@ -163,7 +163,7 @@ async def test_async_step_integration_discovery(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "00:00:00:00:00:01" assert result2["data"] == {} assert len(mock_setup_entry.mock_calls) == 1 @@ -195,7 +195,7 @@ async def test_async_step_integration_discovery_during_onboarding_one_adapter( context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={CONF_ADAPTER: "hci0", CONF_DETAILS: details}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "00:00:00:00:00:01" assert result["data"] == {} assert len(mock_setup_entry.mock_calls) == 1 @@ -239,11 +239,11 @@ async def test_async_step_integration_discovery_during_onboarding_two_adapters( context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={CONF_ADAPTER: "hci1", CONF_DETAILS: details2}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "00:00:00:00:00:01" assert result["data"] == {} - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "00:00:00:00:00:02" assert result2["data"] == {} @@ -277,7 +277,7 @@ async def test_async_step_integration_discovery_during_onboarding( context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={CONF_ADAPTER: "Core Bluetooth", CONF_DETAILS: details}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Core Bluetooth" assert result["data"] == {} assert len(mock_setup_entry.mock_calls) == 1 @@ -302,7 +302,7 @@ async def test_async_step_integration_discovery_already_exists( context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={CONF_ADAPTER: "hci0", CONF_DETAILS: details}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -325,7 +325,7 @@ async def test_options_flow_linux( await hass.async_block_till_done() result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] is None @@ -337,13 +337,13 @@ async def test_options_flow_linux( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_PASSIVE] is True # Verify we can change it to False result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] is None @@ -355,7 +355,7 @@ async def test_options_flow_linux( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_PASSIVE] is False await hass.config_entries.async_unload(entry.entry_id) @@ -438,6 +438,6 @@ async def test_async_step_user_linux_adapter_is_ignored( context={"source": config_entries.SOURCE_USER}, data={}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_adapters" assert result["description_placeholders"] == {"ignored_adapters": "1"} diff --git a/tests/components/bmw_connected_drive/test_config_flow.py b/tests/components/bmw_connected_drive/test_config_flow.py index ab7366e9da4..b562e2b898f 100644 --- a/tests/components/bmw_connected_drive/test_config_flow.py +++ b/tests/components/bmw_connected_drive/test_config_flow.py @@ -7,7 +7,7 @@ from bimmer_connected.api.authentication import MyBMWAuthentication from bimmer_connected.models import MyBMWAPIError, MyBMWAuthError from httpx import RequestError -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.bmw_connected_drive.config_flow import DOMAIN from homeassistant.components.bmw_connected_drive.const import ( CONF_READ_ONLY, @@ -15,6 +15,7 @@ from homeassistant.components.bmw_connected_drive.const import ( ) from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import ( FIXTURE_CONFIG_ENTRY, @@ -41,7 +42,7 @@ async def test_show_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -58,7 +59,7 @@ async def test_authentication_error(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -76,7 +77,7 @@ async def test_connection_error(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -94,7 +95,7 @@ async def test_api_error(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -117,7 +118,7 @@ async def test_full_user_flow_implementation(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, data=FIXTURE_USER_INPUT, ) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == FIXTURE_COMPLETE_ENTRY[CONF_USERNAME] assert result2["data"] == FIXTURE_COMPLETE_ENTRY @@ -143,7 +144,7 @@ async def test_options_flow_implementation(hass: HomeAssistant) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "account_options" result = await hass.config_entries.options.async_configure( @@ -152,7 +153,7 @@ async def test_options_flow_implementation(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_READ_ONLY: True, } @@ -195,7 +196,7 @@ async def test_reauth(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -204,7 +205,7 @@ async def test_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert config_entry.data == FIXTURE_COMPLETE_ENTRY diff --git a/tests/components/braviatv/test_config_flow.py b/tests/components/braviatv/test_config_flow.py index 673344017f7..6fc02dbd36f 100644 --- a/tests/components/braviatv/test_config_flow.py +++ b/tests/components/braviatv/test_config_flow.py @@ -10,7 +10,6 @@ from pybravia import ( ) import pytest -from homeassistant import data_entry_flow from homeassistant.components import ssdp from homeassistant.components.braviatv.const import ( CONF_NICKNAME, @@ -21,6 +20,7 @@ from homeassistant.components.braviatv.const import ( from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_SSDP, SOURCE_USER from homeassistant.const import CONF_CLIENT_ID, CONF_HOST, CONF_MAC, CONF_PIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import instance_id from tests.common import MockConfigEntry @@ -93,7 +93,7 @@ async def test_show_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -105,7 +105,7 @@ async def test_ssdp_discovery(hass: HomeAssistant) -> None: context={"source": SOURCE_SSDP}, data=BRAVIA_SSDP, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" with ( @@ -122,21 +122,21 @@ async def test_ssdp_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "authorize" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_USE_PSK: False} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pin" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PIN: "1234"} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == "very_unique_string" assert result["title"] == "TV-Model" assert result["data"] == { @@ -157,7 +157,7 @@ async def test_ssdp_discovery_fake(hass: HomeAssistant) -> None: data=FAKE_BRAVIA_SSDP, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_bravia_device" @@ -181,7 +181,7 @@ async def test_ssdp_discovery_exist(hass: HomeAssistant) -> None: data=BRAVIA_SSDP, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -261,7 +261,7 @@ async def test_no_ip_control(hass: HomeAssistant) -> None: result["flow_id"], user_input={CONF_USE_PSK: False} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_ip_control" @@ -298,7 +298,7 @@ async def test_duplicate_error(hass: HomeAssistant) -> None: result["flow_id"], user_input={CONF_PIN: "1234"} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -319,21 +319,21 @@ async def test_create_entry(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "authorize" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_USE_PSK: False} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pin" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PIN: "1234"} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == "very_unique_string" assert result["title"] == "TV-Model" assert result["data"] == { @@ -360,21 +360,21 @@ async def test_create_entry_psk(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "authorize" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_USE_PSK: True} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "psk" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PIN: "mypsk"} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == "very_unique_string" assert result["title"] == "TV-Model" assert result["data"] == { @@ -427,7 +427,7 @@ async def test_reauth_successful(hass: HomeAssistant, use_psk, new_pin) -> None: data=config_entry.data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "authorize" result = await hass.config_entries.flow.async_configure( @@ -437,6 +437,6 @@ async def test_reauth_successful(hass: HomeAssistant, use_psk, new_pin) -> None: result["flow_id"], user_input={CONF_PIN: new_pin} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert config_entry.data[CONF_PIN] == new_pin diff --git a/tests/components/bring/test_config_flow.py b/tests/components/bring/test_config_flow.py index 29abad94fad..be5ce48680c 100644 --- a/tests/components/bring/test_config_flow.py +++ b/tests/components/bring/test_config_flow.py @@ -32,7 +32,7 @@ async def test_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result = await hass.config_entries.flow.async_init( @@ -44,7 +44,7 @@ async def test_form( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DATA_STEP["email"] assert result["data"] == MOCK_DATA_STEP assert len(mock_setup_entry.mock_calls) == 1 @@ -73,7 +73,7 @@ async def test_flow_user_init_data_unknown_error_and_recover( user_input=MOCK_DATA_STEP, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == text_error # Recover @@ -110,5 +110,5 @@ async def test_flow_user_init_data_already_configured( user_input=MOCK_DATA_STEP, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/brother/test_config_flow.py b/tests/components/brother/test_config_flow.py index 2eff4ed2770..a476ec8f579 100644 --- a/tests/components/brother/test_config_flow.py +++ b/tests/components/brother/test_config_flow.py @@ -7,12 +7,12 @@ from unittest.mock import patch from brother import SnmpError, UnsupportedModelError import pytest -from homeassistant import data_entry_flow from homeassistant.components import zeroconf from homeassistant.components.brother.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_TYPE from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry, load_fixture @@ -27,7 +27,7 @@ async def test_show_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -46,7 +46,7 @@ async def test_create_entry_with_hostname(hass: HomeAssistant) -> None: data={CONF_HOST: "example.local", CONF_TYPE: "laser"}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "HL-L2340DW 0123456789" assert result["data"][CONF_HOST] == "example.local" assert result["data"][CONF_TYPE] == "laser" @@ -65,7 +65,7 @@ async def test_create_entry_with_ipv4_address(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "HL-L2340DW 0123456789" assert result["data"][CONF_HOST] == "127.0.0.1" assert result["data"][CONF_TYPE] == "laser" @@ -86,7 +86,7 @@ async def test_create_entry_with_ipv6_address(hass: HomeAssistant) -> None: data={CONF_HOST: "2001:db8::1428:57ab", CONF_TYPE: "laser"}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "HL-L2340DW 0123456789" assert result["data"][CONF_HOST] == "2001:db8::1428:57ab" assert result["data"][CONF_TYPE] == "laser" @@ -140,7 +140,7 @@ async def test_unsupported_model_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unsupported_model" @@ -160,7 +160,7 @@ async def test_device_exists_abort(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -185,7 +185,7 @@ async def test_zeroconf_exception(hass: HomeAssistant, exc: Exception) -> None: ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -209,7 +209,7 @@ async def test_zeroconf_unsupported_model(hass: HomeAssistant) -> None: ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unsupported_model" assert len(mock_get_data.mock_calls) == 0 @@ -244,7 +244,7 @@ async def test_zeroconf_device_exists_abort(hass: HomeAssistant) -> None: ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" # Test config entry got updated with latest IP @@ -274,7 +274,7 @@ async def test_zeroconf_no_probe_existing_device(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(mock_get_data.mock_calls) == 0 @@ -305,13 +305,13 @@ async def test_zeroconf_confirm_create_entry(hass: HomeAssistant) -> None: assert result["step_id"] == "zeroconf_confirm" assert result["description_placeholders"]["model"] == "HL-L2340DW" assert result["description_placeholders"]["serial_number"] == "0123456789" - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_TYPE: "laser"} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "HL-L2340DW 0123456789" assert result["data"][CONF_HOST] == "127.0.0.1" assert result["data"][CONF_TYPE] == "laser" diff --git a/tests/components/brottsplatskartan/test_config_flow.py b/tests/components/brottsplatskartan/test_config_flow.py index 3571277c6f3..41ac297961f 100644 --- a/tests/components/brottsplatskartan/test_config_flow.py +++ b/tests/components/brottsplatskartan/test_config_flow.py @@ -19,7 +19,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -28,7 +28,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Brottsplatskartan HOME" assert result2["data"] == { "area": None, @@ -44,7 +44,7 @@ async def test_form_location(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -58,7 +58,7 @@ async def test_form_location(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Brottsplatskartan 59.32, 18.06" assert result2["data"] == { "area": None, @@ -74,7 +74,7 @@ async def test_form_area(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -89,7 +89,7 @@ async def test_form_area(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Brottsplatskartan Stockholms län" assert result2["data"] == { "latitude": None, diff --git a/tests/components/brunt/test_config_flow.py b/tests/components/brunt/test_config_flow.py index dfa1e9f992a..6c4d928c0ca 100644 --- a/tests/components/brunt/test_config_flow.py +++ b/tests/components/brunt/test_config_flow.py @@ -6,10 +6,11 @@ from aiohttp import ClientResponseError from aiohttp.client_exceptions import ServerDisconnectedError import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.brunt.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -36,7 +37,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "test-username" assert result2["data"] == CONFIG assert len(mock_setup_entry.mock_calls) == 1 @@ -58,7 +59,7 @@ async def test_form_duplicate_login(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -81,17 +82,17 @@ async def test_form_error(hass: HomeAssistant, side_effect, error_message) -> No DOMAIN, context={"source": config_entries.SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": error_message} @pytest.mark.parametrize( ("side_effect", "result_type", "password", "step_id", "reason"), [ - (None, data_entry_flow.FlowResultType.ABORT, "test", None, "reauth_successful"), + (None, FlowResultType.ABORT, "test", None, "reauth_successful"), ( Exception, - data_entry_flow.FlowResultType.FORM, + FlowResultType.FORM, CONFIG[CONF_PASSWORD], "reauth_confirm", None, @@ -118,7 +119,7 @@ async def test_reauth( }, data=None, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( "homeassistant.components.brunt.config_flow.BruntClientAsync.async_login", diff --git a/tests/components/bsblan/test_config_flow.py b/tests/components/bsblan/test_config_flow.py index db2b0f8f85c..91e4338d688 100644 --- a/tests/components/bsblan/test_config_flow.py +++ b/tests/components/bsblan/test_config_flow.py @@ -26,7 +26,7 @@ async def test_full_user_flow_implementation( context={"source": SOURCE_USER}, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( @@ -40,7 +40,7 @@ async def test_full_user_flow_implementation( }, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == format_mac("00:80:41:19:69:90") assert result2.get("data") == { CONF_HOST: "127.0.0.1", @@ -64,7 +64,7 @@ async def test_show_user_form(hass: HomeAssistant) -> None: ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM async def test_connection_error( @@ -86,7 +86,7 @@ async def test_connection_error( }, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {"base": "cannot_connect"} assert result.get("step_id") == "user" @@ -110,5 +110,5 @@ async def test_user_device_exists_abort( }, ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" diff --git a/tests/components/bthome/test_config_flow.py b/tests/components/bthome/test_config_flow.py index 1a785858752..acf490d341e 100644 --- a/tests/components/bthome/test_config_flow.py +++ b/tests/components/bthome/test_config_flow.py @@ -27,13 +27,13 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=TEMP_HUMI_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch("homeassistant.components.bthome.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "ATC 18B2" assert result2["data"] == {} assert result2["result"].unique_id == "A4:C1:38:8D:18:B2" @@ -56,7 +56,7 @@ async def test_async_step_bluetooth_during_onboarding(hass: HomeAssistant) -> No data=TEMP_HUMI_SERVICE_INFO, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "ATC 18B2" assert result["data"] == {} assert result["result"].unique_id == "A4:C1:38:8D:18:B2" @@ -73,7 +73,7 @@ async def test_async_step_bluetooth_valid_device_with_encryption( context={"source": config_entries.SOURCE_BLUETOOTH}, data=TEMP_HUMI_ENCRYPTED_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "get_encryption_key" with patch("homeassistant.components.bthome.async_setup_entry", return_value=True): @@ -81,7 +81,7 @@ async def test_async_step_bluetooth_valid_device_with_encryption( result["flow_id"], user_input={"bindkey": "231d39c1d7cc1ab1aee224cd096db932"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "TEST DEVICE 80A5" assert result2["data"] == {"bindkey": "231d39c1d7cc1ab1aee224cd096db932"} assert result2["result"].unique_id == "54:48:E6:8F:80:A5" @@ -96,14 +96,14 @@ async def test_async_step_bluetooth_valid_device_encryption_wrong_key( context={"source": config_entries.SOURCE_BLUETOOTH}, data=TEMP_HUMI_ENCRYPTED_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "get_encryption_key" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"bindkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "get_encryption_key" assert result2["errors"]["bindkey"] == "decryption_failed" @@ -113,7 +113,7 @@ async def test_async_step_bluetooth_valid_device_encryption_wrong_key( result["flow_id"], user_input={"bindkey": "231d39c1d7cc1ab1aee224cd096db932"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "TEST DEVICE 80A5" assert result2["data"] == {"bindkey": "231d39c1d7cc1ab1aee224cd096db932"} assert result2["result"].unique_id == "54:48:E6:8F:80:A5" @@ -128,7 +128,7 @@ async def test_async_step_bluetooth_valid_device_encryption_wrong_key_length( context={"source": config_entries.SOURCE_BLUETOOTH}, data=TEMP_HUMI_ENCRYPTED_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "get_encryption_key" result2 = await hass.config_entries.flow.async_configure( @@ -136,7 +136,7 @@ async def test_async_step_bluetooth_valid_device_encryption_wrong_key_length( user_input={"bindkey": "aa"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "get_encryption_key" assert result2["errors"]["bindkey"] == "expected_32_characters" @@ -146,7 +146,7 @@ async def test_async_step_bluetooth_valid_device_encryption_wrong_key_length( result["flow_id"], user_input={"bindkey": "231d39c1d7cc1ab1aee224cd096db932"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "TEST DEVICE 80A5" assert result2["data"] == {"bindkey": "231d39c1d7cc1ab1aee224cd096db932"} assert result2["result"].unique_id == "54:48:E6:8F:80:A5" @@ -159,7 +159,7 @@ async def test_async_step_bluetooth_not_supported(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_BTHOME_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -169,7 +169,7 @@ async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -186,7 +186,7 @@ async def test_async_step_user_no_devices_found_2(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -200,14 +200,14 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch("homeassistant.components.bthome.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"address": "54:48:E6:8F:80:A5"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "b-parasite 80A5" assert result2["data"] == {} assert result2["result"].unique_id == "54:48:E6:8F:80:A5" @@ -225,14 +225,14 @@ async def test_async_step_user_with_found_devices_encryption( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result1 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"address": "54:48:E6:8F:80:A5"}, ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "get_encryption_key" with patch("homeassistant.components.bthome.async_setup_entry", return_value=True): @@ -241,7 +241,7 @@ async def test_async_step_user_with_found_devices_encryption( user_input={"bindkey": "231d39c1d7cc1ab1aee224cd096db932"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "TEST DEVICE 80A5" assert result2["data"] == {"bindkey": "231d39c1d7cc1ab1aee224cd096db932"} assert result2["result"].unique_id == "54:48:E6:8F:80:A5" @@ -260,7 +260,7 @@ async def test_async_step_user_with_found_devices_encryption_wrong_key( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # Pick a device @@ -268,7 +268,7 @@ async def test_async_step_user_with_found_devices_encryption_wrong_key( result["flow_id"], user_input={"address": "54:48:E6:8F:80:A5"}, ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "get_encryption_key" # Try an incorrect key @@ -276,7 +276,7 @@ async def test_async_step_user_with_found_devices_encryption_wrong_key( result["flow_id"], user_input={"bindkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "get_encryption_key" assert result2["errors"]["bindkey"] == "decryption_failed" @@ -287,7 +287,7 @@ async def test_async_step_user_with_found_devices_encryption_wrong_key( user_input={"bindkey": "231d39c1d7cc1ab1aee224cd096db932"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "TEST DEVICE 80A5" assert result2["data"] == {"bindkey": "231d39c1d7cc1ab1aee224cd096db932"} assert result2["result"].unique_id == "54:48:E6:8F:80:A5" @@ -306,7 +306,7 @@ async def test_async_step_user_with_found_devices_encryption_wrong_key_length( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # Select a single device @@ -314,7 +314,7 @@ async def test_async_step_user_with_found_devices_encryption_wrong_key_length( result["flow_id"], user_input={"address": "54:48:E6:8F:80:A5"}, ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "get_encryption_key" # Try an incorrect key @@ -323,7 +323,7 @@ async def test_async_step_user_with_found_devices_encryption_wrong_key_length( user_input={"bindkey": "aa"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "get_encryption_key" assert result2["errors"]["bindkey"] == "expected_32_characters" @@ -334,7 +334,7 @@ async def test_async_step_user_with_found_devices_encryption_wrong_key_length( user_input={"bindkey": "231d39c1d7cc1ab1aee224cd096db932"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "TEST DEVICE 80A5" assert result2["data"] == {"bindkey": "231d39c1d7cc1ab1aee224cd096db932"} assert result2["result"].unique_id == "54:48:E6:8F:80:A5" @@ -350,7 +350,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -364,7 +364,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - result["flow_id"], user_input={"address": "A4:C1:38:8D:18:B2"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -386,7 +386,7 @@ async def test_async_step_user_with_found_devices_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -403,7 +403,7 @@ async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant) - context={"source": config_entries.SOURCE_BLUETOOTH}, data=PRST_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -414,7 +414,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=PRST_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -422,7 +422,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=PRST_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -435,7 +435,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=PRST_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -446,14 +446,14 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch("homeassistant.components.bthome.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"address": "54:48:E6:8F:80:A5"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "b-parasite 80A5" assert result2["data"] == {} assert result2["result"].unique_id == "54:48:E6:8F:80:A5" @@ -498,7 +498,7 @@ async def test_async_step_reauth(hass: HomeAssistant) -> None: result["flow_id"], user_input={"bindkey": "231d39c1d7cc1ab1aee224cd096db932"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -538,7 +538,7 @@ async def test_async_step_reauth_wrong_key(hass: HomeAssistant) -> None: result["flow_id"], user_input={"bindkey": "5b51a7c91cde6707c9ef18dada143a58"}, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "get_encryption_key" assert result2["errors"]["bindkey"] == "decryption_failed" @@ -546,7 +546,7 @@ async def test_async_step_reauth_wrong_key(hass: HomeAssistant) -> None: result["flow_id"], user_input={"bindkey": "231d39c1d7cc1ab1aee224cd096db932"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -574,5 +574,5 @@ async def test_async_step_reauth_abort_early(hass: HomeAssistant) -> None: data=entry.data | {"device": device}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" diff --git a/tests/components/buienradar/test_config_flow.py b/tests/components/buienradar/test_config_flow.py index 6a7db5e9066..9fb0d9c4c48 100644 --- a/tests/components/buienradar/test_config_flow.py +++ b/tests/components/buienradar/test_config_flow.py @@ -2,10 +2,11 @@ import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.buienradar.const import DOMAIN from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -92,7 +93,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: user_input={"country_code": "BE", "delta": 450, "timeframe": 30}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() diff --git a/tests/components/caldav/test_config_flow.py b/tests/components/caldav/test_config_flow.py index 6af7d5c670c..e7cbf9dd7ea 100644 --- a/tests/components/caldav/test_config_flow.py +++ b/tests/components/caldav/test_config_flow.py @@ -35,7 +35,7 @@ async def test_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert not result.get("errors") result2 = await hass.config_entries.flow.async_configure( @@ -49,7 +49,7 @@ async def test_form( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == TEST_USERNAME assert result2.get("data") == { CONF_URL: TEST_URL, @@ -93,7 +93,7 @@ async def test_caldav_client_error( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("errors") == {"base": expected_error} @@ -124,7 +124,7 @@ async def test_reauth_success( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "reauth_successful" # Verify updated configuration entry @@ -167,7 +167,7 @@ async def test_reauth_failure( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert result2.get("errors") == {"base": "cannot_connect"} # Complete the form and it succeeds this time @@ -180,7 +180,7 @@ async def test_reauth_failure( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "reauth_successful" # Verify updated configuration entry @@ -223,7 +223,7 @@ async def test_multiple_config_entries( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert not result.get("errors") result2 = await hass.config_entries.flow.async_configure( @@ -232,7 +232,7 @@ async def test_multiple_config_entries( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == user_input[CONF_USERNAME] assert result2.get("data") == { **user_input, @@ -271,7 +271,7 @@ async def test_duplicate_config_entries( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert not result.get("errors") result2 = await hass.config_entries.flow.async_configure( @@ -280,5 +280,5 @@ async def test_duplicate_config_entries( ) await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "already_configured" diff --git a/tests/components/canary/test_config_flow.py b/tests/components/canary/test_config_flow.py index 3c32c683a39..552aa9089ce 100644 --- a/tests/components/canary/test_config_flow.py +++ b/tests/components/canary/test_config_flow.py @@ -24,7 +24,7 @@ async def test_user_form(hass: HomeAssistant, canary_config_flow) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -37,7 +37,7 @@ async def test_user_form(hass: HomeAssistant, canary_config_flow) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "test-username" assert result["data"] == {**USER_INPUT, CONF_TIMEOUT: DEFAULT_TIMEOUT} @@ -60,7 +60,7 @@ async def test_user_form_cannot_connect( USER_INPUT, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} canary_config_flow.side_effect = ConnectTimeout() @@ -70,7 +70,7 @@ async def test_user_form_cannot_connect( USER_INPUT, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -89,7 +89,7 @@ async def test_user_form_unexpected_exception( USER_INPUT, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -104,7 +104,7 @@ async def test_user_form_single_instance_allowed( context={"source": SOURCE_USER}, data=USER_INPUT, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -117,7 +117,7 @@ async def test_options_flow(hass: HomeAssistant, canary) -> None: assert entry.options[CONF_TIMEOUT] == DEFAULT_TIMEOUT result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" with _patch_async_setup(), _patch_async_setup_entry(): @@ -127,6 +127,6 @@ async def test_options_flow(hass: HomeAssistant, canary) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_FFMPEG_ARGUMENTS] == "-v" assert result["data"][CONF_TIMEOUT] == 7 diff --git a/tests/components/cast/test_config_flow.py b/tests/components/cast/test_config_flow.py index a7b9311e88b..ab24aa4df5c 100644 --- a/tests/components/cast/test_config_flow.py +++ b/tests/components/cast/test_config_flow.py @@ -4,10 +4,11 @@ from unittest.mock import ANY, patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import cast from homeassistant.components.cast.home_assistant_cast import CAST_USER_NAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -29,10 +30,10 @@ async def test_creating_entry_sets_up_media_player(hass: HomeAssistant) -> None: ) # Confirmation form - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -194,7 +195,7 @@ async def test_option_flow(hass: HomeAssistant, parameter_data) -> None: # Test ignore_cec and uuid options are hidden if advanced options are disabled result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "basic_options" data_schema = result["data_schema"].schema assert set(data_schema) == {"known_hosts"} @@ -205,7 +206,7 @@ async def test_option_flow(hass: HomeAssistant, parameter_data) -> None: result = await hass.config_entries.options.async_init( config_entry.entry_id, context=context ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "basic_options" data_schema = result["data_schema"].schema for other_param in basic_parameters: @@ -222,7 +223,7 @@ async def test_option_flow(hass: HomeAssistant, parameter_data) -> None: result["flow_id"], user_input=user_input_dict, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "advanced_options" for other_param in basic_parameters: if other_param == parameter: @@ -247,7 +248,7 @@ async def test_option_flow(hass: HomeAssistant, parameter_data) -> None: result["flow_id"], user_input=user_input_dict, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] is None for other_param in advanced_parameters: if other_param == parameter: @@ -261,7 +262,7 @@ async def test_option_flow(hass: HomeAssistant, parameter_data) -> None: result["flow_id"], user_input={"known_hosts": ""}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] is None expected_data = {**orig_data, "known_hosts": []} if parameter in advanced_parameters: diff --git a/tests/components/ccm15/test_config_flow.py b/tests/components/ccm15/test_config_flow.py index 87c93179f4e..01da3282885 100644 --- a/tests/components/ccm15/test_config_flow.py +++ b/tests/components/ccm15/test_config_flow.py @@ -16,7 +16,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -31,7 +31,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "1.1.1.1" assert result2["data"] == { CONF_HOST: "1.1.1.1", @@ -47,7 +47,7 @@ async def test_form_invalid_host( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -62,7 +62,7 @@ async def test_form_invalid_host( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} assert len(mock_setup_entry.mock_calls) == 0 @@ -76,7 +76,7 @@ async def test_form_invalid_host( }, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY async def test_form_cannot_connect(hass: HomeAssistant) -> None: @@ -95,7 +95,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} with patch( @@ -108,7 +108,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY async def test_form_unexpected_error(hass: HomeAssistant) -> None: @@ -128,7 +128,7 @@ async def test_form_unexpected_error(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} with patch( @@ -141,7 +141,7 @@ async def test_form_unexpected_error(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY async def test_duplicate_host(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: @@ -168,5 +168,5 @@ async def test_duplicate_host(hass: HomeAssistant, mock_setup_entry: AsyncMock) }, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/cert_expiry/test_config_flow.py b/tests/components/cert_expiry/test_config_flow.py index aa5f32c0ca2..3fd696f5953 100644 --- a/tests/components/cert_expiry/test_config_flow.py +++ b/tests/components/cert_expiry/test_config_flow.py @@ -6,10 +6,11 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.cert_expiry.const import DEFAULT_PORT, DOMAIN from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .const import HOST, PORT from .helpers import future_timestamp @@ -24,7 +25,7 @@ async def test_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -33,7 +34,7 @@ async def test_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: HOST, CONF_PORT: PORT} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == PORT @@ -45,7 +46,7 @@ async def test_user_with_bad_cert(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -56,7 +57,7 @@ async def test_user_with_bad_cert(hass: HomeAssistant) -> None: result["flow_id"], user_input={CONF_HOST: HOST, CONF_PORT: PORT} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == PORT @@ -81,7 +82,7 @@ async def test_import_host_only(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == DEFAULT_PORT @@ -106,7 +107,7 @@ async def test_import_host_and_port(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == PORT @@ -131,7 +132,7 @@ async def test_import_non_default_port(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"{HOST}:888" assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == 888 @@ -156,7 +157,7 @@ async def test_import_with_name(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == PORT @@ -175,7 +176,7 @@ async def test_bad_import(hass: HomeAssistant) -> None: data={CONF_HOST: HOST}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "import_failed" @@ -192,7 +193,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_IMPORT}, data={CONF_HOST: HOST, CONF_PORT: PORT}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" result = await hass.config_entries.flow.async_init( @@ -200,7 +201,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, data={CONF_HOST: HOST, CONF_PORT: PORT}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -217,7 +218,7 @@ async def test_abort_on_socket_failed(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: HOST} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_HOST: "resolve_failed"} with patch( @@ -227,7 +228,7 @@ async def test_abort_on_socket_failed(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: HOST} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_HOST: "connection_timeout"} with patch( @@ -237,5 +238,5 @@ async def test_abort_on_socket_failed(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: HOST} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_HOST: "connection_refused"} diff --git a/tests/components/cloudflare/test_config_flow.py b/tests/components/cloudflare/test_config_flow.py index 142eab621e5..4b0df91bc60 100644 --- a/tests/components/cloudflare/test_config_flow.py +++ b/tests/components/cloudflare/test_config_flow.py @@ -25,7 +25,7 @@ async def test_user_form(hass: HomeAssistant, cfupdate_flow) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -35,7 +35,7 @@ async def test_user_form(hass: HomeAssistant, cfupdate_flow) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zone" assert result["errors"] == {} @@ -45,7 +45,7 @@ async def test_user_form(hass: HomeAssistant, cfupdate_flow) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "records" assert result["errors"] is None @@ -56,7 +56,7 @@ async def test_user_form(hass: HomeAssistant, cfupdate_flow) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == USER_INPUT_ZONE[CONF_ZONE] assert result["data"] @@ -84,7 +84,7 @@ async def test_user_form_cannot_connect(hass: HomeAssistant, cfupdate_flow) -> N USER_INPUT, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -102,7 +102,7 @@ async def test_user_form_invalid_auth(hass: HomeAssistant, cfupdate_flow) -> Non USER_INPUT, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -122,7 +122,7 @@ async def test_user_form_unexpected_exception( USER_INPUT, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "unknown"} @@ -136,7 +136,7 @@ async def test_user_form_single_instance_allowed(hass: HomeAssistant) -> None: context={CONF_SOURCE: SOURCE_USER}, data=USER_INPUT, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -154,7 +154,7 @@ async def test_reauth_flow(hass: HomeAssistant, cfupdate_flow) -> None: }, data=entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with _patch_async_setup_entry() as mock_setup_entry: @@ -164,7 +164,7 @@ async def test_reauth_flow(hass: HomeAssistant, cfupdate_flow) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data[CONF_API_TOKEN] == "other_token" diff --git a/tests/components/co2signal/test_config_flow.py b/tests/components/co2signal/test_config_flow.py index e3bf9e3c818..7397b6e2355 100644 --- a/tests/components/co2signal/test_config_flow.py +++ b/tests/components/co2signal/test_config_flow.py @@ -26,7 +26,7 @@ async def test_form_home(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -42,7 +42,7 @@ async def test_form_home(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "CO2 Signal" assert result2["data"] == { "api_key": "api_key", @@ -57,7 +57,7 @@ async def test_form_coordinates(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -67,7 +67,7 @@ async def test_form_coordinates(hass: HomeAssistant) -> None: "api_key": "api_key", }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM with patch( "homeassistant.components.co2signal.async_setup_entry", @@ -82,7 +82,7 @@ async def test_form_coordinates(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "12.3, 45.6" assert result3["data"] == { "latitude": 12.3, @@ -99,7 +99,7 @@ async def test_form_country(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -109,7 +109,7 @@ async def test_form_country(hass: HomeAssistant) -> None: "api_key": "api_key", }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM with patch( "homeassistant.components.co2signal.async_setup_entry", @@ -123,7 +123,7 @@ async def test_form_country(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "fr" assert result3["data"] == { "country_code": "fr", @@ -167,7 +167,7 @@ async def test_form_error_handling( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": err_code} # reset mock and test if now succeeds @@ -183,7 +183,7 @@ async def test_form_error_handling( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "CO2 Signal" assert result["data"] == { "api_key": "api_key", @@ -207,7 +207,7 @@ async def test_reauth( data=None, ) - assert init_result["type"] == FlowResultType.FORM + assert init_result["type"] is FlowResultType.FORM assert init_result["step_id"] == "reauth" with patch( @@ -222,6 +222,6 @@ async def test_reauth( ) await hass.async_block_till_done() - assert configure_result["type"] == FlowResultType.ABORT + assert configure_result["type"] is FlowResultType.ABORT assert configure_result["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/color_extractor/test_config_flow.py b/tests/components/color_extractor/test_config_flow.py index 844712f1938..886c7991177 100644 --- a/tests/components/color_extractor/test_config_flow.py +++ b/tests/components/color_extractor/test_config_flow.py @@ -18,7 +18,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" with patch( @@ -30,7 +30,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: user_input={}, ) - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == "Color extractor" assert result.get("data") == {} assert result.get("options") == {} @@ -51,7 +51,7 @@ async def test_single_instance_allowed( DOMAIN, context={"source": source}, data={} ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" @@ -65,7 +65,7 @@ async def test_import_flow( data={}, ) - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == "Color extractor" assert result.get("data") == {} assert result.get("options") == {} diff --git a/tests/components/comelit/test_config_flow.py b/tests/components/comelit/test_config_flow.py index 851e179dd95..333bf09bd20 100644 --- a/tests/components/comelit/test_config_flow.py +++ b/tests/components/comelit/test_config_flow.py @@ -40,13 +40,13 @@ async def test_full_flow( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=user_input ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_HOST] == user_input[CONF_HOST] assert result["data"][CONF_PORT] == user_input[CONF_PORT] assert result["data"][CONF_PIN] == user_input[CONF_PIN] @@ -70,7 +70,7 @@ async def test_exception_connection(hass: HomeAssistant, side_effect, error) -> result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" with ( @@ -89,7 +89,7 @@ async def test_exception_connection(hass: HomeAssistant, side_effect, error) -> result["flow_id"], user_input=MOCK_USER_BRIDGE_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is not None assert result["errors"]["base"] == error @@ -119,7 +119,7 @@ async def test_reauth_successful(hass: HomeAssistant) -> None: data=mock_config.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -130,7 +130,7 @@ async def test_reauth_successful(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" @@ -161,7 +161,7 @@ async def test_reauth_not_successful(hass: HomeAssistant, side_effect, error) -> data=mock_config.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -171,7 +171,7 @@ async def test_reauth_not_successful(hass: HomeAssistant, side_effect, error) -> }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] is not None assert result["errors"]["base"] == error diff --git a/tests/components/cpuspeed/test_config_flow.py b/tests/components/cpuspeed/test_config_flow.py index 323eb80d712..0ebb8aede49 100644 --- a/tests/components/cpuspeed/test_config_flow.py +++ b/tests/components/cpuspeed/test_config_flow.py @@ -20,7 +20,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( @@ -28,7 +28,7 @@ async def test_full_user_flow( user_input={}, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "CPU Speed" assert result2.get("data") == {} @@ -49,7 +49,7 @@ async def test_already_configured( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" assert len(mock_setup_entry.mock_calls) == 0 @@ -66,7 +66,7 @@ async def test_not_compatible( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" mock_cpuinfo_config_flow.return_value = {} @@ -75,7 +75,7 @@ async def test_not_compatible( user_input={}, ) - assert result2.get("type") == FlowResultType.ABORT + assert result2.get("type") is FlowResultType.ABORT assert result2.get("reason") == "not_compatible" assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/crownstone/test_config_flow.py b/tests/components/crownstone/test_config_flow.py index 04f69f3a74a..d7705e6026b 100644 --- a/tests/components/crownstone/test_config_flow.py +++ b/tests/components/crownstone/test_config_flow.py @@ -14,7 +14,6 @@ from crownstone_cloud.exceptions import ( import pytest from serial.tools.list_ports_common import ListPortInfo -from homeassistant import data_entry_flow from homeassistant.components import usb from homeassistant.components.crownstone.const import ( CONF_USB_MANUAL_PATH, @@ -28,6 +27,7 @@ from homeassistant.components.crownstone.const import ( ) 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 @@ -186,7 +186,7 @@ async def test_no_user_input( DOMAIN, context={"source": "user"} ) # show the login form - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert crownstone_setup.call_count == 0 @@ -216,7 +216,7 @@ async def test_abort_if_configured( result = await start_config_flow(hass, get_mocked_crownstone_cloud()) # test if we abort if we try to configure the same entry - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert crownstone_setup.call_count == 0 @@ -233,7 +233,7 @@ async def test_authentication_errors( result = await start_config_flow(hass, cloud) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} # side effect: auth error account not verified @@ -243,7 +243,7 @@ async def test_authentication_errors( result = await start_config_flow(hass, cloud) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "account_not_verified"} assert crownstone_setup.call_count == 0 @@ -258,7 +258,7 @@ async def test_unknown_error( result = await start_config_flow(hass, cloud) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "unknown_error"} assert crownstone_setup.call_count == 0 @@ -278,14 +278,14 @@ async def test_successful_login_no_usb( result = await start_config_flow(hass, get_mocked_crownstone_cloud()) # should show usb form - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "usb_config" # don't setup USB dongle, create entry result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_USB_PATH: DONT_USE_USB} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == entry_data_without_usb assert result["options"] == entry_options_without_usb assert crownstone_setup.call_count == 1 @@ -311,7 +311,7 @@ async def test_successful_login_with_usb( hass, get_mocked_crownstone_cloud(create_mocked_spheres(2)) ) # should show usb form - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "usb_config" assert pyserial_comports_none_types.call_count == 1 @@ -331,7 +331,7 @@ async def test_successful_login_with_usb( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_USB_PATH: port_select} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "usb_sphere_config" assert pyserial_comports_none_types.call_count == 2 assert usb_path.call_count == 1 @@ -340,7 +340,7 @@ async def test_successful_login_with_usb( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_USB_SPHERE: "sphere_name_1"} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == entry_data_with_usb assert result["options"] == entry_options_with_usb assert crownstone_setup.call_count == 1 @@ -363,7 +363,7 @@ async def test_successful_login_with_manual_usb_path( hass, get_mocked_crownstone_cloud(create_mocked_spheres(1)) ) # should show usb form - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "usb_config" assert pyserial_comports.call_count == 1 @@ -372,7 +372,7 @@ async def test_successful_login_with_manual_usb_path( result["flow_id"], user_input={CONF_USB_PATH: MANUAL_PATH} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "usb_manual_config" assert pyserial_comports.call_count == 2 @@ -384,7 +384,7 @@ async def test_successful_login_with_manual_usb_path( # since we only have 1 sphere here, test that it's automatically selected and # creating entry without asking for user input - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == entry_data_with_manual_usb assert result["options"] == entry_options_with_manual_usb assert crownstone_setup.call_count == 1 @@ -420,7 +420,7 @@ async def test_options_flow_setup_usb( ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema @@ -434,7 +434,7 @@ async def test_options_flow_setup_usb( result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_USE_USB_OPTION: True} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "usb_config" assert pyserial_comports.call_count == 1 @@ -454,7 +454,7 @@ async def test_options_flow_setup_usb( result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_USB_PATH: port_select} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "usb_sphere_config" assert pyserial_comports.call_count == 2 assert usb_path.call_count == 1 @@ -463,7 +463,7 @@ async def test_options_flow_setup_usb( result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_USB_SPHERE: "sphere_name_1"} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == create_mocked_entry_options_conf( usb_path="/dev/serial/by-id/crownstone-usb", usb_sphere="sphere_id_1" ) @@ -497,7 +497,7 @@ async def test_options_flow_remove_usb(hass: HomeAssistant) -> None: ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema @@ -514,7 +514,7 @@ async def test_options_flow_remove_usb(hass: HomeAssistant) -> None: CONF_USB_SPHERE_OPTION: "sphere_name_0", }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == create_mocked_entry_options_conf( usb_path=None, usb_sphere=None ) @@ -550,13 +550,13 @@ async def test_options_flow_manual_usb_path( ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + 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_USE_USB_OPTION: True} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "usb_config" assert pyserial_comports.call_count == 1 @@ -565,7 +565,7 @@ async def test_options_flow_manual_usb_path( result["flow_id"], user_input={CONF_USB_PATH: MANUAL_PATH} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "usb_manual_config" assert pyserial_comports.call_count == 2 @@ -575,7 +575,7 @@ async def test_options_flow_manual_usb_path( result["flow_id"], user_input={CONF_USB_MANUAL_PATH: path} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == create_mocked_entry_options_conf( usb_path=path, usb_sphere="sphere_id_0" ) @@ -609,14 +609,14 @@ async def test_options_flow_change_usb_sphere(hass: HomeAssistant) -> None: ), ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + 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_USE_USB_OPTION: True, CONF_USB_SPHERE_OPTION: "sphere_name_2"}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == create_mocked_entry_options_conf( usb_path="/dev/serial/by-id/crownstone-usb", usb_sphere="sphere_id_2" ) diff --git a/tests/components/daikin/test_config_flow.py b/tests/components/daikin/test_config_flow.py index ece17b6aafe..6d957384d4d 100644 --- a/tests/components/daikin/test_config_flow.py +++ b/tests/components/daikin/test_config_flow.py @@ -51,7 +51,7 @@ async def test_user(hass: HomeAssistant, mock_daikin) -> None: context={"source": SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_init( @@ -59,7 +59,7 @@ async def test_user(hass: HomeAssistant, mock_daikin) -> None: context={"source": SOURCE_USER}, data={CONF_HOST: HOST}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST assert result["data"][KEY_MAC] == MAC @@ -74,7 +74,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant, mock_daikin) -> None: data={CONF_HOST: HOST, KEY_MAC: MAC}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -97,7 +97,7 @@ async def test_device_abort(hass: HomeAssistant, mock_daikin, s_effect, reason) context={"source": SOURCE_USER}, data={CONF_HOST: HOST, KEY_MAC: MAC}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": reason} assert result["step_id"] == "user" @@ -109,7 +109,7 @@ async def test_api_password_abort(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, data={CONF_HOST: HOST, CONF_API_KEY: "aa", CONF_PASSWORD: "aa"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "api_password"} assert result["step_id"] == "user" @@ -141,7 +141,7 @@ async def test_discovery_zeroconf( context={"source": source}, data=data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" MockConfigEntry(domain="daikin", unique_id=unique_id).add_to_hass(hass) @@ -151,7 +151,7 @@ async def test_discovery_zeroconf( data={CONF_HOST: HOST}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" result = await hass.config_entries.flow.async_init( @@ -160,5 +160,5 @@ async def test_discovery_zeroconf( data=data, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index a6910ef4b55..6da940e0918 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -57,14 +57,14 @@ async def test_flow_discovered_bridges( DECONZ_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "1.2.3.4"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" aioclient_mock.post( @@ -77,7 +77,7 @@ async def test_flow_discovered_bridges( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == BRIDGEID assert result["data"] == { CONF_HOST: "1.2.3.4", @@ -104,7 +104,7 @@ async def test_flow_manual_configuration_decision( result["flow_id"], user_input={CONF_HOST: CONF_MANUAL_INPUT} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual_input" result = await hass.config_entries.flow.async_configure( @@ -112,7 +112,7 @@ async def test_flow_manual_configuration_decision( user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 80}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" aioclient_mock.post( @@ -131,7 +131,7 @@ async def test_flow_manual_configuration_decision( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == BRIDGEID assert result["data"] == { CONF_HOST: "1.2.3.4", @@ -155,7 +155,7 @@ async def test_flow_manual_configuration( DECONZ_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual_input" result = await hass.config_entries.flow.async_configure( @@ -163,7 +163,7 @@ async def test_flow_manual_configuration( user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 80}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" aioclient_mock.post( @@ -182,7 +182,7 @@ async def test_flow_manual_configuration( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == BRIDGEID assert result["data"] == { CONF_HOST: "1.2.3.4", @@ -201,7 +201,7 @@ async def test_manual_configuration_after_discovery_timeout( DECONZ_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual_input" assert not hass.config_entries.flow._progress[result["flow_id"]].bridges @@ -216,7 +216,7 @@ async def test_manual_configuration_after_discovery_ResponseError( DECONZ_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual_input" assert not hass.config_entries.flow._progress[result["flow_id"]].bridges @@ -237,7 +237,7 @@ async def test_manual_configuration_update_configuration( DECONZ_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual_input" result = await hass.config_entries.flow.async_configure( @@ -245,7 +245,7 @@ async def test_manual_configuration_update_configuration( user_input={CONF_HOST: "2.3.4.5", CONF_PORT: 80}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" aioclient_mock.post( @@ -264,7 +264,7 @@ async def test_manual_configuration_update_configuration( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == "2.3.4.5" @@ -285,7 +285,7 @@ async def test_manual_configuration_dont_update_configuration( DECONZ_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual_input" result = await hass.config_entries.flow.async_configure( @@ -293,7 +293,7 @@ async def test_manual_configuration_dont_update_configuration( user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 80}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" aioclient_mock.post( @@ -312,7 +312,7 @@ async def test_manual_configuration_dont_update_configuration( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -330,7 +330,7 @@ async def test_manual_configuration_timeout_get_bridge( DECONZ_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual_input" result = await hass.config_entries.flow.async_configure( @@ -338,7 +338,7 @@ async def test_manual_configuration_timeout_get_bridge( user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 80}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" aioclient_mock.post( @@ -353,7 +353,7 @@ async def test_manual_configuration_timeout_get_bridge( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_bridges" @@ -384,7 +384,7 @@ async def test_link_step_fails( result["flow_id"], user_input={CONF_HOST: "1.2.3.4"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" aioclient_mock.post("http://1.2.3.4:80/api", exc=raised_error) @@ -393,7 +393,7 @@ async def test_link_step_fails( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {"base": error_string} @@ -410,7 +410,7 @@ async def test_reauth_flow_update_configuration( context={"source": SOURCE_REAUTH}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" new_api_key = "new_key" @@ -431,7 +431,7 @@ async def test_reauth_flow_update_configuration( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_API_KEY] == new_api_key @@ -454,7 +454,7 @@ async def test_flow_ssdp_discovery( context={"source": SOURCE_SSDP}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" flows = hass.config_entries.flow.async_progress() @@ -471,7 +471,7 @@ async def test_flow_ssdp_discovery( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == BRIDGEID assert result["data"] == { CONF_HOST: "1.2.3.4", @@ -505,7 +505,7 @@ async def test_ssdp_discovery_update_configuration( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == "2.3.4.5" assert len(mock_setup_entry.mock_calls) == 1 @@ -531,7 +531,7 @@ async def test_ssdp_discovery_dont_update_configuration( context={"source": SOURCE_SSDP}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == "1.2.3.4" @@ -558,7 +558,7 @@ async def test_ssdp_discovery_dont_update_existing_hassio_configuration( context={"source": SOURCE_SSDP}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == "1.2.3.4" @@ -581,7 +581,7 @@ async def test_flow_hassio_discovery(hass: HomeAssistant) -> None: ), context={"source": SOURCE_HASSIO}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "hassio_confirm" assert result["description_placeholders"] == {"addon": "Mock Addon"} @@ -600,7 +600,7 @@ async def test_flow_hassio_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].data == { CONF_HOST: "mock-deconz", CONF_PORT: 80, @@ -636,7 +636,7 @@ async def test_hassio_discovery_update_configuration( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == "2.3.4.5" assert config_entry.data[CONF_PORT] == 8080 @@ -666,7 +666,7 @@ async def test_hassio_discovery_dont_update_configuration( context={"source": SOURCE_HASSIO}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -678,7 +678,7 @@ async def test_option_flow( result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "deconz_devices" result = await hass.config_entries.options.async_configure( @@ -690,7 +690,7 @@ async def test_option_flow( }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_ALLOW_CLIP_SENSOR: False, CONF_ALLOW_DECONZ_GROUPS: False, diff --git a/tests/components/deluge/test_config_flow.py b/tests/components/deluge/test_config_flow.py index 1e7cecd8850..37229d4a72e 100644 --- a/tests/components/deluge/test_config_flow.py +++ b/tests/components/deluge/test_config_flow.py @@ -62,7 +62,7 @@ async def test_flow_user(hass: HomeAssistant, api) -> None: context={"source": SOURCE_USER}, data=CONF_DATA, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == CONF_DATA @@ -80,7 +80,7 @@ async def test_flow_user_already_configured(hass: HomeAssistant, api) -> None: DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -89,7 +89,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant, conn_error) -> None result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -99,7 +99,7 @@ async def test_flow_user_unknown_error(hass: HomeAssistant, unknown_error) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} @@ -123,13 +123,13 @@ async def test_flow_reauth(hass: HomeAssistant, api) -> None: data=CONF_DATA, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONF_DATA, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data == CONF_DATA diff --git a/tests/components/denonavr/test_config_flow.py b/tests/components/denonavr/test_config_flow.py index 5f5a5c8f17c..f675b188fb9 100644 --- a/tests/components/denonavr/test_config_flow.py +++ b/tests/components/denonavr/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.components.denonavr.config_flow import ( CONF_MANUFACTURER, @@ -20,6 +20,7 @@ from homeassistant.components.denonavr.config_flow import ( ) from homeassistant.const import CONF_HOST, CONF_MODEL from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -436,7 +437,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -449,7 +450,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_SHOW_ALL_SOURCES: True, CONF_ZONE2: True, diff --git a/tests/components/derivative/test_config_flow.py b/tests/components/derivative/test_config_flow.py index 9002a201f85..3db0227c2a6 100644 --- a/tests/components/derivative/test_config_flow.py +++ b/tests/components/derivative/test_config_flow.py @@ -20,7 +20,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -39,7 +39,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "My derivative" assert result["data"] == {} assert result["options"] == { @@ -96,7 +96,7 @@ async def test_options(hass: HomeAssistant, platform) -> None: 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["type"] is FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema assert get_suggested(schema, "round") == 1.0 @@ -112,7 +112,7 @@ async def test_options(hass: HomeAssistant, platform) -> None: "unit_time": "h", }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "name": "My derivative", "round": 2.0, diff --git a/tests/components/devialet/test_config_flow.py b/tests/components/devialet/test_config_flow.py index 05174b50f0d..200a2673913 100644 --- a/tests/components/devialet/test_config_flow.py +++ b/tests/components/devialet/test_config_flow.py @@ -31,7 +31,7 @@ async def test_show_user_form(hass: HomeAssistant) -> None: ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM async def test_cannot_connect( @@ -49,7 +49,7 @@ async def test_cannot_connect( data=user_input, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -67,7 +67,7 @@ async def test_user_device_exists_abort( data=user_input, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -82,7 +82,7 @@ async def test_full_user_flow_implementation( context={CONF_SOURCE: SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" user_input = MOCK_USER_INPUT.copy() @@ -94,7 +94,7 @@ async def test_full_user_flow_implementation( user_input=user_input, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"] @@ -150,6 +150,6 @@ async def test_async_step_confirm( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_INPUT.copy() ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/devolo_home_control/test_config_flow.py b/tests/components/devolo_home_control/test_config_flow.py index 1aa8e7f829d..d26da474e39 100644 --- a/tests/components/devolo_home_control/test_config_flow.py +++ b/tests/components/devolo_home_control/test_config_flow.py @@ -25,7 +25,7 @@ async def test_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} await _setup(hass, result) @@ -39,7 +39,7 @@ async def test_form_invalid_credentials_user(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( @@ -62,7 +62,7 @@ async def test_form_already_configured(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, data={"username": "test-username", "password": "test-password"}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -72,7 +72,7 @@ async def test_form_advanced_options(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER, "show_advanced_options": True}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -115,7 +115,7 @@ async def test_form_zeroconf(hass: HomeAssistant) -> None: ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM await _setup(hass, result) @@ -131,7 +131,7 @@ async def test_form_invalid_credentials_zeroconf(hass: HomeAssistant) -> None: ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -150,7 +150,7 @@ async def test_zeroconf_wrong_device(hass: HomeAssistant) -> None: ) assert result["reason"] == "Not a devolo Home Control gateway." - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT result = await hass.config_entries.flow.async_init( DOMAIN, @@ -159,7 +159,7 @@ async def test_zeroconf_wrong_device(hass: HomeAssistant) -> None: ) assert result["reason"] == "Not a devolo Home Control gateway." - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT async def test_form_reauth(hass: HomeAssistant) -> None: @@ -180,7 +180,7 @@ async def test_form_reauth(hass: HomeAssistant) -> None: ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with ( patch( @@ -198,7 +198,7 @@ async def test_form_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert len(mock_setup_entry.mock_calls) == 1 @@ -246,7 +246,7 @@ async def test_form_uuid_change_reauth(hass: HomeAssistant) -> None: ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with ( patch( @@ -264,7 +264,7 @@ async def test_form_uuid_change_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "reauth_failed"} diff --git a/tests/components/devolo_home_network/test_config_flow.py b/tests/components/devolo_home_network/test_config_flow.py index 5d23037df54..5aa2bfa274e 100644 --- a/tests/components/devolo_home_network/test_config_flow.py +++ b/tests/components/devolo_home_network/test_config_flow.py @@ -37,7 +37,7 @@ async def test_form(hass: HomeAssistant, info: dict[str, Any]) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -52,7 +52,7 @@ async def test_form(hass: HomeAssistant, info: dict[str, Any]) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["result"].unique_id == info["serial_number"] assert result2["title"] == info["title"] assert result2["data"] == { @@ -83,7 +83,7 @@ async def test_form_error(hass: HomeAssistant, exception_type, expected_error) - }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {CONF_BASE: expected_error} @@ -96,7 +96,7 @@ async def test_zeroconf(hass: HomeAssistant) -> None: ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["description_placeholders"] == {"host_name": "test"} context = next( @@ -134,7 +134,7 @@ async def test_abort_zeroconf_wrong_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_ZEROCONF}, data=DISCOVERY_INFO_WRONG_DEVICE, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "home_control" @@ -158,7 +158,7 @@ async def test_abort_if_configued(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" # Abort on concurrent zeroconf discovery flow @@ -167,7 +167,7 @@ async def test_abort_if_configued(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_ZEROCONF}, data=DISCOVERY_INFO_CHANGED, ) - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "already_configured" assert entry.data[CONF_IP_ADDRESS] == IP_ALT @@ -192,7 +192,7 @@ async def test_form_reauth(hass: HomeAssistant) -> None: ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.devolo_home_network.async_setup_entry", @@ -204,7 +204,7 @@ async def test_form_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/dexcom/test_config_flow.py b/tests/components/dexcom/test_config_flow.py index f87f365a7e6..e8893e21d0e 100644 --- a/tests/components/dexcom/test_config_flow.py +++ b/tests/components/dexcom/test_config_flow.py @@ -4,10 +4,11 @@ from unittest.mock import patch from pydexcom import AccountError, SessionError -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.dexcom.const import DOMAIN, MG_DL, MMOL_L from homeassistant.const import CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import CONFIG @@ -20,7 +21,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -39,7 +40,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == CONFIG[CONF_USERNAME] assert result2["data"] == CONFIG assert len(mock_setup_entry.mock_calls) == 1 @@ -60,7 +61,7 @@ async def test_form_account_error(hass: HomeAssistant) -> None: CONFIG, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -79,7 +80,7 @@ async def test_form_session_error(hass: HomeAssistant) -> None: CONFIG, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -98,7 +99,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: CONFIG, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -113,14 +114,14 @@ async def test_option_flow_default(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result2 = await hass.config_entries.options.async_configure( result["flow_id"], user_input={}, ) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { CONF_UNIT_OF_MEASUREMENT: MG_DL, } @@ -137,14 +138,14 @@ async def test_option_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + 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_UNIT_OF_MEASUREMENT: MMOL_L}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_UNIT_OF_MEASUREMENT: MMOL_L, } diff --git a/tests/components/directv/test_config_flow.py b/tests/components/directv/test_config_flow.py index 569e165a0a6..ad22aa871b7 100644 --- a/tests/components/directv/test_config_flow.py +++ b/tests/components/directv/test_config_flow.py @@ -33,7 +33,7 @@ async def test_show_user_form(hass: HomeAssistant) -> None: ) assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM async def test_show_ssdp_form( @@ -47,7 +47,7 @@ async def test_show_ssdp_form( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "ssdp_confirm" assert result["description_placeholders"] == {CONF_NAME: HOST} @@ -65,7 +65,7 @@ async def test_cannot_connect( data=user_input, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -83,7 +83,7 @@ async def test_ssdp_cannot_connect( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -100,7 +100,7 @@ async def test_ssdp_confirm_cannot_connect( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -117,7 +117,7 @@ async def test_user_device_exists_abort( data=user_input, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -134,7 +134,7 @@ async def test_ssdp_device_exists_abort( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -152,7 +152,7 @@ async def test_ssdp_with_receiver_id_device_exists_abort( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -171,7 +171,7 @@ async def test_unknown_error( data=user_input, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -190,7 +190,7 @@ async def test_ssdp_unknown_error( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -209,7 +209,7 @@ async def test_ssdp_confirm_unknown_error( data=discovery_info, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -224,7 +224,7 @@ async def test_full_user_flow_implementation( context={CONF_SOURCE: SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" user_input = MOCK_USER_INPUT.copy() @@ -234,7 +234,7 @@ async def test_full_user_flow_implementation( user_input=user_input, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"] @@ -253,7 +253,7 @@ async def test_full_ssdp_flow_implementation( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "ssdp_confirm" assert result["description_placeholders"] == {CONF_NAME: HOST} @@ -261,7 +261,7 @@ async def test_full_ssdp_flow_implementation( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"] diff --git a/tests/components/discord/test_config_flow.py b/tests/components/discord/test_config_flow.py index ba1909c48c8..9b37179e86d 100644 --- a/tests/components/discord/test_config_flow.py +++ b/tests/components/discord/test_config_flow.py @@ -2,10 +2,11 @@ import nextcord -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.discord.const import DOMAIN from homeassistant.const import CONF_API_TOKEN, CONF_SOURCE from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import ( CONF_DATA, @@ -29,7 +30,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"] == CONF_DATA @@ -46,7 +47,7 @@ async def test_flow_user_already_configured(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -59,7 +60,7 @@ async def test_flow_user_invalid_auth(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, data=CONF_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -68,7 +69,7 @@ async def test_flow_user_invalid_auth(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"] == CONF_DATA @@ -82,7 +83,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, data=CONF_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -91,7 +92,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"] == CONF_DATA @@ -105,7 +106,7 @@ async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, data=CONF_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} @@ -114,7 +115,7 @@ async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_INPUT, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"] == CONF_DATA @@ -132,7 +133,7 @@ async def test_flow_reauth(hass: HomeAssistant) -> None: data=entry.data, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" new_conf = {CONF_API_TOKEN: "1234567890123"} @@ -142,7 +143,7 @@ async def test_flow_reauth(hass: HomeAssistant) -> None: result["flow_id"], user_input=new_conf, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "invalid_auth"} @@ -151,6 +152,6 @@ async def test_flow_reauth(hass: HomeAssistant) -> None: result["flow_id"], user_input=new_conf, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data == CONF_DATA | new_conf diff --git a/tests/components/discovergy/test_config_flow.py b/tests/components/discovergy/test_config_flow.py index b8da429d881..2464ba3846f 100644 --- a/tests/components/discovergy/test_config_flow.py +++ b/tests/components/discovergy/test_config_flow.py @@ -5,11 +5,11 @@ from unittest.mock import AsyncMock, patch from pydiscovergy.error import DiscovergyClientError, HTTPError, InvalidLogin import pytest -from homeassistant import data_entry_flow from homeassistant.components.discovergy.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 from tests.common import MockConfigEntry @@ -19,7 +19,7 @@ async def test_form(hass: HomeAssistant, discovergy: AsyncMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -35,7 +35,7 @@ async def test_form(hass: HomeAssistant, discovergy: AsyncMock) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "test@example.com" assert result2["data"] == { CONF_EMAIL: "test@example.com", @@ -56,7 +56,7 @@ async def test_reauth( data=None, ) - assert init_result["type"] == data_entry_flow.FlowResultType.FORM + assert init_result["type"] is FlowResultType.FORM assert init_result["step_id"] == "reauth" with patch( @@ -72,7 +72,7 @@ async def test_reauth( ) await hass.async_block_till_done() - assert configure_result["type"] == data_entry_flow.FlowResultType.ABORT + assert configure_result["type"] is FlowResultType.ABORT assert configure_result["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 @@ -100,7 +100,7 @@ async def test_form_fail( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": message} @@ -114,6 +114,6 @@ async def test_form_fail( }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "test@example.com" assert "errors" not in result diff --git a/tests/components/dlink/test_config_flow.py b/tests/components/dlink/test_config_flow.py index 01e61f7a8fa..b6f025bb5b0 100644 --- a/tests/components/dlink/test_config_flow.py +++ b/tests/components/dlink/test_config_flow.py @@ -2,12 +2,12 @@ from unittest.mock import MagicMock, patch -from homeassistant import data_entry_flow from homeassistant.components import dhcp from homeassistant.components.dlink.const import DEFAULT_NAME, DOMAIN from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .conftest import ( CONF_DATA, @@ -35,7 +35,7 @@ async def test_flow_user(hass: HomeAssistant, mocked_plug: MagicMock) -> None: result["flow_id"], user_input=CONF_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == CONF_DATA @@ -48,7 +48,7 @@ async def test_flow_user_already_configured( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -62,7 +62,7 @@ async def test_flow_user_cannot_connect( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "cannot_connect" @@ -71,7 +71,7 @@ async def test_flow_user_cannot_connect( result["flow_id"], user_input=CONF_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == CONF_DATA @@ -85,7 +85,7 @@ async def test_flow_user_unknown_error( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "unknown" @@ -94,7 +94,7 @@ async def test_flow_user_unknown_error( result["flow_id"], user_input=CONF_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == CONF_DATA @@ -104,14 +104,14 @@ async def test_dhcp(hass: HomeAssistant, mocked_plug: MagicMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm_discovery" with patch_config_flow(mocked_plug), _patch_setup_entry(): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONF_DHCP_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == CONF_DATA @@ -123,14 +123,14 @@ async def test_dhcp_failed_legacy_auth( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm_discovery" with patch_config_flow(mocked_plug_legacy_no_auth): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONF_DHCP_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == "cannot_connect" with patch_config_flow(mocked_plug), _patch_setup_entry(): @@ -138,7 +138,7 @@ async def test_dhcp_failed_legacy_auth( result["flow_id"], user_input=CONF_DHCP_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == CONF_DATA @@ -151,7 +151,7 @@ async def test_dhcp_already_configured( DOMAIN, context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.unique_id == "aabbccddeeff" @@ -168,14 +168,14 @@ async def test_dhcp_unique_id_assignment( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, data=dhcp_data ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm_discovery" with patch_config_flow(mocked_plug), _patch_setup_entry(): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONF_DHCP_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == CONF_DATA | {CONF_HOST: "2.3.4.5"} assert result["result"].unique_id == "11:22:33:44:55:66" @@ -188,6 +188,6 @@ async def test_dhcp_changed_ip( DOMAIN, context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW_NEW_IP ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry_with_uid.data[CONF_HOST] == "5.6.7.8" diff --git a/tests/components/dlna_dmr/test_config_flow.py b/tests/components/dlna_dmr/test_config_flow.py index 32cfd8ad5a9..55cf20859d3 100644 --- a/tests/components/dlna_dmr/test_config_flow.py +++ b/tests/components/dlna_dmr/test_config_flow.py @@ -112,7 +112,7 @@ async def test_user_flow_undiscovered_manual(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual" @@ -120,7 +120,7 @@ async def test_user_flow_undiscovered_manual(hass: HomeAssistant) -> None: result["flow_id"], user_input={CONF_URL: MOCK_DEVICE_LOCATION} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -144,7 +144,7 @@ async def test_user_flow_discovered_manual( result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -152,7 +152,7 @@ async def test_user_flow_discovered_manual( result["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual" @@ -160,7 +160,7 @@ async def test_user_flow_discovered_manual( result["flow_id"], user_input={CONF_URL: MOCK_DEVICE_LOCATION} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -182,7 +182,7 @@ async def test_user_flow_selected(hass: HomeAssistant, ssdp_scanner_mock: Mock) result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -190,7 +190,7 @@ async def test_user_flow_selected(hass: HomeAssistant, ssdp_scanner_mock: Mock) result["flow_id"], user_input={CONF_HOST: MOCK_DEVICE_NAME} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -211,7 +211,7 @@ async def test_user_flow_uncontactable( result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual" @@ -219,7 +219,7 @@ async def test_user_flow_uncontactable( result["flow_id"], user_input={CONF_URL: MOCK_DEVICE_LOCATION} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} assert result["step_id"] == "manual" @@ -244,7 +244,7 @@ async def test_user_flow_embedded_st( result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual" @@ -252,7 +252,7 @@ async def test_user_flow_embedded_st( result["flow_id"], user_input={CONF_URL: MOCK_DEVICE_LOCATION} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -272,7 +272,7 @@ async def test_user_flow_wrong_st(hass: HomeAssistant, domain_data_mock: Mock) - result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual" @@ -280,7 +280,7 @@ async def test_user_flow_wrong_st(hass: HomeAssistant, domain_data_mock: Mock) - result["flow_id"], user_input={CONF_URL: MOCK_DEVICE_LOCATION} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "not_dmr"} assert result["step_id"] == "manual" @@ -295,7 +295,7 @@ async def test_ssdp_flow_success(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=MOCK_DISCOVERY, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -303,7 +303,7 @@ async def test_ssdp_flow_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -327,7 +327,7 @@ async def test_ssdp_flow_unavailable( context={"source": config_entries.SOURCE_SSDP}, data=MOCK_DISCOVERY, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" domain_data_mock.upnp_factory.async_create_device.side_effect = UpnpError @@ -337,7 +337,7 @@ async def test_ssdp_flow_unavailable( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -368,7 +368,7 @@ async def test_ssdp_flow_existing( }, ), ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry_mock.data[CONF_URL] == NEW_DEVICE_LOCATION @@ -388,7 +388,7 @@ async def test_ssdp_flow_duplicate_location( context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry_mock.data[CONF_URL] == MOCK_DEVICE_LOCATION @@ -414,7 +414,7 @@ async def test_ssdp_duplicate_mac_ignored_entry( context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -437,7 +437,7 @@ async def test_ssdp_duplicate_mac_configured_entry( context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -453,7 +453,7 @@ async def test_ssdp_add_mac( context={"source": config_entries.SOURCE_SSDP}, data=MOCK_DISCOVERY, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" await hass.async_block_till_done() @@ -474,7 +474,7 @@ async def test_ssdp_dont_remove_mac( context={"source": config_entries.SOURCE_SSDP}, data=MOCK_DISCOVERY, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" await hass.async_block_till_done() @@ -502,7 +502,7 @@ async def test_ssdp_flow_upnp_udn( }, ), ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry_mock.data[CONF_URL] == NEW_DEVICE_LOCATION @@ -518,7 +518,7 @@ async def test_ssdp_missing_services(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_dmr" # Service list does not contain services @@ -530,7 +530,7 @@ async def test_ssdp_missing_services(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_dmr" # AVTransport service is missing @@ -546,7 +546,7 @@ async def test_ssdp_missing_services(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=discovery ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_dmr" @@ -568,7 +568,7 @@ async def test_ssdp_single_service(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_dmr" @@ -582,7 +582,7 @@ async def test_ssdp_ignore_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "alternative_integration" discovery = dataclasses.replace(MOCK_DISCOVERY) @@ -595,7 +595,7 @@ async def test_ssdp_ignore_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "alternative_integration" for manufacturer, model in [ @@ -613,7 +613,7 @@ async def test_ssdp_ignore_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "alternative_integration" @@ -635,7 +635,7 @@ async def test_ignore_flow(hass: HomeAssistant, ssdp_scanner_mock: Mock) -> None ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -659,7 +659,7 @@ async def test_ignore_flow_no_ssdp( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: None, @@ -680,7 +680,7 @@ async def test_unignore_flow(hass: HomeAssistant, ssdp_scanner_mock: Mock) -> No ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME # Device was found via SSDP, matching the 2nd device type tried @@ -698,7 +698,7 @@ async def test_unignore_flow(hass: HomeAssistant, ssdp_scanner_mock: Mock) -> No context={"source": config_entries.SOURCE_UNIGNORE}, data={"unique_id": MOCK_DEVICE_UDN}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -706,7 +706,7 @@ async def test_unignore_flow(hass: HomeAssistant, ssdp_scanner_mock: Mock) -> No ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -730,7 +730,7 @@ async def test_unignore_flow_offline( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME # Device is not in the SSDP discoveries (perhaps HA restarted between ignore and unignore) @@ -742,7 +742,7 @@ async def test_unignore_flow_offline( context={"source": config_entries.SOURCE_UNIGNORE}, data={"unique_id": MOCK_DEVICE_UDN}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "discovery_error" @@ -756,7 +756,7 @@ async def test_get_mac_address_ipv4( context={"source": config_entries.SOURCE_SSDP}, data=MOCK_DISCOVERY, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" mock_get_mac_address.assert_called_once_with(ip=MOCK_DEVICE_HOST_ADDR) @@ -780,7 +780,7 @@ async def test_get_mac_address_ipv6( context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" # The scope must be removed for get_mac_address to work correctly @@ -821,7 +821,7 @@ async def test_options_flow( config_entry_mock.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry_mock.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {} @@ -835,7 +835,7 @@ async def test_options_flow( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {"base": "invalid_url"} @@ -850,7 +850,7 @@ async def test_options_flow( }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_LISTEN_PORT: 2222, CONF_CALLBACK_URL_OVERRIDE: "http://override/callback", diff --git a/tests/components/dlna_dms/test_config_flow.py b/tests/components/dlna_dms/test_config_flow.py index 8a2bda611a7..b61b4a42c49 100644 --- a/tests/components/dlna_dms/test_config_flow.py +++ b/tests/components/dlna_dms/test_config_flow.py @@ -11,11 +11,12 @@ from unittest.mock import Mock, patch from async_upnp_client.exceptions import UpnpError import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.components.dlna_dms.const import CONF_SOURCE_ID, DOMAIN from homeassistant.const import CONF_DEVICE_ID, CONF_HOST, CONF_URL from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .conftest import ( MOCK_DEVICE_HOST, @@ -88,7 +89,7 @@ async def test_user_flow(hass: HomeAssistant, ssdp_scanner_mock: Mock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -97,7 +98,7 @@ async def test_user_flow(hass: HomeAssistant, ssdp_scanner_mock: Mock) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -121,7 +122,7 @@ async def test_user_flow_no_devices( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -135,7 +136,7 @@ async def test_ssdp_flow_success(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=MOCK_DISCOVERY, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -143,7 +144,7 @@ async def test_ssdp_flow_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -166,7 +167,7 @@ async def test_ssdp_flow_unavailable( context={"source": config_entries.SOURCE_SSDP}, data=MOCK_DISCOVERY, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" upnp_factory_mock.async_create_device.side_effect = UpnpError @@ -176,7 +177,7 @@ async def test_ssdp_flow_unavailable( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -206,7 +207,7 @@ async def test_ssdp_flow_existing( }, ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry_mock.data[CONF_URL] == NEW_DEVICE_LOCATION @@ -221,7 +222,7 @@ async def test_ssdp_flow_duplicate_location( context={"source": config_entries.SOURCE_SSDP}, data=MOCK_DISCOVERY, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry_mock.data[CONF_URL] == MOCK_DEVICE_LOCATION @@ -235,7 +236,7 @@ async def test_ssdp_flow_bad_data(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "bad_ssdp" # Missing USN @@ -245,7 +246,7 @@ async def test_ssdp_flow_bad_data(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "bad_ssdp" @@ -285,7 +286,7 @@ async def test_duplicate_name( context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -293,7 +294,7 @@ async def test_duplicate_name( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: new_device_location, @@ -323,7 +324,7 @@ async def test_ssdp_flow_upnp_udn( }, ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry_mock.data[CONF_URL] == NEW_DEVICE_LOCATION @@ -339,7 +340,7 @@ async def test_ssdp_missing_services(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_dms" # Service list does not contain services @@ -351,7 +352,7 @@ async def test_ssdp_missing_services(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_dms" # ContentDirectory service is missing @@ -367,7 +368,7 @@ async def test_ssdp_missing_services(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=discovery ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_dms" @@ -389,5 +390,5 @@ async def test_ssdp_single_service(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_dms" diff --git a/tests/components/dnsip/test_config_flow.py b/tests/components/dnsip/test_config_flow.py index 5bfa1539d44..54ce26b15a8 100644 --- a/tests/components/dnsip/test_config_flow.py +++ b/tests/components/dnsip/test_config_flow.py @@ -54,7 +54,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "home-assistant.io" assert result2["data"] == { "hostname": "home-assistant.io", @@ -99,7 +99,7 @@ async def test_form_adv(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "home-assistant.io" assert result2["data"] == { "hostname": "home-assistant.io", @@ -132,7 +132,7 @@ async def test_form_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_hostname"} @@ -178,7 +178,7 @@ async def test_flow_already_exist(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -209,7 +209,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -221,7 +221,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "resolver": "8.8.8.8", "resolver_ipv6": "2001:4860:4860::8888", @@ -257,7 +257,7 @@ async def test_options_flow_empty_return(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -266,7 +266,7 @@ async def test_options_flow_empty_return(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "resolver": "208.67.222.222", "resolver_ipv6": "2620:119:53::53", @@ -337,7 +337,7 @@ async def test_options_error(hass: HomeAssistant, p_input: dict[str, str]) -> No ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "init" if p_input[CONF_IPV4]: assert result2["errors"] == {"resolver": "invalid_resolver"} diff --git a/tests/components/doorbird/test_config_flow.py b/tests/components/doorbird/test_config_flow.py index 4939bada6f8..5d73c0785a4 100644 --- a/tests/components/doorbird/test_config_flow.py +++ b/tests/components/doorbird/test_config_flow.py @@ -6,11 +6,12 @@ from unittest.mock import MagicMock, Mock, patch import pytest import requests -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.doorbird.const import CONF_EVENTS, DOMAIN from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -46,7 +47,7 @@ async def test_user_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} doorbirdapi = _get_mock_doorbirdapi_return_values( @@ -195,7 +196,7 @@ async def test_form_zeroconf_correct_oui(hass: HomeAssistant) -> None: ), ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -265,7 +266,7 @@ async def test_form_zeroconf_correct_oui_wrong_device( ), ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_doorbird_device" @@ -285,7 +286,7 @@ async def test_form_user_cannot_connect(hass: HomeAssistant) -> None: VALID_CONFIG, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -326,12 +327,12 @@ async def test_options_flow(hass: HomeAssistant) -> None: ): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + 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_EVENTS: "eventa, eventc, eventq"} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_EVENTS: ["eventa", "eventc", "eventq"]} diff --git a/tests/components/dormakaba_dkey/test_config_flow.py b/tests/components/dormakaba_dkey/test_config_flow.py index d29e176bb7e..499e5844949 100644 --- a/tests/components/dormakaba_dkey/test_config_flow.py +++ b/tests/components/dormakaba_dkey/test_config_flow.py @@ -27,7 +27,7 @@ async def test_user_step_success(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -37,7 +37,7 @@ async def test_user_step_success(hass: HomeAssistant) -> None: CONF_ADDRESS: DKEY_DISCOVERY_INFO.address, }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "associate" assert result["errors"] is None @@ -53,7 +53,7 @@ async def test_user_step_no_devices_found(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -74,7 +74,7 @@ async def test_user_step_no_new_devices_found(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -88,7 +88,7 @@ async def test_user_step_device_added_between_steps_1(hass: HomeAssistant) -> No DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -101,7 +101,7 @@ async def test_user_step_device_added_between_steps_1(hass: HomeAssistant) -> No result["flow_id"], user_input={"address": DKEY_DISCOVERY_INFO.address}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -114,7 +114,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=DKEY_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -125,7 +125,7 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -133,7 +133,7 @@ async def test_async_step_user_takes_precedence_over_discovery( CONF_ADDRESS: DKEY_DISCOVERY_INFO.address, }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "associate" assert result["errors"] is None @@ -150,12 +150,12 @@ async def test_bluetooth_step_success(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=DKEY_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" assert result["errors"] is None result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "associate" assert result["errors"] is None @@ -178,7 +178,7 @@ async def _test_common_success(hass: HomeAssistant, result: FlowResult) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], {"activation_code": "1234-1234"} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DKEY_DISCOVERY_INFO.name assert result["data"] == { CONF_ADDRESS: DKEY_DISCOVERY_INFO.address, @@ -200,7 +200,7 @@ async def test_bluetooth_step_already_configured(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=DKEY_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -211,7 +211,7 @@ async def test_bluetooth_step_already_in_progress(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=DKEY_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -219,7 +219,7 @@ async def test_bluetooth_step_already_in_progress(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=DKEY_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -237,17 +237,17 @@ async def test_bluetooth_step_cannot_connect(hass: HomeAssistant, exc, error) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=DKEY_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" assert result["errors"] is None result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" assert result["errors"] is None result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "associate" assert result["errors"] is None @@ -258,7 +258,7 @@ async def test_bluetooth_step_cannot_connect(hass: HomeAssistant, exc, error) -> result = await hass.config_entries.flow.async_configure( result["flow_id"], {"activation_code": "1234-1234"} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == error @@ -276,17 +276,17 @@ async def test_bluetooth_step_cannot_associate(hass: HomeAssistant, exc, error) context={"source": config_entries.SOURCE_BLUETOOTH}, data=DKEY_DISCOVERY_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" assert result["errors"] is None result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" assert result["errors"] is None result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "associate" assert result["errors"] is None @@ -297,7 +297,7 @@ async def test_bluetooth_step_cannot_associate(hass: HomeAssistant, exc, error) result = await hass.config_entries.flow.async_configure( result["flow_id"], {"activation_code": "1234-1234"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "associate" assert result["errors"] == {"base": error} @@ -315,7 +315,7 @@ async def test_reauth(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_REAUTH, "entry_id": entry.entry_id}, data=entry.data, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( @@ -328,7 +328,7 @@ async def test_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "no_longer_in_range"} @@ -342,7 +342,7 @@ async def test_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "associate" assert result["errors"] is None @@ -359,7 +359,7 @@ async def test_reauth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], {"activation_code": "1234-1234"} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data == { CONF_ADDRESS: DKEY_DISCOVERY_INFO.address, diff --git a/tests/components/downloader/test_config_flow.py b/tests/components/downloader/test_config_flow.py index 5e75a9b33ba..45c2302b605 100644 --- a/tests/components/downloader/test_config_flow.py +++ b/tests/components/downloader/test_config_flow.py @@ -30,13 +30,13 @@ async def test_user_form(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONFIG, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.downloader.config_flow.DownloaderConfigFlow._validate_input", side_effect=DirectoryDoesNotExist, ): - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -54,7 +54,7 @@ async def test_user_form(hass: HomeAssistant) -> None: user_input=CONFIG, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Downloader" assert result["data"] == {"download_dir": "download_dir"} @@ -73,7 +73,7 @@ async def test_single_instance_allowed( DOMAIN, context={"source": source} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -95,7 +95,7 @@ async def test_import_flow_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Downloader" assert result["data"] == {} assert result["options"] == {} diff --git a/tests/components/dremel_3d_printer/test_config_flow.py b/tests/components/dremel_3d_printer/test_config_flow.py index 938068aa9b0..5484f1e1191 100644 --- a/tests/components/dremel_3d_printer/test_config_flow.py +++ b/tests/components/dremel_3d_printer/test_config_flow.py @@ -23,7 +23,7 @@ async def test_full_user_flow_implementation(hass: HomeAssistant, connection) -> DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch_async_setup_entry(): @@ -31,7 +31,7 @@ async def test_full_user_flow_implementation(hass: HomeAssistant, connection) -> result["flow_id"], user_input=CONF_DATA ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "DREMEL 3D45" assert result["data"] == CONF_DATA @@ -43,7 +43,7 @@ async def test_already_configured( result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -54,7 +54,7 @@ async def test_cannot_connect(hass: HomeAssistant, connection) -> None: DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -63,7 +63,7 @@ async def test_cannot_connect(hass: HomeAssistant, connection) -> None: result["flow_id"], user_input=CONF_DATA ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == CONF_DATA @@ -74,7 +74,7 @@ async def test_unknown_error(hass: HomeAssistant, connection) -> None: DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} @@ -83,6 +83,6 @@ async def test_unknown_error(hass: HomeAssistant, connection) -> None: result["flow_id"], user_input=CONF_DATA ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "DREMEL 3D45" assert result["data"] == CONF_DATA diff --git a/tests/components/drop_connect/test_config_flow.py b/tests/components/drop_connect/test_config_flow.py index 180b6fef860..4785ea9348f 100644 --- a/tests/components/drop_connect/test_config_flow.py +++ b/tests/components/drop_connect/test_config_flow.py @@ -32,7 +32,7 @@ async def test_mqtt_setup(hass: HomeAssistant, mqtt_mock: MqttMockHAClient) -> N ) await hass.async_block_till_done() assert result is not None - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "drop_command_topic": "drop_connect/DROP-1_C0FFEE/cmd/255", "drop_data_topic": "drop_connect/DROP-1_C0FFEE/data/255/#", @@ -69,7 +69,7 @@ async def test_duplicate(hass: HomeAssistant, mqtt_mock: MqttMockHAClient) -> No ) await hass.async_block_till_done() assert result is not None - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY # Attempting configuration of the same object should abort result = await hass.config_entries.flow.async_init( diff --git a/tests/components/dsmr/test_config_flow.py b/tests/components/dsmr/test_config_flow.py index 687c6b4a3bc..a8eea28d748 100644 --- a/tests/components/dsmr/test_config_flow.py +++ b/tests/components/dsmr/test_config_flow.py @@ -9,7 +9,7 @@ import pytest import serial import serial.tools.list_ports -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.dsmr import DOMAIN, config_flow from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -196,7 +196,7 @@ async def test_setup_serial( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None @@ -205,7 +205,7 @@ async def test_setup_serial( {"type": "Serial"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_serial" assert result["errors"] == {} @@ -216,7 +216,7 @@ async def test_setup_serial( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == port.device assert result["data"] == entry_data @@ -499,7 +499,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: patch("homeassistant.components.dsmr.async_setup_entry", return_value=True), patch("homeassistant.components.dsmr.async_unload_entry", return_value=True), ): - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() diff --git a/tests/components/dsmr_reader/test_config_flow.py b/tests/components/dsmr_reader/test_config_flow.py index cc605eaa49c..e31e4f154c0 100644 --- a/tests/components/dsmr_reader/test_config_flow.py +++ b/tests/components/dsmr_reader/test_config_flow.py @@ -12,7 +12,7 @@ async def test_user_step(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] is None @@ -20,12 +20,12 @@ async def test_user_step(hass: HomeAssistant) -> None: result["flow_id"], user_input={} ) - assert config_result["type"] == FlowResultType.CREATE_ENTRY + assert config_result["type"] is FlowResultType.CREATE_ENTRY assert config_result["title"] == "DSMR Reader" duplicate_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert duplicate_result["type"] == FlowResultType.ABORT + assert duplicate_result["type"] is FlowResultType.ABORT assert duplicate_result["reason"] == "single_instance_allowed" diff --git a/tests/components/dunehd/test_config_flow.py b/tests/components/dunehd/test_config_flow.py index bf3137e0204..a35c1eec4cc 100644 --- a/tests/components/dunehd/test_config_flow.py +++ b/tests/components/dunehd/test_config_flow.py @@ -2,11 +2,11 @@ from unittest.mock import patch -from homeassistant import data_entry_flow from homeassistant.components.dunehd.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -77,7 +77,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=CONFIG_HOSTNAME ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "dunehd-host" assert result["data"] == {CONF_HOST: "dunehd-host"} @@ -94,6 +94,6 @@ async def test_create_entry_with_ipv6_address(hass: HomeAssistant) -> None: data={CONF_HOST: "2001:db8::1428:57ab"}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "2001:db8::1428:57ab" assert result["data"] == {CONF_HOST: "2001:db8::1428:57ab"} diff --git a/tests/components/duotecno/test_config_flow.py b/tests/components/duotecno/test_config_flow.py index b62b6e90801..77946babd8c 100644 --- a/tests/components/duotecno/test_config_flow.py +++ b/tests/components/duotecno/test_config_flow.py @@ -20,7 +20,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -37,7 +37,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "1.1.1.1" assert result2["data"] == { "host": "1.1.1.1", @@ -71,7 +71,7 @@ async def test_invalid(hass: HomeAssistant, test_side_effect, test_error): }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": test_error} with patch("duotecno.controller.PyDuotecno.connect"): @@ -83,7 +83,7 @@ async def test_invalid(hass: HomeAssistant, test_side_effect, test_error): "password": "test-password2", }, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "1.1.1.1" assert result2["data"] == { "host": "1.1.1.1", @@ -105,5 +105,5 @@ async def test_already_setup(hass: HomeAssistant, mock_setup_entry: AsyncMock) - DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/dwd_weather_warnings/test_config_flow.py b/tests/components/dwd_weather_warnings/test_config_flow.py index 625532a4f04..3558ff5ed93 100644 --- a/tests/components/dwd_weather_warnings/test_config_flow.py +++ b/tests/components/dwd_weather_warnings/test_config_flow.py @@ -38,7 +38,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.dwd_weather_warnings.config_flow.DwdWeatherWarningsAPI", @@ -50,7 +50,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: # Test for invalid region identifier. await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_identifier"} with patch( @@ -63,7 +63,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: # Test for successfully created entry. await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "807111000" assert result["data"] == { CONF_REGION_IDENTIFIER: "807111000", @@ -85,7 +85,7 @@ async def test_config_flow_already_configured(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.dwd_weather_warnings.config_flow.DwdWeatherWarningsAPI", @@ -96,5 +96,5 @@ async def test_config_flow_already_configured(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/easyenergy/test_config_flow.py b/tests/components/easyenergy/test_config_flow.py index 4e76d48b663..da7048793b3 100644 --- a/tests/components/easyenergy/test_config_flow.py +++ b/tests/components/easyenergy/test_config_flow.py @@ -17,7 +17,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" assert "flow_id" in result @@ -26,7 +26,7 @@ async def test_full_user_flow( user_input={}, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "easyEnergy" assert result2.get("data") == {} diff --git a/tests/components/ecobee/test_config_flow.py b/tests/components/ecobee/test_config_flow.py index 91d9f848ffd..20d3dabb1ea 100644 --- a/tests/components/ecobee/test_config_flow.py +++ b/tests/components/ecobee/test_config_flow.py @@ -5,7 +5,6 @@ from unittest.mock import patch from pyecobee import ECOBEE_API_KEY, ECOBEE_REFRESH_TOKEN import pytest -from homeassistant import data_entry_flow from homeassistant.components.ecobee import config_flow from homeassistant.components.ecobee.const import ( CONF_REFRESH_TOKEN, @@ -14,6 +13,7 @@ from homeassistant.components.ecobee.const import ( ) from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -27,7 +27,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant) -> None: result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -38,7 +38,7 @@ async def test_user_step_without_user_input(hass: HomeAssistant) -> None: flow.hass.data[DATA_ECOBEE_CONFIG] = {} result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -55,7 +55,7 @@ async def test_pin_request_succeeds(hass: HomeAssistant) -> None: result = await flow.async_step_user(user_input={CONF_API_KEY: "api-key"}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "authorize" assert result["description_placeholders"] == {"pin": "test-pin"} @@ -72,7 +72,7 @@ async def test_pin_request_fails(hass: HomeAssistant) -> None: result = await flow.async_step_user(user_input={CONF_API_KEY: "api-key"}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "pin_request_failed" @@ -93,7 +93,7 @@ async def test_token_request_succeeds(hass: HomeAssistant) -> None: result = await flow.async_step_authorize(user_input={}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DOMAIN assert result["data"] == { CONF_API_KEY: "test-api-key", @@ -116,7 +116,7 @@ async def test_token_request_fails(hass: HomeAssistant) -> None: result = await flow.async_step_authorize(user_input={}) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "authorize" assert result["errors"]["base"] == "token_request_failed" assert result["description_placeholders"] == {"pin": "test-pin"} @@ -131,7 +131,7 @@ async def test_import_flow_triggered_but_no_ecobee_conf(hass: HomeAssistant) -> result = await flow.async_step_import(import_data=None) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -158,7 +158,7 @@ async def test_import_flow_triggered_with_ecobee_conf_and_valid_data_and_valid_t result = await flow.async_step_import(import_data=None) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DOMAIN assert result["data"] == { CONF_API_KEY: "test-api-key", diff --git a/tests/components/ecoforest/test_config_flow.py b/tests/components/ecoforest/test_config_flow.py index 95c63a2515d..ae18960c7f9 100644 --- a/tests/components/ecoforest/test_config_flow.py +++ b/tests/components/ecoforest/test_config_flow.py @@ -21,7 +21,7 @@ async def test_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -34,7 +34,7 @@ async def test_form( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert "result" in result assert result["result"].unique_id == "1234" assert result["title"] == "Ecoforest 1234" @@ -53,7 +53,7 @@ async def test_form_device_already_configured( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -66,7 +66,7 @@ async def test_form_device_already_configured( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -100,7 +100,7 @@ async def test_flow_fails( config, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": message} with patch( @@ -113,4 +113,4 @@ async def test_flow_fails( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY diff --git a/tests/components/econet/test_config_flow.py b/tests/components/econet/test_config_flow.py index 7647b77e0a6..2ef10c1bd41 100644 --- a/tests/components/econet/test_config_flow.py +++ b/tests/components/econet/test_config_flow.py @@ -20,7 +20,7 @@ async def test_bad_credentials(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -38,7 +38,7 @@ async def test_bad_credentials(hass: HomeAssistant) -> None: }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == { "base": "invalid_auth", @@ -51,7 +51,7 @@ async def test_generic_error_from_library(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -69,7 +69,7 @@ async def test_generic_error_from_library(hass: HomeAssistant) -> None: }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == { "base": "cannot_connect", @@ -82,7 +82,7 @@ async def test_auth_worked(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -100,7 +100,7 @@ async def test_auth_worked(hass: HomeAssistant) -> None: }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_EMAIL: "admin@localhost.com", CONF_PASSWORD: "password0", @@ -120,7 +120,7 @@ async def test_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with ( @@ -138,5 +138,5 @@ async def test_already_configured(hass: HomeAssistant) -> None: }, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/ecovacs/test_config_flow.py b/tests/components/ecovacs/test_config_flow.py index 6bd30c3a201..0a161f88baa 100644 --- a/tests/components/ecovacs/test_config_flow.py +++ b/tests/components/ecovacs/test_config_flow.py @@ -49,7 +49,7 @@ async def _test_user_flow( context={"source": SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert not result["errors"] @@ -71,7 +71,7 @@ async def _test_user_flow_show_advanced_options( context={"source": SOURCE_USER, "show_advanced_options": True}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -80,7 +80,7 @@ async def _test_user_flow_show_advanced_options( user_input=user_input_user or {}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert not result["errors"] @@ -131,7 +131,7 @@ async def test_user_flow( hass, **test_fn_args, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == entry_data[CONF_USERNAME] assert result["data"] == entry_data mock_setup_entry.assert_called() @@ -214,7 +214,7 @@ async def test_user_flow_raise_error( hass, **test_fn_args, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": reason_rest} mock_authenticator_authenticate.assert_called() @@ -229,7 +229,7 @@ async def test_user_flow_raise_error( result["flow_id"], user_input=user_input_auth, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == errors_mqtt(user_input_auth) mock_authenticator_authenticate.assert_called() @@ -243,7 +243,7 @@ async def test_user_flow_raise_error( result["flow_id"], user_input=user_input_auth, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == entry_data[CONF_USERNAME] assert result["data"] == entry_data mock_setup_entry.assert_called() @@ -269,7 +269,7 @@ async def test_user_flow_self_hosted_error( user_input_user=_USER_STEP_SELF_HOSTED, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == { CONF_OVERRIDE_REST_URL: "invalid_url_schema_override_rest_url", @@ -297,7 +297,7 @@ async def test_user_flow_self_hosted_error( assert ssl_context.verify_mode == ssl.CERT_NONE assert ssl_context.check_hostname is False - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == data[CONF_USERNAME] assert result["data"] == data mock_setup_entry.assert_called() @@ -320,7 +320,7 @@ async def test_import_flow( ) mock_authenticator_authenticate.assert_called() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == VALID_ENTRY_DATA_CLOUD[CONF_USERNAME] assert result["data"] == VALID_ENTRY_DATA_CLOUD assert (HOMEASSISTANT_DOMAIN, f"deprecated_yaml_{DOMAIN}") in issue_registry.issues @@ -340,7 +340,7 @@ async def test_import_flow_already_configured( context={"source": SOURCE_IMPORT}, data=IMPORT_DATA.copy(), ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert (HOMEASSISTANT_DOMAIN, f"deprecated_yaml_{DOMAIN}") in issue_registry.issues @@ -374,7 +374,7 @@ async def test_import_flow_error( }, data=IMPORT_DATA.copy(), ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == reason assert ( DOMAIN, @@ -410,7 +410,7 @@ async def test_import_flow_invalid_data( }, data=user_input, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == reason assert ( DOMAIN, diff --git a/tests/components/ecowitt/test_config_flow.py b/tests/components/ecowitt/test_config_flow.py index 24a45e2d31b..a2054c1282d 100644 --- a/tests/components/ecowitt/test_config_flow.py +++ b/tests/components/ecowitt/test_config_flow.py @@ -16,7 +16,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -29,7 +29,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Ecowitt" assert result2["data"] == { "webhook_id": result2["description_placeholders"]["path"].split("/")[-1], diff --git a/tests/components/edl21/test_config_flow.py b/tests/components/edl21/test_config_flow.py index 030ff7ae63e..97ad1464d77 100644 --- a/tests/components/edl21/test_config_flow.py +++ b/tests/components/edl21/test_config_flow.py @@ -22,7 +22,7 @@ async def test_show_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -30,7 +30,7 @@ async def test_show_form(hass: HomeAssistant) -> None: VALID_CONFIG, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_TITLE assert result["data"][CONF_SERIAL_PORT] == VALID_CONFIG[CONF_SERIAL_PORT] @@ -49,5 +49,5 @@ async def test_integration_already_exists(hass: HomeAssistant) -> None: data=VALID_CONFIG, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/efergy/test_config_flow.py b/tests/components/efergy/test_config_flow.py index 3a7529da395..9a66c42bc9a 100644 --- a/tests/components/efergy/test_config_flow.py +++ b/tests/components/efergy/test_config_flow.py @@ -24,14 +24,14 @@ async def test_flow_user(hass: HomeAssistant) -> None: DOMAIN, context={CONF_SOURCE: SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONF_DATA, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == CONF_DATA assert result["result"].unique_id == HID @@ -44,7 +44,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "cannot_connect" @@ -56,7 +56,7 @@ async def test_flow_user_invalid_auth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "invalid_auth" @@ -68,7 +68,7 @@ async def test_flow_user_unknown(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "unknown" @@ -87,7 +87,7 @@ async def test_flow_reauth(hass: HomeAssistant) -> None: data=CONF_DATA, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" new_conf = {CONF_API_KEY: "1234567890"} @@ -95,6 +95,6 @@ async def test_flow_reauth(hass: HomeAssistant) -> None: result["flow_id"], user_input=new_conf, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data == new_conf diff --git a/tests/components/electrasmart/test_config_flow.py b/tests/components/electrasmart/test_config_flow.py index 957c140862f..cf0d1b5ab15 100644 --- a/tests/components/electrasmart/test_config_flow.py +++ b/tests/components/electrasmart/test_config_flow.py @@ -40,7 +40,7 @@ async def test_form(hass: HomeAssistant): data={CONF_PHONE_NUMBER: "0521234567"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == CONF_OTP @@ -73,7 +73,7 @@ async def test_one_time_password(hass: HomeAssistant): result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_OTP: "1234"} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_one_time_password_api_error(hass: HomeAssistant): @@ -99,7 +99,7 @@ async def test_one_time_password_api_error(hass: HomeAssistant): result["flow_id"], {CONF_OTP: "1234"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM async def test_cannot_connect(hass: HomeAssistant): @@ -115,7 +115,7 @@ async def test_cannot_connect(hass: HomeAssistant): context={"source": config_entries.SOURCE_USER}, data={CONF_PHONE_NUMBER: "0521234567"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -138,7 +138,7 @@ async def test_invalid_phone_number(hass: HomeAssistant): data={CONF_PHONE_NUMBER: "0521234567"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"phone_number": "invalid_phone_number"} @@ -171,6 +171,6 @@ async def test_invalid_auth(hass: HomeAssistant): result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_OTP: "1234"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == CONF_OTP assert result["errors"] == {CONF_OTP: "invalid_auth"} diff --git a/tests/components/electric_kiwi/test_config_flow.py b/tests/components/electric_kiwi/test_config_flow.py index d91936eeebf..d74abab7692 100644 --- a/tests/components/electric_kiwi/test_config_flow.py +++ b/tests/components/electric_kiwi/test_config_flow.py @@ -49,7 +49,7 @@ async def test_config_flow_no_credentials(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "missing_credentials" diff --git a/tests/components/elgato/test_config_flow.py b/tests/components/elgato/test_config_flow.py index def12307107..6da99241b64 100644 --- a/tests/components/elgato/test_config_flow.py +++ b/tests/components/elgato/test_config_flow.py @@ -29,14 +29,14 @@ async def test_full_user_flow_implementation( context={"source": SOURCE_USER}, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "127.0.0.1", CONF_PORT: 9123} ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2 == snapshot assert len(mock_setup_entry.mock_calls) == 1 @@ -66,7 +66,7 @@ async def test_full_zeroconf_flow_implementation( assert result.get("description_placeholders") == {"serial_number": "CN11A1A00001"} assert result.get("step_id") == "zeroconf_confirm" - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM progress = hass.config_entries.flow.async_progress() assert len(progress) == 1 @@ -78,7 +78,7 @@ async def test_full_zeroconf_flow_implementation( result["flow_id"], user_input={} ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2 == snapshot assert len(mock_setup_entry.mock_calls) == 1 @@ -97,7 +97,7 @@ async def test_connection_error( data={CONF_HOST: "127.0.0.1", CONF_PORT: 9123}, ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {"base": "cannot_connect"} assert result.get("step_id") == "user" @@ -123,7 +123,7 @@ async def test_zeroconf_connection_error( ) assert result.get("reason") == "cannot_connect" - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT @pytest.mark.usefixtures("mock_elgato") @@ -138,7 +138,7 @@ async def test_user_device_exists_abort( data={CONF_HOST: "127.0.0.1", CONF_PORT: 9123}, ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -162,7 +162,7 @@ async def test_zeroconf_device_exists_abort( ), ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" entries = hass.config_entries.async_entries(DOMAIN) @@ -183,7 +183,7 @@ async def test_zeroconf_device_exists_abort( ), ) - assert result.get("type") == FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" entries = hass.config_entries.async_entries(DOMAIN) @@ -212,7 +212,7 @@ async def test_zeroconf_during_onboarding( ), ) - assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result == snapshot assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/elkm1/test_config_flow.py b/tests/components/elkm1/test_config_flow.py index 592efc16b5e..c361063d7ea 100644 --- a/tests/components/elkm1/test_config_flow.py +++ b/tests/components/elkm1/test_config_flow.py @@ -53,7 +53,7 @@ async def test_discovery_ignored_entry(hass: HomeAssistant) -> None: data=ELK_DISCOVERY_INFO, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -264,7 +264,7 @@ async def test_form_user_with_insecure_elk_times_out(hass: HomeAssistant) -> Non ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -304,7 +304,7 @@ async def test_form_user_with_secure_elk_no_discovery_ip_already_configured( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "address_already_configured" @@ -1051,7 +1051,7 @@ async def test_form_import_existing(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "address_already_configured" @@ -1079,7 +1079,7 @@ async def test_discovered_by_dhcp_or_discovery_mac_address_mismatch_host_already ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.unique_id == "cc:cc:cc:cc:cc:cc" @@ -1108,7 +1108,7 @@ async def test_discovered_by_dhcp_or_discovery_adds_missing_unique_id( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.unique_id == MOCK_MAC @@ -1124,7 +1124,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: data=ELK_DISCOVERY_INFO, ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with _patch_discovery(), _patch_elk(): @@ -1134,7 +1134,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: data=DHCP_DISCOVERY, ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_in_progress" with _patch_discovery(), _patch_elk(): @@ -1148,7 +1148,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: ), ) await hass.async_block_till_done() - assert result3["type"] == FlowResultType.ABORT + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "already_in_progress" @@ -1163,7 +1163,7 @@ async def test_discovered_by_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovered_connection" assert result["errors"] == {} @@ -1213,7 +1213,7 @@ async def test_discovered_by_discovery_non_standard_port(hass: HomeAssistant) -> ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovered_connection" assert result["errors"] == {} @@ -1271,7 +1271,7 @@ async def test_discovered_by_discovery_url_already_configured( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -1284,7 +1284,7 @@ async def test_discovered_by_dhcp_udp_responds(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovered_connection" assert result["errors"] == {} @@ -1334,7 +1334,7 @@ async def test_discovered_by_dhcp_udp_responds_with_nonsecure_port( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovered_connection" assert result["errors"] == {} @@ -1389,7 +1389,7 @@ async def test_discovered_by_dhcp_udp_responds_existing_config_entry( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovered_connection" assert result["errors"] == {} @@ -1434,7 +1434,7 @@ async def test_discovered_by_dhcp_no_udp_response(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" diff --git a/tests/components/elmax/test_config_flow.py b/tests/components/elmax/test_config_flow.py index 6782b3f9b7a..c00de2003c2 100644 --- a/tests/components/elmax/test_config_flow.py +++ b/tests/components/elmax/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch from elmax_api.exceptions import ElmaxBadLoginError, ElmaxBadPinError, ElmaxNetworkError -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.elmax.const import ( CONF_ELMAX_MODE, @@ -23,6 +23,7 @@ from homeassistant.components.elmax.const import ( ) from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import ( MOCK_DIRECT_CERT, @@ -89,7 +90,7 @@ async def test_show_menu(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "choose_mode" @@ -116,7 +117,7 @@ async def test_direct_setup(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_direct_show_form(hass: HomeAssistant) -> None: @@ -134,7 +135,7 @@ async def test_direct_show_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( set_mode_result["flow_id"], {"next_step_id": CONF_ELMAX_MODE_DIRECT} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == CONF_ELMAX_MODE_DIRECT assert result["errors"] is None @@ -168,7 +169,7 @@ async def test_cloud_setup(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_zeroconf_form_setup_api_not_supported(hass): @@ -178,7 +179,7 @@ async def test_zeroconf_form_setup_api_not_supported(hass): context={"source": config_entries.SOURCE_ZEROCONF}, data=MOCK_ZEROCONF_DISCOVERY_INFO_NOT_SUPPORTED, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -189,7 +190,7 @@ async def test_zeroconf_discovery(hass): context={"source": config_entries.SOURCE_ZEROCONF}, data=MOCK_ZEROCONF_DISCOVERY_INFO, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zeroconf_setup" assert result["errors"] is None @@ -206,7 +207,7 @@ async def test_zeroconf_setup_show_form(hass): result["flow_id"], ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zeroconf_setup" @@ -227,7 +228,7 @@ async def test_zeroconf_setup(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_zeroconf_already_configured(hass): @@ -252,7 +253,7 @@ async def test_zeroconf_already_configured(hass): context={"source": config_entries.SOURCE_ZEROCONF}, data=MOCK_ZEROCONF_DISCOVERY_INFO, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -283,7 +284,7 @@ async def test_zeroconf_panel_changed_ip(hass): ) # Expect we abort the configuration as "already configured" - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" # Expect the panel ip has been updated. @@ -330,7 +331,7 @@ async def test_one_config_allowed_cloud(hass: HomeAssistant) -> None: CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, }, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -355,7 +356,7 @@ async def test_cloud_invalid_credentials(hass: HomeAssistant) -> None: }, ) assert login_result["step_id"] == CONF_ELMAX_MODE_CLOUD - assert login_result["type"] == data_entry_flow.FlowResultType.FORM + assert login_result["type"] is FlowResultType.FORM assert login_result["errors"] == {"base": "invalid_auth"} @@ -380,7 +381,7 @@ async def test_cloud_connection_error(hass: HomeAssistant) -> None: }, ) assert login_result["step_id"] == CONF_ELMAX_MODE_CLOUD - assert login_result["type"] == data_entry_flow.FlowResultType.FORM + assert login_result["type"] is FlowResultType.FORM assert login_result["errors"] == {"base": "network_error"} @@ -407,7 +408,7 @@ async def test_direct_connection_error(hass: HomeAssistant) -> None: }, ) assert result["step_id"] == CONF_ELMAX_MODE_DIRECT - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "network_error"} @@ -434,7 +435,7 @@ async def test_direct_wrong_panel_code(hass: HomeAssistant) -> None: }, ) assert result["step_id"] == CONF_ELMAX_MODE_DIRECT - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -466,7 +467,7 @@ async def test_unhandled_error(hass: HomeAssistant) -> None: }, ) assert result["step_id"] == "panels" - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "unknown"} @@ -499,7 +500,7 @@ async def test_invalid_pin(hass: HomeAssistant) -> None: }, ) assert result["step_id"] == "panels" - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_pin"} @@ -525,7 +526,7 @@ async def test_no_online_panel(hass: HomeAssistant) -> None: }, ) assert login_result["step_id"] == CONF_ELMAX_MODE_CLOUD - assert login_result["type"] == data_entry_flow.FlowResultType.FORM + assert login_result["type"] is FlowResultType.FORM assert login_result["errors"] == {"base": "no_panel_online"} @@ -557,7 +558,7 @@ async def test_show_reauth(hass: HomeAssistant) -> None: CONF_ELMAX_PASSWORD: MOCK_PASSWORD, }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" @@ -602,7 +603,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: CONF_ELMAX_PASSWORD: MOCK_PASSWORD, }, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT await hass.async_block_till_done() assert result["reason"] == "reauth_successful" @@ -650,7 +651,7 @@ async def test_reauth_panel_disappeared(hass: HomeAssistant) -> None: }, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "reauth_panel_disappeared"} @@ -696,7 +697,7 @@ async def test_reauth_invalid_pin(hass: HomeAssistant) -> None: }, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_pin"} @@ -742,5 +743,5 @@ async def test_reauth_bad_login(hass: HomeAssistant) -> None: }, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} diff --git a/tests/components/elvia/test_config_flow.py b/tests/components/elvia/test_config_flow.py index 2fda29217c4..34edac499ac 100644 --- a/tests/components/elvia/test_config_flow.py +++ b/tests/components/elvia/test_config_flow.py @@ -27,7 +27,7 @@ async def test_single_metering_point( DOMAIN, context={"source": config_entries.SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -42,7 +42,7 @@ async def test_single_metering_point( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "1234" assert result["data"] == { CONF_API_TOKEN: TEST_API_TOKEN, @@ -60,7 +60,7 @@ async def test_multiple_metering_points( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -80,7 +80,7 @@ async def test_multiple_metering_points( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select_meter" result = await hass.config_entries.flow.async_configure( @@ -91,7 +91,7 @@ async def test_multiple_metering_points( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "5678" assert result["data"] == { CONF_API_TOKEN: TEST_API_TOKEN, @@ -109,7 +109,7 @@ async def test_no_metering_points( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -124,7 +124,7 @@ async def test_no_metering_points( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_metering_points" assert len(mock_setup_entry.mock_calls) == 0 @@ -139,7 +139,7 @@ async def test_bad_data( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -154,7 +154,7 @@ async def test_bad_data( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_metering_points" assert len(mock_setup_entry.mock_calls) == 0 @@ -175,7 +175,7 @@ async def test_abort_when_metering_point_id_exist( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -190,7 +190,7 @@ async def test_abort_when_metering_point_id_exist( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "metering_point_id_already_configured" assert len(mock_setup_entry.mock_calls) == 0 @@ -227,7 +227,7 @@ async def test_form_exceptions( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": base_error} # Simulate that the user gives up and closes the window... diff --git a/tests/components/energenie_power_sockets/test_config_flow.py b/tests/components/energenie_power_sockets/test_config_flow.py index ef433d0ef09..aee26438629 100644 --- a/tests/components/energenie_power_sockets/test_config_flow.py +++ b/tests/components/energenie_power_sockets/test_config_flow.py @@ -27,14 +27,14 @@ async def test_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert not result1["errors"] # check with valid data result2 = await hass.config_entries.flow.async_configure( result1["flow_id"], user_input=demo_config_data ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY async def test_user_flow_already_exists( @@ -54,7 +54,7 @@ async def test_user_flow_already_exists( await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -75,7 +75,7 @@ async def test_user_flow_no_new_device( await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_device" @@ -93,7 +93,7 @@ async def test_user_flow_no_device_found( DOMAIN, context={"source": SOURCE_USER} ) - assert result1["type"] == FlowResultType.ABORT + assert result1["type"] is FlowResultType.ABORT assert result1["reason"] == "no_device" @@ -111,14 +111,14 @@ async def test_user_flow_device_not_found( DOMAIN, context={"source": SOURCE_USER} ) - assert result1["type"] == FlowResultType.FORM + assert result1["type"] is FlowResultType.FORM assert not result1["errors"] # check with valid data result2 = await hass.config_entries.flow.async_configure( result1["flow_id"], user_input=demo_config_data ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "device_not_found" @@ -136,5 +136,5 @@ async def test_user_flow_no_usb_access( DOMAIN, context={"source": SOURCE_USER} ) - assert result1["type"] == FlowResultType.ABORT + assert result1["type"] is FlowResultType.ABORT assert result1["reason"] == "usb_error" diff --git a/tests/components/energyzero/test_config_flow.py b/tests/components/energyzero/test_config_flow.py index d16ea5cc8a8..a9fe8534fd5 100644 --- a/tests/components/energyzero/test_config_flow.py +++ b/tests/components/energyzero/test_config_flow.py @@ -20,7 +20,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" assert "flow_id" in result @@ -29,7 +29,7 @@ async def test_full_user_flow( user_input={}, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2 == snapshot assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/enocean/test_config_flow.py b/tests/components/enocean/test_config_flow.py index 45a4e6e387f..96c0843906f 100644 --- a/tests/components/enocean/test_config_flow.py +++ b/tests/components/enocean/test_config_flow.py @@ -2,11 +2,12 @@ from unittest.mock import Mock, patch -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.enocean.config_flow import EnOceanFlowHandler from homeassistant.components.enocean.const import DOMAIN from homeassistant.const import CONF_DEVICE from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -26,7 +27,7 @@ async def test_user_flow_cannot_create_multiple_instances(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -39,7 +40,7 @@ async def test_user_flow_with_detected_dongle(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "detect" devices = result["data_schema"].schema.get("device").container assert FAKE_DONGLE_PATH in devices @@ -53,7 +54,7 @@ async def test_user_flow_with_no_detected_dongle(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" @@ -66,7 +67,7 @@ async def test_detection_flow_with_valid_path(hass: HomeAssistant) -> None: DOMAIN, context={"source": "detect"}, data={CONF_DEVICE: USER_PROVIDED_PATH} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_DEVICE] == USER_PROVIDED_PATH @@ -85,7 +86,7 @@ async def test_detection_flow_with_custom_path(hass: HomeAssistant) -> None: data={CONF_DEVICE: USER_PROVIDED_PATH}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" @@ -104,7 +105,7 @@ async def test_detection_flow_with_invalid_path(hass: HomeAssistant) -> None: data={CONF_DEVICE: USER_PROVIDED_PATH}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "detect" assert CONF_DEVICE in result["errors"] @@ -118,7 +119,7 @@ async def test_manual_flow_with_valid_path(hass: HomeAssistant) -> None: DOMAIN, context={"source": "manual"}, data={CONF_DEVICE: USER_PROVIDED_PATH} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_DEVICE] == USER_PROVIDED_PATH @@ -134,7 +135,7 @@ async def test_manual_flow_with_invalid_path(hass: HomeAssistant) -> None: DOMAIN, context={"source": "manual"}, data={CONF_DEVICE: USER_PROVIDED_PATH} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" assert CONF_DEVICE in result["errors"] @@ -150,7 +151,7 @@ async def test_import_flow_with_valid_path(hass: HomeAssistant) -> None: data=DATA_TO_IMPORT, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_DEVICE] == DATA_TO_IMPORT[CONF_DEVICE] @@ -168,5 +169,5 @@ async def test_import_flow_with_invalid_path(hass: HomeAssistant) -> None: data=DATA_TO_IMPORT, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_dongle_path" diff --git a/tests/components/environment_canada/test_config_flow.py b/tests/components/environment_canada/test_config_flow.py index e9513644947..aa1297571b5 100644 --- a/tests/components/environment_canada/test_config_flow.py +++ b/tests/components/environment_canada/test_config_flow.py @@ -6,10 +6,11 @@ import xml.etree.ElementTree as et import aiohttp import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.environment_canada.const import CONF_STATION, DOMAIN from homeassistant.const import CONF_LANGUAGE, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -65,7 +66,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: flow["flow_id"], FAKE_CONFIG ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == FAKE_CONFIG assert result["title"] == FAKE_TITLE @@ -93,7 +94,7 @@ async def test_create_same_entry_twice(hass: HomeAssistant) -> None: flow["flow_id"], FAKE_CONFIG ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -142,6 +143,6 @@ async def test_lat_lon_not_specified(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=fake_config ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == FAKE_CONFIG assert result["title"] == FAKE_TITLE diff --git a/tests/components/epion/test_config_flow.py b/tests/components/epion/test_config_flow.py index 8d246ac4dd4..c5d556996b6 100644 --- a/tests/components/epion/test_config_flow.py +++ b/tests/components/epion/test_config_flow.py @@ -33,7 +33,7 @@ async def test_user_flow(hass: HomeAssistant, mock_epion: MagicMock) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Epion integration" assert result["data"] == { CONF_API_KEY: API_KEY, @@ -63,7 +63,7 @@ async def test_form_exceptions( {CONF_API_KEY: API_KEY}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": error} mock_epion.return_value.get_current.side_effect = None @@ -78,7 +78,7 @@ async def test_form_exceptions( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Epion integration" assert result["data"] == { CONF_API_KEY: API_KEY, @@ -107,5 +107,5 @@ async def test_duplicate_entry(hass: HomeAssistant, mock_epion: MagicMock) -> No ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/escea/test_config_flow.py b/tests/components/escea/test_config_flow.py index 7d467fc50a0..3cb914bfaf3 100644 --- a/tests/components/escea/test_config_flow.py +++ b/tests/components/escea/test_config_flow.py @@ -60,12 +60,12 @@ async def test_not_found( ) # Confirmation form - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" assert discovery_service.return_value.close.call_count == 1 @@ -95,12 +95,12 @@ async def test_found( ) # Confirmation form - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert mock_setup.call_count == 1 @@ -117,6 +117,6 @@ async def test_single_instance_allowed(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" assert discovery_service.call_count == 0 diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index e06b96356ae..439092d9fb1 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -16,7 +16,7 @@ from aioesphomeapi import ( import aiohttp import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import dhcp, zeroconf from homeassistant.components.esphome import DomainData, dashboard from homeassistant.components.esphome.const import ( @@ -56,7 +56,7 @@ async def test_user_connection_works( data=None, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_init( @@ -65,7 +65,7 @@ async def test_user_connection_works( data={CONF_HOST: "127.0.0.1", CONF_PORT: 80}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_HOST: "127.0.0.1", CONF_PORT: 80, @@ -104,7 +104,7 @@ async def test_user_connection_updates_host( data=None, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_init( @@ -112,7 +112,7 @@ async def test_user_connection_updates_host( context={"source": config_entries.SOURCE_USER}, data={CONF_HOST: "127.0.0.1", CONF_PORT: 80}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == "127.0.0.1" @@ -136,14 +136,14 @@ async def test_user_sets_unique_id( "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) - assert discovery_result["type"] == FlowResultType.FORM + assert discovery_result["type"] is FlowResultType.FORM assert discovery_result["step_id"] == "discovery_confirm" discovery_result = await hass.config_entries.flow.async_configure( discovery_result["flow_id"], {}, ) - assert discovery_result["type"] == FlowResultType.CREATE_ENTRY + assert discovery_result["type"] is FlowResultType.CREATE_ENTRY assert discovery_result["data"] == { CONF_HOST: "192.168.43.183", CONF_PORT: 6053, @@ -158,14 +158,14 @@ async def test_user_sets_unique_id( data=None, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -185,7 +185,7 @@ async def test_user_resolve_error( data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "resolve_error"} @@ -213,7 +213,7 @@ async def test_user_causes_zeroconf_to_abort( "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) - assert discovery_result["type"] == FlowResultType.FORM + assert discovery_result["type"] is FlowResultType.FORM assert discovery_result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_init( @@ -222,14 +222,14 @@ async def test_user_causes_zeroconf_to_abort( data=None, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_HOST: "127.0.0.1", CONF_PORT: 6053, @@ -253,7 +253,7 @@ async def test_user_connection_error( data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "connection_error"} @@ -274,14 +274,14 @@ async def test_user_with_password( data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "authenticate" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "password1"} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_HOST: "127.0.0.1", CONF_PORT: 6053, @@ -304,7 +304,7 @@ async def test_user_invalid_password( data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "authenticate" mock_client.connect.side_effect = InvalidAuthAPIError @@ -313,7 +313,7 @@ async def test_user_invalid_password( result["flow_id"], user_input={CONF_PASSWORD: "invalid"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "authenticate" assert result["errors"] == {"base": "invalid_auth"} @@ -347,14 +347,14 @@ async def test_user_dashboard_has_wrong_key( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "encryption_key" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_HOST: "127.0.0.1", CONF_PORT: 6053, @@ -402,7 +402,7 @@ async def test_user_discovers_name_and_gets_key_from_dashboard( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_HOST: "127.0.0.1", CONF_PORT: 6053, @@ -455,14 +455,14 @@ async def test_user_discovers_name_and_gets_key_from_dashboard_fails( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "encryption_key" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_HOST: "127.0.0.1", CONF_PORT: 6053, @@ -510,14 +510,14 @@ async def test_user_discovers_name_and_dashboard_is_unavailable( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "encryption_key" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_HOST: "127.0.0.1", CONF_PORT: 6053, @@ -540,7 +540,7 @@ async def test_login_connection_error( data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "authenticate" mock_client.connect.side_effect = APIConnectionError @@ -549,7 +549,7 @@ async def test_login_connection_error( result["flow_id"], user_input={CONF_PASSWORD: "valid"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "authenticate" assert result["errors"] == {"base": "connection_error"} @@ -577,7 +577,7 @@ async def test_discovery_initiation( flow["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "test" assert result["data"][CONF_HOST] == "192.168.43.183" assert result["data"][CONF_PORT] == 6053 @@ -602,7 +602,7 @@ async def test_discovery_no_mac( flow = await hass.config_entries.flow.async_init( "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) - assert flow["type"] == FlowResultType.ABORT + assert flow["type"] is FlowResultType.ABORT assert flow["reason"] == "mdns_missing_mac" @@ -631,7 +631,7 @@ async def test_discovery_already_configured( "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -652,13 +652,13 @@ async def test_discovery_duplicate_data( result = await hass.config_entries.flow.async_init( "esphome", data=service_info, context={"source": config_entries.SOURCE_ZEROCONF} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_init( "esphome", data=service_info, context={"source": config_entries.SOURCE_ZEROCONF} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -687,7 +687,7 @@ async def test_discovery_updates_unique_id( "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.unique_id == "11:22:33:44:55:aa" @@ -705,7 +705,7 @@ async def test_user_requires_psk( data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "encryption_key" assert result["errors"] == {} @@ -727,7 +727,7 @@ async def test_encryption_key_valid_psk( data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "encryption_key" mock_client.device_info = AsyncMock( @@ -737,7 +737,7 @@ async def test_encryption_key_valid_psk( result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_HOST: "127.0.0.1", CONF_PORT: 6053, @@ -761,7 +761,7 @@ async def test_encryption_key_invalid_psk( data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "encryption_key" mock_client.device_info.side_effect = InvalidEncryptionKeyAPIError @@ -769,7 +769,7 @@ async def test_encryption_key_invalid_psk( result["flow_id"], user_input={CONF_NOISE_PSK: INVALID_NOISE_PSK} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "encryption_key" assert result["errors"] == {"base": "invalid_psk"} assert mock_client.noise_psk == INVALID_NOISE_PSK @@ -793,7 +793,7 @@ async def test_reauth_initiation( "unique_id": entry.unique_id, }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" @@ -821,7 +821,7 @@ async def test_reauth_confirm_valid( result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK @@ -870,7 +870,7 @@ async def test_reauth_fixed_via_dashboard( }, ) - assert result["type"] == FlowResultType.ABORT, result + assert result["type"] is FlowResultType.ABORT, result assert result["reason"] == "reauth_successful" assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK @@ -913,7 +913,7 @@ async def test_reauth_fixed_via_dashboard_add_encryption_remove_password( }, ) - assert result["type"] == FlowResultType.ABORT, result + assert result["type"] is FlowResultType.ABORT, result assert result["reason"] == "reauth_successful" assert mock_config_entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK assert mock_config_entry.data[CONF_PASSWORD] == "" @@ -940,7 +940,7 @@ async def test_reauth_fixed_via_remove_password( }, ) - assert result["type"] == FlowResultType.ABORT, result + assert result["type"] is FlowResultType.ABORT, result assert result["reason"] == "reauth_successful" assert mock_config_entry.data[CONF_PASSWORD] == "" @@ -976,7 +976,7 @@ async def test_reauth_fixed_via_dashboard_at_confirm( }, ) - assert result["type"] == FlowResultType.FORM, result + assert result["type"] is FlowResultType.FORM, result assert result["step_id"] == "reauth_confirm" mock_dashboard["configured"].append( @@ -995,7 +995,7 @@ async def test_reauth_fixed_via_dashboard_at_confirm( # We just fetch the form result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT, result + assert result["type"] is FlowResultType.ABORT, result assert result["reason"] == "reauth_successful" assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK @@ -1026,7 +1026,7 @@ async def test_reauth_confirm_invalid( result["flow_id"], user_input={CONF_NOISE_PSK: INVALID_NOISE_PSK} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] assert result["errors"]["base"] == "invalid_psk" @@ -1038,7 +1038,7 @@ async def test_reauth_confirm_invalid( result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK @@ -1068,7 +1068,7 @@ async def test_reauth_confirm_invalid_with_unique_id( result["flow_id"], user_input={CONF_NOISE_PSK: INVALID_NOISE_PSK} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] assert result["errors"]["base"] == "invalid_psk" @@ -1080,7 +1080,7 @@ async def test_reauth_confirm_invalid_with_unique_id( result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK @@ -1105,7 +1105,7 @@ async def test_discovery_dhcp_updates_host( "esphome", context={"source": config_entries.SOURCE_DHCP}, data=service_info ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == "192.168.43.184" @@ -1135,7 +1135,7 @@ async def test_discovery_dhcp_no_changes( "esphome", context={"source": config_entries.SOURCE_DHCP}, data=service_info ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == "192.168.43.183" @@ -1157,7 +1157,7 @@ async def test_discovery_hassio(hass: HomeAssistant, mock_dashboard) -> None: context={"source": config_entries.SOURCE_HASSIO}, ) assert result - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "service_received" dash = dashboard.async_get_dashboard(hass) @@ -1188,7 +1188,7 @@ async def test_zeroconf_encryption_key_via_dashboard( "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) - assert flow["type"] == FlowResultType.FORM + assert flow["type"] is FlowResultType.FORM assert flow["step_id"] == "discovery_confirm" mock_dashboard["configured"].append( @@ -1219,7 +1219,7 @@ async def test_zeroconf_encryption_key_via_dashboard( assert len(mock_get_encryption_key.mock_calls) == 1 - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "test8266" assert result["data"][CONF_HOST] == "192.168.43.183" assert result["data"][CONF_PORT] == 6053 @@ -1255,7 +1255,7 @@ async def test_zeroconf_encryption_key_via_dashboard_with_api_encryption_prop( "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) - assert flow["type"] == FlowResultType.FORM + assert flow["type"] is FlowResultType.FORM assert flow["step_id"] == "discovery_confirm" mock_dashboard["configured"].append( @@ -1285,7 +1285,7 @@ async def test_zeroconf_encryption_key_via_dashboard_with_api_encryption_prop( assert len(mock_get_encryption_key.mock_calls) == 1 - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "test8266" assert result["data"][CONF_HOST] == "192.168.43.183" assert result["data"][CONF_PORT] == 6053 @@ -1320,7 +1320,7 @@ async def test_zeroconf_no_encryption_key_via_dashboard( "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) - assert flow["type"] == FlowResultType.FORM + assert flow["type"] is FlowResultType.FORM assert flow["step_id"] == "discovery_confirm" await dashboard.async_get_dashboard(hass).async_refresh() @@ -1331,7 +1331,7 @@ async def test_zeroconf_no_encryption_key_via_dashboard( flow["flow_id"], user_input={} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "encryption_key" @@ -1352,7 +1352,7 @@ async def test_option_flow( result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({}) == { CONF_ALLOW_SERVICE_CALLS: DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS @@ -1369,7 +1369,7 @@ async def test_option_flow( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_ALLOW_SERVICE_CALLS: option_value} assert len(mock_reload.mock_calls) == int(option_value) @@ -1398,14 +1398,14 @@ async def test_user_discovers_name_no_dashboard( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "encryption_key" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_HOST: "127.0.0.1", CONF_PORT: 6053, diff --git a/tests/components/eufylife_ble/test_config_flow.py b/tests/components/eufylife_ble/test_config_flow.py index c3590077d93..cab70437925 100644 --- a/tests/components/eufylife_ble/test_config_flow.py +++ b/tests/components/eufylife_ble/test_config_flow.py @@ -19,7 +19,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=T9146_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( "homeassistant.components.eufylife_ble.async_setup_entry", return_value=True @@ -27,7 +27,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Smart Scale C1" assert result2["data"] == {"model": "eufy T9146"} assert result2["result"].unique_id == "11:22:33:44:55:66" @@ -40,7 +40,7 @@ async def test_async_step_bluetooth_not_eufylife(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_EUFYLIFE_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -50,7 +50,7 @@ async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -64,7 +64,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( "homeassistant.components.eufylife_ble.async_setup_entry", return_value=True @@ -73,7 +73,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: result["flow_id"], user_input={"address": "11:22:33:44:55:66"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Smart Scale C1" assert result2["data"] == {"model": "eufy T9146"} assert result2["result"].unique_id == "11:22:33:44:55:66" @@ -89,7 +89,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" entry = MockConfigEntry( @@ -105,7 +105,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - result["flow_id"], user_input={"address": "11:22:33:44:55:66"}, ) - assert result2["type"] == FlowResultType.ABORT + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -127,7 +127,7 @@ async def test_async_step_user_with_found_devices_already_setup( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -144,7 +144,7 @@ async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant) - context={"source": config_entries.SOURCE_BLUETOOTH}, data=T9146_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -155,7 +155,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=T9146_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" result = await hass.config_entries.flow.async_init( @@ -163,7 +163,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_BLUETOOTH}, data=T9146_SERVICE_INFO, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -176,7 +176,7 @@ async def test_async_step_user_takes_precedence_over_discovery( context={"source": config_entries.SOURCE_BLUETOOTH}, data=T9146_SERVICE_INFO, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( @@ -187,7 +187,7 @@ async def test_async_step_user_takes_precedence_over_discovery( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.eufylife_ble.async_setup_entry", return_value=True @@ -196,7 +196,7 @@ async def test_async_step_user_takes_precedence_over_discovery( result["flow_id"], user_input={"address": "11:22:33:44:55:66"}, ) - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Smart Scale C1" assert result2["data"] == {"model": "eufy T9146"} assert result2["result"].unique_id == "11:22:33:44:55:66" diff --git a/tests/components/evil_genius_labs/test_config_flow.py b/tests/components/evil_genius_labs/test_config_flow.py index b6bdae940ba..398cfde560a 100644 --- a/tests/components/evil_genius_labs/test_config_flow.py +++ b/tests/components/evil_genius_labs/test_config_flow.py @@ -18,7 +18,7 @@ async def test_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -47,7 +47,7 @@ async def test_form( ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Fibonacci256-23D4" assert result2["data"] == { "host": "1.1.1.1", @@ -74,7 +74,7 @@ async def test_form_cannot_connect( }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} assert "Unable to connect" in caplog.text @@ -96,7 +96,7 @@ async def test_form_timeout(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "timeout"} @@ -117,5 +117,5 @@ async def test_form_unknown(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == FlowResultType.FORM + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/ezviz/test_config_flow.py b/tests/components/ezviz/test_config_flow.py index c99c9c0fe9e..57c3ae0600e 100644 --- a/tests/components/ezviz/test_config_flow.py +++ b/tests/components/ezviz/test_config_flow.py @@ -51,7 +51,7 @@ async def test_user_form(hass: HomeAssistant, ezviz_config_flow) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -62,7 +62,7 @@ async def test_user_form(hass: HomeAssistant, ezviz_config_flow) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "test-username" assert result["data"] == {**API_LOGIN_RETURN_VALIDATE} @@ -71,7 +71,7 @@ async def test_user_form(hass: HomeAssistant, ezviz_config_flow) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured_account" @@ -90,7 +90,7 @@ async def test_user_custom_url(hass: HomeAssistant, ezviz_config_flow) -> None: }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user_custom_url" assert result["errors"] == {} @@ -101,7 +101,7 @@ async def test_user_custom_url(hass: HomeAssistant, ezviz_config_flow) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == API_LOGIN_RETURN_VALIDATE assert len(mock_setup_entry.mock_calls) == 1 @@ -113,7 +113,7 @@ async def test_async_step_reauth(hass: HomeAssistant, ezviz_config_flow) -> None result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -124,7 +124,7 @@ async def test_async_step_reauth(hass: HomeAssistant, ezviz_config_flow) -> None ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "test-username" assert result["data"] == {**API_LOGIN_RETURN_VALIDATE} @@ -133,7 +133,7 @@ async def test_async_step_reauth(hass: HomeAssistant, ezviz_config_flow) -> None result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_REAUTH}, data=USER_INPUT_VALIDATE ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -146,7 +146,7 @@ async def test_async_step_reauth(hass: HomeAssistant, ezviz_config_flow) -> None ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" @@ -158,7 +158,7 @@ async def test_step_discovery_abort_if_cloud_account_missing( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_INTEGRATION_DISCOVERY}, data=DISCOVERY_INFO ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {} @@ -171,7 +171,7 @@ async def test_step_discovery_abort_if_cloud_account_missing( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "ezviz_cloud_account_missing" @@ -181,7 +181,7 @@ async def test_step_reauth_abort_if_cloud_account_missing(hass: HomeAssistant) - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_REAUTH}, data=USER_INPUT_VALIDATE ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "ezviz_cloud_account_missing" @@ -195,7 +195,7 @@ async def test_async_step_integration_discovery( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_INTEGRATION_DISCOVERY}, data=DISCOVERY_INFO ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {} @@ -209,7 +209,7 @@ async def test_async_step_integration_discovery( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_PASSWORD: "test-pass", CONF_TYPE: ATTR_TYPE_CAMERA, @@ -228,7 +228,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: assert entry.options[CONF_TIMEOUT] == DEFAULT_TIMEOUT result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] is None @@ -238,7 +238,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_FFMPEG_ARGUMENTS] == "/H.264" assert result["data"][CONF_TIMEOUT] == 25 @@ -250,7 +250,7 @@ async def test_user_form_exception(hass: HomeAssistant, ezviz_config_flow) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -261,7 +261,7 @@ async def test_user_form_exception(hass: HomeAssistant, ezviz_config_flow) -> No USER_INPUT_VALIDATE, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -272,7 +272,7 @@ async def test_user_form_exception(hass: HomeAssistant, ezviz_config_flow) -> No USER_INPUT_VALIDATE, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_host"} @@ -283,7 +283,7 @@ async def test_user_form_exception(hass: HomeAssistant, ezviz_config_flow) -> No USER_INPUT_VALIDATE, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "mfa_required"} @@ -294,7 +294,7 @@ async def test_user_form_exception(hass: HomeAssistant, ezviz_config_flow) -> No USER_INPUT_VALIDATE, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -305,7 +305,7 @@ async def test_user_form_exception(hass: HomeAssistant, ezviz_config_flow) -> No USER_INPUT_VALIDATE, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -322,7 +322,7 @@ async def test_discover_exception_step1( context={"source": SOURCE_INTEGRATION_DISCOVERY}, data={ATTR_SERIAL: "C66666", CONF_IP_ADDRESS: "test-ip"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {} @@ -337,7 +337,7 @@ async def test_discover_exception_step1( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {"base": "invalid_auth"} @@ -351,7 +351,7 @@ async def test_discover_exception_step1( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {"base": "invalid_host"} @@ -365,7 +365,7 @@ async def test_discover_exception_step1( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {"base": "invalid_auth"} @@ -379,7 +379,7 @@ async def test_discover_exception_step1( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {"base": "mfa_required"} @@ -393,7 +393,7 @@ async def test_discover_exception_step1( }, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -411,7 +411,7 @@ async def test_discover_exception_step3( context={"source": SOURCE_INTEGRATION_DISCOVERY}, data={ATTR_SERIAL: "C66666", CONF_IP_ADDRESS: "test-ip"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {} @@ -426,7 +426,7 @@ async def test_discover_exception_step3( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {"base": "invalid_auth"} @@ -440,7 +440,7 @@ async def test_discover_exception_step3( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {"base": "invalid_host"} @@ -454,7 +454,7 @@ async def test_discover_exception_step3( }, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -476,7 +476,7 @@ async def test_user_custom_url_exception( }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user_custom_url" assert result["errors"] == {} @@ -485,7 +485,7 @@ async def test_user_custom_url_exception( {CONF_URL: "test-user"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user_custom_url" assert result["errors"] == {"base": "invalid_auth"} @@ -496,7 +496,7 @@ async def test_user_custom_url_exception( {CONF_URL: "test-user"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user_custom_url" assert result["errors"] == {"base": "invalid_host"} @@ -507,7 +507,7 @@ async def test_user_custom_url_exception( {CONF_URL: "test-user"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user_custom_url" assert result["errors"] == {"base": "invalid_auth"} @@ -518,7 +518,7 @@ async def test_user_custom_url_exception( {CONF_URL: "test-user"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user_custom_url" assert result["errors"] == {"base": "mfa_required"} @@ -529,7 +529,7 @@ async def test_user_custom_url_exception( {CONF_URL: "test-user"}, ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -541,7 +541,7 @@ async def test_async_step_reauth_exception( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -552,7 +552,7 @@ async def test_async_step_reauth_exception( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "test-username" assert result["data"] == {**API_LOGIN_RETURN_VALIDATE} @@ -561,7 +561,7 @@ async def test_async_step_reauth_exception( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_REAUTH}, data=USER_INPUT_VALIDATE ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -575,7 +575,7 @@ async def test_async_step_reauth_exception( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "invalid_host"} @@ -589,7 +589,7 @@ async def test_async_step_reauth_exception( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "invalid_host"} @@ -603,7 +603,7 @@ async def test_async_step_reauth_exception( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "mfa_required"} @@ -617,7 +617,7 @@ async def test_async_step_reauth_exception( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "invalid_auth"} @@ -631,5 +631,5 @@ async def test_async_step_reauth_exception( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" From 17da077c74f789088fd4024da31bcd521905f5b0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Apr 2024 11:22:40 -1000 Subject: [PATCH 193/967] Avoid trying to load platform that are known to not exist in async_prepare_setup_platform (#114659) --- homeassistant/setup.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 32bb8f361d6..b1fc080a429 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -504,6 +504,12 @@ async def async_prepare_setup_platform( log_error(f"Unable to import the component ({exc}).") return None + if not integration.platforms_exists((domain,)): + log_error( + f"Platform not found (No module named '{integration.pkg_path}.{domain}')" + ) + return None + try: platform = await integration.async_get_platform(domain) except ImportError as exc: From 1779fe8f629e19ab7ec19aa560704e54820e6160 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Apr 2024 11:37:16 -1000 Subject: [PATCH 194/967] Bump yalexs to 3.0.1 (#114678) * Bump yalexs to 3.0.1 changelog: https://github.com/bdraco/yalexs/compare/v2.0.0...v3.0.1 * fix for breaking change --- homeassistant/components/august/lock.py | 5 ++++- homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/august/lock.py b/homeassistant/components/august/lock.py index e711edd6893..a6b549b8c89 100644 --- a/homeassistant/components/august/lock.py +++ b/homeassistant/components/august/lock.py @@ -135,7 +135,10 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity): 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 is LockStatus.UNLOCKING + self._attr_is_unlocking = self._lock_status in ( + LockStatus.UNLOCKING, + LockStatus.UNLATCHING, + ) self._attr_extra_state_attributes = { ATTR_BATTERY_LEVEL: self._detail.battery_level diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 27c5f11ec6e..e380a00cbc0 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==2.0.0", "yalexs-ble==2.4.2"] + "requirements": ["yalexs==3.0.1", "yalexs-ble==2.4.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8a35cd7a2ee..6f7bdd3f729 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2905,7 +2905,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.2 # homeassistant.components.august -yalexs==2.0.0 +yalexs==3.0.1 # homeassistant.components.yeelight yeelight==0.7.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 903f14831b3..de4991f1321 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2246,7 +2246,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.2 # homeassistant.components.august -yalexs==2.0.0 +yalexs==3.0.1 # homeassistant.components.yeelight yeelight==0.7.14 From f26a7843c67607b5a0c2446e42d931770ec08c46 Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Tue, 2 Apr 2024 18:10:15 -0400 Subject: [PATCH 195/967] Fix Sonos play imported playlists (#113934) --- .../components/sonos/media_browser.py | 14 +++ .../components/sonos/media_player.py | 12 +- tests/components/sonos/test_media_player.py | 117 ++++++++++++++++++ 3 files changed, 137 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/sonos/media_browser.py b/homeassistant/components/sonos/media_browser.py index 967e81061ed..6e6f388ed50 100644 --- a/homeassistant/components/sonos/media_browser.py +++ b/homeassistant/components/sonos/media_browser.py @@ -497,6 +497,20 @@ def get_media( """Fetch media/album.""" search_type = MEDIA_TYPES_TO_SONOS.get(search_type, search_type) + if search_type == "playlists": + # Format is S:TITLE or S:ITEM_ID + splits = item_id.split(":") + title = splits[1] if len(splits) > 1 else None + playlist = next( + ( + p + for p in media_library.get_playlists() + if (item_id == p.item_id or title == p.title) + ), + None, + ) + return playlist + if not item_id.startswith("A:ALBUM") and search_type == SONOS_ALBUM: item_id = "A:ALBUMARTIST/" + "/".join(item_id.split("/")[2:]) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 12e8b44652a..581bdaad37d 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -626,13 +626,13 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): soco.play_uri(media_id, force_radio=is_radio) elif media_type == MediaType.PLAYLIST: if media_id.startswith("S:"): - item = media_browser.get_media(self.media.library, media_id, media_type) - soco.play_uri(item.get_uri()) - return - try: + playlist = media_browser.get_media( + self.media.library, media_id, media_type + ) + else: playlists = soco.get_sonos_playlists(complete_result=True) - playlist = next(p for p in playlists if p.title == media_id) - except StopIteration: + playlist = next((p for p in playlists if p.title == media_id), None) + if not playlist: _LOGGER.error('Could not find a Sonos playlist named "%s"', media_id) else: soco.clear_queue() diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index d89a1076db3..c181520b85d 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -1,5 +1,13 @@ """Tests for the Sonos Media Player platform.""" +import logging + +import pytest + +from homeassistant.components.media_player import ( + DOMAIN as MP_DOMAIN, + SERVICE_PLAY_MEDIA, +) from homeassistant.const import STATE_IDLE from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import ( @@ -8,6 +16,8 @@ from homeassistant.helpers.device_registry import ( DeviceRegistry, ) +from .conftest import SoCoMockFactory + async def test_device_registry( hass: HomeAssistant, device_registry: DeviceRegistry, async_autosetup_sonos, soco @@ -53,3 +63,110 @@ async def test_entity_basic( assert attributes["friendly_name"] == "Zone A" assert attributes["is_volume_muted"] is False assert attributes["volume_level"] == 0.19 + + +class _MockMusicServiceItem: + """Mocks a Soco MusicServiceItem.""" + + def __init__( + self, + title: str, + item_id: str, + parent_id: str, + item_class: str, + ) -> None: + """Initialize the mock item.""" + self.title = title + self.item_id = item_id + self.item_class = item_class + self.parent_id = parent_id + + def get_uri(self) -> str: + """Return URI.""" + return self.item_id.replace("S://", "x-file-cifs://") + + +_mock_playlists = [ + _MockMusicServiceItem( + "playlist1", + "S://192.168.1.68/music/iTunes/iTunes%20Music%20Library.xml#GUID_1", + "A:PLAYLISTS", + "object.container.playlistContainer", + ), + _MockMusicServiceItem( + "playlist2", + "S://192.168.1.68/music/iTunes/iTunes%20Music%20Library.xml#GUID_2", + "A:PLAYLISTS", + "object.container.playlistContainer", + ), +] + + +@pytest.mark.parametrize( + ("media_content_id", "expected_item_id"), + [ + ( + _mock_playlists[0].item_id, + _mock_playlists[0].item_id, + ), + ( + f"S:{_mock_playlists[1].title}", + _mock_playlists[1].item_id, + ), + ], +) +async def test_play_media_music_library_playlist( + hass: HomeAssistant, + soco_factory: SoCoMockFactory, + async_autosetup_sonos, + discover, + media_content_id, + expected_item_id, +) -> None: + """Test that playlists can be found by id or title.""" + soco_mock = soco_factory.mock_list.get("192.168.42.2") + soco_mock.music_library.get_playlists.return_value = _mock_playlists + + await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + "entity_id": "media_player.zone_a", + "media_content_type": "playlist", + "media_content_id": media_content_id, + }, + blocking=True, + ) + + assert soco_mock.clear_queue.call_count == 1 + assert soco_mock.add_to_queue.call_count == 1 + assert soco_mock.add_to_queue.call_args_list[0].args[0].item_id == expected_item_id + assert soco_mock.play_from_queue.call_count == 1 + + +async def test_play_media_music_library_playlist_dne( + hass: HomeAssistant, + soco_factory: SoCoMockFactory, + async_autosetup_sonos, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test error handling when attempting to play a non-existent playlist .""" + media_content_id = "S:nonexistent" + soco_mock = soco_factory.mock_list.get("192.168.42.2") + soco_mock.music_library.get_playlists.return_value = _mock_playlists + + with caplog.at_level(logging.ERROR): + caplog.clear() + await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + "entity_id": "media_player.zone_a", + "media_content_type": "playlist", + "media_content_id": media_content_id, + }, + blocking=True, + ) + assert soco_mock.play_uri.call_count == 0 + assert media_content_id in caplog.text + assert "playlist" in caplog.text From 3c76036c159cf88c889988489b52126e8d4bd504 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 3 Apr 2024 00:12:31 +0200 Subject: [PATCH 196/967] Update frontend to 20240402.2 (#114683) --- 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 2010a9985b3..3ac7efa9fab 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==20240402.1"] + "requirements": ["home-assistant-frontend==20240402.2"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1dc5cd9886a..56ea6b6b0ba 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -30,7 +30,7 @@ habluetooth==2.4.2 hass-nabucasa==0.79.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240402.1 +home-assistant-frontend==20240402.2 home-assistant-intents==2024.3.29 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 6f7bdd3f729..b8e8cdbdf39 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1081,7 +1081,7 @@ hole==0.8.0 holidays==0.46 # homeassistant.components.frontend -home-assistant-frontend==20240402.1 +home-assistant-frontend==20240402.2 # homeassistant.components.conversation home-assistant-intents==2024.3.29 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index de4991f1321..a92b31793df 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -880,7 +880,7 @@ hole==0.8.0 holidays==0.46 # homeassistant.components.frontend -home-assistant-frontend==20240402.1 +home-assistant-frontend==20240402.2 # homeassistant.components.conversation home-assistant-intents==2024.3.29 From 06a752aa927246fb347980b82f0c6bdc59ae6640 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Apr 2024 16:09:27 -1000 Subject: [PATCH 197/967] Small speed ups to ambient_station (#114698) --- homeassistant/components/ambient_station/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index 0984e21a722..b55a7b866cc 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -93,7 +93,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: ambient = hass.data[DOMAIN].pop(entry.entry_id) - hass.async_create_task(ambient.ws_disconnect()) + hass.async_create_task(ambient.ws_disconnect(), eager_start=True) return unload_ok @@ -179,7 +179,8 @@ class AmbientStation: self._hass.async_create_task( self._hass.config_entries.async_forward_entry_setups( self._entry, PLATFORMS - ) + ), + eager_start=True, ) self._entry_setup_complete = True self._ws_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY From d17f308c6afc6b01f7786acc3ff55e3ff045d695 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Apr 2024 17:47:22 -1000 Subject: [PATCH 198/967] Small speed up to starting and stopping cloud (#114696) --- homeassistant/components/cloud/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index d32278ba8f0..80c2e86a2a3 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -262,7 +262,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Shutdown event.""" await cloud.stop() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown) + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, _shutdown, run_immediately=True + ) _remote_handle_prefs_updated(cloud) @@ -343,7 +345,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: DOMAIN, {"platform_loaded": tts_platform_loaded}, config, - ) + ), + eager_start=True, ) async_call_later( From adbaed2c6d6169a9476488288c25b6662e8b46ff Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Apr 2024 21:02:32 -1000 Subject: [PATCH 199/967] Reduce code for registry items with a base class (#114689) --- homeassistant/helpers/device_registry.py | 36 +++++-------- homeassistant/helpers/entity_registry.py | 40 +++----------- .../helpers/normalized_name_base_registry.py | 49 +++++++---------- homeassistant/helpers/registry.py | 52 ++++++++++++++++++- 4 files changed, 89 insertions(+), 88 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 9666ad302ad..76daa1266dd 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -2,8 +2,7 @@ from __future__ import annotations -from collections import UserDict -from collections.abc import Mapping, ValuesView +from collections.abc import Mapping from enum import StrEnum from functools import lru_cache, partial import logging @@ -31,7 +30,7 @@ from .deprecation import ( ) from .frame import report from .json import JSON_DUMP, find_paths_unserializable_data, json_bytes, json_fragment -from .registry import BaseRegistry +from .registry import BaseRegistry, BaseRegistryItems from .typing import UNDEFINED, UndefinedType if TYPE_CHECKING: @@ -443,7 +442,7 @@ class DeviceRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]): _EntryTypeT = TypeVar("_EntryTypeT", DeviceEntry, DeletedDeviceEntry) -class DeviceRegistryItems(UserDict[str, _EntryTypeT]): +class DeviceRegistryItems(BaseRegistryItems[_EntryTypeT]): """Container for device registry items, maps device id -> entry. Maintains two additional indexes: @@ -457,33 +456,22 @@ class DeviceRegistryItems(UserDict[str, _EntryTypeT]): self._connections: dict[tuple[str, str], _EntryTypeT] = {} self._identifiers: dict[tuple[str, str], _EntryTypeT] = {} - def values(self) -> ValuesView[_EntryTypeT]: - """Return the underlying values to avoid __iter__ overhead.""" - return self.data.values() - - def __setitem__(self, key: str, entry: _EntryTypeT) -> None: - """Add an item.""" - data = self.data - if key in data: - old_entry = data[key] - for connection in old_entry.connections: - del self._connections[connection] - for identifier in old_entry.identifiers: - del self._identifiers[identifier] - data[key] = entry + def _index_entry(self, key: str, entry: _EntryTypeT) -> None: + """Index an entry.""" for connection in entry.connections: self._connections[connection] = entry for identifier in entry.identifiers: self._identifiers[identifier] = entry - def __delitem__(self, key: str) -> None: - """Remove an item.""" - entry = self[key] - for connection in entry.connections: + def _unindex_entry( + self, key: str, replacement_entry: _EntryTypeT | None = None + ) -> None: + """Unindex an entry.""" + old_entry = self.data[key] + for connection in old_entry.connections: del self._connections[connection] - for identifier in entry.identifiers: + for identifier in old_entry.identifiers: del self._identifiers[identifier] - super().__delitem__(key) def get_entry( self, diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index ad9ddcd5c4c..e19c4290a1d 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -10,8 +10,7 @@ timer. from __future__ import annotations -from collections import UserDict -from collections.abc import Callable, Iterable, KeysView, Mapping, ValuesView +from collections.abc import Callable, Iterable, KeysView, Mapping from datetime import datetime, timedelta from enum import StrEnum import logging @@ -53,7 +52,7 @@ from homeassistant.util.read_only_dict import ReadOnlyDict from . import device_registry as dr, storage from .device_registry import EVENT_DEVICE_REGISTRY_UPDATED from .json import JSON_DUMP, find_paths_unserializable_data, json_bytes, json_fragment -from .registry import BaseRegistry +from .registry import BaseRegistry, BaseRegistryItems from .typing import UNDEFINED, UndefinedType if TYPE_CHECKING: @@ -510,7 +509,7 @@ class EntityRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]): return data -class EntityRegistryItems(UserDict[str, RegistryEntry]): +class EntityRegistryItems(BaseRegistryItems[RegistryEntry]): """Container for entity registry items, maps entity_id -> entry. Maintains four additional indexes: @@ -529,16 +528,8 @@ class EntityRegistryItems(UserDict[str, RegistryEntry]): self._device_id_index: dict[str, dict[str, Literal[True]]] = {} self._area_id_index: dict[str, dict[str, Literal[True]]] = {} - def values(self) -> ValuesView[RegistryEntry]: - """Return the underlying values to avoid __iter__ overhead.""" - return self.data.values() - - def __setitem__(self, key: str, entry: RegistryEntry) -> None: - """Add an item.""" - data = self.data - if key in data: - self._unindex_entry(key) - data[key] = entry + def _index_entry(self, key: str, entry: RegistryEntry) -> None: + """Index an entry.""" self._entry_ids[entry.id] = entry self._index[(entry.domain, entry.platform, entry.unique_id)] = entry.entity_id # python has no ordered set, so we use a dict with True values @@ -550,21 +541,9 @@ class EntityRegistryItems(UserDict[str, RegistryEntry]): if (area_id := entry.area_id) is not None: self._area_id_index.setdefault(area_id, {})[key] = True - def _unindex_entry_value( - self, key: str, value: str, index: dict[str, dict[str, Literal[True]]] + def _unindex_entry( + self, key: str, replacement_entry: RegistryEntry | None = None ) -> None: - """Unindex an entry value. - - key is the entry key - value is the value to unindex such as config_entry_id or device_id. - index is the index to unindex from. - """ - entries = index[value] - del entries[key] - if not entries: - del index[value] - - def _unindex_entry(self, key: str) -> None: """Unindex an entry.""" entry = self.data[key] del self._entry_ids[entry.id] @@ -576,11 +555,6 @@ class EntityRegistryItems(UserDict[str, RegistryEntry]): if area_id := entry.area_id: self._unindex_entry_value(key, area_id, self._area_id_index) - def __delitem__(self, key: str) -> None: - """Remove an item.""" - self._unindex_entry(key) - super().__delitem__(key) - def get_device_ids(self) -> KeysView[str]: """Return device ids.""" return self._device_id_index.keys() diff --git a/homeassistant/helpers/normalized_name_base_registry.py b/homeassistant/helpers/normalized_name_base_registry.py index 16280a73750..f14d99b7831 100644 --- a/homeassistant/helpers/normalized_name_base_registry.py +++ b/homeassistant/helpers/normalized_name_base_registry.py @@ -1,10 +1,11 @@ """Provide a base class for registries that use a normalized name index.""" -from collections import UserDict -from collections.abc import ValuesView from dataclasses import dataclass +from functools import lru_cache from typing import TypeVar +from .registry import BaseRegistryItems + @dataclass(slots=True, frozen=True, kw_only=True) class NormalizedNameBaseRegistryEntry: @@ -17,12 +18,13 @@ class NormalizedNameBaseRegistryEntry: _VT = TypeVar("_VT", bound=NormalizedNameBaseRegistryEntry) +@lru_cache(maxsize=1024) def normalize_name(name: str) -> str: """Normalize a name by removing whitespace and case folding.""" return name.casefold().replace(" ", "") -class NormalizedNameBaseRegistryItems(UserDict[str, _VT]): +class NormalizedNameBaseRegistryItems(BaseRegistryItems[_VT]): """Base container for normalized name registry items, maps key -> entry. Maintains an additional index: @@ -34,34 +36,21 @@ class NormalizedNameBaseRegistryItems(UserDict[str, _VT]): super().__init__() self._normalized_names: dict[str, _VT] = {} - def values(self) -> ValuesView[_VT]: - """Return the underlying values to avoid __iter__ overhead.""" - return self.data.values() + def _unindex_entry(self, key: str, replacement_entry: _VT | None = None) -> None: + old_entry = self.data[key] + if ( + replacement_entry is not None + and (normalized_name := normalize_name(replacement_entry.name)) + != old_entry.normalized_name + and normalized_name in self._normalized_names + ): + raise ValueError( + f"The name {replacement_entry.name} ({normalized_name}) is already in use" + ) + del self._normalized_names[old_entry.normalized_name] - def __setitem__(self, key: str, entry: _VT) -> None: - """Add an item.""" - data = self.data - normalized_name = normalize_name(entry.name) - - if key in data: - old_entry = data[key] - if ( - normalized_name != old_entry.normalized_name - and normalized_name in self._normalized_names - ): - raise ValueError( - f"The name {entry.name} ({normalized_name}) is already in use" - ) - del self._normalized_names[old_entry.normalized_name] - data[key] = entry - self._normalized_names[normalized_name] = entry - - def __delitem__(self, key: str) -> None: - """Remove an item.""" - entry = self[key] - normalized_name = normalize_name(entry.name) - del self._normalized_names[normalized_name] - super().__delitem__(key) + def _index_entry(self, key: str, entry: _VT) -> None: + self._normalized_names[normalize_name(entry.name)] = entry def get_by_name(self, name: str) -> _VT | None: """Get entry by name.""" diff --git a/homeassistant/helpers/registry.py b/homeassistant/helpers/registry.py index d5b1035531a..0057190848a 100644 --- a/homeassistant/helpers/registry.py +++ b/homeassistant/helpers/registry.py @@ -3,7 +3,9 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any +from collections import UserDict +from collections.abc import ValuesView +from typing import TYPE_CHECKING, Any, Literal, TypeVar from homeassistant.core import CoreState, HomeAssistant, callback @@ -14,6 +16,54 @@ SAVE_DELAY = 10 SAVE_DELAY_LONG = 180 +_DataT = TypeVar("_DataT") + + +class BaseRegistryItems(UserDict[str, _DataT], ABC): + """Base class for registry items.""" + + data: dict[str, _DataT] + + def values(self) -> ValuesView[_DataT]: + """Return the underlying values to avoid __iter__ overhead.""" + return self.data.values() + + @abstractmethod + def _index_entry(self, key: str, entry: _DataT) -> None: + """Index an entry.""" + + @abstractmethod + def _unindex_entry(self, key: str, replacement_entry: _DataT | None = None) -> None: + """Unindex an entry.""" + + def __setitem__(self, key: str, entry: _DataT) -> None: + """Add an item.""" + data = self.data + if key in data: + self._unindex_entry(key, entry) + data[key] = entry + self._index_entry(key, entry) + + def _unindex_entry_value( + self, key: str, value: str, index: dict[str, dict[str, Literal[True]]] + ) -> None: + """Unindex an entry value. + + key is the entry key + value is the value to unindex such as config_entry_id or device_id. + index is the index to unindex from. + """ + entries = index[value] + del entries[key] + if not entries: + del index[value] + + def __delitem__(self, key: str) -> None: + """Remove an item.""" + self._unindex_entry(key) + super().__delitem__(key) + + class BaseRegistry(ABC): """Class to implement a registry.""" From d058615961b31ea053d1e9d4dc225d316cf91cbb Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 3 Apr 2024 09:10:10 +0200 Subject: [PATCH 200/967] Add service homeworks.send_command (#114059) * Add service homeworks.send_command * Translate exception --- .../components/homeworks/__init__.py | 77 ++++++++++++++++++- homeassistant/components/homeworks/icons.json | 5 ++ .../components/homeworks/services.yaml | 13 ++++ .../components/homeworks/strings.json | 21 +++++ tests/components/homeworks/test_init.py | 76 ++++++++++++++++++ 5 files changed, 190 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/homeworks/icons.json create mode 100644 homeassistant/components/homeworks/services.yaml diff --git a/homeassistant/components/homeworks/__init__.py b/homeassistant/components/homeworks/__init__.py index 83ae12dffba..a67e69bc9c6 100644 --- a/homeassistant/components/homeworks/__init__.py +++ b/homeassistant/components/homeworks/__init__.py @@ -2,8 +2,10 @@ from __future__ import annotations +from collections.abc import Mapping from dataclasses import dataclass import logging +from time import sleep from typing import Any from pyhomeworks.pyhomeworks import HW_BUTTON_PRESSED, HW_BUTTON_RELEASED, Homeworks @@ -18,8 +20,8 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, Platform, ) -from homeassistant.core import Event, HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.core import Event, HomeAssistant, ServiceCall, callback +from homeassistant.exceptions import ConfigEntryNotReady, ServiceValidationError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send @@ -40,6 +42,8 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.LIGHT] +CONF_COMMAND = "command" + EVENT_BUTTON_PRESS = "homeworks_button_press" EVENT_BUTTON_RELEASE = "homeworks_button_release" @@ -77,6 +81,13 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) +SERVICE_SEND_COMMAND_SCHEMA = vol.Schema( + { + vol.Required(CONF_CONTROLLER_ID): str, + vol.Required(CONF_COMMAND): vol.All(cv.ensure_list, [str]), + } +) + @dataclass class HomeworksData: @@ -87,6 +98,66 @@ class HomeworksData: keypads: dict[str, HomeworksKeypad] +@callback +def async_setup_services(hass: HomeAssistant) -> None: + """Set up services for Lutron Homeworks Series 4 and 8 integration.""" + + async def async_call_service(service_call: ServiceCall) -> None: + """Call the service.""" + await async_send_command(hass, service_call.data) + + hass.services.async_register( + DOMAIN, + "send_command", + async_call_service, + schema=SERVICE_SEND_COMMAND_SCHEMA, + ) + + +async def async_send_command(hass: HomeAssistant, data: Mapping[str, Any]) -> None: + """Send command to a controller.""" + + def get_controller_ids() -> list[str]: + """Get homeworks data for the specified controller ID.""" + return [data.controller_id for data in hass.data[DOMAIN].values()] + + def get_homeworks_data(controller_id: str) -> HomeworksData | None: + """Get homeworks data for the specified controller ID.""" + data: HomeworksData + for data in hass.data[DOMAIN].values(): + if data.controller_id == controller_id: + return data + return None + + def send_commands(controller: Homeworks, commands: list[str]) -> None: + """Send commands to controller.""" + _LOGGER.debug("Send commands: %s", commands) + for command in commands: + if command.lower().startswith("delay"): + delay = int(command.partition(" ")[2]) + _LOGGER.debug("Sleeping for %s ms", delay) + sleep(delay / 1000) + else: + _LOGGER.debug("Sending command '%s'", command) + # pylint: disable-next=protected-access + controller._send(command) + + homeworks_data = get_homeworks_data(data[CONF_CONTROLLER_ID]) + if not homeworks_data: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_controller_id", + translation_placeholders={ + "controller_id": data[CONF_CONTROLLER_ID], + "controller_ids": ",".join(get_controller_ids()), + }, + ) + + await hass.async_add_executor_job( + send_commands, homeworks_data.controller, data[CONF_COMMAND] + ) + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Start Homeworks controller.""" @@ -97,6 +168,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) ) + async_setup_services(hass) + return True diff --git a/homeassistant/components/homeworks/icons.json b/homeassistant/components/homeworks/icons.json new file mode 100644 index 00000000000..f53b447d96e --- /dev/null +++ b/homeassistant/components/homeworks/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "send_command": "mdi:console" + } +} diff --git a/homeassistant/components/homeworks/services.yaml b/homeassistant/components/homeworks/services.yaml new file mode 100644 index 00000000000..8989fc51f1d --- /dev/null +++ b/homeassistant/components/homeworks/services.yaml @@ -0,0 +1,13 @@ +send_command: + fields: + controller_id: + required: true + example: "lutron_homeworks" + selector: + text: + command: + required: true + example: "KBP, [02:08:02:01], 1" + selector: + text: + multiple: true diff --git a/homeassistant/components/homeworks/strings.json b/homeassistant/components/homeworks/strings.json index 03c09e12888..46c58515f39 100644 --- a/homeassistant/components/homeworks/strings.json +++ b/homeassistant/components/homeworks/strings.json @@ -39,6 +39,11 @@ } } }, + "exceptions": { + "invalid_controller_id": { + "message": "Invalid controller_id '{controller_id}', expected one of '{controller_ids}'" + } + }, "options": { "error": { "duplicated_addr": "The specified address is already in use", @@ -142,5 +147,21 @@ "title": "[%key:component::homeworks::options::step::init::menu_options::select_edit_light%]" } } + }, + "services": { + "send_command": { + "name": "Send command", + "description": "Send custom command to a controller", + "fields": { + "command": { + "name": "Command", + "description": "Command to send to the controller. This can either be a single command or a list of commands." + }, + "controller_id": { + "name": "Controller ID", + "description": "The controller to which to send command." + } + } + } } } diff --git a/tests/components/homeworks/test_init.py b/tests/components/homeworks/test_init.py index 566e0b4beb4..1969bb448ec 100644 --- a/tests/components/homeworks/test_init.py +++ b/tests/components/homeworks/test_init.py @@ -3,12 +3,14 @@ from unittest.mock import ANY, MagicMock 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.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 @@ -114,3 +116,77 @@ async def test_keypad_events( await hass.async_block_till_done() assert len(press_events) == 1 assert len(release_events) == 1 + + +async def test_send_command( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_homeworks: MagicMock, +) -> None: + """Test the send command service.""" + mock_controller = MagicMock() + mock_homeworks.return_value = mock_controller + + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + mock_controller._send.reset_mock() + await hass.services.async_call( + DOMAIN, + "send_command", + {"controller_id": "main_controller", "command": "KBP, [02:08:02:01], 1"}, + blocking=True, + ) + assert len(mock_controller._send.mock_calls) == 1 + assert mock_controller._send.mock_calls[0][1] == ("KBP, [02:08:02:01], 1",) + + mock_controller._send.reset_mock() + await hass.services.async_call( + DOMAIN, + "send_command", + { + "controller_id": "main_controller", + "command": [ + "KBP, [02:08:02:01], 1", + "KBH, [02:08:02:01], 1", + "KBR, [02:08:02:01], 1", + ], + }, + blocking=True, + ) + assert len(mock_controller._send.mock_calls) == 3 + assert mock_controller._send.mock_calls[0][1] == ("KBP, [02:08:02:01], 1",) + assert mock_controller._send.mock_calls[1][1] == ("KBH, [02:08:02:01], 1",) + assert mock_controller._send.mock_calls[2][1] == ("KBR, [02:08:02:01], 1",) + + mock_controller._send.reset_mock() + await hass.services.async_call( + DOMAIN, + "send_command", + { + "controller_id": "main_controller", + "command": [ + "KBP, [02:08:02:01], 1", + "delay 50", + "KBH, [02:08:02:01], 1", + "dElAy 100", + "KBR, [02:08:02:01], 1", + ], + }, + blocking=True, + ) + assert len(mock_controller._send.mock_calls) == 3 + assert mock_controller._send.mock_calls[0][1] == ("KBP, [02:08:02:01], 1",) + assert mock_controller._send.mock_calls[1][1] == ("KBH, [02:08:02:01], 1",) + assert mock_controller._send.mock_calls[2][1] == ("KBR, [02:08:02:01], 1",) + + mock_controller._send.reset_mock() + with pytest.raises(ServiceValidationError): + await hass.services.async_call( + DOMAIN, + "send_command", + {"controller_id": "unknown_controller", "command": "KBP, [02:08:02:01], 1"}, + blocking=True, + ) + assert len(mock_controller._send.mock_calls) == 0 From 53cbb83e46a2c398284cfe6d9b0d1f0449b893fa Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Wed, 3 Apr 2024 03:12:00 -0400 Subject: [PATCH 201/967] Import zha quirks in the executor (#114685) --- homeassistant/components/zha/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index ef603a4ea71..de761138ce1 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -124,8 +124,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b zha_data = get_zha_data(hass) if zha_data.yaml_config.get(CONF_ENABLE_QUIRKS, True): - setup_quirks( - custom_quirks_path=zha_data.yaml_config.get(CONF_CUSTOM_QUIRKS_PATH) + await hass.async_add_import_executor_job( + setup_quirks, zha_data.yaml_config.get(CONF_CUSTOM_QUIRKS_PATH) ) # Load and cache device trigger information early From 3eafdadc8fe8905dfbda1fcdbca2f03918966c51 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Apr 2024 09:13:53 +0200 Subject: [PATCH 202/967] Bump Wandalen/wretry.action from 3.0.0 to 3.0.1 (#114714) 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 66965bf5363..9ab7e235a68 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1070,7 +1070,7 @@ jobs: pattern: coverage-* - name: Upload coverage to Codecov (full coverage) if: needs.info.outputs.test_full_suite == 'true' - uses: Wandalen/wretry.action@v3.0.0 + uses: Wandalen/wretry.action@v3.0.1 with: action: codecov/codecov-action@v3.1.3 with: | @@ -1081,7 +1081,7 @@ jobs: attempt_delay: 30000 - name: Upload coverage to Codecov (partial coverage) if: needs.info.outputs.test_full_suite == 'false' - uses: Wandalen/wretry.action@v3.0.0 + uses: Wandalen/wretry.action@v3.0.1 with: action: codecov/codecov-action@v3.1.3 with: | From a76753097064991a21988f33003d6f39cc9c7296 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Apr 2024 21:14:15 -1000 Subject: [PATCH 203/967] Migrate homeassistant_sky_connect to use eager_start for tasks (#114706) --- .../components/homeassistant_sky_connect/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homeassistant_sky_connect/__init__.py b/homeassistant/components/homeassistant_sky_connect/__init__.py index 1ee4710769b..a85a1161792 100644 --- a/homeassistant/components/homeassistant_sky_connect/__init__.py +++ b/homeassistant/components/homeassistant_sky_connect/__init__.py @@ -30,7 +30,9 @@ async def _async_usb_scan_done(hass: HomeAssistant, entry: ConfigEntry) -> None: if not usb.async_is_plugged_in(hass, matcher): # The USB dongle is not plugged in, remove the config entry - hass.async_create_task(hass.config_entries.async_remove(entry.entry_id)) + hass.async_create_task( + hass.config_entries.async_remove(entry.entry_id), eager_start=True + ) return usb_dev = entry.data["device"] @@ -73,7 +75,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @callback def async_usb_scan_done() -> None: """Handle usb discovery started.""" - hass.async_create_task(_async_usb_scan_done(hass, entry)) + hass.async_create_task(_async_usb_scan_done(hass, entry), eager_start=True) unsub_usb = usb.async_register_initial_scan_callback(hass, async_usb_scan_done) entry.async_on_unload(unsub_usb) From b9281327c43822bb8a0bf972c96590cd6bc3bc45 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 3 Apr 2024 09:21:17 +0200 Subject: [PATCH 204/967] Use FlowResultType enum in config flow tests A-M (#114681) --- .../accuweather/test_config_flow.py | 2 +- tests/components/acmeda/test_config_flow.py | 2 +- tests/components/adax/test_config_flow.py | 8 +- tests/components/adguard/test_config_flow.py | 2 +- tests/components/aemet/test_config_flow.py | 2 +- tests/components/airly/test_config_flow.py | 2 +- tests/components/airnow/test_config_flow.py | 12 +- .../components/airthings/test_config_flow.py | 2 +- .../components/airtouch4/test_config_flow.py | 13 +- tests/components/airzone/test_config_flow.py | 2 +- .../aladdin_connect/test_config_flow.py | 4 +- .../androidtv_remote/test_config_flow.py | 8 +- tests/components/apple_tv/test_config_flow.py | 12 +- tests/components/aurora/test_config_flow.py | 8 +- .../aurora_abb_powerone/test_config_flow.py | 6 +- .../aussie_broadband/test_config_flow.py | 2 +- .../azure_event_hub/test_config_flow.py | 8 +- tests/components/baf/test_config_flow.py | 6 +- tests/components/blebox/test_config_flow.py | 4 +- tests/components/blink/test_config_flow.py | 24 ++-- tests/components/bond/test_config_flow.py | 47 +++---- .../components/bosch_shc/test_config_flow.py | 51 ++++---- tests/components/bring/test_config_flow.py | 2 +- .../components/broadlink/test_config_flow.py | 95 +++++++------- tests/components/brunt/test_config_flow.py | 2 +- .../components/buienradar/test_config_flow.py | 10 +- tests/components/caldav/test_config_flow.py | 4 +- tests/components/cast/test_config_flow.py | 18 +-- tests/components/cloud/test_config_flow.py | 5 +- tests/components/coinbase/test_config_flow.py | 23 ++-- tests/components/control4/test_config_flow.py | 19 +-- .../components/coolmaster/test_config_flow.py | 11 +- tests/components/denonavr/test_config_flow.py | 48 +++---- tests/components/devialet/test_config_flow.py | 6 +- .../devolo_home_control/test_config_flow.py | 4 +- tests/components/dnsip/test_config_flow.py | 2 +- tests/components/doorbird/test_config_flow.py | 14 +-- .../drop_connect/test_config_flow.py | 16 +-- tests/components/dsmr/test_config_flow.py | 46 +++---- tests/components/dynalite/test_config_flow.py | 13 +- tests/components/eafm/test_config_flow.py | 9 +- tests/components/elkm1/test_config_flow.py | 94 +++++++------- tests/components/emonitor/test_config_flow.py | 21 ++-- .../emulated_roku/test_config_flow.py | 5 +- .../enphase_envoy/test_config_flow.py | 41 +++--- .../environment_canada/test_config_flow.py | 2 +- tests/components/epson/test_config_flow.py | 9 +- .../components/faa_delays/test_config_flow.py | 8 +- tests/components/firmata/test_config_flow.py | 9 +- tests/components/fitbit/test_config_flow.py | 2 +- .../flick_electric/test_config_flow.py | 2 +- tests/components/flipr/test_config_flow.py | 8 +- tests/components/flo/test_config_flow.py | 7 +- tests/components/flume/test_config_flow.py | 17 +-- tests/components/flux_led/test_config_flow.py | 68 +++++----- tests/components/fyta/test_config_flow.py | 2 +- tests/components/generic/test_config_flow.py | 20 +-- tests/components/github/test_config_flow.py | 2 +- tests/components/google/test_config_flow.py | 54 ++++---- .../google_assistant_sdk/test_config_flow.py | 19 +-- .../google_mail/test_config_flow.py | 7 +- .../google_sheets/test_config_flow.py | 11 +- .../growatt_server/test_config_flow.py | 4 +- tests/components/guardian/test_config_flow.py | 4 +- tests/components/habitica/test_config_flow.py | 13 +- tests/components/harmony/test_config_flow.py | 14 +-- tests/components/hassio/test_config_flow.py | 5 +- .../here_travel_time/test_config_flow.py | 4 +- tests/components/hlk_sw16/test_config_flow.py | 17 +-- tests/components/homekit/test_config_flow.py | 8 +- .../homekit_controller/test_config_flow.py | 84 ++++++------- .../homematicip_cloud/test_config_flow.py | 19 +-- .../components/homewizard/test_config_flow.py | 4 +- tests/components/hue/test_config_flow.py | 69 +++++----- .../components/huisbaasje/test_config_flow.py | 2 +- .../husqvarna_automower/test_config_flow.py | 4 +- .../hvv_departures/test_config_flow.py | 10 +- .../components/iaqualink/test_config_flow.py | 11 +- tests/components/insteon/test_config_flow.py | 20 +-- .../intellifire/test_config_flow.py | 2 +- tests/components/ipma/test_config_flow.py | 6 +- tests/components/iss/test_config_flow.py | 2 +- tests/components/isy994/test_config_flow.py | 8 +- tests/components/jellyfin/test_config_flow.py | 28 ++--- tests/components/juicenet/test_config_flow.py | 13 +- tests/components/kmtronic/test_config_flow.py | 10 +- tests/components/knx/test_config_flow.py | 2 +- tests/components/kodi/test_config_flow.py | 69 +++++----- .../components/konnected/test_config_flow.py | 119 +++++++++--------- .../kostal_plenticore/test_config_flow.py | 17 +-- tests/components/kraken/test_config_flow.py | 9 +- tests/components/kulersky/test_config_flow.py | 13 +- .../lg_soundbar/test_config_flow.py | 39 +++--- tests/components/lifx/test_config_flow.py | 58 ++++----- tests/components/litejet/test_config_flow.py | 8 +- .../litterrobot/test_config_flow.py | 12 +- tests/components/lookin/test_config_flow.py | 12 +- .../lutron_caseta/test_config_flow.py | 44 +++---- tests/components/melcloud/test_config_flow.py | 10 +- tests/components/met/test_config_flow.py | 17 +-- .../met_eireann/test_config_flow.py | 4 +- .../components/metoffice/test_config_flow.py | 11 +- .../components/microbees/test_config_flow.py | 4 +- tests/components/mikrotik/test_config_flow.py | 14 +-- tests/components/mill/test_config_flow.py | 8 +- .../components/monoprice/test_config_flow.py | 8 +- .../motion_blinds/test_config_flow.py | 50 ++++---- .../components/motioneye/test_config_flow.py | 16 +-- tests/components/mqtt/test_config_flow.py | 62 ++++----- tests/components/mullvad/test_config_flow.py | 2 +- tests/components/mutesync/test_config_flow.py | 7 +- 111 files changed, 997 insertions(+), 961 deletions(-) diff --git a/tests/components/accuweather/test_config_flow.py b/tests/components/accuweather/test_config_flow.py index acac15204f9..bc75ef17309 100644 --- a/tests/components/accuweather/test_config_flow.py +++ b/tests/components/accuweather/test_config_flow.py @@ -113,7 +113,7 @@ async def test_integration_already_exists(hass: HomeAssistant) -> None: data=VALID_CONFIG, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/acmeda/test_config_flow.py b/tests/components/acmeda/test_config_flow.py index 1b79e37cab3..5227d283f25 100644 --- a/tests/components/acmeda/test_config_flow.py +++ b/tests/components/acmeda/test_config_flow.py @@ -146,5 +146,5 @@ async def test_already_configured(hass: HomeAssistant, mock_hub_discover) -> Non DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" diff --git a/tests/components/adax/test_config_flow.py b/tests/components/adax/test_config_flow.py index 40640c66143..579ca2019f8 100644 --- a/tests/components/adax/test_config_flow.py +++ b/tests/components/adax/test_config_flow.py @@ -58,7 +58,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == str(TEST_DATA["account_id"]) assert result3["data"] == { ACCOUNT_ID: TEST_DATA["account_id"], @@ -124,7 +124,7 @@ async def test_flow_entry_already_exists(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "abort" + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "already_configured" @@ -173,7 +173,7 @@ async def test_local_create_entry(hass: HomeAssistant) -> None: ) test_data[CONNECTION_TYPE] = LOCAL - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "8383838" assert result["data"] == { "connection_type": "Local", @@ -229,7 +229,7 @@ async def test_local_flow_entry_already_exists(hass: HomeAssistant) -> None: test_data, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/adguard/test_config_flow.py b/tests/components/adguard/test_config_flow.py index 3229a753699..d493962611f 100644 --- a/tests/components/adguard/test_config_flow.py +++ b/tests/components/adguard/test_config_flow.py @@ -116,7 +116,7 @@ async def test_integration_already_exists(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, ) assert result - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" diff --git a/tests/components/aemet/test_config_flow.py b/tests/components/aemet/test_config_flow.py index a9a2d45f618..45fec473396 100644 --- a/tests/components/aemet/test_config_flow.py +++ b/tests/components/aemet/test_config_flow.py @@ -127,7 +127,7 @@ async def test_form_duplicated_id( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/airly/test_config_flow.py b/tests/components/airly/test_config_flow.py index 96f4d95995b..7c0cac805d3 100644 --- a/tests/components/airly/test_config_flow.py +++ b/tests/components/airly/test_config_flow.py @@ -98,7 +98,7 @@ async def test_duplicate_error( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/airnow/test_config_flow.py b/tests/components/airnow/test_config_flow.py index 740adec4b00..b62cb43844b 100644 --- a/tests/components/airnow/test_config_flow.py +++ b/tests/components/airnow/test_config_flow.py @@ -35,7 +35,7 @@ async def test_form_invalid_auth(hass: HomeAssistant, config, setup_airnow) -> N DOMAIN, context={"source": config_entries.SOURCE_USER} ) result2 = await hass.config_entries.flow.async_configure(result["flow_id"], config) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -46,7 +46,7 @@ async def test_form_invalid_location(hass: HomeAssistant, config, setup_airnow) DOMAIN, context={"source": config_entries.SOURCE_USER} ) result2 = await hass.config_entries.flow.async_configure(result["flow_id"], config) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_location"} @@ -57,7 +57,7 @@ async def test_form_cannot_connect(hass: HomeAssistant, config, setup_airnow) -> DOMAIN, context={"source": config_entries.SOURCE_USER} ) result2 = await hass.config_entries.flow.async_configure(result["flow_id"], config) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -68,7 +68,7 @@ async def test_form_empty_result(hass: HomeAssistant, config, setup_airnow) -> N DOMAIN, context={"source": config_entries.SOURCE_USER} ) result2 = await hass.config_entries.flow.async_configure(result["flow_id"], config) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_location"} @@ -79,7 +79,7 @@ async def test_form_unexpected(hass: HomeAssistant, config, setup_airnow) -> Non DOMAIN, context={"source": config_entries.SOURCE_USER} ) result2 = await hass.config_entries.flow.async_configure(result["flow_id"], config) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -89,7 +89,7 @@ async def test_entry_already_exists(hass: HomeAssistant, config, config_entry) - DOMAIN, context={"source": config_entries.SOURCE_USER} ) result2 = await hass.config_entries.flow.async_configure(result["flow_id"], config) - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/airthings/test_config_flow.py b/tests/components/airthings/test_config_flow.py index ff4cdfa30d2..081e1bfd86d 100644 --- a/tests/components/airthings/test_config_flow.py +++ b/tests/components/airthings/test_config_flow.py @@ -121,5 +121,5 @@ async def test_flow_entry_already_exists(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=TEST_DATA ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/airtouch4/test_config_flow.py b/tests/components/airtouch4/test_config_flow.py index e5e3672f69d..d574531faa7 100644 --- a/tests/components/airtouch4/test_config_flow.py +++ b/tests/components/airtouch4/test_config_flow.py @@ -7,6 +7,7 @@ from airtouch4pyapi.airtouch import AirTouch, AirTouchAc, AirTouchGroup, AirTouc from homeassistant import config_entries from homeassistant.components.airtouch4.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType async def test_form(hass: HomeAssistant) -> None: @@ -14,7 +15,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None mock_ac = AirTouchAc() mock_groups = AirTouchGroup() @@ -39,7 +40,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "0.0.0.1" assert result2["data"] == { "host": "0.0.0.1", @@ -62,7 +63,7 @@ async def test_form_timeout(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "0.0.0.1"} ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -81,7 +82,7 @@ async def test_form_library_error_message(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "0.0.0.1"} ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -100,7 +101,7 @@ async def test_form_connection_refused(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "0.0.0.1"} ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -124,5 +125,5 @@ async def test_form_no_units(hass: HomeAssistant) -> None: result["flow_id"], {"host": "0.0.0.1"} ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "no_units"} diff --git a/tests/components/airzone/test_config_flow.py b/tests/components/airzone/test_config_flow.py index 090674d5fd2..072699c7a26 100644 --- a/tests/components/airzone/test_config_flow.py +++ b/tests/components/airzone/test_config_flow.py @@ -177,7 +177,7 @@ async def test_form_duplicated_id(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/aladdin_connect/test_config_flow.py b/tests/components/aladdin_connect/test_config_flow.py index d41b88de8e4..65b8b24a59d 100644 --- a/tests/components/aladdin_connect/test_config_flow.py +++ b/tests/components/aladdin_connect/test_config_flow.py @@ -116,7 +116,7 @@ async def test_form_already_configured( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER with patch( @@ -132,7 +132,7 @@ async def test_form_already_configured( ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/androidtv_remote/test_config_flow.py b/tests/components/androidtv_remote/test_config_flow.py index eb51f9465c3..8778630be8d 100644 --- a/tests/components/androidtv_remote/test_config_flow.py +++ b/tests/components/androidtv_remote/test_config_flow.py @@ -880,7 +880,7 @@ async def test_options_flow( # Trigger options flow, first time result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" data_schema = result["data_schema"].schema assert set(data_schema) == {"enable_ime"} @@ -889,7 +889,7 @@ async def test_options_flow( result["flow_id"], user_input={"enable_ime": False}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert mock_config_entry.options == {"enable_ime": False} await hass.async_block_till_done() @@ -902,7 +902,7 @@ async def test_options_flow( result["flow_id"], user_input={"enable_ime": False}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert mock_config_entry.options == {"enable_ime": False} await hass.async_block_till_done() @@ -915,7 +915,7 @@ async def test_options_flow( result["flow_id"], user_input={"enable_ime": True}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert mock_config_entry.options == {"enable_ime": True} await hass.async_block_till_done() diff --git a/tests/components/apple_tv/test_config_flow.py b/tests/components/apple_tv/test_config_flow.py index e7025890ec4..e7bfa68bdaf 100644 --- a/tests/components/apple_tv/test_config_flow.py +++ b/tests/components/apple_tv/test_config_flow.py @@ -136,7 +136,7 @@ async def test_user_adds_full_device(hass: HomeAssistant, full_device, pairing) result6 = await hass.config_entries.flow.async_configure( result["flow_id"], {"pin": 1234} ) - assert result6["type"] == "create_entry" + assert result6["type"] is FlowResultType.CREATE_ENTRY assert result6["data"] == { "address": "127.0.0.1", "credentials": { @@ -174,7 +174,7 @@ async def test_user_adds_dmap_device( result6 = await hass.config_entries.flow.async_configure( result["flow_id"], {"pin": 1234} ) - assert result6["type"] == "create_entry" + assert result6["type"] is FlowResultType.CREATE_ENTRY assert result6["data"] == { "address": "127.0.0.1", "credentials": {Protocol.DMAP.value: "dmap_creds"}, @@ -540,7 +540,7 @@ async def test_ignores_disabled_service( result3 = await hass.config_entries.flow.async_configure( result["flow_id"], {"pin": 1111} ) - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["data"] == { "address": "127.0.0.1", "credentials": { @@ -621,7 +621,7 @@ async def test_zeroconf_add_mrp_device( result3 = await hass.config_entries.flow.async_configure( result["flow_id"], {"pin": 1111} ) - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["data"] == { "address": "127.0.0.1", "credentials": {Protocol.MRP.value: "mrp_creds"}, @@ -651,7 +651,7 @@ async def test_zeroconf_add_dmap_device( assert result2["description_placeholders"] == {"protocol": "DMAP", "pin": "1111"} result3 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["data"] == { "address": "127.0.0.1", "credentials": {Protocol.DMAP.value: "dmap_creds"}, @@ -1224,7 +1224,7 @@ async def test_option_start_off(hass: HomeAssistant) -> None: result2 = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_START_OFF: True} ) - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options[CONF_START_OFF] diff --git a/tests/components/aurora/test_config_flow.py b/tests/components/aurora/test_config_flow.py index a6d6a67cf30..a91c4eb8bc9 100644 --- a/tests/components/aurora/test_config_flow.py +++ b/tests/components/aurora/test_config_flow.py @@ -23,7 +23,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -42,7 +42,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Aurora visibility" assert result2["data"] == DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -64,7 +64,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: DATA, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -85,7 +85,7 @@ async def test_with_unknown_error(hass: HomeAssistant) -> None: DATA, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} diff --git a/tests/components/aurora_abb_powerone/test_config_flow.py b/tests/components/aurora_abb_powerone/test_config_flow.py index 91ab362b5bc..9c27c14d633 100644 --- a/tests/components/aurora_abb_powerone/test_config_flow.py +++ b/tests/components/aurora_abb_powerone/test_config_flow.py @@ -31,7 +31,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -91,7 +91,7 @@ async def test_form_no_comports(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_serial_ports" @@ -107,7 +107,7 @@ async def test_form_invalid_com_ports(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( diff --git a/tests/components/aussie_broadband/test_config_flow.py b/tests/components/aussie_broadband/test_config_flow.py index bc7ed8b8167..6ee674ab0f4 100644 --- a/tests/components/aussie_broadband/test_config_flow.py +++ b/tests/components/aussie_broadband/test_config_flow.py @@ -233,5 +233,5 @@ async def test_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result7["type"] == "abort" + assert result7["type"] is FlowResultType.ABORT assert result7["reason"] == "reauth_successful" diff --git a/tests/components/azure_event_hub/test_config_flow.py b/tests/components/azure_event_hub/test_config_flow.py index 70914f0ee83..cedbc5b43d6 100644 --- a/tests/components/azure_event_hub/test_config_flow.py +++ b/tests/components/azure_event_hub/test_config_flow.py @@ -56,14 +56,14 @@ async def test_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( result["flow_id"], step1_config.copy(), ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == step_id result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], @@ -136,7 +136,7 @@ async def test_connection_error_sas( context={"source": config_entries.SOURCE_USER}, data=BASE_CONFIG_SAS.copy(), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None mock_get_eventhub_properties.side_effect = side_effect @@ -165,7 +165,7 @@ async def test_connection_error_cs( context={"source": config_entries.SOURCE_USER}, data=BASE_CONFIG_CS.copy(), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None mock_from_connection_string.return_value.get_eventhub_properties.side_effect = ( side_effect diff --git a/tests/components/baf/test_config_flow.py b/tests/components/baf/test_config_flow.py index cf91be0d400..765801d22cf 100644 --- a/tests/components/baf/test_config_flow.py +++ b/tests/components/baf/test_config_flow.py @@ -30,7 +30,7 @@ async def test_form_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -113,7 +113,7 @@ async def test_zeroconf_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "My Fan" assert result2["data"] == {CONF_IP_ADDRESS: "127.0.0.1"} assert len(mock_setup_entry.mock_calls) == 1 @@ -182,7 +182,7 @@ async def test_user_flow_is_not_blocked_by_discovery(hass: HomeAssistant) -> Non result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( diff --git a/tests/components/blebox/test_config_flow.py b/tests/components/blebox/test_config_flow.py index e94553e10cf..7213b33555c 100644 --- a/tests/components/blebox/test_config_flow.py +++ b/tests/components/blebox/test_config_flow.py @@ -66,7 +66,7 @@ async def test_flow_works( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_init( @@ -75,7 +75,7 @@ async def test_flow_works( data={config_flow.CONF_HOST: "172.2.3.4", config_flow.CONF_PORT: 80}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "My gate controller" assert result["data"] == { config_flow.CONF_HOST: "172.2.3.4", diff --git a/tests/components/blink/test_config_flow.py b/tests/components/blink/test_config_flow.py index 10b34fa532c..82ea847dcf2 100644 --- a/tests/components/blink/test_config_flow.py +++ b/tests/components/blink/test_config_flow.py @@ -17,7 +17,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -37,7 +37,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "blink" assert result2["result"].unique_id == "blink@example.com" assert result2["data"] == { @@ -72,7 +72,7 @@ async def test_form_2fa(hass: HomeAssistant) -> None: {"username": "blink@example.com", "password": "example"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "2fa" with ( @@ -98,7 +98,7 @@ async def test_form_2fa(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "blink" assert result3["result"].unique_id == "blink@example.com" assert len(mock_setup_entry.mock_calls) == 1 @@ -123,7 +123,7 @@ async def test_form_2fa_connect_error(hass: HomeAssistant) -> None: {"username": "blink@example.com", "password": "example"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "2fa" with ( @@ -149,7 +149,7 @@ async def test_form_2fa_connect_error(hass: HomeAssistant) -> None: result2["flow_id"], {"pin": "1234"} ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": "cannot_connect"} @@ -172,7 +172,7 @@ async def test_form_2fa_invalid_key(hass: HomeAssistant) -> None: {"username": "blink@example.com", "password": "example"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "2fa" with ( @@ -200,7 +200,7 @@ async def test_form_2fa_invalid_key(hass: HomeAssistant) -> None: result2["flow_id"], {"pin": "1234"} ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": "invalid_access_token"} @@ -223,7 +223,7 @@ async def test_form_2fa_unknown_error(hass: HomeAssistant) -> None: {"username": "blink@example.com", "password": "example"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "2fa" with ( @@ -249,7 +249,7 @@ async def test_form_2fa_unknown_error(hass: HomeAssistant) -> None: result2["flow_id"], {"pin": "1234"} ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": "unknown"} @@ -267,7 +267,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: result["flow_id"], {"username": "blink@example.com", "password": "example"} ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -285,7 +285,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: result["flow_id"], {"username": "blink@example.com", "password": "example"} ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/bond/test_config_flow.py b/tests/components/bond/test_config_flow.py index bfe61c536d9..d61ed4844a1 100644 --- a/tests/components/bond/test_config_flow.py +++ b/tests/components/bond/test_config_flow.py @@ -15,6 +15,7 @@ from homeassistant.components.bond.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .common import ( patch_bond_bridge, @@ -35,7 +36,7 @@ async def test_user_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -53,7 +54,7 @@ async def test_user_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "bond-name" assert result2["data"] == { CONF_HOST: "some host", @@ -68,7 +69,7 @@ async def test_user_form_with_non_bridge(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -90,7 +91,7 @@ async def test_user_form_with_non_bridge(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "New Fan" assert result2["data"] == { CONF_HOST: "some host", @@ -117,7 +118,7 @@ async def test_user_form_invalid_auth(hass: HomeAssistant) -> None: {CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -137,7 +138,7 @@ async def test_user_form_cannot_connect(hass: HomeAssistant) -> None: {CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -157,7 +158,7 @@ async def test_user_form_old_firmware(hass: HomeAssistant) -> None: {CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "old_firmware"} @@ -205,7 +206,7 @@ async def test_user_form_one_entry_per_device_allowed(hass: HomeAssistant) -> No ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" assert len(mock_setup_entry.mock_calls) == 0 @@ -228,7 +229,7 @@ async def test_zeroconf_form(hass: HomeAssistant) -> None: type="mock_type", ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -243,7 +244,7 @@ async def test_zeroconf_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "bond-name" assert result2["data"] == { CONF_HOST: "127.0.0.1", @@ -270,7 +271,7 @@ async def test_zeroconf_form_token_unavailable(hass: HomeAssistant) -> None: ), ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -285,7 +286,7 @@ async def test_zeroconf_form_token_unavailable(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "bond-name" assert result2["data"] == { CONF_HOST: "127.0.0.1", @@ -312,7 +313,7 @@ async def test_zeroconf_form_token_times_out(hass: HomeAssistant) -> None: ), ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -327,7 +328,7 @@ async def test_zeroconf_form_token_times_out(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "bond-name" assert result2["data"] == { CONF_HOST: "127.0.0.1", @@ -359,7 +360,7 @@ async def test_zeroconf_form_with_token_available(hass: HomeAssistant) -> None: ), ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with _patch_async_setup_entry() as mock_setup_entry: @@ -369,7 +370,7 @@ async def test_zeroconf_form_with_token_available(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "discovered-name" assert result2["data"] == { CONF_HOST: "127.0.0.1", @@ -403,7 +404,7 @@ async def test_zeroconf_form_with_token_available_name_unavailable( ), ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with _patch_async_setup_entry() as mock_setup_entry: @@ -413,7 +414,7 @@ async def test_zeroconf_form_with_token_available_name_unavailable( ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "ZXXX12345" assert result2["data"] == { CONF_HOST: "127.0.0.1", @@ -448,7 +449,7 @@ async def test_zeroconf_already_configured(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data["host"] == "127.0.0.2" assert len(mock_setup_entry.mock_calls) == 1 @@ -486,7 +487,7 @@ async def test_zeroconf_in_setup_retry_state(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data["host"] == "127.0.0.2" assert len(mock_setup_entry.mock_calls) == 1 @@ -533,7 +534,7 @@ async def test_zeroconf_already_configured_refresh_token(hass: HomeAssistant) -> ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data["host"] == "127.0.0.2" assert entry.data[CONF_ACCESS_TOKEN] == "discovered-token" @@ -572,7 +573,7 @@ async def test_zeroconf_already_configured_no_reload_same_host( ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(mock_setup_entry.mock_calls) == 0 @@ -618,7 +619,7 @@ async def _help_test_form_unexpected_error( result["flow_id"], user_input ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/bosch_shc/test_config_flow.py b/tests/components/bosch_shc/test_config_flow.py index 2fe2b98308d..b3a28151c93 100644 --- a/tests/components/bosch_shc/test_config_flow.py +++ b/tests/components/bosch_shc/test_config_flow.py @@ -16,6 +16,7 @@ from homeassistant.components import zeroconf from homeassistant.components.bosch_shc.config_flow import write_tls_asset from homeassistant.components.bosch_shc.const import CONF_SHC_CERT, CONF_SHC_KEY, DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -40,7 +41,7 @@ async def test_form_user(hass: HomeAssistant, mock_zeroconf: None) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -65,7 +66,7 @@ async def test_form_user(hass: HomeAssistant, mock_zeroconf: None) -> None: {"host": "1.1.1.1"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "credentials" assert result2["errors"] == {} @@ -92,7 +93,7 @@ async def test_form_user(hass: HomeAssistant, mock_zeroconf: None) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "shc012345" assert result3["data"] == { "host": "1.1.1.1", @@ -125,7 +126,7 @@ async def test_form_get_info_connection_error( }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -147,7 +148,7 @@ async def test_form_get_info_exception(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "unknown"} @@ -179,7 +180,7 @@ async def test_form_pairing_error(hass: HomeAssistant, mock_zeroconf: None) -> N {"host": "1.1.1.1"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "credentials" assert result2["errors"] == {} @@ -193,7 +194,7 @@ async def test_form_pairing_error(hass: HomeAssistant, mock_zeroconf: None) -> N ) await hass.async_block_till_done() - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "credentials" assert result3["errors"] == {"base": "pairing_failed"} @@ -225,7 +226,7 @@ async def test_form_user_invalid_auth(hass: HomeAssistant, mock_zeroconf: None) {"host": "1.1.1.1"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "credentials" assert result2["errors"] == {} @@ -251,7 +252,7 @@ async def test_form_user_invalid_auth(hass: HomeAssistant, mock_zeroconf: None) ) await hass.async_block_till_done() - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "credentials" assert result3["errors"] == {"base": "invalid_auth"} @@ -285,7 +286,7 @@ async def test_form_validate_connection_error( {"host": "1.1.1.1"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "credentials" assert result2["errors"] == {} @@ -311,7 +312,7 @@ async def test_form_validate_connection_error( ) await hass.async_block_till_done() - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "credentials" assert result3["errors"] == {"base": "cannot_connect"} @@ -345,7 +346,7 @@ async def test_form_validate_session_error( {"host": "1.1.1.1"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "credentials" assert result2["errors"] == {} @@ -371,7 +372,7 @@ async def test_form_validate_session_error( ) await hass.async_block_till_done() - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "credentials" assert result3["errors"] == {"base": "session_error"} @@ -405,7 +406,7 @@ async def test_form_validate_exception( {"host": "1.1.1.1"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "credentials" assert result2["errors"] == {} @@ -431,7 +432,7 @@ async def test_form_validate_exception( ) await hass.async_block_till_done() - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "credentials" assert result3["errors"] == {"base": "unknown"} @@ -471,7 +472,7 @@ async def test_form_already_configured( {"host": "1.1.1.1"}, ) - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" # Test config entry got updated with latest IP @@ -502,7 +503,7 @@ async def test_zeroconf(hass: HomeAssistant, mock_zeroconf: None) -> None: data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm_discovery" assert result["errors"] == {} context = next( @@ -516,7 +517,7 @@ async def test_zeroconf(hass: HomeAssistant, mock_zeroconf: None) -> None: result["flow_id"], {}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "credentials" with ( @@ -544,7 +545,7 @@ async def test_zeroconf(hass: HomeAssistant, mock_zeroconf: None) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "shc012345" assert result3["data"] == { "host": "1.1.1.1", @@ -588,7 +589,7 @@ async def test_zeroconf_already_configured( context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" # Test config entry got updated with latest IP @@ -607,7 +608,7 @@ async def test_zeroconf_cannot_connect( data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -626,7 +627,7 @@ async def test_zeroconf_not_bosch_shc(hass: HomeAssistant, mock_zeroconf: None) ), context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_bosch_shc" @@ -650,7 +651,7 @@ async def test_reauth(hass: HomeAssistant, mock_zeroconf: None) -> None: context={"source": config_entries.SOURCE_REAUTH}, data=mock_config.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with ( @@ -674,7 +675,7 @@ async def test_reauth(hass: HomeAssistant, mock_zeroconf: None) -> None: {"host": "2.2.2.2"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "credentials" assert result2["errors"] == {} @@ -701,7 +702,7 @@ async def test_reauth(hass: HomeAssistant, mock_zeroconf: None) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "abort" + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" assert mock_config.data["host"] == "2.2.2.2" diff --git a/tests/components/bring/test_config_flow.py b/tests/components/bring/test_config_flow.py index be5ce48680c..351ba533101 100644 --- a/tests/components/bring/test_config_flow.py +++ b/tests/components/bring/test_config_flow.py @@ -86,7 +86,7 @@ async def test_flow_user_init_data_unknown_error_and_recover( user_input=MOCK_DATA_STEP, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].title == MOCK_DATA_STEP["email"] assert result["data"] == MOCK_DATA_STEP diff --git a/tests/components/broadlink/test_config_flow.py b/tests/components/broadlink/test_config_flow.py index 143742d3a9a..2def8c0b3b9 100644 --- a/tests/components/broadlink/test_config_flow.py +++ b/tests/components/broadlink/test_config_flow.py @@ -11,6 +11,7 @@ from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.components.broadlink.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import get_device @@ -42,7 +43,7 @@ async def test_flow_user_works(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -52,7 +53,7 @@ async def test_flow_user_works(hass: HomeAssistant) -> None: {"host": device.host, "timeout": device.timeout}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "finish" assert result["errors"] == {} @@ -61,7 +62,7 @@ async def test_flow_user_works(hass: HomeAssistant) -> None: {"name": device.name}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == device.name assert result["data"] == device.get_entry_data() @@ -93,7 +94,7 @@ async def test_flow_user_already_in_progress(hass: HomeAssistant) -> None: {"host": device.host, "timeout": device.timeout}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -120,7 +121,7 @@ async def test_flow_user_mac_already_configured(hass: HomeAssistant) -> None: {"host": device.host, "timeout": device.timeout}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert dict(mock_entry.data) == device.get_entry_data() @@ -139,7 +140,7 @@ async def test_flow_user_invalid_ip_address(hass: HomeAssistant) -> None: {"host": "0.0.0.1"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_host"} @@ -156,7 +157,7 @@ async def test_flow_user_invalid_hostname(hass: HomeAssistant) -> None: {"host": "pancakemaster.local"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_host"} @@ -175,7 +176,7 @@ async def test_flow_user_device_not_found(hass: HomeAssistant) -> None: {"host": device.host}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -195,7 +196,7 @@ async def test_flow_user_device_not_supported(hass: HomeAssistant) -> None: {"host": device.host}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -211,7 +212,7 @@ async def test_flow_user_network_unreachable(hass: HomeAssistant) -> None: {"host": "192.168.1.32"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -228,7 +229,7 @@ async def test_flow_user_os_error(hass: HomeAssistant) -> None: {"host": "192.168.1.32"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} @@ -249,7 +250,7 @@ async def test_flow_auth_authentication_error(hass: HomeAssistant) -> None: {"host": device.host, "timeout": device.timeout}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reset" assert result["errors"] == {"base": "invalid_auth"} @@ -270,7 +271,7 @@ async def test_flow_auth_network_timeout(hass: HomeAssistant) -> None: {"host": device.host}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "cannot_connect"} @@ -291,7 +292,7 @@ async def test_flow_auth_firmware_error(hass: HomeAssistant) -> None: {"host": device.host}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "unknown"} @@ -312,7 +313,7 @@ async def test_flow_auth_network_unreachable(hass: HomeAssistant) -> None: {"host": device.host}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "cannot_connect"} @@ -333,7 +334,7 @@ async def test_flow_auth_os_error(hass: HomeAssistant) -> None: {"host": device.host}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "unknown"} @@ -365,7 +366,7 @@ async def test_flow_reset_works(hass: HomeAssistant) -> None: {"name": device.name}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == device.name assert result["data"] == device.get_entry_data() @@ -386,7 +387,7 @@ async def test_flow_unlock_works(hass: HomeAssistant) -> None: {"host": device.host, "timeout": device.timeout}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "unlock" assert result["errors"] == {} @@ -400,7 +401,7 @@ async def test_flow_unlock_works(hass: HomeAssistant) -> None: {"name": device.name}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == device.name assert result["data"] == device.get_entry_data() @@ -430,7 +431,7 @@ async def test_flow_unlock_network_timeout(hass: HomeAssistant) -> None: {"unlock": True}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "unlock" assert result["errors"] == {"base": "cannot_connect"} @@ -457,7 +458,7 @@ async def test_flow_unlock_firmware_error(hass: HomeAssistant) -> None: {"unlock": True}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "unlock" assert result["errors"] == {"base": "unknown"} @@ -484,7 +485,7 @@ async def test_flow_unlock_network_unreachable(hass: HomeAssistant) -> None: {"unlock": True}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "unlock" assert result["errors"] == {"base": "cannot_connect"} @@ -511,7 +512,7 @@ async def test_flow_unlock_os_error(hass: HomeAssistant) -> None: {"unlock": True}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "unlock" assert result["errors"] == {"base": "unknown"} @@ -542,7 +543,7 @@ async def test_flow_do_not_unlock(hass: HomeAssistant) -> None: {"name": device.name}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == device.name assert result["data"] == device.get_entry_data() @@ -561,7 +562,7 @@ async def test_flow_import_works(hass: HomeAssistant) -> None: data={"host": device.host}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "finish" assert result["errors"] == {} @@ -570,7 +571,7 @@ async def test_flow_import_works(hass: HomeAssistant) -> None: {"name": device.name}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == device.name assert result["data"]["host"] == device.host assert result["data"]["mac"] == device.mac @@ -595,7 +596,7 @@ async def test_flow_import_already_in_progress(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=data ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -613,7 +614,7 @@ async def test_flow_import_host_already_configured(hass: HomeAssistant) -> None: data={"host": device.host}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -636,7 +637,7 @@ async def test_flow_import_mac_already_configured(hass: HomeAssistant) -> None: data={"host": device.host}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_entry.data["host"] == device.host @@ -654,7 +655,7 @@ async def test_flow_import_device_not_found(hass: HomeAssistant) -> None: data={"host": "192.168.1.32"}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -670,7 +671,7 @@ async def test_flow_import_device_not_supported(hass: HomeAssistant) -> None: data={"host": device.host}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -683,7 +684,7 @@ async def test_flow_import_invalid_ip_address(hass: HomeAssistant) -> None: data={"host": "0.0.0.1"}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_host" @@ -696,7 +697,7 @@ async def test_flow_import_invalid_hostname(hass: HomeAssistant) -> None: data={"host": "hotdog.local"}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_host" @@ -709,7 +710,7 @@ async def test_flow_import_network_unreachable(hass: HomeAssistant) -> None: data={"host": "192.168.1.64"}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -722,7 +723,7 @@ async def test_flow_import_os_error(hass: HomeAssistant) -> None: data={"host": "192.168.1.64"}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -740,7 +741,7 @@ async def test_flow_reauth_works(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=data ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reset" mock_api = device.get_mock_api() @@ -751,7 +752,7 @@ async def test_flow_reauth_works(hass: HomeAssistant) -> None: {"host": device.host, "timeout": device.timeout}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert dict(mock_entry.data) == device.get_entry_data() @@ -785,7 +786,7 @@ async def test_flow_reauth_invalid_host(hass: HomeAssistant) -> None: {"host": device.host, "timeout": device.timeout}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_host"} @@ -819,7 +820,7 @@ async def test_flow_reauth_valid_host(hass: HomeAssistant) -> None: {"host": device.host, "timeout": device.timeout}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_entry.data["host"] == device.host @@ -846,7 +847,7 @@ async def test_dhcp_can_finish(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "finish" result2 = await hass.config_entries.flow.async_configure( @@ -855,7 +856,7 @@ async def test_dhcp_can_finish(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Living Room" assert result2["data"] == { "host": "1.2.3.4", @@ -880,7 +881,7 @@ async def test_dhcp_fails_to_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -899,7 +900,7 @@ async def test_dhcp_unreachable(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -918,7 +919,7 @@ async def test_dhcp_connect_unknown_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -939,7 +940,7 @@ async def test_dhcp_device_not_supported(hass: HomeAssistant) -> None: ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -964,7 +965,7 @@ async def test_dhcp_already_exists(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -989,6 +990,6 @@ async def test_dhcp_updates_host(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_entry.data["host"] == "4.5.6.7" diff --git a/tests/components/brunt/test_config_flow.py b/tests/components/brunt/test_config_flow.py index 6c4d928c0ca..2796882a3c1 100644 --- a/tests/components/brunt/test_config_flow.py +++ b/tests/components/brunt/test_config_flow.py @@ -24,7 +24,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( diff --git a/tests/components/buienradar/test_config_flow.py b/tests/components/buienradar/test_config_flow.py index 9fb0d9c4c48..316cb90a348 100644 --- a/tests/components/buienradar/test_config_flow.py +++ b/tests/components/buienradar/test_config_flow.py @@ -22,7 +22,7 @@ async def test_config_flow_setup_(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -31,7 +31,7 @@ async def test_config_flow_setup_(hass: HomeAssistant) -> None: {CONF_LATITUDE: TEST_LATITUDE, CONF_LONGITUDE: TEST_LONGITUDE}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"{TEST_LATITUDE},{TEST_LONGITUDE}" assert result["data"] == { CONF_LATITUDE: TEST_LATITUDE, @@ -55,7 +55,7 @@ async def test_config_flow_already_configured_weather(hass: HomeAssistant) -> No DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -64,7 +64,7 @@ async def test_config_flow_already_configured_weather(hass: HomeAssistant) -> No {CONF_LATITUDE: TEST_LATITUDE, CONF_LONGITUDE: TEST_LONGITUDE}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -85,7 +85,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( diff --git a/tests/components/caldav/test_config_flow.py b/tests/components/caldav/test_config_flow.py index e7cbf9dd7ea..c6d5552c874 100644 --- a/tests/components/caldav/test_config_flow.py +++ b/tests/components/caldav/test_config_flow.py @@ -113,7 +113,7 @@ async def test_reauth_success( "entry_id": config_entry.entry_id, }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result2 = await hass.config_entries.flow.async_configure( @@ -154,7 +154,7 @@ async def test_reauth_failure( "entry_id": config_entry.entry_id, }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" dav_client.return_value.principal.side_effect = DAVError diff --git a/tests/components/cast/test_config_flow.py b/tests/components/cast/test_config_flow.py index ab24aa4df5c..2c0c36d6632 100644 --- a/tests/components/cast/test_config_flow.py +++ b/tests/components/cast/test_config_flow.py @@ -55,7 +55,7 @@ async def test_single_instance(hass: HomeAssistant, source) -> None: result = await hass.config_entries.flow.async_init( "cast", context={"source": source} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -64,13 +64,13 @@ async def test_user_setup(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( "cast", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) users = await hass.auth.async_get_users() assert next(user for user in users if user.name == CAST_USER_NAME) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].data == { "ignore_cec": [], "known_hosts": [], @@ -84,7 +84,7 @@ async def test_user_setup_options(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( "cast", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"known_hosts": "192.168.0.1, , 192.168.0.2 "} @@ -92,7 +92,7 @@ async def test_user_setup_options(hass: HomeAssistant) -> None: users = await hass.auth.async_get_users() assert next(user for user in users if user.name == CAST_USER_NAME) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].data == { "ignore_cec": [], "known_hosts": ["192.168.0.1", "192.168.0.2"], @@ -106,13 +106,13 @@ async def test_zeroconf_setup(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( "cast", context={"source": config_entries.SOURCE_ZEROCONF} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) users = await hass.auth.async_get_users() assert next(user for user in users if user.name == CAST_USER_NAME) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].data == { "ignore_cec": [], "known_hosts": [], @@ -132,7 +132,7 @@ async def test_zeroconf_setup_onboarding(hass: HomeAssistant) -> None: users = await hass.auth.async_get_users() assert next(user for user in users if user.name == CAST_USER_NAME) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].data == { "ignore_cec": [], "known_hosts": [], @@ -278,7 +278,7 @@ async def test_known_hosts(hass: HomeAssistant, castbrowser_mock) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], {"known_hosts": "192.168.0.1, 192.168.0.2"} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done(wait_background_tasks=True) config_entry = hass.config_entries.async_entries("cast")[0] diff --git a/tests/components/cloud/test_config_flow.py b/tests/components/cloud/test_config_flow.py index 6b506d6b883..9f6e1762482 100644 --- a/tests/components/cloud/test_config_flow.py +++ b/tests/components/cloud/test_config_flow.py @@ -4,6 +4,7 @@ from unittest.mock import patch from homeassistant.components.cloud.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -23,7 +24,7 @@ async def test_config_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "system"} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Home Assistant Cloud" assert result["data"] == {} await hass.async_block_till_done() @@ -40,5 +41,5 @@ async def test_multiple_entries(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "system"} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/coinbase/test_config_flow.py b/tests/components/coinbase/test_config_flow.py index 79b0115bc7c..f213392bb1e 100644 --- a/tests/components/coinbase/test_config_flow.py +++ b/tests/components/coinbase/test_config_flow.py @@ -16,6 +16,7 @@ from homeassistant.components.coinbase.const import ( ) from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .common import ( init_mock_coinbase, @@ -32,7 +33,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -59,7 +60,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Test User" assert result2["data"] == {CONF_API_KEY: "123456", CONF_API_TOKEN: "AbCDeF"} assert len(mock_setup_entry.mock_calls) == 1 @@ -95,7 +96,7 @@ async def test_form_invalid_auth( }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} assert "Coinbase rejected API credentials due to an unknown error" in caplog.text @@ -117,7 +118,7 @@ async def test_form_invalid_auth( }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth_key"} assert "Coinbase rejected API credentials due to an invalid API key" in caplog.text @@ -139,7 +140,7 @@ async def test_form_invalid_auth( }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth_secret"} assert ( "Coinbase rejected API credentials due to an invalid API secret" in caplog.text @@ -164,7 +165,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -186,7 +187,7 @@ async def test_form_catch_all_exception(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -219,7 +220,7 @@ async def test_option_form(hass: HomeAssistant) -> None: CONF_EXCHANGE_PRECISION: 5, }, ) - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() assert len(mock_update_listener.mock_calls) == 1 @@ -249,7 +250,7 @@ async def test_form_bad_account_currency(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "currency_unavailable"} @@ -277,7 +278,7 @@ async def test_form_bad_exchange_rate(hass: HomeAssistant) -> None: CONF_EXCHANGE_PRECISION: 5, }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "exchange_rate_unavailable"} @@ -311,5 +312,5 @@ async def test_option_catch_all_exception(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/control4/test_config_flow.py b/tests/components/control4/test_config_flow.py index 8ec6df063e5..d1faf2da6c6 100644 --- a/tests/components/control4/test_config_flow.py +++ b/tests/components/control4/test_config_flow.py @@ -15,6 +15,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -48,7 +49,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} c4_account = _get_mock_c4_account() @@ -77,7 +78,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "control4_model_00AA00AA00AA" assert result2["data"] == { CONF_HOST: "1.1.1.1", @@ -107,7 +108,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -130,7 +131,7 @@ async def test_form_unexpected_exception(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -159,7 +160,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -170,14 +171,14 @@ async def test_option_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + 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_SCAN_INTERVAL: 4}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_SCAN_INTERVAL: 4, } @@ -190,13 +191,13 @@ async def test_option_flow_defaults(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + 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"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL, } diff --git a/tests/components/coolmaster/test_config_flow.py b/tests/components/coolmaster/test_config_flow.py index ef7828e126d..83a074815b5 100644 --- a/tests/components/coolmaster/test_config_flow.py +++ b/tests/components/coolmaster/test_config_flow.py @@ -6,6 +6,7 @@ from homeassistant import config_entries from homeassistant.components.coolmaster.config_flow import AVAILABLE_MODES from homeassistant.components.coolmaster.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType def _flow_data(): @@ -21,7 +22,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -39,7 +40,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "1.1.1.1" assert result2["data"] == { "host": "1.1.1.1", @@ -64,7 +65,7 @@ async def test_form_timeout(hass: HomeAssistant) -> None: result["flow_id"], _flow_data() ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -82,7 +83,7 @@ async def test_form_connection_refused(hass: HomeAssistant) -> None: result["flow_id"], _flow_data() ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -100,5 +101,5 @@ async def test_form_no_units(hass: HomeAssistant) -> None: result["flow_id"], _flow_data() ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "no_units"} diff --git a/tests/components/denonavr/test_config_flow.py b/tests/components/denonavr/test_config_flow.py index f675b188fb9..324b795052c 100644 --- a/tests/components/denonavr/test_config_flow.py +++ b/tests/components/denonavr/test_config_flow.py @@ -92,7 +92,7 @@ async def test_config_flow_manual_host_success(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -101,7 +101,7 @@ async def test_config_flow_manual_host_success(hass: HomeAssistant) -> None: {CONF_HOST: TEST_HOST}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_NAME assert result["data"] == { CONF_HOST: TEST_HOST, @@ -122,7 +122,7 @@ async def test_config_flow_manual_discover_1_success(hass: HomeAssistant) -> Non DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -135,7 +135,7 @@ async def test_config_flow_manual_discover_1_success(hass: HomeAssistant) -> Non {}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_NAME assert result["data"] == { CONF_HOST: TEST_HOST, @@ -156,7 +156,7 @@ async def test_config_flow_manual_discover_2_success(hass: HomeAssistant) -> Non DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -169,7 +169,7 @@ async def test_config_flow_manual_discover_2_success(hass: HomeAssistant) -> Non {}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select" assert result["errors"] == {} @@ -178,7 +178,7 @@ async def test_config_flow_manual_discover_2_success(hass: HomeAssistant) -> Non {"select_host": TEST_HOST2}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_NAME assert result["data"] == { CONF_HOST: TEST_HOST2, @@ -199,7 +199,7 @@ async def test_config_flow_manual_discover_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -212,7 +212,7 @@ async def test_config_flow_manual_discover_error(hass: HomeAssistant) -> None: {}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "discovery_error"} @@ -226,7 +226,7 @@ async def test_config_flow_manual_host_no_serial(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -239,7 +239,7 @@ async def test_config_flow_manual_host_no_serial(hass: HomeAssistant) -> None: {CONF_HOST: TEST_HOST}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_NAME assert result["data"] == { CONF_HOST: TEST_HOST, @@ -259,7 +259,7 @@ async def test_config_flow_manual_host_connection_error(hass: HomeAssistant) -> DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -278,7 +278,7 @@ async def test_config_flow_manual_host_connection_error(hass: HomeAssistant) -> {CONF_HOST: TEST_HOST}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -291,7 +291,7 @@ async def test_config_flow_manual_host_no_device_info(hass: HomeAssistant) -> No DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -304,7 +304,7 @@ async def test_config_flow_manual_host_no_device_info(hass: HomeAssistant) -> No {CONF_HOST: TEST_HOST}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -325,7 +325,7 @@ async def test_config_flow_ssdp(hass: HomeAssistant) -> None: ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -333,7 +333,7 @@ async def test_config_flow_ssdp(hass: HomeAssistant) -> None: {}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_NAME assert result["data"] == { CONF_HOST: TEST_HOST, @@ -365,7 +365,7 @@ async def test_config_flow_ssdp_not_denon(hass: HomeAssistant) -> None: ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_denonavr_manufacturer" @@ -387,7 +387,7 @@ async def test_config_flow_ssdp_missing_info(hass: HomeAssistant) -> None: ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_denonavr_missing" @@ -411,7 +411,7 @@ async def test_config_flow_ssdp_ignored_model(hass: HomeAssistant) -> None: ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_denonavr_manufacturer" @@ -471,7 +471,7 @@ async def test_config_flow_manual_host_no_serial_double_config( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -484,7 +484,7 @@ async def test_config_flow_manual_host_no_serial_double_config( {CONF_HOST: TEST_HOST}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_NAME assert result["data"] == { CONF_HOST: TEST_HOST, @@ -498,7 +498,7 @@ async def test_config_flow_manual_host_no_serial_double_config( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -511,5 +511,5 @@ async def test_config_flow_manual_host_no_serial_double_config( {CONF_HOST: TEST_HOST}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/devialet/test_config_flow.py b/tests/components/devialet/test_config_flow.py index 200a2673913..6fec4e927dc 100644 --- a/tests/components/devialet/test_config_flow.py +++ b/tests/components/devialet/test_config_flow.py @@ -111,7 +111,7 @@ async def test_zeroconf_devialet( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=MOCK_ZEROCONF_DATA ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM with patch( "homeassistant.components.devialet.async_setup_entry", @@ -123,7 +123,7 @@ async def test_zeroconf_devialet( ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Livingroom" assert result2["data"] == { CONF_HOST: HOST, @@ -140,7 +140,7 @@ async def test_async_step_confirm( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=MOCK_ZEROCONF_DATA ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" aioclient_mock.get( diff --git a/tests/components/devolo_home_control/test_config_flow.py b/tests/components/devolo_home_control/test_config_flow.py index d26da474e39..48f9bf31f4f 100644 --- a/tests/components/devolo_home_control/test_config_flow.py +++ b/tests/components/devolo_home_control/test_config_flow.py @@ -95,7 +95,7 @@ async def test_form_advanced_options(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "devolo Home Control" assert result2["data"] == { "username": "test-username", @@ -286,7 +286,7 @@ async def _setup(hass: HomeAssistant, result: FlowResult) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "devolo Home Control" assert result2["data"] == { "username": "test-username", diff --git a/tests/components/dnsip/test_config_flow.py b/tests/components/dnsip/test_config_flow.py index 54ce26b15a8..29c8d81dd2d 100644 --- a/tests/components/dnsip/test_config_flow.py +++ b/tests/components/dnsip/test_config_flow.py @@ -32,7 +32,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["data_schema"] == DATA_SCHEMA assert result["errors"] == {} diff --git a/tests/components/doorbird/test_config_flow.py b/tests/components/doorbird/test_config_flow.py index 5d73c0785a4..cd4ddccda87 100644 --- a/tests/components/doorbird/test_config_flow.py +++ b/tests/components/doorbird/test_config_flow.py @@ -72,7 +72,7 @@ async def test_user_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "1.2.3.4" assert result2["data"] == { "host": "1.2.3.4", @@ -100,7 +100,7 @@ async def test_form_zeroconf_wrong_oui(hass: HomeAssistant) -> None: type="mock_type", ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_doorbird_device" @@ -120,7 +120,7 @@ async def test_form_zeroconf_link_local_ignored(hass: HomeAssistant) -> None: type="mock_type", ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "link_local_address" @@ -147,7 +147,7 @@ async def test_form_zeroconf_ipv4_address(hass: HomeAssistant) -> None: type="mock_type", ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == "4.4.4.4" @@ -168,7 +168,7 @@ async def test_form_zeroconf_non_ipv4_ignored(hass: HomeAssistant) -> None: type="mock_type", ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_ipv4_address" @@ -219,7 +219,7 @@ async def test_form_zeroconf_correct_oui(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "1.2.3.4" assert result2["data"] == { "host": "1.2.3.4", @@ -307,7 +307,7 @@ async def test_form_user_invalid_auth(hass: HomeAssistant) -> None: VALID_CONFIG, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} diff --git a/tests/components/drop_connect/test_config_flow.py b/tests/components/drop_connect/test_config_flow.py index 4785ea9348f..8cd765b46b4 100644 --- a/tests/components/drop_connect/test_config_flow.py +++ b/tests/components/drop_connect/test_config_flow.py @@ -24,7 +24,7 @@ async def test_mqtt_setup(hass: HomeAssistant, mqtt_mock: MqttMockHAClient) -> N data=discovery_info, ) assert result is not None - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -61,7 +61,7 @@ async def test_duplicate(hass: HomeAssistant, mqtt_mock: MqttMockHAClient) -> No data=discovery_info, ) assert result is not None - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -78,7 +78,7 @@ async def test_duplicate(hass: HomeAssistant, mqtt_mock: MqttMockHAClient) -> No data=discovery_info, ) assert result is not None - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_discovery_info" @@ -100,7 +100,7 @@ async def test_mqtt_setup_incomplete_payload( data=discovery_info, ) assert result is not None - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_discovery_info" @@ -122,7 +122,7 @@ async def test_mqtt_setup_bad_json( data=discovery_info, ) assert result is not None - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_discovery_info" @@ -144,7 +144,7 @@ async def test_mqtt_setup_bad_topic( data=discovery_info, ) assert result is not None - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_discovery_info" @@ -166,7 +166,7 @@ async def test_mqtt_setup_no_payload( data=discovery_info, ) assert result is not None - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_discovery_info" @@ -175,5 +175,5 @@ async def test_user_setup(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( "drop_connect", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" diff --git a/tests/components/dsmr/test_config_flow.py b/tests/components/dsmr/test_config_flow.py index a8eea28d748..791797f7dcd 100644 --- a/tests/components/dsmr/test_config_flow.py +++ b/tests/components/dsmr/test_config_flow.py @@ -39,7 +39,7 @@ async def test_setup_network( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None @@ -48,7 +48,7 @@ async def test_setup_network( {"type": "Network"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_network" assert result["errors"] == {} @@ -70,7 +70,7 @@ async def test_setup_network( "protocol": "dsmr_protocol", } - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "10.10.0.1:1234" assert result["data"] == {**entry_data, **SERIAL_DATA} @@ -87,7 +87,7 @@ async def test_setup_network_rfxtrx( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None @@ -96,7 +96,7 @@ async def test_setup_network_rfxtrx( {"type": "Network"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_network" assert result["errors"] == {} @@ -121,7 +121,7 @@ async def test_setup_network_rfxtrx( "protocol": "rfxtrx_dsmr_protocol", } - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "10.10.0.1:1234" assert result["data"] == {**entry_data, **SERIAL_DATA} @@ -237,7 +237,7 @@ async def test_setup_serial_rfxtrx( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None @@ -246,7 +246,7 @@ async def test_setup_serial_rfxtrx( {"type": "Serial"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_serial" assert result["errors"] == {} @@ -266,7 +266,7 @@ async def test_setup_serial_rfxtrx( "protocol": "rfxtrx_dsmr_protocol", } - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == port.device assert result["data"] == {**entry_data, **SERIAL_DATA} @@ -280,7 +280,7 @@ async def test_setup_serial_manual( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None @@ -289,7 +289,7 @@ async def test_setup_serial_manual( {"type": "Serial"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_serial" assert result["errors"] == {} @@ -298,7 +298,7 @@ async def test_setup_serial_manual( {"port": "Enter Manually", "dsmr_version": "2.2"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_serial_manual_path" assert result["errors"] is None @@ -314,7 +314,7 @@ async def test_setup_serial_manual( "protocol": "dsmr_protocol", } - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "/dev/ttyUSB0" assert result["data"] == {**entry_data, **SERIAL_DATA} @@ -338,7 +338,7 @@ async def test_setup_serial_fail( side_effect=chain([serial.SerialException], repeat(DEFAULT)), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None @@ -347,7 +347,7 @@ async def test_setup_serial_fail( {"type": "Serial"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_serial" assert result["errors"] == {} @@ -360,7 +360,7 @@ async def test_setup_serial_fail( {"port": port.device, "dsmr_version": "2.2"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_serial" assert result["errors"] == {"base": "cannot_connect"} @@ -398,7 +398,7 @@ async def test_setup_serial_timeout( ) rfxtrx_protocol.wait_closed = first_timeout_wait_closed - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None @@ -407,7 +407,7 @@ async def test_setup_serial_timeout( {"type": "Serial"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_serial" assert result["errors"] == {} @@ -416,7 +416,7 @@ async def test_setup_serial_timeout( result["flow_id"], {"port": port.device, "dsmr_version": "2.2"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_serial" assert result["errors"] == {"base": "cannot_communicate"} @@ -442,7 +442,7 @@ async def test_setup_serial_wrong_telegram( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None @@ -451,7 +451,7 @@ async def test_setup_serial_wrong_telegram( {"type": "Serial"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_serial" assert result["errors"] == {} @@ -463,7 +463,7 @@ async def test_setup_serial_wrong_telegram( {"port": port.device, "dsmr_version": "2.2"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_serial" assert result["errors"] == {"base": "cannot_communicate"} @@ -485,7 +485,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( diff --git a/tests/components/dynalite/test_config_flow.py b/tests/components/dynalite/test_config_flow.py index 724cb616deb..bdbd03faa22 100644 --- a/tests/components/dynalite/test_config_flow.py +++ b/tests/components/dynalite/test_config_flow.py @@ -8,6 +8,7 @@ from homeassistant import config_entries from homeassistant.components import dynalite from homeassistant.const import CONF_PORT from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers.issue_registry import ( IssueSeverity, async_get as async_get_issue_registry, @@ -86,7 +87,7 @@ async def test_existing(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_IMPORT}, data={dynalite.CONF_HOST: host}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -116,7 +117,7 @@ async def test_existing_update(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert mock_dyn_dev().configure.call_count == 2 assert mock_dyn_dev().configure.mock_calls[1][1][0]["port"] == port2 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -136,7 +137,7 @@ async def test_two_entries(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_IMPORT}, data={dynalite.CONF_HOST: host2}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].state == config_entries.ConfigEntryState.LOADED @@ -148,7 +149,7 @@ async def test_setup_user(hass): dynalite.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None @@ -161,7 +162,7 @@ async def test_setup_user(hass): {"host": host, "port": port}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].state == config_entries.ConfigEntryState.LOADED assert result["title"] == host assert result["data"] == { @@ -188,5 +189,5 @@ async def test_setup_user_existing_host(hass): {"host": host, "port": 1234}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/eafm/test_config_flow.py b/tests/components/eafm/test_config_flow.py index e8f86154e67..9773a8b619e 100644 --- a/tests/components/eafm/test_config_flow.py +++ b/tests/components/eafm/test_config_flow.py @@ -8,6 +8,7 @@ from voluptuous.error import Invalid from homeassistant import config_entries from homeassistant.components.eafm import const from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType async def test_flow_no_discovered_stations( @@ -18,7 +19,7 @@ async def test_flow_no_discovered_stations( result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_stations" @@ -31,7 +32,7 @@ async def test_flow_invalid_station(hass: HomeAssistant, mock_get_stations) -> N result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM with pytest.raises(Invalid): result = await hass.config_entries.flow.async_configure( @@ -53,14 +54,14 @@ async def test_flow_works( result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM with patch("homeassistant.components.eafm.async_setup_entry", return_value=True): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"station": "My station"} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "My station" assert result["data"] == { "station": "L12345", diff --git a/tests/components/elkm1/test_config_flow.py b/tests/components/elkm1/test_config_flow.py index c361063d7ea..e56bb5f4699 100644 --- a/tests/components/elkm1/test_config_flow.py +++ b/tests/components/elkm1/test_config_flow.py @@ -66,7 +66,7 @@ async def test_form_user_with_secure_elk_no_discovery(hass: HomeAssistant) -> No ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual_connection" @@ -95,7 +95,7 @@ async def test_form_user_with_secure_elk_no_discovery(hass: HomeAssistant) -> No ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "ElkM1" assert result2["data"] == { "auto_configure": True, @@ -123,7 +123,7 @@ async def test_form_user_with_insecure_elk_skip_discovery(hass: HomeAssistant) - ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual_connection" @@ -152,7 +152,7 @@ async def test_form_user_with_insecure_elk_skip_discovery(hass: HomeAssistant) - ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "ElkM1" assert result2["data"] == { "auto_configure": True, @@ -180,7 +180,7 @@ async def test_form_user_with_insecure_elk_no_discovery(hass: HomeAssistant) -> ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual_connection" @@ -209,7 +209,7 @@ async def test_form_user_with_insecure_elk_no_discovery(hass: HomeAssistant) -> ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "ElkM1" assert result2["data"] == { "auto_configure": True, @@ -237,7 +237,7 @@ async def test_form_user_with_insecure_elk_times_out(hass: HomeAssistant) -> Non ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual_connection" @@ -285,7 +285,7 @@ async def test_form_user_with_secure_elk_no_discovery_ip_already_configured( ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual_connection" @@ -317,7 +317,7 @@ async def test_form_user_with_secure_elk_with_discovery(hass: HomeAssistant) -> ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -350,7 +350,7 @@ async def test_form_user_with_secure_elk_with_discovery(hass: HomeAssistant) -> ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "ElkM1 ddeeff" assert result3["data"] == { "auto_configure": True, @@ -375,7 +375,7 @@ async def test_form_user_with_secure_elk_with_discovery_pick_manual( ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -411,7 +411,7 @@ async def test_form_user_with_secure_elk_with_discovery_pick_manual( ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "ElkM1" assert result3["data"] == { "auto_configure": True, @@ -436,7 +436,7 @@ async def test_form_user_with_secure_elk_with_discovery_pick_manual_direct_disco ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -472,7 +472,7 @@ async def test_form_user_with_secure_elk_with_discovery_pick_manual_direct_disco ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "ElkM1 ddeeff" assert result3["data"] == { "auto_configure": True, @@ -495,7 +495,7 @@ async def test_form_user_with_tls_elk_no_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual_connection" @@ -524,7 +524,7 @@ async def test_form_user_with_tls_elk_no_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "ElkM1" assert result2["data"] == { "auto_configure": True, @@ -546,7 +546,7 @@ async def test_form_user_with_non_secure_elk_no_discovery(hass: HomeAssistant) - ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual_connection" @@ -573,7 +573,7 @@ async def test_form_user_with_non_secure_elk_no_discovery(hass: HomeAssistant) - ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "guest_house" assert result2["data"] == { "auto_configure": True, @@ -595,7 +595,7 @@ async def test_form_user_with_serial_elk_no_discovery(hass: HomeAssistant) -> No ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual_connection" @@ -622,7 +622,7 @@ async def test_form_user_with_serial_elk_no_discovery(hass: HomeAssistant) -> No ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "ElkM1" assert result2["data"] == { "auto_configure": True, @@ -667,7 +667,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -703,7 +703,7 @@ async def test_unknown_exception(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -730,7 +730,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {CONF_PASSWORD: "invalid_auth"} @@ -757,7 +757,7 @@ async def test_form_invalid_auth_no_password(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {CONF_PASSWORD: "invalid_auth"} @@ -807,7 +807,7 @@ async def test_form_import(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "ohana" assert result["data"] == { @@ -877,7 +877,7 @@ async def test_form_import_device_discovered(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "ohana" assert result["result"].unique_id == MOCK_MAC assert result["data"] == { @@ -929,7 +929,7 @@ async def test_form_import_non_secure_device_discovered(hass: HomeAssistant) -> ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "ohana" assert result["result"].unique_id == MOCK_MAC assert result["data"] == { @@ -973,7 +973,7 @@ async def test_form_import_non_secure_non_stanadard_port_device_discovered( ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "ohana" assert result["result"].unique_id == MOCK_MAC assert result["data"] == { @@ -1007,7 +1007,7 @@ async def test_form_import_non_secure_device_discovered_invalid_auth( ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_auth" @@ -1189,7 +1189,7 @@ async def test_discovered_by_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "ElkM1 ddeeff" assert result2["data"] == { "auto_configure": True, @@ -1239,7 +1239,7 @@ async def test_discovered_by_discovery_non_standard_port(hass: HomeAssistant) -> ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "ElkM1 ddeeff" assert result2["data"] == { "auto_configure": True, @@ -1310,7 +1310,7 @@ async def test_discovered_by_dhcp_udp_responds(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "ElkM1 ddeeff" assert result2["data"] == { "auto_configure": True, @@ -1359,7 +1359,7 @@ async def test_discovered_by_dhcp_udp_responds_with_nonsecure_port( ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "ElkM1 ddeeff" assert result2["data"] == { "auto_configure": True, @@ -1412,7 +1412,7 @@ async def test_discovered_by_dhcp_udp_responds_existing_config_entry( ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "ElkM1 ddeeff" assert result2["data"] == { "auto_configure": True, @@ -1450,7 +1450,7 @@ async def test_multiple_instances_with_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert not result["errors"] assert result["step_id"] == "user" @@ -1483,7 +1483,7 @@ async def test_multiple_instances_with_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "ElkM1 ddeeff" assert result3["data"] == { "auto_configure": True, @@ -1502,7 +1502,7 @@ async def test_multiple_instances_with_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert not result["errors"] assert result["step_id"] == "user" @@ -1532,7 +1532,7 @@ async def test_multiple_instances_with_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "ElkM1 ddeefe" assert result3["data"] == { "auto_configure": True, @@ -1551,7 +1551,7 @@ async def test_multiple_instances_with_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual_connection" @@ -1575,7 +1575,7 @@ async def test_multiple_instances_with_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "guest_house" assert result2["data"] == { "auto_configure": True, @@ -1599,7 +1599,7 @@ async def test_multiple_instances_with_tls_v12(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert not result["errors"] assert result["step_id"] == "user" @@ -1612,7 +1612,7 @@ async def test_multiple_instances_with_tls_v12(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert not result["errors"] assert result2["step_id"] == "discovered_connection" with ( @@ -1636,7 +1636,7 @@ async def test_multiple_instances_with_tls_v12(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "ElkM1 ddeeff" assert result3["data"] == { "auto_configure": True, @@ -1655,7 +1655,7 @@ async def test_multiple_instances_with_tls_v12(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert not result["errors"] assert result["step_id"] == "user" @@ -1686,7 +1686,7 @@ async def test_multiple_instances_with_tls_v12(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "ElkM1 ddeefe" assert result3["data"] == { "auto_configure": True, @@ -1705,7 +1705,7 @@ async def test_multiple_instances_with_tls_v12(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual_connection" @@ -1731,7 +1731,7 @@ async def test_multiple_instances_with_tls_v12(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "guest_house" assert result2["data"] == { "auto_configure": True, diff --git a/tests/components/emonitor/test_config_flow.py b/tests/components/emonitor/test_config_flow.py index 07809a83d89..e77ebcc08b0 100644 --- a/tests/components/emonitor/test_config_flow.py +++ b/tests/components/emonitor/test_config_flow.py @@ -10,6 +10,7 @@ from homeassistant.components import dhcp from homeassistant.components.emonitor.const import DOMAIN from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -32,7 +33,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -53,7 +54,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Emonitor DDEEFF" assert result2["data"] == { "host": "1.2.3.4", @@ -78,7 +79,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -99,7 +100,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {CONF_HOST: "cannot_connect"} @@ -117,7 +118,7 @@ async def test_dhcp_can_confirm(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["description_placeholders"] == { "host": "1.2.3.4", @@ -134,7 +135,7 @@ async def test_dhcp_can_confirm(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Emonitor DDEEFF" assert result2["data"] == { "host": "1.2.3.4", @@ -156,7 +157,7 @@ async def test_dhcp_fails_to_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -181,7 +182,7 @@ async def test_dhcp_already_exists(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -198,7 +199,7 @@ async def test_user_unique_id_already_exists(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -219,5 +220,5 @@ async def test_user_unique_id_already_exists(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/emulated_roku/test_config_flow.py b/tests/components/emulated_roku/test_config_flow.py index 700adbf0039..45cb83b4fea 100644 --- a/tests/components/emulated_roku/test_config_flow.py +++ b/tests/components/emulated_roku/test_config_flow.py @@ -3,6 +3,7 @@ from homeassistant import config_entries from homeassistant.components.emulated_roku import config_flow from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -15,7 +16,7 @@ async def test_flow_works(hass: HomeAssistant, mock_get_source_ip) -> None: data={"name": "Emulated Roku Test", "listen_port": 8060}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Emulated Roku Test" assert result["data"] == {"name": "Emulated Roku Test", "listen_port": 8060} @@ -34,5 +35,5 @@ async def test_flow_already_registered_entry( data={"name": "Emulated Roku Test", "listen_port": 8062}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/enphase_envoy/test_config_flow.py b/tests/components/enphase_envoy/test_config_flow.py index 9000cf92e0e..7af0cd584a4 100644 --- a/tests/components/enphase_envoy/test_config_flow.py +++ b/tests/components/enphase_envoy/test_config_flow.py @@ -11,6 +11,7 @@ from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.enphase_envoy.const import DOMAIN, PLATFORMS from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType async def test_form(hass: HomeAssistant, config, setup_enphase_envoy) -> None: @@ -18,7 +19,7 @@ async def test_form(hass: HomeAssistant, config, setup_enphase_envoy) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -30,7 +31,7 @@ async def test_form(hass: HomeAssistant, config, setup_enphase_envoy) -> None: }, ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Envoy 1234" assert result2["data"] == { "host": "1.1.1.1", @@ -48,7 +49,7 @@ async def test_user_no_serial_number( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -60,7 +61,7 @@ async def test_user_no_serial_number( }, ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Envoy" assert result2["data"] == { "host": "1.1.1.1", @@ -78,7 +79,7 @@ async def test_user_fetching_serial_fails( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -90,7 +91,7 @@ async def test_user_fetching_serial_fails( }, ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Envoy" assert result2["data"] == { "host": "1.1.1.1", @@ -120,7 +121,7 @@ async def test_form_invalid_auth(hass: HomeAssistant, setup_enphase_envoy) -> No }, ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -142,7 +143,7 @@ async def test_form_cannot_connect(hass: HomeAssistant, setup_enphase_envoy) -> }, ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -164,7 +165,7 @@ async def test_form_unknown_error(hass: HomeAssistant, setup_enphase_envoy) -> N }, ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -193,7 +194,7 @@ async def test_zeroconf_pre_token_firmware( type="mock_type", ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert _get_schema_default(result["data_schema"].schema, "username") == "installer" @@ -207,7 +208,7 @@ async def test_zeroconf_pre_token_firmware( }, ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Envoy 1234" assert result2["result"].unique_id == "1234" assert result2["data"] == { @@ -235,7 +236,7 @@ async def test_zeroconf_token_firmware( type="mock_type", ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert _get_schema_default(result["data_schema"].schema, "username") == "" @@ -248,7 +249,7 @@ async def test_zeroconf_token_firmware( }, ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Envoy 1234" assert result2["result"].unique_id == "1234" assert result2["data"] == { @@ -278,7 +279,7 @@ async def test_form_host_already_exists( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} # existing config @@ -295,7 +296,7 @@ async def test_form_host_already_exists( "password": "wrong-password", }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} # still original config after failure @@ -313,7 +314,7 @@ async def test_form_host_already_exists( }, ) await hass.async_block_till_done() - assert result3["type"] == "abort" + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" # updated config with new ip and changed pw @@ -340,7 +341,7 @@ async def test_zeroconf_serial_already_exists( ), ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data["host"] == "4.4.4.4" @@ -364,7 +365,7 @@ async def test_zeroconf_serial_already_exists_ignores_ipv6( ), ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_ipv4_address" assert config_entry.data["host"] == "1.1.1.1" @@ -389,7 +390,7 @@ async def test_zeroconf_host_already_exists( ), ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.unique_id == "1234" @@ -414,7 +415,7 @@ async def test_reauth(hass: HomeAssistant, config_entry, setup_enphase_envoy) -> }, ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" diff --git a/tests/components/environment_canada/test_config_flow.py b/tests/components/environment_canada/test_config_flow.py index aa1297571b5..3571c74cdcc 100644 --- a/tests/components/environment_canada/test_config_flow.py +++ b/tests/components/environment_canada/test_config_flow.py @@ -123,7 +123,7 @@ async def test_exception_handling(hass: HomeAssistant, error) -> None: {}, ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": base_error} diff --git a/tests/components/epson/test_config_flow.py b/tests/components/epson/test_config_flow.py index c6ca921df0f..d485a4bfdef 100644 --- a/tests/components/epson/test_config_flow.py +++ b/tests/components/epson/test_config_flow.py @@ -8,6 +8,7 @@ from homeassistant import config_entries from homeassistant.components.epson.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_NAME, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType async def test_form(hass: HomeAssistant) -> None: @@ -17,7 +18,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == config_entries.SOURCE_USER with ( @@ -40,7 +41,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "test-epson" assert result2["data"] == {CONF_HOST: "1.1.1.1"} assert len(mock_setup_entry.mock_calls) == 1 @@ -61,7 +62,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: {CONF_HOST: "1.1.1.1", CONF_NAME: "test-epson"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -80,5 +81,5 @@ async def test_form_powered_off(hass: HomeAssistant) -> None: {CONF_HOST: "1.1.1.1", CONF_NAME: "test-epson"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "powered_off"} diff --git a/tests/components/faa_delays/test_config_flow.py b/tests/components/faa_delays/test_config_flow.py index 61e5ffb8e6b..4420bc73632 100644 --- a/tests/components/faa_delays/test_config_flow.py +++ b/tests/components/faa_delays/test_config_flow.py @@ -26,7 +26,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -44,7 +44,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "test" assert result2["data"] == { "id": "test", @@ -80,7 +80,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -98,5 +98,5 @@ async def test_form_unexpected_exception(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/firmata/test_config_flow.py b/tests/components/firmata/test_config_flow.py index a3c8ca7e728..0ef98a27bb6 100644 --- a/tests/components/firmata/test_config_flow.py +++ b/tests/components/firmata/test_config_flow.py @@ -8,6 +8,7 @@ from homeassistant import config_entries from homeassistant.components.firmata.const import CONF_SERIAL_PORT, DOMAIN from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType async def test_import_cannot_connect_pymata(hass: HomeAssistant) -> None: @@ -23,7 +24,7 @@ async def test_import_cannot_connect_pymata(hass: HomeAssistant) -> None: data={CONF_SERIAL_PORT: "/dev/nonExistent"}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -40,7 +41,7 @@ async def test_import_cannot_connect_serial(hass: HomeAssistant) -> None: data={CONF_SERIAL_PORT: "/dev/nonExistent"}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -57,7 +58,7 @@ async def test_import_cannot_connect_serial_timeout(hass: HomeAssistant) -> None data={CONF_SERIAL_PORT: "/dev/nonExistent"}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -79,7 +80,7 @@ async def test_import(hass: HomeAssistant) -> None: data={CONF_SERIAL_PORT: "/dev/nonExistent"}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "serial-/dev/nonExistent" assert result["data"] == { CONF_NAME: "serial-/dev/nonExistent", diff --git a/tests/components/fitbit/test_config_flow.py b/tests/components/fitbit/test_config_flow.py index 2fd431176b9..843a85dec68 100644 --- a/tests/components/fitbit/test_config_flow.py +++ b/tests/components/fitbit/test_config_flow.py @@ -480,7 +480,7 @@ async def test_reauth_flow( "entry_id": config_entry.entry_id, }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( diff --git a/tests/components/flick_electric/test_config_flow.py b/tests/components/flick_electric/test_config_flow.py index 1b56aaf6376..1b3ed1de34d 100644 --- a/tests/components/flick_electric/test_config_flow.py +++ b/tests/components/flick_electric/test_config_flow.py @@ -29,7 +29,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( diff --git a/tests/components/flipr/test_config_flow.py b/tests/components/flipr/test_config_flow.py index 60dcc15a701..b99e6af7383 100644 --- a/tests/components/flipr/test_config_flow.py +++ b/tests/components/flipr/test_config_flow.py @@ -47,7 +47,7 @@ async def test_invalid_credential(hass: HomeAssistant, mock_setup) -> None: }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -129,7 +129,7 @@ async def test_no_flip_id(hass: HomeAssistant, mock_setup) -> None: ) assert result["step_id"] == "user" - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "no_flipr_id_found"} assert len(mock_flipr_client.mock_calls) == 1 @@ -148,7 +148,7 @@ async def test_http_errors(hass: HomeAssistant, mock_setup) -> None: }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} with patch( @@ -165,5 +165,5 @@ async def test_http_errors(hass: HomeAssistant, mock_setup) -> None: }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "unknown"} diff --git a/tests/components/flo/test_config_flow.py b/tests/components/flo/test_config_flow.py index f5a730a2056..99f8f315fb2 100644 --- a/tests/components/flo/test_config_flow.py +++ b/tests/components/flo/test_config_flow.py @@ -9,6 +9,7 @@ from homeassistant import config_entries from homeassistant.components.flo.const import DOMAIN from homeassistant.const import CONTENT_TYPE_JSON from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .common import TEST_EMAIL_ADDRESS, TEST_PASSWORD, TEST_TOKEN, TEST_USER_ID @@ -21,7 +22,7 @@ async def test_form(hass: HomeAssistant, aioclient_mock_fixture) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -31,7 +32,7 @@ async def test_form(hass: HomeAssistant, aioclient_mock_fixture) -> None: result["flow_id"], {"username": TEST_USER_ID, "password": TEST_PASSWORD} ) - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_USER_ID assert result2["data"] == {"username": TEST_USER_ID, "password": TEST_PASSWORD} await hass.async_block_till_done() @@ -68,5 +69,5 @@ async def test_form_cannot_connect( result["flow_id"], {"username": "test-username", "password": "test-password"} ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/flume/test_config_flow.py b/tests/components/flume/test_config_flow.py index 8fa66c03258..706cee44739 100644 --- a/tests/components/flume/test_config_flow.py +++ b/tests/components/flume/test_config_flow.py @@ -13,6 +13,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -29,7 +30,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} mock_flume_device_list = _get_mocked_flume_device_list() @@ -59,7 +60,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "test-username" assert result2["data"] == { CONF_USERNAME: "test-username", @@ -96,7 +97,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"password": "invalid_auth"} @@ -125,7 +126,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -147,7 +148,7 @@ async def test_reauth(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_REAUTH, "unique_id": "test@test.org"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with ( @@ -167,7 +168,7 @@ async def test_reauth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"password": "invalid_auth"} with ( @@ -187,7 +188,7 @@ async def test_reauth(hass: HomeAssistant) -> None: }, ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": "cannot_connect"} mock_flume_device_list = _get_mocked_flume_device_list() @@ -214,5 +215,5 @@ async def test_reauth(hass: HomeAssistant) -> None: ) assert mock_setup_entry.called - assert result4["type"] == "abort" + assert result4["type"] is FlowResultType.ABORT assert result4["reason"] == "reauth_successful" diff --git a/tests/components/flux_led/test_config_flow.py b/tests/components/flux_led/test_config_flow.py index a4ba42ed629..d95bc99f097 100644 --- a/tests/components/flux_led/test_config_flow.py +++ b/tests/components/flux_led/test_config_flow.py @@ -55,13 +55,13 @@ async def test_discovery(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -69,13 +69,13 @@ async def test_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -91,7 +91,7 @@ async def test_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == DEFAULT_ENTRY_TITLE assert result3["data"] == { CONF_MINOR_VERSION: 4, @@ -111,7 +111,7 @@ async def test_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -119,7 +119,7 @@ async def test_discovery(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "no_devices_found" @@ -130,13 +130,13 @@ async def test_discovery_legacy(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -144,13 +144,13 @@ async def test_discovery_legacy(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -166,7 +166,7 @@ async def test_discovery_legacy(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == DEFAULT_ENTRY_TITLE assert result3["data"] == { CONF_MINOR_VERSION: 4, @@ -186,7 +186,7 @@ async def test_discovery_legacy(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -194,7 +194,7 @@ async def test_discovery_legacy(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "no_devices_found" @@ -212,7 +212,7 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -220,7 +220,7 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -229,7 +229,7 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -237,7 +237,7 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -249,7 +249,7 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No result3 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_DEVICE: MAC_ADDRESS} ) - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == DEFAULT_ENTRY_TITLE assert result3["data"] == { CONF_MINOR_VERSION: 4, @@ -270,7 +270,7 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -278,7 +278,7 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "no_devices_found" @@ -292,7 +292,7 @@ async def test_discovery_no_device(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "no_devices_found" @@ -301,7 +301,7 @@ async def test_manual_working_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -312,7 +312,7 @@ async def test_manual_working_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -327,7 +327,7 @@ async def test_manual_working_discovery(hass: HomeAssistant) -> None: result["flow_id"], {CONF_HOST: IP_ADDRESS} ) await hass.async_block_till_done() - assert result4["type"] == "create_entry" + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == DEFAULT_ENTRY_TITLE assert result4["data"] == { CONF_MINOR_VERSION: 4, @@ -351,7 +351,7 @@ async def test_manual_working_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -360,7 +360,7 @@ async def test_manual_no_discovery_data(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -375,7 +375,7 @@ async def test_manual_no_discovery_data(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_HOST: IP_ADDRESS, CONF_MODEL_NUM: MODEL_NUM, @@ -446,7 +446,7 @@ async def test_discovered_by_discovery(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { CONF_MINOR_VERSION: 4, CONF_HOST: IP_ADDRESS, @@ -485,7 +485,7 @@ async def test_discovered_by_dhcp_udp_responds(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { CONF_MINOR_VERSION: 4, CONF_HOST: IP_ADDRESS, @@ -524,7 +524,7 @@ async def test_discovered_by_dhcp_no_udp_response(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { CONF_HOST: IP_ADDRESS, CONF_MODEL_NUM: MODEL_NUM, @@ -559,7 +559,7 @@ async def test_discovered_by_dhcp_partial_udp_response_fallback_tcp( result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { CONF_HOST: IP_ADDRESS, CONF_MODEL_NUM: MODEL_NUM, @@ -703,7 +703,7 @@ async def test_options(hass: HomeAssistant) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" user_input = { @@ -716,7 +716,7 @@ async def test_options(hass: HomeAssistant) -> None: result["flow_id"], user_input ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == user_input assert result2["data"] == config_entry.options assert hass.states.get("light.bulb_rgbcw_ddeeff") is not None diff --git a/tests/components/fyta/test_config_flow.py b/tests/components/fyta/test_config_flow.py index 93b83caa379..60e6fc76c5b 100644 --- a/tests/components/fyta/test_config_flow.py +++ b/tests/components/fyta/test_config_flow.py @@ -40,7 +40,7 @@ async def test_user_flow( ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == USERNAME assert result2["data"] == {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py index 751361d47dd..f22bb4d93da 100644 --- a/tests/components/generic/test_config_flow.py +++ b/tests/components/generic/test_config_flow.py @@ -127,7 +127,7 @@ async def test_form_only_stillimage( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} data = TESTDATA.copy() @@ -501,7 +501,7 @@ async def test_form_image_http_exceptions( ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == expected_message @@ -518,7 +518,7 @@ async def test_form_stream_invalidimage( ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"still_image_url": "invalid_still_image"} @@ -535,7 +535,7 @@ async def test_form_stream_invalidimage2( ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"still_image_url": "unable_still_load_no_image"} @@ -552,7 +552,7 @@ async def test_form_stream_invalidimage3( ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"still_image_url": "invalid_still_image"} @@ -571,7 +571,7 @@ async def test_form_stream_timeout(hass: HomeAssistant, fakeimg_png, user_flow) user_flow["flow_id"], TESTDATA, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"stream_source": "timeout"} @@ -588,7 +588,7 @@ async def test_form_stream_worker_error( user_flow["flow_id"], TESTDATA, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"stream_source": "Some message"} @@ -606,7 +606,7 @@ async def test_form_stream_permission_error( user_flow["flow_id"], TESTDATA, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"stream_source": "stream_not_permitted"} @@ -623,7 +623,7 @@ async def test_form_no_route_to_host( user_flow["flow_id"], TESTDATA, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"stream_source": "stream_no_route_to_host"} @@ -640,7 +640,7 @@ async def test_form_stream_io_error( user_flow["flow_id"], TESTDATA, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"stream_source": "stream_io_error"} diff --git a/tests/components/github/test_config_flow.py b/tests/components/github/test_config_flow.py index 882ed88edb2..a721298c129 100644 --- a/tests/components/github/test_config_flow.py +++ b/tests/components/github/test_config_flow.py @@ -276,7 +276,7 @@ async def test_options_flow( result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( diff --git a/tests/components/google/test_config_flow.py b/tests/components/google/test_config_flow.py index c27808c24aa..12af97c8604 100644 --- a/tests/components/google/test_config_flow.py +++ b/tests/components/google/test_config_flow.py @@ -151,7 +151,7 @@ async def test_full_flow_application_creds( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == "progress" + assert result.get("type") is FlowResultType.SHOW_PROGRESS assert result.get("step_id") == "auth" assert "description_placeholders" in result assert "url" in result["description_placeholders"] @@ -167,7 +167,7 @@ async def test_full_flow_application_creds( flow_id=result["flow_id"] ) - assert result.get("type") == "create_entry" + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == EMAIL_ADDRESS assert "data" in result data = result["data"] @@ -213,7 +213,7 @@ async def test_code_error( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "oauth_error" @@ -233,7 +233,7 @@ async def test_timeout_error( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "timeout_connect" @@ -252,7 +252,7 @@ async def test_expired_after_exchange( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == "progress" + assert result.get("type") is FlowResultType.SHOW_PROGRESS assert result.get("step_id") == "auth" assert "description_placeholders" in result assert "url" in result["description_placeholders"] @@ -267,7 +267,7 @@ async def test_expired_after_exchange( await hass.async_block_till_done() result = await hass.config_entries.flow.async_configure(flow_id=result["flow_id"]) - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "code_expired" @@ -287,7 +287,7 @@ async def test_exchange_error( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == "progress" + assert result.get("type") is FlowResultType.SHOW_PROGRESS assert result.get("step_id") == "auth" assert "description_placeholders" in result assert "url" in result["description_placeholders"] @@ -309,7 +309,7 @@ async def test_exchange_error( # Status has not updated, will retry result = await hass.config_entries.flow.async_configure(flow_id=result["flow_id"]) - assert result.get("type") == "progress" + assert result.get("type") is FlowResultType.SHOW_PROGRESS assert result.get("step_id") == "auth" # Run another tick, which attempts credential exchange again @@ -323,7 +323,7 @@ async def test_exchange_error( flow_id=result["flow_id"] ) - assert result.get("type") == "create_entry" + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == EMAIL_ADDRESS assert "data" in result data = result["data"] @@ -373,7 +373,7 @@ async def test_duplicate_config_entries( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == "progress" + assert result.get("type") is FlowResultType.SHOW_PROGRESS assert result.get("step_id") == "auth" assert "description_placeholders" in result assert "url" in result["description_placeholders"] @@ -383,7 +383,7 @@ async def test_duplicate_config_entries( await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA) await hass.async_block_till_done() result = await hass.config_entries.flow.async_configure(flow_id=result["flow_id"]) - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -415,7 +415,7 @@ async def test_multiple_config_entries( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == "progress" + assert result.get("type") is FlowResultType.SHOW_PROGRESS assert result.get("step_id") == "auth" assert "description_placeholders" in result assert "url" in result["description_placeholders"] @@ -430,7 +430,7 @@ async def test_multiple_config_entries( result = await hass.config_entries.flow.async_configure( flow_id=result["flow_id"] ) - assert result.get("type") == "create_entry" + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == "another-email@example.com" assert len(mock_setup.mock_calls) == 1 @@ -445,7 +445,7 @@ async def test_missing_configuration( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "missing_credentials" @@ -471,7 +471,7 @@ async def test_wrong_configuration( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "oauth_error" @@ -506,14 +506,14 @@ async def test_reauth_flow( }, data=config_entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( flow_id=result["flow_id"], user_input={}, ) - assert result.get("type") == "progress" + assert result.get("type") is FlowResultType.SHOW_PROGRESS assert result.get("step_id") == "auth" assert "description_placeholders" in result assert "url" in result["description_placeholders"] @@ -529,7 +529,7 @@ async def test_reauth_flow( flow_id=result["flow_id"] ) - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "reauth_successful" entries = hass.config_entries.async_entries(DOMAIN) @@ -576,7 +576,7 @@ async def test_calendar_lookup_failure( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == "progress" + assert result.get("type") is FlowResultType.SHOW_PROGRESS assert result.get("step_id") == "auth" assert "description_placeholders" in result assert "url" in result["description_placeholders"] @@ -590,7 +590,7 @@ async def test_calendar_lookup_failure( flow_id=result["flow_id"] ) - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == reason @@ -611,7 +611,7 @@ async def test_options_flow_triggers_reauth( assert config_entry.options == {} # Default is read_write result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" data_schema = result["data_schema"].schema assert set(data_schema) == {"calendar_access"} @@ -622,7 +622,7 @@ async def test_options_flow_triggers_reauth( "calendar_access": "read_only", }, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == {"calendar_access": "read_only"} @@ -643,7 +643,7 @@ async def test_options_flow_no_changes( assert config_entry.options == {} # Default is read_write result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -652,7 +652,7 @@ async def test_options_flow_no_changes( "calendar_access": "read_write", }, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == {"calendar_access": "read_write"} @@ -687,7 +687,7 @@ async def test_web_auth_compatibility( "redirect_uri": "https://example.com/auth/external/callback", }, ) - assert result["type"] == "external" + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" @@ -770,7 +770,7 @@ async def test_web_reauth_flow( }, data=config_entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( @@ -791,7 +791,7 @@ async def test_web_reauth_flow( "redirect_uri": "https://example.com/auth/external/callback", }, ) - assert result.get("type") == "external" + assert result.get("type") is FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" diff --git a/tests/components/google_assistant_sdk/test_config_flow.py b/tests/components/google_assistant_sdk/test_config_flow.py index 49e849398af..4a4931d7bae 100644 --- a/tests/components/google_assistant_sdk/test_config_flow.py +++ b/tests/components/google_assistant_sdk/test_config_flow.py @@ -5,6 +5,7 @@ from unittest.mock import patch from homeassistant import config_entries from homeassistant.components.google_assistant_sdk.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow from .conftest import CLIENT_ID, ComponentSetup @@ -68,7 +69,7 @@ async def test_full_flow( assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(mock_setup.mock_calls) == 1 - assert result.get("type") == "create_entry" + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == TITLE assert "result" in result assert result.get("result").unique_id is None @@ -144,7 +145,7 @@ async def test_reauth( assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(mock_setup.mock_calls) == 1 - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "reauth_successful" assert config_entry.unique_id is None @@ -206,7 +207,7 @@ async def test_single_instance_allowed( ) result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" @@ -221,7 +222,7 @@ async def test_options_flow( # Trigger options flow, first time result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" data_schema = result["data_schema"].schema assert set(data_schema) == {"language_code"} @@ -230,12 +231,12 @@ async def test_options_flow( result["flow_id"], user_input={"language_code": "es-ES"}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == {"language_code": "es-ES"} # Retrigger options flow, not change language result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" data_schema = result["data_schema"].schema assert set(data_schema) == {"language_code"} @@ -244,12 +245,12 @@ async def test_options_flow( result["flow_id"], user_input={"language_code": "es-ES"}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == {"language_code": "es-ES"} # Retrigger options flow, change language result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" data_schema = result["data_schema"].schema assert set(data_schema) == {"language_code"} @@ -258,5 +259,5 @@ async def test_options_flow( result["flow_id"], user_input={"language_code": "en-US"}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == {"language_code": "en-US"} diff --git a/tests/components/google_mail/test_config_flow.py b/tests/components/google_mail/test_config_flow.py index 62db6603988..06479504f9d 100644 --- a/tests/components/google_mail/test_config_flow.py +++ b/tests/components/google_mail/test_config_flow.py @@ -8,6 +8,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.google_mail.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow from .conftest import CLIENT_ID, GOOGLE_AUTH_URI, GOOGLE_TOKEN_URI, SCOPES, TITLE @@ -63,7 +64,7 @@ async def test_full_flow( assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(mock_setup.mock_calls) == 1 - assert result.get("type") == "create_entry" + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == TITLE assert "result" in result assert result.get("result").unique_id == TITLE @@ -160,7 +161,7 @@ async def test_reauth( assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result["reason"] == abort_reason assert result["description_placeholders"] == placeholders assert len(mock_setup.mock_calls) == calls @@ -212,5 +213,5 @@ async def test_already_configured( ), ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" diff --git a/tests/components/google_sheets/test_config_flow.py b/tests/components/google_sheets/test_config_flow.py index edf4580485f..5d8a19d1b61 100644 --- a/tests/components/google_sheets/test_config_flow.py +++ b/tests/components/google_sheets/test_config_flow.py @@ -13,6 +13,7 @@ from homeassistant.components.application_credentials import ( ) from homeassistant.components.google_sheets.const import DOMAIN 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 @@ -104,7 +105,7 @@ async def test_full_flow( assert len(mock_setup.mock_calls) == 1 assert len(mock_client.mock_calls) == 2 - assert result.get("type") == "create_entry" + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == TITLE assert "result" in result assert result.get("result").unique_id == SHEET_ID @@ -163,7 +164,7 @@ async def test_create_sheet_error( ) result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "create_spreadsheet_failure" @@ -238,7 +239,7 @@ async def test_reauth( assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(mock_setup.mock_calls) == 1 - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "reauth_successful" assert config_entry.unique_id == SHEET_ID @@ -313,7 +314,7 @@ async def test_reauth_abort( ) result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "open_spreadsheet_failure" @@ -376,5 +377,5 @@ async def test_already_configured( ) result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" diff --git a/tests/components/growatt_server/test_config_flow.py b/tests/components/growatt_server/test_config_flow.py index 81ca870a22e..e17ea90047b 100644 --- a/tests/components/growatt_server/test_config_flow.py +++ b/tests/components/growatt_server/test_config_flow.py @@ -93,7 +93,7 @@ async def test_no_plants_on_account(hass: HomeAssistant) -> None: result["flow_id"], user_input ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_plants" @@ -180,5 +180,5 @@ async def test_existing_plant_configured(hass: HomeAssistant) -> None: result["flow_id"], user_input ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/guardian/test_config_flow.py b/tests/components/guardian/test_config_flow.py index 06ce37a32af..0f99578768a 100644 --- a/tests/components/guardian/test_config_flow.py +++ b/tests/components/guardian/test_config_flow.py @@ -129,7 +129,7 @@ async def test_step_zeroconf_already_in_progress(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf_data ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -176,7 +176,7 @@ async def test_step_dhcp_already_in_progress(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, data=dhcp_data ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" diff --git a/tests/components/habitica/test_config_flow.py b/tests/components/habitica/test_config_flow.py index fe5ddcacdea..4dfc696daf2 100644 --- a/tests/components/habitica/test_config_flow.py +++ b/tests/components/habitica/test_config_flow.py @@ -7,6 +7,7 @@ from aiohttp import ClientResponseError from homeassistant import config_entries from homeassistant.components.habitica.const import DEFAULT_URL, DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -17,7 +18,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} mock_obj = MagicMock() @@ -42,7 +43,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Default username" assert result2["data"] == { "url": DEFAULT_URL, @@ -75,7 +76,7 @@ async def test_form_invalid_credentials(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_credentials"} @@ -101,7 +102,7 @@ async def test_form_unexpected_exception(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -117,7 +118,7 @@ async def test_manual_flow_config_exist(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_IMPORT} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" mock_obj = MagicMock() @@ -136,5 +137,5 @@ async def test_manual_flow_config_exist(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/harmony/test_config_flow.py b/tests/components/harmony/test_config_flow.py index 3dc5a612452..d87bfd32326 100644 --- a/tests/components/harmony/test_config_flow.py +++ b/tests/components/harmony/test_config_flow.py @@ -29,7 +29,7 @@ async def test_user_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} harmonyapi = _get_mock_harmonyapi(connect=True) @@ -49,7 +49,7 @@ async def test_user_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "friend" assert result2["data"] == {"host": "1.2.3.4", "name": "friend"} assert len(mock_setup_entry.mock_calls) == 1 @@ -74,7 +74,7 @@ async def test_form_ssdp(hass: HomeAssistant) -> None: }, ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {} assert result["description_placeholders"] == { @@ -104,7 +104,7 @@ async def test_form_ssdp(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Harmony Hub" assert result2["data"] == {"host": "192.168.1.12", "name": "Harmony Hub"} assert len(mock_setup_entry.mock_calls) == 1 @@ -129,7 +129,7 @@ async def test_form_ssdp_fails_to_get_remote_id(hass: HomeAssistant) -> None: }, ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -168,7 +168,7 @@ async def test_form_ssdp_aborts_before_checking_remoteid_if_host_known( }, ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT async def test_form_cannot_connect(hass: HomeAssistant) -> None: @@ -191,7 +191,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/hassio/test_config_flow.py b/tests/components/hassio/test_config_flow.py index 1c56f4e25f5..1153203817d 100644 --- a/tests/components/hassio/test_config_flow.py +++ b/tests/components/hassio/test_config_flow.py @@ -4,6 +4,7 @@ from unittest.mock import patch from homeassistant.components.hassio import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType async def test_config_flow(hass: HomeAssistant) -> None: @@ -21,7 +22,7 @@ async def test_config_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "system"} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Supervisor" assert result["data"] == {} await hass.async_block_till_done() @@ -36,5 +37,5 @@ async def test_multiple_entries(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "system"} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/here_travel_time/test_config_flow.py b/tests/components/here_travel_time/test_config_flow.py index 1309878a2f3..002e2ed8fdb 100644 --- a/tests/components/here_travel_time/test_config_flow.py +++ b/tests/components/here_travel_time/test_config_flow.py @@ -266,7 +266,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -289,7 +289,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/hlk_sw16/test_config_flow.py b/tests/components/hlk_sw16/test_config_flow.py index e8c0d36c81c..6a758ec5066 100644 --- a/tests/components/hlk_sw16/test_config_flow.py +++ b/tests/components/hlk_sw16/test_config_flow.py @@ -6,6 +6,7 @@ from unittest.mock import patch from homeassistant import config_entries from homeassistant.components.hlk_sw16.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType class MockSW16Client: @@ -54,7 +55,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} conf = { @@ -83,7 +84,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "127.0.0.1:8080" assert result2["data"] == { "host": "127.0.0.1", @@ -101,7 +102,7 @@ async def test_form(hass: HomeAssistant) -> None: result3 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {} result4 = await hass.config_entries.flow.async_configure( @@ -109,7 +110,7 @@ async def test_form(hass: HomeAssistant) -> None: conf, ) - assert result4["type"] == "abort" + assert result4["type"] is FlowResultType.ABORT assert result4["reason"] == "already_configured" @@ -119,7 +120,7 @@ async def test_import(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} conf = { @@ -148,7 +149,7 @@ async def test_import(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "127.0.0.1:8080" assert result2["data"] == { "host": "127.0.0.1", @@ -180,7 +181,7 @@ async def test_form_invalid_data(hass: HomeAssistant) -> None: conf, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -205,5 +206,5 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: conf, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index 0cd8e3284db..ff47abab833 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -51,7 +51,7 @@ async def test_setup_in_bridge_mode(hass: HomeAssistant, mock_get_source_ip) -> result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -113,7 +113,7 @@ async def test_setup_in_bridge_mode_name_taken( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -199,7 +199,7 @@ async def test_setup_creates_entries_for_accessory_mode_devices( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -1290,7 +1290,7 @@ async def test_converting_bridge_to_accessory_mode( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index fbbd945b987..a336758f4ac 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -243,7 +243,7 @@ async def test_discovery_works( context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair" assert get_flow_context(hass, result) == { "source": config_entries.SOURCE_ZEROCONF, @@ -253,14 +253,14 @@ async def test_discovery_works( # User initiates pairing - device enters pairing mode and displays code result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair" # Pairing doesn't error error and pairing results result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"pairing_code": "111-22-333"} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Koogeek-LS1-20833F" assert result["data"] == {} @@ -276,7 +276,7 @@ async def test_abort_duplicate_flow(hass: HomeAssistant, controller) -> None: context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair" result = await hass.config_entries.flow.async_init( @@ -284,7 +284,7 @@ async def test_abort_duplicate_flow(hass: HomeAssistant, controller) -> None: context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -300,7 +300,7 @@ async def test_pair_already_paired_1(hass: HomeAssistant, controller) -> None: context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_paired" @@ -317,7 +317,7 @@ async def test_unknown_domain_type(hass: HomeAssistant, controller) -> None: context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "ignored_model" @@ -335,7 +335,7 @@ async def test_id_missing(hass: HomeAssistant, controller) -> None: context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_properties" @@ -352,7 +352,7 @@ async def test_discovery_ignored_model(hass: HomeAssistant, controller) -> None: context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "ignored_model" @@ -380,7 +380,7 @@ async def test_discovery_ignored_hk_bridge( context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "ignored_model" @@ -408,7 +408,7 @@ async def test_discovery_does_not_ignore_non_homekit( context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM async def test_discovery_broken_pairing_flag(hass: HomeAssistant, controller) -> None: @@ -483,7 +483,7 @@ async def test_discovery_invalid_config_entry(hass: HomeAssistant, controller) - assert config_entry_count == 0 # And new config flow should continue allowing user to set up a new pairing - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM async def test_discovery_ignored_config_entry(hass: HomeAssistant, controller) -> None: @@ -549,7 +549,7 @@ async def test_discovery_already_configured(hass: HomeAssistant, controller) -> context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data["AccessoryIP"] == discovery_info.host assert entry.data["AccessoryPort"] == discovery_info.port @@ -587,7 +587,7 @@ async def test_discovery_already_configured_update_csharp( context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" await hass.async_block_till_done() @@ -615,7 +615,7 @@ async def test_pair_abort_errors_on_start( test_exc = exception("error") with patch.object(device, "async_start_pairing", side_effect=test_exc): result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == expected @@ -640,7 +640,7 @@ async def test_pair_try_later_errors_on_start( with patch.object(device, "async_start_pairing", side_effect=test_exc): result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result2["step_id"] == expected - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM # Device is rebooted or placed into pairing mode as they have been instructed @@ -654,7 +654,7 @@ async def test_pair_try_later_errors_on_start( result3["flow_id"], user_input={"pairing_code": "111-22-333"} ) - assert result4["type"] == "create_entry" + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == "Koogeek-LS1-20833F" @@ -686,7 +686,7 @@ async def test_pair_form_errors_on_start( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"pairing_code": "111-22-333"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"]["pairing_code"] == expected assert get_flow_context(hass, result) == { @@ -697,7 +697,7 @@ async def test_pair_form_errors_on_start( # User gets back the form result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} # User re-tries entering pairing code @@ -705,7 +705,7 @@ async def test_pair_form_errors_on_start( result["flow_id"], user_input={"pairing_code": "111-22-333"} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Koogeek-LS1-20833F" @@ -736,7 +736,7 @@ async def test_pair_abort_errors_on_finish( with patch.object(device, "async_start_pairing", return_value=finish_pairing): result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert get_flow_context(hass, result) == { "title_placeholders": {"name": "TestDevice", "category": "Outlet"}, "unique_id": "00:00:00:00:00:00", @@ -747,7 +747,7 @@ async def test_pair_abort_errors_on_finish( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"pairing_code": "111-22-333"} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == expected @@ -778,7 +778,7 @@ async def test_pair_form_errors_on_finish( with patch.object(device, "async_start_pairing", return_value=finish_pairing): result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert get_flow_context(hass, result) == { "title_placeholders": {"name": "TestDevice", "category": "Outlet"}, "unique_id": "00:00:00:00:00:00", @@ -789,7 +789,7 @@ async def test_pair_form_errors_on_finish( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"pairing_code": "111-22-333"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"]["pairing_code"] == expected assert get_flow_context(hass, result) == { @@ -826,7 +826,7 @@ async def test_pair_unknown_errors(hass: HomeAssistant, controller) -> None: with patch.object(device, "async_start_pairing", return_value=finish_pairing): result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert get_flow_context(hass, result) == { "title_placeholders": {"name": "TestDevice", "category": "Outlet"}, "unique_id": "00:00:00:00:00:00", @@ -837,7 +837,7 @@ async def test_pair_unknown_errors(hass: HomeAssistant, controller) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"pairing_code": "111-22-333"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"]["pairing_code"] == "pairing_failed" assert ( result["description_placeholders"]["error"] == "The bluetooth connection failed" @@ -860,7 +860,7 @@ async def test_user_works(hass: HomeAssistant, controller) -> None: "homekit_controller", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert get_flow_context(hass, result) == { "source": config_entries.SOURCE_USER, @@ -869,7 +869,7 @@ async def test_user_works(hass: HomeAssistant, controller) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"device": "TestDevice"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair" assert get_flow_context(hass, result) == { @@ -881,7 +881,7 @@ async def test_user_works(hass: HomeAssistant, controller) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"pairing_code": "111-22-333"} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Koogeek-LS1-20833F" @@ -897,7 +897,7 @@ async def test_user_pairing_with_insecure_setup_code( "homekit_controller", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert get_flow_context(hass, result) == { "source": config_entries.SOURCE_USER, @@ -906,7 +906,7 @@ async def test_user_pairing_with_insecure_setup_code( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"device": "TestDevice"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair" assert get_flow_context(hass, result) == { @@ -918,7 +918,7 @@ async def test_user_pairing_with_insecure_setup_code( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"pairing_code": "123-45-678"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair" assert result["errors"] == {"pairing_code": "insecure_setup_code"} @@ -926,7 +926,7 @@ async def test_user_pairing_with_insecure_setup_code( result["flow_id"], user_input={"pairing_code": "123-45-678", "allow_insecure_setup_codes": True}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Koogeek-LS1-20833F" @@ -935,7 +935,7 @@ async def test_user_no_devices(hass: HomeAssistant, controller) -> None: result = await hass.config_entries.flow.async_init( "homekit_controller", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices" @@ -952,7 +952,7 @@ async def test_user_no_unpaired_devices(hass: HomeAssistant, controller) -> None "homekit_controller", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices" @@ -966,7 +966,7 @@ async def test_unignore_works(hass: HomeAssistant, controller) -> None: context={"source": config_entries.SOURCE_UNIGNORE}, data={"unique_id": device.description.id}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair" assert get_flow_context(hass, result) == { "title_placeholders": {"name": "TestDevice", "category": "Other"}, @@ -976,14 +976,14 @@ async def test_unignore_works(hass: HomeAssistant, controller) -> None: # User initiates pairing by clicking on 'configure' - device enters pairing mode and displays code result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair" # Pairing finalized result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"pairing_code": "111-22-333"} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Koogeek-LS1-20833F" @@ -1000,7 +1000,7 @@ async def test_unignore_ignores_missing_devices( data={"unique_id": "00:00:00:00:00:01"}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "accessory_not_found_error" @@ -1022,7 +1022,7 @@ async def test_discovery_dismiss_existing_flow_on_paired( context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pair" await hass.async_block_till_done() assert ( @@ -1038,7 +1038,7 @@ async def test_discovery_dismiss_existing_flow_on_paired( context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_paired" await hass.async_block_till_done() assert ( @@ -1088,7 +1088,7 @@ async def test_mdns_update_to_paired_during_pairing( with patch.object(device, "async_start_pairing", _async_start_pairing): result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert get_flow_context(hass, result) == { "title_placeholders": {"name": "TestDevice", "category": "Outlet"}, "unique_id": "00:00:00:00:00:00", diff --git a/tests/components/homematicip_cloud/test_config_flow.py b/tests/components/homematicip_cloud/test_config_flow.py index 4b0d1c26b8f..d541bce4648 100644 --- a/tests/components/homematicip_cloud/test_config_flow.py +++ b/tests/components/homematicip_cloud/test_config_flow.py @@ -11,6 +11,7 @@ from homeassistant.components.homematicip_cloud.const import ( HMIPC_PIN, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -38,7 +39,7 @@ async def test_flow_works(hass: HomeAssistant, simple_mock_home) -> None: data=DEFAULT_CONFIG, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {"base": "press_the_button"} @@ -70,7 +71,7 @@ async def test_flow_works(hass: HomeAssistant, simple_mock_home) -> None: result["flow_id"], user_input={} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "ABC123" assert result["data"] == {"hapid": "ABC123", "authtoken": True, "name": "hmip"} assert result["result"].unique_id == "ABC123" @@ -88,7 +89,7 @@ async def test_flow_init_connection_error(hass: HomeAssistant) -> None: data=DEFAULT_CONFIG, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" @@ -114,7 +115,7 @@ async def test_flow_link_connection_error(hass: HomeAssistant) -> None: data=DEFAULT_CONFIG, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "connection_aborted" @@ -136,7 +137,7 @@ async def test_flow_link_press_button(hass: HomeAssistant) -> None: data=DEFAULT_CONFIG, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {"base": "press_the_button"} @@ -147,7 +148,7 @@ async def test_init_flow_show_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( HMIPC_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" @@ -164,7 +165,7 @@ async def test_init_already_configured(hass: HomeAssistant) -> None: data=DEFAULT_CONFIG, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -193,7 +194,7 @@ async def test_import_config(hass: HomeAssistant, simple_mock_home) -> None: data=IMPORT_CONFIG, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "ABC123" assert result["data"] == {"authtoken": "123", "hapid": "ABC123", "name": "hmip"} assert result["result"].unique_id == "ABC123" @@ -222,5 +223,5 @@ async def test_import_existing_config(hass: HomeAssistant) -> None: data=IMPORT_CONFIG, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/homewizard/test_config_flow.py b/tests/components/homewizard/test_config_flow.py index 08f1436c03a..8d12a8a1787 100644 --- a/tests/components/homewizard/test_config_flow.py +++ b/tests/components/homewizard/test_config_flow.py @@ -322,7 +322,7 @@ async def test_abort_flow( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -349,7 +349,7 @@ async def test_reauth_flow( }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index 74cceb03aba..325c32227e3 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -13,6 +13,7 @@ from homeassistant.components import zeroconf from homeassistant.components.hue import config_flow, const from homeassistant.components.hue.errors import CannotConnect 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 @@ -61,14 +62,14 @@ async def test_flow_works(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"id": disc_bridge.id} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" flow = next( @@ -83,7 +84,7 @@ async def test_flow_works(hass: HomeAssistant) -> None: result["flow_id"], user_input={} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Hue Bridge aabbccddeeff" assert result["data"] == { "host": "1.2.3.4", @@ -108,14 +109,14 @@ async def test_manual_flow_works(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"id": "manual"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" with patch.object(config_flow, "discover_bridge", return_value=disc_bridge): @@ -123,7 +124,7 @@ async def test_manual_flow_works(hass: HomeAssistant) -> None: result["flow_id"], {"host": "2.2.2.2"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" with ( @@ -132,7 +133,7 @@ async def test_manual_flow_works(hass: HomeAssistant) -> None: ): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"Hue Bridge {disc_bridge.id}" assert result["data"] == { "host": "2.2.2.2", @@ -159,14 +160,14 @@ async def test_manual_flow_bridge_exist(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "2.2.2.2"} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -179,7 +180,7 @@ async def test_manual_flow_no_discovered_bridges( result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" @@ -199,7 +200,7 @@ async def test_flow_all_discovered_bridges_exist( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" @@ -219,7 +220,7 @@ async def test_flow_bridges_discovered( result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" with pytest.raises(vol.Invalid): @@ -243,7 +244,7 @@ async def test_flow_two_bridges_discovered_one_new( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({"id": "beer"}) assert result["data_schema"]({"id": "manual"}) @@ -261,7 +262,7 @@ async def test_flow_timeout_discovery(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" @@ -285,7 +286,7 @@ async def test_flow_link_unknown_error(hass: HomeAssistant) -> None: result["flow_id"], user_input={} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {"base": "linking"} @@ -310,7 +311,7 @@ async def test_flow_link_button_not_pressed(hass: HomeAssistant) -> None: result["flow_id"], user_input={} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {"base": "register_failed"} @@ -335,7 +336,7 @@ async def test_flow_link_cannot_connect(hass: HomeAssistant) -> None: result["flow_id"], user_input={} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -350,7 +351,7 @@ async def test_import_with_no_config( data={"host": "0.0.0.0"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" @@ -385,7 +386,7 @@ async def test_creating_entry_removes_entries_for_same_host_or_bridge( context={"source": config_entries.SOURCE_IMPORT}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" with ( @@ -397,7 +398,7 @@ async def test_creating_entry_removes_entries_for_same_host_or_bridge( ): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Hue Bridge id-1234" assert result["data"] == { "host": "2.2.2.2", @@ -431,7 +432,7 @@ async def test_bridge_homekit( ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" flow = next( @@ -454,7 +455,7 @@ async def test_bridge_import_already_configured(hass: HomeAssistant) -> None: data={"host": "0.0.0.0", "properties": {"id": "aa:bb:cc:dd:ee:ff"}}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -481,7 +482,7 @@ async def test_bridge_homekit_already_configured( ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -496,7 +497,7 @@ async def test_options_flow_v1(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema assert ( @@ -516,7 +517,7 @@ async def test_options_flow_v1(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { const.CONF_ALLOW_HUE_GROUPS: True, const.CONF_ALLOW_UNREACHABLE: True, @@ -550,7 +551,7 @@ async def test_options_flow_v2( result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema assert _get_schema_default(schema, const.CONF_IGNORE_AVAILABILITY) == [] @@ -560,7 +561,7 @@ async def test_options_flow_v2( user_input={const.CONF_IGNORE_AVAILABILITY: [mock_dev_id]}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { const.CONF_IGNORE_AVAILABILITY: [mock_dev_id], } @@ -589,7 +590,7 @@ async def test_bridge_zeroconf( ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" @@ -625,7 +626,7 @@ async def test_bridge_zeroconf_already_exists( ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data["host"] == "192.168.1.217" @@ -650,7 +651,7 @@ async def test_bridge_zeroconf_ipv6(hass: HomeAssistant) -> None: ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_host" @@ -676,7 +677,7 @@ async def test_bridge_connection_failed( # a warning message should have been logged that the bridge could not be reached assert "Error while attempting to retrieve discovery information" in caplog.text - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" # test again with zeroconf discovered wrong bridge IP @@ -697,7 +698,7 @@ async def test_bridge_connection_failed( }, ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" # test again with homekit discovered wrong bridge IP @@ -714,7 +715,7 @@ async def test_bridge_connection_failed( type="mock_type", ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" # repeat test with import flow @@ -723,5 +724,5 @@ async def test_bridge_connection_failed( context={"source": config_entries.SOURCE_IMPORT}, data={"host": "blah"}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" diff --git a/tests/components/huisbaasje/test_config_flow.py b/tests/components/huisbaasje/test_config_flow.py index cca5c572712..bf595737f29 100644 --- a/tests/components/huisbaasje/test_config_flow.py +++ b/tests/components/huisbaasje/test_config_flow.py @@ -50,7 +50,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert form_result["type"] == "create_entry" + assert form_result["type"] is FlowResultType.CREATE_ENTRY assert form_result["title"] == "test-username" assert form_result["data"] == { "id": "test-id", diff --git a/tests/components/husqvarna_automower/test_config_flow.py b/tests/components/husqvarna_automower/test_config_flow.py index ce986c4c724..0a345eed627 100644 --- a/tests/components/husqvarna_automower/test_config_flow.py +++ b/tests/components/husqvarna_automower/test_config_flow.py @@ -190,7 +190,7 @@ async def test_reauth( assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "reauth_successful" assert mock_config_entry.unique_id == USER_ID @@ -261,7 +261,7 @@ async def test_reauth_wrong_account( assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "wrong_account" assert mock_config_entry.unique_id == USER_ID diff --git a/tests/components/hvv_departures/test_config_flow.py b/tests/components/hvv_departures/test_config_flow.py index 5a7aa8f8dde..d9545b903c1 100644 --- a/tests/components/hvv_departures/test_config_flow.py +++ b/tests/components/hvv_departures/test_config_flow.py @@ -77,7 +77,7 @@ async def test_user_flow(hass: HomeAssistant) -> None: {CONF_STATION: "Wartenau"}, ) - assert result_station_select["type"] == "create_entry" + assert result_station_select["type"] is FlowResultType.CREATE_ENTRY assert result_station_select["title"] == "Wartenau" assert result_station_select["data"] == { CONF_HOST: "api-test.geofox.de", @@ -159,7 +159,7 @@ async def test_user_flow_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result_user["type"] == "form" + assert result_user["type"] is FlowResultType.FORM assert result_user["errors"] == {"base": "invalid_auth"} @@ -181,7 +181,7 @@ async def test_user_flow_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result_user["type"] == "form" + assert result_user["type"] is FlowResultType.FORM assert result_user["errors"] == {"base": "cannot_connect"} @@ -217,7 +217,7 @@ async def test_user_flow_station(hass: HomeAssistant) -> None: result_user["flow_id"], None, ) - assert result_station["type"] == "form" + assert result_station["type"] is FlowResultType.FORM assert result_station["step_id"] == "station" @@ -255,7 +255,7 @@ async def test_user_flow_station_select(hass: HomeAssistant) -> None: None, ) - assert result_station_select["type"] == "form" + assert result_station_select["type"] is FlowResultType.FORM assert result_station_select["step_id"] == "station_select" diff --git a/tests/components/iaqualink/test_config_flow.py b/tests/components/iaqualink/test_config_flow.py index 64a09e218c3..4aaa66416f6 100644 --- a/tests/components/iaqualink/test_config_flow.py +++ b/tests/components/iaqualink/test_config_flow.py @@ -9,6 +9,7 @@ from iaqualink.exception import ( from homeassistant.components.iaqualink import config_flow from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType async def test_already_configured( @@ -23,7 +24,7 @@ async def test_already_configured( result = await flow.async_step_user(config_data) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT async def test_without_config(hass: HomeAssistant) -> None: @@ -34,7 +35,7 @@ async def test_without_config(hass: HomeAssistant) -> None: result = await flow.async_step_user() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -50,7 +51,7 @@ async def test_with_invalid_credentials(hass: HomeAssistant, config_data) -> Non ): result = await flow.async_step_user(config_data) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -66,7 +67,7 @@ async def test_service_exception(hass: HomeAssistant, config_data) -> None: ): result = await flow.async_step_user(config_data) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -83,6 +84,6 @@ async def test_with_existing_config(hass: HomeAssistant, config_data) -> None: ): result = await flow.async_step_user(config_data) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == config_data["username"] assert result["data"] == config_data diff --git a/tests/components/insteon/test_config_flow.py b/tests/components/insteon/test_config_flow.py index 35ebfb62b77..711290ef3b1 100644 --- a/tests/components/insteon/test_config_flow.py +++ b/tests/components/insteon/test_config_flow.py @@ -124,7 +124,7 @@ async def test_form_select_modem(hass: HomeAssistant) -> None: result = await _init_form(hass, STEP_HUB_V2) assert result["step_id"] == STEP_HUB_V2 - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM async def test_fail_on_existing(hass: HomeAssistant) -> None: @@ -155,7 +155,7 @@ async def test_form_select_plm(hass: HomeAssistant) -> None: result2, mock_setup, mock_setup_entry = await _device_form( hass, result["flow_id"], mock_successful_connection, MOCK_USER_INPUT_PLM ) - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == MOCK_USER_INPUT_PLM assert len(mock_setup.mock_calls) == 1 @@ -173,7 +173,7 @@ async def test_form_select_plm_no_usb(hass: HomeAssistant) -> None: hass, result["flow_id"], mock_successful_connection, None ) USB_PORTS.update(temp_usb_list) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == STEP_PLM_MANUALLY @@ -189,8 +189,8 @@ async def test_form_select_plm_manual(hass: HomeAssistant) -> None: result3, mock_setup, mock_setup_entry = await _device_form( hass, result2["flow_id"], mock_successful_connection, MOCK_USER_INPUT_PLM ) - assert result2["type"] == "form" - assert result3["type"] == "create_entry" + assert result2["type"] is FlowResultType.FORM + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["data"] == MOCK_USER_INPUT_PLM assert len(mock_setup.mock_calls) == 1 @@ -205,7 +205,7 @@ async def test_form_select_hub_v1(hass: HomeAssistant) -> None: result2, mock_setup, mock_setup_entry = await _device_form( hass, result["flow_id"], mock_successful_connection, MOCK_USER_INPUT_HUB_V1 ) - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { **MOCK_USER_INPUT_HUB_V1, CONF_HUB_VERSION: 1, @@ -223,7 +223,7 @@ async def test_form_select_hub_v2(hass: HomeAssistant) -> None: result2, mock_setup, mock_setup_entry = await _device_form( hass, result["flow_id"], mock_successful_connection, MOCK_USER_INPUT_HUB_V2 ) - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { **MOCK_USER_INPUT_HUB_V2, CONF_HUB_VERSION: 2, @@ -263,7 +263,7 @@ async def test_failed_connection_plm(hass: HomeAssistant) -> None: result2, _, _ = await _device_form( hass, result["flow_id"], mock_failed_connection, MOCK_USER_INPUT_PLM ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -278,7 +278,7 @@ async def test_failed_connection_plm_manually(hass: HomeAssistant) -> None: result3, _, _ = await _device_form( hass, result["flow_id"], mock_failed_connection, MOCK_USER_INPUT_PLM ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": "cannot_connect"} @@ -290,7 +290,7 @@ async def test_failed_connection_hub(hass: HomeAssistant) -> None: result2, _, _ = await _device_form( hass, result["flow_id"], mock_failed_connection, MOCK_USER_INPUT_HUB_V2 ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/intellifire/test_config_flow.py b/tests/components/intellifire/test_config_flow.py index 6c5e082ba13..ba4e2f039a3 100644 --- a/tests/components/intellifire/test_config_flow.py +++ b/tests/components/intellifire/test_config_flow.py @@ -358,5 +358,5 @@ async def test_dhcp_discovery_non_intellifire_device( ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_intellifire_device" diff --git a/tests/components/ipma/test_config_flow.py b/tests/components/ipma/test_config_flow.py index e17c8d011a9..ef9b667f03d 100644 --- a/tests/components/ipma/test_config_flow.py +++ b/tests/components/ipma/test_config_flow.py @@ -27,7 +27,7 @@ async def test_config_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" test_data = { @@ -57,7 +57,7 @@ async def test_config_flow_failures(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" test_data = { @@ -109,5 +109,5 @@ async def test_flow_entry_already_exists(hass: HomeAssistant, init_integration) ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/iss/test_config_flow.py b/tests/components/iss/test_config_flow.py index 73261fbc2e7..2fa7b63e937 100644 --- a/tests/components/iss/test_config_flow.py +++ b/tests/components/iss/test_config_flow.py @@ -69,5 +69,5 @@ async def test_options(hass: HomeAssistant) -> None: }, ) - assert configured.get("type") == "create_entry" + assert configured.get("type") is FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_SHOW_ON_MAP: True} diff --git a/tests/components/isy994/test_config_flow.py b/tests/components/isy994/test_config_flow.py index dc9c19e9e75..411439e2e70 100644 --- a/tests/components/isy994/test_config_flow.py +++ b/tests/components/isy994/test_config_flow.py @@ -649,7 +649,7 @@ async def test_reauth(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_REAUTH, "unique_id": MOCK_UUID}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( @@ -664,7 +664,7 @@ async def test_reauth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"password": "invalid_auth"} with patch( @@ -679,7 +679,7 @@ async def test_reauth(hass: HomeAssistant) -> None: }, ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": "cannot_connect"} with ( @@ -699,5 +699,5 @@ async def test_reauth(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert mock_setup_entry.called - assert result4["type"] == "abort" + assert result4["type"] is FlowResultType.ABORT assert result4["reason"] == "reauth_successful" diff --git a/tests/components/jellyfin/test_config_flow.py b/tests/components/jellyfin/test_config_flow.py index 2256a3d5d89..b55766c2c68 100644 --- a/tests/components/jellyfin/test_config_flow.py +++ b/tests/components/jellyfin/test_config_flow.py @@ -41,7 +41,7 @@ async def test_form( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -50,7 +50,7 @@ async def test_form( ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "JELLYFIN-SERVER" assert result2["data"] == { CONF_CLIENT_DEVICE_ID: "TEST-UUID", @@ -75,7 +75,7 @@ async def test_form_cannot_connect( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} mock_client.auth.connect_to_address.return_value = await async_load_json_fixture( @@ -88,7 +88,7 @@ async def test_form_cannot_connect( ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} assert len(mock_client.auth.connect_to_address.mock_calls) == 1 @@ -104,7 +104,7 @@ async def test_form_invalid_auth( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} mock_client.auth.login.return_value = await async_load_json_fixture( @@ -117,7 +117,7 @@ async def test_form_invalid_auth( ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} assert len(mock_client.auth.connect_to_address.mock_calls) == 1 @@ -131,7 +131,7 @@ async def test_form_exception( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} mock_client.auth.connect_to_address.side_effect = Exception("UnknownException") @@ -142,7 +142,7 @@ async def test_form_exception( ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} assert len(mock_client.auth.connect_to_address.mock_calls) == 1 @@ -158,7 +158,7 @@ async def test_form_persists_device_id_on_error( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} mock_client_device_id.return_value = "TEST-UUID-1" @@ -172,7 +172,7 @@ async def test_form_persists_device_id_on_error( ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} mock_client_device_id.return_value = "TEST-UUID-2" @@ -187,7 +187,7 @@ async def test_form_persists_device_id_on_error( await hass.async_block_till_done() assert result3 - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["data"] == { CONF_CLIENT_DEVICE_ID: "TEST-UUID-1", CONF_URL: TEST_URL, @@ -291,7 +291,7 @@ async def test_reauth_cannot_connect( ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} assert len(mock_client.auth.connect_to_address.mock_calls) == 1 @@ -354,7 +354,7 @@ async def test_reauth_invalid( ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} assert len(mock_client.auth.connect_to_address.mock_calls) == 1 @@ -417,7 +417,7 @@ async def test_reauth_exception( ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} assert len(mock_client.auth.connect_to_address.mock_calls) == 1 diff --git a/tests/components/juicenet/test_config_flow.py b/tests/components/juicenet/test_config_flow.py index 2a4be10b49b..48d63cd8cd0 100644 --- a/tests/components/juicenet/test_config_flow.py +++ b/tests/components/juicenet/test_config_flow.py @@ -9,6 +9,7 @@ from homeassistant import config_entries from homeassistant.components.juicenet.const import DOMAIN from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType def _mock_juicenet_return_value(get_devices=None): @@ -23,7 +24,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -43,7 +44,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "JuiceNet" assert result2["data"] == {CONF_ACCESS_TOKEN: "access_token"} assert len(mock_setup.mock_calls) == 1 @@ -64,7 +65,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: result["flow_id"], {CONF_ACCESS_TOKEN: "access_token"} ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -82,7 +83,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: result["flow_id"], {CONF_ACCESS_TOKEN: "access_token"} ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -100,7 +101,7 @@ async def test_form_catch_unknown_errors(hass: HomeAssistant) -> None: result["flow_id"], {CONF_ACCESS_TOKEN: "access_token"} ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -126,7 +127,7 @@ async def test_import(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "JuiceNet" assert result["data"] == {CONF_ACCESS_TOKEN: "access_token"} assert len(mock_setup.mock_calls) == 1 diff --git a/tests/components/kmtronic/test_config_flow.py b/tests/components/kmtronic/test_config_flow.py index a59821b4ec5..725058c662a 100644 --- a/tests/components/kmtronic/test_config_flow.py +++ b/tests/components/kmtronic/test_config_flow.py @@ -24,7 +24,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -41,7 +41,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "1.1.1.1" assert result2["data"] == { "host": "1.1.1.1", @@ -109,7 +109,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -132,7 +132,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -155,5 +155,5 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/knx/test_config_flow.py b/tests/components/knx/test_config_flow.py index 44d5b8bcf80..f12a57f97ba 100644 --- a/tests/components/knx/test_config_flow.py +++ b/tests/components/knx/test_config_flow.py @@ -148,7 +148,7 @@ async def test_user_single_instance(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/kodi/test_config_flow.py b/tests/components/kodi/test_config_flow.py index ecc3bc1f672..d570654be93 100644 --- a/tests/components/kodi/test_config_flow.py +++ b/tests/components/kodi/test_config_flow.py @@ -11,6 +11,7 @@ from homeassistant.components.kodi.config_flow import ( ) from homeassistant.components.kodi.const import DEFAULT_TIMEOUT, DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .util import ( TEST_CREDENTIALS, @@ -34,7 +35,7 @@ async def user_flow(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} return result["flow_id"] @@ -59,7 +60,7 @@ async def test_user_flow(hass: HomeAssistant, user_flow) -> None: result = await hass.config_entries.flow.async_configure(user_flow, TEST_HOST) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_HOST["host"] assert result["data"] == { **TEST_HOST, @@ -87,7 +88,7 @@ async def test_form_valid_auth(hass: HomeAssistant, user_flow) -> None: ): result = await hass.config_entries.flow.async_configure(user_flow, TEST_HOST) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "credentials" assert result["errors"] == {} @@ -110,7 +111,7 @@ async def test_form_valid_auth(hass: HomeAssistant, user_flow) -> None: ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_HOST["host"] assert result["data"] == { **TEST_HOST, @@ -142,7 +143,7 @@ async def test_form_valid_ws_port(hass: HomeAssistant, user_flow) -> None: ): result = await hass.config_entries.flow.async_configure(user_flow, TEST_HOST) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "ws_port" assert result["errors"] == {} @@ -165,7 +166,7 @@ async def test_form_valid_ws_port(hass: HomeAssistant, user_flow) -> None: ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_HOST["host"] assert result["data"] == { **TEST_HOST, @@ -198,7 +199,7 @@ async def test_form_empty_ws_port(hass: HomeAssistant, user_flow) -> None: ): result = await hass.config_entries.flow.async_configure(user_flow, TEST_HOST) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "ws_port" assert result["errors"] == {} @@ -211,7 +212,7 @@ async def test_form_empty_ws_port(hass: HomeAssistant, user_flow) -> None: ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_HOST["host"] assert result["data"] == { **TEST_HOST, @@ -239,7 +240,7 @@ async def test_form_invalid_auth(hass: HomeAssistant, user_flow) -> None: ): result = await hass.config_entries.flow.async_configure(user_flow, TEST_HOST) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "credentials" assert result["errors"] == {} @@ -257,7 +258,7 @@ async def test_form_invalid_auth(hass: HomeAssistant, user_flow) -> None: result["flow_id"], TEST_CREDENTIALS ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "credentials" assert result["errors"] == {"base": "invalid_auth"} @@ -275,7 +276,7 @@ async def test_form_invalid_auth(hass: HomeAssistant, user_flow) -> None: result["flow_id"], TEST_CREDENTIALS ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "credentials" assert result["errors"] == {"base": "cannot_connect"} @@ -293,7 +294,7 @@ async def test_form_invalid_auth(hass: HomeAssistant, user_flow) -> None: result["flow_id"], TEST_CREDENTIALS ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "credentials" assert result["errors"] == {"base": "unknown"} @@ -316,7 +317,7 @@ async def test_form_invalid_auth(hass: HomeAssistant, user_flow) -> None: result["flow_id"], TEST_CREDENTIALS ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "ws_port" assert result["errors"] == {} @@ -335,7 +336,7 @@ async def test_form_cannot_connect_http(hass: HomeAssistant, user_flow) -> None: ): result = await hass.config_entries.flow.async_configure(user_flow, TEST_HOST) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -354,7 +355,7 @@ async def test_form_exception_http(hass: HomeAssistant, user_flow) -> None: ): result = await hass.config_entries.flow.async_configure(user_flow, TEST_HOST) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} @@ -378,7 +379,7 @@ async def test_form_cannot_connect_ws(hass: HomeAssistant, user_flow) -> None: ): result = await hass.config_entries.flow.async_configure(user_flow, TEST_HOST) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "ws_port" assert result["errors"] == {} @@ -399,7 +400,7 @@ async def test_form_cannot_connect_ws(hass: HomeAssistant, user_flow) -> None: result["flow_id"], TEST_WS_PORT ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "ws_port" assert result["errors"] == {"base": "cannot_connect"} @@ -417,7 +418,7 @@ async def test_form_cannot_connect_ws(hass: HomeAssistant, user_flow) -> None: result["flow_id"], TEST_WS_PORT ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "ws_port" assert result["errors"] == {"base": "cannot_connect"} @@ -441,7 +442,7 @@ async def test_form_exception_ws(hass: HomeAssistant, user_flow) -> None: ): result = await hass.config_entries.flow.async_configure(user_flow, TEST_HOST) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "ws_port" assert result["errors"] == {} @@ -460,7 +461,7 @@ async def test_form_exception_ws(hass: HomeAssistant, user_flow) -> None: result["flow_id"], TEST_WS_PORT ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "ws_port" assert result["errors"] == {"base": "unknown"} @@ -483,7 +484,7 @@ async def test_discovery(hass: HomeAssistant) -> None: data=TEST_DISCOVERY, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" with patch( @@ -495,7 +496,7 @@ async def test_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "hostname" assert result["data"] == { **TEST_HOST, @@ -527,7 +528,7 @@ async def test_discovery_cannot_connect_http(hass: HomeAssistant) -> None: data=TEST_DISCOVERY, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -554,7 +555,7 @@ async def test_discovery_cannot_connect_ws(hass: HomeAssistant) -> None: data=TEST_DISCOVERY, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "ws_port" assert result["errors"] == {} @@ -577,7 +578,7 @@ async def test_discovery_exception_http(hass: HomeAssistant, user_flow) -> None: data=TEST_DISCOVERY, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -599,7 +600,7 @@ async def test_discovery_invalid_auth(hass: HomeAssistant) -> None: data=TEST_DISCOVERY, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "credentials" assert result["errors"] == {} @@ -622,14 +623,14 @@ async def test_discovery_duplicate_data(hass: HomeAssistant) -> None: data=TEST_DISCOVERY, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=TEST_DISCOVERY ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -647,7 +648,7 @@ async def test_discovery_updates_unique_id(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=TEST_DISCOVERY ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data["host"] == "1.1.1.1" @@ -663,7 +664,7 @@ async def test_discovery_without_unique_id(hass: HomeAssistant) -> None: data=TEST_DISCOVERY_WO_UUID, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_uuid" @@ -690,7 +691,7 @@ async def test_form_import(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_IMPORT["name"] assert result["data"] == TEST_IMPORT @@ -715,7 +716,7 @@ async def test_form_import_invalid_auth(hass: HomeAssistant) -> None: data=TEST_IMPORT, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_auth" @@ -737,7 +738,7 @@ async def test_form_import_cannot_connect(hass: HomeAssistant) -> None: data=TEST_IMPORT, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -759,5 +760,5 @@ async def test_form_import_exception(hass: HomeAssistant) -> None: data=TEST_IMPORT, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" diff --git a/tests/components/konnected/test_config_flow.py b/tests/components/konnected/test_config_flow.py index c46e115d159..5865616c544 100644 --- a/tests/components/konnected/test_config_flow.py +++ b/tests/components/konnected/test_config_flow.py @@ -8,6 +8,7 @@ from homeassistant import config_entries from homeassistant.components import konnected, ssdp from homeassistant.components.konnected import config_flow from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -33,7 +34,7 @@ async def test_flow_works(hass: HomeAssistant, mock_panel) -> None: result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" mock_panel.get_status.return_value = { @@ -43,7 +44,7 @@ async def test_flow_works(hass: HomeAssistant, mock_panel) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"port": 1234, "host": "1.2.3.4"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["description_placeholders"] == { "model": "Konnected Alarm Panel", @@ -55,7 +56,7 @@ async def test_flow_works(hass: HomeAssistant, mock_panel) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"]["host"] == "1.2.3.4" assert result["data"]["port"] == 1234 assert result["data"]["model"] == "Konnected" @@ -70,7 +71,7 @@ async def test_pro_flow_works(hass: HomeAssistant, mock_panel) -> None: result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # pro uses chipId instead of MAC as unique id @@ -82,7 +83,7 @@ async def test_pro_flow_works(hass: HomeAssistant, mock_panel) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"port": 1234, "host": "1.2.3.4"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["description_placeholders"] == { "model": "Konnected Alarm Panel Pro", @@ -94,7 +95,7 @@ async def test_pro_flow_works(hass: HomeAssistant, mock_panel) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"]["host"] == "1.2.3.4" assert result["data"]["port"] == 1234 assert result["data"]["model"] == "Konnected Pro" @@ -126,7 +127,7 @@ async def test_ssdp(hass: HomeAssistant, mock_panel) -> None: ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["description_placeholders"] == { "model": "Konnected Alarm Panel", @@ -151,7 +152,7 @@ async def test_ssdp(hass: HomeAssistant, mock_panel) -> None: ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" # Test abort if invalid data @@ -167,7 +168,7 @@ async def test_ssdp(hass: HomeAssistant, mock_panel) -> None: ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" # Test abort if invalid manufacturer @@ -185,7 +186,7 @@ async def test_ssdp(hass: HomeAssistant, mock_panel) -> None: ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_konn_panel" # Test abort if invalid model @@ -203,7 +204,7 @@ async def test_ssdp(hass: HomeAssistant, mock_panel) -> None: ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_konn_panel" # Test abort if already configured @@ -227,7 +228,7 @@ async def test_ssdp(hass: HomeAssistant, mock_panel) -> None: ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -267,21 +268,21 @@ async def test_import_no_host_user_finish(hass: HomeAssistant, mock_panel) -> No "id": "aabbccddeeff", }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "import_confirm" assert result["description_placeholders"]["id"] == "aabbccddeeff" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # confirm user is prompted to enter host result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"host": "1.1.1.1", "port": 1234} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["description_placeholders"] == { "model": "Konnected Alarm Panel Pro", @@ -294,7 +295,7 @@ async def test_import_no_host_user_finish(hass: HomeAssistant, mock_panel) -> No result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_import_ssdp_host_user_finish(hass: HomeAssistant, mock_panel) -> None: @@ -334,7 +335,7 @@ async def test_import_ssdp_host_user_finish(hass: HomeAssistant, mock_panel) -> "id": "somechipid", }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "import_confirm" assert result["description_placeholders"]["id"] == "somechipid" @@ -352,13 +353,13 @@ async def test_import_ssdp_host_user_finish(hass: HomeAssistant, mock_panel) -> }, ), ) - assert ssdp_result["type"] == "abort" + assert ssdp_result["type"] is FlowResultType.ABORT assert ssdp_result["reason"] == "already_in_progress" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert result["description_placeholders"] == { "model": "Konnected Alarm Panel Pro", @@ -371,7 +372,7 @@ async def test_import_ssdp_host_user_finish(hass: HomeAssistant, mock_panel) -> result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_ssdp_already_configured(hass: HomeAssistant, mock_panel) -> None: @@ -399,7 +400,7 @@ async def test_ssdp_already_configured(hass: HomeAssistant, mock_panel) -> None: }, ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -479,7 +480,7 @@ async def test_ssdp_host_update(hass: HomeAssistant, mock_panel) -> None: }, ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT # confirm the host value was updated, access_token was not entry = hass.config_entries.async_entries(config_flow.DOMAIN)[0] @@ -537,13 +538,13 @@ async def test_import_existing_config(hass: HomeAssistant, mock_panel) -> None: } ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "host": "1.2.3.4", "port": 1234, @@ -665,7 +666,7 @@ async def test_import_existing_config_entry(hass: HomeAssistant, mock_panel) -> }, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT # We should have updated the host info but not the access token assert len(hass.config_entries.async_entries("konnected")) == 1 @@ -717,13 +718,13 @@ async def test_import_pin_config(hass: HomeAssistant, mock_panel) -> None: } ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "host": "1.2.3.4", "port": 1234, @@ -802,7 +803,7 @@ async def test_option_flow(hass: HomeAssistant, mock_panel) -> None: result = await hass.config_entries.options.async_init( entry.entry_id, context={"source": "test"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_io" result = await hass.config_entries.options.async_configure( @@ -817,7 +818,7 @@ async def test_option_flow(hass: HomeAssistant, mock_panel) -> None: "out": "Switchable Output", }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_binary" assert result["description_placeholders"] == { "zone": "Zone 2", @@ -827,7 +828,7 @@ async def test_option_flow(hass: HomeAssistant, mock_panel) -> None: result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"type": "door"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_binary" assert result["description_placeholders"] == { "zone": "Zone 6", @@ -838,7 +839,7 @@ async def test_option_flow(hass: HomeAssistant, mock_panel) -> None: result["flow_id"], user_input={"type": "window", "name": "winder", "inverse": True}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_digital" assert result["description_placeholders"] == { "zone": "Zone 3", @@ -848,7 +849,7 @@ async def test_option_flow(hass: HomeAssistant, mock_panel) -> None: result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"type": "dht"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_switch" assert result["description_placeholders"] == { "zone": "Zone 4", @@ -859,7 +860,7 @@ async def test_option_flow(hass: HomeAssistant, mock_panel) -> None: result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_switch" assert result["description_placeholders"] == { "zone": "OUT", @@ -879,7 +880,7 @@ async def test_option_flow(hass: HomeAssistant, mock_panel) -> None: }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_switch" assert result["description_placeholders"] == { "zone": "OUT", @@ -899,7 +900,7 @@ async def test_option_flow(hass: HomeAssistant, mock_panel) -> None: }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_misc" # make sure we enforce url format result = await hass.config_entries.options.async_configure( @@ -912,7 +913,7 @@ async def test_option_flow(hass: HomeAssistant, mock_panel) -> None: }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_misc" result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -923,7 +924,7 @@ async def test_option_flow(hass: HomeAssistant, mock_panel) -> None: "api_host": "http://overridehost:1111", }, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "io": { "2": "Binary Sensor", @@ -988,7 +989,7 @@ async def test_option_flow_pro(hass: HomeAssistant, mock_panel) -> None: result = await hass.config_entries.options.async_init( entry.entry_id, context={"source": "test"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_io" result = await hass.config_entries.options.async_configure( @@ -1003,7 +1004,7 @@ async def test_option_flow_pro(hass: HomeAssistant, mock_panel) -> None: "7": "Digital Sensor", }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_io_ext" result = await hass.config_entries.options.async_configure( @@ -1019,14 +1020,14 @@ async def test_option_flow_pro(hass: HomeAssistant, mock_panel) -> None: "alarm2_out2": "Disabled", }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_binary" # zone 2 result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"type": "door"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_binary" # zone 6 @@ -1034,42 +1035,42 @@ async def test_option_flow_pro(hass: HomeAssistant, mock_panel) -> None: result["flow_id"], user_input={"type": "window", "name": "winder", "inverse": True}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_binary" # zone 10 result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"type": "door"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_binary" # zone 11 result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"type": "window"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_digital" # zone 3 result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"type": "dht"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_digital" # zone 7 result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"type": "ds18b20", "name": "temper"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_switch" # zone 4 result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_switch" # zone 8 @@ -1083,21 +1084,21 @@ async def test_option_flow_pro(hass: HomeAssistant, mock_panel) -> None: "repeat": 4, }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_switch" # zone out1 result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_switch" # zone alarm1 result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_misc" result = await hass.config_entries.options.async_configure( @@ -1105,7 +1106,7 @@ async def test_option_flow_pro(hass: HomeAssistant, mock_panel) -> None: user_input={"discovery": False, "blink": True, "override_api_host": False}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "io": { "10": "Binary Sensor", @@ -1201,7 +1202,7 @@ async def test_option_flow_import(hass: HomeAssistant, mock_panel) -> None: result = await hass.config_entries.options.async_init( entry.entry_id, context={"source": "test"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_io" # confirm the defaults are set based on current config - we"ll spot check this throughout @@ -1218,7 +1219,7 @@ async def test_option_flow_import(hass: HomeAssistant, mock_panel) -> None: "3": "Switchable Output", }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_io_ext" schema = result["data_schema"]({}) assert schema["8"] == "Disabled" @@ -1227,7 +1228,7 @@ async def test_option_flow_import(hass: HomeAssistant, mock_panel) -> None: result["flow_id"], user_input={}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_binary" # zone 1 @@ -1238,7 +1239,7 @@ async def test_option_flow_import(hass: HomeAssistant, mock_panel) -> None: result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"type": "door"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_digital" # zone 2 @@ -1249,7 +1250,7 @@ async def test_option_flow_import(hass: HomeAssistant, mock_panel) -> None: result["flow_id"], user_input={"type": "dht"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_switch" # zone 3 @@ -1263,7 +1264,7 @@ async def test_option_flow_import(hass: HomeAssistant, mock_panel) -> None: result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"activation": "high", "more_states": "No"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_misc" schema = result["data_schema"]({}) @@ -1275,7 +1276,7 @@ async def test_option_flow_import(hass: HomeAssistant, mock_panel) -> None: ) # verify the updated fields - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "io": {"1": "Binary Sensor", "2": "Digital Sensor", "3": "Switchable Output"}, "discovery": True, @@ -1348,7 +1349,7 @@ async def test_option_flow_existing(hass: HomeAssistant, mock_panel) -> None: result = await hass.config_entries.options.async_init( entry.entry_id, context={"source": "test"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options_io" # confirm the defaults are pulled in from the existing options diff --git a/tests/components/kostal_plenticore/test_config_flow.py b/tests/components/kostal_plenticore/test_config_flow.py index 41acfb1d136..d94256ebf1a 100644 --- a/tests/components/kostal_plenticore/test_config_flow.py +++ b/tests/components/kostal_plenticore/test_config_flow.py @@ -9,6 +9,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.kostal_plenticore.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -44,7 +45,7 @@ async def test_form_g1( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -91,7 +92,7 @@ async def test_form_g1( "scb:network", "Hostname" ) - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "scb" assert result2["data"] == { "host": "1.1.1.1", @@ -110,7 +111,7 @@ async def test_form_g2( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -157,7 +158,7 @@ async def test_form_g2( "scb:network", "Network:Hostname" ) - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "scb" assert result2["data"] == { "host": "1.1.1.1", @@ -196,7 +197,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"password": "invalid_auth"} @@ -230,7 +231,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"host": "cannot_connect"} @@ -264,7 +265,7 @@ async def test_form_unexpected_error(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -288,5 +289,5 @@ async def test_already_configured(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/kraken/test_config_flow.py b/tests/components/kraken/test_config_flow.py index e6639264291..e1971ec3ab8 100644 --- a/tests/components/kraken/test_config_flow.py +++ b/tests/components/kraken/test_config_flow.py @@ -5,6 +5,7 @@ from unittest.mock import patch from homeassistant.components.kraken.const import CONF_TRACKED_ASSET_PAIRS, DOMAIN 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 @@ -20,13 +21,13 @@ async def test_config_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "user"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert len(mock_setup_entry.mock_calls) == 1 @@ -37,7 +38,7 @@ async def test_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "user"} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -86,7 +87,7 @@ async def test_options(hass: HomeAssistant) -> None: CONF_TRACKED_ASSET_PAIRS: ["ADA/ETH"], }, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() ada_eth_sensor = hass.states.get("sensor.ada_eth_ask") diff --git a/tests/components/kulersky/test_config_flow.py b/tests/components/kulersky/test_config_flow.py index 9638df360c8..a2f3949bd07 100644 --- a/tests/components/kulersky/test_config_flow.py +++ b/tests/components/kulersky/test_config_flow.py @@ -7,6 +7,7 @@ import pykulersky from homeassistant import config_entries from homeassistant.components.kulersky.config_flow import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType async def test_flow_success(hass: HomeAssistant) -> None: @@ -15,7 +16,7 @@ async def test_flow_success(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None light = MagicMock(spec=pykulersky.Light) @@ -37,7 +38,7 @@ async def test_flow_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Kuler Sky" assert result2["data"] == {} @@ -50,7 +51,7 @@ async def test_flow_no_devices_found(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -69,7 +70,7 @@ async def test_flow_no_devices_found(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "no_devices_found" assert len(mock_setup_entry.mock_calls) == 0 @@ -80,7 +81,7 @@ async def test_flow_exceptions_caught(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -99,6 +100,6 @@ async def test_flow_exceptions_caught(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "no_devices_found" assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/lg_soundbar/test_config_flow.py b/tests/components/lg_soundbar/test_config_flow.py index 4fab919c555..806c993e792 100644 --- a/tests/components/lg_soundbar/test_config_flow.py +++ b/tests/components/lg_soundbar/test_config_flow.py @@ -11,6 +11,7 @@ from homeassistant import config_entries from homeassistant.components.lg_soundbar.const import DEFAULT_PORT, DOMAIN from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -58,7 +59,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -83,7 +84,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "name" assert result2["result"].unique_id == "uuid" assert result2["data"] == { @@ -99,7 +100,7 @@ async def test_form_mac_info_response_empty(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -124,7 +125,7 @@ async def test_form_mac_info_response_empty(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "name" assert result2["result"].unique_id == "uuid" assert result2["data"] == { @@ -145,7 +146,7 @@ async def test_form_uuid_present_in_both_functions_uuid_q_empty( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -172,7 +173,7 @@ async def test_form_uuid_present_in_both_functions_uuid_q_empty( ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "name" assert result2["result"].unique_id == "uuid" assert result2["data"] == { @@ -193,7 +194,7 @@ async def test_form_uuid_present_in_both_functions_uuid_q_not_empty( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -224,7 +225,7 @@ async def test_form_uuid_present_in_both_functions_uuid_q_not_empty( ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "name" assert result2["result"].unique_id == "uuid" assert result2["data"] == { @@ -240,7 +241,7 @@ async def test_form_uuid_missing_from_mac_info(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -266,7 +267,7 @@ async def test_form_uuid_missing_from_mac_info(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "name" assert result2["result"].unique_id == "uuid" assert result2["data"] == { @@ -282,7 +283,7 @@ async def test_form_uuid_not_provided_by_api(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -311,7 +312,7 @@ async def test_form_uuid_not_provided_by_api(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "name" assert result2["result"].unique_id is None assert result2["data"] == { @@ -327,7 +328,7 @@ async def test_form_both_queues_empty(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -352,7 +353,7 @@ async def test_form_both_queues_empty(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "no_data"} assert len(mock_setup_entry.mock_calls) == 0 @@ -373,7 +374,7 @@ async def test_no_uuid_host_already_configured(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -395,7 +396,7 @@ async def test_no_uuid_host_already_configured(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -416,7 +417,7 @@ async def test_form_socket_timeout(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -437,7 +438,7 @@ async def test_form_os_error(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -474,5 +475,5 @@ async def test_form_already_configured(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/lifx/test_config_flow.py b/tests/components/lifx/test_config_flow.py index 67fcc1b7dfa..59b7090788a 100644 --- a/tests/components/lifx/test_config_flow.py +++ b/tests/components/lifx/test_config_flow.py @@ -41,13 +41,13 @@ async def test_discovery(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -55,13 +55,13 @@ async def test_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -77,7 +77,7 @@ async def test_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == DEFAULT_ENTRY_TITLE assert result3["data"] == {CONF_HOST: IP_ADDRESS} mock_setup.assert_called_once() @@ -87,7 +87,7 @@ async def test_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -95,7 +95,7 @@ async def test_discovery(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "no_devices_found" @@ -106,13 +106,13 @@ async def test_discovery_but_cannot_connect(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -122,7 +122,7 @@ async def test_discovery_but_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "abort" + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "cannot_connect" @@ -140,7 +140,7 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -148,7 +148,7 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -157,7 +157,7 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -165,7 +165,7 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -177,7 +177,7 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No result3 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_DEVICE: SERIAL} ) - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == DEFAULT_ENTRY_TITLE assert result3["data"] == { CONF_HOST: IP_ADDRESS, @@ -190,7 +190,7 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -198,7 +198,7 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "no_devices_found" @@ -215,7 +215,7 @@ async def test_discovery_no_device(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "no_devices_found" @@ -224,7 +224,7 @@ async def test_manual(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -238,7 +238,7 @@ async def test_manual(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -253,7 +253,7 @@ async def test_manual(hass: HomeAssistant) -> None: result["flow_id"], {CONF_HOST: IP_ADDRESS} ) await hass.async_block_till_done() - assert result4["type"] == "create_entry" + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == DEFAULT_ENTRY_TITLE assert result4["data"] == { CONF_HOST: IP_ADDRESS, @@ -272,7 +272,7 @@ async def test_manual(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -281,7 +281,7 @@ async def test_manual_dns_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -312,7 +312,7 @@ async def test_manual_dns_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -322,7 +322,7 @@ async def test_manual_no_capabilities(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -337,7 +337,7 @@ async def test_manual_no_capabilities(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_HOST: IP_ADDRESS, } @@ -448,7 +448,7 @@ async def test_discovered_by_dhcp_or_discovery( result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { CONF_HOST: IP_ADDRESS, } @@ -548,7 +548,7 @@ async def test_refuse_relays(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -560,7 +560,7 @@ async def test_refuse_relays(hass: HomeAssistant) -> None: result["flow_id"], {CONF_HOST: IP_ADDRESS} ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/litejet/test_config_flow.py b/tests/components/litejet/test_config_flow.py index 4803fd393d8..7b8fa481b69 100644 --- a/tests/components/litejet/test_config_flow.py +++ b/tests/components/litejet/test_config_flow.py @@ -19,7 +19,7 @@ async def test_show_config_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -31,7 +31,7 @@ async def test_create_entry(hass: HomeAssistant, mock_litejet) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "/dev/test" assert result["data"] == test_data @@ -50,7 +50,7 @@ async def test_flow_entry_already_exists(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -65,7 +65,7 @@ async def test_flow_open_failed(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"][CONF_PORT] == "open_failed" diff --git a/tests/components/litterrobot/test_config_flow.py b/tests/components/litterrobot/test_config_flow.py index b8739a73fb8..5ffb78c7782 100644 --- a/tests/components/litterrobot/test_config_flow.py +++ b/tests/components/litterrobot/test_config_flow.py @@ -22,7 +22,7 @@ async def test_form(hass: HomeAssistant, mock_account) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -40,7 +40,7 @@ async def test_form(hass: HomeAssistant, mock_account) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == CONFIG[DOMAIN][CONF_USERNAME] assert result2["data"] == CONFIG[DOMAIN] assert len(mock_setup_entry.mock_calls) == 1 @@ -59,7 +59,7 @@ async def test_already_configured(hass: HomeAssistant) -> None: data=CONFIG[litterrobot.DOMAIN], ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -77,7 +77,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: result["flow_id"], CONFIG[DOMAIN] ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -95,7 +95,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: result["flow_id"], CONFIG[DOMAIN] ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -112,7 +112,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: result["flow_id"], CONFIG[DOMAIN] ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/lookin/test_config_flow.py b/tests/components/lookin/test_config_flow.py index e3896b9c3d4..1eddbe23b24 100644 --- a/tests/components/lookin/test_config_flow.py +++ b/tests/components/lookin/test_config_flow.py @@ -31,7 +31,7 @@ async def test_manual_setup(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -59,7 +59,7 @@ async def test_manual_setup_already_exists(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -78,7 +78,7 @@ async def test_manual_setup_device_offline(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -88,7 +88,7 @@ async def test_manual_setup_device_offline(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_HOST: "cannot_connect"} @@ -97,7 +97,7 @@ async def test_manual_setup_unknown_exception(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -107,7 +107,7 @@ async def test_manual_setup_unknown_exception(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "unknown"} diff --git a/tests/components/lutron_caseta/test_config_flow.py b/tests/components/lutron_caseta/test_config_flow.py index fd529353b98..b2edaa07155 100644 --- a/tests/components/lutron_caseta/test_config_flow.py +++ b/tests/components/lutron_caseta/test_config_flow.py @@ -75,7 +75,7 @@ async def test_bridge_import_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == CasetaConfigFlow.ENTRY_DEFAULT_TITLE assert result["data"] == entry_mock_data assert result["result"].unique_id == "000004d2" @@ -102,7 +102,7 @@ async def test_bridge_cannot_connect(hass: HomeAssistant) -> None: data=entry_mock_data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == STEP_IMPORT_FAILED assert result["errors"] == {"base": ERROR_CANNOT_CONNECT} @@ -125,7 +125,7 @@ async def test_bridge_cannot_connect_unknown_error(hass: HomeAssistant) -> None: data=EMPTY_MOCK_CONFIG_ENTRY, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == STEP_IMPORT_FAILED assert result["errors"] == {"base": ERROR_CANNOT_CONNECT} @@ -145,7 +145,7 @@ async def test_bridge_invalid_ssl_error(hass: HomeAssistant) -> None: data=EMPTY_MOCK_CONFIG_ENTRY, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == STEP_IMPORT_FAILED assert result["errors"] == {"base": ERROR_CANNOT_CONNECT} @@ -195,7 +195,7 @@ async def test_already_configured_with_ignored(hass: HomeAssistant) -> None: CONF_CA_CERTS: "", }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM async def test_form_user(hass: HomeAssistant, tmp_path: Path) -> None: @@ -207,7 +207,7 @@ async def test_form_user(hass: HomeAssistant, tmp_path: Path) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -218,7 +218,7 @@ async def test_form_user(hass: HomeAssistant, tmp_path: Path) -> None: }, ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "link" with ( @@ -240,7 +240,7 @@ async def test_form_user(hass: HomeAssistant, tmp_path: Path) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "1.1.1.1" assert result3["data"] == { CONF_HOST: "1.1.1.1", @@ -261,7 +261,7 @@ async def test_form_user_pairing_fails(hass: HomeAssistant, tmp_path: Path) -> N result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -272,7 +272,7 @@ async def test_form_user_pairing_fails(hass: HomeAssistant, tmp_path: Path) -> N }, ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "link" with ( @@ -294,7 +294,7 @@ async def test_form_user_pairing_fails(hass: HomeAssistant, tmp_path: Path) -> N ) await hass.async_block_till_done() - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": "cannot_connect"} assert len(mock_setup.mock_calls) == 0 assert len(mock_setup_entry.mock_calls) == 0 @@ -311,7 +311,7 @@ async def test_form_user_reuses_existing_assets_when_pairing_again( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -322,7 +322,7 @@ async def test_form_user_reuses_existing_assets_when_pairing_again( }, ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "link" with ( @@ -344,7 +344,7 @@ async def test_form_user_reuses_existing_assets_when_pairing_again( ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "1.1.1.1" assert result3["data"] == { CONF_HOST: "1.1.1.1", @@ -366,7 +366,7 @@ async def test_form_user_reuses_existing_assets_when_pairing_again( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -380,7 +380,7 @@ async def test_form_user_reuses_existing_assets_when_pairing_again( ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "link" with ( @@ -396,7 +396,7 @@ async def test_form_user_reuses_existing_assets_when_pairing_again( ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "1.1.1.1" assert result3["data"] == { CONF_HOST: "1.1.1.1", @@ -433,7 +433,7 @@ async def test_zeroconf_host_already_configured( ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -461,7 +461,7 @@ async def test_zeroconf_lutron_id_already_configured(hass: HomeAssistant) -> Non ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == "1.1.1.1" @@ -484,7 +484,7 @@ async def test_zeroconf_not_lutron_device(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_lutron_device" @@ -512,7 +512,7 @@ async def test_zeroconf(hass: HomeAssistant, source, tmp_path: Path) -> None: ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" with ( @@ -534,7 +534,7 @@ async def test_zeroconf(hass: HomeAssistant, source, tmp_path: Path) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "abc" assert result2["data"] == { CONF_HOST: "1.1.1.1", diff --git a/tests/components/melcloud/test_config_flow.py b/tests/components/melcloud/test_config_flow.py index 312daebd93f..621838e8c67 100644 --- a/tests/components/melcloud/test_config_flow.py +++ b/tests/components/melcloud/test_config_flow.py @@ -52,7 +52,7 @@ async def test_form(hass: HomeAssistant, mock_login, mock_get_devices) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -64,7 +64,7 @@ async def test_form(hass: HomeAssistant, mock_login, mock_get_devices) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "test-email@test-domain.com" assert result2["data"] == { "username": "test-email@test-domain.com", @@ -90,7 +90,7 @@ async def test_form_errors( ) assert len(mock_login.mock_calls) == 1 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == reason @@ -114,7 +114,7 @@ async def test_form_response_errors( data={"username": "test-email@test-domain.com", "password": "test-password"}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == message @@ -139,7 +139,7 @@ async def test_token_refresh(hass: HomeAssistant, mock_login, mock_get_devices) }, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/met/test_config_flow.py b/tests/components/met/test_config_flow.py index 8d0b1620022..c494c4afeb9 100644 --- a/tests/components/met/test_config_flow.py +++ b/tests/components/met/test_config_flow.py @@ -9,6 +9,7 @@ from homeassistant.components.met.const import DOMAIN, HOME_LOCATION_NAME from homeassistant.config import async_process_ha_core_config from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import init_integration @@ -31,7 +32,7 @@ async def test_show_config_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -49,7 +50,7 @@ async def test_flow_with_home_location(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" default_data = result["data_schema"]({}) @@ -72,7 +73,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "home" assert result["data"] == test_data @@ -100,7 +101,7 @@ async def test_flow_entry_already_exists(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"]["name"] == "already_configured" @@ -110,7 +111,7 @@ async def test_onboarding_step(hass: HomeAssistant) -> None: DOMAIN, context={"source": "onboarding"}, data={} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == HOME_LOCATION_NAME assert result["data"] == {"track_home": True} @@ -134,7 +135,7 @@ async def test_onboarding_step_abort_no_home( DOMAIN, context={"source": "onboarding"}, data={} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_home" @@ -153,7 +154,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: # Test show Options form result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" # Test Options flow updated config entry @@ -164,7 +165,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: entry.entry_id, data=update_data ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Mock Title" assert result["data"] == update_data weatherdatamock.assert_called_with( diff --git a/tests/components/met_eireann/test_config_flow.py b/tests/components/met_eireann/test_config_flow.py index 2bb5c9ffe8f..cddc20b835a 100644 --- a/tests/components/met_eireann/test_config_flow.py +++ b/tests/components/met_eireann/test_config_flow.py @@ -26,7 +26,7 @@ async def test_show_config_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER @@ -44,7 +44,7 @@ async def test_flow_with_home_location(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER default_data = result["data_schema"]({}) diff --git a/tests/components/metoffice/test_config_flow.py b/tests/components/metoffice/test_config_flow.py index e4424b0b394..c2e75d89c1a 100644 --- a/tests/components/metoffice/test_config_flow.py +++ b/tests/components/metoffice/test_config_flow.py @@ -8,6 +8,7 @@ import requests_mock from homeassistant import config_entries from homeassistant.components.metoffice.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .const import ( METOFFICE_CONFIG_WAVERTREE, @@ -33,7 +34,7 @@ async def test_form(hass: HomeAssistant, requests_mock: requests_mock.Mocker) -> result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -45,7 +46,7 @@ async def test_form(hass: HomeAssistant, requests_mock: requests_mock.Mocker) -> ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_SITE_NAME_WAVERTREE assert result2["data"] == { "api_key": TEST_API_KEY, @@ -90,7 +91,7 @@ async def test_form_already_configured( data=METOFFICE_CONFIG_WAVERTREE, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -112,7 +113,7 @@ async def test_form_cannot_connect( {"api_key": TEST_API_KEY}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -132,5 +133,5 @@ async def test_form_unknown_error( {"api_key": TEST_API_KEY}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/microbees/test_config_flow.py b/tests/components/microbees/test_config_flow.py index 9f8ca8c7747..327d0214f7a 100644 --- a/tests/components/microbees/test_config_flow.py +++ b/tests/components/microbees/test_config_flow.py @@ -152,7 +152,7 @@ async def test_config_reauth_profile( }, data=config_entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) @@ -213,7 +213,7 @@ async def test_config_reauth_wrong_account( }, data=config_entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) diff --git a/tests/components/mikrotik/test_config_flow.py b/tests/components/mikrotik/test_config_flow.py index 0cc62b9bd8e..f34fde0c9a5 100644 --- a/tests/components/mikrotik/test_config_flow.py +++ b/tests/components/mikrotik/test_config_flow.py @@ -133,7 +133,7 @@ async def test_host_already_configured(hass: HomeAssistant, auth_error) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=DEMO_USER_INPUT ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -184,7 +184,7 @@ async def test_reauth_success(hass: HomeAssistant, api) -> None: data=DEMO_USER_INPUT, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["description_placeholders"] == {CONF_USERNAME: "username"} @@ -195,7 +195,7 @@ async def test_reauth_success(hass: HomeAssistant, api) -> None: }, ) - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -216,7 +216,7 @@ async def test_reauth_failed(hass: HomeAssistant, auth_error) -> None: data=DEMO_USER_INPUT, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result2 = await hass.config_entries.flow.async_configure( @@ -226,7 +226,7 @@ async def test_reauth_failed(hass: HomeAssistant, auth_error) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == { CONF_PASSWORD: "invalid_auth", } @@ -249,7 +249,7 @@ async def test_reauth_failed_conn_error(hass: HomeAssistant, conn_error) -> None data=DEMO_USER_INPUT, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result2 = await hass.config_entries.flow.async_configure( @@ -259,5 +259,5 @@ async def test_reauth_failed_conn_error(hass: HomeAssistant, conn_error) -> None }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/mill/test_config_flow.py b/tests/components/mill/test_config_flow.py index 23c7c3d0e67..832aaef3b19 100644 --- a/tests/components/mill/test_config_flow.py +++ b/tests/components/mill/test_config_flow.py @@ -47,7 +47,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "user" assert result["data"] == { CONF_USERNAME: "user", @@ -92,7 +92,7 @@ async def test_flow_entry_already_exists(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -160,7 +160,7 @@ async def test_local_create_entry(hass: HomeAssistant) -> None: ) test_data[CONNECTION_TYPE] = LOCAL - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == test_data[CONF_IP_ADDRESS] assert result["data"] == test_data @@ -211,7 +211,7 @@ async def test_local_flow_entry_already_exists(hass: HomeAssistant) -> None: test_data, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/monoprice/test_config_flow.py b/tests/components/monoprice/test_config_flow.py index 30d0266eeea..fafdb3c2ecf 100644 --- a/tests/components/monoprice/test_config_flow.py +++ b/tests/components/monoprice/test_config_flow.py @@ -32,7 +32,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -50,7 +50,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == CONFIG[CONF_PORT] assert result2["data"] == { CONF_PORT: CONFIG[CONF_PORT], @@ -73,7 +73,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: result["flow_id"], CONFIG ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -91,7 +91,7 @@ async def test_generic_exception(hass: HomeAssistant) -> None: result["flow_id"], CONFIG ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/motion_blinds/test_config_flow.py b/tests/components/motion_blinds/test_config_flow.py index ac49d51f7ef..4168c3a1f63 100644 --- a/tests/components/motion_blinds/test_config_flow.py +++ b/tests/components/motion_blinds/test_config_flow.py @@ -133,7 +133,7 @@ async def test_config_flow_manual_host_success(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -142,7 +142,7 @@ async def test_config_flow_manual_host_success(hass: HomeAssistant) -> None: {CONF_HOST: TEST_HOST}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "connect" assert result["errors"] == {} @@ -151,7 +151,7 @@ async def test_config_flow_manual_host_success(hass: HomeAssistant) -> None: {CONF_API_KEY: TEST_API_KEY}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_GATEWAY_NAME assert result["data"] == { CONF_HOST: TEST_HOST, @@ -166,7 +166,7 @@ async def test_config_flow_discovery_1_success(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -175,7 +175,7 @@ async def test_config_flow_discovery_1_success(hass: HomeAssistant) -> None: {}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "connect" assert result["errors"] == {} @@ -188,7 +188,7 @@ async def test_config_flow_discovery_1_success(hass: HomeAssistant) -> None: {CONF_API_KEY: TEST_API_KEY}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_GATEWAY_NAME assert result["data"] == { CONF_HOST: TEST_HOST, @@ -203,7 +203,7 @@ async def test_config_flow_discovery_2_success(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -216,7 +216,7 @@ async def test_config_flow_discovery_2_success(hass: HomeAssistant) -> None: {}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select" assert result["data_schema"].schema["select_ip"].container == [ TEST_HOST, @@ -229,7 +229,7 @@ async def test_config_flow_discovery_2_success(hass: HomeAssistant) -> None: {"select_ip": TEST_HOST2}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "connect" assert result["errors"] == {} @@ -242,7 +242,7 @@ async def test_config_flow_discovery_2_success(hass: HomeAssistant) -> None: {CONF_API_KEY: TEST_API_KEY}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_GATEWAY_NAME assert result["data"] == { CONF_HOST: TEST_HOST2, @@ -257,7 +257,7 @@ async def test_config_flow_connection_error(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -266,7 +266,7 @@ async def test_config_flow_connection_error(hass: HomeAssistant) -> None: {CONF_HOST: TEST_HOST}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "connect" assert result["errors"] == {} @@ -279,7 +279,7 @@ async def test_config_flow_connection_error(hass: HomeAssistant) -> None: {CONF_API_KEY: TEST_API_KEY}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "connection_error" @@ -289,7 +289,7 @@ async def test_config_flow_discovery_fail(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -302,7 +302,7 @@ async def test_config_flow_discovery_fail(hass: HomeAssistant) -> None: {}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "discovery_error"} @@ -313,7 +313,7 @@ async def test_config_flow_invalid_interface(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -322,7 +322,7 @@ async def test_config_flow_invalid_interface(hass: HomeAssistant) -> None: {CONF_HOST: TEST_HOST}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "connect" assert result["errors"] == {} @@ -335,7 +335,7 @@ async def test_config_flow_invalid_interface(hass: HomeAssistant) -> None: {CONF_API_KEY: TEST_API_KEY}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_GATEWAY_NAME assert result["data"] == { CONF_HOST: TEST_HOST, @@ -356,7 +356,7 @@ async def test_dhcp_flow(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=dhcp_data ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "connect" assert result["errors"] == {} @@ -369,7 +369,7 @@ async def test_dhcp_flow(hass: HomeAssistant) -> None: {CONF_API_KEY: TEST_API_KEY}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_GATEWAY_NAME assert result["data"] == { CONF_HOST: TEST_HOST, @@ -394,7 +394,7 @@ async def test_dhcp_flow_abort(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=dhcp_data ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_motionblinds" @@ -414,7 +414,7 @@ async def test_dhcp_flow_abort_invalid_response(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=dhcp_data ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_motionblinds" @@ -468,7 +468,7 @@ async def test_change_connection_settings(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -477,7 +477,7 @@ async def test_change_connection_settings(hass: HomeAssistant) -> None: {CONF_HOST: TEST_HOST2}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "connect" assert result["errors"] == {} @@ -486,7 +486,7 @@ async def test_change_connection_settings(hass: HomeAssistant) -> None: {CONF_API_KEY: TEST_API_KEY2}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert config_entry.data[CONF_HOST] == TEST_HOST2 assert config_entry.data[CONF_API_KEY] == TEST_API_KEY2 assert config_entry.data[const.CONF_INTERFACE] == TEST_HOST_ANY diff --git a/tests/components/motioneye/test_config_flow.py b/tests/components/motioneye/test_config_flow.py index 404200bd01a..816fb31933a 100644 --- a/tests/components/motioneye/test_config_flow.py +++ b/tests/components/motioneye/test_config_flow.py @@ -35,7 +35,7 @@ async def test_user_success(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert not result["errors"] mock_client = create_mock_motioneye_client() @@ -62,7 +62,7 @@ async def test_user_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"{TEST_URL}" assert result["data"] == { CONF_URL: TEST_URL, @@ -160,7 +160,7 @@ async def test_user_invalid_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} assert mock_client.async_client_close.called @@ -189,7 +189,7 @@ async def test_user_invalid_url(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_url"} @@ -220,7 +220,7 @@ async def test_user_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} assert mock_client.async_client_close.called @@ -250,7 +250,7 @@ async def test_user_request_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "unknown"} assert mock_client.async_client_close.called @@ -272,7 +272,7 @@ async def test_reauth(hass: HomeAssistant) -> None: }, data=config_entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert not result["errors"] mock_client = create_mock_motioneye_client() @@ -329,7 +329,7 @@ async def test_duplicate(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert not result["errors"] mock_client = create_mock_motioneye_client() diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index f160fc0561a..bbba791137a 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -186,13 +186,13 @@ async def test_user_connection_works( result = await hass.config_entries.flow.async_init( "mqtt", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"broker": "127.0.0.1"} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].data == { "broker": "127.0.0.1", "port": 1883, @@ -217,7 +217,7 @@ async def test_user_v5_connection_works( "mqtt", context={"source": config_entries.SOURCE_USER, "show_advanced_options": True}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"broker": "127.0.0.1", "advanced_options": True} @@ -233,7 +233,7 @@ async def test_user_v5_connection_works( mqtt.CONF_PROTOCOL: "5", }, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].data == { "broker": "another-broker", "discovery": True, @@ -255,13 +255,13 @@ async def test_user_connection_fails( result = await hass.config_entries.flow.async_init( "mqtt", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"broker": "127.0.0.1"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == "cannot_connect" # Check we tried the connection @@ -285,13 +285,13 @@ async def test_manual_config_set( result = await hass.config_entries.flow.async_init( "mqtt", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"broker": "127.0.0.1", "port": "1883"} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].data == { "broker": "127.0.0.1", "port": 1883, @@ -318,7 +318,7 @@ async def test_user_single_instance(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( "mqtt", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -329,7 +329,7 @@ async def test_hassio_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( "mqtt", context={"source": config_entries.SOURCE_HASSIO} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -385,7 +385,7 @@ async def test_hassio_confirm( ), context={"source": config_entries.SOURCE_HASSIO}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "hassio_confirm" assert result["description_placeholders"] == {"addon": "Mock Addon"} @@ -394,7 +394,7 @@ async def test_hassio_confirm( result["flow_id"], {"discovery": True} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].data == { "broker": "mock-broker", "port": 1883, @@ -434,7 +434,7 @@ async def test_hassio_cannot_connect( ), context={"source": config_entries.SOURCE_HASSIO}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "hassio_confirm" assert result["description_placeholders"] == {"addon": "Mock Addon"} @@ -443,7 +443,7 @@ async def test_hassio_cannot_connect( result["flow_id"], {"discovery": True} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == "cannot_connect" # Check we tried the connection assert len(mock_try_connection_time_out.mock_calls) @@ -1074,7 +1074,7 @@ async def test_options_user_connection_fails( }, ) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM mock_try_connection_time_out.reset_mock() result = await hass.config_entries.options.async_configure( @@ -1082,7 +1082,7 @@ async def test_options_user_connection_fails( user_input={mqtt.CONF_BROKER: "bad-broker", mqtt.CONF_PORT: 2345}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == "cannot_connect" # Check we tried the connection @@ -1111,21 +1111,21 @@ async def test_options_bad_birth_message_fails( mock_try_connection.return_value = True result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={mqtt.CONF_BROKER: "another-broker", mqtt.CONF_PORT: 2345}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"birth_topic": "ha_state/online/#"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == "bad_birth" # Check config entry did not update @@ -1152,21 +1152,21 @@ async def test_options_bad_will_message_fails( mock_try_connection.return_value = True result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={mqtt.CONF_BROKER: "another-broker", mqtt.CONF_PORT: 2345}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"will_topic": "ha_state/offline/#"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == "bad_will" # Check config entry did not update @@ -1321,7 +1321,7 @@ async def test_setup_with_advanced_settings( result = await hass.config_entries.options.async_init( config_entry.entry_id, context={"show_advanced_options": True} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "broker" assert result["data_schema"].schema["advanced_options"] @@ -1336,7 +1336,7 @@ async def test_setup_with_advanced_settings( "advanced_options": True, }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "broker" assert "advanced_options" not in result["data_schema"].schema assert result["data_schema"].schema[mqtt.CONF_CLIENT_ID] @@ -1365,7 +1365,7 @@ async def test_setup_with_advanced_settings( mqtt.CONF_TRANSPORT: "websockets", }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "broker" assert "advanced_options" not in result["data_schema"].schema assert result["data_schema"].schema[mqtt.CONF_CLIENT_ID] @@ -1400,7 +1400,7 @@ async def test_setup_with_advanced_settings( }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "broker" assert result["errors"]["base"] == "bad_ws_headers" @@ -1425,7 +1425,7 @@ async def test_setup_with_advanced_settings( }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options" result = await hass.config_entries.options.async_configure( @@ -1435,7 +1435,7 @@ async def test_setup_with_advanced_settings( mqtt.CONF_DISCOVERY_PREFIX: "homeassistant_test", }, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY # Check config entry result assert config_entry.data == { @@ -1482,7 +1482,7 @@ async def test_change_websockets_transport_to_tcp( mock_try_connection.return_value = True result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "broker" assert result["data_schema"].schema["transport"] assert result["data_schema"].schema["ws_path"] @@ -1499,7 +1499,7 @@ async def test_change_websockets_transport_to_tcp( mqtt.CONF_WS_PATH: "/some_path", }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options" result = await hass.config_entries.options.async_configure( @@ -1509,7 +1509,7 @@ async def test_change_websockets_transport_to_tcp( mqtt.CONF_DISCOVERY_PREFIX: "homeassistant_test", }, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY # Check config entry result assert config_entry.data == { diff --git a/tests/components/mullvad/test_config_flow.py b/tests/components/mullvad/test_config_flow.py index a2468fbe7d2..dbf90be3416 100644 --- a/tests/components/mullvad/test_config_flow.py +++ b/tests/components/mullvad/test_config_flow.py @@ -36,7 +36,7 @@ async def test_form_user(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Mullvad VPN" assert result2["data"] == {} assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/mutesync/test_config_flow.py b/tests/components/mutesync/test_config_flow.py index f667671da74..07bcfe66f02 100644 --- a/tests/components/mutesync/test_config_flow.py +++ b/tests/components/mutesync/test_config_flow.py @@ -8,6 +8,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.mutesync.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType async def test_form(hass: HomeAssistant) -> None: @@ -16,7 +17,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -37,7 +38,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "1.1.1.1" assert result2["data"] == { "host": "1.1.1.1", @@ -74,5 +75,5 @@ async def test_form_error( }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": error} From 05c1963815a8cb057a8740c36e86d4ce1a2b93b5 Mon Sep 17 00:00:00 2001 From: Jonas Fors Lellky Date: Wed, 3 Apr 2024 09:23:06 +0200 Subject: [PATCH 205/967] Bump flexit_bacnet to 2.2.1 (#114641) --- homeassistant/components/flexit_bacnet/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flexit_bacnet/manifest.json b/homeassistant/components/flexit_bacnet/manifest.json index d230e4ebb7a..40390162ce6 100644 --- a/homeassistant/components/flexit_bacnet/manifest.json +++ b/homeassistant/components/flexit_bacnet/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/flexit_bacnet", "integration_type": "device", "iot_class": "local_polling", - "requirements": ["flexit_bacnet==2.1.0"] + "requirements": ["flexit_bacnet==2.2.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index b8e8cdbdf39..888358f3384 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -871,7 +871,7 @@ fixerio==1.0.0a0 fjaraskupan==2.3.0 # homeassistant.components.flexit_bacnet -flexit_bacnet==2.1.0 +flexit_bacnet==2.2.1 # homeassistant.components.flipr flipr-api==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a92b31793df..9ebe2010b39 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -709,7 +709,7 @@ fivem-api==0.1.2 fjaraskupan==2.3.0 # homeassistant.components.flexit_bacnet -flexit_bacnet==2.1.0 +flexit_bacnet==2.2.1 # homeassistant.components.flipr flipr-api==1.5.1 From 7a543af8ee56dcd3e43733882a217a678342f3c5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Apr 2024 21:45:08 -1000 Subject: [PATCH 206/967] Simplify homekit_controller cache clear (#114692) --- homeassistant/components/homekit_controller/entity.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/homeassistant/components/homekit_controller/entity.py b/homeassistant/components/homekit_controller/entity.py index 136c063f280..c5478ccb97d 100644 --- a/homeassistant/components/homekit_controller/entity.py +++ b/homeassistant/components/homekit_controller/entity.py @@ -2,7 +2,6 @@ from __future__ import annotations -import contextlib from typing import Any from aiohomekit.model.characteristics import ( @@ -80,11 +79,7 @@ class HomeKitEntity(Entity): def _async_clear_property_cache(self, properties: tuple[str, ...]) -> None: """Clear the cache of properties.""" for prop in properties: - # suppress is slower than try-except-pass, but - # we do not expect to have many properties to clear - # or this to be called often. - with contextlib.suppress(AttributeError): - delattr(self, prop) + self.__dict__.pop(prop, None) @callback def _async_reconfigure(self) -> None: From f3ba71328926fe7a59befb8b767b61feb3aff569 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 3 Apr 2024 09:53:20 +0200 Subject: [PATCH 207/967] Use FlowResultType enum in config flow tests N-Z (#114682) Use FlowResultType enum in config flow tests --- tests/components/nanoleaf/test_config_flow.py | 41 ++-- tests/components/nest/test_config_flow.py | 42 ++-- tests/components/nexia/test_config_flow.py | 15 +- tests/components/nextbus/test_config_flow.py | 2 +- .../nmap_tracker/test_config_flow.py | 20 +- tests/components/nobo_hub/test_config_flow.py | 29 +-- tests/components/nuheat/test_config_flow.py | 13 +- tests/components/nws/test_config_flow.py | 13 +- .../components/octoprint/test_config_flow.py | 58 ++--- .../components/omnilogic/test_config_flow.py | 12 +- .../openexchangerates/test_config_flow.py | 2 +- .../opentherm_gw/test_config_flow.py | 16 +- tests/components/overkiz/test_config_flow.py | 74 +++--- .../panasonic_viera/test_config_flow.py | 75 +++--- .../components/philips_js/test_config_flow.py | 20 +- tests/components/picnic/test_config_flow.py | 6 +- tests/components/plaato/test_config_flow.py | 2 +- .../plum_lightpad/test_config_flow.py | 9 +- .../components/poolsense/test_config_flow.py | 2 +- .../components/powerwall/test_config_flow.py | 30 +-- .../private_ble_device/test_config_flow.py | 14 +- tests/components/profiler/test_config_flow.py | 7 +- tests/components/prosegur/test_config_flow.py | 10 +- tests/components/pushover/test_config_flow.py | 2 +- tests/components/qnap_qsw/test_config_flow.py | 6 +- tests/components/rachio/test_config_flow.py | 15 +- .../components/radiotherm/test_config_flow.py | 2 +- tests/components/rainbird/test_config_flow.py | 4 +- tests/components/rfxtrx/test_config_flow.py | 80 +++--- tests/components/ring/test_config_flow.py | 6 +- tests/components/risco/test_config_flow.py | 8 +- .../rituals_perfume_genie/test_config_flow.py | 11 +- tests/components/roomba/test_config_flow.py | 2 +- tests/components/roon/test_config_flow.py | 10 +- .../rtsp_to_webrtc/test_config_flow.py | 39 +-- tests/components/rympro/test_config_flow.py | 8 +- .../components/samsungtv/test_config_flow.py | 172 ++++++------- .../screenlogic/test_config_flow.py | 35 +-- tests/components/sense/test_config_flow.py | 35 +-- tests/components/sentry/test_config_flow.py | 2 +- tests/components/sharkiq/test_config_flow.py | 9 +- tests/components/sia/test_config_flow.py | 6 +- tests/components/sleepiq/test_config_flow.py | 2 +- .../smart_meter_texas/test_config_flow.py | 13 +- tests/components/smarttub/test_config_flow.py | 6 +- tests/components/solarlog/test_config_flow.py | 4 +- tests/components/solax/test_config_flow.py | 13 +- .../somfy_mylink/test_config_flow.py | 22 +- tests/components/songpal/test_config_flow.py | 2 +- tests/components/sonos/test_config_flow.py | 23 +- tests/components/starline/test_config_flow.py | 13 +- tests/components/steamist/test_config_flow.py | 2 +- tests/components/subaru/test_config_flow.py | 19 +- tests/components/sunweg/test_config_flow.py | 4 +- .../surepetcare/test_config_flow.py | 16 +- .../test_config_flow.py | 8 +- .../components/switchbee/test_config_flow.py | 2 +- tests/components/tado/test_config_flow.py | 10 +- tests/components/tasmota/test_config_flow.py | 31 +-- .../tesla_wall_connector/test_config_flow.py | 8 +- tests/components/tplink/test_config_flow.py | 18 +- tests/components/tractive/test_config_flow.py | 27 ++- .../test_config_flow.py | 4 +- tests/components/twinkly/test_config_flow.py | 17 +- .../ukraine_alarm/test_config_flow.py | 2 +- tests/components/unifi/test_config_flow.py | 10 +- tests/components/upb/test_config_flow.py | 17 +- tests/components/vera/test_config_flow.py | 2 +- tests/components/voip/test_config_flow.py | 4 +- tests/components/volumio/test_config_flow.py | 25 +- tests/components/wallbox/test_config_flow.py | 8 +- .../components/whirlpool/test_config_flow.py | 16 +- tests/components/withings/test_config_flow.py | 4 +- tests/components/wiz/test_config_flow.py | 40 +-- tests/components/wolflink/test_config_flow.py | 6 +- tests/components/ws66i/test_config_flow.py | 10 +- .../xiaomi_aqara/test_config_flow.py | 59 ++--- .../xiaomi_miio/test_config_flow.py | 106 ++++---- tests/components/yeelight/test_config_flow.py | 64 ++--- tests/components/youtube/test_config_flow.py | 4 +- tests/components/zerproc/test_config_flow.py | 13 +- tests/components/zha/test_config_flow.py | 8 +- tests/components/zwave_js/test_config_flow.py | 229 +++++++++--------- 83 files changed, 941 insertions(+), 914 deletions(-) diff --git a/tests/components/nanoleaf/test_config_flow.py b/tests/components/nanoleaf/test_config_flow.py index 5fe32c81eba..eaa1c60dcd4 100644 --- a/tests/components/nanoleaf/test_config_flow.py +++ b/tests/components/nanoleaf/test_config_flow.py @@ -13,6 +13,7 @@ from homeassistant.components import ssdp, zeroconf from homeassistant.components.nanoleaf.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_TOKEN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -55,7 +56,7 @@ async def test_user_unavailable_user_step_link_step(hass: HomeAssistant) -> None CONF_HOST: TEST_HOST, }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} assert not result2["last_step"] @@ -70,7 +71,7 @@ async def test_user_unavailable_user_step_link_step(hass: HomeAssistant) -> None CONF_HOST: TEST_HOST, }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "link" with patch( @@ -78,7 +79,7 @@ async def test_user_unavailable_user_step_link_step(hass: HomeAssistant) -> None side_effect=Unavailable, ): result3 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result3["type"] == "abort" + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "cannot_connect" @@ -106,7 +107,7 @@ async def test_user_error_setup_finish( CONF_HOST: TEST_HOST, }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "link" with ( @@ -119,7 +120,7 @@ async def test_user_error_setup_finish( ), ): result3 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result3["type"] == "abort" + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == reason @@ -139,7 +140,7 @@ async def test_user_not_authorizing_new_tokens_user_step_link_step( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" assert not result["last_step"] @@ -150,24 +151,24 @@ async def test_user_not_authorizing_new_tokens_user_step_link_step( CONF_HOST: TEST_HOST, }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] is None assert result2["step_id"] == "link" result3 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["errors"] is None assert result3["step_id"] == "link" result4 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result4["type"] == "form" + assert result4["type"] is FlowResultType.FORM assert result4["errors"] == {"base": "not_allowing_new_tokens"} assert result4["step_id"] == "link" mock_nanoleaf.return_value.authorize.side_effect = None result5 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result5["type"] == "create_entry" + assert result5["type"] is FlowResultType.CREATE_ENTRY assert result5["title"] == TEST_NAME assert result5["data"] == { CONF_HOST: TEST_HOST, @@ -192,7 +193,7 @@ async def test_user_exception_user_step(hass: HomeAssistant) -> None: CONF_HOST: TEST_HOST, }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "unknown"} assert not result2["last_step"] @@ -212,14 +213,14 @@ async def test_user_exception_user_step(hass: HomeAssistant) -> None: mock_nanoleaf.return_value.authorize.side_effect = Exception() result4 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result4["type"] == "form" + assert result4["type"] is FlowResultType.FORM assert result4["step_id"] == "link" assert result4["errors"] == {"base": "unknown"} mock_nanoleaf.return_value.authorize.side_effect = None mock_nanoleaf.return_value.get_info.side_effect = Exception() result5 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result5["type"] == "abort" + assert result5["type"] is FlowResultType.ABORT assert result5["reason"] == "unknown" @@ -257,7 +258,7 @@ async def test_discovery_link_unavailable( type=type_in_discovery_info, ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" context = next( @@ -273,7 +274,7 @@ async def test_discovery_link_unavailable( side_effect=Unavailable, ): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -305,11 +306,11 @@ async def test_reauth(hass: HomeAssistant) -> None: }, data=entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data[CONF_HOST] == TEST_HOST @@ -403,7 +404,7 @@ async def test_import_discovery_integration( ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_NAME assert result["data"] == { CONF_HOST: TEST_HOST, @@ -451,14 +452,14 @@ async def test_ssdp_discovery(hass: HomeAssistant) -> None: ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "link" result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_NAME assert result2["data"] == { CONF_HOST: TEST_HOST, diff --git a/tests/components/nest/test_config_flow.py b/tests/components/nest/test_config_flow.py index 8d2a9e96d63..b7ca64db232 100644 --- a/tests/components/nest/test_config_flow.py +++ b/tests/components/nest/test_config_flow.py @@ -19,7 +19,7 @@ from homeassistant.components import dhcp from homeassistant.components.nest.const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult +from homeassistant.data_entry_flow import FlowResult, FlowResultType from homeassistant.helpers import config_entry_oauth2_flow from .common import ( @@ -67,13 +67,13 @@ class OAuthFixture: project_id: str = PROJECT_ID, ) -> None: """Invoke multiple steps in the app credentials based flow.""" - assert result.get("type") == "form" + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "cloud_project" result = await self.async_configure( result, {"cloud_project_id": CLOUD_PROJECT_ID} ) - assert result.get("type") == "form" + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "device_project" result = await self.async_configure(result, {"project_id": project_id}) @@ -82,7 +82,7 @@ class OAuthFixture: async def async_oauth_web_flow(self, result: dict, project_id=PROJECT_ID) -> None: """Invoke the oauth flow for Web Auth with fake responses.""" state = self.create_state(result, WEB_REDIRECT_URL) - assert result["type"] == "external" + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["url"] == self.authorize_url( state, WEB_REDIRECT_URL, @@ -175,7 +175,7 @@ class OAuthFixture: async def async_pubsub_flow(self, result: dict, cloud_project_id="") -> None: """Verify the pubsub creation step.""" # Render form with a link to get an auth token - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pubsub" assert "description_placeholders" in result assert "url" in result["description_placeholders"] @@ -246,14 +246,14 @@ async def test_config_flow_restart( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == "form" + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "cloud_project" # Change the values to show they are reflected below result = await oauth.async_configure( result, {"cloud_project_id": "new-cloud-project-id"} ) - assert result.get("type") == "form" + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "device_project" result = await oauth.async_configure(result, {"project_id": "new-project-id"}) @@ -291,17 +291,17 @@ async def test_config_flow_wrong_project_id( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == "form" + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "cloud_project" result = await oauth.async_configure(result, {"cloud_project_id": CLOUD_PROJECT_ID}) - assert result.get("type") == "form" + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "device_project" # Enter the cloud project id instead of device access project id (really we just check # they are the same value which is never correct) result = await oauth.async_configure(result, {"project_id": CLOUD_PROJECT_ID}) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert "errors" in result assert "project_id" in result["errors"] assert result["errors"]["project_id"] == "wrong_project_id" @@ -351,7 +351,7 @@ async def test_config_flow_pubsub_configuration_error( mock_subscriber.create_subscription.side_effect = ConfigurationException result = await oauth.async_configure(result, {"code": "1234"}) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert "errors" in result assert "cloud_project_id" in result["errors"] assert result["errors"]["cloud_project_id"] == "bad_project_id" @@ -372,7 +372,7 @@ async def test_config_flow_pubsub_subscriber_error( mock_subscriber.create_subscription.side_effect = SubscriberException() result = await oauth.async_configure(result, {"code": "1234"}) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert "errors" in result assert "cloud_project_id" in result["errors"] assert result["errors"]["cloud_project_id"] == "subscriber_error" @@ -414,15 +414,15 @@ async def test_duplicate_config_entries( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == "form" + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "cloud_project" result = await oauth.async_configure(result, {"cloud_project_id": CLOUD_PROJECT_ID}) - assert result.get("type") == "form" + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "device_project" result = await oauth.async_configure(result, {"project_id": PROJECT_ID}) - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -522,7 +522,7 @@ async def test_pubsub_subscription_auth_failure( oauth.async_mock_refresh() result = await oauth.async_configure(result, {"code": "1234"}) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_access_token" @@ -693,11 +693,11 @@ async def test_dhcp_discovery( data=FAKE_DHCP_DATA, ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "create_cloud_project" result = await oauth.async_configure(result, {}) - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "missing_credentials" @@ -713,11 +713,11 @@ async def test_dhcp_discovery_with_creds( data=FAKE_DHCP_DATA, ) await hass.async_block_till_done() - assert result.get("type") == "form" + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "cloud_project" result = await oauth.async_configure(result, {"cloud_project_id": CLOUD_PROJECT_ID}) - assert result.get("type") == "form" + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "device_project" result = await oauth.async_configure(result, {"project_id": PROJECT_ID}) @@ -775,5 +775,5 @@ async def test_token_error( ) result = await oauth.async_configure(result, user_input=None) - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == error_reason diff --git a/tests/components/nexia/test_config_flow.py b/tests/components/nexia/test_config_flow.py index 02a3cf06728..6b7956a3bf7 100644 --- a/tests/components/nexia/test_config_flow.py +++ b/tests/components/nexia/test_config_flow.py @@ -10,6 +10,7 @@ from homeassistant import config_entries from homeassistant.components.nexia.const import CONF_BRAND, DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType @pytest.mark.parametrize("brand", [BRAND_ASAIR, BRAND_NEXIA]) @@ -19,7 +20,7 @@ async def test_form(hass: HomeAssistant, brand) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -42,7 +43,7 @@ async def test_form(hass: HomeAssistant, brand) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "myhouse" assert result2["data"] == { CONF_BRAND: brand, @@ -76,7 +77,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -99,7 +100,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -124,7 +125,7 @@ async def test_form_invalid_auth_http_401(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -149,7 +150,7 @@ async def test_form_cannot_connect_not_found(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -172,5 +173,5 @@ async def test_form_broad_exception(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/nextbus/test_config_flow.py b/tests/components/nextbus/test_config_flow.py index 5d0158a50cb..dd16c65e802 100644 --- a/tests/components/nextbus/test_config_flow.py +++ b/tests/components/nextbus/test_config_flow.py @@ -124,7 +124,7 @@ async def test_user_config( ) await hass.async_block_till_done() - assert result.get("type") == "form" + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "route" # Select route diff --git a/tests/components/nmap_tracker/test_config_flow.py b/tests/components/nmap_tracker/test_config_flow.py index f9b6964dc7d..2e12c53a759 100644 --- a/tests/components/nmap_tracker/test_config_flow.py +++ b/tests/components/nmap_tracker/test_config_flow.py @@ -31,7 +31,7 @@ async def test_form(hass: HomeAssistant, hosts: str, mock_get_source_ip) -> None result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} schema_defaults = result["data_schema"]({}) @@ -52,7 +52,7 @@ async def test_form(hass: HomeAssistant, hosts: str, mock_get_source_ip) -> None ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == f"Nmap Tracker {hosts}" assert result2["data"] == {} assert result2["options"] == { @@ -70,7 +70,7 @@ async def test_form_range(hass: HomeAssistant, mock_get_source_ip) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -88,7 +88,7 @@ async def test_form_range(hass: HomeAssistant, mock_get_source_ip) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Nmap Tracker 192.168.0.5-12" assert result2["data"] == {} assert result2["options"] == { @@ -106,7 +106,7 @@ async def test_form_invalid_hosts(hass: HomeAssistant, mock_get_source_ip) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -120,7 +120,7 @@ async def test_form_invalid_hosts(hass: HomeAssistant, mock_get_source_ip) -> No ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {CONF_HOSTS: "invalid_hosts"} @@ -141,7 +141,7 @@ async def test_form_already_configured(hass: HomeAssistant, mock_get_source_ip) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -155,7 +155,7 @@ async def test_form_already_configured(hass: HomeAssistant, mock_get_source_ip) ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -165,7 +165,7 @@ async def test_form_invalid_excludes(hass: HomeAssistant, mock_get_source_ip) -> result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -179,7 +179,7 @@ async def test_form_invalid_excludes(hass: HomeAssistant, mock_get_source_ip) -> ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {CONF_EXCLUDE: "invalid_hosts"} diff --git a/tests/components/nobo_hub/test_config_flow.py b/tests/components/nobo_hub/test_config_flow.py index 1d6feb6e28a..61c84f90cd8 100644 --- a/tests/components/nobo_hub/test_config_flow.py +++ b/tests/components/nobo_hub/test_config_flow.py @@ -5,6 +5,7 @@ from unittest.mock import PropertyMock, patch from homeassistant import config_entries from homeassistant.components.nobo_hub.const import CONF_OVERRIDE_TYPE, DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -18,7 +19,7 @@ async def test_configure_with_discover(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( @@ -27,7 +28,7 @@ async def test_configure_with_discover(hass: HomeAssistant) -> None: "device": "1.1.1.1", }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {} assert result2["step_id"] == "selected" @@ -52,7 +53,7 @@ async def test_configure_with_discover(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "My Nobø Ecohub" assert result3["data"] == { "ip_address": "1.1.1.1", @@ -72,7 +73,7 @@ async def test_configure_manual(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual" @@ -98,7 +99,7 @@ async def test_configure_manual(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "My Nobø Ecohub" assert result2["data"] == { "serial": "123456789012", @@ -125,7 +126,7 @@ async def test_configure_user_selected_manual(hass: HomeAssistant) -> None: "device": "manual", }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {} assert result2["step_id"] == "manual" @@ -151,7 +152,7 @@ async def test_configure_user_selected_manual(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "My Nobø Ecohub" assert result2["data"] == { "serial": "123456789012", @@ -183,7 +184,7 @@ async def test_configure_invalid_serial_suffix(hass: HomeAssistant) -> None: {"serial_suffix": "ABC"}, ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": "invalid_serial"} @@ -202,7 +203,7 @@ async def test_configure_invalid_serial_undiscovered(hass: HomeAssistant) -> Non {"ip_address": "1.1.1.1", "serial": "123456789"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_serial"} @@ -221,7 +222,7 @@ async def test_configure_invalid_ip_address(hass: HomeAssistant) -> None: {"serial": "123456789012", "ip_address": "ABCD"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_ip"} @@ -250,7 +251,7 @@ async def test_configure_cannot_connect(hass: HomeAssistant) -> None: result2["flow_id"], {"serial_suffix": "012"}, ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": "cannot_connect"} mock_connect.assert_awaited_once_with("1.1.1.1", "123456789012") @@ -271,7 +272,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -281,7 +282,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_OVERRIDE_TYPE: "Constant"} result = await hass.config_entries.options.async_init(config_entry.entry_id) @@ -292,5 +293,5 @@ async def test_options_flow(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_OVERRIDE_TYPE: "Now"} diff --git a/tests/components/nuheat/test_config_flow.py b/tests/components/nuheat/test_config_flow.py index 1e7a6215143..f96edf82c0b 100644 --- a/tests/components/nuheat/test_config_flow.py +++ b/tests/components/nuheat/test_config_flow.py @@ -9,6 +9,7 @@ from homeassistant import config_entries from homeassistant.components.nuheat.const import CONF_SERIAL_NUMBER, DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .mocks import _get_mock_thermostat_run @@ -19,7 +20,7 @@ async def test_form_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} mock_thermostat = _get_mock_thermostat_run() @@ -47,7 +48,7 @@ async def test_form_user(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Master bathroom" assert result2["data"] == { CONF_SERIAL_NUMBER: "12345", @@ -76,7 +77,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} response_mock = MagicMock() @@ -94,7 +95,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -126,7 +127,7 @@ async def test_form_invalid_thermostat(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_thermostat"} @@ -149,5 +150,5 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/nws/test_config_flow.py b/tests/components/nws/test_config_flow.py index fe8017c55e1..b20f038c9f7 100644 --- a/tests/components/nws/test_config_flow.py +++ b/tests/components/nws/test_config_flow.py @@ -7,6 +7,7 @@ import aiohttp from homeassistant import config_entries from homeassistant.components.nws.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType async def test_form(hass: HomeAssistant, mock_simple_nws_config) -> None: @@ -17,7 +18,7 @@ async def test_form(hass: HomeAssistant, mock_simple_nws_config) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -29,7 +30,7 @@ async def test_form(hass: HomeAssistant, mock_simple_nws_config) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "ABC" assert result2["data"] == { "api_key": "test", @@ -54,7 +55,7 @@ async def test_form_cannot_connect(hass: HomeAssistant, mock_simple_nws_config) {"api_key": "test"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -72,7 +73,7 @@ async def test_form_unknown_error(hass: HomeAssistant, mock_simple_nws_config) - {"api_key": "test"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -94,7 +95,7 @@ async def test_form_already_configured( ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert len(mock_setup_entry.mock_calls) == 1 result = await hass.config_entries.flow.async_init( @@ -111,6 +112,6 @@ async def test_form_already_configured( ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/octoprint/test_config_flow.py b/tests/components/octoprint/test_config_flow.py index 3fb0d2a10bc..738fbea0887 100644 --- a/tests/components/octoprint/test_config_flow.py +++ b/tests/components/octoprint/test_config_flow.py @@ -19,7 +19,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert not result["errors"] with patch( @@ -37,7 +37,7 @@ async def test_form(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS with ( patch( @@ -61,7 +61,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "1.1.1.1" assert result2["data"] == { "username": "testuser", @@ -94,7 +94,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: "path": "/", }, ) - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS with patch( "pyoctoprintapi.OctoprintClient.request_app_key", return_value="test-key" @@ -103,7 +103,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: result["flow_id"], ) await hass.async_block_till_done() - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS with patch( "pyoctoprintapi.OctoprintClient.get_discovery_info", @@ -123,7 +123,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == "cannot_connect" @@ -144,7 +144,7 @@ async def test_form_unknown_exception(hass: HomeAssistant) -> None: "path": "/", }, ) - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS with patch( "pyoctoprintapi.OctoprintClient.request_app_key", return_value="test-key" @@ -153,7 +153,7 @@ async def test_form_unknown_exception(hass: HomeAssistant) -> None: result["flow_id"], ) await hass.async_block_till_done() - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS with patch( "pyoctoprintapi.OctoprintClient.get_discovery_info", @@ -173,7 +173,7 @@ async def test_form_unknown_exception(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == "unknown" @@ -193,7 +193,7 @@ async def test_show_zerconf_form(hass: HomeAssistant) -> None: type="mock_type", ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert not result["errors"] result = await hass.config_entries.flow.async_configure( @@ -202,7 +202,7 @@ async def test_show_zerconf_form(hass: HomeAssistant) -> None: "username": "testuser", }, ) - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS with patch( "pyoctoprintapi.OctoprintClient.request_app_key", return_value="test-key" @@ -212,7 +212,7 @@ async def test_show_zerconf_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS with ( patch( @@ -262,7 +262,7 @@ async def test_show_ssdp_form(hass: HomeAssistant) -> None: }, ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert not result["errors"] result = await hass.config_entries.flow.async_configure( @@ -271,7 +271,7 @@ async def test_show_ssdp_form(hass: HomeAssistant) -> None: "username": "testuser", }, ) - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS with patch( "pyoctoprintapi.OctoprintClient.request_app_key", return_value="test-key" @@ -281,7 +281,7 @@ async def test_show_ssdp_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS with ( patch( @@ -406,7 +406,7 @@ async def test_failed_auth(hass: HomeAssistant) -> None: "path": "/", }, ) - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS with patch("pyoctoprintapi.OctoprintClient.request_app_key", side_effect=ApiError): result = await hass.config_entries.flow.async_configure( @@ -414,10 +414,10 @@ async def test_failed_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "auth_failed" @@ -438,7 +438,7 @@ async def test_failed_auth_unexpected_error(hass: HomeAssistant) -> None: "path": "/", }, ) - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS with patch("pyoctoprintapi.OctoprintClient.request_app_key", side_effect=Exception): result = await hass.config_entries.flow.async_configure( @@ -446,10 +446,10 @@ async def test_failed_auth_unexpected_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "auth_failed" @@ -465,7 +465,7 @@ async def test_user_duplicate_entry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert not result["errors"] with patch( @@ -483,7 +483,7 @@ async def test_user_duplicate_entry(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS with ( patch( @@ -507,7 +507,7 @@ async def test_user_duplicate_entry(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" assert len(mock_setup.mock_calls) == 0 assert len(mock_setup_entry.mock_calls) == 0 @@ -535,7 +535,7 @@ async def test_duplicate_zerconf_ignored(hass: HomeAssistant) -> None: type="mock_type", ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -561,7 +561,7 @@ async def test_duplicate_ssdp_ignored(hass: HomeAssistant) -> None: }, ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -589,7 +589,7 @@ async def test_reauth_form(hass: HomeAssistant) -> None: }, data=entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert not result["errors"] with patch( @@ -602,7 +602,7 @@ async def test_reauth_form(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS with patch( "homeassistant.components.octoprint.async_setup_entry", @@ -613,5 +613,5 @@ async def test_reauth_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" diff --git a/tests/components/omnilogic/test_config_flow.py b/tests/components/omnilogic/test_config_flow.py index 25d1c41ba68..b4955220916 100644 --- a/tests/components/omnilogic/test_config_flow.py +++ b/tests/components/omnilogic/test_config_flow.py @@ -20,7 +20,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -39,7 +39,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Omnilogic" assert result2["data"] == DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -53,7 +53,7 @@ async def test_already_configured(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -73,7 +73,7 @@ async def test_with_invalid_credentials(hass: HomeAssistant) -> None: DATA, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -94,7 +94,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: DATA, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -115,7 +115,7 @@ async def test_with_unknown_error(hass: HomeAssistant) -> None: DATA, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} diff --git a/tests/components/openexchangerates/test_config_flow.py b/tests/components/openexchangerates/test_config_flow.py index 7c8ad7dfc77..2bc24e6852b 100644 --- a/tests/components/openexchangerates/test_config_flow.py +++ b/tests/components/openexchangerates/test_config_flow.py @@ -235,6 +235,6 @@ async def test_reauth( ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/opentherm_gw/test_config_flow.py b/tests/components/opentherm_gw/test_config_flow.py index ff4ea9d7bb4..24b41df8124 100644 --- a/tests/components/opentherm_gw/test_config_flow.py +++ b/tests/components/opentherm_gw/test_config_flow.py @@ -35,7 +35,7 @@ async def test_form_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -60,7 +60,7 @@ async def test_form_user(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Test Entry 1" assert result2["data"] == { CONF_NAME: "Test Entry 1", @@ -99,7 +99,7 @@ async def test_form_import(hass: HomeAssistant) -> None: data={CONF_ID: "legacy_gateway", CONF_DEVICE: "/dev/ttyUSB1"}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "legacy_gateway" assert result["data"] == { CONF_NAME: "legacy_gateway", @@ -150,10 +150,10 @@ async def test_form_duplicate_entries(hass: HomeAssistant) -> None: result3 = await hass.config_entries.flow.async_configure( flow3["flow_id"], {CONF_NAME: "Test Entry 2", CONF_DEVICE: "/dev/ttyUSB0"} ) - assert result1["type"] == "create_entry" - assert result2["type"] == "form" + assert result1["type"] is FlowResultType.CREATE_ENTRY + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "id_exists"} - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": "already_configured"} assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -178,7 +178,7 @@ async def test_form_connection_timeout(hass: HomeAssistant) -> None: {CONF_NAME: "Test Entry 1", CONF_DEVICE: "socket://192.0.2.254:1234"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "timeout_connect"} assert len(mock_connect.mock_calls) == 1 @@ -199,7 +199,7 @@ async def test_form_connection_error(hass: HomeAssistant) -> None: result["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB0"} ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} assert len(mock_connect.mock_calls) == 1 diff --git a/tests/components/overkiz/test_config_flow.py b/tests/components/overkiz/test_config_flow.py index d99fe57233c..50870ae85fe 100644 --- a/tests/components/overkiz/test_config_flow.py +++ b/tests/components/overkiz/test_config_flow.py @@ -79,14 +79,14 @@ async def test_form_cloud(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> N DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"hub": TEST_SERVER}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "local_or_cloud" result3 = await hass.config_entries.flow.async_configure( @@ -94,7 +94,7 @@ async def test_form_cloud(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> N {"api_type": "cloud"}, ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "cloud" with ( @@ -122,14 +122,14 @@ async def test_form_only_cloud_supported( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"hub": TEST_SERVER2}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "cloud" with ( @@ -157,14 +157,14 @@ async def test_form_local_happy_flow( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"hub": TEST_SERVER}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "local_or_cloud" result3 = await hass.config_entries.flow.async_configure( @@ -172,7 +172,7 @@ async def test_form_local_happy_flow( {"api_type": "local"}, ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "local" with patch.multiple( @@ -218,14 +218,14 @@ async def test_form_invalid_auth_cloud( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"hub": TEST_SERVER}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "local_or_cloud" result3 = await hass.config_entries.flow.async_configure( @@ -233,7 +233,7 @@ async def test_form_invalid_auth_cloud( {"api_type": "cloud"}, ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "cloud" with patch("pyoverkiz.client.OverkizClient.login", side_effect=side_effect): @@ -274,14 +274,14 @@ async def test_form_invalid_auth_local( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"hub": TEST_SERVER}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "local_or_cloud" result3 = await hass.config_entries.flow.async_configure( @@ -289,7 +289,7 @@ async def test_form_invalid_auth_local( {"api_type": "local"}, ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "local" with patch("pyoverkiz.client.OverkizClient.login", side_effect=side_effect): @@ -317,14 +317,14 @@ async def test_form_local_developer_mode_disabled( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"hub": TEST_SERVER}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "local_or_cloud" result3 = await hass.config_entries.flow.async_configure( @@ -332,7 +332,7 @@ async def test_form_local_developer_mode_disabled( {"api_type": "local"}, ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "local" with patch.multiple( @@ -369,14 +369,14 @@ async def test_form_invalid_cozytouch_auth( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"hub": TEST_SERVER_COZYTOUCH}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "cloud" with patch("pyoverkiz.client.OverkizClient.login", side_effect=side_effect): @@ -407,14 +407,14 @@ async def test_cloud_abort_on_duplicate_entry( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"hub": TEST_SERVER}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "local_or_cloud" result3 = await hass.config_entries.flow.async_configure( @@ -422,7 +422,7 @@ async def test_cloud_abort_on_duplicate_entry( {"api_type": "cloud"}, ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "cloud" with ( @@ -461,14 +461,14 @@ async def test_local_abort_on_duplicate_entry( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"hub": TEST_SERVER}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "local_or_cloud" result3 = await hass.config_entries.flow.async_configure( @@ -476,7 +476,7 @@ async def test_local_abort_on_duplicate_entry( {"api_type": "local"}, ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "local" with patch.multiple( @@ -517,14 +517,14 @@ async def test_cloud_allow_multiple_unique_entries( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"hub": TEST_SERVER}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "local_or_cloud" result3 = await hass.config_entries.flow.async_configure( @@ -532,7 +532,7 @@ async def test_cloud_allow_multiple_unique_entries( {"api_type": "cloud"}, ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "cloud" with ( @@ -547,7 +547,7 @@ async def test_cloud_allow_multiple_unique_entries( {"username": TEST_EMAIL, "password": TEST_PASSWORD}, ) - assert result4["type"] == "create_entry" + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == TEST_EMAIL assert result4["data"] == { "api_type": "cloud", @@ -790,7 +790,7 @@ async def test_dhcp_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No {"hub": TEST_SERVER}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "local_or_cloud" await hass.config_entries.flow.async_configure( @@ -810,7 +810,7 @@ async def test_dhcp_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No }, ) - assert result4["type"] == "create_entry" + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == TEST_EMAIL assert result4["data"] == { "username": TEST_EMAIL, @@ -861,7 +861,7 @@ async def test_zeroconf_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) - {"hub": TEST_SERVER}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "local_or_cloud" result3 = await hass.config_entries.flow.async_configure( @@ -869,7 +869,7 @@ async def test_zeroconf_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) - {"api_type": "cloud"}, ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "cloud" with ( @@ -884,7 +884,7 @@ async def test_zeroconf_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) - {"username": TEST_EMAIL, "password": TEST_PASSWORD}, ) - assert result4["type"] == "create_entry" + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == TEST_EMAIL assert result4["data"] == { "username": TEST_EMAIL, @@ -914,7 +914,7 @@ async def test_local_zeroconf_flow( {"hub": TEST_SERVER}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "local_or_cloud" result3 = await hass.config_entries.flow.async_configure( @@ -922,7 +922,7 @@ async def test_local_zeroconf_flow( {"api_type": "local"}, ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["step_id"] == "local" with patch.multiple( @@ -938,7 +938,7 @@ async def test_local_zeroconf_flow( {"username": TEST_EMAIL, "password": TEST_PASSWORD, "verify_ssl": False}, ) - assert result4["type"] == "create_entry" + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == "gateway-1234-5678-9123.local:8443" assert result4["data"] == { "username": TEST_EMAIL, diff --git a/tests/components/panasonic_viera/test_config_flow.py b/tests/components/panasonic_viera/test_config_flow.py index f9664f5d657..49a2ae6fc90 100644 --- a/tests/components/panasonic_viera/test_config_flow.py +++ b/tests/components/panasonic_viera/test_config_flow.py @@ -13,6 +13,7 @@ from homeassistant.components.panasonic_viera.const import ( ) from homeassistant.const import CONF_PIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .conftest import ( MOCK_BASIC_DATA, @@ -32,7 +33,7 @@ async def test_flow_non_encrypted(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" mock_remote = get_mock_remote(encrypted=False) @@ -46,7 +47,7 @@ async def test_flow_non_encrypted(hass: HomeAssistant) -> None: {**MOCK_BASIC_DATA}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == {**MOCK_CONFIG_DATA, ATTR_DEVICE_INFO: MOCK_DEVICE_INFO} @@ -58,7 +59,7 @@ async def test_flow_not_connected_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -70,7 +71,7 @@ async def test_flow_not_connected_error(hass: HomeAssistant) -> None: {**MOCK_BASIC_DATA}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -82,7 +83,7 @@ async def test_flow_unknown_abort(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -94,7 +95,7 @@ async def test_flow_unknown_abort(hass: HomeAssistant) -> None: {**MOCK_BASIC_DATA}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -107,7 +108,7 @@ async def test_flow_encrypted_not_connected_pin_code_request( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" mock_remote = get_mock_remote(encrypted=True, request_error=TimeoutError) @@ -121,7 +122,7 @@ async def test_flow_encrypted_not_connected_pin_code_request( {**MOCK_BASIC_DATA}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -132,7 +133,7 @@ async def test_flow_encrypted_unknown_pin_code_request(hass: HomeAssistant) -> N DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" mock_remote = get_mock_remote(encrypted=True, request_error=Exception) @@ -146,7 +147,7 @@ async def test_flow_encrypted_unknown_pin_code_request(hass: HomeAssistant) -> N {**MOCK_BASIC_DATA}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -157,7 +158,7 @@ async def test_flow_encrypted_valid_pin_code(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" mock_remote = get_mock_remote( @@ -175,7 +176,7 @@ async def test_flow_encrypted_valid_pin_code(hass: HomeAssistant) -> None: {**MOCK_BASIC_DATA}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing" result = await hass.config_entries.flow.async_configure( @@ -183,7 +184,7 @@ async def test_flow_encrypted_valid_pin_code(hass: HomeAssistant) -> None: {CONF_PIN: "1234"}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == { **MOCK_CONFIG_DATA, @@ -199,7 +200,7 @@ async def test_flow_encrypted_invalid_pin_code_error(hass: HomeAssistant) -> Non DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" mock_remote = get_mock_remote(encrypted=True, authorize_error=SOAPError) @@ -213,7 +214,7 @@ async def test_flow_encrypted_invalid_pin_code_error(hass: HomeAssistant) -> Non {**MOCK_BASIC_DATA}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing" with patch( @@ -225,7 +226,7 @@ async def test_flow_encrypted_invalid_pin_code_error(hass: HomeAssistant) -> Non {CONF_PIN: "0000"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing" assert result["errors"] == {"base": ERROR_INVALID_PIN_CODE} @@ -237,7 +238,7 @@ async def test_flow_encrypted_not_connected_abort(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" mock_remote = get_mock_remote(encrypted=True, authorize_error=TimeoutError) @@ -251,7 +252,7 @@ async def test_flow_encrypted_not_connected_abort(hass: HomeAssistant) -> None: {**MOCK_BASIC_DATA}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing" result = await hass.config_entries.flow.async_configure( @@ -259,7 +260,7 @@ async def test_flow_encrypted_not_connected_abort(hass: HomeAssistant) -> None: {CONF_PIN: "0000"}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -270,7 +271,7 @@ async def test_flow_encrypted_unknown_abort(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" mock_remote = get_mock_remote(encrypted=True, authorize_error=Exception) @@ -284,7 +285,7 @@ async def test_flow_encrypted_unknown_abort(hass: HomeAssistant) -> None: {**MOCK_BASIC_DATA}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing" result = await hass.config_entries.flow.async_configure( @@ -292,7 +293,7 @@ async def test_flow_encrypted_unknown_abort(hass: HomeAssistant) -> None: {CONF_PIN: "0000"}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -311,7 +312,7 @@ async def test_flow_non_encrypted_already_configured_abort(hass: HomeAssistant) data={**MOCK_BASIC_DATA}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -330,7 +331,7 @@ async def test_flow_encrypted_already_configured_abort(hass: HomeAssistant) -> N data={**MOCK_BASIC_DATA}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -349,7 +350,7 @@ async def test_imported_flow_non_encrypted(hass: HomeAssistant) -> None: data={**MOCK_CONFIG_DATA}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == {**MOCK_CONFIG_DATA, ATTR_DEVICE_INFO: MOCK_DEVICE_INFO} @@ -373,7 +374,7 @@ async def test_imported_flow_encrypted_valid_pin_code(hass: HomeAssistant) -> No data={**MOCK_CONFIG_DATA}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing" result = await hass.config_entries.flow.async_configure( @@ -381,7 +382,7 @@ async def test_imported_flow_encrypted_valid_pin_code(hass: HomeAssistant) -> No {CONF_PIN: "1234"}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == { **MOCK_CONFIG_DATA, @@ -407,7 +408,7 @@ async def test_imported_flow_encrypted_invalid_pin_code_error( data={**MOCK_CONFIG_DATA}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing" with patch( @@ -419,7 +420,7 @@ async def test_imported_flow_encrypted_invalid_pin_code_error( {CONF_PIN: "0000"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing" assert result["errors"] == {"base": ERROR_INVALID_PIN_CODE} @@ -439,7 +440,7 @@ async def test_imported_flow_encrypted_not_connected_abort(hass: HomeAssistant) data={**MOCK_CONFIG_DATA}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing" result = await hass.config_entries.flow.async_configure( @@ -447,7 +448,7 @@ async def test_imported_flow_encrypted_not_connected_abort(hass: HomeAssistant) {CONF_PIN: "0000"}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -466,7 +467,7 @@ async def test_imported_flow_encrypted_unknown_abort(hass: HomeAssistant) -> Non data={**MOCK_CONFIG_DATA}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing" result = await hass.config_entries.flow.async_configure( @@ -474,7 +475,7 @@ async def test_imported_flow_encrypted_unknown_abort(hass: HomeAssistant) -> Non {CONF_PIN: "0000"}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -491,7 +492,7 @@ async def test_imported_flow_not_connected_error(hass: HomeAssistant) -> None: data={**MOCK_CONFIG_DATA}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -509,7 +510,7 @@ async def test_imported_flow_unknown_abort(hass: HomeAssistant) -> None: data={**MOCK_CONFIG_DATA}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -530,7 +531,7 @@ async def test_imported_flow_non_encrypted_already_configured_abort( data={**MOCK_BASIC_DATA}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -551,5 +552,5 @@ async def test_imported_flow_encrypted_already_configured_abort( data={**MOCK_BASIC_DATA}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/philips_js/test_config_flow.py b/tests/components/philips_js/test_config_flow.py index fdf4825b116..d7f539db9cf 100644 --- a/tests/components/philips_js/test_config_flow.py +++ b/tests/components/philips_js/test_config_flow.py @@ -44,7 +44,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -53,7 +53,7 @@ async def test_form(hass: HomeAssistant, mock_setup_entry) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Philips TV (1234567890)" assert result2["data"] == MOCK_CONFIG assert len(mock_setup_entry.mock_calls) == 1 @@ -106,7 +106,7 @@ async def test_form_cannot_connect(hass: HomeAssistant, mock_tv) -> None: result["flow_id"], MOCK_USERINPUT ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -121,7 +121,7 @@ async def test_form_unexpected_error(hass: HomeAssistant, mock_tv) -> None: result["flow_id"], MOCK_USERINPUT ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "unknown"} @@ -132,7 +132,7 @@ async def test_pairing(hass: HomeAssistant, mock_tv_pairable, mock_setup_entry) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( @@ -140,7 +140,7 @@ async def test_pairing(hass: HomeAssistant, mock_tv_pairable, mock_setup_entry) MOCK_USERINPUT, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} mock_tv.setTransport.assert_called_with(True) @@ -179,7 +179,7 @@ async def test_pair_request_failed( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( @@ -205,14 +205,14 @@ async def test_pair_grant_failed( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_USERINPUT, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} mock_tv.setTransport.assert_called_with(True) @@ -224,7 +224,7 @@ async def test_pair_grant_failed( result["flow_id"], {"pin": "1234"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"pin": "invalid_pin"} # Test with unexpected failure diff --git a/tests/components/picnic/test_config_flow.py b/tests/components/picnic/test_config_flow.py index 694ca0df31f..9ba18dac9a9 100644 --- a/tests/components/picnic/test_config_flow.py +++ b/tests/components/picnic/test_config_flow.py @@ -60,7 +60,7 @@ async def test_form(hass: HomeAssistant, picnic_api) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Picnic" assert result2["data"] == { CONF_ACCESS_TOKEN: picnic_api().session.auth_token, @@ -238,7 +238,7 @@ async def test_step_reauth_failed(hass: HomeAssistant) -> None: await hass.async_block_till_done() # Check that the returned flow has type form with error set - assert result_configure["type"] == "form" + assert result_configure["type"] is FlowResultType.FORM assert result_configure["errors"] == {"base": "invalid_auth"} assert len(hass.config_entries.async_entries()) == 1 @@ -277,7 +277,7 @@ async def test_step_reauth_different_account(hass: HomeAssistant, picnic_api) -> await hass.async_block_till_done() # Check that the returned flow has type form with error set - assert result_configure["type"] == "form" + assert result_configure["type"] is FlowResultType.FORM assert result_configure["errors"] == {"base": "different_account"} assert len(hass.config_entries.async_entries()) == 1 diff --git a/tests/components/plaato/test_config_flow.py b/tests/components/plaato/test_config_flow.py index f87b2db7fef..efda354f20d 100644 --- a/tests/components/plaato/test_config_flow.py +++ b/tests/components/plaato/test_config_flow.py @@ -47,7 +47,7 @@ async def test_show_config_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" diff --git a/tests/components/plum_lightpad/test_config_flow.py b/tests/components/plum_lightpad/test_config_flow.py index 37934942734..ca7c110c963 100644 --- a/tests/components/plum_lightpad/test_config_flow.py +++ b/tests/components/plum_lightpad/test_config_flow.py @@ -7,6 +7,7 @@ from requests.exceptions import ConnectTimeout from homeassistant import config_entries from homeassistant.components.plum_lightpad.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -17,7 +18,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -33,7 +34,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "test-plum-username" assert result2["data"] == { "username": "test-plum-username", @@ -57,7 +58,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: {"username": "test-plum-username", "password": "test-plum-password"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -84,6 +85,6 @@ async def test_form_one_entry_per_email_allowed(hass: HomeAssistant) -> None: {"username": "test-plum-username", "password": "test-plum-password"}, ) - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/poolsense/test_config_flow.py b/tests/components/poolsense/test_config_flow.py index 7fbb42ea106..49f790b5075 100644 --- a/tests/components/poolsense/test_config_flow.py +++ b/tests/components/poolsense/test_config_flow.py @@ -31,7 +31,7 @@ async def test_invalid_credentials(hass: HomeAssistant) -> None: data={CONF_EMAIL: "test-email", CONF_PASSWORD: "test-password"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} diff --git a/tests/components/powerwall/test_config_flow.py b/tests/components/powerwall/test_config_flow.py index 207c9b32d2f..3e13fc7242d 100644 --- a/tests/components/powerwall/test_config_flow.py +++ b/tests/components/powerwall/test_config_flow.py @@ -36,7 +36,7 @@ async def test_form_source_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} mock_powerwall = await _mock_powerwall_site_name(hass, "MySite") @@ -57,7 +57,7 @@ async def test_form_source_user(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "MySite" assert result2["data"] == VALID_CONFIG assert len(mock_setup_entry.mock_calls) == 1 @@ -81,7 +81,7 @@ async def test_form_cannot_connect(hass: HomeAssistant, exc: Exception) -> None: VALID_CONFIG, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {CONF_IP_ADDRESS: "cannot_connect"} @@ -104,7 +104,7 @@ async def test_invalid_auth(hass: HomeAssistant) -> None: VALID_CONFIG, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {CONF_PASSWORD: "invalid_auth"} @@ -124,7 +124,7 @@ async def test_form_unknown_exeption(hass: HomeAssistant) -> None: result["flow_id"], VALID_CONFIG ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -147,7 +147,7 @@ async def test_form_wrong_version(hass: HomeAssistant) -> None: VALID_CONFIG, ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": "wrong_version"} @@ -166,7 +166,7 @@ async def test_already_configured(hass: HomeAssistant) -> None: hostname="any", ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -193,7 +193,7 @@ async def test_already_configured_with_ignored(hass: HomeAssistant) -> None: hostname="00GGX", ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -212,7 +212,7 @@ async def test_already_configured_with_ignored(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Some site" assert result2["data"] == {"ip_address": "1.1.1.1", "password": "00GGX"} assert len(mock_setup_entry.mock_calls) == 1 @@ -235,7 +235,7 @@ async def test_dhcp_discovery_manual_configure(hass: HomeAssistant) -> None: hostname="any", ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -254,7 +254,7 @@ async def test_dhcp_discovery_manual_configure(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Some site" assert result2["data"] == VALID_CONFIG assert len(mock_setup_entry.mock_calls) == 1 @@ -277,7 +277,7 @@ async def test_dhcp_discovery_auto_configure(hass: HomeAssistant) -> None: hostname="00GGX", ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -296,7 +296,7 @@ async def test_dhcp_discovery_auto_configure(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Some site" assert result2["data"] == {"ip_address": "1.1.1.1", "password": "00GGX"} assert len(mock_setup_entry.mock_calls) == 1 @@ -340,7 +340,7 @@ async def test_form_reauth(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_REAUTH, "entry_id": entry.entry_id}, data=entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} mock_powerwall = await _mock_powerwall_site_name(hass, "My site") @@ -363,7 +363,7 @@ async def test_form_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/private_ble_device/test_config_flow.py b/tests/components/private_ble_device/test_config_flow.py index 64a3c9c1d2e..a8821dddace 100644 --- a/tests/components/private_ble_device/test_config_flow.py +++ b/tests/components/private_ble_device/test_config_flow.py @@ -13,7 +13,7 @@ from tests.components.bluetooth import inject_bluetooth_service_info def assert_form_error(result: FlowResult, key: str, value: str) -> None: """Assert that a flow returned a form error.""" - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] assert result["errors"][key] == value @@ -35,7 +35,7 @@ async def test_invalid_irk(hass: HomeAssistant, enable_bluetooth: None) -> None: result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"irk": "irk:000000"} @@ -48,7 +48,7 @@ async def test_invalid_irk_base64(hass: HomeAssistant, enable_bluetooth: None) - result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"irk": "Ucredacted4T8n!!ZZZ=="} @@ -61,7 +61,7 @@ async def test_invalid_irk_hex(hass: HomeAssistant, enable_bluetooth: None) -> N result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"irk": "irk:abcdefghi"} @@ -74,7 +74,7 @@ async def test_irk_not_found(hass: HomeAssistant, enable_bluetooth: None) -> Non result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -102,7 +102,7 @@ async def test_flow_works(hass: HomeAssistant, enable_bluetooth: None) -> None: result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM # Check you can finish the flow with patch( @@ -141,7 +141,7 @@ async def test_flow_works_by_base64( result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM # Check you can finish the flow with patch( diff --git a/tests/components/profiler/test_config_flow.py b/tests/components/profiler/test_config_flow.py index 93542f90520..189a1ac2377 100644 --- a/tests/components/profiler/test_config_flow.py +++ b/tests/components/profiler/test_config_flow.py @@ -5,6 +5,7 @@ from unittest.mock import patch from homeassistant import config_entries from homeassistant.components.profiler.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -15,7 +16,7 @@ async def test_form_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( @@ -28,7 +29,7 @@ async def test_form_user(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Profiler" assert result2["data"] == {} assert len(mock_setup_entry.mock_calls) == 1 @@ -41,5 +42,5 @@ async def test_form_user_only_once(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/prosegur/test_config_flow.py b/tests/components/prosegur/test_config_flow.py index cd44c899824..9362cecc289 100644 --- a/tests/components/prosegur/test_config_flow.py +++ b/tests/components/prosegur/test_config_flow.py @@ -19,7 +19,7 @@ async def test_form(hass: HomeAssistant, mock_list_contracts) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -48,7 +48,7 @@ async def test_form(hass: HomeAssistant, mock_list_contracts) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Contract 123" assert result3["data"] == { "contract": "123", @@ -80,7 +80,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -103,7 +103,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -126,7 +126,7 @@ async def test_form_unknown_exception(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/pushover/test_config_flow.py b/tests/components/pushover/test_config_flow.py index 9b92033414c..14347084288 100644 --- a/tests/components/pushover/test_config_flow.py +++ b/tests/components/pushover/test_config_flow.py @@ -189,7 +189,7 @@ async def test_reauth_failed(hass: HomeAssistant, mock_pushover: MagicMock) -> N data=MOCK_CONFIG, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" mock_pushover.side_effect = BadAPIRequestError("400: application token is invalid") diff --git a/tests/components/qnap_qsw/test_config_flow.py b/tests/components/qnap_qsw/test_config_flow.py index 334f74ccf4b..94e80d3cd16 100644 --- a/tests/components/qnap_qsw/test_config_flow.py +++ b/tests/components/qnap_qsw/test_config_flow.py @@ -103,7 +103,7 @@ async def test_form_duplicated_id(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -121,7 +121,7 @@ async def test_form_unique_id_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_id" @@ -194,7 +194,7 @@ async def test_dhcp_flow(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: TEST_PASSWORD, diff --git a/tests/components/rachio/test_config_flow.py b/tests/components/rachio/test_config_flow.py index b7325349746..1eaec1bc46e 100644 --- a/tests/components/rachio/test_config_flow.py +++ b/tests/components/rachio/test_config_flow.py @@ -12,6 +12,7 @@ from homeassistant.components.rachio.const import ( ) from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -31,7 +32,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} rachio_mock = _mock_rachio_return_value( @@ -59,7 +60,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "myusername" assert result2["data"] == { CONF_API_KEY: "api_key", @@ -87,7 +88,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: {CONF_API_KEY: "api_key"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -109,7 +110,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: {CONF_API_KEY: "api_key"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -129,7 +130,7 @@ async def test_form_homekit(hass: HomeAssistant) -> None: type="mock_type", ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} flow = next( flow @@ -154,7 +155,7 @@ async def test_form_homekit(hass: HomeAssistant) -> None: type="mock_type", ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -180,5 +181,5 @@ async def test_form_homekit_ignored(hass: HomeAssistant) -> None: type="mock_type", ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/radiotherm/test_config_flow.py b/tests/components/radiotherm/test_config_flow.py index 002d6b71ae6..a188f8fcb70 100644 --- a/tests/components/radiotherm/test_config_flow.py +++ b/tests/components/radiotherm/test_config_flow.py @@ -31,7 +31,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( diff --git a/tests/components/rainbird/test_config_flow.py b/tests/components/rainbird/test_config_flow.py index 4768954850d..0e4b04690de 100644 --- a/tests/components/rainbird/test_config_flow.py +++ b/tests/components/rainbird/test_config_flow.py @@ -102,7 +102,7 @@ async def test_controller_flow( """Test the controller is setup correctly.""" result = await complete_flow(hass) - assert result.get("type") == "create_entry" + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == HOST assert "result" in result assert dict(result["result"].data) == expected_config_entry @@ -291,7 +291,7 @@ async def test_options_flow(hass: HomeAssistant, mock_setup: Mock) -> None: # Setup config flow result = await complete_flow(hass) - assert result.get("type") == "create_entry" + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == HOST assert "result" in result assert result["result"].data == CONFIG_ENTRY_DATA diff --git a/tests/components/rfxtrx/test_config_flow.py b/tests/components/rfxtrx/test_config_flow.py index dad48a04290..3e97b4cfc30 100644 --- a/tests/components/rfxtrx/test_config_flow.py +++ b/tests/components/rfxtrx/test_config_flow.py @@ -45,7 +45,7 @@ async def test_setup_network(transport_mock, hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -54,7 +54,7 @@ async def test_setup_network(transport_mock, hass: HomeAssistant) -> None: {"type": "Network"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_network" assert result["errors"] == {} @@ -63,7 +63,7 @@ async def test_setup_network(transport_mock, hass: HomeAssistant) -> None: result["flow_id"], {"host": "10.10.0.1", "port": 1234} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "RFXTRX" assert result["data"] == { "host": "10.10.0.1", @@ -83,7 +83,7 @@ async def test_setup_serial(com_mock, transport_mock, hass: HomeAssistant) -> No DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -92,7 +92,7 @@ async def test_setup_serial(com_mock, transport_mock, hass: HomeAssistant) -> No {"type": "Serial"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_serial" assert result["errors"] == {} @@ -101,7 +101,7 @@ async def test_setup_serial(com_mock, transport_mock, hass: HomeAssistant) -> No result["flow_id"], {"device": port.device} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "RFXTRX" assert result["data"] == { "host": None, @@ -121,7 +121,7 @@ async def test_setup_serial_manual( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -130,7 +130,7 @@ async def test_setup_serial_manual( {"type": "Serial"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_serial" assert result["errors"] == {} @@ -138,7 +138,7 @@ async def test_setup_serial_manual( result["flow_id"], {"device": "Enter Manually"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_serial_manual_path" assert result["errors"] == {} @@ -147,7 +147,7 @@ async def test_setup_serial_manual( result["flow_id"], {"device": "/dev/ttyUSB0"} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "RFXTRX" assert result["data"] == { "host": None, @@ -165,7 +165,7 @@ async def test_setup_network_fail(transport_mock, hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -174,7 +174,7 @@ async def test_setup_network_fail(transport_mock, hass: HomeAssistant) -> None: {"type": "Network"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_network" assert result["errors"] == {} @@ -182,7 +182,7 @@ async def test_setup_network_fail(transport_mock, hass: HomeAssistant) -> None: result["flow_id"], {"host": "10.10.0.1", "port": 1234} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_network" assert result["errors"] == {"base": "cannot_connect"} @@ -197,7 +197,7 @@ async def test_setup_serial_fail(com_mock, transport_mock, hass: HomeAssistant) DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -206,7 +206,7 @@ async def test_setup_serial_fail(com_mock, transport_mock, hass: HomeAssistant) {"type": "Serial"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_serial" assert result["errors"] == {} @@ -214,7 +214,7 @@ async def test_setup_serial_fail(com_mock, transport_mock, hass: HomeAssistant) result["flow_id"], {"device": port.device} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_serial" assert result["errors"] == {"base": "cannot_connect"} @@ -229,7 +229,7 @@ async def test_setup_serial_manual_fail( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -238,7 +238,7 @@ async def test_setup_serial_manual_fail( {"type": "Serial"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_serial" assert result["errors"] == {} @@ -246,7 +246,7 @@ async def test_setup_serial_manual_fail( result["flow_id"], {"device": "Enter Manually"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_serial_manual_path" assert result["errors"] == {} @@ -254,7 +254,7 @@ async def test_setup_serial_manual_fail( result["flow_id"], {"device": "/dev/ttyUSB0"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "setup_serial_manual_path" assert result["errors"] == {"base": "cannot_connect"} @@ -277,7 +277,7 @@ async def test_options_global(hass: HomeAssistant) -> None: with patch("homeassistant.components.rfxtrx.async_setup_entry", return_value=True): result = await start_options_flow(hass, entry) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "prompt_options" result = await hass.config_entries.options.async_configure( @@ -312,7 +312,7 @@ async def test_no_protocols(hass: HomeAssistant) -> None: with patch("homeassistant.components.rfxtrx.async_setup_entry", return_value=True): result = await start_options_flow(hass, entry) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "prompt_options" result = await hass.config_entries.options.async_configure( @@ -345,7 +345,7 @@ async def test_options_add_device(hass: HomeAssistant) -> None: ) result = await start_options_flow(hass, entry) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "prompt_options" # Try with invalid event code @@ -354,7 +354,7 @@ async def test_options_add_device(hass: HomeAssistant) -> None: user_input={"automatic_add": True, "event_code": "1234"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "prompt_options" assert result["errors"] assert result["errors"]["event_code"] == "invalid_event_code" @@ -368,7 +368,7 @@ async def test_options_add_device(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "set_device_options" result = await hass.config_entries.options.async_configure( @@ -409,7 +409,7 @@ async def test_options_add_duplicate_device(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "prompt_options" result = await hass.config_entries.options.async_configure( @@ -420,7 +420,7 @@ async def test_options_add_duplicate_device(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "prompt_options" assert result["errors"] assert result["errors"]["event_code"] == "already_configured_device" @@ -508,7 +508,7 @@ async def test_options_replace_sensor_device(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "prompt_options" result = await hass.config_entries.options.async_configure( @@ -519,7 +519,7 @@ async def test_options_replace_sensor_device(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "set_device_options" result = await hass.config_entries.options.async_configure( @@ -641,7 +641,7 @@ async def test_options_replace_control_device(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "prompt_options" result = await hass.config_entries.options.async_configure( @@ -652,7 +652,7 @@ async def test_options_replace_control_device(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "set_device_options" result = await hass.config_entries.options.async_configure( @@ -702,7 +702,7 @@ async def test_options_add_and_configure_device(hass: HomeAssistant) -> None: ) result = await start_options_flow(hass, entry) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "prompt_options" result = await hass.config_entries.options.async_configure( @@ -713,7 +713,7 @@ async def test_options_add_and_configure_device(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "set_device_options" result = await hass.config_entries.options.async_configure( @@ -726,7 +726,7 @@ async def test_options_add_and_configure_device(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "set_device_options" assert result["errors"] assert result["errors"]["off_delay"] == "invalid_input_off_delay" @@ -764,7 +764,7 @@ async def test_options_add_and_configure_device(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "prompt_options" result = await hass.config_entries.options.async_configure( @@ -775,7 +775,7 @@ async def test_options_add_and_configure_device(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "set_device_options" result = await hass.config_entries.options.async_configure( @@ -811,7 +811,7 @@ async def test_options_configure_rfy_cover_device(hass: HomeAssistant) -> None: ) result = await start_options_flow(hass, entry) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "prompt_options" result = await hass.config_entries.options.async_configure( @@ -822,7 +822,7 @@ async def test_options_configure_rfy_cover_device(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "set_device_options" result = await hass.config_entries.options.async_configure( @@ -849,7 +849,7 @@ async def test_options_configure_rfy_cover_device(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "prompt_options" result = await hass.config_entries.options.async_configure( @@ -860,7 +860,7 @@ async def test_options_configure_rfy_cover_device(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "set_device_options" result = await hass.config_entries.options.async_configure( diff --git a/tests/components/ring/test_config_flow.py b/tests/components/ring/test_config_flow.py index ae3301be3ed..bedb4604814 100644 --- a/tests/components/ring/test_config_flow.py +++ b/tests/components/ring/test_config_flow.py @@ -24,7 +24,7 @@ async def test_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -33,7 +33,7 @@ async def test_form( ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "hello@home-assistant.io" assert result2["data"] == { "username": "hello@home-assistant.io", @@ -63,7 +63,7 @@ async def test_form_error( {"username": "hello@home-assistant.io", "password": "test-password"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": errors_msg} diff --git a/tests/components/risco/test_config_flow.py b/tests/components/risco/test_config_flow.py index 3f7a166b465..7589bc0ae14 100644 --- a/tests/components/risco/test_config_flow.py +++ b/tests/components/risco/test_config_flow.py @@ -158,7 +158,7 @@ async def test_form_reauth(hass: HomeAssistant, cloud_config_entry) -> None: context={"source": config_entries.SOURCE_REAUTH}, data=cloud_config_entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -183,7 +183,7 @@ async def test_form_reauth(hass: HomeAssistant, cloud_config_entry) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert cloud_config_entry.data[CONF_PASSWORD] == "new_password" assert len(mock_setup_entry.mock_calls) == 1 @@ -199,7 +199,7 @@ async def test_form_reauth_with_new_username( context={"source": config_entries.SOURCE_REAUTH}, data=cloud_config_entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -224,7 +224,7 @@ async def test_form_reauth_with_new_username( ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert cloud_config_entry.data[CONF_USERNAME] == "new_user" assert cloud_config_entry.unique_id == "new_user" diff --git a/tests/components/rituals_perfume_genie/test_config_flow.py b/tests/components/rituals_perfume_genie/test_config_flow.py index 45f14399f15..6c0a09a8303 100644 --- a/tests/components/rituals_perfume_genie/test_config_flow.py +++ b/tests/components/rituals_perfume_genie/test_config_flow.py @@ -10,6 +10,7 @@ from homeassistant import config_entries from homeassistant.components.rituals_perfume_genie.const import ACCOUNT_HASH, DOMAIN from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType TEST_EMAIL = "rituals@example.com" VALID_PASSWORD = "passw0rd" @@ -29,7 +30,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -51,7 +52,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_EMAIL assert isinstance(result2["data"][ACCOUNT_HASH], str) assert len(mock_setup_entry.mock_calls) == 1 @@ -75,7 +76,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -97,7 +98,7 @@ async def test_form_auth_exception(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -121,5 +122,5 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/roomba/test_config_flow.py b/tests/components/roomba/test_config_flow.py index e097a7bd0ea..e5f882afa36 100644 --- a/tests/components/roomba/test_config_flow.py +++ b/tests/components/roomba/test_config_flow.py @@ -1009,7 +1009,7 @@ async def test_dhcp_discovery_partial_hostname(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" with patch( diff --git a/tests/components/roon/test_config_flow.py b/tests/components/roon/test_config_flow.py index a9bae92f5e6..6f83331d1c7 100644 --- a/tests/components/roon/test_config_flow.py +++ b/tests/components/roon/test_config_flow.py @@ -94,7 +94,7 @@ async def test_successful_discovery_and_auth(hass: HomeAssistant) -> None: await hass.async_block_till_done() # Should go straight to link if server was discovered - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {} @@ -136,7 +136,7 @@ async def test_unsuccessful_discovery_user_form_and_auth(hass: HomeAssistant) -> await hass.async_block_till_done() # Should show the form if server was not discovered - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "fallback" assert result["errors"] == {} @@ -183,7 +183,7 @@ async def test_duplicate_config(hass: HomeAssistant) -> None: await hass.async_block_till_done() # Should show the form if server was not discovered - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "fallback" assert result["errors"] == {} @@ -230,7 +230,7 @@ async def test_successful_discovery_no_auth(hass: HomeAssistant) -> None: await hass.async_block_till_done() # Should go straight to link if server was discovered - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {} @@ -265,7 +265,7 @@ async def test_unexpected_exception(hass: HomeAssistant) -> None: await hass.async_block_till_done() # Should go straight to link if server was discovered - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {} diff --git a/tests/components/rtsp_to_webrtc/test_config_flow.py b/tests/components/rtsp_to_webrtc/test_config_flow.py index 16d4779a92c..504ede68ac7 100644 --- a/tests/components/rtsp_to_webrtc/test_config_flow.py +++ b/tests/components/rtsp_to_webrtc/test_config_flow.py @@ -11,6 +11,7 @@ from homeassistant.components.hassio import HassioServiceInfo from homeassistant.components.rtsp_to_webrtc import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .conftest import ComponentSetup @@ -22,7 +23,7 @@ async def test_web_full_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == "form" + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" assert result.get("data_schema").schema.get("server_url") == str assert not result.get("errors") @@ -36,7 +37,7 @@ async def test_web_full_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], {"server_url": "https://example.com"} ) - assert result.get("type") == "create_entry" + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == "https://example.com" assert "result" in result assert result["result"].data == {"server_url": "https://example.com"} @@ -52,7 +53,7 @@ async def test_single_config_entry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" @@ -61,7 +62,7 @@ async def test_invalid_url(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == "form" + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" assert result.get("data_schema").schema.get("server_url") == str assert not result.get("errors") @@ -69,7 +70,7 @@ async def test_invalid_url(hass: HomeAssistant) -> None: result["flow_id"], {"server_url": "not-a-url"} ) - assert result.get("type") == "form" + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" assert result.get("errors") == {"server_url": "invalid_url"} @@ -79,7 +80,7 @@ async def test_server_unreachable(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == "form" + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" assert not result.get("errors") with patch( @@ -89,7 +90,7 @@ async def test_server_unreachable(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], {"server_url": "https://example.com"} ) - assert result.get("type") == "form" + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" assert result.get("errors") == {"base": "server_unreachable"} @@ -99,7 +100,7 @@ async def test_server_failure(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == "form" + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" assert not result.get("errors") with patch( @@ -109,7 +110,7 @@ async def test_server_failure(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], {"server_url": "https://example.com"} ) - assert result.get("type") == "form" + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" assert result.get("errors") == {"base": "server_failure"} @@ -130,7 +131,7 @@ async def test_hassio_discovery(hass: HomeAssistant) -> None: ), context={"source": config_entries.SOURCE_HASSIO}, ) - assert result.get("type") == "form" + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "hassio_confirm" assert result.get("description_placeholders") == {"addon": "RTSPtoWebRTC"} @@ -146,7 +147,7 @@ async def test_hassio_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result.get("type") == "create_entry" + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == "RTSPtoWebRTC" assert "result" in result assert result["result"].data == {"server_url": "http://fake-server:8083"} @@ -173,7 +174,7 @@ async def test_hassio_single_config_entry(hass: HomeAssistant) -> None: ), context={"source": config_entries.SOURCE_HASSIO}, ) - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" @@ -196,7 +197,7 @@ async def test_hassio_ignored(hass: HomeAssistant) -> None: ), context={"source": config_entries.SOURCE_HASSIO}, ) - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" @@ -217,7 +218,7 @@ async def test_hassio_discovery_server_failure(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_HASSIO}, ) - assert result.get("type") == "form" + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "hassio_confirm" assert not result.get("errors") @@ -226,7 +227,7 @@ async def test_hassio_discovery_server_failure(hass: HomeAssistant) -> None: side_effect=rtsp_to_webrtc.exceptions.ResponseError(), ): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result.get("type") == "abort" + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "server_failure" @@ -246,7 +247,7 @@ async def test_options_flow( assert not config_entry.options result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" data_schema = result["data_schema"].schema assert set(data_schema) == {"stun_server"} @@ -257,17 +258,17 @@ async def test_options_flow( "stun_server": "example.com:1234", }, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() assert config_entry.options == {"stun_server": "example.com:1234"} # Clear the value result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == "form" + 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"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() assert config_entry.options == {} diff --git a/tests/components/rympro/test_config_flow.py b/tests/components/rympro/test_config_flow.py index 05078eb9a6c..e92b7c23357 100644 --- a/tests/components/rympro/test_config_flow.py +++ b/tests/components/rympro/test_config_flow.py @@ -171,7 +171,7 @@ async def test_form_reauth(hass: HomeAssistant, config_entry) -> None: }, data=config_entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -197,7 +197,7 @@ async def test_form_reauth(hass: HomeAssistant, config_entry) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert config_entry.data[CONF_PASSWORD] == "new_password" assert len(mock_setup_entry.mock_calls) == 1 @@ -214,7 +214,7 @@ async def test_form_reauth_with_new_account(hass: HomeAssistant, config_entry) - }, data=config_entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -240,7 +240,7 @@ async def test_form_reauth_with_new_account(hass: HomeAssistant, config_entry) - ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert config_entry.data[CONF_UNIQUE_ID] == "new-account-number" assert config_entry.unique_id == "new-account-number" diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 8ce1467b451..1ca8fc82151 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -214,7 +214,7 @@ async def test_user_legacy(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # entry was added @@ -222,7 +222,7 @@ async def test_user_legacy(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_USER_DATA ) # legacy tv entry created - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "fake_name" assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_NAME] == "fake_name" @@ -243,7 +243,7 @@ async def test_user_legacy_does_not_ok_first_time(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # entry was added result2 = await hass.config_entries.flow.async_configure( @@ -257,7 +257,7 @@ async def test_user_legacy_does_not_ok_first_time(hass: HomeAssistant) -> None: ) # legacy tv entry created - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "fake_name" assert result3["data"][CONF_HOST] == "fake_host" assert result3["data"][CONF_NAME] == "fake_name" @@ -277,7 +277,7 @@ async def test_user_websocket(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" # entry was added @@ -285,7 +285,7 @@ async def test_user_websocket(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_USER_DATA ) # websocket tv entry created - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Living Room (82GXARRS)" assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_NAME] == "Living Room" @@ -304,7 +304,7 @@ async def test_user_encrypted_websocket( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -321,7 +321,7 @@ async def test_user_encrypted_websocket( result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "encrypted_pairing" result3 = await hass.config_entries.flow.async_configure( @@ -334,7 +334,7 @@ async def test_user_encrypted_websocket( result3["flow_id"], user_input={"pin": "1234"} ) - assert result4["type"] == "create_entry" + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == "TV-UE48JU6470 (UE48JU6400)" assert result4["data"][CONF_HOST] == "fake_host" assert result4["data"][CONF_NAME] == "TV-UE48JU6470" @@ -385,7 +385,7 @@ async def test_user_legacy_not_supported(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == RESULT_NOT_SUPPORTED @@ -406,7 +406,7 @@ async def test_user_websocket_not_supported(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == RESULT_NOT_SUPPORTED @@ -429,7 +429,7 @@ async def test_user_websocket_access_denied( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == RESULT_NOT_SUPPORTED assert "Please check the Device Connection Manager on your TV" in caplog.text @@ -491,7 +491,7 @@ async def test_user_not_successful(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == RESULT_CANNOT_CONNECT @@ -511,7 +511,7 @@ async def test_user_not_successful_2(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == RESULT_CANNOT_CONNECT @@ -522,14 +522,14 @@ async def test_ssdp(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" # entry was added result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input="whatever" ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "fake_model" assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_NAME] == "fake_model" @@ -546,7 +546,7 @@ async def test_ssdp_no_manufacturer(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA_NO_MANUFACTURER, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -574,13 +574,13 @@ async def test_ssdp_noprefix(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA_NOPREFIX, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" # entry was added result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input="whatever" ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "fake2_model" assert result["data"][CONF_HOST] == "fake2_host" assert result["data"][CONF_NAME] == "fake2_model" @@ -600,7 +600,7 @@ async def test_ssdp_legacy_missing_auth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" # missing authentication @@ -650,13 +650,13 @@ async def test_ssdp_websocket_success_populates_mac_address_and_ssdp_location( context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA_RENDERING_CONTROL_ST, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input="whatever" ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Living Room (82GXARRS)" assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_NAME] == "Living Room" @@ -680,13 +680,13 @@ async def test_ssdp_websocket_success_populates_mac_address_and_main_tv_ssdp_loc context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA_MAIN_TV_AGENT_ST, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input="whatever" ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Living Room (82GXARRS)" assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_NAME] == "Living Room" @@ -710,7 +710,7 @@ async def test_ssdp_encrypted_websocket_success_populates_mac_address_and_ssdp_l context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA_RENDERING_CONTROL_ST, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" with patch( @@ -738,7 +738,7 @@ async def test_ssdp_encrypted_websocket_success_populates_mac_address_and_ssdp_l result3["flow_id"], user_input={"pin": "1234"} ) - assert result4["type"] == "create_entry" + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == "TV-UE48JU6470 (UE48JU6400)" assert result4["data"][CONF_HOST] == "fake_host" assert result4["data"][CONF_NAME] == "TV-UE48JU6470" @@ -793,7 +793,7 @@ async def test_ssdp_websocket_cannot_connect(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == RESULT_CANNOT_CONNECT @@ -807,7 +807,7 @@ async def test_ssdp_model_not_supported(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA_WRONGMODEL, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == RESULT_NOT_SUPPORTED @@ -832,14 +832,14 @@ async def test_ssdp_not_successful(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" # device not found result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input="whatever" ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == RESULT_CANNOT_CONNECT @@ -864,14 +864,14 @@ async def test_ssdp_not_successful_2(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" # device not found result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input="whatever" ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == RESULT_CANNOT_CONNECT @@ -886,14 +886,14 @@ async def test_ssdp_already_in_progress(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" # failed as already in progress result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == RESULT_ALREADY_IN_PROGRESS @@ -908,7 +908,7 @@ async def test_ssdp_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY entry = result["result"] assert entry.data[CONF_MANUFACTURER] == DEFAULT_MANUFACTURER assert entry.data[CONF_MODEL] == "fake_model" @@ -918,7 +918,7 @@ async def test_ssdp_already_configured(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == RESULT_ALREADY_CONFIGURED # check updated device info @@ -935,14 +935,14 @@ async def test_dhcp_wireless(hass: HomeAssistant) -> None: data=MOCK_DHCP_DATA, ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" # entry was added result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input="whatever" ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "TV-UE48JU6470 (UE48JU6400)" assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_NAME] == "TV-UE48JU6470" @@ -964,14 +964,14 @@ async def test_dhcp_wired(hass: HomeAssistant, rest_api: Mock) -> None: data=MOCK_DHCP_DATA, ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" # entry was added result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input="whatever" ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Samsung Frame (43) (UE43LS003)" assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_NAME] == "Samsung Frame (43)" @@ -990,14 +990,14 @@ async def test_zeroconf(hass: HomeAssistant) -> None: data=MOCK_ZEROCONF_DATA, ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" # entry was added result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input="whatever" ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Living Room (82GXARRS)" assert result["data"][CONF_HOST] == "127.0.0.1" assert result["data"][CONF_NAME] == "Living Room" @@ -1026,7 +1026,7 @@ async def test_zeroconf_ignores_soundbar(hass: HomeAssistant, rest_api: Mock) -> data=MOCK_ZEROCONF_DATA, ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -1039,7 +1039,7 @@ async def test_zeroconf_no_device_info(hass: HomeAssistant) -> None: data=MOCK_ZEROCONF_DATA, ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -1052,7 +1052,7 @@ async def test_zeroconf_and_dhcp_same_time(hass: HomeAssistant) -> None: data=MOCK_DHCP_DATA, ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result2 = await hass.config_entries.flow.async_init( @@ -1061,7 +1061,7 @@ async def test_zeroconf_and_dhcp_same_time(hass: HomeAssistant) -> None: data=MOCK_ZEROCONF_DATA, ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_in_progress" @@ -1103,7 +1103,7 @@ async def test_autodetect_websocket(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_METHOD] == "websocket" assert result["data"][CONF_TOKEN] == "123456789" remotews.assert_called_once_with(**AUTODETECT_WEBSOCKET_SSL) @@ -1153,7 +1153,7 @@ async def test_websocket_no_mac(hass: HomeAssistant, mac_address: Mock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_METHOD] == "websocket" assert result["data"][CONF_TOKEN] == "123456789" assert result["data"][CONF_MAC] == "gg:ee:tt:mm:aa:cc" @@ -1205,7 +1205,7 @@ async def test_autodetect_not_supported(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == RESULT_NOT_SUPPORTED assert remote.call_count == 1 assert remote.call_args_list == [call(AUTODETECT_LEGACY)] @@ -1217,7 +1217,7 @@ async def test_autodetect_legacy(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"][CONF_METHOD] == "legacy" assert result["data"][CONF_NAME] == "fake_name" assert result["data"][CONF_MAC] is None @@ -1239,7 +1239,7 @@ async def test_autodetect_none(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == RESULT_CANNOT_CONNECT assert remote.call_count == 1 assert remote.call_args_list == [ @@ -1268,7 +1268,7 @@ async def test_update_old_entry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == RESULT_ALREADY_CONFIGURED config_entries_domain = hass.config_entries.async_entries(DOMAIN) @@ -1297,7 +1297,7 @@ async def test_update_missing_mac_unique_id_added_from_dhcp( await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_MAC] == "aa:bb:cc:dd:ee:ff" assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" @@ -1321,7 +1321,7 @@ async def test_update_incorrectly_formatted_mac_unique_id_added_from_dhcp( await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_MAC] == "aa:bb:cc:dd:ee:ff" assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" @@ -1345,7 +1345,7 @@ async def test_update_missing_mac_unique_id_added_from_zeroconf( await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_MAC] == "aa:bb:zz:ee:rr:oo" assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" @@ -1371,7 +1371,7 @@ async def test_update_missing_model_added_from_ssdp( await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_MODEL] == "fake_model" @@ -1392,7 +1392,7 @@ async def test_update_missing_mac_unique_id_ssdp_location_added_from_ssdp( await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_MAC] == "aa:bb:aa:aa:aa:aa" # Wrong st @@ -1419,7 +1419,7 @@ async def test_update_zeroconf_discovery_preserved_unique_id( data=MOCK_ZEROCONF_DATA, ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" assert entry.data[CONF_MAC] == "aa:bb:zz:ee:rr:oo" assert entry.unique_id == "original" @@ -1448,7 +1448,7 @@ async def test_update_missing_mac_unique_id_added_ssdp_location_updated_from_ssd await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_MAC] == "aa:bb:aa:aa:aa:aa" # Wrong ST, ssdp location should not change @@ -1481,7 +1481,7 @@ async def test_update_missing_mac_unique_id_added_ssdp_location_rendering_st_upd await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_MAC] == "aa:bb:aa:aa:aa:aa" # Correct ST, ssdp location should change @@ -1516,7 +1516,7 @@ async def test_update_missing_mac_unique_id_added_ssdp_location_main_tv_agent_st await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_MAC] == "aa:bb:aa:aa:aa:aa" # Main TV Agent ST, ssdp location should change @@ -1551,7 +1551,7 @@ async def test_update_ssdp_location_rendering_st_updated_from_ssdp( await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_MAC] == "aa:bb:aa:aa:aa:aa" # Correct ST, ssdp location should be added @@ -1582,7 +1582,7 @@ async def test_update_main_tv_ssdp_location_rendering_st_updated_from_ssdp( await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_MAC] == "aa:bb:aa:aa:aa:aa" # Correct ST for MainTV, ssdp location should be added @@ -1613,7 +1613,7 @@ async def test_update_missing_mac_added_unique_id_preserved_from_zeroconf( await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_MAC] == "aa:bb:zz:ee:rr:oo" assert entry.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" @@ -1641,7 +1641,7 @@ async def test_update_legacy_missing_mac_from_dhcp( await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_MAC] == "aa:bb:cc:dd:ee:ff" assert entry.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" @@ -1678,7 +1678,7 @@ async def test_update_legacy_missing_mac_from_dhcp_no_unique_id( await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" assert entry.data[CONF_MAC] == "aa:bb:cc:dd:ee:ff" assert entry.unique_id is None @@ -1704,7 +1704,7 @@ async def test_update_ssdp_location_unique_id_added_from_ssdp( await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_MAC] == "aa:bb:aa:aa:aa:aa" # Wrong st @@ -1732,7 +1732,7 @@ async def test_update_ssdp_location_unique_id_added_from_ssdp_with_rendering_con await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_MAC] == "aa:bb:aa:aa:aa:aa" # Correct st @@ -1753,7 +1753,7 @@ async def test_form_reauth_legacy(hass: HomeAssistant) -> None: context={"entry_id": entry.entry_id, "source": config_entries.SOURCE_REAUTH}, data=entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -1761,7 +1761,7 @@ async def test_form_reauth_legacy(hass: HomeAssistant) -> None: {}, ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -1777,7 +1777,7 @@ async def test_form_reauth_websocket(hass: HomeAssistant) -> None: context={"entry_id": entry.entry_id, "source": config_entries.SOURCE_REAUTH}, data=entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -1785,7 +1785,7 @@ async def test_form_reauth_websocket(hass: HomeAssistant) -> None: {}, ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.state == config_entries.ConfigEntryState.LOADED @@ -1802,7 +1802,7 @@ async def test_form_reauth_websocket_cannot_connect( context={"entry_id": entry.entry_id, "source": config_entries.SOURCE_REAUTH}, data=entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch.object(remotews, "open", side_effect=ConnectionFailure): @@ -1812,7 +1812,7 @@ async def test_form_reauth_websocket_cannot_connect( ) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": RESULT_AUTH_MISSING} result3 = await hass.config_entries.flow.async_configure( @@ -1821,7 +1821,7 @@ async def test_form_reauth_websocket_cannot_connect( ) await hass.async_block_till_done() - assert result3["type"] == "abort" + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" @@ -1834,7 +1834,7 @@ async def test_form_reauth_websocket_not_supported(hass: HomeAssistant) -> None: context={"entry_id": entry.entry_id, "source": config_entries.SOURCE_REAUTH}, data=entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -1847,7 +1847,7 @@ async def test_form_reauth_websocket_not_supported(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "not_supported" @@ -1867,7 +1867,7 @@ async def test_form_reauth_encrypted(hass: HomeAssistant) -> None: context={"entry_id": entry.entry_id, "source": config_entries.SOURCE_REAUTH}, data=entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -1882,7 +1882,7 @@ async def test_form_reauth_encrypted(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -1891,14 +1891,14 @@ async def test_form_reauth_encrypted(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm_encrypted" # Invalid PIN result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"pin": "invalid"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm_encrypted" # Valid PIN @@ -1906,7 +1906,7 @@ async def test_form_reauth_encrypted(hass: HomeAssistant) -> None: result["flow_id"], user_input={"pin": "1234"} ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.state == config_entries.ConfigEntryState.LOADED @@ -1945,7 +1945,7 @@ async def test_update_incorrect_udn_matching_upnp_udn_unique_id_added_from_ssdp( await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_MAC] == "aa:bb:aa:aa:aa:aa" assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" @@ -1971,7 +1971,7 @@ async def test_update_incorrect_udn_matching_mac_unique_id_added_from_ssdp( await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_MAC] == "aa:bb:aa:aa:aa:aa" assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" @@ -1998,7 +1998,7 @@ async def test_update_incorrect_udn_matching_mac_from_dhcp( await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_MAC] == "aa:bb:aa:aa:aa:aa" assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" @@ -2025,7 +2025,7 @@ async def test_no_update_incorrect_udn_not_matching_mac_from_dhcp( await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 0 - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" assert entry.data[CONF_MAC] == "aa:bb:ss:ss:dd:pp" assert entry.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" diff --git a/tests/components/screenlogic/test_config_flow.py b/tests/components/screenlogic/test_config_flow.py index be1617e3105..8ca6bd4cb90 100644 --- a/tests/components/screenlogic/test_config_flow.py +++ b/tests/components/screenlogic/test_config_flow.py @@ -24,6 +24,7 @@ from homeassistant.components.screenlogic.const import ( ) from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, CONF_SCAN_INTERVAL from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -47,7 +48,7 @@ async def test_flow_discovery(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "gateway_select" @@ -60,7 +61,7 @@ async def test_flow_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Pentair: 01-01-01" assert result2["data"] == { CONF_IP_ADDRESS: "1.1.1.1", @@ -80,7 +81,7 @@ async def test_flow_discover_none(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "gateway_entry" @@ -96,7 +97,7 @@ async def test_flow_discover_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "gateway_entry" @@ -119,7 +120,7 @@ async def test_flow_discover_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Pentair: 01-01-01" assert result3["data"] == { CONF_IP_ADDRESS: "1.1.1.1", @@ -141,7 +142,7 @@ async def test_dhcp(hass: HomeAssistant) -> None: ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "gateway_entry" with ( @@ -163,7 +164,7 @@ async def test_dhcp(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Pentair: 01-01-01" assert result3["data"] == { CONF_IP_ADDRESS: "1.1.1.1", @@ -190,7 +191,7 @@ async def test_form_manual_entry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "gateway_select" @@ -198,7 +199,7 @@ async def test_form_manual_entry(hass: HomeAssistant) -> None: result["flow_id"], user_input={GATEWAY_SELECT_KEY: GATEWAY_MANUAL_ENTRY} ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {} assert result2["step_id"] == "gateway_entry" @@ -221,7 +222,7 @@ async def test_form_manual_entry(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "Pentair: 01-01-01" assert result3["data"] == { CONF_IP_ADDRESS: "1.1.1.1", @@ -252,7 +253,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {CONF_IP_ADDRESS: "cannot_connect"} @@ -270,14 +271,14 @@ async def test_option_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + 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_SCAN_INTERVAL: 15}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_SCAN_INTERVAL: 15} @@ -295,13 +296,13 @@ async def test_option_flow_defaults(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + 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"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL, } @@ -321,13 +322,13 @@ async def test_option_flow_input_floor(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + 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_SCAN_INTERVAL: 1} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_SCAN_INTERVAL: MIN_SCAN_INTERVAL, } diff --git a/tests/components/sense/test_config_flow.py b/tests/components/sense/test_config_flow.py index dc1cee43662..e564603ea87 100644 --- a/tests/components/sense/test_config_flow.py +++ b/tests/components/sense/test_config_flow.py @@ -14,6 +14,7 @@ from homeassistant import config_entries from homeassistant.components.sense.const import DOMAIN from homeassistant.const import CONF_CODE from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -51,7 +52,7 @@ async def test_form(hass: HomeAssistant, mock_sense) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -64,7 +65,7 @@ async def test_form(hass: HomeAssistant, mock_sense) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "test-email" assert result2["data"] == MOCK_CONFIG assert len(mock_setup_entry.mock_calls) == 1 @@ -85,7 +86,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: {"timeout": "6", "email": "test-email", "password": "test-password"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -102,7 +103,7 @@ async def test_form_mfa_required(hass: HomeAssistant, mock_sense) -> None: {"timeout": "6", "email": "test-email", "password": "test-password"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "validation" mock_sense.return_value.validate_mfa.side_effect = None @@ -111,7 +112,7 @@ async def test_form_mfa_required(hass: HomeAssistant, mock_sense) -> None: {CONF_CODE: "012345"}, ) - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "test-email" assert result3["data"] == MOCK_CONFIG @@ -129,7 +130,7 @@ async def test_form_mfa_required_wrong(hass: HomeAssistant, mock_sense) -> None: {"timeout": "6", "email": "test-email", "password": "test-password"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "validation" mock_sense.return_value.validate_mfa.side_effect = SenseAuthenticationException @@ -139,7 +140,7 @@ async def test_form_mfa_required_wrong(hass: HomeAssistant, mock_sense) -> None: {CONF_CODE: "000000"}, ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": "invalid_auth"} assert result3["step_id"] == "validation" @@ -157,7 +158,7 @@ async def test_form_mfa_required_timeout(hass: HomeAssistant, mock_sense) -> Non {"timeout": "6", "email": "test-email", "password": "test-password"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "validation" mock_sense.return_value.validate_mfa.side_effect = SenseAPITimeoutException @@ -166,7 +167,7 @@ async def test_form_mfa_required_timeout(hass: HomeAssistant, mock_sense) -> Non {CONF_CODE: "000000"}, ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": "cannot_connect"} @@ -183,7 +184,7 @@ async def test_form_mfa_required_exception(hass: HomeAssistant, mock_sense) -> N {"timeout": "6", "email": "test-email", "password": "test-password"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "validation" mock_sense.return_value.validate_mfa.side_effect = Exception @@ -192,7 +193,7 @@ async def test_form_mfa_required_exception(hass: HomeAssistant, mock_sense) -> N {CONF_CODE: "000000"}, ) - assert result3["type"] == "form" + assert result3["type"] is FlowResultType.FORM assert result3["errors"] == {"base": "unknown"} @@ -211,7 +212,7 @@ async def test_form_timeout(hass: HomeAssistant) -> None: {"timeout": "6", "email": "test-email", "password": "test-password"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -230,7 +231,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: {"timeout": "6", "email": "test-email", "password": "test-password"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -249,7 +250,7 @@ async def test_form_unknown_exception(hass: HomeAssistant) -> None: {"timeout": "6", "email": "test-email", "password": "test-password"}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -270,7 +271,7 @@ async def test_reauth_no_form(hass: HomeAssistant, mock_sense) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=MOCK_CONFIG ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" @@ -290,7 +291,7 @@ async def test_reauth_password(hass: HomeAssistant, mock_sense) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=entry.data ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM mock_sense.return_value.authenticate.side_effect = None with patch( @@ -303,5 +304,5 @@ async def test_reauth_password(hass: HomeAssistant, mock_sense) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" diff --git a/tests/components/sentry/test_config_flow.py b/tests/components/sentry/test_config_flow.py index f3136b639de..bd24023ac5e 100644 --- a/tests/components/sentry/test_config_flow.py +++ b/tests/components/sentry/test_config_flow.py @@ -44,7 +44,7 @@ async def test_full_user_flow_implementation(hass: HomeAssistant) -> None: {"dsn": "http://public@sentry.local/1"}, ) - assert result2.get("type") == "create_entry" + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "Sentry" assert result2.get("data") == { "dsn": "http://public@sentry.local/1", diff --git a/tests/components/sharkiq/test_config_flow.py b/tests/components/sharkiq/test_config_flow.py index a81c185fd71..cf75bff1686 100644 --- a/tests/components/sharkiq/test_config_flow.py +++ b/tests/components/sharkiq/test_config_flow.py @@ -9,6 +9,7 @@ from sharkiq import AylaApi, SharkIqAuthError, SharkIqError from homeassistant import config_entries from homeassistant.components.sharkiq.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.setup import async_setup_component from .const import ( @@ -41,7 +42,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -56,7 +57,7 @@ async def test_form(hass: HomeAssistant) -> None: CONFIG, ) - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == f"{TEST_USERNAME:s}" assert result2["data"] == { "username": TEST_USERNAME, @@ -89,7 +90,7 @@ async def test_form_error(hass: HomeAssistant, exc: Exception, base_error: str) CONFIG, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"].get("base") == base_error @@ -105,7 +106,7 @@ async def test_reauth_success(hass: HomeAssistant) -> None: data=CONFIG, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" diff --git a/tests/components/sia/test_config_flow.py b/tests/components/sia/test_config_flow.py index c46a2ebbf46..36f2292bdea 100644 --- a/tests/components/sia/test_config_flow.py +++ b/tests/components/sia/test_config_flow.py @@ -205,7 +205,7 @@ async def test_abort_form(hass: HomeAssistant) -> None: get_abort = await hass.config_entries.flow.async_configure( start_another_flow["flow_id"], BASIC_CONFIG ) - assert get_abort["type"] == "abort" + assert get_abort["type"] is FlowResultType.ABORT assert get_abort["reason"] == "already_configured" @@ -239,7 +239,7 @@ async def test_validation_errors_user( flow_id = flow_at_user_step["flow_id"] config[field] = value result_err = await hass.config_entries.flow.async_configure(flow_id, config) - assert result_err["type"] == "form" + assert result_err["type"] is FlowResultType.FORM assert result_err["errors"] == {"base": error} @@ -269,7 +269,7 @@ async def test_validation_errors_account( flow_id = flow_at_add_account_step["flow_id"] config[field] = value result_err = await hass.config_entries.flow.async_configure(flow_id, config) - assert result_err["type"] == "form" + assert result_err["type"] is FlowResultType.FORM assert result_err["errors"] == {"base": error} diff --git a/tests/components/sleepiq/test_config_flow.py b/tests/components/sleepiq/test_config_flow.py index fff483d2f15..af08f5aa9fe 100644 --- a/tests/components/sleepiq/test_config_flow.py +++ b/tests/components/sleepiq/test_config_flow.py @@ -81,7 +81,7 @@ async def test_success(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch("asyncsleepiq.AsyncSleepIQ.login", return_value=True): diff --git a/tests/components/smart_meter_texas/test_config_flow.py b/tests/components/smart_meter_texas/test_config_flow.py index 53f7a2eb5fd..a98597686d5 100644 --- a/tests/components/smart_meter_texas/test_config_flow.py +++ b/tests/components/smart_meter_texas/test_config_flow.py @@ -13,6 +13,7 @@ from homeassistant import config_entries from homeassistant.components.smart_meter_texas.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -25,7 +26,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -40,7 +41,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_LOGIN[CONF_USERNAME] assert result2["data"] == TEST_LOGIN assert len(mock_setup_entry.mock_calls) == 1 @@ -61,7 +62,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: TEST_LOGIN, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -82,7 +83,7 @@ async def test_form_cannot_connect(hass: HomeAssistant, side_effect) -> None: result["flow_id"], TEST_LOGIN ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -101,7 +102,7 @@ async def test_form_unknown_exception(hass: HomeAssistant) -> None: TEST_LOGIN, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -123,5 +124,5 @@ async def test_form_duplicate_account(hass: HomeAssistant) -> None: data={"username": "user123", "password": "password123"}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/smarttub/test_config_flow.py b/tests/components/smarttub/test_config_flow.py index 47204e2154e..c625f217405 100644 --- a/tests/components/smarttub/test_config_flow.py +++ b/tests/components/smarttub/test_config_flow.py @@ -18,7 +18,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( @@ -30,7 +30,7 @@ async def test_form(hass: HomeAssistant) -> None: {CONF_EMAIL: "test-email", CONF_PASSWORD: "test-password"}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "test-email" assert result["data"] == { CONF_EMAIL: "test-email", @@ -53,7 +53,7 @@ async def test_form_invalid_auth(hass: HomeAssistant, smarttub_api) -> None: {CONF_EMAIL: "test-email", CONF_PASSWORD: "test-password"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} diff --git a/tests/components/solarlog/test_config_flow.py b/tests/components/solarlog/test_config_flow.py index 4b840dd0cf9..c356a129806 100644 --- a/tests/components/solarlog/test_config_flow.py +++ b/tests/components/solarlog/test_config_flow.py @@ -23,7 +23,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -41,7 +41,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + 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 len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/solax/test_config_flow.py b/tests/components/solax/test_config_flow.py index 61bd9003439..c787962cc8c 100644 --- a/tests/components/solax/test_config_flow.py +++ b/tests/components/solax/test_config_flow.py @@ -10,6 +10,7 @@ from homeassistant import config_entries from homeassistant.components.solax.const import DOMAIN from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType def __mock_real_time_api_success(): @@ -31,7 +32,7 @@ async def test_form_success(hass: HomeAssistant) -> None: flow = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert flow["type"] == "form" + assert flow["type"] is FlowResultType.FORM assert flow["errors"] == {} with ( @@ -51,7 +52,7 @@ async def test_form_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert entry_result["type"] == "create_entry" + assert entry_result["type"] is FlowResultType.CREATE_ENTRY assert entry_result["title"] == "ABCDEFGHIJ" assert entry_result["data"] == { CONF_IP_ADDRESS: "192.168.1.87", @@ -66,7 +67,7 @@ async def test_form_connect_error(hass: HomeAssistant) -> None: flow = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert flow["type"] == "form" + assert flow["type"] is FlowResultType.FORM assert flow["errors"] == {} with patch( @@ -78,7 +79,7 @@ async def test_form_connect_error(hass: HomeAssistant) -> None: {CONF_IP_ADDRESS: "192.168.1.87", CONF_PORT: 80, CONF_PASSWORD: "password"}, ) - assert entry_result["type"] == "form" + assert entry_result["type"] is FlowResultType.FORM assert entry_result["errors"] == {"base": "cannot_connect"} @@ -87,7 +88,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: flow = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert flow["type"] == "form" + assert flow["type"] is FlowResultType.FORM assert flow["errors"] == {} with patch( @@ -99,5 +100,5 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: {CONF_IP_ADDRESS: "192.168.1.87", CONF_PORT: 80, CONF_PASSWORD: "password"}, ) - assert entry_result["type"] == "form" + assert entry_result["type"] is FlowResultType.FORM assert entry_result["errors"] == {"base": "unknown"} diff --git a/tests/components/somfy_mylink/test_config_flow.py b/tests/components/somfy_mylink/test_config_flow.py index 175fcd68477..9084d988ec9 100644 --- a/tests/components/somfy_mylink/test_config_flow.py +++ b/tests/components/somfy_mylink/test_config_flow.py @@ -24,7 +24,7 @@ async def test_form_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -47,7 +47,7 @@ async def test_form_user(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "MyLink 1.1.1.1" assert result2["data"] == { CONF_HOST: "1.1.1.1", @@ -68,7 +68,7 @@ async def test_form_user_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -91,7 +91,7 @@ async def test_form_user_already_configured(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert len(mock_setup_entry.mock_calls) == 0 @@ -118,7 +118,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -141,7 +141,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -164,7 +164,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -272,7 +272,7 @@ async def test_form_user_already_configured_from_dhcp(hass: HomeAssistant) -> No await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert len(mock_setup_entry.mock_calls) == 0 @@ -293,7 +293,7 @@ async def test_already_configured_with_ignored(hass: HomeAssistant) -> None: hostname="somfy_eeff", ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM async def test_dhcp_discovery(hass: HomeAssistant) -> None: @@ -308,7 +308,7 @@ async def test_dhcp_discovery(hass: HomeAssistant) -> None: hostname="somfy_eeff", ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -331,7 +331,7 @@ async def test_dhcp_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "MyLink 1.1.1.1" assert result2["data"] == { CONF_HOST: "1.1.1.1", diff --git a/tests/components/songpal/test_config_flow.py b/tests/components/songpal/test_config_flow.py index 12c1ef3ec70..8f503360702 100644 --- a/tests/components/songpal/test_config_flow.py +++ b/tests/components/songpal/test_config_flow.py @@ -64,7 +64,7 @@ async def test_flow_ssdp(hass: HomeAssistant) -> None: context={"source": SOURCE_SSDP}, data=SSDP_DATA, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["description_placeholders"] == { CONF_NAME: FRIENDLY_NAME, diff --git a/tests/components/sonos/test_config_flow.py b/tests/components/sonos/test_config_flow.py index 186e45e3d84..141013dec20 100644 --- a/tests/components/sonos/test_config_flow.py +++ b/tests/components/sonos/test_config_flow.py @@ -11,6 +11,7 @@ from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.sonos.const import DATA_SONOS_DISCOVERY_MANAGER, DOMAIN from homeassistant.const import CONF_HOSTS from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.setup import async_setup_component @@ -24,9 +25,9 @@ async def test_user_form( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices_found" # Initiate a discovery to allow config entry creation @@ -40,7 +41,7 @@ async def test_user_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( patch( @@ -58,7 +59,7 @@ async def test_user_form( ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Sonos" assert result2["data"] == {} assert len(mock_setup.mock_calls) == 1 @@ -78,7 +79,7 @@ async def test_user_form_already_created(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -93,7 +94,7 @@ async def test_zeroconf_form( context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf_payload, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -112,7 +113,7 @@ async def test_zeroconf_form( ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Sonos" assert result2["data"] == {} @@ -157,7 +158,7 @@ async def test_ssdp_discovery(hass: HomeAssistant, soco) -> None: ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Sonos" assert result["data"] == {} @@ -191,7 +192,7 @@ async def test_zeroconf_sonos_v1(hass: HomeAssistant) -> None: }, ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -210,7 +211,7 @@ async def test_zeroconf_sonos_v1(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Sonos" assert result2["data"] == {} @@ -232,6 +233,6 @@ async def test_zeroconf_form_not_sonos( context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf_payload, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_sonos_device" assert len(mock_manager.mock_calls) == 0 diff --git a/tests/components/starline/test_config_flow.py b/tests/components/starline/test_config_flow.py index 0d7d7cc0731..291bf143cbf 100644 --- a/tests/components/starline/test_config_flow.py +++ b/tests/components/starline/test_config_flow.py @@ -5,6 +5,7 @@ import requests_mock from homeassistant import config_entries from homeassistant.components.starline import config_flow from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType TEST_APP_ID = "666" TEST_APP_SECRET = "appsecret" @@ -45,7 +46,7 @@ async def test_flow_works(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth_app" result = await hass.config_entries.flow.async_configure( @@ -55,7 +56,7 @@ async def test_flow_works(hass: HomeAssistant) -> None: config_flow.CONF_APP_SECRET: TEST_APP_SECRET, }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth_user" result = await hass.config_entries.flow.async_configure( @@ -65,7 +66,7 @@ async def test_flow_works(hass: HomeAssistant) -> None: config_flow.CONF_PASSWORD: TEST_APP_PASSWORD, }, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == f"Application {TEST_APP_ID}" @@ -83,7 +84,7 @@ async def test_step_auth_app_code_falls(hass: HomeAssistant) -> None: config_flow.CONF_APP_SECRET: TEST_APP_SECRET, }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth_app" assert result["errors"] == {"base": "error_auth_app"} @@ -106,7 +107,7 @@ async def test_step_auth_app_token_falls(hass: HomeAssistant) -> None: config_flow.CONF_APP_SECRET: TEST_APP_SECRET, }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth_app" assert result["errors"] == {"base": "error_auth_app"} @@ -123,6 +124,6 @@ async def test_step_auth_user_falls(hass: HomeAssistant) -> None: config_flow.CONF_PASSWORD: TEST_APP_PASSWORD, } ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth_user" assert result["errors"] == {"base": "error_auth_user"} diff --git a/tests/components/steamist/test_config_flow.py b/tests/components/steamist/test_config_flow.py index 6152721ed0a..40578113bb3 100644 --- a/tests/components/steamist/test_config_flow.py +++ b/tests/components/steamist/test_config_flow.py @@ -173,7 +173,7 @@ async def test_discovery(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] diff --git a/tests/components/subaru/test_config_flow.py b/tests/components/subaru/test_config_flow.py index 76bad81bff4..9bddeeee051 100644 --- a/tests/components/subaru/test_config_flow.py +++ b/tests/components/subaru/test_config_flow.py @@ -12,6 +12,7 @@ from homeassistant.components.subaru import config_flow from homeassistant.components.subaru.const import CONF_UPDATE_ENABLED, DOMAIN from homeassistant.const import CONF_DEVICE_ID, CONF_PIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.setup import async_setup_component from .conftest import ( @@ -45,7 +46,7 @@ async def test_user_form_init(user_form) -> None: assert user_form["errors"] is None assert user_form["handler"] == DOMAIN assert user_form["step_id"] == "user" - assert user_form["type"] == "form" + assert user_form["type"] is FlowResultType.FORM async def test_user_form_repeat_identifier(hass: HomeAssistant, user_form) -> None: @@ -64,7 +65,7 @@ async def test_user_form_repeat_identifier(hass: HomeAssistant, user_form) -> No TEST_CREDS, ) assert len(mock_connect.mock_calls) == 0 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -79,7 +80,7 @@ async def test_user_form_cannot_connect(hass: HomeAssistant, user_form) -> None: TEST_CREDS, ) assert len(mock_connect.mock_calls) == 1 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -94,7 +95,7 @@ async def test_user_form_invalid_auth(hass: HomeAssistant, user_form) -> None: TEST_CREDS, ) assert len(mock_connect.mock_calls) == 1 - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -207,7 +208,7 @@ async def test_two_factor_request_fail( user_input={config_flow.CONF_CONTACT_METHOD: "email@addr.com"}, ) assert len(mock_two_factor_request.mock_calls) == 1 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "two_factor_request_failed" @@ -302,7 +303,7 @@ async def test_pin_form_bad_pin_format(hass: HomeAssistant, pin_form) -> None: ) assert len(mock_test_pin.mock_calls) == 0 assert len(mock_update_saved_pin.mock_calls) == 1 - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "bad_pin_format"} @@ -361,7 +362,7 @@ async def test_pin_form_incorrect_pin(hass: HomeAssistant, pin_form) -> None: ) assert len(mock_test_pin.mock_calls) == 1 assert len(mock_update_saved_pin.mock_calls) == 1 - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "incorrect_pin"} @@ -370,7 +371,7 @@ async def test_option_flow_form(options_form) -> None: assert options_form["description_placeholders"] is None assert options_form["errors"] is None assert options_form["step_id"] == "init" - assert options_form["type"] == "form" + assert options_form["type"] is FlowResultType.FORM async def test_option_flow(hass: HomeAssistant, options_form) -> None: @@ -381,7 +382,7 @@ async def test_option_flow(hass: HomeAssistant, options_form) -> None: CONF_UPDATE_ENABLED: False, }, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_UPDATE_ENABLED: False, } diff --git a/tests/components/sunweg/test_config_flow.py b/tests/components/sunweg/test_config_flow.py index 3f250ebc994..427e540f21b 100644 --- a/tests/components/sunweg/test_config_flow.py +++ b/tests/components/sunweg/test_config_flow.py @@ -142,7 +142,7 @@ async def test_no_plants_on_account(hass: HomeAssistant) -> None: result["flow_id"], SUNWEG_USER_INPUT ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_plants" @@ -219,5 +219,5 @@ async def test_existing_plant_configured(hass: HomeAssistant, plant_fixture) -> result["flow_id"], SUNWEG_USER_INPUT ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/surepetcare/test_config_flow.py b/tests/components/surepetcare/test_config_flow.py index c8f77012502..c3c13195aca 100644 --- a/tests/components/surepetcare/test_config_flow.py +++ b/tests/components/surepetcare/test_config_flow.py @@ -165,7 +165,7 @@ async def test_reauthentication(hass: HomeAssistant) -> None: data=old_entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "reauth_confirm" @@ -179,7 +179,7 @@ async def test_reauthentication(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -202,7 +202,7 @@ async def test_reauthentication_failure(hass: HomeAssistant) -> None: data=old_entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "reauth_confirm" @@ -217,7 +217,7 @@ async def test_reauthentication_failure(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result2["errors"]["base"] == "invalid_auth" @@ -240,7 +240,7 @@ async def test_reauthentication_cannot_connect(hass: HomeAssistant) -> None: data=old_entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "reauth_confirm" @@ -255,7 +255,7 @@ async def test_reauthentication_cannot_connect(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result2["errors"]["base"] == "cannot_connect" @@ -278,7 +278,7 @@ async def test_reauthentication_unknown_failure(hass: HomeAssistant) -> None: data=old_entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "reauth_confirm" @@ -293,5 +293,5 @@ async def test_reauthentication_unknown_failure(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result2["errors"]["base"] == "unknown" diff --git a/tests/components/swiss_public_transport/test_config_flow.py b/tests/components/swiss_public_transport/test_config_flow.py index a18db9bf2de..47969cdc9dd 100644 --- a/tests/components/swiss_public_transport/test_config_flow.py +++ b/tests/components/swiss_public_transport/test_config_flow.py @@ -34,7 +34,7 @@ async def test_flow_user_init_data_success(hass: HomeAssistant) -> None: config_flow.DOMAIN, context={"source": "user"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["handler"] == "swiss_public_transport" assert result["data_schema"] == config_flow.DATA_SCHEMA @@ -52,7 +52,7 @@ async def test_flow_user_init_data_success(hass: HomeAssistant) -> None: user_input=MOCK_DATA_STEP, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].title == "test_start test_destination" assert result["data"] == MOCK_DATA_STEP @@ -83,7 +83,7 @@ async def test_flow_user_init_data_error_and_recover( user_input=MOCK_DATA_STEP, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == text_error # Recover @@ -94,7 +94,7 @@ async def test_flow_user_init_data_error_and_recover( user_input=MOCK_DATA_STEP, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].title == "test_start test_destination" assert result["data"] == MOCK_DATA_STEP diff --git a/tests/components/switchbee/test_config_flow.py b/tests/components/switchbee/test_config_flow.py index 50ad6d22cd1..c9132972ab4 100644 --- a/tests/components/switchbee/test_config_flow.py +++ b/tests/components/switchbee/test_config_flow.py @@ -29,7 +29,7 @@ async def test_form(hass: HomeAssistant, test_cucode_in_coordinator_data) -> Non result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( diff --git a/tests/components/tado/test_config_flow.py b/tests/components/tado/test_config_flow.py index c954a4b79af..6f44bee8960 100644 --- a/tests/components/tado/test_config_flow.py +++ b/tests/components/tado/test_config_flow.py @@ -176,7 +176,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: {"username": "test-username", "password": "test-password"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -199,7 +199,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: {"username": "test-username", "password": "test-password"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -220,7 +220,7 @@ async def test_no_homes(hass: HomeAssistant) -> None: {"username": "test-username", "password": "test-password"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "no_homes"} @@ -240,7 +240,7 @@ async def test_form_homekit(hass: HomeAssistant) -> None: type="mock_type", ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} flow = next( flow @@ -267,7 +267,7 @@ async def test_form_homekit(hass: HomeAssistant) -> None: type="mock_type", ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT async def test_import_step(hass: HomeAssistant) -> None: diff --git a/tests/components/tasmota/test_config_flow.py b/tests/components/tasmota/test_config_flow.py index 2db4d7c6493..4d5b655a9c9 100644 --- a/tests/components/tasmota/test_config_flow.py +++ b/tests/components/tasmota/test_config_flow.py @@ -2,6 +2,7 @@ from homeassistant import config_entries from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers.service_info.mqtt import MqttServiceInfo from tests.common import MockConfigEntry @@ -18,7 +19,7 @@ async def test_mqtt_abort_if_existing_entry( "tasmota", context={"source": config_entries.SOURCE_MQTT} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -46,7 +47,7 @@ async def test_mqtt_abort_invalid_topic( result = await hass.config_entries.flow.async_init( "tasmota", context={"source": config_entries.SOURCE_MQTT}, data=discovery_info ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_discovery_info" discovery_info = MqttServiceInfo( @@ -60,7 +61,7 @@ async def test_mqtt_abort_invalid_topic( result = await hass.config_entries.flow.async_init( "tasmota", context={"source": config_entries.SOURCE_MQTT}, data=discovery_info ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_discovery_info" discovery_info = MqttServiceInfo( @@ -83,7 +84,7 @@ async def test_mqtt_abort_invalid_topic( result = await hass.config_entries.flow.async_init( "tasmota", context={"source": config_entries.SOURCE_MQTT}, data=discovery_info ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM async def test_mqtt_setup(hass: HomeAssistant, mqtt_mock: MqttMockHAClient) -> None: @@ -108,11 +109,11 @@ async def test_mqtt_setup(hass: HomeAssistant, mqtt_mock: MqttMockHAClient) -> N result = await hass.config_entries.flow.async_init( "tasmota", context={"source": config_entries.SOURCE_MQTT}, data=discovery_info ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].data == {"discovery_prefix": "tasmota/discovery"} @@ -121,11 +122,11 @@ async def test_user_setup(hass: HomeAssistant, mqtt_mock: MqttMockHAClient) -> N result = await hass.config_entries.flow.async_init( "tasmota", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].data == { "discovery_prefix": "tasmota/discovery", } @@ -139,13 +140,13 @@ async def test_user_setup_advanced( "tasmota", context={"source": config_entries.SOURCE_USER, "show_advanced_options": True}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"discovery_prefix": "test_tasmota/discovery"} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].data == { "discovery_prefix": "test_tasmota/discovery", } @@ -159,13 +160,13 @@ async def test_user_setup_advanced_strip_wildcard( "tasmota", context={"source": config_entries.SOURCE_USER, "show_advanced_options": True}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"discovery_prefix": "test_tasmota/discovery/#"} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].data == { "discovery_prefix": "test_tasmota/discovery", } @@ -179,13 +180,13 @@ async def test_user_setup_invalid_topic_prefix( "tasmota", context={"source": config_entries.SOURCE_USER, "show_advanced_options": True}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"discovery_prefix": "tasmota/config/##"} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == "invalid_discovery_topic" @@ -198,5 +199,5 @@ async def test_user_single_instance( result = await hass.config_entries.flow.async_init( "tasmota", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/tesla_wall_connector/test_config_flow.py b/tests/components/tesla_wall_connector/test_config_flow.py index 84d655a629e..a0c28262658 100644 --- a/tests/components/tesla_wall_connector/test_config_flow.py +++ b/tests/components/tesla_wall_connector/test_config_flow.py @@ -98,7 +98,7 @@ async def test_form_already_configured( ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" # Test config entry got updated with latest IP @@ -120,7 +120,7 @@ async def test_dhcp_can_finish( ), ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -154,7 +154,7 @@ async def test_dhcp_already_exists( ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -178,5 +178,5 @@ async def test_dhcp_error_from_wall_connector( ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" diff --git a/tests/components/tplink/test_config_flow.py b/tests/components/tplink/test_config_flow.py index 4e80ce3e890..e83ff173701 100644 --- a/tests/components/tplink/test_config_flow.py +++ b/tests/components/tplink/test_config_flow.py @@ -55,13 +55,13 @@ async def test_discovery(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -69,13 +69,13 @@ async def test_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -92,7 +92,7 @@ async def test_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == DEFAULT_ENTRY_TITLE assert result3["data"] == CREATE_ENTRY_DATA_LEGACY mock_setup.assert_called_once() @@ -132,7 +132,7 @@ async def test_discovery_auth( }, ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_auth_confirm" assert not result["errors"] @@ -184,7 +184,7 @@ async def test_discovery_auth_errors( }, ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_auth_confirm" assert not result["errors"] @@ -235,7 +235,7 @@ async def test_discovery_new_credentials( }, ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_auth_confirm" assert not result["errors"] @@ -287,7 +287,7 @@ async def test_discovery_new_credentials_invalid( }, ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_auth_confirm" assert not result["errors"] diff --git a/tests/components/tractive/test_config_flow.py b/tests/components/tractive/test_config_flow.py index 45a37730ff4..5cedb51e5af 100644 --- a/tests/components/tractive/test_config_flow.py +++ b/tests/components/tractive/test_config_flow.py @@ -7,6 +7,7 @@ import aiotractive from homeassistant import config_entries from homeassistant.components.tractive.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -22,7 +23,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -38,7 +39,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "test-email@example.com" assert result2["data"] == USER_INPUT assert len(mock_setup_entry.mock_calls) == 1 @@ -59,7 +60,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: USER_INPUT, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -78,7 +79,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: USER_INPUT, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -96,7 +97,7 @@ async def test_flow_entry_already_exists(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=USER_INPUT ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -119,7 +120,7 @@ async def test_reauthentication(hass: HomeAssistant) -> None: data=old_entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "reauth_confirm" @@ -136,7 +137,7 @@ async def test_reauthentication(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 @@ -160,7 +161,7 @@ async def test_reauthentication_failure(hass: HomeAssistant) -> None: data=old_entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "reauth_confirm" @@ -175,7 +176,7 @@ async def test_reauthentication_failure(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result2["errors"]["base"] == "invalid_auth" @@ -198,7 +199,7 @@ async def test_reauthentication_unknown_failure(hass: HomeAssistant) -> None: data=old_entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "reauth_confirm" @@ -213,7 +214,7 @@ async def test_reauthentication_unknown_failure(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result2["errors"]["base"] == "unknown" @@ -236,7 +237,7 @@ async def test_reauthentication_failure_no_existing_entry(hass: HomeAssistant) - data=old_entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "reauth_confirm" @@ -247,5 +248,5 @@ async def test_reauthentication_failure_no_existing_entry(hass: HomeAssistant) - ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_failed_existing" diff --git a/tests/components/trafikverket_weatherstation/test_config_flow.py b/tests/components/trafikverket_weatherstation/test_config_flow.py index d2bd794daf7..771336301ff 100644 --- a/tests/components/trafikverket_weatherstation/test_config_flow.py +++ b/tests/components/trafikverket_weatherstation/test_config_flow.py @@ -28,7 +28,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -49,7 +49,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Vallby" assert result2["data"] == { "api_key": "1234567890", diff --git a/tests/components/twinkly/test_config_flow.py b/tests/components/twinkly/test_config_flow.py index f797f9b01b6..9b9aeafd082 100644 --- a/tests/components/twinkly/test_config_flow.py +++ b/tests/components/twinkly/test_config_flow.py @@ -7,6 +7,7 @@ from homeassistant.components import dhcp from homeassistant.components.twinkly.const import DOMAIN as TWINKLY_DOMAIN from homeassistant.const import CONF_HOST, CONF_ID, CONF_MODEL, CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import TEST_MODEL, TEST_NAME, ClientMock @@ -23,7 +24,7 @@ async def test_invalid_host(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( TWINKLY_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( @@ -31,7 +32,7 @@ async def test_invalid_host(hass: HomeAssistant) -> None: {CONF_HOST: "dummy"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {CONF_HOST: "cannot_connect"} @@ -49,7 +50,7 @@ async def test_success_flow(hass: HomeAssistant) -> None: TWINKLY_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -58,7 +59,7 @@ async def test_success_flow(hass: HomeAssistant) -> None: {CONF_HOST: "dummy"}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_NAME assert result["data"] == { CONF_HOST: "dummy", @@ -85,7 +86,7 @@ async def test_dhcp_can_confirm(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" @@ -109,12 +110,12 @@ async def test_dhcp_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "form" + 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"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_NAME assert result["data"] == { CONF_HOST: "1.2.3.4", @@ -154,5 +155,5 @@ async def test_dhcp_already_exists(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/ukraine_alarm/test_config_flow.py b/tests/components/ukraine_alarm/test_config_flow.py index da4f242ea7a..ba37f188079 100644 --- a/tests/components/ukraine_alarm/test_config_flow.py +++ b/tests/components/ukraine_alarm/test_config_flow.py @@ -221,7 +221,7 @@ async def test_max_regions(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "max_regions" diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index 2f9bf68f086..b269392f707 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -581,7 +581,7 @@ async def test_option_flow_integration_not_setup( hass.data[UNIFI_DOMAIN].pop(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "integration_not_setup" @@ -602,7 +602,7 @@ async def test_form_ssdp(hass: HomeAssistant) -> None: }, ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -647,7 +647,7 @@ async def test_form_ssdp_aborts_if_host_already_exists(hass: HomeAssistant) -> N }, ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -674,7 +674,7 @@ async def test_form_ssdp_aborts_if_serial_already_exists(hass: HomeAssistant) -> }, ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -701,7 +701,7 @@ async def test_form_ssdp_gets_form_with_ignored_entry(hass: HomeAssistant) -> No }, ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} context = next( diff --git a/tests/components/upb/test_config_flow.py b/tests/components/upb/test_config_flow.py index b3c3dfce15c..3a82eaa0ae7 100644 --- a/tests/components/upb/test_config_flow.py +++ b/tests/components/upb/test_config_flow.py @@ -5,6 +5,7 @@ from unittest.mock import MagicMock, PropertyMock, patch from homeassistant import config_entries from homeassistant.components.upb.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType def mocked_upb(sync_complete=True, config_ok=True): @@ -63,9 +64,9 @@ async def test_full_upb_flow_with_serial_port(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert flow["type"] == "form" + assert flow["type"] is FlowResultType.FORM assert flow["errors"] == {} - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "UPB" assert result["data"] == { "host": "serial:///dev/ttyS0:115200", @@ -77,7 +78,7 @@ async def test_full_upb_flow_with_serial_port(hass: HomeAssistant) -> None: async def test_form_user_with_tcp_upb(hass: HomeAssistant) -> None: """Test we can setup a serial upb.""" result = await valid_tcp_flow(hass) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {"host": "tcp://1.2.3.4", "file_path": "upb.upe"} await hass.async_block_till_done() @@ -92,14 +93,14 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: ): result = await valid_tcp_flow(hass, sync_complete=False) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} async def test_form_missing_upb_file(hass: HomeAssistant) -> None: """Test we handle cannot connect error.""" result = await valid_tcp_flow(hass, config_ok=False) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_upb_file"} @@ -107,7 +108,7 @@ async def test_form_user_with_already_configured(hass: HomeAssistant) -> None: """Test we can setup a TCP upb.""" _ = await valid_tcp_flow(hass) result2 = await valid_tcp_flow(hass) - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" await hass.async_block_till_done() @@ -128,7 +129,7 @@ async def test_form_import(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "UPB" assert result["data"] == {"host": "tcp://42.4.2.42", "file_path": "upb.upe"} @@ -145,7 +146,7 @@ async def test_form_junk_input(hass: HomeAssistant) -> None: data={"foo": "goo", "goo": "foo"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "unknown"} await hass.async_block_till_done() diff --git a/tests/components/vera/test_config_flow.py b/tests/components/vera/test_config_flow.py index e5d60aa3e23..057945450e3 100644 --- a/tests/components/vera/test_config_flow.py +++ b/tests/components/vera/test_config_flow.py @@ -118,7 +118,7 @@ async def test_async_step_finish_error(hass: HomeAssistant) -> None: data={CONF_CONTROLLER: "http://127.0.0.1:123/"}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" assert result["description_placeholders"] == { "base_url": "http://127.0.0.1:123" diff --git a/tests/components/voip/test_config_flow.py b/tests/components/voip/test_config_flow.py index ec2a65576e6..1b7aaad7c03 100644 --- a/tests/components/voip/test_config_flow.py +++ b/tests/components/voip/test_config_flow.py @@ -16,7 +16,7 @@ async def test_form_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( voip.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert not result["errors"] with patch( @@ -41,7 +41,7 @@ async def test_single_instance( result = await hass.config_entries.flow.async_init( voip.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/volumio/test_config_flow.py b/tests/components/volumio/test_config_flow.py index 7d185161d0a..9c3708f970c 100644 --- a/tests/components/volumio/test_config_flow.py +++ b/tests/components/volumio/test_config_flow.py @@ -8,6 +8,7 @@ from homeassistant.components import zeroconf from homeassistant.components.volumio.config_flow import CannotConnectError from homeassistant.components.volumio.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -43,7 +44,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -62,7 +63,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "TestVolumio" assert result2["data"] == {**TEST_SYSTEM_INFO, **TEST_CONNECTION} @@ -103,7 +104,7 @@ async def test_form_updates_unique_id(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" assert entry.data == {**TEST_SYSTEM_INFO, **TEST_CONNECTION} @@ -114,7 +115,7 @@ async def test_empty_system_info(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -133,7 +134,7 @@ async def test_empty_system_info(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_CONNECTION["host"] assert result2["data"] == { "host": TEST_CONNECTION["host"], @@ -160,7 +161,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: TEST_CONNECTION, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -179,7 +180,7 @@ async def test_form_exception(hass: HomeAssistant) -> None: TEST_CONNECTION, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -206,7 +207,7 @@ async def test_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_DISCOVERY_RESULT["name"] assert result2["data"] == TEST_DISCOVERY_RESULT @@ -232,7 +233,7 @@ async def test_discovery_cannot_connect(hass: HomeAssistant) -> None: user_input={}, ) - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "cannot_connect" @@ -241,13 +242,13 @@ async def test_discovery_duplicate_data(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=TEST_DISCOVERY ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=TEST_DISCOVERY ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -278,7 +279,7 @@ async def test_discovery_updates_unique_id(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data == TEST_DISCOVERY_RESULT diff --git a/tests/components/wallbox/test_config_flow.py b/tests/components/wallbox/test_config_flow.py index c0428ef47db..0c4497929dc 100644 --- a/tests/components/wallbox/test_config_flow.py +++ b/tests/components/wallbox/test_config_flow.py @@ -78,7 +78,7 @@ async def test_form_cannot_authenticate(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -108,7 +108,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -176,7 +176,7 @@ async def test_form_reauth(hass: HomeAssistant, entry: MockConfigEntry) -> None: }, ) - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" await hass.config_entries.async_unload(entry.entry_id) @@ -216,7 +216,7 @@ async def test_form_reauth_invalid(hass: HomeAssistant, entry: MockConfigEntry) }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "reauth_invalid"} await hass.config_entries.async_unload(entry.entry_id) diff --git a/tests/components/whirlpool/test_config_flow.py b/tests/components/whirlpool/test_config_flow.py index debee3df743..e3896a436d4 100644 --- a/tests/components/whirlpool/test_config_flow.py +++ b/tests/components/whirlpool/test_config_flow.py @@ -25,7 +25,7 @@ async def test_form(hass: HomeAssistant, region, brand) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER with ( @@ -56,7 +56,7 @@ async def test_form(hass: HomeAssistant, region, brand) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "test-username" assert result2["data"] == { "username": "test-username", @@ -84,7 +84,7 @@ async def test_form_invalid_auth(hass: HomeAssistant, region, brand) -> None: result["flow_id"], CONFIG_INPUT | {"region": region[0], "brand": brand[0]}, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -105,7 +105,7 @@ async def test_form_cannot_connect(hass: HomeAssistant, region, brand) -> None: "brand": brand[0], }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -126,7 +126,7 @@ async def test_form_auth_timeout(hass: HomeAssistant, region, brand) -> None: "brand": brand[0], }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -147,7 +147,7 @@ async def test_form_generic_auth_exception(hass: HomeAssistant, region, brand) - "brand": brand[0], }, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -164,7 +164,7 @@ async def test_form_already_configured(hass: HomeAssistant, region, brand) -> No DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER with ( @@ -192,7 +192,7 @@ async def test_form_already_configured(hass: HomeAssistant, region, brand) -> No ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/withings/test_config_flow.py b/tests/components/withings/test_config_flow.py index 2bafbc97573..9f4b265ed4f 100644 --- a/tests/components/withings/test_config_flow.py +++ b/tests/components/withings/test_config_flow.py @@ -151,7 +151,7 @@ async def test_config_reauth_profile( }, data=polling_config_entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) @@ -213,7 +213,7 @@ async def test_config_reauth_wrong_account( }, data=polling_config_entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) diff --git a/tests/components/wiz/test_config_flow.py b/tests/components/wiz/test_config_flow.py index 3969c3ab1b3..c9ac4b023f7 100644 --- a/tests/components/wiz/test_config_flow.py +++ b/tests/components/wiz/test_config_flow.py @@ -49,7 +49,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} # Patch functions with ( @@ -68,7 +68,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "WiZ Dimmable White ABCABC" assert result2["data"] == { CONF_HOST: "1.1.1.1", @@ -82,7 +82,7 @@ async def test_user_flow_enters_dns_name(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -110,7 +110,7 @@ async def test_user_flow_enters_dns_name(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "WiZ Dimmable White ABCABC" assert result3["data"] == { CONF_HOST: "1.1.1.1", @@ -145,7 +145,7 @@ async def test_user_form_exceptions( TEST_CONNECTION, ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": error_base} @@ -168,7 +168,7 @@ async def test_form_updates_unique_id(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" assert entry.data[CONF_HOST] == FAKE_IP @@ -291,7 +291,7 @@ async def test_discovered_by_dhcp_or_integration_discovery( ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == name assert result2["data"] == { CONF_HOST: "1.1.1.1", @@ -364,7 +364,7 @@ async def test_setup_via_discovery(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -372,7 +372,7 @@ async def test_setup_via_discovery(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -380,7 +380,7 @@ async def test_setup_via_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -388,7 +388,7 @@ async def test_setup_via_discovery(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -407,7 +407,7 @@ async def test_setup_via_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "WiZ Dimmable White ABCABC" assert result3["data"] == { CONF_HOST: "1.1.1.1", @@ -419,7 +419,7 @@ async def test_setup_via_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -427,7 +427,7 @@ async def test_setup_via_discovery(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "no_devices_found" @@ -437,7 +437,7 @@ async def test_setup_via_discovery_cannot_connect(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -445,7 +445,7 @@ async def test_setup_via_discovery_cannot_connect(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -462,7 +462,7 @@ async def test_setup_via_discovery_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == "abort" + assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "cannot_connect" @@ -472,7 +472,7 @@ async def test_setup_via_discovery_exception_finds_nothing(hass: HomeAssistant) DOMAIN, context={"source": config_entries.SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -528,7 +528,7 @@ async def test_discovery_with_firmware_update(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "WiZ RGBWW Tunable ABCABC" assert result2["data"] == { CONF_HOST: "1.1.1.1", @@ -564,7 +564,7 @@ async def test_discovered_during_onboarding(hass: HomeAssistant, source, data) - ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "WiZ Dimmable White ABCABC" assert result["data"] == { CONF_HOST: "1.1.1.1", diff --git a/tests/components/wolflink/test_config_flow.py b/tests/components/wolflink/test_config_flow.py index 8c497ae3943..bd71d9d3180 100644 --- a/tests/components/wolflink/test_config_flow.py +++ b/tests/components/wolflink/test_config_flow.py @@ -92,7 +92,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=INPUT_CONFIG ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -106,7 +106,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=INPUT_CONFIG ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -120,7 +120,7 @@ async def test_form_unknown_exception(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=INPUT_CONFIG ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "unknown"} diff --git a/tests/components/ws66i/test_config_flow.py b/tests/components/ws66i/test_config_flow.py index 4124bb9b662..3fe8499ac97 100644 --- a/tests/components/ws66i/test_config_flow.py +++ b/tests/components/ws66i/test_config_flow.py @@ -30,7 +30,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( @@ -52,7 +52,7 @@ async def test_form(hass: HomeAssistant) -> None: ws66i_instance.open.assert_called_once() ws66i_instance.close.assert_called_once() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "WS66i Amp" assert result2["data"] == {CONF_IP_ADDRESS: CONFIG[CONF_IP_ADDRESS]} @@ -72,7 +72,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: result["flow_id"], CONFIG ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -89,7 +89,7 @@ async def test_form_wrong_ip(hass: HomeAssistant) -> None: result["flow_id"], CONFIG ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -106,7 +106,7 @@ async def test_generic_exception(hass: HomeAssistant) -> None: result["flow_id"], CONFIG ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/xiaomi_aqara/test_config_flow.py b/tests/components/xiaomi_aqara/test_config_flow.py index 67991714203..141e245815e 100644 --- a/tests/components/xiaomi_aqara/test_config_flow.py +++ b/tests/components/xiaomi_aqara/test_config_flow.py @@ -11,6 +11,7 @@ from homeassistant.components import zeroconf from homeassistant.components.xiaomi_aqara import config_flow, const from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT, CONF_PROTOCOL from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType ZEROCONF_NAME = "name" ZEROCONF_PROP = "properties" @@ -89,7 +90,7 @@ async def test_config_flow_user_success(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -98,7 +99,7 @@ async def test_config_flow_user_success(hass: HomeAssistant) -> None: {const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "settings" assert result["errors"] == {} @@ -107,7 +108,7 @@ async def test_config_flow_user_success(hass: HomeAssistant) -> None: {const.CONF_KEY: TEST_KEY, CONF_NAME: TEST_NAME}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_NAME assert result["data"] == { CONF_HOST: TEST_HOST, @@ -126,7 +127,7 @@ async def test_config_flow_user_multiple_success(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -141,7 +142,7 @@ async def test_config_flow_user_multiple_success(hass: HomeAssistant) -> None: {const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select" assert result["errors"] == {} @@ -150,7 +151,7 @@ async def test_config_flow_user_multiple_success(hass: HomeAssistant) -> None: {"select_ip": TEST_HOST_2}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "settings" assert result["errors"] == {} @@ -159,7 +160,7 @@ async def test_config_flow_user_multiple_success(hass: HomeAssistant) -> None: {const.CONF_KEY: TEST_KEY, CONF_NAME: TEST_NAME}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_NAME assert result["data"] == { CONF_HOST: TEST_HOST_2, @@ -178,7 +179,7 @@ async def test_config_flow_user_no_key_success(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -187,7 +188,7 @@ async def test_config_flow_user_no_key_success(hass: HomeAssistant) -> None: {const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "settings" assert result["errors"] == {} @@ -196,7 +197,7 @@ async def test_config_flow_user_no_key_success(hass: HomeAssistant) -> None: {CONF_NAME: TEST_NAME}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_NAME assert result["data"] == { CONF_HOST: TEST_HOST, @@ -215,7 +216,7 @@ async def test_config_flow_user_host_mac_success(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -234,7 +235,7 @@ async def test_config_flow_user_host_mac_success(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "settings" assert result["errors"] == {} @@ -243,7 +244,7 @@ async def test_config_flow_user_host_mac_success(hass: HomeAssistant) -> None: {CONF_NAME: TEST_NAME}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_NAME assert result["data"] == { CONF_HOST: TEST_HOST, @@ -262,7 +263,7 @@ async def test_config_flow_user_discovery_error(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -277,7 +278,7 @@ async def test_config_flow_user_discovery_error(hass: HomeAssistant) -> None: {const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "discovery_error"} @@ -288,7 +289,7 @@ async def test_config_flow_user_invalid_interface(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -303,7 +304,7 @@ async def test_config_flow_user_invalid_interface(hass: HomeAssistant) -> None: {const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {const.CONF_INTERFACE: "invalid_interface"} @@ -314,7 +315,7 @@ async def test_config_flow_user_invalid_host(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -333,7 +334,7 @@ async def test_config_flow_user_invalid_host(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"host": "invalid_host"} @@ -344,7 +345,7 @@ async def test_config_flow_user_invalid_mac(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -363,7 +364,7 @@ async def test_config_flow_user_invalid_mac(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"mac": "invalid_mac"} @@ -374,7 +375,7 @@ async def test_config_flow_user_invalid_key(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -389,7 +390,7 @@ async def test_config_flow_user_invalid_key(hass: HomeAssistant) -> None: {const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "settings" assert result["errors"] == {} @@ -398,7 +399,7 @@ async def test_config_flow_user_invalid_key(hass: HomeAssistant) -> None: {const.CONF_KEY: TEST_KEY, CONF_NAME: TEST_NAME}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "settings" assert result["errors"] == {const.CONF_KEY: "invalid_key"} @@ -419,7 +420,7 @@ async def test_zeroconf_success(hass: HomeAssistant) -> None: ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -428,7 +429,7 @@ async def test_zeroconf_success(hass: HomeAssistant) -> None: {const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "settings" assert result["errors"] == {} @@ -437,7 +438,7 @@ async def test_zeroconf_success(hass: HomeAssistant) -> None: {const.CONF_KEY: TEST_KEY, CONF_NAME: TEST_NAME}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_NAME assert result["data"] == { CONF_HOST: TEST_HOST, @@ -466,7 +467,7 @@ async def test_zeroconf_missing_data(hass: HomeAssistant) -> None: ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_xiaomi_aqara" @@ -486,5 +487,5 @@ async def test_zeroconf_unknown_device(hass: HomeAssistant) -> None: ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_xiaomi_aqara" diff --git a/tests/components/xiaomi_miio/test_config_flow.py b/tests/components/xiaomi_miio/test_config_flow.py index 87f3fb383c3..481be189ddd 100644 --- a/tests/components/xiaomi_miio/test_config_flow.py +++ b/tests/components/xiaomi_miio/test_config_flow.py @@ -119,7 +119,7 @@ async def test_config_flow_step_gateway_connect_error(hass: HomeAssistant) -> No const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" assert result["errors"] == {} @@ -128,7 +128,7 @@ async def test_config_flow_step_gateway_connect_error(hass: HomeAssistant) -> No {const.CONF_MANUAL: True}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" assert result["errors"] == {} @@ -141,7 +141,7 @@ async def test_config_flow_step_gateway_connect_error(hass: HomeAssistant) -> No {CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "connect" assert result["errors"] == {"base": "cannot_connect"} @@ -152,7 +152,7 @@ async def test_config_flow_gateway_success(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" assert result["errors"] == {} @@ -161,7 +161,7 @@ async def test_config_flow_gateway_success(hass: HomeAssistant) -> None: {const.CONF_MANUAL: True}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" assert result["errors"] == {} @@ -170,7 +170,7 @@ async def test_config_flow_gateway_success(hass: HomeAssistant) -> None: {CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_MODEL assert result["data"] == { const.CONF_FLOW_TYPE: const.CONF_GATEWAY, @@ -190,7 +190,7 @@ async def test_config_flow_gateway_cloud_success(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" assert result["errors"] == {} @@ -203,7 +203,7 @@ async def test_config_flow_gateway_cloud_success(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_NAME assert result["data"] == { const.CONF_FLOW_TYPE: const.CONF_GATEWAY, @@ -223,7 +223,7 @@ async def test_config_flow_gateway_cloud_multiple_success(hass: HomeAssistant) - const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" assert result["errors"] == {} @@ -240,7 +240,7 @@ async def test_config_flow_gateway_cloud_multiple_success(hass: HomeAssistant) - }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "select" assert result["errors"] == {} @@ -249,7 +249,7 @@ async def test_config_flow_gateway_cloud_multiple_success(hass: HomeAssistant) - {"select_device": f"{TEST_NAME2} - {TEST_MODEL}"}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_NAME2 assert result["data"] == { const.CONF_FLOW_TYPE: const.CONF_GATEWAY, @@ -269,7 +269,7 @@ async def test_config_flow_gateway_cloud_incomplete(hass: HomeAssistant) -> None const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" assert result["errors"] == {} @@ -281,7 +281,7 @@ async def test_config_flow_gateway_cloud_incomplete(hass: HomeAssistant) -> None }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" assert result["errors"] == {"base": "cloud_credentials_incomplete"} @@ -292,7 +292,7 @@ async def test_config_flow_gateway_cloud_login_error(hass: HomeAssistant) -> Non const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" assert result["errors"] == {} @@ -309,7 +309,7 @@ async def test_config_flow_gateway_cloud_login_error(hass: HomeAssistant) -> Non }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" assert result["errors"] == {"base": "cloud_login_error"} @@ -326,7 +326,7 @@ async def test_config_flow_gateway_cloud_login_error(hass: HomeAssistant) -> Non }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" assert result["errors"] == {"base": "cloud_login_error"} @@ -343,7 +343,7 @@ async def test_config_flow_gateway_cloud_login_error(hass: HomeAssistant) -> Non }, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -353,7 +353,7 @@ async def test_config_flow_gateway_cloud_no_devices(hass: HomeAssistant) -> None const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" assert result["errors"] == {} @@ -370,7 +370,7 @@ async def test_config_flow_gateway_cloud_no_devices(hass: HomeAssistant) -> None }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" assert result["errors"] == {"base": "cloud_no_devices"} @@ -387,7 +387,7 @@ async def test_config_flow_gateway_cloud_no_devices(hass: HomeAssistant) -> None }, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -397,7 +397,7 @@ async def test_config_flow_gateway_cloud_missing_token(hass: HomeAssistant) -> N const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" assert result["errors"] == {} @@ -425,7 +425,7 @@ async def test_config_flow_gateway_cloud_missing_token(hass: HomeAssistant) -> N }, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "incomplete_info" @@ -445,7 +445,7 @@ async def test_zeroconf_gateway_success(hass: HomeAssistant) -> None: ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" assert result["errors"] == {} @@ -458,7 +458,7 @@ async def test_zeroconf_gateway_success(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_NAME assert result["data"] == { const.CONF_FLOW_TYPE: const.CONF_GATEWAY, @@ -488,7 +488,7 @@ async def test_zeroconf_unknown_device(hass: HomeAssistant) -> None: ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_xiaomi_miio" @@ -508,7 +508,7 @@ async def test_zeroconf_no_data(hass: HomeAssistant) -> None: ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_xiaomi_miio" @@ -528,7 +528,7 @@ async def test_zeroconf_missing_data(hass: HomeAssistant) -> None: ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_xiaomi_miio" @@ -538,7 +538,7 @@ async def test_config_flow_step_device_connect_error(hass: HomeAssistant) -> Non const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" assert result["errors"] == {} @@ -547,7 +547,7 @@ async def test_config_flow_step_device_connect_error(hass: HomeAssistant) -> Non {const.CONF_MANUAL: True}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" assert result["errors"] == {} @@ -560,7 +560,7 @@ async def test_config_flow_step_device_connect_error(hass: HomeAssistant) -> Non {CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "connect" assert result["errors"] == {"base": "cannot_connect"} @@ -571,7 +571,7 @@ async def test_config_flow_step_unknown_device(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" assert result["errors"] == {} @@ -580,7 +580,7 @@ async def test_config_flow_step_unknown_device(hass: HomeAssistant) -> None: {const.CONF_MANUAL: True}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" assert result["errors"] == {} @@ -595,7 +595,7 @@ async def test_config_flow_step_unknown_device(hass: HomeAssistant) -> None: {CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "connect" assert result["errors"] == {"base": "unknown_device"} @@ -606,7 +606,7 @@ async def test_config_flow_step_device_manual_model_error(hass: HomeAssistant) - const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" assert result["errors"] == {} @@ -615,7 +615,7 @@ async def test_config_flow_step_device_manual_model_error(hass: HomeAssistant) - {const.CONF_MANUAL: True}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" assert result["errors"] == {} @@ -628,7 +628,7 @@ async def test_config_flow_step_device_manual_model_error(hass: HomeAssistant) - {CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "connect" assert result["errors"] == {"base": "cannot_connect"} @@ -641,7 +641,7 @@ async def test_config_flow_step_device_manual_model_error(hass: HomeAssistant) - {CONF_MODEL: TEST_MODEL}, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @@ -651,7 +651,7 @@ async def test_config_flow_step_device_manual_model_succes(hass: HomeAssistant) const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" assert result["errors"] == {} @@ -660,7 +660,7 @@ async def test_config_flow_step_device_manual_model_succes(hass: HomeAssistant) {const.CONF_MANUAL: True}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" assert result["errors"] == {} @@ -675,7 +675,7 @@ async def test_config_flow_step_device_manual_model_succes(hass: HomeAssistant) {CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "connect" assert result["errors"] == {"base": "wrong_token"} @@ -690,7 +690,7 @@ async def test_config_flow_step_device_manual_model_succes(hass: HomeAssistant) {CONF_MODEL: overwrite_model}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == overwrite_model assert result["data"] == { const.CONF_FLOW_TYPE: CONF_DEVICE, @@ -710,7 +710,7 @@ async def config_flow_device_success(hass, model_to_test): const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" assert result["errors"] == {} @@ -719,7 +719,7 @@ async def config_flow_device_success(hass, model_to_test): {const.CONF_MANUAL: True}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" assert result["errors"] == {} @@ -734,7 +734,7 @@ async def config_flow_device_success(hass, model_to_test): {CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == model_to_test assert result["data"] == { const.CONF_FLOW_TYPE: CONF_DEVICE, @@ -756,7 +756,7 @@ async def config_flow_generic_roborock(hass): const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" assert result["errors"] == {} @@ -765,7 +765,7 @@ async def config_flow_generic_roborock(hass): {const.CONF_MANUAL: True}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" assert result["errors"] == {} @@ -780,7 +780,7 @@ async def config_flow_generic_roborock(hass): {CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == dummy_model assert result["data"] == { const.CONF_FLOW_TYPE: CONF_DEVICE, @@ -810,7 +810,7 @@ async def zeroconf_device_success(hass, zeroconf_name_to_test, model_to_test): ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" assert result["errors"] == {} @@ -819,7 +819,7 @@ async def zeroconf_device_success(hass, zeroconf_name_to_test, model_to_test): {const.CONF_MANUAL: True}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" assert result["errors"] == {} @@ -834,7 +834,7 @@ async def zeroconf_device_success(hass, zeroconf_name_to_test, model_to_test): {CONF_TOKEN: TEST_TOKEN}, ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == model_to_test assert result["data"] == { const.CONF_FLOW_TYPE: CONF_DEVICE, @@ -980,7 +980,7 @@ async def test_reauth(hass: HomeAssistant) -> None: data=config_entry.data, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -988,7 +988,7 @@ async def test_reauth(hass: HomeAssistant) -> None: {}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "cloud" assert result["errors"] == {} @@ -1001,7 +1001,7 @@ async def test_reauth(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" config_data = config_entry.data.copy() diff --git a/tests/components/yeelight/test_config_flow.py b/tests/components/yeelight/test_config_flow.py index 06ad341b739..4d788ba8258 100644 --- a/tests/components/yeelight/test_config_flow.py +++ b/tests/components/yeelight/test_config_flow.py @@ -67,12 +67,12 @@ async def test_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -80,12 +80,12 @@ async def test_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -98,7 +98,7 @@ async def test_discovery(hass: HomeAssistant) -> None: result3 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_DEVICE: ID} ) - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == UNIQUE_FRIENDLY_NAME assert result3["data"] == {CONF_ID: ID, CONF_HOST: IP_ADDRESS, CONF_MODEL: MODEL} await hass.async_block_till_done() @@ -109,13 +109,13 @@ async def test_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] with _patch_discovery(), _patch_discovery_interval(): result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "no_devices_found" @@ -142,7 +142,7 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -151,7 +151,7 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No await hass.async_block_till_done() await hass.async_block_till_done() - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -160,13 +160,13 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] with _patch_discovery(), _patch_discovery_interval(): result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -178,7 +178,7 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No result3 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_DEVICE: ID} ) - assert result3["type"] == "create_entry" + assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == UNIQUE_FRIENDLY_NAME assert result3["data"] == { CONF_ID: ID, @@ -192,13 +192,13 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] with _patch_discovery(), _patch_discovery_interval(): result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "no_devices_found" @@ -215,7 +215,7 @@ async def test_discovery_no_device(hass: HomeAssistant) -> None: ): result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "no_devices_found" @@ -241,7 +241,7 @@ async def test_import(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" # Success @@ -255,7 +255,7 @@ async def test_import(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == { CONF_NAME: DEFAULT_NAME, @@ -278,7 +278,7 @@ async def test_import(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -287,7 +287,7 @@ async def test_manual(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -302,7 +302,7 @@ async def test_manual(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: IP_ADDRESS} ) - assert result2["type"] == "form" + assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -331,7 +331,7 @@ async def test_manual(hass: HomeAssistant) -> None: result["flow_id"], {CONF_HOST: IP_ADDRESS} ) await hass.async_block_till_done() - assert result4["type"] == "create_entry" + assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == "Color 0x15243f" assert result4["data"] == { CONF_HOST: IP_ADDRESS, @@ -353,7 +353,7 @@ async def test_manual(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: IP_ADDRESS} ) - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -382,7 +382,7 @@ async def test_options(hass: HomeAssistant) -> None: assert hass.states.get(f"light.{NAME}_nightlight") is None result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" config[CONF_NIGHTLIGHT_SWITCH] = True @@ -394,7 +394,7 @@ async def test_options(hass: HomeAssistant) -> None: result["flow_id"], user_input ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == config assert result2["data"] == config_entry.options assert hass.states.get(f"light.{NAME}_nightlight") is not None @@ -425,7 +425,7 @@ async def test_options_unknown_model(hass: HomeAssistant) -> None: assert hass.states.get(f"light.{NAME}_nightlight") is None result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" config[CONF_NIGHTLIGHT_SWITCH] = True @@ -436,7 +436,7 @@ async def test_options_unknown_model(hass: HomeAssistant) -> None: result["flow_id"], user_input ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == config assert result2["data"] == config_entry.options assert hass.states.get(f"light.{NAME}_nightlight") is not None @@ -447,7 +447,7 @@ async def test_manual_no_capabilities(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -469,7 +469,7 @@ async def test_manual_no_capabilities(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: IP_ADDRESS} ) - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_HOST: IP_ADDRESS, CONF_ID: None, @@ -604,7 +604,7 @@ async def test_discovered_by_dhcp_or_homekit(hass: HomeAssistant, source, data) result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { CONF_HOST: IP_ADDRESS, CONF_ID: "0x000000000015243f", @@ -697,7 +697,7 @@ async def test_discovered_ssdp(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { CONF_HOST: IP_ADDRESS, CONF_ID: "0x000000000015243f", @@ -751,7 +751,7 @@ async def test_discovered_zeroconf(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { CONF_HOST: IP_ADDRESS, CONF_ID: "0x000000000015243f", @@ -916,7 +916,7 @@ async def test_discovered_during_onboarding(hass: HomeAssistant, source, data) - ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_HOST: IP_ADDRESS, CONF_ID: "0x000000000015243f", diff --git a/tests/components/youtube/test_config_flow.py b/tests/components/youtube/test_config_flow.py index 30800e5399a..95a56155980 100644 --- a/tests/components/youtube/test_config_flow.py +++ b/tests/components/youtube/test_config_flow.py @@ -75,7 +75,7 @@ async def test_full_flow( assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(mock_setup.mock_calls) == 1 - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TITLE assert "result" in result assert result["result"].unique_id == "UC_x5XG1OV2P6uZZ5FSM9Ttw" @@ -300,7 +300,7 @@ async def test_reauth( assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == abort_reason assert result["description_placeholders"] == placeholders assert len(mock_setup.mock_calls) == calls diff --git a/tests/components/zerproc/test_config_flow.py b/tests/components/zerproc/test_config_flow.py index e512b2a668e..33bcb812b63 100644 --- a/tests/components/zerproc/test_config_flow.py +++ b/tests/components/zerproc/test_config_flow.py @@ -7,6 +7,7 @@ import pyzerproc from homeassistant import config_entries from homeassistant.components.zerproc.config_flow import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType async def test_flow_success(hass: HomeAssistant) -> None: @@ -15,7 +16,7 @@ async def test_flow_success(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -34,7 +35,7 @@ async def test_flow_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Zerproc" assert result2["data"] == {} @@ -47,7 +48,7 @@ async def test_flow_no_devices_found(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -65,7 +66,7 @@ async def test_flow_no_devices_found(hass: HomeAssistant) -> None: {}, ) - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "no_devices_found" await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 0 @@ -77,7 +78,7 @@ async def test_flow_exceptions_caught(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["errors"] is None with ( @@ -95,7 +96,7 @@ async def test_flow_exceptions_caught(hass: HomeAssistant) -> None: {}, ) - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "no_devices_found" await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 29b78ce450d..cb61be236bf 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -580,7 +580,7 @@ async def test_discovery_via_usb_deconz_already_discovered(hass: HomeAssistant) ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_zha_device" @@ -602,7 +602,7 @@ async def test_discovery_via_usb_deconz_already_setup(hass: HomeAssistant) -> No ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_zha_device" @@ -684,7 +684,7 @@ async def test_discovery_already_setup(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -812,7 +812,7 @@ async def test_user_flow_existing_config_entry(hass: HomeAssistant) -> None: DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT @patch(f"bellows.{PROBE_FUNCTION_PATH}", return_value=False) diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index 511fb8d7570..8da17e228be 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -19,6 +19,7 @@ from homeassistant.components.zeroconf import ZeroconfServiceInfo from homeassistant.components.zwave_js.config_flow import SERVER_VERSION_TIMEOUT, TITLE from homeassistant.components.zwave_js.const import ADDON_SLUG, DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -193,7 +194,7 @@ async def test_manual(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM with ( patch( @@ -212,7 +213,7 @@ async def test_manual(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Z-Wave JS" assert result2["data"] == { "url": "ws://localhost:3000", @@ -277,7 +278,7 @@ async def test_manual_errors( entry = integration result = await getattr(hass.config_entries, flow).async_init(**flow_params(entry)) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" result = await getattr(hass.config_entries, flow).async_configure( @@ -287,7 +288,7 @@ async def test_manual_errors( }, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" assert result["errors"] == {"base": error} @@ -310,7 +311,7 @@ async def test_manual_already_configured(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" result = await hass.config_entries.flow.async_configure( @@ -320,7 +321,7 @@ async def test_manual_already_configured(hass: HomeAssistant) -> None: }, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data["url"] == "ws://1.1.1.1:3001" assert entry.data["use_addon"] is False @@ -366,7 +367,7 @@ async def test_supervisor_discovery( result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TITLE assert result["data"] == { "url": "ws://host1:3001", @@ -402,7 +403,7 @@ async def test_supervisor_discovery_cannot_connect( ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -433,20 +434,20 @@ async def test_clean_discovery_on_user_create( ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"use_addon": False} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" with ( @@ -467,7 +468,7 @@ async def test_clean_discovery_on_user_create( await hass.async_block_till_done() assert len(hass.config_entries.flow.async_progress()) == 0 - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TITLE assert result["data"] == { "url": "ws://localhost:3000", @@ -507,7 +508,7 @@ async def test_abort_discovery_with_existing_entry( ), ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" # Assert that the entry data is updated with discovery info. assert entry.data["url"] == "ws://host1:3001" @@ -522,7 +523,7 @@ async def test_abort_hassio_discovery_with_existing_flow( context={"source": config_entries.SOURCE_USB}, data=USB_DISCOVERY_INFO, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "usb_confirm" result2 = await hass.config_entries.flow.async_init( @@ -536,7 +537,7 @@ async def test_abort_hassio_discovery_with_existing_flow( ), ) - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_in_progress" @@ -559,7 +560,7 @@ async def test_abort_hassio_discovery_for_other_addon( ), ) - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "not_zwave_js_addon" @@ -580,12 +581,12 @@ async def test_usb_discovery( context={"source": config_entries.SOURCE_USB}, data=USB_DISCOVERY_INFO, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "usb_confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "install_addon" # Make sure the flow continues when the progress task is done. @@ -595,7 +596,7 @@ async def test_usb_discovery( assert install_addon.call_args == call(hass, "core_zwave_js") - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure_addon" result = await hass.config_entries.flow.async_configure( @@ -622,7 +623,7 @@ async def test_usb_discovery( }, ) - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" with ( @@ -640,7 +641,7 @@ async def test_usb_discovery( assert start_addon.call_args == call(hass, "core_zwave_js") - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TITLE assert result["data"] == { "url": "ws://host1:3001", @@ -674,12 +675,12 @@ async def test_usb_discovery_addon_not_running( context={"source": config_entries.SOURCE_USB}, data=USB_DISCOVERY_INFO, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "usb_confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure_addon" # Make sure the discovered usb device is preferred. @@ -715,7 +716,7 @@ async def test_usb_discovery_addon_not_running( }, ) - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" with ( @@ -733,7 +734,7 @@ async def test_usb_discovery_addon_not_running( assert start_addon.call_args == call(hass, "core_zwave_js") - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TITLE assert result["data"] == { "url": "ws://host1:3001", @@ -772,11 +773,11 @@ async def test_discovery_addon_not_running( ) assert result["step_id"] == "hassio_confirm" - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure_addon" result = await hass.config_entries.flow.async_configure( @@ -804,7 +805,7 @@ async def test_discovery_addon_not_running( }, ) - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" with ( @@ -822,7 +823,7 @@ async def test_discovery_addon_not_running( assert start_addon.call_args == call(hass, "core_zwave_js") - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TITLE assert result["data"] == { "url": "ws://host1:3001", @@ -860,12 +861,12 @@ async def test_discovery_addon_not_installed( ) assert result["step_id"] == "hassio_confirm" - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result["step_id"] == "install_addon" - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS await hass.async_block_till_done() @@ -873,7 +874,7 @@ async def test_discovery_addon_not_installed( assert install_addon.call_args == call(hass, "core_zwave_js") - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure_addon" result = await hass.config_entries.flow.async_configure( @@ -901,7 +902,7 @@ async def test_discovery_addon_not_installed( }, ) - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" with ( @@ -919,7 +920,7 @@ async def test_discovery_addon_not_installed( assert start_addon.call_args == call(hass, "core_zwave_js") - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TITLE assert result["data"] == { "url": "ws://host1:3001", @@ -950,7 +951,7 @@ async def test_abort_usb_discovery_with_existing_flow( ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "hassio_confirm" result2 = await hass.config_entries.flow.async_init( @@ -958,7 +959,7 @@ async def test_abort_usb_discovery_with_existing_flow( context={"source": config_entries.SOURCE_USB}, data=USB_DISCOVERY_INFO, ) - assert result2["type"] == "abort" + assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_in_progress" @@ -979,7 +980,7 @@ async def test_abort_usb_discovery_already_configured( context={"source": config_entries.SOURCE_USB}, data=USB_DISCOVERY_INFO, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -990,7 +991,7 @@ async def test_usb_discovery_requires_supervisor(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USB}, data=USB_DISCOVERY_INFO, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "discovery_requires_supervisor" @@ -1003,7 +1004,7 @@ async def test_usb_discovery_already_running( context={"source": config_entries.SOURCE_USB}, data=USB_DISCOVERY_INFO, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -1020,7 +1021,7 @@ async def test_abort_usb_discovery_aborts_specific_devices( context={"source": config_entries.SOURCE_USB}, data=discovery_info, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_zwave_device" @@ -1031,14 +1032,14 @@ async def test_not_addon(hass: HomeAssistant, supervisor) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"use_addon": False} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" with ( @@ -1058,7 +1059,7 @@ async def test_not_addon(hass: HomeAssistant, supervisor) -> None: ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TITLE assert result["data"] == { "url": "ws://localhost:3000", @@ -1093,7 +1094,7 @@ async def test_addon_running( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" with ( @@ -1110,7 +1111,7 @@ async def test_addon_running( ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TITLE assert result["data"] == { "url": "ws://host1:3001", @@ -1181,14 +1182,14 @@ async def test_addon_running_failures( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"use_addon": True} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == abort_reason @@ -1227,14 +1228,14 @@ async def test_addon_running_already_configured( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"use_addon": True} ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data["url"] == "ws://host1:3001" assert entry.data["usb_path"] == "/test_new" @@ -1260,14 +1261,14 @@ async def test_addon_installed( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"use_addon": True} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure_addon" result = await hass.config_entries.flow.async_configure( @@ -1295,7 +1296,7 @@ async def test_addon_installed( }, ) - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" with ( @@ -1313,7 +1314,7 @@ async def test_addon_installed( assert start_addon.call_args == call(hass, "core_zwave_js") - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TITLE assert result["data"] == { "url": "ws://host1:3001", @@ -1348,14 +1349,14 @@ async def test_addon_installed_start_failure( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"use_addon": True} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure_addon" result = await hass.config_entries.flow.async_configure( @@ -1383,7 +1384,7 @@ async def test_addon_installed_start_failure( }, ) - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" await hass.async_block_till_done() @@ -1391,7 +1392,7 @@ async def test_addon_installed_start_failure( assert start_addon.call_args == call(hass, "core_zwave_js") - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "addon_start_failed" @@ -1423,14 +1424,14 @@ async def test_addon_installed_failures( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"use_addon": True} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure_addon" result = await hass.config_entries.flow.async_configure( @@ -1458,7 +1459,7 @@ async def test_addon_installed_failures( }, ) - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" await hass.async_block_till_done() @@ -1466,7 +1467,7 @@ async def test_addon_installed_failures( assert start_addon.call_args == call(hass, "core_zwave_js") - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "addon_start_failed" @@ -1489,14 +1490,14 @@ async def test_addon_installed_set_options_failure( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"use_addon": True} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure_addon" result = await hass.config_entries.flow.async_configure( @@ -1524,7 +1525,7 @@ async def test_addon_installed_set_options_failure( }, ) - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "addon_set_config_failed" assert start_addon.call_count == 0 @@ -1561,14 +1562,14 @@ async def test_addon_installed_already_configured( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"use_addon": True} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure_addon" result = await hass.config_entries.flow.async_configure( @@ -1596,7 +1597,7 @@ async def test_addon_installed_already_configured( }, ) - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" await hass.async_block_till_done() @@ -1604,7 +1605,7 @@ async def test_addon_installed_already_configured( assert start_addon.call_args == call(hass, "core_zwave_js") - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data["url"] == "ws://host1:3001" assert entry.data["usb_path"] == "/new" @@ -1630,14 +1631,14 @@ async def test_addon_not_installed( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"use_addon": True} ) - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "install_addon" # Make sure the flow continues when the progress task is done. @@ -1647,7 +1648,7 @@ async def test_addon_not_installed( assert install_addon.call_args == call(hass, "core_zwave_js") - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure_addon" result = await hass.config_entries.flow.async_configure( @@ -1675,7 +1676,7 @@ async def test_addon_not_installed( }, ) - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" with ( @@ -1693,7 +1694,7 @@ async def test_addon_not_installed( assert start_addon.call_args == call(hass, "core_zwave_js") - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TITLE assert result["data"] == { "url": "ws://host1:3001", @@ -1719,14 +1720,14 @@ async def test_install_addon_failure( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"use_addon": True} ) - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS # Make sure the flow continues when the progress task is done. await hass.async_block_till_done() @@ -1735,7 +1736,7 @@ async def test_install_addon_failure( assert install_addon.call_args == call(hass, "core_zwave_js") - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "addon_install_failed" @@ -1749,7 +1750,7 @@ async def test_options_manual(hass: HomeAssistant, client, integration) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" result = await hass.config_entries.options.async_configure( @@ -1757,7 +1758,7 @@ async def test_options_manual(hass: HomeAssistant, client, integration) -> None: ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert entry.data["url"] == "ws://1.1.1.1:3001" assert entry.data["use_addon"] is False assert entry.data["integration_created_addon"] is False @@ -1774,7 +1775,7 @@ async def test_options_manual_different_device( result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" result = await hass.config_entries.options.async_configure( @@ -1782,7 +1783,7 @@ async def test_options_manual_different_device( ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "different_device" @@ -1798,14 +1799,14 @@ async def test_options_not_addon( result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.options.async_configure( result["flow_id"], {"use_addon": False} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" result = await hass.config_entries.options.async_configure( @@ -1816,7 +1817,7 @@ async def test_options_not_addon( ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert entry.data["url"] == "ws://localhost:3000" assert entry.data["use_addon"] is False assert entry.data["integration_created_addon"] is False @@ -1908,14 +1909,14 @@ async def test_options_addon_running( result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.options.async_configure( result["flow_id"], {"use_addon": True} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure_addon" result = await hass.config_entries.options.async_configure( @@ -1931,7 +1932,7 @@ async def test_options_addon_running( ) assert client.disconnect.call_count == disconnect_calls - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" await hass.async_block_till_done() @@ -1940,7 +1941,7 @@ async def test_options_addon_running( assert restart_addon.call_args == call(hass, "core_zwave_js") - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert entry.data["url"] == "ws://host1:3001" assert entry.data["usb_path"] == new_addon_options["device"] assert entry.data["s0_legacy_key"] == new_addon_options["s0_legacy_key"] @@ -2017,14 +2018,14 @@ async def test_options_addon_running_no_changes( result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.options.async_configure( result["flow_id"], {"use_addon": True} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure_addon" result = await hass.config_entries.options.async_configure( @@ -2037,7 +2038,7 @@ async def test_options_addon_running_no_changes( assert set_addon_options.call_count == 0 assert restart_addon.call_count == 0 - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert entry.data["url"] == "ws://host1:3001" assert entry.data["usb_path"] == new_addon_options["device"] assert entry.data["s0_legacy_key"] == new_addon_options["s0_legacy_key"] @@ -2160,14 +2161,14 @@ async def test_options_different_device( result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.options.async_configure( result["flow_id"], {"use_addon": True} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure_addon" result = await hass.config_entries.options.async_configure( @@ -2183,7 +2184,7 @@ async def test_options_different_device( {"options": new_addon_options}, ) assert client.disconnect.call_count == disconnect_calls - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" await hass.async_block_till_done() @@ -2205,7 +2206,7 @@ async def test_options_different_device( "core_zwave_js", {"options": addon_options}, ) - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" await hass.async_block_till_done() @@ -2216,7 +2217,7 @@ async def test_options_different_device( result = await hass.config_entries.options.async_configure(result["flow_id"]) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "different_device" assert entry.data == data assert client.connect.call_count == 2 @@ -2318,14 +2319,14 @@ async def test_options_addon_restart_failed( result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.options.async_configure( result["flow_id"], {"use_addon": True} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure_addon" result = await hass.config_entries.options.async_configure( @@ -2341,7 +2342,7 @@ async def test_options_addon_restart_failed( {"options": new_addon_options}, ) assert client.disconnect.call_count == disconnect_calls - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" await hass.async_block_till_done() @@ -2360,7 +2361,7 @@ async def test_options_addon_restart_failed( "core_zwave_js", {"options": old_addon_options}, ) - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" await hass.async_block_till_done() @@ -2371,7 +2372,7 @@ async def test_options_addon_restart_failed( result = await hass.config_entries.options.async_configure(result["flow_id"]) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "addon_start_failed" assert entry.data == data assert client.connect.call_count == 2 @@ -2445,14 +2446,14 @@ async def test_options_addon_running_server_info_failure( result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.options.async_configure( result["flow_id"], {"use_addon": True} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure_addon" result = await hass.config_entries.options.async_configure( @@ -2461,7 +2462,7 @@ async def test_options_addon_running_server_info_failure( ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" assert entry.data == data assert client.connect.call_count == 2 @@ -2553,14 +2554,14 @@ async def test_options_addon_not_installed( result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.options.async_configure( result["flow_id"], {"use_addon": True} ) - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "install_addon" # Make sure the flow continues when the progress task is done. @@ -2570,7 +2571,7 @@ async def test_options_addon_not_installed( assert install_addon.call_args == call(hass, "core_zwave_js") - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure_addon" result = await hass.config_entries.options.async_configure( @@ -2586,7 +2587,7 @@ async def test_options_addon_not_installed( ) assert client.disconnect.call_count == disconnect_calls - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" await hass.async_block_till_done() @@ -2598,7 +2599,7 @@ async def test_options_addon_not_installed( await hass.async_block_till_done() await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert entry.data["url"] == "ws://host1:3001" assert entry.data["usb_path"] == new_addon_options["device"] assert entry.data["s0_legacy_key"] == new_addon_options["s0_legacy_key"] @@ -2627,14 +2628,14 @@ async def test_import_addon_installed( data={"usb_path": "/test/imported", "network_key": "imported123"}, ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"use_addon": True} ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure_addon" # the default input should be the imported data @@ -2666,7 +2667,7 @@ async def test_import_addon_installed( }, ) - assert result["type"] == "progress" + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" with ( @@ -2684,7 +2685,7 @@ async def test_import_addon_installed( assert start_addon.call_args == call(hass, "core_zwave_js") - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TITLE assert result["data"] == { "url": "ws://host1:3001", @@ -2717,7 +2718,7 @@ async def test_zeroconf(hass: HomeAssistant) -> None: ), ) - assert result["type"] == "form" + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zeroconf_confirm" with ( @@ -2732,7 +2733,7 @@ async def test_zeroconf(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TITLE assert result["data"] == { "url": "ws://127.0.0.1:3000", From 4a879ce42498ee7c5d36255263642add133af7d2 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 3 Apr 2024 09:56:19 +0200 Subject: [PATCH 208/967] Fix Downloader config flow (#114718) --- homeassistant/components/downloader/config_flow.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/downloader/config_flow.py b/homeassistant/components/downloader/config_flow.py index 69393c04985..635c241edc4 100644 --- a/homeassistant/components/downloader/config_flow.py +++ b/homeassistant/components/downloader/config_flow.py @@ -55,8 +55,9 @@ class DownloaderConfigFlow(ConfigFlow, domain=DOMAIN): async def _validate_input(self, user_input: dict[str, Any]) -> None: """Validate the user input if the directory exists.""" - if not os.path.isabs(user_input[CONF_DOWNLOAD_DIR]): - download_path = self.hass.config.path(user_input[CONF_DOWNLOAD_DIR]) + download_path = user_input[CONF_DOWNLOAD_DIR] + if not os.path.isabs(download_path): + download_path = self.hass.config.path(download_path) if not os.path.isdir(download_path): _LOGGER.error( From be3c923c7f23ff25b88eaca82504b437b31800c7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Apr 2024 22:06:16 -1000 Subject: [PATCH 209/967] Use eager_start to load utility_meter platforms (#114699) --- homeassistant/components/utility_meter/__init__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/utility_meter/__init__.py b/homeassistant/components/utility_meter/__init__.py index 71df488de7e..4bacde32367 100644 --- a/homeassistant/components/utility_meter/__init__.py +++ b/homeassistant/components/utility_meter/__init__.py @@ -150,7 +150,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: DOMAIN, {meter: {CONF_METER: meter}}, config, - ) + ), + eager_start=True, ) else: # create tariff selection @@ -161,7 +162,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: DOMAIN, {CONF_METER: meter, CONF_TARIFFS: conf[CONF_TARIFFS]}, config, - ) + ), + eager_start=True, ) hass.data[DATA_UTILITY][meter][CONF_TARIFF_ENTITY] = ( @@ -180,7 +182,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.async_create_task( discovery.async_load_platform( hass, SENSOR_DOMAIN, DOMAIN, tariff_confs, config - ) + ), + eager_start=True, ) return True From 74d8c6cce4b13a62537deae091765d1f0112c9e3 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 3 Apr 2024 11:11:56 +0200 Subject: [PATCH 210/967] Fix synology_dsm test side effects (#114722) --- tests/components/synology_dsm/test_media_source.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/components/synology_dsm/test_media_source.py b/tests/components/synology_dsm/test_media_source.py index e806014dcd6..24e9a378c02 100644 --- a/tests/components/synology_dsm/test_media_source.py +++ b/tests/components/synology_dsm/test_media_source.py @@ -428,6 +428,8 @@ async def test_media_view( # success dsm_with_photos.photos.download_item = AsyncMock(return_value=b"xxxx") - tempfile.tempdir = tmp_path - result = await view.get(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" + ) + assert isinstance(result, web.Response) From 41a88c876dd3efb50bc87d92cc86de6ea699d553 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Apr 2024 23:49:32 -1000 Subject: [PATCH 211/967] Avoid useless done check in config entries async_create_task (#114695) If the task is not started with eager_start it will never be done right away --- homeassistant/config_entries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 9f5f6b9135b..212d0322af6 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1072,7 +1072,7 @@ class ConfigEntry: task = hass.async_create_task( target, f"{name} {self.title} {self.domain} {self.entry_id}", eager_start ) - if task.done(): + if eager_start and task.done(): return task self._tasks.add(task) task.add_done_callback(self._tasks.remove) From e870d420a60f58391b9dd3be6d06fc5684a34841 Mon Sep 17 00:00:00 2001 From: Lenn <78048721+LennP@users.noreply.github.com> Date: Wed, 3 Apr 2024 12:11:46 +0200 Subject: [PATCH 212/967] Rename Motionblinds BLE integration to Motionblinds Bluetooth (#114584) --- homeassistant/components/motionblinds_ble/__init__.py | 10 +++++----- homeassistant/components/motionblinds_ble/button.py | 2 +- .../components/motionblinds_ble/config_flow.py | 4 ++-- homeassistant/components/motionblinds_ble/const.py | 2 +- homeassistant/components/motionblinds_ble/cover.py | 2 +- homeassistant/components/motionblinds_ble/entity.py | 4 ++-- .../components/motionblinds_ble/manifest.json | 2 +- homeassistant/components/motionblinds_ble/select.py | 2 +- homeassistant/generated/integrations.json | 2 +- tests/components/motionblinds_ble/__init__.py | 2 +- tests/components/motionblinds_ble/conftest.py | 2 +- tests/components/motionblinds_ble/test_config_flow.py | 2 +- 12 files changed, 18 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/motionblinds_ble/__init__.py b/homeassistant/components/motionblinds_ble/__init__.py index f70625cd36d..3c6df12e878 100644 --- a/homeassistant/components/motionblinds_ble/__init__.py +++ b/homeassistant/components/motionblinds_ble/__init__.py @@ -1,4 +1,4 @@ -"""Motionblinds BLE integration.""" +"""Motionblinds Bluetooth integration.""" from __future__ import annotations @@ -34,9 +34,9 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up Motionblinds BLE integration.""" + """Set up Motionblinds Bluetooth integration.""" - _LOGGER.debug("Setting up Motionblinds BLE integration") + _LOGGER.debug("Setting up Motionblinds Bluetooth integration") # The correct time is needed for encryption _LOGGER.debug("Setting timezone for encryption: %s", hass.config.time_zone) @@ -46,7 +46,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up Motionblinds BLE device from a config entry.""" + """Set up Motionblinds Bluetooth device from a config entry.""" _LOGGER.debug("(%s) Setting up device", entry.data[CONF_MAC_CODE]) @@ -94,7 +94,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Unload Motionblinds BLE device from a config entry.""" + """Unload Motionblinds Bluetooth device from a config entry.""" if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/motionblinds_ble/button.py b/homeassistant/components/motionblinds_ble/button.py index d3bd22e9276..a099276cd85 100644 --- a/homeassistant/components/motionblinds_ble/button.py +++ b/homeassistant/components/motionblinds_ble/button.py @@ -1,4 +1,4 @@ -"""Button entities for the Motionblinds BLE integration.""" +"""Button entities for the Motionblinds Bluetooth integration.""" from __future__ import annotations diff --git a/homeassistant/components/motionblinds_ble/config_flow.py b/homeassistant/components/motionblinds_ble/config_flow.py index 0282c4d5584..23302ae9624 100644 --- a/homeassistant/components/motionblinds_ble/config_flow.py +++ b/homeassistant/components/motionblinds_ble/config_flow.py @@ -1,4 +1,4 @@ -"""Config flow for Motionblinds BLE integration.""" +"""Config flow for Motionblinds Bluetooth integration.""" from __future__ import annotations @@ -38,7 +38,7 @@ CONFIG_SCHEMA = vol.Schema({vol.Required(CONF_MAC_CODE): str}) class FlowHandler(ConfigFlow, domain=DOMAIN): - """Handle a config flow for Motionblinds BLE.""" + """Handle a config flow for Motionblinds Bluetooth.""" def __init__(self) -> None: """Initialize a ConfigFlow.""" diff --git a/homeassistant/components/motionblinds_ble/const.py b/homeassistant/components/motionblinds_ble/const.py index d2eb5821b9f..bd88927559e 100644 --- a/homeassistant/components/motionblinds_ble/const.py +++ b/homeassistant/components/motionblinds_ble/const.py @@ -1,4 +1,4 @@ -"""Constants for the Motionblinds BLE integration.""" +"""Constants for the Motionblinds Bluetooth integration.""" ATTR_CONNECT = "connect" ATTR_DISCONNECT = "disconnect" diff --git a/homeassistant/components/motionblinds_ble/cover.py b/homeassistant/components/motionblinds_ble/cover.py index c4f14dc5605..afeeb5b0d70 100644 --- a/homeassistant/components/motionblinds_ble/cover.py +++ b/homeassistant/components/motionblinds_ble/cover.py @@ -1,4 +1,4 @@ -"""Cover entities for the Motionblinds BLE integration.""" +"""Cover entities for the Motionblinds Bluetooth integration.""" from __future__ import annotations diff --git a/homeassistant/components/motionblinds_ble/entity.py b/homeassistant/components/motionblinds_ble/entity.py index 5c2b3ae9afb..0b8171e7acd 100644 --- a/homeassistant/components/motionblinds_ble/entity.py +++ b/homeassistant/components/motionblinds_ble/entity.py @@ -1,4 +1,4 @@ -"""Base entities for the Motionblinds BLE integration.""" +"""Base entities for the Motionblinds Bluetooth integration.""" import logging @@ -16,7 +16,7 @@ _LOGGER = logging.getLogger(__name__) class MotionblindsBLEEntity(Entity): - """Base class for Motionblinds BLE entities.""" + """Base class for Motionblinds Bluetooth entities.""" _attr_has_entity_name = True _attr_should_poll = False diff --git a/homeassistant/components/motionblinds_ble/manifest.json b/homeassistant/components/motionblinds_ble/manifest.json index 2a24dd67483..aa727be13f8 100644 --- a/homeassistant/components/motionblinds_ble/manifest.json +++ b/homeassistant/components/motionblinds_ble/manifest.json @@ -1,6 +1,6 @@ { "domain": "motionblinds_ble", - "name": "Motionblinds BLE", + "name": "Motionblinds Bluetooth", "bluetooth": [ { "local_name": "MOTION_*", diff --git a/homeassistant/components/motionblinds_ble/select.py b/homeassistant/components/motionblinds_ble/select.py index 2ba2b8df2d4..c297c887910 100644 --- a/homeassistant/components/motionblinds_ble/select.py +++ b/homeassistant/components/motionblinds_ble/select.py @@ -1,4 +1,4 @@ -"""Select entities for the Motionblinds BLE integration.""" +"""Select entities for the Motionblinds Bluetooth integration.""" from __future__ import annotations diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index feb5a373505..a0cf46d7f1d 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -3754,7 +3754,7 @@ "integration_type": "device", "config_flow": true, "iot_class": "assumed_state", - "name": "Motionblinds BLE" + "name": "Motionblinds Bluetooth" } } }, diff --git a/tests/components/motionblinds_ble/__init__.py b/tests/components/motionblinds_ble/__init__.py index 302c3266ea1..c2385555dbf 100644 --- a/tests/components/motionblinds_ble/__init__.py +++ b/tests/components/motionblinds_ble/__init__.py @@ -1 +1 @@ -"""Tests for the Motionblinds BLE integration.""" +"""Tests for the Motionblinds Bluetooth integration.""" diff --git a/tests/components/motionblinds_ble/conftest.py b/tests/components/motionblinds_ble/conftest.py index 8cd1adb1c0e..ae487957302 100644 --- a/tests/components/motionblinds_ble/conftest.py +++ b/tests/components/motionblinds_ble/conftest.py @@ -1,4 +1,4 @@ -"""Setup the MotionBlinds BLE tests.""" +"""Setup the Motionblinds Bluetooth tests.""" from unittest.mock import AsyncMock, Mock, patch diff --git a/tests/components/motionblinds_ble/test_config_flow.py b/tests/components/motionblinds_ble/test_config_flow.py index 7b964f7d5e9..887d20d71ce 100644 --- a/tests/components/motionblinds_ble/test_config_flow.py +++ b/tests/components/motionblinds_ble/test_config_flow.py @@ -1,4 +1,4 @@ -"""Test the MotionBlinds BLE config flow.""" +"""Test the Motionblinds Bluetooth config flow.""" from unittest.mock import patch From 742643936f5ae86e8e0502a0b4c06eaed93a06cd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Apr 2024 00:12:52 -1000 Subject: [PATCH 213/967] Migrate legacy device_tracker task creation to use eager_start (#114703) Many of these can finish synchronously without being scheduled on the loop --- homeassistant/components/device_tracker/legacy.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index 91cf35f43bd..47751bf5e90 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -524,7 +524,7 @@ def async_setup_scanner_platform( ] kwargs["gps_accuracy"] = 0 - hass.async_create_task(async_see_device(**kwargs)) + hass.async_create_task(async_see_device(**kwargs), eager_start=True) cancel_legacy_scan = async_track_time_interval( hass, @@ -532,7 +532,7 @@ def async_setup_scanner_platform( interval, name=f"device_tracker {platform} legacy scan", ) - hass.async_create_task(async_device_tracker_scan(None)) + hass.async_create_task(async_device_tracker_scan(None), eager_start=True) @callback def _on_hass_stop(_: Event) -> None: @@ -722,7 +722,8 @@ class DeviceTracker: self.hass.async_create_task( self.async_update_config( self.hass.config.path(YAML_DEVICES), dev_id, device - ) + ), + eager_start=True, ) async def async_update_config(self, path: str, dev_id: str, device: Device) -> None: @@ -743,7 +744,9 @@ class DeviceTracker: """ for device in self.devices.values(): if (device.track and device.last_update_home) and device.stale(now): - self.hass.async_create_task(device.async_update_ha_state(True)) + self.hass.async_create_task( + device.async_update_ha_state(True), eager_start=True + ) async def async_setup_tracked_device(self) -> None: """Set up all not exists tracked devices. From cf4c02b9fa75ae4bb9533410d3ab8cd446d8d435 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Apr 2024 00:20:32 -1000 Subject: [PATCH 214/967] Simplify core state cache clear (#114694) same as #113136 but for core --- homeassistant/core.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 58e94d63352..1edeb666492 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -440,8 +440,7 @@ class HomeAssistant: """Set the current state.""" self.state = state for prop in ("is_running", "is_stopping"): - with suppress(AttributeError): - delattr(self, prop) + self.__dict__.pop(prop, None) def start(self) -> int: """Start Home Assistant. From ef047707d9f5e691d6854e8bbe5d83b226156f9e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Apr 2024 00:21:37 -1000 Subject: [PATCH 215/967] Simplify config entry cache clear (#114691) same as #113136 but for config entries --- homeassistant/config_entries.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 212d0322af6..f92e442e5a3 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -13,7 +13,6 @@ from collections.abc import ( Mapping, ValuesView, ) -import contextlib from contextvars import ContextVar from copy import deepcopy from enum import Enum, StrEnum @@ -463,8 +462,7 @@ class ConfigEntry: def clear_cache(self) -> None: """Clear cached properties.""" - with contextlib.suppress(AttributeError): - delattr(self, "as_json_fragment") + self.__dict__.pop("as_json_fragment", None) @cached_property def as_json_fragment(self) -> json_fragment: From 80e066a7a8f545942cd2c5d7fc7c65ab325ac178 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Apr 2024 00:23:36 -1000 Subject: [PATCH 216/967] Use eager_start to create async_update_alerts task in homeassistant_alerts (#114707) If there are no alerts, the task will finish synchronously --- homeassistant/components/homeassistant_alerts/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/homeassistant_alerts/__init__.py b/homeassistant/components/homeassistant_alerts/__init__.py index 338d8679b19..7d0a7c588dd 100644 --- a/homeassistant/components/homeassistant_alerts/__init__.py +++ b/homeassistant/components/homeassistant_alerts/__init__.py @@ -84,7 +84,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if not coordinator.last_update_success: return - hass.async_create_task(async_update_alerts()) + hass.async_create_task(async_update_alerts(), eager_start=True) coordinator = AlertUpdateCoordinator(hass) coordinator.async_add_listener(async_schedule_update_alerts) From e522f2f67ec19a6a7f80cc630d35c0d2bc5d2948 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Apr 2024 00:36:55 -1000 Subject: [PATCH 217/967] Create bond fallback polling tasks eagerly (#114705) There was not reason to delay here --- homeassistant/components/bond/entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/bond/entity.py b/homeassistant/components/bond/entity.py index 02137d27b3d..f547707d5f1 100644 --- a/homeassistant/components/bond/entity.py +++ b/homeassistant/components/bond/entity.py @@ -128,7 +128,7 @@ class BondEntity(Entity): _FALLBACK_SCAN_INTERVAL, ) return - self.hass.async_create_task(self._async_update()) + self.hass.async_create_task(self._async_update(), eager_start=True) async def _async_update(self) -> None: """Fetch via the API.""" From b9f27d2b31b68afe0d8fba0d6df6e4151c11b6a3 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 3 Apr 2024 13:50:34 +0200 Subject: [PATCH 218/967] Avoid blocking IO in downloader config flow (#114741) --- homeassistant/components/downloader/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/downloader/config_flow.py b/homeassistant/components/downloader/config_flow.py index 635c241edc4..15af8b56163 100644 --- a/homeassistant/components/downloader/config_flow.py +++ b/homeassistant/components/downloader/config_flow.py @@ -59,7 +59,7 @@ class DownloaderConfigFlow(ConfigFlow, domain=DOMAIN): if not os.path.isabs(download_path): download_path = self.hass.config.path(download_path) - if not os.path.isdir(download_path): + if not await self.hass.async_add_executor_job(os.path.isdir, download_path): _LOGGER.error( "Download path %s does not exist. File Downloader not active", download_path, From 2b9f22f11e565421015bd2c926f15ff9def9ef3d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Apr 2024 01:53:17 -1000 Subject: [PATCH 219/967] Make creation of capabilities_updated_at deque in Entity lazy (#114711) Most entities will never update their capabilities so we should avoid creating the deque as its a large chunk of the entity creation time --- homeassistant/helpers/entity.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 988ce29ade2..5c8cff2f60b 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -1131,7 +1131,16 @@ class Entity( ): if not self.__capabilities_updated_at_reported: time_now = hass.loop.time() - capabilities_updated_at = self.__capabilities_updated_at + # _Entity__capabilities_updated_at is because of name mangling + if not ( + capabilities_updated_at := getattr( + self, "_Entity__capabilities_updated_at", None + ) + ): + self.__capabilities_updated_at = deque( + maxlen=CAPABILITIES_UPDATE_LIMIT + 1 + ) + capabilities_updated_at = self.__capabilities_updated_at capabilities_updated_at.append(time_now) while time_now - capabilities_updated_at[0] > 3600: capabilities_updated_at.popleft() @@ -1444,8 +1453,6 @@ class Entity( ) self._async_subscribe_device_updates() - self.__capabilities_updated_at = deque(maxlen=CAPABILITIES_UPDATE_LIMIT + 1) - async def async_internal_will_remove_from_hass(self) -> None: """Run when entity will be removed from hass. From 613bdebfe59aef2480067e6d6befe52b0685c412 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Wed, 3 Apr 2024 15:15:23 +0200 Subject: [PATCH 220/967] Migrate sabnzbd to use data update coordinator (#114745) * Migrate sabnzbd to use data update coordinator * Add to coveragerc * Setup coordinator after migration * Use kB/s as UoM * Add suggested --- .coveragerc | 1 + CODEOWNERS | 4 +- homeassistant/components/sabnzbd/__init__.py | 98 +++++-------------- homeassistant/components/sabnzbd/const.py | 10 -- .../components/sabnzbd/coordinator.py | 40 ++++++++ .../components/sabnzbd/manifest.json | 2 +- homeassistant/components/sabnzbd/sensor.py | 57 +++++------ 7 files changed, 88 insertions(+), 124 deletions(-) create mode 100644 homeassistant/components/sabnzbd/coordinator.py diff --git a/.coveragerc b/.coveragerc index cbabcb7733d..ed658f3ca55 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1184,6 +1184,7 @@ omit = homeassistant/components/rympro/coordinator.py homeassistant/components/rympro/sensor.py homeassistant/components/sabnzbd/__init__.py + homeassistant/components/sabnzbd/coordinator.py homeassistant/components/sabnzbd/sensor.py homeassistant/components/saj/sensor.py homeassistant/components/satel_integra/* diff --git a/CODEOWNERS b/CODEOWNERS index 59359e708f9..fa06757896c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1177,8 +1177,8 @@ build.json @home-assistant/supervisor /tests/components/ruuvitag_ble/ @akx /homeassistant/components/rympro/ @OnFreund @elad-bar @maorcc /tests/components/rympro/ @OnFreund @elad-bar @maorcc -/homeassistant/components/sabnzbd/ @shaiu -/tests/components/sabnzbd/ @shaiu +/homeassistant/components/sabnzbd/ @shaiu @jpbede +/tests/components/sabnzbd/ @shaiu @jpbede /homeassistant/components/saj/ @fredericvl /homeassistant/components/samsungtv/ @chemelli74 @epenet /tests/components/samsungtv/ @chemelli74 @epenet diff --git a/homeassistant/components/sabnzbd/__init__.py b/homeassistant/components/sabnzbd/__init__.py index 6a68f98203b..ebb9284a7f2 100644 --- a/homeassistant/components/sabnzbd/__init__.py +++ b/homeassistant/components/sabnzbd/__init__.py @@ -6,7 +6,6 @@ from collections.abc import Callable, Coroutine import logging from typing import Any -from pysabnzbd import SabnzbdApiException import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry, ConfigEntryState @@ -23,9 +22,7 @@ from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import async_get -from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries -from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType from .const import ( @@ -37,15 +34,11 @@ from .const import ( DEFAULT_SPEED_LIMIT, DEFAULT_SSL, DOMAIN, - KEY_API, - KEY_API_DATA, - KEY_NAME, SERVICE_PAUSE, SERVICE_RESUME, SERVICE_SET_SPEED, - SIGNAL_SABNZBD_UPDATED, - UPDATE_INTERVAL, ) +from .coordinator import SabnzbdUpdateCoordinator from .sab import get_client from .sensor import OLD_SENSOR_KEYS @@ -179,30 +172,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not sab_api: raise ConfigEntryNotReady - sab_api_data = SabnzbdApiData(sab_api) - - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { - KEY_API: sab_api, - KEY_API_DATA: sab_api_data, - KEY_NAME: entry.data[CONF_NAME], - } - await migrate_unique_id(hass, entry) update_device_identifiers(hass, entry) + coordinator = SabnzbdUpdateCoordinator(hass, sab_api) + await coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + @callback def extract_api( - func: Callable[[ServiceCall, SabnzbdApiData], Coroutine[Any, Any, None]], + func: Callable[ + [ServiceCall, SabnzbdUpdateCoordinator], Coroutine[Any, Any, None] + ], ) -> Callable[[ServiceCall], Coroutine[Any, Any, None]]: """Define a decorator to get the correct api for a service call.""" async def wrapper(call: ServiceCall) -> None: """Wrap the service function.""" entry_id = async_get_entry_id_for_service_call(hass, call) - api_data = hass.data[DOMAIN][entry_id][KEY_API_DATA] + coordinator: SabnzbdUpdateCoordinator = hass.data[DOMAIN][entry_id] try: - await func(call, api_data) + await func(call, coordinator) except Exception as err: raise HomeAssistantError( f"Error while executing {func.__name__}: {err}" @@ -211,17 +202,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return wrapper @extract_api - async def async_pause_queue(call: ServiceCall, api: SabnzbdApiData) -> None: - await api.async_pause_queue() + async def async_pause_queue( + call: ServiceCall, coordinator: SabnzbdUpdateCoordinator + ) -> None: + await coordinator.sab_api.pause_queue() @extract_api - async def async_resume_queue(call: ServiceCall, api: SabnzbdApiData) -> None: - await api.async_resume_queue() + async def async_resume_queue( + call: ServiceCall, coordinator: SabnzbdUpdateCoordinator + ) -> None: + await coordinator.sab_api.resume_queue() @extract_api - async def async_set_queue_speed(call: ServiceCall, api: SabnzbdApiData) -> None: + async def async_set_queue_speed( + call: ServiceCall, coordinator: SabnzbdUpdateCoordinator + ) -> None: speed = call.data.get(ATTR_SPEED) - await api.async_set_queue_speed(speed) + await coordinator.sab_api.set_speed_limit(speed) for service, method, schema in ( (SERVICE_PAUSE, async_pause_queue, SERVICE_BASE_SCHEMA), @@ -233,18 +230,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.services.async_register(DOMAIN, service, method, schema=schema) - async def async_update_sabnzbd(now): - """Refresh SABnzbd queue data.""" - try: - await sab_api.refresh_data() - async_dispatcher_send(hass, SIGNAL_SABNZBD_UPDATED, None) - except SabnzbdApiException as err: - _LOGGER.error(err) - - entry.async_on_unload( - async_track_time_interval(hass, async_update_sabnzbd, UPDATE_INTERVAL) - ) - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -268,42 +253,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.services.async_remove(DOMAIN, service_name) return unload_ok - - -class SabnzbdApiData: - """Class for storing/refreshing sabnzbd api queue data.""" - - def __init__(self, sab_api): - """Initialize component.""" - self.sab_api = sab_api - - async def async_pause_queue(self): - """Pause Sabnzbd queue.""" - - try: - return await self.sab_api.pause_queue() - except SabnzbdApiException as err: - _LOGGER.error(err) - return False - - async def async_resume_queue(self): - """Resume Sabnzbd queue.""" - - try: - return await self.sab_api.resume_queue() - except SabnzbdApiException as err: - _LOGGER.error(err) - return False - - async def async_set_queue_speed(self, limit): - """Set speed limit for the Sabnzbd queue.""" - - try: - return await self.sab_api.set_speed_limit(limit) - except SabnzbdApiException as err: - _LOGGER.error(err) - return False - - def get_queue_field(self, field): - """Return the value for the given field from the Sabnzbd queue.""" - return self.sab_api.queue.get(field) diff --git a/homeassistant/components/sabnzbd/const.py b/homeassistant/components/sabnzbd/const.py index a9cd80898f7..55346509133 100644 --- a/homeassistant/components/sabnzbd/const.py +++ b/homeassistant/components/sabnzbd/const.py @@ -1,7 +1,5 @@ """Constants for the Sabnzbd component.""" -from datetime import timedelta - DOMAIN = "sabnzbd" DATA_SABNZBD = "sabnzbd" @@ -14,14 +12,6 @@ DEFAULT_PORT = 8080 DEFAULT_SPEED_LIMIT = "100" DEFAULT_SSL = False -UPDATE_INTERVAL = timedelta(seconds=30) - SERVICE_PAUSE = "pause" SERVICE_RESUME = "resume" SERVICE_SET_SPEED = "set_speed" - -SIGNAL_SABNZBD_UPDATED = "sabnzbd_updated" - -KEY_API = "api" -KEY_API_DATA = "api_data" -KEY_NAME = "name" diff --git a/homeassistant/components/sabnzbd/coordinator.py b/homeassistant/components/sabnzbd/coordinator.py new file mode 100644 index 00000000000..5db59bb584b --- /dev/null +++ b/homeassistant/components/sabnzbd/coordinator.py @@ -0,0 +1,40 @@ +"""DataUpdateCoordinator for the SABnzbd integration.""" + +from datetime import timedelta +import logging +from typing import Any + +from pysabnzbd import SabnzbdApi, SabnzbdApiException + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +_LOGGER = logging.getLogger(__name__) + + +class SabnzbdUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): + """The SABnzbd update coordinator.""" + + def __init__( + self, + hass: HomeAssistant, + sab_api: SabnzbdApi, + ) -> None: + """Initialize the SABnzbd update coordinator.""" + self.sab_api = sab_api + + super().__init__( + hass, + _LOGGER, + name="SABnzbd", + update_interval=timedelta(seconds=30), + ) + + async def _async_update_data(self) -> dict[str, Any]: + """Get the latest data from the SABnzbd API.""" + try: + await self.sab_api.refresh_data() + except SabnzbdApiException as err: + raise UpdateFailed("Error while fetching data") from err + + return self.sab_api.queue diff --git a/homeassistant/components/sabnzbd/manifest.json b/homeassistant/components/sabnzbd/manifest.json index 1fb0d09dd60..afc35a2340e 100644 --- a/homeassistant/components/sabnzbd/manifest.json +++ b/homeassistant/components/sabnzbd/manifest.json @@ -1,7 +1,7 @@ { "domain": "sabnzbd", "name": "SABnzbd", - "codeowners": ["@shaiu"], + "codeowners": ["@shaiu", "@jpbede"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sabnzbd", "iot_class": "local_polling", diff --git a/homeassistant/components/sabnzbd/sensor.py b/homeassistant/components/sabnzbd/sensor.py index d5f19b5e718..d956d06f1ac 100644 --- a/homeassistant/components/sabnzbd/sensor.py +++ b/homeassistant/components/sabnzbd/sensor.py @@ -14,11 +14,12 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfDataRate, UnitOfInformation from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import DOMAIN, SIGNAL_SABNZBD_UPDATED -from .const import DEFAULT_NAME, KEY_API_DATA +from . import DOMAIN, SabnzbdUpdateCoordinator +from .const import DEFAULT_NAME @dataclass(frozen=True, kw_only=True) @@ -28,18 +29,18 @@ class SabnzbdSensorEntityDescription(SensorEntityDescription): key: str -SPEED_KEY = "kbpersec" - SENSOR_TYPES: tuple[SabnzbdSensorEntityDescription, ...] = ( SabnzbdSensorEntityDescription( key="status", translation_key="status", ), SabnzbdSensorEntityDescription( - key=SPEED_KEY, + key="kbpersec", translation_key="speed", device_class=SensorDeviceClass.DATA_RATE, - native_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND, + native_unit_of_measurement=UnitOfDataRate.KILOBYTES_PER_SECOND, + suggested_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND, + suggested_display_precision=1, state_class=SensorStateClass.MEASUREMENT, ), SabnzbdSensorEntityDescription( @@ -74,6 +75,7 @@ SENSOR_TYPES: tuple[SabnzbdSensorEntityDescription, ...] = ( key="noofslots_total", translation_key="queue_count", state_class=SensorStateClass.TOTAL, + suggested_display_precision=2, ), SabnzbdSensorEntityDescription( key="day_size", @@ -82,6 +84,7 @@ SENSOR_TYPES: tuple[SabnzbdSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.DATA_SIZE, entity_registry_enabled_default=False, state_class=SensorStateClass.TOTAL_INCREASING, + suggested_display_precision=2, ), SabnzbdSensorEntityDescription( key="week_size", @@ -90,6 +93,7 @@ SENSOR_TYPES: tuple[SabnzbdSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.DATA_SIZE, entity_registry_enabled_default=False, state_class=SensorStateClass.TOTAL_INCREASING, + suggested_display_precision=2, ), SabnzbdSensorEntityDescription( key="month_size", @@ -98,6 +102,7 @@ SENSOR_TYPES: tuple[SabnzbdSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.DATA_SIZE, entity_registry_enabled_default=False, state_class=SensorStateClass.TOTAL_INCREASING, + suggested_display_precision=2, ), SabnzbdSensorEntityDescription( key="total_size", @@ -105,6 +110,7 @@ SENSOR_TYPES: tuple[SabnzbdSensorEntityDescription, ...] = ( native_unit_of_measurement=UnitOfInformation.GIGABYTES, device_class=SensorDeviceClass.DATA_SIZE, state_class=SensorStateClass.TOTAL_INCREASING, + suggested_display_precision=2, ), ) @@ -131,15 +137,14 @@ async def async_setup_entry( """Set up a Sabnzbd sensor entry.""" entry_id = config_entry.entry_id - - sab_api_data = hass.data[DOMAIN][entry_id][KEY_API_DATA] + coordinator: SabnzbdUpdateCoordinator = hass.data[DOMAIN][entry_id] async_add_entities( - [SabnzbdSensor(sab_api_data, sensor, entry_id) for sensor in SENSOR_TYPES] + [SabnzbdSensor(coordinator, sensor, entry_id) for sensor in SENSOR_TYPES] ) -class SabnzbdSensor(SensorEntity): +class SabnzbdSensor(CoordinatorEntity[SabnzbdUpdateCoordinator], SensorEntity): """Representation of an SABnzbd sensor.""" entity_description: SabnzbdSensorEntityDescription @@ -148,40 +153,22 @@ class SabnzbdSensor(SensorEntity): def __init__( self, - sabnzbd_api_data, + coordinator: SabnzbdUpdateCoordinator, description: SabnzbdSensorEntityDescription, entry_id, ) -> None: """Initialize the sensor.""" + super().__init__(coordinator) self._attr_unique_id = f"{entry_id}_{description.key}" self.entity_description = description - self._sabnzbd_api = sabnzbd_api_data self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, entry_id)}, name=DEFAULT_NAME, ) - async def async_added_to_hass(self) -> None: - """Call when entity about to be added to hass.""" - self.async_on_remove( - async_dispatcher_connect( - self.hass, SIGNAL_SABNZBD_UPDATED, self.update_state - ) - ) - - def update_state(self, args): - """Get the latest data and updates the states.""" - self._attr_native_value = self._sabnzbd_api.get_queue_field( - self.entity_description.key - ) - - if self._attr_native_value is not None: - if self.entity_description.key == SPEED_KEY: - self._attr_native_value = round( - float(self._attr_native_value) / 1024, 1 - ) - elif "size" in self.entity_description.key: - self._attr_native_value = round(float(self._attr_native_value), 2) - self.schedule_update_ha_state() + @property + def native_value(self) -> StateType: + """Return latest sensor data.""" + return self.coordinator.data.get(self.entity_description.key) From 90c06d6538fd714aa049f4d9c11de0566ee425c6 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 3 Apr 2024 15:19:49 +0200 Subject: [PATCH 221/967] Update frontend to 20240403.0 (#114747) --- 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 3ac7efa9fab..e2826fdb185 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==20240402.2"] + "requirements": ["home-assistant-frontend==20240403.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 56ea6b6b0ba..15d165c6b5e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -30,7 +30,7 @@ habluetooth==2.4.2 hass-nabucasa==0.79.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240402.2 +home-assistant-frontend==20240403.0 home-assistant-intents==2024.3.29 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 888358f3384..05a107c6ac5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1081,7 +1081,7 @@ hole==0.8.0 holidays==0.46 # homeassistant.components.frontend -home-assistant-frontend==20240402.2 +home-assistant-frontend==20240403.0 # homeassistant.components.conversation home-assistant-intents==2024.3.29 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9ebe2010b39..d2fbdbb9b98 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -880,7 +880,7 @@ hole==0.8.0 holidays==0.46 # homeassistant.components.frontend -home-assistant-frontend==20240402.2 +home-assistant-frontend==20240403.0 # homeassistant.components.conversation home-assistant-intents==2024.3.29 From ed88c2abc9bcc29e80e6c9cc72967d2479554a6a Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 3 Apr 2024 15:43:12 +0200 Subject: [PATCH 222/967] Replace pytest-test-groups by custom tests splitter (#114381) Co-authored-by: Martin Hjelmare Co-authored-by: Paulus Schoutsen --- .github/workflows/ci.yaml | 255 +++++++++++++++++++++++++++++++------- .gitignore | 3 + requirements_test.txt | 1 - script/split_tests.py | 225 +++++++++++++++++++++++++++++++++ 4 files changed, 437 insertions(+), 47 deletions(-) create mode 100755 script/split_tests.py diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9ab7e235a68..7f78b5fcdab 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -670,14 +670,61 @@ jobs: python --version mypy homeassistant/components/${{ needs.info.outputs.integrations_glob }} - pytest: + prepare-pytest-full: runs-on: ubuntu-22.04 if: | (github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core') && github.event.inputs.lint-only != 'true' && github.event.inputs.pylint-only != 'true' && github.event.inputs.mypy-only != 'true' - && (needs.info.outputs.test_full_suite == 'true' || needs.info.outputs.tests_glob) + && needs.info.outputs.test_full_suite == 'true' + needs: + - info + - base + name: Split tests for full run + steps: + - name: Install additional OS dependencies + run: | + sudo apt-get update + sudo apt-get -y install \ + bluez \ + ffmpeg + - name: Check out code from GitHub + uses: actions/checkout@v4.1.2 + - 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 base 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: Run split_tests.py + run: | + . venv/bin/activate + python -m script.split_tests ${{ needs.info.outputs.test_group_count }} tests + - name: Upload pytest_buckets + uses: actions/upload-artifact@v4.3.1 + with: + name: pytest_buckets + path: pytest_buckets.txt + overwrite: true + + pytest-full: + runs-on: ubuntu-22.04 + if: | + (github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core') + && github.event.inputs.lint-only != 'true' + && github.event.inputs.pylint-only != 'true' + && github.event.inputs.mypy-only != 'true' + && needs.info.outputs.test_full_suite == 'true' needs: - info - base @@ -686,6 +733,7 @@ jobs: - lint-other - lint-ruff - mypy + - prepare-pytest-full strategy: fail-fast: false matrix: @@ -722,12 +770,15 @@ jobs: - name: Register pytest slow test problem matcher run: | echo "::add-matcher::.github/workflows/matchers/pytest-slow.json" + - name: Download pytest_buckets + uses: actions/download-artifact@v4.1.4 + with: + name: pytest_buckets - name: Compile English translations run: | . venv/bin/activate python3 -m script.translations develop --all - - name: Run pytest (fully) - if: needs.info.outputs.test_full_suite == 'true' + - name: Run pytest timeout-minutes: 60 id: pytest-full env: @@ -748,50 +799,13 @@ jobs: --durations=10 \ -n auto \ --dist=loadfile \ - --test-group-count ${{ needs.info.outputs.test_group_count }} \ - --test-group=${{ matrix.group }} \ ${cov_params[@]} \ -o console_output_style=count \ -p no:sugar \ - tests \ - 2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt - - name: Run pytest (partially) - if: needs.info.outputs.test_full_suite == 'false' - timeout-minutes: 10 - id: pytest-partial - shell: bash - env: - PYTHONDONTWRITEBYTECODE: 1 - run: | - . venv/bin/activate - python --version - set -o pipefail - - if [[ ! -f "tests/components/${{ matrix.group }}/__init__.py" ]]; then - echo "::error:: missing file tests/components/${{ matrix.group }}/__init__.py" - exit 1 - fi - - cov_params=() - if [[ "${{ needs.info.outputs.skip_coverage }}" != "true" ]]; then - cov_params+=(--cov="homeassistant.components.${{ matrix.group }}") - cov_params+=(--cov-report=xml) - cov_params+=(--cov-report=term-missing) - fi - - python3 -b -X dev -m pytest \ - -qq \ - --timeout=9 \ - -n auto \ - ${cov_params[@]} \ - -o console_output_style=count \ - --durations=0 \ - --durations-min=1 \ - -p no:sugar \ - tests/components/${{ matrix.group }} \ + $(sed -n "${{ matrix.group }},1p" pytest_buckets.txt) \ 2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt - name: Upload pytest output - if: success() || failure() && (steps.pytest-full.conclusion == 'failure' || steps.pytest-partial.conclusion == 'failure') + if: success() || failure() && steps.pytest-full.conclusion == 'failure' uses: actions/upload-artifact@v4.3.1 with: name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }} @@ -804,6 +818,8 @@ jobs: name: coverage-${{ matrix.python-version }}-${{ matrix.group }} path: coverage.xml overwrite: true + - name: Remove pytest_buckets + run: rm pytest_buckets.txt - name: Check dirty run: | ./script/check_dirty @@ -1053,13 +1069,160 @@ jobs: run: | ./script/check_dirty - coverage: - name: Upload test coverage to Codecov + coverage-full: + name: Upload test coverage to Codecov (full suite) if: needs.info.outputs.skip_coverage != 'true' runs-on: ubuntu-22.04 needs: - info - - pytest + - pytest-full + - pytest-postgres + - pytest-mariadb + timeout-minutes: 10 + steps: + - name: Check out code from GitHub + uses: actions/checkout@v4.1.2 + - name: Download all coverage artifacts + uses: actions/download-artifact@v4.1.4 + with: + pattern: coverage-* + - name: Upload coverage to Codecov (full coverage) + if: needs.info.outputs.test_full_suite == 'true' + uses: Wandalen/wretry.action@v2.1.0 + with: + action: codecov/codecov-action@v3.1.3 + with: | + fail_ci_if_error: true + flags: full-suite + token: ${{ env.CODECOV_TOKEN }} + attempt_limit: 5 + attempt_delay: 30000 + - name: Upload coverage to Codecov (partial coverage) + if: needs.info.outputs.test_full_suite == 'false' + uses: Wandalen/wretry.action@v2.1.0 + with: + action: codecov/codecov-action@v3.1.3 + with: | + fail_ci_if_error: true + token: ${{ env.CODECOV_TOKEN }} + attempt_limit: 5 + attempt_delay: 30000 + + pytest-partial: + runs-on: ubuntu-22.04 + if: | + (github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core') + && github.event.inputs.lint-only != 'true' + && github.event.inputs.pylint-only != 'true' + && github.event.inputs.mypy-only != 'true' + && needs.info.outputs.tests_glob + needs: + - info + - base + - gen-requirements-all + - hassfest + - lint-other + - lint-ruff + - mypy + strategy: + fail-fast: false + matrix: + group: ${{ fromJson(needs.info.outputs.test_groups) }} + python-version: ${{ fromJson(needs.info.outputs.python_versions) }} + name: >- + Run tests Python ${{ matrix.python-version }} (${{ matrix.group }}) + steps: + - name: Install additional OS dependencies + run: | + sudo apt-get update + sudo apt-get -y install \ + bluez \ + ffmpeg + - name: Check out code from GitHub + uses: actions/checkout@v4.1.2 + - name: Set up Python ${{ matrix.python-version }} + id: python + uses: actions/setup-python@v5.1.0 + with: + python-version: ${{ matrix.python-version }} + check-latest: true + - name: Restore full Python ${{ matrix.python-version }} 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 Python problem matcher + run: | + echo "::add-matcher::.github/workflows/matchers/python.json" + - name: Register pytest slow test problem matcher + run: | + echo "::add-matcher::.github/workflows/matchers/pytest-slow.json" + - name: Compile English translations + run: | + . venv/bin/activate + python3 -m script.translations develop --all + - name: Run pytest + timeout-minutes: 10 + id: pytest-partial + shell: bash + env: + PYTHONDONTWRITEBYTECODE: 1 + run: | + . venv/bin/activate + python --version + set -o pipefail + + if [[ ! -f "tests/components/${{ matrix.group }}/__init__.py" ]]; then + echo "::error:: missing file tests/components/${{ matrix.group }}/__init__.py" + exit 1 + fi + + cov_params=() + if [[ "${{ needs.info.outputs.skip_coverage }}" != "true" ]]; then + cov_params+=(--cov="homeassistant.components.${{ matrix.group }}") + cov_params+=(--cov-report=xml) + cov_params+=(--cov-report=term-missing) + fi + + python3 -b -X dev -m pytest \ + -qq \ + --timeout=9 \ + -n auto \ + ${cov_params[@]} \ + -o console_output_style=count \ + --durations=0 \ + --durations-min=1 \ + -p no:sugar \ + tests/components/${{ matrix.group }} \ + 2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt + - name: Upload pytest output + if: success() || failure() && steps.pytest-partial.conclusion == 'failure' + uses: actions/upload-artifact@v4.3.1 + with: + name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }} + path: pytest-*.txt + overwrite: true + - name: Upload coverage artifact + if: needs.info.outputs.skip_coverage != 'true' + uses: actions/upload-artifact@v4.3.1 + with: + name: coverage-${{ matrix.python-version }}-${{ matrix.group }} + path: coverage.xml + overwrite: true + - name: Check dirty + run: | + ./script/check_dirty + + coverage-partial: + name: Upload test coverage to Codecov (partial suite) + if: needs.info.outputs.skip_coverage != 'true' + runs-on: ubuntu-22.04 + needs: + - info + - pytest-partial timeout-minutes: 10 steps: - name: Check out code from GitHub diff --git a/.gitignore b/.gitignore index 8a4154e4769..206595f06c9 100644 --- a/.gitignore +++ b/.gitignore @@ -132,3 +132,6 @@ tmp_cache # python-language-server / Rope .ropeproject + +# Will be created from script/split_tests.py +pytest_buckets.txt \ No newline at end of file diff --git a/requirements_test.txt b/requirements_test.txt index c2e5774e137..553f44eeb25 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -23,7 +23,6 @@ pytest-cov==5.0.0 pytest-freezer==0.4.8 pytest-github-actions-annotate-failures==0.2.0 pytest-socket==0.7.0 -pytest-test-groups==1.0.3 pytest-sugar==1.0.0 pytest-timeout==2.3.1 pytest-unordered==0.6.0 diff --git a/script/split_tests.py b/script/split_tests.py new file mode 100755 index 00000000000..8da03bd749b --- /dev/null +++ b/script/split_tests.py @@ -0,0 +1,225 @@ +#!/usr/bin/env python3 +"""Helper script to split test into n buckets.""" + +from __future__ import annotations + +import argparse +from dataclasses import dataclass, field +from math import ceil +from pathlib import Path +import subprocess +import sys +from typing import Final + + +class Bucket: + """Class to hold bucket.""" + + def __init__( + self, + ): + """Initialize bucket.""" + self.total_tests = 0 + self._paths: list[str] = [] + + def add(self, part: TestFolder | TestFile) -> None: + """Add tests to bucket.""" + part.add_to_bucket() + self.total_tests += part.total_tests + self._paths.append(str(part.path)) + + def get_paths_line(self) -> str: + """Return paths.""" + return " ".join(self._paths) + "\n" + + +class BucketHolder: + """Class to hold buckets.""" + + def __init__(self, tests_per_bucket: int, bucket_count: int) -> None: + """Initialize bucket holder.""" + self._tests_per_bucket = tests_per_bucket + self._bucket_count = bucket_count + self._buckets: list[Bucket] = [Bucket() for _ in range(bucket_count)] + + def split_tests(self, test_folder: TestFolder) -> None: + """Split tests into buckets.""" + digits = len(str(test_folder.total_tests)) + sorted_tests = sorted( + test_folder.get_all_flatten(), reverse=True, key=lambda x: x.total_tests + ) + for tests in sorted_tests: + print(f"{tests.total_tests:>{digits}} tests in {tests.path}") + if tests.added_to_bucket: + # Already added to bucket + continue + + smallest_bucket = min(self._buckets, key=lambda x: x.total_tests) + if ( + smallest_bucket.total_tests + tests.total_tests < self._tests_per_bucket + ) or isinstance(tests, TestFile): + smallest_bucket.add(tests) + + # verify that all tests are added to a bucket + if not test_folder.added_to_bucket: + raise ValueError("Not all tests are added to a bucket") + + def create_ouput_file(self) -> None: + """Create output file.""" + with open("pytest_buckets.txt", "w") as file: + for idx, bucket in enumerate(self._buckets): + print(f"Bucket {idx+1} has {bucket.total_tests} tests") + file.write(bucket.get_paths_line()) + + +@dataclass +class TestFile: + """Class represents a single test file and the number of tests it has.""" + + total_tests: int + path: Path + added_to_bucket: bool = field(default=False, init=False) + + def add_to_bucket(self) -> None: + """Add test file to bucket.""" + if self.added_to_bucket: + raise ValueError("Already added to bucket") + self.added_to_bucket = True + + def __gt__(self, other: TestFile) -> bool: + """Return if greater than.""" + return self.total_tests > other.total_tests + + +class TestFolder: + """Class to hold a folder with test files and folders.""" + + def __init__(self, path: Path) -> None: + """Initialize test folder.""" + self.path: Final = path + self.children: dict[Path, TestFolder | TestFile] = {} + + @property + def total_tests(self) -> int: + """Return total tests.""" + return sum([test.total_tests for test in self.children.values()]) + + @property + def added_to_bucket(self) -> bool: + """Return if added to bucket.""" + return all(test.added_to_bucket for test in self.children.values()) + + def add_to_bucket(self) -> None: + """Add test file to bucket.""" + if self.added_to_bucket: + raise ValueError("Already added to bucket") + for child in self.children.values(): + child.add_to_bucket() + + def __repr__(self) -> str: + """Return representation.""" + return ( + f"TestFolder(total_tests={self.total_tests}, children={len(self.children)})" + ) + + def add_test_file(self, file: TestFile) -> None: + """Add test file to folder.""" + path = file.path + relative_path = path.relative_to(self.path) + if not relative_path.parts: + raise ValueError("Path is not a child of this folder") + + if len(relative_path.parts) == 1: + self.children[path] = file + return + + child_path = self.path / relative_path.parts[0] + if (child := self.children.get(child_path)) is None: + self.children[child_path] = child = TestFolder(child_path) + elif not isinstance(child, TestFolder): + raise ValueError("Child is not a folder") + child.add_test_file(file) + + def get_all_flatten(self) -> list[TestFolder | TestFile]: + """Return self and all children as flatten list.""" + result: list[TestFolder | TestFile] = [self] + for child in self.children.values(): + if isinstance(child, TestFolder): + result.extend(child.get_all_flatten()) + else: + result.append(child) + return result + + +def collect_tests(path: Path) -> TestFolder: + """Collect all tests.""" + result = subprocess.run( + ["pytest", "--collect-only", "-qq", "-p", "no:warnings", path], + check=False, + capture_output=True, + text=True, + ) + + if result.returncode != 0: + print("Failed to collect tests:") + print(result.stderr) + print(result.stdout) + sys.exit(1) + + folder = TestFolder(path) + + for line in result.stdout.splitlines(): + if not line.strip(): + continue + file_path, _, total_tests = line.partition(": ") + if not path or not total_tests: + print(f"Unexpected line: {line}") + sys.exit(1) + + file = TestFile(int(total_tests), Path(file_path)) + folder.add_test_file(file) + + return folder + + +def main() -> None: + """Execute script.""" + parser = argparse.ArgumentParser(description="Split tests into n buckets.") + + def check_greater_0(value: str) -> int: + ivalue = int(value) + if ivalue <= 0: + raise argparse.ArgumentTypeError( + f"{value} is an invalid. Must be greater than 0" + ) + return ivalue + + parser.add_argument( + "bucket_count", + help="Number of buckets to split tests into", + type=check_greater_0, + ) + parser.add_argument( + "path", + help="Path to the test files to split into buckets", + type=Path, + ) + + arguments = parser.parse_args() + + print("Collecting tests...") + tests = collect_tests(arguments.path) + tests_per_bucket = ceil(tests.total_tests / arguments.bucket_count) + + bucket_holder = BucketHolder(tests_per_bucket, arguments.bucket_count) + print("Splitting tests...") + bucket_holder.split_tests(tests) + + print(f"Total tests: {tests.total_tests}") + print(f"Estimated tests per bucket: {tests_per_bucket}") + + bucket_holder.create_ouput_file() + + +if __name__ == "__main__": + main() From f91994d788305436f6998f5d68cdfcee29a2ab88 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 3 Apr 2024 16:01:56 +0200 Subject: [PATCH 223/967] Revert the logger level in the Shelly update platform (#114749) Revert debug level Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/shelly/update.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/update.py b/homeassistant/components/shelly/update.py index 56ad1f2ef67..dc6e9c9698a 100644 --- a/homeassistant/components/shelly/update.py +++ b/homeassistant/components/shelly/update.py @@ -296,7 +296,7 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity): await self.coordinator.async_shutdown_device_and_start_reauth() else: self._ota_in_progress = True - LOGGER.info("OTA update call for %s successful", self.coordinator.name) + LOGGER.debug("OTA update call for %s successful", self.coordinator.name) class RpcSleepingUpdateEntity( From 7adced687680834362bfea10e39fb21ae0e69334 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 3 Apr 2024 16:33:58 +0200 Subject: [PATCH 224/967] Allow passing area/device/entity IDs to floor_id and floor_name (#114748) --- homeassistant/helpers/template.py | 16 ++++ tests/helpers/test_template.py | 125 +++++++++++++++++++++++------- 2 files changed, 115 insertions(+), 26 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index f51cda6927f..0f2dd735a66 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1408,6 +1408,12 @@ def floor_id(hass: HomeAssistant, lookup_value: Any) -> str | None: floor_registry = fr.async_get(hass) if floor := floor_registry.async_get_floor_by_name(str(lookup_value)): return floor.floor_id + + if aid := area_id(hass, lookup_value): + area_reg = area_registry.async_get(hass) + if area := area_reg.async_get_area(aid): + return area.floor_id + return None @@ -1416,6 +1422,16 @@ def floor_name(hass: HomeAssistant, lookup_value: str) -> str | None: floor_registry = fr.async_get(hass) if floor := floor_registry.async_get_floor(lookup_value): return floor.name + + if aid := area_id(hass, lookup_value): + area_reg = area_registry.async_get(hass) + if ( + (area := area_reg.async_get_area(aid)) + and area.floor_id + and (floor := floor_registry.async_get_floor(area.floor_id)) + ): + return floor.name + return None diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 6f455c3dda4..54fdf0368eb 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -5198,17 +5198,23 @@ async def test_floors( async def test_floor_id( hass: HomeAssistant, floor_registry: fr.FloorRegistry, + area_registry: ar.AreaRegistry, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, ) -> None: """Test floor_id function.""" - # Test non existing floor name - info = render_to_info(hass, "{{ floor_id('Third floor') }}") - assert_result_info(info, None) - assert info.rate_limit is None + def test(value: str, expected: str | None) -> None: + info = render_to_info(hass, f"{{{{ floor_id('{value}') }}}}") + assert_result_info(info, expected) + assert info.rate_limit is None - info = render_to_info(hass, "{{ 'Third floor' | floor_id }}") - assert_result_info(info, None) - assert info.rate_limit is None + info = render_to_info(hass, f"{{{{ '{value}' | floor_id }}}}") + assert_result_info(info, expected) + assert info.rate_limit is None + + # Test non existing floor name + test("Third floor", None) # Test wrong value type info = render_to_info(hass, "{{ floor_id(42) }}") @@ -5221,28 +5227,65 @@ async def test_floor_id( # Test with an actual floor floor = floor_registry.async_create("First floor") - info = render_to_info(hass, "{{ floor_id('First floor') }}") - assert_result_info(info, floor.floor_id) - assert info.rate_limit is None + test("First floor", floor.floor_id) - info = render_to_info(hass, "{{ 'First floor' | floor_id }}") - assert_result_info(info, floor.floor_id) - assert info.rate_limit is None + config_entry = MockConfigEntry(domain="light") + config_entry.add_to_hass(hass) + area_entry_hex = area_registry.async_get_or_create("123abc") + + # Create area, device, entity and assign area to device and entity + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_entry = entity_registry.async_get_or_create( + "light", + "hue", + "5678", + config_entry=config_entry, + device_id=device_entry.id, + ) + device_entry = device_registry.async_update_device( + device_entry.id, area_id=area_entry_hex.id + ) + entity_entry = entity_registry.async_update_entity( + entity_entry.entity_id, area_id=area_entry_hex.id + ) + + test(area_entry_hex.id, None) + test(device_entry.id, None) + test(entity_entry.entity_id, None) + + # Add floor to area + area_entry_hex = area_registry.async_update( + area_entry_hex.id, floor_id=floor.floor_id + ) + + test(area_entry_hex.id, floor.floor_id) + test(device_entry.id, floor.floor_id) + test(entity_entry.entity_id, floor.floor_id) async def test_floor_name( hass: HomeAssistant, floor_registry: fr.FloorRegistry, + area_registry: ar.AreaRegistry, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, ) -> None: """Test floor_name function.""" - # Test non existing floor ID - info = render_to_info(hass, "{{ floor_name('third_floor') }}") - assert_result_info(info, None) - assert info.rate_limit is None - info = render_to_info(hass, "{{ 'third_floor' | floor_name }}") - assert_result_info(info, None) - assert info.rate_limit is None + def test(value: str, expected: str | None) -> None: + info = render_to_info(hass, f"{{{{ floor_name('{value}') }}}}") + assert_result_info(info, expected) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ '{value}' | floor_name }}}}") + assert_result_info(info, expected) + assert info.rate_limit is None + + # Test non existing floor name + test("Third floor", None) # Test wrong value type info = render_to_info(hass, "{{ floor_name(42) }}") @@ -5255,13 +5298,43 @@ async def test_floor_name( # Test existing floor ID floor = floor_registry.async_create("First floor") - info = render_to_info(hass, f"{{{{ floor_name('{floor.floor_id}') }}}}") - assert_result_info(info, floor.name) - assert info.rate_limit is None + test(floor.floor_id, floor.name) - info = render_to_info(hass, f"{{{{ '{floor.floor_id}' | floor_name }}}}") - assert_result_info(info, floor.name) - assert info.rate_limit is None + config_entry = MockConfigEntry(domain="light") + config_entry.add_to_hass(hass) + area_entry_hex = area_registry.async_get_or_create("123abc") + + # Create area, device, entity and assign area to device and entity + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_entry = entity_registry.async_get_or_create( + "light", + "hue", + "5678", + config_entry=config_entry, + device_id=device_entry.id, + ) + device_entry = device_registry.async_update_device( + device_entry.id, area_id=area_entry_hex.id + ) + entity_entry = entity_registry.async_update_entity( + entity_entry.entity_id, area_id=area_entry_hex.id + ) + + test(area_entry_hex.id, None) + test(device_entry.id, None) + test(entity_entry.entity_id, None) + + # Add floor to area + area_entry_hex = area_registry.async_update( + area_entry_hex.id, floor_id=floor.floor_id + ) + + test(area_entry_hex.id, floor.name) + test(device_entry.id, floor.name) + test(entity_entry.entity_id, floor.name) async def test_floor_areas( From e2c99d226eeb1ce537e010cd49e2112e58c1d179 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 3 Apr 2024 16:45:07 +0200 Subject: [PATCH 225/967] Fix CI after splitting tests (#114754) --- .github/workflows/ci.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7f78b5fcdab..ac7be91d1bd 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1116,6 +1116,7 @@ jobs: && github.event.inputs.pylint-only != 'true' && github.event.inputs.mypy-only != 'true' && needs.info.outputs.tests_glob + && needs.info.outputs.test_full_suite == 'false' needs: - info - base From 26c7e170e9c5b9b2d11f0a348d1f537e393a394b Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 3 Apr 2024 10:27:26 -0500 Subject: [PATCH 226/967] Bump intents (#114755) --- 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 07fc86313ba..76c5b5ad666 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -7,5 +7,5 @@ "integration_type": "system", "iot_class": "local_push", "quality_scale": "internal", - "requirements": ["hassil==1.6.1", "home-assistant-intents==2024.3.29"] + "requirements": ["hassil==1.6.1", "home-assistant-intents==2024.4.3"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 15d165c6b5e..c486820f48c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -31,7 +31,7 @@ hass-nabucasa==0.79.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 home-assistant-frontend==20240403.0 -home-assistant-intents==2024.3.29 +home-assistant-intents==2024.4.3 httpx==0.27.0 ifaddr==0.2.0 Jinja2==3.1.3 diff --git a/requirements_all.txt b/requirements_all.txt index 05a107c6ac5..d59030c8d25 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1084,7 +1084,7 @@ holidays==0.46 home-assistant-frontend==20240403.0 # homeassistant.components.conversation -home-assistant-intents==2024.3.29 +home-assistant-intents==2024.4.3 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d2fbdbb9b98..dab43a1a5aa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -883,7 +883,7 @@ holidays==0.46 home-assistant-frontend==20240403.0 # homeassistant.components.conversation -home-assistant-intents==2024.3.29 +home-assistant-intents==2024.4.3 # homeassistant.components.home_connect homeconnect==0.7.2 From dcef40f27c10077f79deb01270a329f58e03860a Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 3 Apr 2024 17:32:26 +0200 Subject: [PATCH 227/967] Update frontend to 20240403.1 (#114756) --- 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 e2826fdb185..1890572bf5a 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==20240403.0"] + "requirements": ["home-assistant-frontend==20240403.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c486820f48c..b5ea145844e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -30,7 +30,7 @@ habluetooth==2.4.2 hass-nabucasa==0.79.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240403.0 +home-assistant-frontend==20240403.1 home-assistant-intents==2024.4.3 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index d59030c8d25..8507c5afb55 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1081,7 +1081,7 @@ hole==0.8.0 holidays==0.46 # homeassistant.components.frontend -home-assistant-frontend==20240403.0 +home-assistant-frontend==20240403.1 # homeassistant.components.conversation home-assistant-intents==2024.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dab43a1a5aa..d9679f1524b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -880,7 +880,7 @@ hole==0.8.0 holidays==0.46 # homeassistant.components.frontend -home-assistant-frontend==20240403.0 +home-assistant-frontend==20240403.1 # homeassistant.components.conversation home-assistant-intents==2024.4.3 From 6369b756532bed2754a8a9069b44badc8b28710d Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:53:44 +0200 Subject: [PATCH 228/967] Fix Synology DSM setup in case no Surveillance Station permission (#114757) --- homeassistant/components/synology_dsm/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index a0c3a10774f..ec93c92a698 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -105,6 +105,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if ( SynoSurveillanceStation.INFO_API_KEY in available_apis and SynoSurveillanceStation.HOME_MODE_API_KEY in available_apis + and api.surveillance_station is not None ): coordinator_switches = SynologyDSMSwitchUpdateCoordinator(hass, entry, api) await coordinator_switches.async_config_entry_first_refresh() From 51a3e790480f4b7b9fc081b3f6aa6637b04bf805 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chris=20Br=C3=A4ucker?= <7303810+chrisbraucker@users.noreply.github.com> Date: Wed, 3 Apr 2024 18:37:20 +0200 Subject: [PATCH 229/967] Add wake on LAN via Fritz!Box for tracked devices (#106778) --- homeassistant/components/fritz/button.py | 92 +++++++++++++++++- homeassistant/components/fritz/common.py | 11 +++ homeassistant/components/fritz/const.py | 4 + tests/components/fritz/conftest.py | 10 +- tests/components/fritz/const.py | 70 ++++++++++++- tests/components/fritz/test_button.py | 119 ++++++++++++++++++++++- tests/components/fritz/test_switch.py | 2 +- tests/components/fritz/test_update.py | 2 +- 8 files changed, 293 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/fritz/button.py b/homeassistant/components/fritz/button.py index d56350dd1d0..cfd0e09412d 100644 --- a/homeassistant/components/fritz/button.py +++ b/homeassistant/components/fritz/button.py @@ -14,12 +14,13 @@ from homeassistant.components.button import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .common import AvmWrapper -from .const import DOMAIN +from .common import AvmWrapper, FritzData, FritzDevice, FritzDeviceBase, _is_tracked +from .const import BUTTON_TYPE_WOL, CONNECTION_TYPE_LAN, DATA_FRITZ, DOMAIN, MeshRoles _LOGGER = logging.getLogger(__name__) @@ -70,8 +71,28 @@ async def async_setup_entry( _LOGGER.debug("Setting up buttons") avm_wrapper: AvmWrapper = hass.data[DOMAIN][entry.entry_id] - async_add_entities( - [FritzButton(avm_wrapper, entry.title, button) for button in BUTTONS] + entities_list: list[ButtonEntity] = [ + FritzButton(avm_wrapper, entry.title, button) for button in BUTTONS + ] + + if avm_wrapper.mesh_role == MeshRoles.SLAVE: + async_add_entities(entities_list) + return + + data_fritz: FritzData = hass.data[DATA_FRITZ] + entities_list += _async_wol_buttons_list(avm_wrapper, data_fritz) + + async_add_entities(entities_list) + + @callback + def async_update_avm_device() -> None: + """Update the values of the AVM device.""" + async_add_entities(_async_wol_buttons_list(avm_wrapper, data_fritz)) + + entry.async_on_unload( + async_dispatcher_connect( + hass, avm_wrapper.signal_device_new, async_update_avm_device + ) ) @@ -101,3 +122,64 @@ class FritzButton(ButtonEntity): async def async_press(self) -> None: """Triggers Fritz!Box service.""" await self.entity_description.press_action(self.avm_wrapper) + + +@callback +def _async_wol_buttons_list( + avm_wrapper: AvmWrapper, + data_fritz: FritzData, +) -> list[FritzBoxWOLButton]: + """Add new WOL button entities from the AVM device.""" + _LOGGER.debug("Setting up %s buttons", BUTTON_TYPE_WOL) + + new_wols: list[FritzBoxWOLButton] = [] + + if avm_wrapper.unique_id not in data_fritz.wol_buttons: + data_fritz.wol_buttons[avm_wrapper.unique_id] = set() + + for mac, device in avm_wrapper.devices.items(): + if _is_tracked(mac, data_fritz.wol_buttons.values()): + _LOGGER.debug("Skipping wol button creation for device %s", device.hostname) + continue + + if device.connection_type != CONNECTION_TYPE_LAN: + _LOGGER.debug( + "Skipping wol button creation for device %s, not connected via LAN", + device.hostname, + ) + continue + + new_wols.append(FritzBoxWOLButton(avm_wrapper, device)) + data_fritz.wol_buttons[avm_wrapper.unique_id].add(mac) + + _LOGGER.debug("Creating %s wol buttons", len(new_wols)) + return new_wols + + +class FritzBoxWOLButton(FritzDeviceBase, ButtonEntity): + """Defines a FRITZ!Box Tools Wake On LAN button.""" + + _attr_icon = "mdi:lan-pending" + _attr_entity_registry_enabled_default = False + + def __init__(self, avm_wrapper: AvmWrapper, device: FritzDevice) -> None: + """Initialize Fritz!Box WOL button.""" + super().__init__(avm_wrapper, device) + self._name = f"{self.hostname} Wake on LAN" + self._attr_unique_id = f"{self._mac}_wake_on_lan" + self._is_available = True + self._attr_device_info = DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, self._mac)}, + default_manufacturer="AVM", + default_model="FRITZ!Box Tracked device", + default_name=device.hostname, + via_device=( + DOMAIN, + avm_wrapper.unique_id, + ), + ) + + async def async_press(self) -> None: + """Press the button.""" + if self.mac_address: + await self._avm_wrapper.async_wake_on_lan(self.mac_address) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 9d8bcd1ab3e..e4d5e92b742 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -928,6 +928,16 @@ class AvmWrapper(FritzBoxTools): # pylint: disable=hass-enforce-coordinator-mod NewDisallow="0" if turn_on else "1", ) + async def async_wake_on_lan(self, mac_address: str) -> dict[str, Any]: + """Call X_AVM-DE_WakeOnLANByMACAddress service.""" + + return await self._async_service_call( + "Hosts", + "1", + "X_AVM-DE_WakeOnLANByMACAddress", + NewMACAddress=mac_address, + ) + @dataclass class FritzData: @@ -935,6 +945,7 @@ class FritzData: tracked: dict = field(default_factory=dict) profile_switches: dict = field(default_factory=dict) + wol_buttons: dict = field(default_factory=dict) class FritzDeviceBase(update_coordinator.CoordinatorEntity[AvmWrapper]): diff --git a/homeassistant/components/fritz/const.py b/homeassistant/components/fritz/const.py index fb60eaef5f8..caa7d44c378 100644 --- a/homeassistant/components/fritz/const.py +++ b/homeassistant/components/fritz/const.py @@ -65,6 +65,8 @@ SWITCH_TYPE_PORTFORWARD = "PortForward" SWITCH_TYPE_PROFILE = "Profile" SWITCH_TYPE_WIFINETWORK = "WiFiNetwork" +BUTTON_TYPE_WOL = "WakeOnLan" + UPTIME_DEVIATION = 5 FRITZ_EXCEPTIONS = ( @@ -79,3 +81,5 @@ FRITZ_EXCEPTIONS = ( FRITZ_AUTH_EXCEPTIONS = (FritzAuthorizationError, FritzSecurityError) WIFI_STANDARD = {1: "2.4Ghz", 2: "5Ghz", 3: "5Ghz", 4: "Guest"} + +CONNECTION_TYPE_LAN = "LAN" diff --git a/tests/components/fritz/conftest.py b/tests/components/fritz/conftest.py index 2e26f67c1eb..e32ca55f65d 100644 --- a/tests/components/fritz/conftest.py +++ b/tests/components/fritz/conftest.py @@ -77,13 +77,11 @@ class FritzConnectionMock: class FritzHostMock(FritzHosts): """FritzHosts mocking.""" - def get_mesh_topology(self, raw=False): - """Retrurn mocked mesh data.""" - return MOCK_MESH_DATA + get_mesh_topology = MagicMock() + get_mesh_topology.return_value = MOCK_MESH_DATA - def get_hosts_attributes(self): - """Retrurn mocked host attributes data.""" - return MOCK_HOST_ATTRIBUTES_DATA + get_hosts_attributes = MagicMock() + get_hosts_attributes.return_value = MOCK_HOST_ATTRIBUTES_DATA @pytest.fixture(name="fc_data") diff --git a/tests/components/fritz/const.py b/tests/components/fritz/const.py index 30c9f9be174..ce530e32964 100644 --- a/tests/components/fritz/const.py +++ b/tests/components/fritz/const.py @@ -27,7 +27,11 @@ MOCK_CONFIG = { } } MOCK_HOST = "fake_host" -MOCK_IPS = {"fritz.box": "192.168.178.1", "printer": "192.168.178.2"} +MOCK_IPS = { + "fritz.box": "192.168.178.1", + "printer": "192.168.178.2", + "server": "192.168.178.3", +} MOCK_MODELNAME = "FRITZ!Box 7530 AX" MOCK_FIRMWARE = "256.07.29" MOCK_FIRMWARE_AVAILABLE = "7.50" @@ -780,6 +784,45 @@ MOCK_MESH_DATA = { ], } +MOCK_NEW_DEVICE_NODE = { + "uid": "n-900", + "device_name": "server", + "device_model": "", + "device_manufacturer": "", + "device_firmware_version": "", + "device_mac_address": "AA:BB:CC:33:44:55", + "is_meshed": False, + "mesh_role": "unknown", + "meshd_version": "0.0", + "node_interfaces": [ + { + "uid": "ni-901", + "name": "eth0", + "type": "LAN", + "mac_address": "AA:BB:CC:33:44:55", + "blocking_state": "UNKNOWN", + "node_links": [ + { + "uid": "nl-902", + "type": "LAN", + "state": "CONNECTED", + "last_connected": 1642872967, + "node_1_uid": "n-1", + "node_2_uid": "n-900", + "node_interface_1_uid": "ni-31", + "node_interface_2_uid": "ni-901", + "max_data_rate_rx": 1000000, + "max_data_rate_tx": 1000000, + "cur_data_rate_rx": 0, + "cur_data_rate_tx": 0, + "cur_availability_rx": 99, + "cur_availability_tx": 99, + } + ], + } + ], +} + MOCK_HOST_ATTRIBUTES_DATA = [ { "Index": 1, @@ -831,6 +874,31 @@ MOCK_HOST_ATTRIBUTES_DATA = [ "X_AVM-DE_FriendlyName": "fritz.box", "X_AVM-DE_FriendlyNameIsWriteable": "0", }, + { + "Index": 3, + "IPAddress": MOCK_IPS["server"], + "MACAddress": "AA:BB:CC:33:44:55", + "Active": True, + "HostName": "server", + "InterfaceType": "Ethernet", + "X_AVM-DE_Port": 1, + "X_AVM-DE_Speed": 1000, + "X_AVM-DE_UpdateAvailable": False, + "X_AVM-DE_UpdateSuccessful": "unknown", + "X_AVM-DE_InfoURL": None, + "X_AVM-DE_MACAddressList": None, + "X_AVM-DE_Model": None, + "X_AVM-DE_URL": f"http://{MOCK_IPS['server']}", + "X_AVM-DE_Guest": False, + "X_AVM-DE_RequestClient": "0", + "X_AVM-DE_VPN": False, + "X_AVM-DE_WANAccess": "granted", + "X_AVM-DE_Disallow": False, + "X_AVM-DE_IsMeshable": "0", + "X_AVM-DE_Priority": "0", + "X_AVM-DE_FriendlyName": "server", + "X_AVM-DE_FriendlyNameIsWriteable": "1", + }, ] MOCK_USER_DATA = MOCK_CONFIG[DOMAIN][CONF_DEVICES][0] diff --git a/tests/components/fritz/test_button.py b/tests/components/fritz/test_button.py index 106fb7f9bef..f6546296d44 100644 --- a/tests/components/fritz/test_button.py +++ b/tests/components/fritz/test_button.py @@ -1,18 +1,21 @@ """Tests for Fritz!Tools button platform.""" +import copy +from datetime import timedelta from unittest.mock import patch import pytest from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS -from homeassistant.components.fritz.const import DOMAIN +from homeassistant.components.fritz.const import DOMAIN, MeshRoles from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN from homeassistant.core import HomeAssistant +from homeassistant.util.dt import utcnow -from .const import MOCK_USER_DATA +from .const import MOCK_MESH_DATA, MOCK_NEW_DEVICE_NODE, MOCK_USER_DATA -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed async def test_button_setup(hass: HomeAssistant, fc_class_mock, fh_class_mock) -> None: @@ -73,3 +76,113 @@ async def test_buttons( button = hass.states.get(entity_id) assert button.state != STATE_UNKNOWN + + +async def test_wol_button( + hass: HomeAssistant, + entity_registry_enabled_by_default: None, + fc_class_mock, + fh_class_mock, +) -> None: + """Test Fritz!Tools wake on LAN button.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.LOADED + + button = hass.states.get("button.printer_wake_on_lan") + assert button + assert button.state == STATE_UNKNOWN + with patch( + "homeassistant.components.fritz.common.AvmWrapper.async_wake_on_lan" + ) as mock_press_action: + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.printer_wake_on_lan"}, + blocking=True, + ) + await hass.async_block_till_done() + mock_press_action.assert_called_once_with("AA:BB:CC:00:11:22") + + button = hass.states.get("button.printer_wake_on_lan") + assert button.state != STATE_UNKNOWN + + +async def test_wol_button_new_device( + hass: HomeAssistant, + entity_registry_enabled_by_default: None, + fc_class_mock, + fh_class_mock, +) -> None: + """Test WoL button is created for new device at runtime.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + + mesh_data = copy.deepcopy(MOCK_MESH_DATA) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.LOADED + + assert hass.states.get("button.printer_wake_on_lan") + assert not hass.states.get("button.server_wake_on_lan") + + mesh_data["nodes"].append(MOCK_NEW_DEVICE_NODE) + fh_class_mock.get_mesh_topology.return_value = mesh_data + + async_fire_time_changed(hass, utcnow() + timedelta(seconds=60)) + await hass.async_block_till_done(wait_background_tasks=True) + + assert hass.states.get("button.printer_wake_on_lan") + assert hass.states.get("button.server_wake_on_lan") + + +async def test_wol_button_absent_for_mesh_slave( + hass: HomeAssistant, + entity_registry_enabled_by_default: None, + fc_class_mock, + fh_class_mock, +) -> None: + """Test WoL button not created if interviewed box is in slave mode.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + + slave_mesh_data = copy.deepcopy(MOCK_MESH_DATA) + slave_mesh_data["nodes"][0]["mesh_role"] = MeshRoles.SLAVE + fh_class_mock.get_mesh_topology.return_value = slave_mesh_data + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.LOADED + + button = hass.states.get("button.printer_wake_on_lan") + assert button is None + + +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: + """Test WoL button not created if interviewed device is not connected via LAN.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + + printer_wifi_data = copy.deepcopy(MOCK_MESH_DATA) + # initialization logic uses the connection type of the `node_interface_1_uid` pair of the printer + # ni-230 is wifi interface of fritzbox + printer_node_interface = printer_wifi_data["nodes"][1]["node_interfaces"][0] + printer_node_interface["type"] = "WLAN" + printer_node_interface["node_links"][0]["node_interface_1_uid"] = "ni-230" + fh_class_mock.get_mesh_topology.return_value = printer_wifi_data + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.LOADED + + button = hass.states.get("button.printer_wake_on_lan") + assert button is None diff --git a/tests/components/fritz/test_switch.py b/tests/components/fritz/test_switch.py index 722f16fa0de..91d2d42106b 100644 --- a/tests/components/fritz/test_switch.py +++ b/tests/components/fritz/test_switch.py @@ -172,7 +172,7 @@ async def test_switch_setup( expected_wifi_names: list[str], fc_class_mock, fh_class_mock, -): +) -> None: """Test setup of Fritz!Tools switches.""" entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) diff --git a/tests/components/fritz/test_update.py b/tests/components/fritz/test_update.py index 97c9cdec25d..991b67e6285 100644 --- a/tests/components/fritz/test_update.py +++ b/tests/components/fritz/test_update.py @@ -124,4 +124,4 @@ async def test_available_update_can_be_installed( {"entity_id": "update.mock_title_fritz_os"}, blocking=True, ) - assert mocked_update_call.assert_called_once + mocked_update_call.assert_called_once() From d66145358a4dae1c225e57f66531d372da15e394 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 3 Apr 2024 18:40:59 +0200 Subject: [PATCH 230/967] Correct imap services setup (#114760) * Correct imap services setup * Add config schema --- homeassistant/components/imap/__init__.py | 73 ++++++++++++----------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/imap/__init__.py b/homeassistant/components/imap/__init__.py index 6c90889a7d6..22e32187255 100644 --- a/homeassistant/components/imap/__init__.py +++ b/homeassistant/components/imap/__init__.py @@ -19,6 +19,7 @@ from homeassistant.exceptions import ( ServiceValidationError, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType from .const import CONF_ENABLE_PUSH, DOMAIN from .coordinator import ( @@ -37,6 +38,9 @@ CONF_TARGET_FOLDER = "target_folder" _LOGGER = logging.getLogger(__name__) + +CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) + _SERVICE_UID_SCHEMA = vol.Schema( { vol.Required(CONF_ENTRY): cv.string, @@ -95,30 +99,8 @@ def raise_on_error(response: Response, translation_key: str) -> None: ) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up imap from a config entry.""" - try: - imap_client: IMAP4_SSL = await connect_to_server(dict(entry.data)) - except InvalidAuth as err: - raise ConfigEntryAuthFailed from err - except InvalidFolder as err: - raise ConfigEntryError("Selected mailbox folder is invalid.") from err - except (TimeoutError, AioImapException) as err: - raise ConfigEntryNotReady from err - - coordinator_class: type[ - ImapPushDataUpdateCoordinator | ImapPollingDataUpdateCoordinator - ] - enable_push: bool = entry.data.get(CONF_ENABLE_PUSH, True) - if enable_push and imap_client.has_capability("IDLE"): - coordinator_class = ImapPushDataUpdateCoordinator - else: - coordinator_class = ImapPollingDataUpdateCoordinator - - coordinator: ImapPushDataUpdateCoordinator | ImapPollingDataUpdateCoordinator = ( - coordinator_class(hass, imap_client, entry) - ) - await coordinator.async_config_entry_first_refresh() +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up imap services.""" async def async_seen(call: ServiceCall) -> None: """Process mark as seen service call.""" @@ -141,8 +123,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise_on_error(response, "seen_failed") await client.close() - if not hass.services.has_service(DOMAIN, "seen"): - hass.services.async_register(DOMAIN, "seen", async_seen, SERVICE_SEEN_SCHEMA) + hass.services.async_register(DOMAIN, "seen", async_seen, SERVICE_SEEN_SCHEMA) async def async_move(call: ServiceCall) -> None: """Process move email service call.""" @@ -178,8 +159,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) from exc await client.close() - if not hass.services.has_service(DOMAIN, "move"): - hass.services.async_register(DOMAIN, "move", async_move, SERVICE_MOVE_SCHEMA) + hass.services.async_register(DOMAIN, "move", async_move, SERVICE_MOVE_SCHEMA) async def async_delete(call: ServiceCall) -> None: """Process deleting email service call.""" @@ -206,10 +186,35 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) from exc await client.close() - if not hass.services.has_service(DOMAIN, "delete"): - hass.services.async_register( - DOMAIN, "delete", async_delete, SERVICE_DELETE_SCHEMA - ) + hass.services.async_register(DOMAIN, "delete", async_delete, SERVICE_DELETE_SCHEMA) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up imap from a config entry.""" + try: + imap_client: IMAP4_SSL = await connect_to_server(dict(entry.data)) + except InvalidAuth as err: + raise ConfigEntryAuthFailed from err + except InvalidFolder as err: + raise ConfigEntryError("Selected mailbox folder is invalid.") from err + except (TimeoutError, AioImapException) as err: + raise ConfigEntryNotReady from err + + coordinator_class: type[ + ImapPushDataUpdateCoordinator | ImapPollingDataUpdateCoordinator + ] + enable_push: bool = entry.data.get(CONF_ENABLE_PUSH, True) + if enable_push and imap_client.has_capability("IDLE"): + coordinator_class = ImapPushDataUpdateCoordinator + else: + coordinator_class = ImapPollingDataUpdateCoordinator + + coordinator: ImapPushDataUpdateCoordinator | ImapPollingDataUpdateCoordinator = ( + coordinator_class(hass, imap_client, entry) + ) + await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator @@ -229,8 +234,4 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ImapPushDataUpdateCoordinator | ImapPollingDataUpdateCoordinator ) = hass.data[DOMAIN].pop(entry.entry_id) await coordinator.shutdown() - if not hass.data[DOMAIN]: - hass.services.async_remove(DOMAIN, "seen") - hass.services.async_remove(DOMAIN, "move") - hass.services.async_remove(DOMAIN, "delete") return unload_ok From 69a6e9f5d7c842371e5300084cc970c93c106557 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Apr 2024 08:29:48 -1000 Subject: [PATCH 231/967] Use eager_start to forward wemo config entry platforms (#114702) * Use eager_start to forward wemo config entry platforms These can all be setup synchronously * do not create another task --- homeassistant/components/wemo/__init__.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index 8a9a122c03c..4eb33a09553 100644 --- a/homeassistant/components/wemo/__init__.py +++ b/homeassistant/components/wemo/__init__.py @@ -208,16 +208,14 @@ class WemoDispatcher: self._dispatch_backlog[platform] = [coordinator] platforms_to_load.append(platform) - if platforms_to_load: - hass.async_create_task( - hass.config_entries.async_forward_entry_setups( - self._config_entry, platforms_to_load - ) - ) - self._added_serial_numbers.add(wemo.serial_number) self._failed_serial_numbers.discard(wemo.serial_number) + if platforms_to_load: + await hass.config_entries.async_forward_entry_setups( + self._config_entry, platforms_to_load + ) + async def async_connect_platform( self, platform: Platform, dispatch: DispatchCallback ) -> None: From 535da483b6c54216f74f39edc18ce6a839898c0a Mon Sep 17 00:00:00 2001 From: Fexiven <48439988+Fexiven@users.noreply.github.com> Date: Wed, 3 Apr 2024 20:51:55 +0200 Subject: [PATCH 232/967] Rework update_data starlink coordinator (#114642) * Update coordinator.py fixes https://github.com/home-assistant/core/issues/114353 * Rework update_data starlink coordinator * modify return handlung according to ruff * add type annotation for _get_srtarlink_data * add channel_context type annotation * Add docstring * ruff ruff here you go - modfied docstring * Clean up --------- Co-authored-by: Martin Hjelmare --- .../components/starlink/coordinator.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/starlink/coordinator.py b/homeassistant/components/starlink/coordinator.py index ff33b3ecc41..7a09b2f2dee 100644 --- a/homeassistant/components/starlink/coordinator.py +++ b/homeassistant/components/starlink/coordinator.py @@ -55,21 +55,21 @@ class StarlinkUpdateCoordinator(DataUpdateCoordinator[StarlinkData]): update_interval=timedelta(seconds=5), ) + def _get_starlink_data(self) -> StarlinkData: + """Retrieve Starlink data.""" + channel_context = self.channel_context + status = status_data(channel_context) + location = location_data(channel_context) + sleep = get_sleep_config(channel_context) + return StarlinkData(location, sleep, *status) + async def _async_update_data(self) -> StarlinkData: async with asyncio.timeout(4): try: - status = await self.hass.async_add_executor_job( - status_data, self.channel_context - ) - location = await self.hass.async_add_executor_job( - location_data, self.channel_context - ) - sleep = await self.hass.async_add_executor_job( - get_sleep_config, self.channel_context - ) - return StarlinkData(location, sleep, *status) + result = await self.hass.async_add_executor_job(self._get_starlink_data) except GrpcError as exc: raise UpdateFailed from exc + return result async def async_stow_starlink(self, stow: bool) -> None: """Set whether Starlink system tied to this coordinator should be stowed.""" From 3d8a1109085d41d959a4e5d756b002ab2a1c63fe Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Apr 2024 09:36:57 -1000 Subject: [PATCH 233/967] Dispatch the same ReceiveMessage object if the subscription topic is the same (#114769) --- homeassistant/components/mqtt/client.py | 20 ++++++++++++++------ homeassistant/components/mqtt/models.py | 2 +- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index b2fab355c41..83830de4963 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -835,6 +835,7 @@ class MQTT: timestamp = dt_util.utcnow() subscriptions = self._matching_subscriptions(topic) + msg_cache_by_subscription_topic: dict[str, ReceiveMessage] = {} for subscription in subscriptions: if msg.retain: @@ -858,17 +859,24 @@ class MQTT: subscription.job, ) continue - self.hass.async_run_hass_job( - subscription.job, - ReceiveMessage( + subscription_topic = subscription.topic + if subscription_topic not in msg_cache_by_subscription_topic: + # Only make one copy of the message + # per topic so we avoid storing a separate + # dataclass in memory for each subscriber + # to the same topic for retained messages + receive_msg = ReceiveMessage( topic, payload, msg.qos, msg.retain, - subscription.topic, + subscription_topic, timestamp, - ), - ) + ) + msg_cache_by_subscription_topic[subscription_topic] = receive_msg + else: + receive_msg = msg_cache_by_subscription_topic[subscription_topic] + self.hass.async_run_hass_job(subscription.job, receive_msg) self._mqtt_data.state_write_requests.process_write_state_requests(msg) def _mqtt_on_callback( diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index 6e6ae784eec..f53643268e7 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -58,7 +58,7 @@ class PublishMessage: retain: bool -@dataclass +@dataclass(slots=True, frozen=True) class ReceiveMessage: """MQTT Message received.""" From e86fec310b3fcff15d7a27de84ef2ee761c2b998 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Apr 2024 10:24:44 -1000 Subject: [PATCH 234/967] Improve performance of extracting entities by label (#114720) --- homeassistant/helpers/entity_registry.py | 21 +++++++++-- homeassistant/helpers/service.py | 39 ++++++++++---------- tests/components/device_tracker/test_init.py | 13 +++++-- 3 files changed, 46 insertions(+), 27 deletions(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index e19c4290a1d..27e73320841 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -512,11 +512,13 @@ class EntityRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]): class EntityRegistryItems(BaseRegistryItems[RegistryEntry]): """Container for entity registry items, maps entity_id -> entry. - Maintains four additional indexes: + Maintains six additional indexes: - id -> entry - (domain, platform, unique_id) -> entity_id - - config_entry_id -> list[key] - - device_id -> list[key] + - config_entry_id -> dict[key, True] + - device_id -> dict[key, True] + - area_id -> dict[key, True] + - label -> dict[key, True] """ def __init__(self) -> None: @@ -527,6 +529,7 @@ class EntityRegistryItems(BaseRegistryItems[RegistryEntry]): self._config_entry_id_index: dict[str, dict[str, Literal[True]]] = {} self._device_id_index: dict[str, dict[str, Literal[True]]] = {} self._area_id_index: dict[str, dict[str, Literal[True]]] = {} + self._labels_index: dict[str, dict[str, Literal[True]]] = {} def _index_entry(self, key: str, entry: RegistryEntry) -> None: """Index an entry.""" @@ -540,6 +543,8 @@ class EntityRegistryItems(BaseRegistryItems[RegistryEntry]): self._device_id_index.setdefault(device_id, {})[key] = True if (area_id := entry.area_id) is not None: self._area_id_index.setdefault(area_id, {})[key] = True + for label in entry.labels: + self._labels_index.setdefault(label, {})[key] = True def _unindex_entry( self, key: str, replacement_entry: RegistryEntry | None = None @@ -554,6 +559,9 @@ class EntityRegistryItems(BaseRegistryItems[RegistryEntry]): self._unindex_entry_value(key, device_id, self._device_id_index) if area_id := entry.area_id: self._unindex_entry_value(key, area_id, self._area_id_index) + if labels := entry.labels: + for label in labels: + self._unindex_entry_value(key, label, self._labels_index) def get_device_ids(self) -> KeysView[str]: """Return device ids.""" @@ -592,6 +600,11 @@ class EntityRegistryItems(BaseRegistryItems[RegistryEntry]): data = self.data return [data[key] for key in self._area_id_index.get(area_id, ())] + def get_entries_for_label(self, label: str) -> list[RegistryEntry]: + """Get entries for label.""" + data = self.data + return [data[key] for key in self._labels_index.get(label, ())] + class EntityRegistry(BaseRegistry): """Class to hold a registry of entities.""" @@ -1317,7 +1330,7 @@ def async_entries_for_label( registry: EntityRegistry, label_id: str ) -> list[RegistryEntry]: """Return entries that match a label.""" - return [entry for entry in registry.entities.values() if label_id in entry.labels] + return registry.entities.get_entries_for_label(label_id) @callback diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index da27df9d139..43942458233 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -503,15 +503,15 @@ def async_extract_referenced_entity_ids( # noqa: C901 ): return selected - ent_reg = entity_registry.async_get(hass) + entities = entity_registry.async_get(hass).entities dev_reg = device_registry.async_get(hass) area_reg = area_registry.async_get(hass) - floor_reg = floor_registry.async_get(hass) - label_reg = label_registry.async_get(hass) - for floor_id in selector.floor_ids: - if floor_id not in floor_reg.floors: - selected.missing_floors.add(floor_id) + if selector.floor_ids: + floor_reg = floor_registry.async_get(hass) + for floor_id in selector.floor_ids: + if floor_id not in floor_reg.floors: + selected.missing_floors.add(floor_id) for area_id in selector.area_ids: if area_id not in area_reg.areas: @@ -521,12 +521,20 @@ def async_extract_referenced_entity_ids( # noqa: C901 if device_id not in dev_reg.devices: selected.missing_devices.add(device_id) - for label_id in selector.label_ids: - if label_id not in label_reg.labels: - selected.missing_labels.add(label_id) - - # Find areas, devices & entities for targeted labels if selector.label_ids: + label_reg = label_registry.async_get(hass) + for label_id in selector.label_ids: + if label_id not in label_reg.labels: + selected.missing_labels.add(label_id) + + for entity_entry in entities.get_entries_for_label(label_id): + if ( + entity_entry.entity_category is None + and entity_entry.hidden_by is None + ): + selected.indirectly_referenced.add(entity_entry.entity_id) + + # Find areas, devices & entities for targeted labels for area_entry in area_reg.areas.values(): if area_entry.labels.intersection(selector.label_ids): selected.referenced_areas.add(area_entry.id) @@ -535,14 +543,6 @@ def async_extract_referenced_entity_ids( # noqa: C901 if device_entry.labels.intersection(selector.label_ids): selected.referenced_devices.add(device_entry.id) - for entity_entry in ent_reg.entities.values(): - if ( - entity_entry.entity_category is None - and entity_entry.hidden_by is None - and entity_entry.labels.intersection(selector.label_ids) - ): - selected.indirectly_referenced.add(entity_entry.entity_id) - # Find areas for targeted floors if selector.floor_ids: for area_entry in area_reg.areas.values(): @@ -561,7 +561,6 @@ def async_extract_referenced_entity_ids( # noqa: C901 if not selected.referenced_areas and not selected.referenced_devices: return selected - entities = ent_reg.entities # Add indirectly referenced by area selected.indirectly_referenced.update( entry.entity_id diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index cc6cf5c1c1e..6999a99f7ba 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -5,7 +5,7 @@ import json import logging import os from types import ModuleType -from unittest.mock import Mock, call, patch +from unittest.mock import call, patch import pytest @@ -25,6 +25,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, State, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import discovery +from homeassistant.helpers.entity_registry import RegistryEntry from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -396,10 +397,16 @@ async def test_see_service_guard_config_entry( mock_device_tracker_conf: list[legacy.Device], ) -> None: """Test the guard if the device is registered in the entity registry.""" - mock_entry = Mock() dev_id = "test" entity_id = f"{const.DOMAIN}.{dev_id}" - mock_registry(hass, {entity_id: mock_entry}) + mock_registry( + hass, + { + entity_id: RegistryEntry( + entity_id=entity_id, unique_id=1, platform=const.DOMAIN + ) + }, + ) devices = mock_device_tracker_conf assert await async_setup_component(hass, device_tracker.DOMAIN, TEST_PLATFORM) await hass.async_block_till_done() From 5394a2a34a33d3c5eea92c1b3954c3a3c39216fe Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Apr 2024 11:12:38 -1000 Subject: [PATCH 235/967] Load mailbox integration platforms in tracked tasks (#114774) --- homeassistant/components/mailbox/__init__.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/mailbox/__init__.py b/homeassistant/components/mailbox/__init__.py index b000f1eadcb..337a58e3b2f 100644 --- a/homeassistant/components/mailbox/__init__.py +++ b/homeassistant/components/mailbox/__init__.py @@ -110,14 +110,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: component.register_shutdown() await component.async_add_entities([mailbox_entity]) - setup_tasks = [ - asyncio.create_task(async_setup_platform(p_type, p_config)) - for p_type, p_config in config_per_platform(config, DOMAIN) - if p_type is not None - ] - - if setup_tasks: - await asyncio.wait(setup_tasks) + for p_type, p_config in config_per_platform(config, DOMAIN): + if p_type is not None: + hass.async_create_task( + async_setup_platform(p_type, p_config), eager_start=True + ) async def async_platform_discovered( platform: str, info: DiscoveryInfoType | None From c1c664dc09216bf32f427d3775206a99928064cb Mon Sep 17 00:00:00 2001 From: Manuel Dipolt Date: Thu, 4 Apr 2024 00:48:35 +0200 Subject: [PATCH 236/967] Update romy to 0.0.10 (#114785) --- homeassistant/components/romy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/romy/manifest.json b/homeassistant/components/romy/manifest.json index 7e30c418599..efb8072ebbc 100644 --- a/homeassistant/components/romy/manifest.json +++ b/homeassistant/components/romy/manifest.json @@ -5,6 +5,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/romy", "iot_class": "local_polling", - "requirements": ["romy==0.0.9"], + "requirements": ["romy==0.0.10"], "zeroconf": ["_aicu-http._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 8507c5afb55..40e7140b55a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2466,7 +2466,7 @@ rocketchat-API==0.6.1 rokuecp==0.19.2 # homeassistant.components.romy -romy==0.0.9 +romy==0.0.10 # homeassistant.components.roomba roombapy==1.8.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d9679f1524b..2a46a97d1ed 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1900,7 +1900,7 @@ ring-doorbell[listen]==0.8.9 rokuecp==0.19.2 # homeassistant.components.romy -romy==0.0.9 +romy==0.0.10 # homeassistant.components.roomba roombapy==1.8.1 From 841d3940d1a8edf4ad0fcf6d605fde313ccb874e Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 3 Apr 2024 18:20:20 -0600 Subject: [PATCH 237/967] Fix unhandled `KeyError` during Notion setup (#114787) --- homeassistant/components/notion/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index ca45e3a6d16..1793a0cfd47 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -108,7 +108,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: (CONF_REFRESH_TOKEN, client.refresh_token), (CONF_USER_UUID, client.user_uuid), ): - if entry.data[key] == value: + if entry.data.get(key) == value: continue entry_updates["data"][key] = value From 3f76d1f0567e7b545d0ca05a03c3fbb660821f97 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Apr 2024 16:52:17 -1000 Subject: [PATCH 238/967] Add index for area/config_entry/label to the device registry (#114776) * Add index for area/config_entry/label to the device registry * use it for services * naming * naming * tweak --- homeassistant/helpers/device_registry.py | 75 +++++++++++++++++++++--- homeassistant/helpers/service.py | 17 +++--- tests/common.py | 2 +- 3 files changed, 76 insertions(+), 18 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 76daa1266dd..c9a9016560c 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -491,10 +491,71 @@ class DeviceRegistryItems(BaseRegistryItems[_EntryTypeT]): return None +class ActiveDeviceRegistryItems(DeviceRegistryItems[DeviceEntry]): + """Container for active (non-deleted) device registry entries.""" + + def __init__(self) -> None: + """Initialize the container. + + Maintains three additional indexes: + + - area_id -> dict[key, True] + - config_entry_id -> dict[key, True] + - label -> dict[key, True] + """ + super().__init__() + self._area_id_index: dict[str, dict[str, Literal[True]]] = {} + self._config_entry_id_index: dict[str, dict[str, Literal[True]]] = {} + self._labels_index: dict[str, dict[str, Literal[True]]] = {} + + def _index_entry(self, key: str, entry: DeviceEntry) -> None: + """Index an entry.""" + super()._index_entry(key, entry) + if (area_id := entry.area_id) is not None: + self._area_id_index.setdefault(area_id, {})[key] = True + for label in entry.labels: + self._labels_index.setdefault(label, {})[key] = True + for config_entry_id in entry.config_entries: + self._config_entry_id_index.setdefault(config_entry_id, {})[key] = True + + def _unindex_entry( + self, key: str, replacement_entry: DeviceEntry | None = None + ) -> None: + """Unindex an entry.""" + entry = self.data[key] + if area_id := entry.area_id: + self._unindex_entry_value(key, area_id, self._area_id_index) + if labels := entry.labels: + for label in labels: + self._unindex_entry_value(key, label, self._labels_index) + for config_entry_id in entry.config_entries: + self._unindex_entry_value(key, config_entry_id, self._config_entry_id_index) + super()._unindex_entry(key, replacement_entry) + + def get_devices_for_area_id(self, area_id: str) -> list[DeviceEntry]: + """Get devices for area.""" + data = self.data + return [data[key] for key in self._area_id_index.get(area_id, ())] + + def get_devices_for_label(self, label: str) -> list[DeviceEntry]: + """Get devices for label.""" + data = self.data + return [data[key] for key in self._labels_index.get(label, ())] + + def get_devices_for_config_entry_id( + self, config_entry_id: str + ) -> list[DeviceEntry]: + """Get devices for config entry.""" + data = self.data + return [ + data[key] for key in self._config_entry_id_index.get(config_entry_id, ()) + ] + + class DeviceRegistry(BaseRegistry): """Class to hold a registry of devices.""" - devices: DeviceRegistryItems[DeviceEntry] + devices: ActiveDeviceRegistryItems deleted_devices: DeviceRegistryItems[DeletedDeviceEntry] _device_data: dict[str, DeviceEntry] @@ -884,7 +945,7 @@ class DeviceRegistry(BaseRegistry): data = await self._store.async_load() - devices: DeviceRegistryItems[DeviceEntry] = DeviceRegistryItems() + devices = ActiveDeviceRegistryItems() deleted_devices: DeviceRegistryItems[DeletedDeviceEntry] = DeviceRegistryItems() if data is not None: @@ -1018,7 +1079,7 @@ async def async_load(hass: HomeAssistant) -> None: @callback def async_entries_for_area(registry: DeviceRegistry, area_id: str) -> list[DeviceEntry]: """Return entries that match an area.""" - return [device for device in registry.devices.values() if device.area_id == area_id] + return registry.devices.get_devices_for_area_id(area_id) @callback @@ -1026,7 +1087,7 @@ def async_entries_for_label( registry: DeviceRegistry, label_id: str ) -> list[DeviceEntry]: """Return entries that match a label.""" - return [device for device in registry.devices.values() if label_id in device.labels] + return registry.devices.get_devices_for_label(label_id) @callback @@ -1034,11 +1095,7 @@ def async_entries_for_config_entry( registry: DeviceRegistry, config_entry_id: str ) -> list[DeviceEntry]: """Return entries that match a config entry.""" - return [ - device - for device in registry.devices.values() - if config_entry_id in device.config_entries - ] + return registry.devices.get_devices_for_config_entry_id(config_entry_id) @callback diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 43942458233..00dfea23549 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -534,15 +534,14 @@ def async_extract_referenced_entity_ids( # noqa: C901 ): selected.indirectly_referenced.add(entity_entry.entity_id) - # Find areas, devices & entities for targeted labels + for device_entry in dev_reg.devices.get_devices_for_label(label_id): + selected.referenced_devices.add(device_entry.id) + + # Find areas for targeted labels for area_entry in area_reg.areas.values(): if area_entry.labels.intersection(selector.label_ids): selected.referenced_areas.add(area_entry.id) - for device_entry in dev_reg.devices.values(): - if device_entry.labels.intersection(selector.label_ids): - selected.referenced_devices.add(device_entry.id) - # Find areas for targeted floors if selector.floor_ids: for area_entry in area_reg.areas.values(): @@ -554,9 +553,11 @@ def async_extract_referenced_entity_ids( # noqa: C901 selected.referenced_areas.update(selector.area_ids) if selected.referenced_areas: - for device_entry in dev_reg.devices.values(): - if device_entry.area_id in selected.referenced_areas: - selected.referenced_devices.add(device_entry.id) + for area_id in selected.referenced_areas: + selected.referenced_devices.update( + device_entry.id + for device_entry in dev_reg.devices.get_devices_for_area_id(area_id) + ) if not selected.referenced_areas and not selected.referenced_devices: return selected diff --git a/tests/common.py b/tests/common.py index 210eb07d668..d3bcdcbd004 100644 --- a/tests/common.py +++ b/tests/common.py @@ -671,7 +671,7 @@ def mock_device_registry( fixture instead. """ registry = dr.DeviceRegistry(hass) - registry.devices = dr.DeviceRegistryItems() + registry.devices = dr.ActiveDeviceRegistryItems() registry._device_data = registry.devices.data if mock_entries is None: mock_entries = {} From 56d0ad27f0dc67a66eb0568a78453b0e42f35b1f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Apr 2024 16:53:31 -1000 Subject: [PATCH 239/967] Adjust async_load_platform comment to remove dead lock reference (#114771) * Adjust async_load_platform comment Its likely the deadlock here has been fixed for a long time, however we should still do these in a task because it has to wait for base components if they are not loaded yet. * Adjust async_load_platform comment Its likely the deadlock here has been fixed for a long time, however we should still do these in a task because it has to wait for base components if they are not loaded yet. --- homeassistant/helpers/discovery.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/discovery.py b/homeassistant/helpers/discovery.py index 4b5a0117be7..2e14759b814 100644 --- a/homeassistant/helpers/discovery.py +++ b/homeassistant/helpers/discovery.py @@ -152,8 +152,11 @@ async def async_load_platform( Use `async_listen_platform` to register a callback for these events. - Warning: Do not await this inside a setup method to avoid a dead lock. - Use `hass.async_create_task(async_load_platform(..))` instead. + Warning: This method can load a base component if its not loaded which + can take a long time since base components currently have to import + every platform integration listed under it to do config validation. + To avoid waiting for this, use + `hass.async_create_task(async_load_platform(..))` instead. """ assert hass_config is not None, "You need to pass in the real hass config" From 7a2e529bb72a8a89c920fd0285bedb96538be6ba Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Apr 2024 19:51:18 -1000 Subject: [PATCH 240/967] Avoid executor job to start http if server_host is unspecified (#114609) * Avoid executor job to start http if server_host is unspecified Same as #112522 for http * adjust test * CONF_SERVER_HOST is always set now --- homeassistant/components/http/__init__.py | 8 ++++++-- tests/scripts/test_check_config.py | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index c9f8c21e0a3..e89031cb265 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -7,6 +7,7 @@ import datetime from ipaddress import IPv4Network, IPv6Network, ip_network import logging import os +import socket import ssl from tempfile import NamedTemporaryFile from typing import Any, Final, TypedDict, cast @@ -98,11 +99,14 @@ STORAGE_KEY: Final = DOMAIN STORAGE_VERSION: Final = 1 SAVE_DELAY: Final = 180 +_HAS_IPV6 = hasattr(socket, "AF_INET6") +_DEFAULT_BIND = ["0.0.0.0", "::"] if _HAS_IPV6 else ["0.0.0.0"] + HTTP_SCHEMA: Final = vol.All( cv.deprecated(CONF_BASE_URL), vol.Schema( { - vol.Optional(CONF_SERVER_HOST): vol.All( + vol.Optional(CONF_SERVER_HOST, default=_DEFAULT_BIND): vol.All( cv.ensure_list, vol.Length(min=1), [cv.string] ), vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT): cv.port, @@ -183,7 +187,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if conf is None: conf = cast(ConfData, HTTP_SCHEMA({})) - server_host = conf.get(CONF_SERVER_HOST) + server_host = conf[CONF_SERVER_HOST] server_port = conf[CONF_SERVER_PORT] ssl_certificate = conf.get(CONF_SSL_CERTIFICATE) ssl_peer_certificate = conf.get(CONF_SSL_PEER_CERTIFICATE) diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index 96d63206cfc..79c64259f8b 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -135,6 +135,7 @@ def test_secrets(mock_is_file, event_loop, mock_hass_config_yaml: None) -> None: "server_port": 8123, "ssl_profile": "modern", "use_x_frame_options": True, + "server_host": ["0.0.0.0", "::"], } assert res["secret_cache"] == { get_test_config_dir("secrets.yaml"): {"http_pw": "http://google.com"} From 0c3ccabfb1bbbc2f3492bc3621c8f1fb30b464ef Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Apr 2024 20:35:44 -1000 Subject: [PATCH 241/967] Speed up logger setup (#114610) * Speed up logger setup Preload core.logger and avoid saving it until after startup * add comment about 180s * Adjust grammar --------- Co-authored-by: Martin Hjelmare --- homeassistant/bootstrap.py | 1 + homeassistant/components/logger/helpers.py | 16 +++++++++++++--- tests/components/logger/test_init.py | 15 ++++++++++++++- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 97bdd615d69..373c5c0f38c 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -223,6 +223,7 @@ SETUP_ORDER = ( # If they do not exist they will not be loaded # PRELOAD_STORAGE = [ + "core.logger", "core.network", "http.auth", "image", diff --git a/homeassistant/components/logger/helpers.py b/homeassistant/components/logger/helpers.py index a527a081fca..034266428a3 100644 --- a/homeassistant/components/logger/helpers.py +++ b/homeassistant/components/logger/helpers.py @@ -28,6 +28,16 @@ from .const import ( STORAGE_VERSION, ) +SAVE_DELAY = 15.0 +# At startup, we want to save after a long delay to avoid +# saving while the system is still starting up. If the system +# for some reason restarts quickly, it will still be written +# at the final write event. In most cases we expect startup +# to happen in less than 180 seconds, but if it takes longer +# it's likely delayed because of remote I/O and not local +# I/O so it's fine to save at that point. +SAVE_DELAY_LONG = 180.0 + @callback def async_get_domain_config(hass: HomeAssistant) -> LoggerDomainConfig: @@ -148,7 +158,7 @@ class LoggerSettings: for domain, settings in stored_log_config.items() } } - await self._store.async_save(self._async_data_to_save()) + self.async_save(SAVE_DELAY_LONG) @callback def _async_data_to_save(self) -> dict[str, dict[str, dict[str, str]]]: @@ -164,9 +174,9 @@ class LoggerSettings: } @callback - def async_save(self) -> None: + def async_save(self, delay: float = SAVE_DELAY) -> None: """Save settings.""" - self._store.async_delay_save(self._async_data_to_save, 15) + self._store.async_delay_save(self._async_data_to_save, delay) @callback def _async_get_logger_logs(self) -> dict[str, int]: diff --git a/tests/components/logger/test_init.py b/tests/components/logger/test_init.py index 3e30ea0ead0..d6df1f92a72 100644 --- a/tests/components/logger/test_init.py +++ b/tests/components/logger/test_init.py @@ -1,6 +1,7 @@ """The tests for the Logger component.""" from collections import defaultdict +import datetime import logging from typing import Any from unittest.mock import Mock, patch @@ -9,8 +10,12 @@ import pytest from homeassistant.components import logger from homeassistant.components.logger import LOGSEVERITY +from homeassistant.components.logger.helpers import SAVE_DELAY_LONG from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from tests.common import async_fire_time_changed HASS_NS = "unused.homeassistant" COMPONENTS_NS = f"{HASS_NS}.components" @@ -403,7 +408,7 @@ async def test_log_once_removed_from_store( hass: HomeAssistant, hass_storage: dict[str, Any] ) -> None: """Test logs with persistence "once" are removed from the store at startup.""" - hass_storage["core.logger"] = { + store_contents = { "data": { "logs": { ZONE_NS: {"type": "module", "level": "DEBUG", "persistence": "once"} @@ -412,7 +417,15 @@ async def test_log_once_removed_from_store( "key": "core.logger", "version": 1, } + hass_storage["core.logger"] = store_contents assert await async_setup_component(hass, "logger", {}) + assert hass_storage["core.logger"]["data"] == store_contents["data"] + + async_fire_time_changed( + hass, dt_util.utcnow() + datetime.timedelta(seconds=SAVE_DELAY_LONG) + ) + await hass.async_block_till_done() + assert hass_storage["core.logger"]["data"] == {"logs": {}} From c18ff39540cdc1c140a91b01e0ee61188b5fcfa7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Apr 2024 08:38:15 +0200 Subject: [PATCH 242/967] Bump Wandalen/wretry.action from 2.1.0 to 3.0.1 (#114805) Bumps [Wandalen/wretry.action](https://github.com/wandalen/wretry.action) from 2.1.0 to 3.0.1. - [Release notes](https://github.com/wandalen/wretry.action/releases) - [Commits](https://github.com/wandalen/wretry.action/compare/v2.1.0...v3.0.1) --- updated-dependencies: - dependency-name: Wandalen/wretry.action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] 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 ac7be91d1bd..71055e0ea72 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1088,7 +1088,7 @@ jobs: pattern: coverage-* - name: Upload coverage to Codecov (full coverage) if: needs.info.outputs.test_full_suite == 'true' - uses: Wandalen/wretry.action@v2.1.0 + uses: Wandalen/wretry.action@v3.0.1 with: action: codecov/codecov-action@v3.1.3 with: | @@ -1099,7 +1099,7 @@ jobs: attempt_delay: 30000 - name: Upload coverage to Codecov (partial coverage) if: needs.info.outputs.test_full_suite == 'false' - uses: Wandalen/wretry.action@v2.1.0 + uses: Wandalen/wretry.action@v3.0.1 with: action: codecov/codecov-action@v3.1.3 with: | From aa52688d4b83bb909fa339df23e83366a9c967ef Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Apr 2024 21:02:18 -1000 Subject: [PATCH 243/967] Avoid linear search of the device registry in deconz (#114803) --- homeassistant/components/deconz/services.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py index 91f36bb871e..233f9c3f570 100644 --- a/homeassistant/components/deconz/services.py +++ b/homeassistant/components/deconz/services.py @@ -160,8 +160,9 @@ async def async_remove_orphaned_entries_service(hub: DeconzHub) -> None: entities_to_be_removed = [] devices_to_be_removed = [ entry.id - for entry in device_registry.devices.values() - if hub.config_entry.entry_id in entry.config_entries + for entry in device_registry.devices.get_devices_for_config_entry_id( + hub.config_entry.entry_id + ) ] # Don't remove the Gateway host entry From aedfd6c983abe465b37343b83de30a8abd8cdcd1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Apr 2024 21:04:26 -1000 Subject: [PATCH 244/967] Add index for floor/label to the area registry (#114777) --- homeassistant/helpers/area_registry.py | 47 +++++++++++++++++-- homeassistant/helpers/service.py | 12 ++--- tests/common.py | 5 +- .../conversation/test_default_agent.py | 2 +- 4 files changed, 53 insertions(+), 13 deletions(-) diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py index fc535bed610..24f58c56d2f 100644 --- a/homeassistant/helpers/area_registry.py +++ b/homeassistant/helpers/area_registry.py @@ -87,10 +87,49 @@ class AreaRegistryStore(Store[dict[str, list[dict[str, Any]]]]): return old_data +class AreaRegistryItems(NormalizedNameBaseRegistryItems[AreaEntry]): + """Class to hold area registry items.""" + + def __init__(self) -> None: + """Initialize the area registry items.""" + super().__init__() + self._labels_index: dict[str, dict[str, Literal[True]]] = {} + self._floors_index: dict[str, dict[str, Literal[True]]] = {} + + def _index_entry(self, key: str, entry: AreaEntry) -> None: + """Index an entry.""" + if entry.floor_id is not None: + self._floors_index.setdefault(entry.floor_id, {})[key] = True + for label in entry.labels: + self._labels_index.setdefault(label, {})[key] = True + super()._index_entry(key, entry) + + def _unindex_entry( + self, key: str, replacement_entry: AreaEntry | None = None + ) -> None: + entry = self.data[key] + if labels := entry.labels: + for label in labels: + self._unindex_entry_value(key, label, self._labels_index) + if floor_id := entry.floor_id: + self._unindex_entry_value(key, floor_id, self._floors_index) + return super()._unindex_entry(key, replacement_entry) + + def get_areas_for_label(self, label: str) -> list[AreaEntry]: + """Get areas for label.""" + data = self.data + return [data[key] for key in self._labels_index.get(label, ())] + + def get_areas_for_floor(self, floor: str) -> list[AreaEntry]: + """Get areas for floor.""" + data = self.data + return [data[key] for key in self._floors_index.get(floor, ())] + + class AreaRegistry(BaseRegistry): """Class to hold a registry of areas.""" - areas: NormalizedNameBaseRegistryItems[AreaEntry] + areas: AreaRegistryItems _area_data: dict[str, AreaEntry] def __init__(self, hass: HomeAssistant) -> None: @@ -254,7 +293,7 @@ class AreaRegistry(BaseRegistry): data = await self._store.async_load() - areas = NormalizedNameBaseRegistryItems[AreaEntry]() + areas = AreaRegistryItems() if data is not None: for area in data["areas"]: @@ -369,10 +408,10 @@ async def async_load(hass: HomeAssistant) -> None: @callback def async_entries_for_floor(registry: AreaRegistry, floor_id: str) -> list[AreaEntry]: """Return entries that match a floor.""" - return [area for area in registry.areas.values() if floor_id == area.floor_id] + return registry.areas.get_areas_for_floor(floor_id) @callback def async_entries_for_label(registry: AreaRegistry, label_id: str) -> list[AreaEntry]: """Return entries that match a label.""" - return [area for area in registry.areas.values() if label_id in area.labels] + return registry.areas.get_areas_for_label(label_id) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 00dfea23549..9af02402bc0 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -537,16 +537,16 @@ def async_extract_referenced_entity_ids( # noqa: C901 for device_entry in dev_reg.devices.get_devices_for_label(label_id): selected.referenced_devices.add(device_entry.id) - # Find areas for targeted labels - for area_entry in area_reg.areas.values(): - if area_entry.labels.intersection(selector.label_ids): + for area_entry in area_reg.areas.get_areas_for_label(label_id): selected.referenced_areas.add(area_entry.id) # Find areas for targeted floors if selector.floor_ids: - for area_entry in area_reg.areas.values(): - if area_entry.id and area_entry.floor_id in selector.floor_ids: - selected.referenced_areas.add(area_entry.id) + selected.referenced_areas.update( + area_entry.id + for floor_id in selector.floor_ids + for area_entry in area_reg.areas.get_areas_for_floor(floor_id) + ) # Find devices for targeted areas selected.referenced_devices.update(selector.device_ids) diff --git a/tests/common.py b/tests/common.py index d3bcdcbd004..db96e36f7ec 100644 --- a/tests/common.py +++ b/tests/common.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio -from collections import OrderedDict from collections.abc import AsyncGenerator, Generator, Mapping, Sequence from contextlib import asynccontextmanager, contextmanager from datetime import UTC, datetime, timedelta @@ -649,7 +648,9 @@ def mock_area_registry( fixture instead. """ registry = ar.AreaRegistry(hass) - registry.areas = mock_entries or OrderedDict() + registry.areas = ar.AreaRegistryItems() + for key, entry in mock_entries.items(): + registry.areas[key] = entry hass.data[ar.DATA_REGISTRY] = registry return registry diff --git a/tests/components/conversation/test_default_agent.py b/tests/components/conversation/test_default_agent.py index 474198cb8a3..9048a1259c5 100644 --- a/tests/components/conversation/test_default_agent.py +++ b/tests/components/conversation/test_default_agent.py @@ -823,7 +823,7 @@ async def test_empty_aliases( area_kitchen = area_registry.async_get_or_create("kitchen_id") area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen") area_kitchen = area_registry.async_update( - area_kitchen.id, aliases={" "}, floor_id=floor_1 + area_kitchen.id, aliases={" "}, floor_id=floor_1.floor_id ) entry = MockConfigEntry() From 15a821f6ac0b19e0240b388a252a0026b1a92971 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Thu, 4 Apr 2024 09:05:08 +0200 Subject: [PATCH 245/967] Update aioairzone-cloud to v0.4.7 (#114761) --- homeassistant/components/airzone_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/airzone_cloud/manifest.json b/homeassistant/components/airzone_cloud/manifest.json index 14f02620c91..b4445f6fe45 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.4.6"] + "requirements": ["aioairzone-cloud==0.4.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index 40e7140b55a..2858a15fcfe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -185,7 +185,7 @@ aio-georss-gdacs==0.9 aioairq==0.3.2 # homeassistant.components.airzone_cloud -aioairzone-cloud==0.4.6 +aioairzone-cloud==0.4.7 # homeassistant.components.airzone aioairzone==0.7.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2a46a97d1ed..fdb15008e70 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -164,7 +164,7 @@ aio-georss-gdacs==0.9 aioairq==0.3.2 # homeassistant.components.airzone_cloud -aioairzone-cloud==0.4.6 +aioairzone-cloud==0.4.7 # homeassistant.components.airzone aioairzone==0.7.6 From 2b061685733296e3958ae1522269ebaa8ff818b0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Apr 2024 21:09:47 -1000 Subject: [PATCH 246/967] Avoid linear search in traccar to find devices (#114817) * Avoid linear search in traccar to find devices * remove useless check --- homeassistant/components/traccar/device_tracker.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/traccar/device_tracker.py b/homeassistant/components/traccar/device_tracker.py index d82b922f586..5695e434eff 100644 --- a/homeassistant/components/traccar/device_tracker.py +++ b/homeassistant/components/traccar/device_tracker.py @@ -152,9 +152,8 @@ async def async_setup_entry( dev_reg = dr.async_get(hass) dev_ids = { identifier[1] - for device in dev_reg.devices.values() + for device in dev_reg.devices.get_devices_for_config_entry_id(entry.entry_id) for identifier in device.identifiers - if identifier[0] == DOMAIN } if not dev_ids: return From 1462c99bc0751a98b50009b5b0532c7c22f009e5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Apr 2024 21:33:10 -1000 Subject: [PATCH 247/967] Load template platforms with eager_start (#114701) * Load template platforms with eager_start These can all be loaded synchronously * missed some --- homeassistant/components/template/__init__.py | 3 ++- homeassistant/components/template/coordinator.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/template/__init__.py b/homeassistant/components/template/__init__.py index 6d4d3a9367c..f881e61fb76 100644 --- a/homeassistant/components/template/__init__.py +++ b/homeassistant/components/template/__init__.py @@ -109,7 +109,8 @@ async def _process_config(hass: HomeAssistant, hass_config: ConfigType) -> None: "entities": conf_section[platform_domain], }, hass_config, - ) + ), + eager_start=True, ) if coordinator_tasks: diff --git a/homeassistant/components/template/coordinator.py b/homeassistant/components/template/coordinator.py index 3319afa01c2..47de31d07c2 100644 --- a/homeassistant/components/template/coordinator.py +++ b/homeassistant/components/template/coordinator.py @@ -59,7 +59,8 @@ class TriggerUpdateCoordinator(DataUpdateCoordinator): DOMAIN, {"coordinator": self, "entities": self.config[platform_domain]}, hass_config, - ) + ), + eager_start=True, ) async def _attach_triggers(self, start_event=None) -> None: From 7b64097399798c72ed342a17786248bbc067a04f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Apr 2024 21:34:13 -1000 Subject: [PATCH 248/967] Load mobile_app notify platform with eager_start (#114700) --- homeassistant/components/mobile_app/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index 20a5448f2be..4c40e4f22b3 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -82,7 +82,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) hass.async_create_task( - discovery.async_load_platform(hass, Platform.NOTIFY, DOMAIN, {}, config) + discovery.async_load_platform(hass, Platform.NOTIFY, DOMAIN, {}, config), + eager_start=True, ) websocket_api.async_setup_commands(hass) From 7228f63c4a0b47598a9a4d019c45e8417a0d9275 Mon Sep 17 00:00:00 2001 From: cdheiser <10488026+cdheiser@users.noreply.github.com> Date: Thu, 4 Apr 2024 02:24:02 -0700 Subject: [PATCH 249/967] Fix Lutron light brightness values (#114794) Fix brightness values in light.py Bugfix to set the brightness to 0-100 which is what Lutron expects. --- homeassistant/components/lutron/light.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/lutron/light.py b/homeassistant/components/lutron/light.py index 18b5edd1039..eb003fd431a 100644 --- a/homeassistant/components/lutron/light.py +++ b/homeassistant/components/lutron/light.py @@ -141,7 +141,7 @@ class LutronLight(LutronDevice, LightEntity): else: brightness = self._prev_brightness self._prev_brightness = brightness - args = {"new_level": brightness} + args = {"new_level": to_lutron_level(brightness)} if ATTR_TRANSITION in kwargs: args["fade_time_seconds"] = kwargs[ATTR_TRANSITION] self._lutron_device.set_level(**args) From 816ce116bf159d8e7d5fdf84ed1550aaa7fab3c0 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 4 Apr 2024 11:24:26 +0200 Subject: [PATCH 250/967] Remove unnecessary functools.cached_property backport (#114239) --- homeassistant/auth/models.py | 9 +- homeassistant/backports/functools.py | 85 +++---------------- .../alarm_control_panel/__init__.py | 9 +- .../components/automation/__init__.py | 10 +-- .../components/binary_sensor/__init__.py | 9 +- homeassistant/components/button/__init__.py | 8 +- homeassistant/components/camera/__init__.py | 9 +- homeassistant/components/climate/__init__.py | 8 +- homeassistant/components/cover/__init__.py | 8 +- homeassistant/components/date/__init__.py | 9 +- homeassistant/components/datetime/__init__.py | 8 +- .../components/device_tracker/legacy.py | 2 +- homeassistant/components/dlna_dms/dms.py | 2 +- homeassistant/components/event/__init__.py | 9 +- homeassistant/components/fan/__init__.py | 9 +- homeassistant/components/ffmpeg/__init__.py | 9 +- homeassistant/components/fints/sensor.py | 2 +- .../components/geo_location/__init__.py | 9 +- .../components/homekit_controller/climate.py | 9 +- .../components/homekit_controller/cover.py | 9 +- .../components/homekit_controller/fan.py | 9 +- .../homekit_controller/humidifier.py | 9 +- .../components/homekit_controller/light.py | 8 +- .../components/humidifier/__init__.py | 10 +-- homeassistant/components/image/__init__.py | 9 +- .../components/lawn_mower/__init__.py | 9 +- homeassistant/components/light/__init__.py | 8 +- homeassistant/components/lock/__init__.py | 6 +- homeassistant/components/logbook/models.py | 6 +- .../components/media_player/__init__.py | 9 +- .../components/nibe_heatpump/coordinator.py | 2 +- homeassistant/components/number/__init__.py | 6 +- .../components/recorder/models/state.py | 6 +- homeassistant/components/remote/__init__.py | 9 +- homeassistant/components/script/__init__.py | 9 +- homeassistant/components/select/__init__.py | 8 +- homeassistant/components/sensor/__init__.py | 9 +- homeassistant/components/siren/__init__.py | 9 +- homeassistant/components/switch/__init__.py | 8 +- .../components/template/template_entity.py | 8 +- homeassistant/components/text/__init__.py | 8 +- .../components/thread/dataset_store.py | 2 +- homeassistant/components/time/__init__.py | 9 +- homeassistant/components/todo/__init__.py | 9 +- homeassistant/components/update/__init__.py | 9 +- homeassistant/components/vacuum/__init__.py | 9 +- .../components/water_heater/__init__.py | 9 +- homeassistant/components/weather/__init__.py | 9 +- .../zha/core/cluster_handlers/lighting.py | 4 +- homeassistant/components/zha/core/device.py | 2 +- homeassistant/config_entries.py | 5 +- homeassistant/core.py | 5 +- homeassistant/helpers/device_registry.py | 3 +- homeassistant/helpers/entity.py | 5 +- homeassistant/helpers/entity_registry.py | 2 +- homeassistant/helpers/frame.py | 8 +- homeassistant/helpers/script.py | 10 +-- homeassistant/helpers/storage.py | 9 +- homeassistant/loader.py | 5 +- homeassistant/util/yaml/loader.py | 10 +-- pylint/plugins/hass_imports.py | 9 ++ tests/helpers/test_entity.py | 2 +- tests/test_config_entries.py | 2 +- 63 files changed, 123 insertions(+), 411 deletions(-) diff --git a/homeassistant/auth/models.py b/homeassistant/auth/models.py index 2e5f5940544..9242c6a67c6 100644 --- a/homeassistant/auth/models.py +++ b/homeassistant/auth/models.py @@ -3,8 +3,9 @@ from __future__ import annotations from datetime import datetime, timedelta +from functools import cached_property import secrets -from typing import TYPE_CHECKING, Any, NamedTuple +from typing import Any, NamedTuple import uuid import attr @@ -18,12 +19,6 @@ from homeassistant.util import dt as dt_util from . import permissions as perm_mdl from .const import GROUP_ID_ADMIN -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - - TOKEN_TYPE_NORMAL = "normal" TOKEN_TYPE_SYSTEM = "system" TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = "long_lived_access_token" diff --git a/homeassistant/backports/functools.py b/homeassistant/backports/functools.py index 8aab50eeb66..96c9888bd80 100644 --- a/homeassistant/backports/functools.py +++ b/homeassistant/backports/functools.py @@ -1,79 +1,16 @@ -"""Functools backports from standard lib.""" +"""Functools backports from standard lib. -# This file contains parts of Python's module wrapper -# for the _functools C module -# to allow utilities written in Python to be added -# to the functools module. -# Written by Nick Coghlan , -# Raymond Hettinger , -# and Łukasz Langa . -# Copyright © 2001-2023 Python Software Foundation; All Rights Reserved +This file contained the backport of the cached_property implementation of Python 3.12. + +Since we have dropped support for Python 3.11, we can remove this backport. +This file is kept for now to avoid breaking custom components that might +import it. +""" from __future__ import annotations -from collections.abc import Callable -from types import GenericAlias -from typing import Any, Generic, Self, TypeVar, overload +from functools import cached_property -_T = TypeVar("_T") - - -class cached_property(Generic[_T]): - """Backport of Python 3.12's cached_property. - - Includes https://github.com/python/cpython/pull/101890/files - """ - - def __init__(self, func: Callable[[Any], _T]) -> None: - """Initialize.""" - self.func: Callable[[Any], _T] = func - self.attrname: str | None = None - self.__doc__ = func.__doc__ - - def __set_name__(self, owner: type[Any], name: str) -> None: - """Set name.""" - if self.attrname is None: - self.attrname = name - elif name != self.attrname: - raise TypeError( - "Cannot assign the same cached_property to two different names " - f"({self.attrname!r} and {name!r})." - ) - - @overload - def __get__(self, instance: None, owner: type[Any] | None = None) -> Self: ... - - @overload - def __get__(self, instance: Any, owner: type[Any] | None = None) -> _T: ... - - def __get__( - self, instance: Any | None, owner: type[Any] | None = None - ) -> _T | Self: - """Get.""" - if instance is None: - return self - if self.attrname is None: - raise TypeError( - "Cannot use cached_property instance without calling __set_name__ on it." - ) - try: - cache = instance.__dict__ - # not all objects have __dict__ (e.g. class defines slots) - except AttributeError: - msg = ( - f"No '__dict__' attribute on {type(instance).__name__!r} " - f"instance to cache {self.attrname!r} property." - ) - raise TypeError(msg) from None - val = self.func(instance) - try: - cache[self.attrname] = val - except TypeError: - msg = ( - f"The '__dict__' attribute on {type(instance).__name__!r} instance " - f"does not support item assignment for caching {self.attrname!r} property." - ) - raise TypeError(msg) from None - return val - - __class_getitem__ = classmethod(GenericAlias) # type: ignore[var-annotated] +__all__ = [ + "cached_property", +] diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index 63c095ea6ce..3260454826a 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -3,9 +3,9 @@ from __future__ import annotations from datetime import timedelta -from functools import partial +from functools import cached_property, partial import logging -from typing import TYPE_CHECKING, Any, Final, final +from typing import Any, Final, final import voluptuous as vol @@ -50,11 +50,6 @@ from .const import ( # noqa: F401 CodeFormat, ) -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - _LOGGER: Final = logging.getLogger(__name__) SCAN_INTERVAL: Final = timedelta(seconds=30) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 0bd2ed87d20..ff0a5fc5193 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -6,9 +6,9 @@ from abc import ABC, abstractmethod import asyncio from collections.abc import Callable, Mapping from dataclasses import dataclass -from functools import partial +from functools import cached_property, partial import logging -from typing import TYPE_CHECKING, Any, Protocol, cast +from typing import Any, Protocol, cast import voluptuous as vol @@ -112,12 +112,6 @@ from .const import ( from .helpers import async_get_blueprints from .trace import trace_automation -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - - ENTITY_ID_FORMAT = DOMAIN + ".{}" diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index 4fd99c309bc..dad398e2525 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -4,9 +4,9 @@ from __future__ import annotations from datetime import timedelta from enum import StrEnum -from functools import partial +from functools import cached_property, partial import logging -from typing import TYPE_CHECKING, Literal, final +from typing import Literal, final import voluptuous as vol @@ -28,11 +28,6 @@ from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/button/__init__.py b/homeassistant/components/button/__init__.py index 10589aa461f..cb8ac7745b2 100644 --- a/homeassistant/components/button/__init__.py +++ b/homeassistant/components/button/__init__.py @@ -4,8 +4,9 @@ from __future__ import annotations from datetime import timedelta from enum import StrEnum +from functools import cached_property import logging -from typing import TYPE_CHECKING, final +from typing import final import voluptuous as vol @@ -24,11 +25,6 @@ from homeassistant.util import dt as dt_util from .const import DOMAIN, SERVICE_PRESS -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - SCAN_INTERVAL = timedelta(seconds=30) ENTITY_ID_FORMAT = DOMAIN + ".{}" diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index bfeab601352..a81711f2793 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -9,12 +9,12 @@ from contextlib import suppress from dataclasses import asdict from datetime import datetime, timedelta from enum import IntFlag -from functools import partial +from functools import cached_property, partial import logging import os from random import SystemRandom import time -from typing import TYPE_CHECKING, Any, Final, cast, final +from typing import Any, Final, cast, final from aiohttp import hdrs, web import attr @@ -85,11 +85,6 @@ from .const import ( # noqa: F401 from .img_util import scale_jpeg_camera_image from .prefs import CameraPreferences, DynamicStreamSettings # noqa: F401 -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - _LOGGER = logging.getLogger(__name__) SERVICE_ENABLE_MOTION: Final = "enable_motion_detection" diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 00fd69ce63b..bda00c9b57f 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -5,8 +5,9 @@ from __future__ import annotations import asyncio from datetime import timedelta import functools as ft +from functools import cached_property import logging -from typing import TYPE_CHECKING, Any, Literal, final +from typing import Any, Literal, final import voluptuous as vol @@ -117,11 +118,6 @@ from .const import ( # noqa: F401 HVACMode, ) -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - DEFAULT_MIN_TEMP = 7 DEFAULT_MAX_TEMP = 35 DEFAULT_MIN_HUMIDITY = 30 diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index 1eac6844703..71e89797c05 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -6,8 +6,9 @@ from collections.abc import Callable from datetime import timedelta from enum import IntFlag, StrEnum import functools as ft +from functools import cached_property import logging -from typing import TYPE_CHECKING, Any, ParamSpec, TypeVar, final +from typing import Any, ParamSpec, TypeVar, final import voluptuous as vol @@ -46,11 +47,6 @@ from homeassistant.loader import bind_hass from . import group as group_pre_import # noqa: F401 -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - _LOGGER = logging.getLogger(__name__) DOMAIN = "cover" diff --git a/homeassistant/components/date/__init__.py b/homeassistant/components/date/__init__.py index 3cb6ad3a77d..ddd85ffbf06 100644 --- a/homeassistant/components/date/__init__.py +++ b/homeassistant/components/date/__init__.py @@ -3,8 +3,9 @@ from __future__ import annotations from datetime import date, timedelta +from functools import cached_property import logging -from typing import TYPE_CHECKING, final +from typing import final import voluptuous as vol @@ -22,12 +23,6 @@ from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, SERVICE_SET_VALUE -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - - SCAN_INTERVAL = timedelta(seconds=30) ENTITY_ID_FORMAT = DOMAIN + ".{}" diff --git a/homeassistant/components/datetime/__init__.py b/homeassistant/components/datetime/__init__.py index 420cf27b5aa..b1be0a0d08d 100644 --- a/homeassistant/components/datetime/__init__.py +++ b/homeassistant/components/datetime/__init__.py @@ -3,8 +3,9 @@ from __future__ import annotations from datetime import UTC, datetime, timedelta +from functools import cached_property import logging -from typing import TYPE_CHECKING, final +from typing import final import voluptuous as vol @@ -22,11 +23,6 @@ from homeassistant.util import dt as dt_util from .const import ATTR_DATETIME, DOMAIN, SERVICE_SET_VALUE -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - SCAN_INTERVAL = timedelta(seconds=30) ENTITY_ID_FORMAT = DOMAIN + ".{}" diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index 47751bf5e90..1d1d4645bb4 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -5,6 +5,7 @@ from __future__ import annotations import asyncio from collections.abc import Callable, Coroutine, Sequence from datetime import datetime, timedelta +from functools import cached_property import hashlib from types import ModuleType from typing import Any, Final, Protocol, final @@ -13,7 +14,6 @@ import attr import voluptuous as vol from homeassistant import util -from homeassistant.backports.functools import cached_property from homeassistant.components import zone from homeassistant.components.zone import ENTITY_ID_HOME from homeassistant.config import ( diff --git a/homeassistant/components/dlna_dms/dms.py b/homeassistant/components/dlna_dms/dms.py index aaa55e3ad3e..66c328f2e92 100644 --- a/homeassistant/components/dlna_dms/dms.py +++ b/homeassistant/components/dlna_dms/dms.py @@ -7,6 +7,7 @@ from collections.abc import Callable, Coroutine from dataclasses import dataclass from enum import StrEnum import functools +from functools import cached_property from typing import Any, TypeVar, cast from async_upnp_client.aiohttp import AiohttpSessionRequester @@ -17,7 +18,6 @@ from async_upnp_client.exceptions import UpnpActionError, UpnpConnectionError, U from async_upnp_client.profiles.dlna import ContentDirectoryErrorCode, DmsDevice from didl_lite import didl_lite -from homeassistant.backports.functools import cached_property from homeassistant.components import ssdp from homeassistant.components.media_player import BrowseError, MediaClass from homeassistant.components.media_source.error import Unresolvable diff --git a/homeassistant/components/event/__init__.py b/homeassistant/components/event/__init__.py index 2ad0e6d950b..925c0855c71 100644 --- a/homeassistant/components/event/__init__.py +++ b/homeassistant/components/event/__init__.py @@ -5,8 +5,9 @@ from __future__ import annotations from dataclasses import asdict, dataclass from datetime import datetime, timedelta from enum import StrEnum +from functools import cached_property import logging -from typing import TYPE_CHECKING, Any, Self, final +from typing import Any, Self, final from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -22,12 +23,6 @@ from homeassistant.util import dt as dt_util from .const import ATTR_EVENT_TYPE, ATTR_EVENT_TYPES, DOMAIN -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - - SCAN_INTERVAL = timedelta(seconds=30) ENTITY_ID_FORMAT = DOMAIN + ".{}" diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index b62f207bde8..0bed3eb1ff2 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -5,9 +5,10 @@ from __future__ import annotations from datetime import timedelta from enum import IntFlag import functools as ft +from functools import cached_property import logging import math -from typing import TYPE_CHECKING, Any, final +from typing import Any, final import voluptuous as vol @@ -40,12 +41,6 @@ from homeassistant.util.percentage import ( ranged_value_to_percentage, ) -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - - _LOGGER = logging.getLogger(__name__) DOMAIN = "fan" diff --git a/homeassistant/components/ffmpeg/__init__.py b/homeassistant/components/ffmpeg/__init__.py index b4c919fcb79..09d8e2401f0 100644 --- a/homeassistant/components/ffmpeg/__init__.py +++ b/homeassistant/components/ffmpeg/__init__.py @@ -3,8 +3,9 @@ from __future__ import annotations import asyncio +from functools import cached_property import re -from typing import TYPE_CHECKING, Generic, TypeVar +from typing import Generic, TypeVar from haffmpeg.core import HAFFmpeg from haffmpeg.tools import IMAGE_JPEG, FFVersion, ImageFrame @@ -28,12 +29,6 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from homeassistant.util.signal_type import SignalType -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - - _HAFFmpegT = TypeVar("_HAFFmpegT", bound=HAFFmpeg) DOMAIN = "ffmpeg" diff --git a/homeassistant/components/fints/sensor.py b/homeassistant/components/fints/sensor.py index 6d3f1f63b84..4a4c2d05181 100644 --- a/homeassistant/components/fints/sensor.py +++ b/homeassistant/components/fints/sensor.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections import namedtuple from datetime import timedelta +from functools import cached_property import logging from typing import Any @@ -11,7 +12,6 @@ from fints.client import FinTS3PinTanClient from fints.models import SEPAAccount import voluptuous as vol -from homeassistant.backports.functools import cached_property from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_NAME, CONF_PIN, CONF_URL, CONF_USERNAME from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/geo_location/__init__.py b/homeassistant/components/geo_location/__init__.py index d81fab65dc9..48e2f35ccc1 100644 --- a/homeassistant/components/geo_location/__init__.py +++ b/homeassistant/components/geo_location/__init__.py @@ -3,8 +3,9 @@ from __future__ import annotations from datetime import timedelta +from functools import cached_property import logging -from typing import TYPE_CHECKING, Any, final +from typing import Any, final from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE @@ -17,12 +18,6 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - - _LOGGER = logging.getLogger(__name__) ATTR_DISTANCE = "distance" diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index 49ae3bb4a42..544e23798d0 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -2,8 +2,9 @@ from __future__ import annotations +from functools import cached_property import logging -from typing import TYPE_CHECKING, Any, Final +from typing import Any, Final from aiohomekit.model.characteristics import ( ActivationStateValues, @@ -49,12 +50,6 @@ from . import KNOWN_DEVICES from .connection import HKDevice from .entity import HomeKitEntity -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - - _LOGGER = logging.getLogger(__name__) # Map of Homekit operation modes to hass modes diff --git a/homeassistant/components/homekit_controller/cover.py b/homeassistant/components/homekit_controller/cover.py index b15ce645a29..ca041d49e11 100644 --- a/homeassistant/components/homekit_controller/cover.py +++ b/homeassistant/components/homekit_controller/cover.py @@ -2,7 +2,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from functools import cached_property +from typing import Any from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import Service, ServicesTypes @@ -29,12 +30,6 @@ from . import KNOWN_DEVICES from .connection import HKDevice from .entity import HomeKitEntity -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - - STATE_STOPPED = "stopped" CURRENT_GARAGE_STATE_MAP = { diff --git a/homeassistant/components/homekit_controller/fan.py b/homeassistant/components/homekit_controller/fan.py index 39df2b7ce51..79e302ace74 100644 --- a/homeassistant/components/homekit_controller/fan.py +++ b/homeassistant/components/homekit_controller/fan.py @@ -2,7 +2,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from functools import cached_property +from typing import Any from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import Service, ServicesTypes @@ -26,12 +27,6 @@ from . import KNOWN_DEVICES from .connection import HKDevice from .entity import HomeKitEntity -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - - # 0 is clockwise, 1 is counter-clockwise. The match to forward and reverse is so that # its consistent with homeassistant.components.homekit. DIRECTION_TO_HK = { diff --git a/homeassistant/components/homekit_controller/humidifier.py b/homeassistant/components/homekit_controller/humidifier.py index fecba147a71..cbfcfb6d3bb 100644 --- a/homeassistant/components/homekit_controller/humidifier.py +++ b/homeassistant/components/homekit_controller/humidifier.py @@ -2,7 +2,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from functools import cached_property +from typing import Any from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import Service, ServicesTypes @@ -26,12 +27,6 @@ from . import KNOWN_DEVICES from .connection import HKDevice from .entity import HomeKitEntity -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - - HK_MODE_TO_HA = { 0: "off", 1: MODE_AUTO, diff --git a/homeassistant/components/homekit_controller/light.py b/homeassistant/components/homekit_controller/light.py index b314ffe85de..d5f20723ff1 100644 --- a/homeassistant/components/homekit_controller/light.py +++ b/homeassistant/components/homekit_controller/light.py @@ -2,7 +2,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from functools import cached_property +from typing import Any from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import Service, ServicesTypes @@ -24,11 +25,6 @@ from . import KNOWN_DEVICES from .connection import HKDevice from .entity import HomeKitEntity -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/humidifier/__init__.py b/homeassistant/components/humidifier/__init__.py index 1af294d8640..da79df6d52f 100644 --- a/homeassistant/components/humidifier/__init__.py +++ b/homeassistant/components/humidifier/__init__.py @@ -4,9 +4,9 @@ from __future__ import annotations from datetime import timedelta from enum import StrEnum -from functools import partial +from functools import cached_property, partial import logging -from typing import TYPE_CHECKING, Any, final +from typing import Any, final import voluptuous as vol @@ -56,12 +56,6 @@ from .const import ( # noqa: F401 HumidifierEntityFeature, ) -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/image/__init__.py b/homeassistant/components/image/__init__.py index 56dc7cc2cfa..59e5ddceebf 100644 --- a/homeassistant/components/image/__init__.py +++ b/homeassistant/components/image/__init__.py @@ -7,9 +7,10 @@ import collections from contextlib import suppress from dataclasses import dataclass from datetime import datetime, timedelta +from functools import cached_property import logging from random import SystemRandom -from typing import TYPE_CHECKING, Final, final +from typing import Final, final from aiohttp import hdrs, web import httpx @@ -35,12 +36,6 @@ from homeassistant.helpers.typing import UNDEFINED, ConfigType, UndefinedType from .const import DOMAIN, IMAGE_TIMEOUT -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - - _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL: Final = timedelta(seconds=30) diff --git a/homeassistant/components/lawn_mower/__init__.py b/homeassistant/components/lawn_mower/__init__.py index f28bd1308b0..8cb9850bde7 100644 --- a/homeassistant/components/lawn_mower/__init__.py +++ b/homeassistant/components/lawn_mower/__init__.py @@ -3,8 +3,9 @@ from __future__ import annotations from datetime import timedelta +from functools import cached_property import logging -from typing import TYPE_CHECKING, final +from typing import final from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -25,12 +26,6 @@ from .const import ( LawnMowerEntityFeature, ) -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - - SCAN_INTERVAL = timedelta(seconds=60) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 53c7328ece4..726aef73c01 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -7,9 +7,10 @@ import csv import dataclasses from datetime import timedelta from enum import IntFlag, StrEnum +from functools import cached_property import logging import os -from typing import TYPE_CHECKING, Any, Self, cast, final +from typing import Any, Self, cast, final import voluptuous as vol @@ -34,11 +35,6 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass import homeassistant.util.color as color_util -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - DOMAIN = "light" SCAN_INTERVAL = timedelta(seconds=30) DATA_PROFILES = "light_profiles" diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index b2cd28324cb..10c1526c5bb 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -5,6 +5,7 @@ from __future__ import annotations from datetime import timedelta from enum import IntFlag import functools as ft +from functools import cached_property import logging import re from typing import TYPE_CHECKING, Any, final @@ -44,11 +45,6 @@ from homeassistant.helpers.typing import ConfigType, StateType from . import group as group_pre_import # noqa: F401 -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - _LOGGER = logging.getLogger(__name__) ATTR_CHANGED_BY = "changed_by" diff --git a/homeassistant/components/logbook/models.py b/homeassistant/components/logbook/models.py index 1073c6b0d3a..9409c59985c 100644 --- a/homeassistant/components/logbook/models.py +++ b/homeassistant/components/logbook/models.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Callable, Mapping from dataclasses import dataclass +from functools import cached_property from typing import TYPE_CHECKING, Any, cast from sqlalchemy.engine.row import Row @@ -20,11 +21,6 @@ from homeassistant.core import Context, Event, State, callback from homeassistant.util.json import json_loads from homeassistant.util.ulid import ulid_to_bytes -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - @dataclass(slots=True) class LogbookConfig: diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 6535aea3e52..35e1b1cb71e 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -9,12 +9,12 @@ from contextlib import suppress import datetime as dt from enum import StrEnum import functools as ft -from functools import lru_cache +from functools import cached_property, lru_cache import hashlib from http import HTTPStatus import logging import secrets -from typing import TYPE_CHECKING, Any, Final, Required, TypedDict, final +from typing import Any, Final, Required, TypedDict, final from urllib.parse import quote, urlparse from aiohttp import web @@ -134,11 +134,6 @@ from .const import ( # noqa: F401 ) from .errors import BrowseError -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - _LOGGER = logging.getLogger(__name__) ENTITY_ID_FORMAT = DOMAIN + ".{}" diff --git a/homeassistant/components/nibe_heatpump/coordinator.py b/homeassistant/components/nibe_heatpump/coordinator.py index 1711c3d8f2a..a3d37ce0719 100644 --- a/homeassistant/components/nibe_heatpump/coordinator.py +++ b/homeassistant/components/nibe_heatpump/coordinator.py @@ -6,6 +6,7 @@ import asyncio from collections import defaultdict from collections.abc import Callable, Iterable from datetime import date, timedelta +from functools import cached_property from typing import Any, Generic, TypeVar from nibe.coil import Coil, CoilData @@ -13,7 +14,6 @@ from nibe.connection import Connection from nibe.exceptions import CoilNotFoundException, ReadException from nibe.heatpump import HeatPump, Series -from homeassistant.backports.functools import cached_property from homeassistant.config_entries import ConfigEntry from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceInfo diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index d3785e0eae6..e5b307f5e57 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -6,6 +6,7 @@ from collections.abc import Callable from contextlib import suppress import dataclasses from datetime import timedelta +from functools import cached_property import logging from math import ceil, floor from typing import TYPE_CHECKING, Any, Self, final @@ -44,11 +45,6 @@ from .const import ( # noqa: F401 ) from .websocket_api import async_setup as async_setup_ws_api -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - _LOGGER = logging.getLogger(__name__) ENTITY_ID_FORMAT = DOMAIN + ".{}" diff --git a/homeassistant/components/recorder/models/state.py b/homeassistant/components/recorder/models/state.py index e1f23f32118..ca70b856d76 100644 --- a/homeassistant/components/recorder/models/state.py +++ b/homeassistant/components/recorder/models/state.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import datetime +from functools import cached_property import logging from typing import TYPE_CHECKING, Any @@ -19,11 +20,6 @@ import homeassistant.util.dt as dt_util from .state_attributes import decode_attributes_from_source -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - _LOGGER = logging.getLogger(__name__) EMPTY_CONTEXT = Context(id=None) diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py index 2b88c51e936..88813e4a70c 100644 --- a/homeassistant/components/remote/__init__.py +++ b/homeassistant/components/remote/__init__.py @@ -6,8 +6,9 @@ from collections.abc import Iterable from datetime import timedelta from enum import IntFlag import functools as ft +from functools import cached_property import logging -from typing import TYPE_CHECKING, Any, final +from typing import Any, final import voluptuous as vol @@ -37,12 +38,6 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - - _LOGGER = logging.getLogger(__name__) ATTR_ACTIVITY = "activity" diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index 82752ed15bc..6aeb0a9965e 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -5,8 +5,9 @@ from __future__ import annotations from abc import ABC, abstractmethod import asyncio from dataclasses import dataclass +from functools import cached_property import logging -from typing import TYPE_CHECKING, Any, cast +from typing import Any, cast import voluptuous as vol @@ -74,12 +75,6 @@ from .const import ( from .helpers import async_get_blueprints from .trace import trace_script -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - - SCRIPT_SERVICE_SCHEMA = vol.Schema(dict) SCRIPT_TURN_ONOFF_SCHEMA = make_entity_service_schema( {vol.Optional(ATTR_VARIABLES): {str: cv.match_all}} diff --git a/homeassistant/components/select/__init__.py b/homeassistant/components/select/__init__.py index 0c54dfc0aac..6e134c8958c 100644 --- a/homeassistant/components/select/__init__.py +++ b/homeassistant/components/select/__init__.py @@ -3,8 +3,9 @@ from __future__ import annotations from datetime import timedelta +from functools import cached_property import logging -from typing import TYPE_CHECKING, Any, final +from typing import Any, final import voluptuous as vol @@ -32,11 +33,6 @@ from .const import ( SERVICE_SELECT_PREVIOUS, ) -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - SCAN_INTERVAL = timedelta(seconds=30) ENTITY_ID_FORMAT = DOMAIN + ".{}" diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 92499a05af4..1d06e1a24c4 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -8,10 +8,10 @@ from contextlib import suppress from dataclasses import dataclass from datetime import UTC, date, datetime, timedelta from decimal import Decimal, InvalidOperation as DecimalInvalidOperation -from functools import partial +from functools import cached_property, partial import logging from math import ceil, floor, isfinite, log10 -from typing import TYPE_CHECKING, Any, Final, Self, cast, final, override +from typing import Any, Final, Self, cast, final, override from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( # noqa: F401 @@ -91,11 +91,6 @@ from .const import ( # noqa: F401 ) from .websocket_api import async_setup as async_setup_ws_api -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - _LOGGER: Final = logging.getLogger(__name__) ENTITY_ID_FORMAT: Final = DOMAIN + ".{}" diff --git a/homeassistant/components/siren/__init__.py b/homeassistant/components/siren/__init__.py index a083aa9d702..a0a599dd2df 100644 --- a/homeassistant/components/siren/__init__.py +++ b/homeassistant/components/siren/__init__.py @@ -3,9 +3,9 @@ from __future__ import annotations from datetime import timedelta -from functools import partial +from functools import cached_property, partial import logging -from typing import TYPE_CHECKING, Any, TypedDict, cast, final +from typing import Any, TypedDict, cast, final import voluptuous as vol @@ -40,11 +40,6 @@ from .const import ( # noqa: F401 SirenEntityFeature, ) -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=60) diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index 86c67248eea..995bcda294f 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -4,9 +4,8 @@ from __future__ import annotations from datetime import timedelta from enum import StrEnum -from functools import partial +from functools import cached_property, partial import logging -from typing import TYPE_CHECKING import voluptuous as vol @@ -35,11 +34,6 @@ from homeassistant.loader import bind_hass from .const import DOMAIN -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - SCAN_INTERVAL = timedelta(seconds=30) ENTITY_ID_FORMAT = DOMAIN + ".{}" diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index 735fa7ddd23..7f24f31c692 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -4,9 +4,10 @@ from __future__ import annotations from collections.abc import Callable, Mapping import contextlib +from functools import cached_property import itertools import logging -from typing import TYPE_CHECKING, Any +from typing import Any import voluptuous as vol @@ -58,11 +59,6 @@ from .const import ( CONF_PICTURE, ) -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - _LOGGER = logging.getLogger(__name__) TEMPLATE_ENTITY_AVAILABILITY_SCHEMA = vol.Schema( diff --git a/homeassistant/components/text/__init__.py b/homeassistant/components/text/__init__.py index cf29910cc34..f45a9cf3563 100644 --- a/homeassistant/components/text/__init__.py +++ b/homeassistant/components/text/__init__.py @@ -5,9 +5,10 @@ from __future__ import annotations from dataclasses import asdict, dataclass from datetime import timedelta from enum import StrEnum +from functools import cached_property import logging import re -from typing import TYPE_CHECKING, Any, final +from typing import Any, final import voluptuous as vol @@ -34,11 +35,6 @@ from .const import ( SERVICE_SET_VALUE, ) -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - SCAN_INTERVAL = timedelta(seconds=30) ENTITY_ID_FORMAT = DOMAIN + ".{}" diff --git a/homeassistant/components/thread/dataset_store.py b/homeassistant/components/thread/dataset_store.py index de322510ef2..b880be801a4 100644 --- a/homeassistant/components/thread/dataset_store.py +++ b/homeassistant/components/thread/dataset_store.py @@ -5,13 +5,13 @@ from __future__ import annotations from asyncio import Event, Task, wait import dataclasses from datetime import datetime +from functools import cached_property import logging from typing import Any, cast from python_otbr_api import tlv_parser from python_otbr_api.tlv_parser import MeshcopTLVType -from homeassistant.backports.functools import cached_property from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.singleton import singleton diff --git a/homeassistant/components/time/__init__.py b/homeassistant/components/time/__init__.py index 2e87aaac28d..4e101ddd67d 100644 --- a/homeassistant/components/time/__init__.py +++ b/homeassistant/components/time/__init__.py @@ -3,8 +3,9 @@ from __future__ import annotations from datetime import time, timedelta +from functools import cached_property import logging -from typing import TYPE_CHECKING, final +from typing import final import voluptuous as vol @@ -22,12 +23,6 @@ from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, SERVICE_SET_VALUE -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - - SCAN_INTERVAL = timedelta(seconds=30) ENTITY_ID_FORMAT = DOMAIN + ".{}" diff --git a/homeassistant/components/todo/__init__.py b/homeassistant/components/todo/__init__.py index 74ee99b811f..e574c6372a7 100644 --- a/homeassistant/components/todo/__init__.py +++ b/homeassistant/components/todo/__init__.py @@ -3,8 +3,9 @@ from collections.abc import Callable, Iterable import dataclasses import datetime +from functools import cached_property import logging -from typing import TYPE_CHECKING, Any, final +from typing import Any, final import voluptuous as vol @@ -41,12 +42,6 @@ from .const import ( TodoListEntityFeature, ) -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - - _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = datetime.timedelta(seconds=60) diff --git a/homeassistant/components/update/__init__.py b/homeassistant/components/update/__init__.py index 142c9b4a6c3..57d63c92ede 100644 --- a/homeassistant/components/update/__init__.py +++ b/homeassistant/components/update/__init__.py @@ -4,9 +4,9 @@ from __future__ import annotations from datetime import timedelta from enum import StrEnum -from functools import lru_cache +from functools import cached_property, lru_cache import logging -from typing import TYPE_CHECKING, Any, Final, final +from typing import Any, Final, final from awesomeversion import AwesomeVersion, AwesomeVersionCompareException import voluptuous as vol @@ -43,11 +43,6 @@ from .const import ( UpdateEntityFeature, ) -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - SCAN_INTERVAL = timedelta(minutes=15) ENTITY_ID_FORMAT: Final = DOMAIN + ".{}" diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index bdf690ed63f..4f5b6066dbd 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -5,9 +5,9 @@ from __future__ import annotations from collections.abc import Mapping from datetime import timedelta from enum import IntFlag -from functools import partial +from functools import cached_property, partial import logging -from typing import TYPE_CHECKING, Any +from typing import Any import voluptuous as vol @@ -38,11 +38,6 @@ from homeassistant.loader import bind_hass from . import group as group_pre_import # noqa: F401 from .const import STATE_CLEANING, STATE_DOCKED, STATE_ERROR, STATE_RETURNING -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - _LOGGER = logging.getLogger(__name__) DOMAIN = "vacuum" diff --git a/homeassistant/components/water_heater/__init__.py b/homeassistant/components/water_heater/__init__.py index 167acb85914..ad0149919dc 100644 --- a/homeassistant/components/water_heater/__init__.py +++ b/homeassistant/components/water_heater/__init__.py @@ -6,8 +6,9 @@ from collections.abc import Mapping from datetime import timedelta from enum import IntFlag import functools as ft +from functools import cached_property import logging -from typing import TYPE_CHECKING, Any, final +from typing import Any, final import voluptuous as vol @@ -44,12 +45,6 @@ from homeassistant.util.unit_conversion import TemperatureConverter from . import group as group_pre_import # noqa: F401 -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - - DEFAULT_MIN_TEMP = 110 DEFAULT_MAX_TEMP = 140 diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 404154ade2b..95655f439c9 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -6,10 +6,9 @@ import abc from collections.abc import Callable, Iterable from contextlib import suppress from datetime import timedelta -from functools import partial +from functools import cached_property, partial import logging from typing import ( - TYPE_CHECKING, Any, Final, Generic, @@ -84,12 +83,6 @@ from .const import ( ) from .websocket_api import async_setup as async_setup_ws_api -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - - _LOGGER = logging.getLogger(__name__) ATTR_CONDITION_CLASS = "condition_class" diff --git a/homeassistant/components/zha/core/cluster_handlers/lighting.py b/homeassistant/components/zha/core/cluster_handlers/lighting.py index f19ad311f9e..bde0fdbb0e7 100644 --- a/homeassistant/components/zha/core/cluster_handlers/lighting.py +++ b/homeassistant/components/zha/core/cluster_handlers/lighting.py @@ -2,9 +2,9 @@ from __future__ import annotations -from zigpy.zcl.clusters.lighting import Ballast, Color +from functools import cached_property -from homeassistant.backports.functools import cached_property +from zigpy.zcl.clusters.lighting import Ballast, Color from .. import registries from ..const import REPORT_CONFIG_DEFAULT diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index e5fdfe36a9b..65292e275de 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -6,6 +6,7 @@ import asyncio from collections.abc import Callable from datetime import timedelta from enum import Enum +from functools import cached_property import logging import random import time @@ -23,7 +24,6 @@ from zigpy.zcl.clusters.general import Groups, Identify from zigpy.zcl.foundation import Status as ZclStatus, ZCLCommandDef import zigpy.zdo.types as zdo_types -from homeassistant.backports.functools import cached_property from homeassistant.const import ATTR_COMMAND, ATTR_DEVICE_ID, ATTR_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index f92e442e5a3..dbc300891b1 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -17,6 +17,7 @@ from contextvars import ContextVar from copy import deepcopy from enum import Enum, StrEnum import functools +from functools import cached_property import logging from random import randint from types import MappingProxyType @@ -69,8 +70,6 @@ from .util.async_ import create_eager_task from .util.decorator import Registry if TYPE_CHECKING: - from functools import cached_property - from .components.bluetooth import BluetoothServiceInfoBleak from .components.dhcp import DhcpServiceInfo from .components.hassio import HassioServiceInfo @@ -78,8 +77,6 @@ if TYPE_CHECKING: from .components.usb import UsbServiceInfo from .components.zeroconf import ZeroconfServiceInfo from .helpers.service_info.mqtt import MqttServiceInfo -else: - from .backports.functools import cached_property _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/core.py b/homeassistant/core.py index 1edeb666492..9a26a971f64 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -23,6 +23,7 @@ from dataclasses import dataclass import datetime import enum import functools +from functools import cached_property import inspect import logging import os @@ -116,14 +117,10 @@ from .util.unit_system import ( # Typing imports that create a circular dependency if TYPE_CHECKING: - from functools import cached_property - from .auth import AuthManager from .components.http import ApiConfig, HomeAssistantHTTP from .config_entries import ConfigEntries from .helpers.entity import StateInfo -else: - from .backports.functools import cached_property STOPPING_STAGE_SHUTDOWN_TIMEOUT = 20 STOP_STAGE_SHUTDOWN_TIMEOUT = 100 diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index c9a9016560c..8d5fe3f2f94 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Mapping from enum import StrEnum -from functools import lru_cache, partial +from functools import cached_property, lru_cache, partial import logging import time from typing import TYPE_CHECKING, Any, Literal, TypedDict, TypeVar, cast @@ -12,7 +12,6 @@ from typing import TYPE_CHECKING, Any, Literal, TypedDict, TypeVar, cast import attr from yarl import URL -from homeassistant.backports.functools import cached_property from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, callback, get_release_channel from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 5c8cff2f60b..eee35fa4cca 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -9,6 +9,7 @@ from collections.abc import Callable, Coroutine, Iterable, Mapping, MutableMappi import dataclasses from enum import Enum, IntFlag, auto import functools as ft +from functools import cached_property import logging import math from operator import attrgetter @@ -73,11 +74,7 @@ from .event import ( from .typing import UNDEFINED, StateType, UndefinedType if TYPE_CHECKING: - from functools import cached_property - from .entity_platform import EntityPlatform -else: - from homeassistant.backports.functools import cached_property _T = TypeVar("_T") diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 27e73320841..a0e7b669418 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -13,6 +13,7 @@ from __future__ import annotations from collections.abc import Callable, Iterable, KeysView, Mapping from datetime import datetime, timedelta from enum import StrEnum +from functools import cached_property import logging import time from typing import TYPE_CHECKING, Any, Literal, NotRequired, TypedDict, TypeVar, cast @@ -20,7 +21,6 @@ from typing import TYPE_CHECKING, Any, Literal, NotRequired, TypedDict, TypeVar, import attr import voluptuous as vol -from homeassistant.backports.functools import cached_property from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, diff --git a/homeassistant/helpers/frame.py b/homeassistant/helpers/frame.py index ee092717753..d86fec3de43 100644 --- a/homeassistant/helpers/frame.py +++ b/homeassistant/helpers/frame.py @@ -7,21 +7,17 @@ from collections.abc import Callable from contextlib import suppress from dataclasses import dataclass import functools +from functools import cached_property import linecache import logging import sys from types import FrameType -from typing import TYPE_CHECKING, Any, TypeVar, cast +from typing import Any, TypeVar, cast from homeassistant.core import HomeAssistant, async_get_hass from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import async_suggest_report_issue -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - _LOGGER = logging.getLogger(__name__) # Keep track of integrations already reported to prevent flooding diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 2ea7b259872..0b054307702 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -9,11 +9,11 @@ from contextvars import ContextVar from copy import copy from dataclasses import dataclass from datetime import datetime, timedelta -from functools import partial +from functools import cached_property, partial import itertools import logging from types import MappingProxyType -from typing import TYPE_CHECKING, Any, Literal, TypedDict, TypeVar, cast +from typing import Any, Literal, TypedDict, TypeVar, cast import async_interrupt import voluptuous as vol @@ -107,12 +107,6 @@ from .trace import ( from .trigger import async_initialize_triggers, async_validate_trigger_config from .typing import UNDEFINED, ConfigType, UndefinedType -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - - # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs _T = TypeVar("_T") diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 2413a53e605..c2047328013 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -6,12 +6,13 @@ import asyncio from collections.abc import Callable, Iterable, Mapping, Sequence from contextlib import suppress from copy import deepcopy +from functools import cached_property import inspect from json import JSONDecodeError, JSONEncoder import logging import os from pathlib import Path -from typing import TYPE_CHECKING, Any, Generic, TypeVar +from typing import Any, Generic, TypeVar from homeassistant.const import ( EVENT_HOMEASSISTANT_FINAL_WRITE, @@ -34,12 +35,6 @@ from homeassistant.util.file import WriteError from . import json as json_helper -if TYPE_CHECKING: - from functools import cached_property -else: - from ..backports.functools import cached_property - - # mypy: allow-untyped-calls, allow-untyped-defs, no-warn-return-any # mypy: no-check-untyped-defs MAX_LOAD_CONCURRENTLY = 6 diff --git a/homeassistant/loader.py b/homeassistant/loader.py index eb70f0b83af..da8159ca2cf 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -11,6 +11,7 @@ from collections.abc import Callable, Iterable from contextlib import suppress from dataclasses import dataclass import functools as ft +from functools import cached_property import importlib import logging import os @@ -41,15 +42,11 @@ from .generated.zeroconf import HOMEKIT, ZEROCONF from .util.json import JSON_DECODE_EXCEPTIONS, json_loads if TYPE_CHECKING: - from functools import cached_property - # The relative imports below are guarded by TYPE_CHECKING # because they would cause a circular import otherwise. from .config_entries import ConfigEntry from .helpers import device_registry as dr from .helpers.typing import ConfigType -else: - from .backports.functools import cached_property _CallableT = TypeVar("_CallableT", bound=Callable[..., Any]) diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index 28027c97211..d07b578628c 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -8,7 +8,7 @@ from io import StringIO, TextIOWrapper import logging import os from pathlib import Path -from typing import TYPE_CHECKING, Any, TextIO, TypeVar, overload +from typing import Any, TextIO, TypeVar, overload import yaml @@ -22,18 +22,14 @@ except ImportError: SafeLoader as FastestAvailableSafeLoader, ) +from functools import cached_property + from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.frame import report from .const import SECRET_YAML from .objects import Input, NodeDictClass, NodeListClass, NodeStrClass -if TYPE_CHECKING: - from functools import cached_property -else: - from homeassistant.backports.functools import cached_property - - # mypy: allow-untyped-calls, no-warn-return-any JSON_TYPE = list | dict | str diff --git a/pylint/plugins/hass_imports.py b/pylint/plugins/hass_imports.py index b8ec65e4460..d8f85df011f 100644 --- a/pylint/plugins/hass_imports.py +++ b/pylint/plugins/hass_imports.py @@ -25,6 +25,15 @@ _OBSOLETE_IMPORT: dict[str, list[ObsoleteImportMatch]] = { constant=re.compile(r"^StrEnum$"), ), ], + "homeassistant.backports.functools": [ + ObsoleteImportMatch( + reason=( + "We can now use the Python 3.12 provided " + "functools.cached_property instead" + ), + constant=re.compile(r"^cached_property$"), + ), + ], "homeassistant.components.alarm_control_panel": [ ObsoleteImportMatch( reason="replaced by AlarmControlPanelEntityFeature enum", diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index dac03f0be67..70d917dbc7b 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -5,6 +5,7 @@ from collections.abc import Iterable import dataclasses from datetime import timedelta from enum import IntFlag +from functools import cached_property import logging import threading from typing import Any @@ -15,7 +16,6 @@ import pytest from syrupy.assertion import SnapshotAssertion import voluptuous as vol -from homeassistant.backports.functools import cached_property from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS, diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 6af5f2cde3f..b817aaddf5d 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -5,6 +5,7 @@ from __future__ import annotations import asyncio from collections.abc import Generator from datetime import timedelta +from functools import cached_property import logging from typing import Any from unittest.mock import ANY, AsyncMock, Mock, patch @@ -14,7 +15,6 @@ import pytest from syrupy.assertion import SnapshotAssertion from homeassistant import config_entries, data_entry_flow, loader -from homeassistant.backports.functools import cached_property from homeassistant.components import dhcp from homeassistant.components.hassio import HassioServiceInfo from homeassistant.const import ( From 86feae421c3222e9e2491c7da2cfa6fcc1903d4b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Apr 2024 23:38:37 -1000 Subject: [PATCH 251/967] Avoid linear search in hassio to find devices (#114806) --- homeassistant/components/hassio/data.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hassio/data.py b/homeassistant/components/hassio/data.py index a00335d44a2..678d0666c05 100644 --- a/homeassistant/components/hassio/data.py +++ b/homeassistant/components/hassio/data.py @@ -371,9 +371,10 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): # pylint: disable=has # Remove add-ons that are no longer installed from device registry supervisor_addon_devices = { list(device.identifiers)[0][1] - for device in self.dev_reg.devices.values() - if self.entry_id in device.config_entries - and device.model == SupervisorEntityModel.ADDON + for device in self.dev_reg.devices.get_devices_for_config_entry_id( + self.entry_id + ) + if device.model == SupervisorEntityModel.ADDON } if stale_addons := supervisor_addon_devices - set(new_data[DATA_KEY_ADDONS]): async_remove_addons_from_dev_reg(self.dev_reg, stale_addons) From 5be5c37326956b19a1181c8a56668e9c815c1641 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Apr 2024 23:39:16 -1000 Subject: [PATCH 252/967] Avoid linear search in homekit to find devices (#114808) --- homeassistant/components/homekit/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 294dc7f33a6..1b9ccaca8bf 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -953,9 +953,8 @@ class HomeKit: """Purge bridges that exist from failed pairing or manual resets.""" devices_to_purge = [ entry.id - for entry in dev_reg.devices.values() - if self._entry_id in entry.config_entries - and ( + for entry in dev_reg.devices.get_devices_for_config_entry_id(self._entry_id) + if ( identifier not in entry.identifiers # type: ignore[comparison-overlap] or connection not in entry.connections ) From e909242bc5a9f24c494e0814c4c3c33b3a49718b Mon Sep 17 00:00:00 2001 From: Benjamin <46243805+bbr111@users.noreply.github.com> Date: Thu, 4 Apr 2024 11:45:01 +0200 Subject: [PATCH 253/967] Fix missing if statement in homematic (#114832) * homematic fix issue #114807 Update climate.py * Update homeassistant/components/homematic/climate.py --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/homematic/climate.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homematic/climate.py b/homeassistant/components/homematic/climate.py index efdb9324f76..16c345c5635 100644 --- a/homeassistant/components/homematic/climate.py +++ b/homeassistant/components/homematic/climate.py @@ -113,7 +113,11 @@ class HMThermostat(HMDevice, ClimateEntity): @property def preset_modes(self): """Return a list of available preset modes.""" - return [HM_PRESET_MAP[mode] for mode in self._hmdevice.ACTIONNODE] + return [ + HM_PRESET_MAP[mode] + for mode in self._hmdevice.ACTIONNODE + if mode in HM_PRESET_MAP + ] @property def current_humidity(self): From d7153d525fe6dddae7eaf22baad567ff16d38bee Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Apr 2024 23:50:30 -1000 Subject: [PATCH 254/967] Avoid linear search in ibeacon to find devices (#114809) --- homeassistant/components/ibeacon/coordinator.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/ibeacon/coordinator.py b/homeassistant/components/ibeacon/coordinator.py index 27181e80ed8..4f232220440 100644 --- a/homeassistant/components/ibeacon/coordinator.py +++ b/homeassistant/components/ibeacon/coordinator.py @@ -493,14 +493,12 @@ class IBeaconCoordinator: @callback def _async_restore_from_registry(self) -> None: """Restore the state of the Coordinator from the device registry.""" - for device in self._dev_reg.devices.values(): - unique_id = None - for identifier in device.identifiers: - if identifier[0] == DOMAIN: - unique_id = identifier[1] - break - if not unique_id: + for device in self._dev_reg.devices.get_devices_for_config_entry_id( + self._entry.entry_id + ): + if not (identifier := next(iter(device.identifiers), None)): continue + unique_id = identifier[1] # iBeacons with a fixed MAC address if unique_id.count("_") == 3: uuid, major, minor, address = unique_id.split("_") From 0710f4c661e06500709654f182b2784b5f8dc648 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Apr 2024 23:51:57 -1000 Subject: [PATCH 255/967] Avoid linear search in purpleair to find devices (#114816) --- homeassistant/components/purpleair/config_flow.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/purpleair/config_flow.py b/homeassistant/components/purpleair/config_flow.py index f9a6415bb38..6c25681329a 100644 --- a/homeassistant/components/purpleair/config_flow.py +++ b/homeassistant/components/purpleair/config_flow.py @@ -115,8 +115,9 @@ def async_get_remove_sensor_options( device_registry = dr.async_get(hass) return [ SelectOptionDict(value=device_entry.id, label=cast(str, device_entry.name)) - for device_entry in device_registry.devices.values() - if config_entry.entry_id in device_entry.config_entries + for device_entry in device_registry.devices.get_devices_for_config_entry_id( + config_entry.entry_id + ) ] From cc96bc44a07ced7af8010e5ec33e188cf90e1bfa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Apr 2024 23:53:04 -1000 Subject: [PATCH 256/967] Avoid linear search in ps4 to find devices (#114814) --- homeassistant/components/ps4/media_player.py | 21 ++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index f01bc00ba72..77477ba7901 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -350,16 +350,17 @@ class PS4Device(MediaPlayerEntity): self._attr_unique_id = entry.unique_id self.entity_id = entry.entity_id break - for device in d_registry.devices.values(): - if self._entry_id in device.config_entries: - self._attr_device_info = DeviceInfo( - identifiers=device.identifiers, - manufacturer=device.manufacturer, - model=device.model, - name=device.name, - sw_version=device.sw_version, - ) - break + for device in d_registry.devices.get_devices_for_config_entry_id( + self._entry_id + ): + self._attr_device_info = DeviceInfo( + identifiers=device.identifiers, + manufacturer=device.manufacturer, + model=device.model, + name=device.name, + sw_version=device.sw_version, + ) + break else: _sw_version = status["system-version"] From 342e47dcc8f63c95701f1c04cba82e90694bd6e1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Apr 2024 00:05:16 -1000 Subject: [PATCH 257/967] Ensure async_test_home_assistant is passed a str in storage tests (#114813) --- tests/helpers/test_storage.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/helpers/test_storage.py b/tests/helpers/test_storage.py index 0d574e9811f..12dc56db85d 100644 --- a/tests/helpers/test_storage.py +++ b/tests/helpers/test_storage.py @@ -627,7 +627,7 @@ async def test_saving_load_round_trip(tmpdir: py.path.local) -> None: """Test saving and loading round trip.""" loop = asyncio.get_running_loop() config_dir = await loop.run_in_executor(None, tmpdir.mkdir, "temp_storage") - async with async_test_home_assistant(config_dir=config_dir) as hass: + async with async_test_home_assistant(config_dir=config_dir.strpath) as hass: class NamedTupleSubclass(NamedTuple): """A NamedTuple subclass.""" @@ -671,7 +671,7 @@ async def test_loading_corrupt_core_file( loop = asyncio.get_running_loop() tmp_storage = await loop.run_in_executor(None, tmpdir.mkdir, "temp_storage") - async with async_test_home_assistant(config_dir=tmp_storage) as hass: + async with async_test_home_assistant(config_dir=tmp_storage.strpath) as hass: storage_key = "core.anything" store = storage.Store( hass, MOCK_VERSION_2, storage_key, minor_version=MOCK_MINOR_VERSION_1 @@ -730,7 +730,7 @@ async def test_loading_corrupt_file_known_domain( loop = asyncio.get_running_loop() tmp_storage = await loop.run_in_executor(None, tmpdir.mkdir, "temp_storage") - async with async_test_home_assistant(config_dir=tmp_storage) as hass: + async with async_test_home_assistant(config_dir=tmp_storage.strpath) as hass: hass.config.components.add("testdomain") storage_key = "testdomain.testkey" @@ -787,7 +787,7 @@ async def test_os_error_is_fatal(tmpdir: py.path.local) -> None: """Test OSError during load is fatal.""" loop = asyncio.get_running_loop() tmp_storage = await loop.run_in_executor(None, tmpdir.mkdir, "temp_storage") - async with async_test_home_assistant(config_dir=tmp_storage) as hass: + async with async_test_home_assistant(config_dir=tmp_storage.strpath) as hass: store = storage.Store( hass, MOCK_VERSION_2, MOCK_KEY, minor_version=MOCK_MINOR_VERSION_1 ) @@ -817,7 +817,7 @@ async def test_json_load_failure(tmpdir: py.path.local) -> None: """Test json load raising HomeAssistantError.""" loop = asyncio.get_running_loop() tmp_storage = await loop.run_in_executor(None, tmpdir.mkdir, "temp_storage") - async with async_test_home_assistant(config_dir=tmp_storage) as hass: + async with async_test_home_assistant(config_dir=tmp_storage.strpath) as hass: store = storage.Store( hass, MOCK_VERSION_2, MOCK_KEY, minor_version=MOCK_MINOR_VERSION_1 ) @@ -883,7 +883,7 @@ async def test_store_manager_caching( config_dir = await loop.run_in_executor(None, _setup_mock_storage) - async with async_test_home_assistant(config_dir=config_dir) as hass: + async with async_test_home_assistant(config_dir=config_dir.strpath) as hass: store_manager = storage.get_internal_store_manager(hass) assert ( store_manager.async_fetch("integration1") is None @@ -957,7 +957,7 @@ async def test_store_manager_caching( await hass.async_stop(force=True) - async with async_test_home_assistant(config_dir=config_dir) as hass: + async with async_test_home_assistant(config_dir=config_dir.strpath) as hass: store_manager = storage.get_internal_store_manager(hass) assert store_manager.async_fetch("integration1") is None assert store_manager.async_fetch("integration2") is None @@ -992,7 +992,7 @@ async def test_store_manager_caching( # Now make sure everything still works when we do not # manually load the storage manager - async with async_test_home_assistant(config_dir=config_dir) as hass: + async with async_test_home_assistant(config_dir=config_dir.strpath) as hass: integration1 = storage.Store(hass, 1, "integration1") assert await integration1.async_load() == {"integration1": "updated"} await integration1.async_save({"integration1": "updated2"}) @@ -1006,7 +1006,7 @@ async def test_store_manager_caching( await hass.async_stop(force=True) # Now remove the stores - async with async_test_home_assistant(config_dir=config_dir) as hass: + async with async_test_home_assistant(config_dir=config_dir.strpath) as hass: store_manager = storage.get_internal_store_manager(hass) await store_manager.async_initialize() await store_manager.async_preload(["integration1", "integration2"]) @@ -1031,7 +1031,7 @@ async def test_store_manager_caching( await hass.async_stop(force=True) # Now make sure the stores are removed and another run works - async with async_test_home_assistant(config_dir=config_dir) as hass: + async with async_test_home_assistant(config_dir=config_dir.strpath) as hass: store_manager = storage.get_internal_store_manager(hass) await store_manager.async_initialize() await store_manager.async_preload(["integration1"]) @@ -1058,7 +1058,7 @@ async def test_store_manager_sub_dirs(tmpdir: py.path.local) -> None: config_dir = await loop.run_in_executor(None, _setup_mock_storage) - async with async_test_home_assistant(config_dir=config_dir) as hass: + async with async_test_home_assistant(config_dir=config_dir.strpath) as hass: store_manager = storage.get_internal_store_manager(hass) await store_manager.async_initialize() assert store_manager.async_fetch("subdir/integration1") is None @@ -1087,7 +1087,7 @@ async def test_store_manager_cleanup_after_started( config_dir = await loop.run_in_executor(None, _setup_mock_storage) - async with async_test_home_assistant(config_dir=config_dir) as hass: + async with async_test_home_assistant(config_dir=config_dir.strpath) as hass: hass.set_state(CoreState.not_running) store_manager = storage.get_internal_store_manager(hass) await store_manager.async_initialize() @@ -1137,7 +1137,7 @@ async def test_store_manager_cleanup_after_stop( config_dir = await loop.run_in_executor(None, _setup_mock_storage) - async with async_test_home_assistant(config_dir=config_dir) as hass: + async with async_test_home_assistant(config_dir=config_dir.strpath) as hass: hass.set_state(CoreState.not_running) store_manager = storage.get_internal_store_manager(hass) await store_manager.async_initialize() From efde8400e26171f201257c4af74e444a2155edf4 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 4 Apr 2024 12:08:37 +0200 Subject: [PATCH 258/967] Improve generic event typing [rfxtrx] (#114733) --- homeassistant/components/rfxtrx/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 4cacb27b49a..6f0e5932adc 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -25,7 +25,10 @@ from homeassistant.const import ( from homeassistant.core import Event, HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, device_registry as dr -from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.device_registry import ( + DeviceInfo, + EventDeviceRegistryUpdatedData, +) from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -259,7 +262,7 @@ async def async_setup_internal(hass: HomeAssistant, entry: ConfigEntry) -> None: devices.pop(device_id) @callback - def _updated_device(event: Event) -> None: + def _updated_device(event: Event[EventDeviceRegistryUpdatedData]) -> None: if event.data["action"] != "remove": return device_entry = device_registry.deleted_devices[event.data["device_id"]] From 28dc77a72df705116c501d57426e30be05d84562 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 4 Apr 2024 13:45:44 +0200 Subject: [PATCH 259/967] Avoid blocking IO in downloader initialization (#114841) * Avoid blocking IO in downloader initialization * Avoid blocking IO in downloader initialization --- homeassistant/components/downloader/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/downloader/__init__.py b/homeassistant/components/downloader/__init__.py index 94d243e2cf2..3ca503a2167 100644 --- a/homeassistant/components/downloader/__init__.py +++ b/homeassistant/components/downloader/__init__.py @@ -83,7 +83,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not os.path.isabs(download_path): download_path = hass.config.path(download_path) - if not os.path.isdir(download_path): + if not await hass.async_add_executor_job(os.path.isdir, download_path): _LOGGER.error( "Download path %s does not exist. File Downloader not active", download_path ) From e845d127339e3e01ce2485d09719c8149f312c4a Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Thu, 4 Apr 2024 13:25:35 +0100 Subject: [PATCH 260/967] Pin systembridgemodels to 4.0.4 (#114842) --- homeassistant/components/system_bridge/manifest.json | 2 +- requirements_all.txt | 3 +++ requirements_test_all.txt | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/system_bridge/manifest.json b/homeassistant/components/system_bridge/manifest.json index b4365fda778..aea66d22f62 100644 --- a/homeassistant/components/system_bridge/manifest.json +++ b/homeassistant/components/system_bridge/manifest.json @@ -10,6 +10,6 @@ "iot_class": "local_push", "loggers": ["systembridgeconnector"], "quality_scale": "silver", - "requirements": ["systembridgeconnector==4.0.3"], + "requirements": ["systembridgeconnector==4.0.3", "systembridgemodels==4.0.4"], "zeroconf": ["_system-bridge._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 2858a15fcfe..f38c9e38b2a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2661,6 +2661,9 @@ synology-srm==0.2.0 # homeassistant.components.system_bridge systembridgeconnector==4.0.3 +# homeassistant.components.system_bridge +systembridgemodels==4.0.4 + # homeassistant.components.tailscale tailscale==0.6.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fdb15008e70..8f29842a6eb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2056,6 +2056,9 @@ switchbot-api==2.0.0 # homeassistant.components.system_bridge systembridgeconnector==4.0.3 +# homeassistant.components.system_bridge +systembridgemodels==4.0.4 + # homeassistant.components.tailscale tailscale==0.6.0 From a9d43db3151be9688b3ed10eb5927e3a4771cd52 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Apr 2024 08:35:07 -1000 Subject: [PATCH 261/967] Avoid linear search to clear a config entry in the device registry (#114802) --- homeassistant/helpers/device_registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 8d5fe3f2f94..2c160262c50 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -1007,7 +1007,7 @@ class DeviceRegistry(BaseRegistry): def async_clear_config_entry(self, config_entry_id: str) -> None: """Clear config entry from registry entries.""" now_time = time.time() - for device in list(self.devices.values()): + for device in self.devices.get_devices_for_config_entry_id(config_entry_id): self.async_update_device(device.id, remove_config_entry_id=config_entry_id) for deleted_device in list(self.deleted_devices.values()): config_entries = deleted_device.config_entries From c798128ef10b640efcc796a5a3170a8a18e1228c Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 4 Apr 2024 21:01:15 +0200 Subject: [PATCH 262/967] Update frontend to 20240404.0 (#114859) --- 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 1890572bf5a..75c630b4471 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==20240403.1"] + "requirements": ["home-assistant-frontend==20240404.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b5ea145844e..b47828664f9 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -30,7 +30,7 @@ habluetooth==2.4.2 hass-nabucasa==0.79.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240403.1 +home-assistant-frontend==20240404.0 home-assistant-intents==2024.4.3 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index f38c9e38b2a..72e29814e9a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1081,7 +1081,7 @@ hole==0.8.0 holidays==0.46 # homeassistant.components.frontend -home-assistant-frontend==20240403.1 +home-assistant-frontend==20240404.0 # homeassistant.components.conversation home-assistant-intents==2024.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8f29842a6eb..26a316486b7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -880,7 +880,7 @@ hole==0.8.0 holidays==0.46 # homeassistant.components.frontend -home-assistant-frontend==20240403.1 +home-assistant-frontend==20240404.0 # homeassistant.components.conversation home-assistant-intents==2024.4.3 From c7d1319acfd86594888288fae9e22ad2d755f6a2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Apr 2024 09:26:58 -1000 Subject: [PATCH 263/967] Avoid linear search in owntracks to find devices (#114812) --- homeassistant/components/owntracks/device_tracker.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/owntracks/device_tracker.py b/homeassistant/components/owntracks/device_tracker.py index 8471f734196..31af3d845ae 100644 --- a/homeassistant/components/owntracks/device_tracker.py +++ b/homeassistant/components/owntracks/device_tracker.py @@ -30,9 +30,8 @@ async def async_setup_entry( dev_reg = dr.async_get(hass) dev_ids = { identifier[1] - for device in dev_reg.devices.values() + for device in dev_reg.devices.get_devices_for_config_entry_id(entry.entry_id) for identifier in device.identifiers - if identifier[0] == OT_DOMAIN } entities = [] From a83d5e40715bfd361c2518c01173c59de6f07a55 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Apr 2024 09:27:14 -1000 Subject: [PATCH 264/967] Avoid linear search in geofency to find devices (#114810) --- homeassistant/components/geofency/device_tracker.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/geofency/device_tracker.py b/homeassistant/components/geofency/device_tracker.py index 178c72d2071..b72ad4bc04c 100644 --- a/homeassistant/components/geofency/device_tracker.py +++ b/homeassistant/components/geofency/device_tracker.py @@ -38,9 +38,10 @@ async def async_setup_entry( dev_reg = dr.async_get(hass) dev_ids = { identifier[1] - for device in dev_reg.devices.values() + for device in dev_reg.devices.get_devices_for_config_entry_id( + config_entry.entry_id + ) for identifier in device.identifiers - if identifier[0] == GF_DOMAIN } if dev_ids: From 9189cd5ec223aa6ca167640658f58e1e5b36841b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Apr 2024 09:27:24 -1000 Subject: [PATCH 265/967] Avoid linear search in gpslogger to find devices (#114811) --- homeassistant/components/gpslogger/device_tracker.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/gpslogger/device_tracker.py b/homeassistant/components/gpslogger/device_tracker.py index 4a28606662f..b1c7ad9091f 100644 --- a/homeassistant/components/gpslogger/device_tracker.py +++ b/homeassistant/components/gpslogger/device_tracker.py @@ -48,9 +48,8 @@ async def async_setup_entry( dev_reg = dr.async_get(hass) dev_ids = { identifier[1] - for device in dev_reg.devices.values() + for device in dev_reg.devices.get_devices_for_config_entry_id(entry.entry_id) for identifier in device.identifiers - if identifier[0] == GPL_DOMAIN } if not dev_ids: return From 56ef9500f7621d7f114e09c68cfb373e4eae100a Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 4 Apr 2024 21:27:44 +0200 Subject: [PATCH 266/967] Use EventStateChangedData type when firing state changed event (#114740) --- homeassistant/core.py | 22 ++++++++++++++++++++-- homeassistant/helpers/event.py | 20 +++----------------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 9a26a971f64..6e74c8bd010 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -161,6 +161,14 @@ class ConfigSource(enum.StrEnum): YAML = "yaml" +class EventStateChangedData(TypedDict): + """EventStateChanged data.""" + + entity_id: str + old_state: State | None + new_state: State | None + + # SOURCE_* are deprecated as of Home Assistant 2022.2, use ConfigSource instead _DEPRECATED_SOURCE_DISCOVERED = DeprecatedConstantEnum( ConfigSource.DISCOVERED, "2025.1" @@ -2019,9 +2027,14 @@ class StateMachine: return False old_state.expire() + state_changed_data: EventStateChangedData = { + "entity_id": entity_id, + "old_state": old_state, + "new_state": None, + } self._bus._async_fire( # pylint: disable=protected-access EVENT_STATE_CHANGED, - {"entity_id": entity_id, "old_state": old_state, "new_state": None}, + state_changed_data, context=context, ) return True @@ -2170,9 +2183,14 @@ class StateMachine: if old_state is not None: old_state.expire() self._states[entity_id] = state + state_changed_data: EventStateChangedData = { + "entity_id": entity_id, + "old_state": old_state, + "new_state": state, + } self._bus._async_fire( # pylint: disable=protected-access EVENT_STATE_CHANGED, - {"entity_id": entity_id, "old_state": old_state, "new_state": state}, + state_changed_data, context=context, time_fired=timestamp, ) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 749c6d3e6e4..cfbc40e7ed0 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -11,15 +11,7 @@ import functools as ft import logging from random import randint import time -from typing import ( - TYPE_CHECKING, - Any, - Concatenate, - Generic, - ParamSpec, - TypedDict, - TypeVar, -) +from typing import TYPE_CHECKING, Any, Concatenate, Generic, ParamSpec, TypeVar import attr @@ -33,6 +25,8 @@ from homeassistant.const import ( from homeassistant.core import ( CALLBACK_TYPE, Event, + # Explicit reexport of 'EventStateChangedData' for backwards compatibility + EventStateChangedData as EventStateChangedData, # noqa: PLC0414 HassJob, HassJobType, HomeAssistant, @@ -163,14 +157,6 @@ class TrackTemplateResult: result: Any -class EventStateChangedData(TypedDict): - """EventStateChanged data.""" - - entity_id: str - old_state: State | None - new_state: State | None - - def threaded_listener_factory( async_factory: Callable[Concatenate[HomeAssistant, _P], Any], ) -> Callable[Concatenate[HomeAssistant, _P], CALLBACK_TYPE]: From cceea6dac2bbff2958996f77be4352180b6deb6a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Apr 2024 09:30:10 -1000 Subject: [PATCH 267/967] Refactor ConfigStore to avoid needing to pass config_dir (#114827) Co-authored-by: Erik --- homeassistant/core.py | 16 +++++++++++----- homeassistant/helpers/storage.py | 13 +++++-------- tests/test_core.py | 3 +++ 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 6e74c8bd010..f8540ae7e70 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -405,6 +405,7 @@ class HomeAssistant: self.services = ServiceRegistry(self) self.states = StateMachine(self.bus, self.loop) self.config = Config(self, config_dir) + self.config.async_initialize() self.components = loader.Components(self) self.helpers = loader.Helpers(self) self.state: CoreState = CoreState.not_running @@ -2600,12 +2601,12 @@ class ServiceRegistry: class Config: """Configuration settings for Home Assistant.""" + _store: Config._ConfigStore + def __init__(self, hass: HomeAssistant, config_dir: str) -> None: """Initialize a new config object.""" self.hass = hass - self._store = self._ConfigStore(self.hass, config_dir) - self.latitude: float = 0 self.longitude: float = 0 @@ -2656,6 +2657,13 @@ class Config: # If Home Assistant is running in safe mode self.safe_mode: bool = False + def async_initialize(self) -> None: + """Finish initializing a config object. + + This must be called before the config object is used. + """ + self._store = self._ConfigStore(self.hass) + def distance(self, lat: float, lon: float) -> float | None: """Calculate distance from Home Assistant. @@ -2862,7 +2870,6 @@ class Config: "country": self.country, "language": self.language, } - await self._store.async_save(data) # Circular dependency prevents us from generating the class at top level @@ -2872,7 +2879,7 @@ class Config: class _ConfigStore(Store[dict[str, Any]]): """Class to help storing Config data.""" - def __init__(self, hass: HomeAssistant, config_dir: str) -> None: + def __init__(self, hass: HomeAssistant) -> None: """Initialize storage class.""" super().__init__( hass, @@ -2881,7 +2888,6 @@ class Config: private=True, atomic_writes=True, minor_version=CORE_STORAGE_MINOR_VERSION, - config_dir=config_dir, ) self._original_unit_system: str | None = None # from old store 1.1 diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index c2047328013..93594875ac2 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -90,9 +90,7 @@ async def async_migrator( return config -def get_internal_store_manager( - hass: HomeAssistant, config_dir: str | None = None -) -> _StoreManager: +def get_internal_store_manager(hass: HomeAssistant) -> _StoreManager: """Get the store manager. This function is not part of the API and should only be @@ -100,7 +98,7 @@ def get_internal_store_manager( guaranteed to be stable. """ if STORAGE_MANAGER not in hass.data: - manager = _StoreManager(hass, config_dir or hass.config.config_dir) + manager = _StoreManager(hass) hass.data[STORAGE_MANAGER] = manager return hass.data[STORAGE_MANAGER] @@ -111,13 +109,13 @@ class _StoreManager: The store manager is used to cache and manage storage files. """ - def __init__(self, hass: HomeAssistant, config_dir: str) -> None: + def __init__(self, hass: HomeAssistant) -> None: """Initialize storage manager class.""" self._hass = hass self._invalidated: set[str] = set() self._files: set[str] | None = None self._data_preload: dict[str, json_util.JsonValueType] = {} - self._storage_path: Path = Path(config_dir).joinpath(STORAGE_DIR) + self._storage_path: Path = Path(hass.config.config_dir).joinpath(STORAGE_DIR) self._cancel_cleanup: asyncio.TimerHandle | None = None async def async_initialize(self) -> None: @@ -246,7 +244,6 @@ class Store(Generic[_T]): encoder: type[JSONEncoder] | None = None, minor_version: int = 1, read_only: bool = False, - config_dir: str | None = None, ) -> None: """Initialize storage class.""" self.version = version @@ -263,7 +260,7 @@ class Store(Generic[_T]): self._atomic_writes = atomic_writes self._read_only = read_only self._next_write_time = 0.0 - self._manager = get_internal_store_manager(hass, config_dir) + self._manager = get_internal_store_manager(hass) @cached_property def path(self): diff --git a/tests/test_core.py b/tests/test_core.py index a0a197096cd..905d8efe6de 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -2288,6 +2288,7 @@ async def test_additional_data_in_core_config( ) -> None: """Test that we can handle additional data in core configuration.""" config = ha.Config(hass, "/test/ha-config") + config.async_initialize() hass_storage[ha.CORE_STORAGE_KEY] = { "version": 1, "data": {"location_name": "Test Name", "additional_valid_key": "value"}, @@ -2301,6 +2302,7 @@ async def test_incorrect_internal_external_url( ) -> None: """Test that we warn when detecting invalid internal/external url.""" config = ha.Config(hass, "/test/ha-config") + config.async_initialize() hass_storage[ha.CORE_STORAGE_KEY] = { "version": 1, @@ -2314,6 +2316,7 @@ async def test_incorrect_internal_external_url( assert "Invalid internal_url set" not in caplog.text config = ha.Config(hass, "/test/ha-config") + config.async_initialize() hass_storage[ha.CORE_STORAGE_KEY] = { "version": 1, From 1c2499b03ae256a524a595cfefbac439f2babc25 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Thu, 4 Apr 2024 14:45:27 -0500 Subject: [PATCH 268/967] Add "conversation" key to translations (#114887) * Use translated trigger response * Use conversation key instead --- .../components/conversation/default_agent.py | 28 +++++++++++++---- .../components/conversation/strings.json | 5 ++++ script/hassfest/translations.py | 5 ++++ tests/components/conversation/test_trigger.py | 30 +++++++++++++++++++ 4 files changed, 63 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 32ab7924916..8202814d347 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -269,20 +269,38 @@ class DefaultAgent(ConversationEntity): for trigger_id, trigger_result in result.matched_triggers.items() ] - # Use last non-empty result as response. + # Use first non-empty result as response. # # There may be multiple copies of a trigger running when editing in # the UI, so it's critical that we filter out empty responses here. response_text: str | None = None + response_set_by_trigger = False for trigger_future in asyncio.as_completed(trigger_callbacks): - if trigger_response := await trigger_future: - response_text = trigger_response - break + trigger_response = await trigger_future + if trigger_response is None: + continue + + response_text = trigger_response + response_set_by_trigger = True + break # Convert to conversation result response = intent.IntentResponse(language=language) response.response_type = intent.IntentResponseType.ACTION_DONE - response.async_set_speech(response_text or "Done") + + if response_set_by_trigger: + # Response was explicitly set to empty + response_text = response_text or "" + elif not response_text: + # Use translated acknowledgment for pipeline language + translations = await translation.async_get_translations( + self.hass, language, DOMAIN, [DOMAIN] + ) + response_text = translations.get( + f"component.{DOMAIN}.agent.done", "Done" + ) + + response.async_set_speech(response_text) return ConversationResult(response=response) diff --git a/homeassistant/components/conversation/strings.json b/homeassistant/components/conversation/strings.json index 3150623ba65..e3c3aa5af20 100644 --- a/homeassistant/components/conversation/strings.json +++ b/homeassistant/components/conversation/strings.json @@ -37,5 +37,10 @@ } } } + }, + "conversation": { + "agent": { + "done": "Done" + } } } diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index 724f65eafb6..6c20246b396 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -360,6 +360,11 @@ def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema: }, slug_validator=translation_key_validator, ), + vol.Optional("conversation"): { + vol.Required("agent"): { + vol.Required("done"): translation_value_validator, + }, + }, } ) diff --git a/tests/components/conversation/test_trigger.py b/tests/components/conversation/test_trigger.py index 9e78b9b6180..83f4e97c853 100644 --- a/tests/components/conversation/test_trigger.py +++ b/tests/components/conversation/test_trigger.py @@ -104,6 +104,36 @@ async def test_response(hass: HomeAssistant, setup_comp) -> None: assert service_response["response"]["speech"]["plain"]["speech"] == response +async def test_empty_response(hass: HomeAssistant, setup_comp) -> None: + """Test the conversation response action with an empty response.""" + assert await async_setup_component( + hass, + "automation", + { + "automation": { + "trigger": { + "platform": "conversation", + "command": ["Open the pod bay door Hal"], + }, + "action": { + "set_conversation_response": "", + }, + } + }, + ) + + service_response = await hass.services.async_call( + "conversation", + "process", + { + "text": "Open the pod bay door Hal", + }, + blocking=True, + return_response=True, + ) + assert service_response["response"]["speech"]["plain"]["speech"] == "" + + async def test_response_same_sentence(hass: HomeAssistant, calls, setup_comp) -> None: """Test the conversation response action with multiple triggers using the same sentence.""" assert await async_setup_component( From 7c95ecff20d4ee529b1bf8d709f4e10bfa4982b6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 4 Apr 2024 21:51:44 +0200 Subject: [PATCH 269/967] Validate unique_id in entity registry (#114648) Co-authored-by: Shay Levy Co-authored-by: J. Nick Koston --- homeassistant/helpers/entity_registry.py | 135 +++++++++++++++------ tests/helpers/test_entity_registry.py | 142 +++++++++++++++++++++++ 2 files changed, 244 insertions(+), 33 deletions(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index a0e7b669418..b1e92f51c2c 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -10,7 +10,7 @@ timer. from __future__ import annotations -from collections.abc import Callable, Iterable, KeysView, Mapping +from collections.abc import Callable, Hashable, Iterable, KeysView, Mapping from datetime import datetime, timedelta from enum import StrEnum from functools import cached_property @@ -45,6 +45,7 @@ from homeassistant.core import ( valid_entity_id, ) from homeassistant.exceptions import MaxLengthExceeded +from homeassistant.loader import async_suggest_report_issue from homeassistant.util import slugify, uuid as uuid_util from homeassistant.util.json import format_unserializable_data from homeassistant.util.read_only_dict import ReadOnlyDict @@ -606,6 +607,56 @@ class EntityRegistryItems(BaseRegistryItems[RegistryEntry]): return [data[key] for key in self._labels_index.get(label, ())] +def _validate_item( + hass: HomeAssistant, + domain: str, + platform: str, + unique_id: str | Hashable | UndefinedType | Any, + *, + disabled_by: RegistryEntryDisabler | None | UndefinedType = None, + entity_category: EntityCategory | None | UndefinedType = None, + hidden_by: RegistryEntryHider | None | UndefinedType = None, +) -> None: + """Validate entity registry item.""" + if unique_id is not UNDEFINED and not isinstance(unique_id, Hashable): + raise TypeError(f"unique_id must be a string, got {unique_id}") + if unique_id is not UNDEFINED and not isinstance(unique_id, str): + # In HA Core 2025.4, we should fail if unique_id is not a string + report_issue = async_suggest_report_issue(hass, integration_domain=platform) + _LOGGER.error( + ("'%s' from integration %s has a non string unique_id" " '%s', please %s"), + domain, + platform, + unique_id, + report_issue, + ) + return + if ( + disabled_by + and disabled_by is not UNDEFINED + and not isinstance(disabled_by, RegistryEntryDisabler) + ): + raise ValueError( + f"disabled_by must be a RegistryEntryDisabler value, got {disabled_by}" + ) + if ( + entity_category + and entity_category is not UNDEFINED + and not isinstance(entity_category, EntityCategory) + ): + raise ValueError( + f"entity_category must be a valid EntityCategory instance, got {entity_category}" + ) + if ( + hidden_by + and hidden_by is not UNDEFINED + and not isinstance(hidden_by, RegistryEntryHider) + ): + raise ValueError( + f"hidden_by must be a RegistryEntryHider value, got {hidden_by}" + ) + + class EntityRegistry(BaseRegistry): """Class to hold a registry of entities.""" @@ -764,6 +815,16 @@ class EntityRegistry(BaseRegistry): unit_of_measurement=unit_of_measurement, ) + _validate_item( + self.hass, + domain, + platform, + disabled_by=disabled_by, + entity_category=entity_category, + hidden_by=hidden_by, + unique_id=unique_id, + ) + entity_registry_id: str | None = None deleted_entity = self.deleted_entities.pop((domain, platform, unique_id), None) if deleted_entity is not None: @@ -776,11 +837,6 @@ class EntityRegistry(BaseRegistry): known_object_ids, ) - if disabled_by and not isinstance(disabled_by, RegistryEntryDisabler): - raise ValueError("disabled_by must be a RegistryEntryDisabler value") - if hidden_by and not isinstance(hidden_by, RegistryEntryHider): - raise ValueError("hidden_by must be a RegistryEntryHider value") - if ( disabled_by is None and config_entry @@ -789,13 +845,6 @@ class EntityRegistry(BaseRegistry): ): disabled_by = RegistryEntryDisabler.INTEGRATION - if ( - entity_category - and entity_category is not UNDEFINED - and not isinstance(entity_category, EntityCategory) - ): - raise ValueError("entity_category must be a valid EntityCategory instance") - def none_if_undefined(value: T | UndefinedType) -> T | None: """Return None if value is UNDEFINED, otherwise return value.""" return None if value is UNDEFINED else value @@ -954,26 +1003,6 @@ class EntityRegistry(BaseRegistry): new_values: dict[str, Any] = {} # Dict with new key/value pairs old_values: dict[str, Any] = {} # Dict with old key/value pairs - if ( - disabled_by - and disabled_by is not UNDEFINED - and not isinstance(disabled_by, RegistryEntryDisabler) - ): - raise ValueError("disabled_by must be a RegistryEntryDisabler value") - if ( - hidden_by - and hidden_by is not UNDEFINED - and not isinstance(hidden_by, RegistryEntryHider) - ): - raise ValueError("hidden_by must be a RegistryEntryHider value") - - if ( - entity_category - and entity_category is not UNDEFINED - and not isinstance(entity_category, EntityCategory) - ): - raise ValueError("entity_category must be a valid EntityCategory instance") - for attr_name, value in ( ("aliases", aliases), ("area_id", area_id), @@ -1002,6 +1031,18 @@ class EntityRegistry(BaseRegistry): new_values[attr_name] = value old_values[attr_name] = getattr(old, attr_name) + # Only validate if data has changed + if new_values or new_unique_id is not UNDEFINED: + _validate_item( + self.hass, + old.domain, + old.platform, + disabled_by=disabled_by, + entity_category=entity_category, + hidden_by=hidden_by, + unique_id=new_unique_id, + ) + if new_entity_id is not UNDEFINED and new_entity_id != old.entity_id: if not self._entity_id_available(new_entity_id, None): raise ValueError("Entity with this ID is already registered") @@ -1170,6 +1211,27 @@ class EntityRegistry(BaseRegistry): if entity["entity_category"] == "system": entity["entity_category"] = None + try: + domain = split_entity_id(entity["entity_id"])[0] + _validate_item( + self.hass, domain, entity["platform"], entity["unique_id"] + ) + except (TypeError, ValueError) as err: + report_issue = async_suggest_report_issue( + self.hass, integration_domain=entity["platform"] + ) + _LOGGER.error( + ( + "Entity registry entry '%s' from integration %s could not " + "be loaded: '%s', please %s" + ), + entity["entity_id"], + entity["platform"], + str(err), + report_issue, + ) + continue + entities[entity["entity_id"]] = RegistryEntry( aliases=set(entity["aliases"]), area_id=entity["area_id"], @@ -1205,6 +1267,13 @@ class EntityRegistry(BaseRegistry): unit_of_measurement=entity["unit_of_measurement"], ) for entity in data["deleted_entities"]: + try: + domain = split_entity_id(entity["entity_id"])[0] + _validate_item( + self.hass, domain, entity["platform"], entity["unique_id"] + ) + except (TypeError, ValueError): + continue key = ( split_entity_id(entity["entity_id"])[0], entity["platform"], diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 91c749a0d7f..60971d98df2 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -447,6 +447,116 @@ async def test_filter_on_load( assert entry_system_category.entity_category is None +@pytest.mark.parametrize("load_registries", [False]) +async def test_load_bad_data( + hass: HomeAssistant, + hass_storage: dict[str, Any], + caplog: pytest.LogCaptureFixture, +) -> None: + """Test loading invalid data.""" + hass_storage[er.STORAGE_KEY] = { + "version": er.STORAGE_VERSION_MAJOR, + "minor_version": er.STORAGE_VERSION_MINOR, + "data": { + "entities": [ + { + "aliases": [], + "area_id": None, + "capabilities": None, + "categories": {}, + "config_entry_id": None, + "device_class": None, + "device_id": None, + "disabled_by": None, + "entity_category": None, + "entity_id": "test.test1", + "has_entity_name": False, + "hidden_by": None, + "icon": None, + "id": "00001", + "labels": [], + "name": None, + "options": None, + "original_device_class": None, + "original_icon": None, + "original_name": None, + "platform": "super_platform", + "previous_unique_id": None, + "supported_features": 0, + "translation_key": None, + "unique_id": 123, # Should trigger warning + "unit_of_measurement": None, + }, + { + "aliases": [], + "area_id": None, + "capabilities": None, + "categories": {}, + "config_entry_id": None, + "device_class": None, + "device_id": None, + "disabled_by": None, + "entity_category": None, + "entity_id": "test.test2", + "has_entity_name": False, + "hidden_by": None, + "icon": None, + "id": "00002", + "labels": [], + "name": None, + "options": None, + "original_device_class": None, + "original_icon": None, + "original_name": None, + "platform": "super_platform", + "previous_unique_id": None, + "supported_features": 0, + "translation_key": None, + "unique_id": ["not", "valid"], # Should not load + "unit_of_measurement": None, + }, + ], + "deleted_entities": [ + { + "config_entry_id": None, + "entity_id": "test.test3", + "id": "00003", + "orphaned_timestamp": None, + "platform": "super_platform", + "unique_id": 234, # Should trigger warning + }, + { + "config_entry_id": None, + "entity_id": "test.test4", + "id": "00004", + "orphaned_timestamp": None, + "platform": "super_platform", + "unique_id": ["also", "not", "valid"], # Should not load + }, + ], + }, + } + + await er.async_load(hass) + registry = er.async_get(hass) + + assert len(registry.entities) == 1 + assert set(registry.entities.keys()) == {"test.test1"} + + assert len(registry.deleted_entities) == 1 + assert set(registry.deleted_entities.keys()) == {("test", "super_platform", 234)} + + assert ( + "'test' from integration super_platform has a non string unique_id '123', " + "please create a bug report" in caplog.text + ) + assert ( + "Entity registry entry 'test.test2' from integration super_platform could not " + "be loaded: 'unique_id must be a string, got ['not', 'valid']', please create " + "a bug report" in caplog.text + ) + + def test_async_get_entity_id(entity_registry: er.EntityRegistry) -> None: """Test that entity_id is returned.""" entry = entity_registry.async_get_or_create("light", "hue", "1234") @@ -1472,6 +1582,38 @@ 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: + """Test unique_id which is not hashable.""" + with pytest.raises(TypeError): + entity_registry.async_get_or_create("light", "hue", ["not", "valid"]) + + entity_id = entity_registry.async_get_or_create("light", "hue", "1234").entity_id + with pytest.raises(TypeError): + entity_registry.async_update_entity(entity_id, new_unique_id=["not", "valid"]) + + +async def test_unique_id_non_string( + hass: HomeAssistant, + 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) + assert ( + "'light' from integration hue has a non string unique_id '1234', " + "please create a bug report" in caplog.text + ) + + entity_id = entity_registry.async_get_or_create("light", "hue", "1234").entity_id + entity_registry.async_update_entity(entity_id, new_unique_id=2345) + assert ( + "'light' from integration hue has a non string unique_id '2345', " + "please create a bug report" in caplog.text + ) + + def test_migrate_entity_to_new_platform( hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: From 2cfc60b5b6864faba4984c785a60b4f0821364c9 Mon Sep 17 00:00:00 2001 From: tronikos Date: Thu, 4 Apr 2024 13:06:15 -0700 Subject: [PATCH 270/967] Bump opower to 0.4.3 (#114826) Co-authored-by: Joost Lekkerkerker --- 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 879aeb0327b..51ad669733b 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.2"] + "requirements": ["opower==0.4.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 72e29814e9a..313d4090407 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1486,7 +1486,7 @@ openwrt-luci-rpc==1.1.17 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.4.2 +opower==0.4.3 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 26a316486b7..5c9c81f15fa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1180,7 +1180,7 @@ openerz-api==0.3.0 openhomedevice==2.2.0 # homeassistant.components.opower -opower==0.4.2 +opower==0.4.3 # homeassistant.components.oralb oralb-ble==0.17.6 From 95ef087fa84de670d52b2fb9a7778bf19e89a545 Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Thu, 4 Apr 2024 16:34:23 -0400 Subject: [PATCH 271/967] Fix Sonos Tests failing intermittently on CI (#114873) --- tests/components/sonos/test_switch.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/sonos/test_switch.py b/tests/components/sonos/test_switch.py index d8499c50bb1..771045c13c1 100644 --- a/tests/components/sonos/test_switch.py +++ b/tests/components/sonos/test_switch.py @@ -122,6 +122,7 @@ async def test_switch_attributes( # Trigger subscription callback for speaker discovery await fire_zgs_event() + await hass.async_block_till_done(wait_background_tasks=True) status_light_state = hass.states.get(status_light.entity_id) assert status_light_state.state == STATE_ON From b0d1b6555d4d1de5ed6e178e9cbd84d9fca42be9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 4 Apr 2024 22:48:31 +0200 Subject: [PATCH 272/967] Address late review comments on homeworks PRs (#114867) --- .../components/homeworks/__init__.py | 32 +++++++++---------- .../components/homeworks/binary_sensor.py | 8 ++--- homeassistant/components/homeworks/button.py | 27 ++++++++++------ homeassistant/components/homeworks/light.py | 8 ++--- 4 files changed, 40 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/homeworks/__init__.py b/homeassistant/components/homeworks/__init__.py index a67e69bc9c6..fc787d98eea 100644 --- a/homeassistant/components/homeworks/__init__.py +++ b/homeassistant/components/homeworks/__init__.py @@ -2,10 +2,10 @@ from __future__ import annotations +import asyncio from collections.abc import Mapping from dataclasses import dataclass import logging -from time import sleep from typing import Any from pyhomeworks.pyhomeworks import HW_BUTTON_PRESSED, HW_BUTTON_RELEASED, Homeworks @@ -129,19 +129,6 @@ async def async_send_command(hass: HomeAssistant, data: Mapping[str, Any]) -> No return data return None - def send_commands(controller: Homeworks, commands: list[str]) -> None: - """Send commands to controller.""" - _LOGGER.debug("Send commands: %s", commands) - for command in commands: - if command.lower().startswith("delay"): - delay = int(command.partition(" ")[2]) - _LOGGER.debug("Sleeping for %s ms", delay) - sleep(delay / 1000) - else: - _LOGGER.debug("Sending command '%s'", command) - # pylint: disable-next=protected-access - controller._send(command) - homeworks_data = get_homeworks_data(data[CONF_CONTROLLER_ID]) if not homeworks_data: raise ServiceValidationError( @@ -153,9 +140,20 @@ async def async_send_command(hass: HomeAssistant, data: Mapping[str, Any]) -> No }, ) - await hass.async_add_executor_job( - send_commands, homeworks_data.controller, data[CONF_COMMAND] - ) + commands = data[CONF_COMMAND] + _LOGGER.debug("Send commands: %s", commands) + for command in commands: + if command.lower().startswith("delay"): + delay = int(command.partition(" ")[2]) + _LOGGER.debug("Sleeping for %s ms", delay) + await asyncio.sleep(delay / 1000) + else: + _LOGGER.debug("Sending command '%s'", command) + await hass.async_add_executor_job( + # pylint: disable-next=protected-access + homeworks_data.controller._send, + command, + ) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: diff --git a/homeassistant/components/homeworks/binary_sensor.py b/homeassistant/components/homeworks/binary_sensor.py index 9773411d26d..9a9f7086ba5 100644 --- a/homeassistant/components/homeworks/binary_sensor.py +++ b/homeassistant/components/homeworks/binary_sensor.py @@ -36,12 +36,12 @@ async def async_setup_entry( data: HomeworksData = hass.data[DOMAIN][entry.entry_id] controller = data.controller controller_id = entry.options[CONF_CONTROLLER_ID] - devs = [] + entities = [] for keypad in entry.options.get(CONF_KEYPADS, []): for button in keypad[CONF_BUTTONS]: if not button[CONF_LED]: continue - dev = HomeworksBinarySensor( + entity = HomeworksBinarySensor( controller, data.keypads[keypad[CONF_ADDR]], controller_id, @@ -50,8 +50,8 @@ async def async_setup_entry( button[CONF_NAME], button[CONF_NUMBER], ) - devs.append(dev) - async_add_entities(devs, True) + entities.append(entity) + async_add_entities(entities, True) class HomeworksBinarySensor(HomeworksEntity, BinarySensorEntity): diff --git a/homeassistant/components/homeworks/button.py b/homeassistant/components/homeworks/button.py index c8cb616d95b..2f3ba482717 100644 --- a/homeassistant/components/homeworks/button.py +++ b/homeassistant/components/homeworks/button.py @@ -2,7 +2,7 @@ from __future__ import annotations -from time import sleep +import asyncio from pyhomeworks.pyhomeworks import Homeworks @@ -32,10 +32,10 @@ async def async_setup_entry( data: HomeworksData = hass.data[DOMAIN][entry.entry_id] controller = data.controller controller_id = entry.options[CONF_CONTROLLER_ID] - devs = [] + entities = [] for keypad in entry.options.get(CONF_KEYPADS, []): for button in keypad[CONF_BUTTONS]: - dev = HomeworksButton( + entity = HomeworksButton( controller, controller_id, keypad[CONF_ADDR], @@ -44,8 +44,8 @@ async def async_setup_entry( button[CONF_NUMBER], button[CONF_RELEASE_DELAY], ) - devs.append(dev) - async_add_entities(devs, True) + entities.append(entity) + async_add_entities(entities, True) class HomeworksButton(HomeworksEntity, ButtonEntity): @@ -68,12 +68,19 @@ class HomeworksButton(HomeworksEntity, ButtonEntity): ) self._release_delay = release_delay - def press(self) -> None: + async def async_press(self) -> None: """Press the button.""" - # pylint: disable-next=protected-access - self._controller._send(f"KBP, {self._addr}, {self._idx}") + await self.hass.async_add_executor_job( + # pylint: disable-next=protected-access + self._controller._send, + f"KBP, {self._addr}, {self._idx}", + ) if not self._release_delay: return - sleep(self._release_delay) + await asyncio.sleep(self._release_delay) # pylint: disable-next=protected-access - self._controller._send(f"KBR, {self._addr}, {self._idx}") + await self.hass.async_add_executor_job( + # pylint: disable-next=protected-access + self._controller._send, + f"KBR, {self._addr}, {self._idx}", + ) diff --git a/homeassistant/components/homeworks/light.py b/homeassistant/components/homeworks/light.py index 3e3c199c75c..20ae08017d3 100644 --- a/homeassistant/components/homeworks/light.py +++ b/homeassistant/components/homeworks/light.py @@ -28,17 +28,17 @@ async def async_setup_entry( data: HomeworksData = hass.data[DOMAIN][entry.entry_id] controller = data.controller controller_id = entry.options[CONF_CONTROLLER_ID] - devs = [] + entities = [] for dimmer in entry.options.get(CONF_DIMMERS, []): - dev = HomeworksLight( + entity = HomeworksLight( controller, controller_id, dimmer[CONF_ADDR], dimmer[CONF_NAME], dimmer[CONF_RATE], ) - devs.append(dev) - async_add_entities(devs, True) + entities.append(entity) + async_add_entities(entities, True) class HomeworksLight(HomeworksEntity, LightEntity): From e0e54ab9d380e8c5369cc36203fdde3f718fdba8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Apr 2024 11:43:15 -1000 Subject: [PATCH 273/967] Migrate more sonos tasks to use eager_start (#114697) --- homeassistant/components/sonos/__init__.py | 3 ++- homeassistant/components/sonos/speaker.py | 18 +++++++++++++----- tests/components/sonos/conftest.py | 2 +- tests/components/sonos/test_sensor.py | 1 - tests/components/sonos/test_switch.py | 3 +-- 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 028c412cd75..0eab8dcc779 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -230,7 +230,8 @@ class SonosDiscoveryManager: return self.hass.async_create_task( - self.async_add_speakers(zones_to_add, subscription, soco.uid) + self.async_add_speakers(zones_to_add, subscription, soco.uid), + eager_start=True, ) async def async_subscription_failed(now: datetime.datetime) -> None: diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index ebb2738c641..667e2bb405f 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -407,7 +407,9 @@ class SonosSpeaker: @callback def async_renew_failed(self, exception: Exception) -> None: """Handle a failed subscription renewal.""" - self.hass.async_create_task(self._async_renew_failed(exception)) + self.hass.async_create_task( + self._async_renew_failed(exception), eager_start=True + ) async def _async_renew_failed(self, exception: Exception) -> None: """Mark the speaker as offline after a subscription renewal failure. @@ -449,7 +451,9 @@ class SonosSpeaker: """Add the soco instance associated with the event to the callback.""" if "alarm_list_version" not in event.variables: return - self.hass.async_create_task(self.alarms.async_process_event(event, self)) + self.hass.async_create_task( + self.alarms.async_process_event(event, self), eager_start=True + ) @callback def async_dispatch_device_properties(self, event: SonosEvent) -> None: @@ -479,7 +483,9 @@ class SonosSpeaker: return if "container_update_i_ds" not in event.variables: return - self.hass.async_create_task(self.favorites.async_process_event(event, self)) + self.hass.async_create_task( + self.favorites.async_process_event(event, self), eager_start=True + ) @callback def async_dispatch_media_update(self, event: SonosEvent) -> None: @@ -601,7 +607,7 @@ class SonosSpeaker: self.available = True if not was_available: self.async_write_entity_states() - self.hass.async_create_task(self.async_subscribe()) + self.hass.async_create_task(self.async_subscribe(), eager_start=True) @callback def async_check_activity(self, now: datetime.datetime) -> None: @@ -818,7 +824,9 @@ class SonosSpeaker: if "zone_player_uui_ds_in_group" not in event.variables: return self.event_stats.process(event) - self.hass.async_create_task(self.create_update_groups_coro(event)) + self.hass.async_create_task( + self.create_update_groups_coro(event), eager_start=True + ) def create_update_groups_coro(self, event: SonosEvent | None = None) -> Coroutine: """Handle callback for topology change event.""" diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 576c9a80799..4c469028e9a 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -494,6 +494,6 @@ def zgs_event_fixture(hass: HomeAssistant, soco: SoCo, zgs_discovery: str): subscription: SonosMockSubscribe = soco.zoneGroupTopology.subscribe.return_value sub_callback = await subscription.wait_for_callback_to_be_set() sub_callback(event) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) return _wrapper diff --git a/tests/components/sonos/test_sensor.py b/tests/components/sonos/test_sensor.py index 1f4ba8d22cd..45068c01bc0 100644 --- a/tests/components/sonos/test_sensor.py +++ b/tests/components/sonos/test_sensor.py @@ -242,7 +242,6 @@ async def test_favorites_sensor( # Trigger subscription callback for speaker discovery await fire_zgs_event() - await hass.async_block_till_done(wait_background_tasks=True) favorites_updated_event = SonosMockEvent( soco, service, {"favorites_update_id": "2", "container_update_i_ds": "FV:2,2"} diff --git a/tests/components/sonos/test_switch.py b/tests/components/sonos/test_switch.py index 771045c13c1..eb31d991a3a 100644 --- a/tests/components/sonos/test_switch.py +++ b/tests/components/sonos/test_switch.py @@ -117,12 +117,11 @@ async def test_switch_attributes( hass, dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), ) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert m.called # Trigger subscription callback for speaker discovery await fire_zgs_event() - await hass.async_block_till_done(wait_background_tasks=True) status_light_state = hass.states.get(status_light.entity_id) assert status_light_state.state == STATE_ON From 0f0307906504dba934152ac4f2ff0dce6c6f209b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 4 Apr 2024 23:48:29 +0200 Subject: [PATCH 274/967] Update import for EventStateChangedData [i-z] (#114900) --- homeassistant/components/image/__init__.py | 3 +-- homeassistant/components/integration/sensor.py | 13 ++++++++----- homeassistant/components/knx/expose.py | 9 ++++++--- homeassistant/components/logbook/helpers.py | 6 ++---- .../manual_mqtt/alarm_control_panel.py | 3 +-- homeassistant/components/min_max/sensor.py | 7 ++----- .../components/mold_indicator/sensor.py | 13 ++++++++----- homeassistant/components/person/__init__.py | 6 ++---- homeassistant/components/plant/__init__.py | 13 ++++++++----- homeassistant/components/prometheus/__init__.py | 3 +-- .../components/purpleair/config_flow.py | 7 ++----- homeassistant/components/statistics/sensor.py | 2 +- homeassistant/components/switch/light.py | 7 ++----- homeassistant/components/switch_as_x/cover.py | 3 +-- homeassistant/components/switch_as_x/entity.py | 7 ++----- homeassistant/components/switch_as_x/lock.py | 3 +-- homeassistant/components/switch_as_x/valve.py | 3 +-- .../components/template/template_entity.py | 2 +- homeassistant/components/template/trigger.py | 10 ++++++++-- .../components/threshold/binary_sensor.py | 7 ++----- homeassistant/components/trend/binary_sensor.py | 7 ++----- .../components/universal/media_player.py | 3 +-- .../components/utility_meter/sensor.py | 9 +++++++-- .../components/websocket_api/commands.py | 2 +- .../components/websocket_api/messages.py | 3 +-- homeassistant/components/zha/entity.py | 7 ++----- homeassistant/components/zone/__init__.py | 17 +++++++++++------ homeassistant/components/zone/trigger.py | 14 +++++++++----- tests/helpers/test_event.py | 3 +-- 29 files changed, 95 insertions(+), 97 deletions(-) diff --git a/homeassistant/components/image/__init__.py b/homeassistant/components/image/__init__.py index 59e5ddceebf..5b0d1a2a330 100644 --- a/homeassistant/components/image/__init__.py +++ b/homeassistant/components/image/__init__.py @@ -18,7 +18,7 @@ import httpx from homeassistant.components.http import KEY_AUTHENTICATED, KEY_HASS, HomeAssistantView from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONTENT_TYPE_MULTIPART, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, @@ -27,7 +27,6 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import ( - EventStateChangedData, async_track_state_change_event, async_track_time_interval, ) diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index ed017a21527..cf9ba5f2950 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -28,7 +28,13 @@ from homeassistant.const import ( STATE_UNKNOWN, UnitOfTime, ) -from homeassistant.core import Event, HomeAssistant, State, callback +from homeassistant.core import ( + Event, + EventStateChangedData, + HomeAssistant, + State, + callback, +) from homeassistant.helpers import ( config_validation as cv, device_registry as dr, @@ -36,10 +42,7 @@ from homeassistant.helpers import ( ) from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( diff --git a/homeassistant/components/knx/expose.py b/homeassistant/components/knx/expose.py index 6e4a3b80f6e..12343f0dca7 100644 --- a/homeassistant/components/knx/expose.py +++ b/homeassistant/components/knx/expose.py @@ -18,11 +18,14 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import Event, HomeAssistant, State, callback -from homeassistant.helpers.event import ( +from homeassistant.core import ( + Event, EventStateChangedData, - async_track_state_change_event, + HomeAssistant, + State, + callback, ) +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, StateType from .const import CONF_RESPOND_TO_READ, KNX_ADDRESS diff --git a/homeassistant/components/logbook/helpers.py b/homeassistant/components/logbook/helpers.py index 1731fcaddd9..5c25056c041 100644 --- a/homeassistant/components/logbook/helpers.py +++ b/homeassistant/components/logbook/helpers.py @@ -17,6 +17,7 @@ from homeassistant.const import ( from homeassistant.core import ( CALLBACK_TYPE, Event, + EventStateChangedData, HomeAssistant, State, callback, @@ -24,10 +25,7 @@ from homeassistant.core import ( split_entity_id, ) from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) +from homeassistant.helpers.event import async_track_state_change_event from .const import ALWAYS_CONTINUOUS_DOMAINS, AUTOMATION_EVENTS, BUILT_IN_EVENTS, DOMAIN from .models import LogbookConfig diff --git a/homeassistant/components/manual_mqtt/alarm_control_panel.py b/homeassistant/components/manual_mqtt/alarm_control_panel.py index 0cd92b552c6..db81825d7b5 100644 --- a/homeassistant/components/manual_mqtt/alarm_control_panel.py +++ b/homeassistant/components/manual_mqtt/alarm_control_panel.py @@ -28,12 +28,11 @@ from homeassistant.const import ( STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( - EventStateChangedData, async_track_point_in_time, async_track_state_change_event, ) diff --git a/homeassistant/components/min_max/sensor.py b/homeassistant/components/min_max/sensor.py index 4ea63f5a472..f34067fea2e 100644 --- a/homeassistant/components/min_max/sensor.py +++ b/homeassistant/components/min_max/sensor.py @@ -23,13 +23,10 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType diff --git a/homeassistant/components/mold_indicator/sensor.py b/homeassistant/components/mold_indicator/sensor.py index 6839e57c838..cbb531d9672 100644 --- a/homeassistant/components/mold_indicator/sensor.py +++ b/homeassistant/components/mold_indicator/sensor.py @@ -17,13 +17,16 @@ from homeassistant.const import ( STATE_UNKNOWN, UnitOfTemperature, ) -from homeassistant.core import Event, HomeAssistant, State, callback +from homeassistant.core import ( + Event, + EventStateChangedData, + HomeAssistant, + State, + callback, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.unit_conversion import TemperatureConverter from homeassistant.util.unit_system import METRIC_SYSTEM diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index 8aa3251641b..82b3b5ef7bd 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -35,6 +35,7 @@ from homeassistant.const import ( ) from homeassistant.core import ( Event, + EventStateChangedData, HomeAssistant, ServiceCall, State, @@ -48,10 +49,7 @@ from homeassistant.helpers import ( service, ) from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) +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 diff --git a/homeassistant/components/plant/__init__.py b/homeassistant/components/plant/__init__.py index 076f93faf7b..4f35f9eb281 100644 --- a/homeassistant/components/plant/__init__.py +++ b/homeassistant/components/plant/__init__.py @@ -20,15 +20,18 @@ from homeassistant.const import ( STATE_UNKNOWN, UnitOfTemperature, ) -from homeassistant.core import Event, HomeAssistant, State, callback +from homeassistant.core import ( + Event, + EventStateChangedData, + HomeAssistant, + State, + 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 -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index d3a307a6616..09c65c35f5f 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -49,7 +49,7 @@ from homeassistant.const import ( STATE_UNKNOWN, UnitOfTemperature, ) -from homeassistant.core import Event, HomeAssistant, State +from homeassistant.core import Event, EventStateChangedData, HomeAssistant, State from homeassistant.helpers import entityfilter, state as state_helper import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_registry import ( @@ -57,7 +57,6 @@ from homeassistant.helpers.entity_registry import ( EventEntityRegistryUpdatedData, ) from homeassistant.helpers.entity_values import EntityValues -from homeassistant.helpers.event import EventStateChangedData from homeassistant.helpers.typing import ConfigType from homeassistant.util.dt import as_timestamp from homeassistant.util.unit_conversion import TemperatureConverter diff --git a/homeassistant/components/purpleair/config_flow.py b/homeassistant/components/purpleair/config_flow.py index 6c25681329a..5ba88318a1c 100644 --- a/homeassistant/components/purpleair/config_flow.py +++ b/homeassistant/components/purpleair/config_flow.py @@ -25,17 +25,14 @@ from homeassistant.const import ( CONF_LONGITUDE, CONF_SHOW_ON_MAP, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback from homeassistant.helpers import ( aiohttp_client, config_validation as cv, device_registry as dr, entity_registry as er, ) -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.selector import ( SelectOptionDict, SelectSelector, diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index d995f529b7d..5c10768a408 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -35,6 +35,7 @@ from homeassistant.const import ( from homeassistant.core import ( CALLBACK_TYPE, Event, + EventStateChangedData, HomeAssistant, State, callback, @@ -43,7 +44,6 @@ from homeassistant.core import ( from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( - EventStateChangedData, async_track_point_in_utc_time, async_track_state_change_event, ) diff --git a/homeassistant/components/switch/light.py b/homeassistant/components/switch/light.py index 25214822bdb..f226ed57e2a 100644 --- a/homeassistant/components/switch/light.py +++ b/homeassistant/components/switch/light.py @@ -16,14 +16,11 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback from homeassistant.helpers import entity_registry as er import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import DOMAIN as SWITCH_DOMAIN diff --git a/homeassistant/components/switch_as_x/cover.py b/homeassistant/components/switch_as_x/cover.py index 9d03965a242..7c6a7ff38ad 100644 --- a/homeassistant/components/switch_as_x/cover.py +++ b/homeassistant/components/switch_as_x/cover.py @@ -18,10 +18,9 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_ON, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import EventStateChangedData from .const import CONF_INVERT from .entity import BaseInvertableEntity diff --git a/homeassistant/components/switch_as_x/entity.py b/homeassistant/components/switch_as_x/entity.py index e8e57570617..020d92e21ac 100644 --- a/homeassistant/components/switch_as_x/entity.py +++ b/homeassistant/components/switch_as_x/entity.py @@ -13,14 +13,11 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import Entity, ToggleEntity -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) +from homeassistant.helpers.event import async_track_state_change_event from .const import DOMAIN as SWITCH_AS_X_DOMAIN diff --git a/homeassistant/components/switch_as_x/lock.py b/homeassistant/components/switch_as_x/lock.py index 5243ae184ee..2095b06bd84 100644 --- a/homeassistant/components/switch_as_x/lock.py +++ b/homeassistant/components/switch_as_x/lock.py @@ -14,10 +14,9 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_ON, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import EventStateChangedData from .const import CONF_INVERT from .entity import BaseInvertableEntity diff --git a/homeassistant/components/switch_as_x/valve.py b/homeassistant/components/switch_as_x/valve.py index 98f0e52c8a2..8626ca3cfb4 100644 --- a/homeassistant/components/switch_as_x/valve.py +++ b/homeassistant/components/switch_as_x/valve.py @@ -18,10 +18,9 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_ON, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import EventStateChangedData from .const import CONF_INVERT from .entity import BaseInvertableEntity diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index 7f24f31c692..a03b0a1ada0 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -23,6 +23,7 @@ from homeassistant.core import ( CALLBACK_TYPE, Context, Event, + EventStateChangedData, HomeAssistant, State, callback, @@ -32,7 +33,6 @@ from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import ( - EventStateChangedData, TrackTemplate, TrackTemplateResult, TrackTemplateResultInfo, diff --git a/homeassistant/components/template/trigger.py b/homeassistant/components/template/trigger.py index 8e95362ff88..09ad0754634 100644 --- a/homeassistant/components/template/trigger.py +++ b/homeassistant/components/template/trigger.py @@ -8,10 +8,16 @@ import voluptuous as vol from homeassistant import exceptions from homeassistant.const import CONF_FOR, CONF_PLATFORM, CONF_VALUE_TEMPLATE -from homeassistant.core import CALLBACK_TYPE, Event, HassJob, HomeAssistant, callback +from homeassistant.core import ( + CALLBACK_TYPE, + Event, + EventStateChangedData, + HassJob, + HomeAssistant, + callback, +) from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.event import ( - EventStateChangedData, TrackTemplate, TrackTemplateResult, async_call_later, diff --git a/homeassistant/components/threshold/binary_sensor.py b/homeassistant/components/threshold/binary_sensor.py index 364511ca291..9674357eb60 100644 --- a/homeassistant/components/threshold/binary_sensor.py +++ b/homeassistant/components/threshold/binary_sensor.py @@ -22,7 +22,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback from homeassistant.helpers import ( config_validation as cv, device_registry as dr, @@ -30,10 +30,7 @@ from homeassistant.helpers import ( ) from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import CONF_HYSTERESIS, CONF_LOWER, CONF_UPPER diff --git a/homeassistant/components/trend/binary_sensor.py b/homeassistant/components/trend/binary_sensor.py index 526228c2be1..2b70e2394f0 100644 --- a/homeassistant/components/trend/binary_sensor.py +++ b/homeassistant/components/trend/binary_sensor.py @@ -31,14 +31,11 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index 90036e5d47c..5deebc4103b 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -79,13 +79,12 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( - EventStateChangedData, TrackTemplate, TrackTemplateResult, async_track_state_change_event, diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index 26582df1b44..ff993ee3696 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -30,7 +30,13 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import Event, HomeAssistant, State, callback +from homeassistant.core import ( + Event, + EventStateChangedData, + HomeAssistant, + State, + callback, +) from homeassistant.helpers import ( device_registry as dr, entity_platform, @@ -40,7 +46,6 @@ 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.event import ( - EventStateChangedData, async_track_point_in_time, async_track_state_change_event, ) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 11210fcfcbc..7f30c08c076 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -21,6 +21,7 @@ from homeassistant.const import ( from homeassistant.core import ( Context, Event, + EventStateChangedData, HomeAssistant, ServiceResponse, State, @@ -36,7 +37,6 @@ from homeassistant.exceptions import ( from homeassistant.helpers import config_validation as cv, entity, template from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import ( - EventStateChangedData, TrackTemplate, TrackTemplateResult, async_track_template_result, diff --git a/homeassistant/components/websocket_api/messages.py b/homeassistant/components/websocket_api/messages.py index 8de43c57f00..75a9c9999d4 100644 --- a/homeassistant/components/websocket_api/messages.py +++ b/homeassistant/components/websocket_api/messages.py @@ -15,9 +15,8 @@ from homeassistant.const import ( COMPRESSED_STATE_LAST_UPDATED, COMPRESSED_STATE_STATE, ) -from homeassistant.core import Event, State +from homeassistant.core import Event, EventStateChangedData, State from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.event import EventStateChangedData from homeassistant.helpers.json import ( JSON_DUMP, find_paths_unserializable_data, diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index f9f63321d44..f10e377dc46 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -11,7 +11,7 @@ from typing import TYPE_CHECKING, Any, Self from zigpy.quirks.v2 import EntityMetadata, EntityType from homeassistant.const import ATTR_NAME, EntityCategory -from homeassistant.core import CALLBACK_TYPE, Event, callback +from homeassistant.core import CALLBACK_TYPE, Event, EventStateChangedData, callback from homeassistant.helpers import entity from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE, DeviceInfo @@ -19,10 +19,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.restore_state import RestoreEntity from .core.const import ( diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index ee85bda5a6d..eaee24376c7 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -29,7 +29,14 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import Event, HomeAssistant, ServiceCall, State, callback +from homeassistant.core import ( + Event, + EventStateChangedData, + HomeAssistant, + ServiceCall, + State, + callback, +) from homeassistant.helpers import ( collection, config_validation as cv, @@ -166,7 +173,7 @@ def async_setup_track_zone_entity_ids(hass: HomeAssistant) -> None: @callback def _async_add_zone_entity_id( - event_: Event[event.EventStateChangedData], + event_: Event[EventStateChangedData], ) -> None: """Add zone entity ID.""" zone_entity_ids.append(event_.data["entity_id"]) @@ -174,7 +181,7 @@ def async_setup_track_zone_entity_ids(hass: HomeAssistant) -> None: @callback def _async_remove_zone_entity_id( - event_: Event[event.EventStateChangedData], + event_: Event[EventStateChangedData], ) -> None: """Remove zone entity ID.""" zone_entity_ids.remove(event_.data["entity_id"]) @@ -384,9 +391,7 @@ class Zone(collection.CollectionEntity): self.async_write_ha_state() @callback - def _person_state_change_listener( - self, evt: Event[event.EventStateChangedData] - ) -> None: + def _person_state_change_listener(self, evt: Event[EventStateChangedData]) -> None: person_entity_id = evt.data["entity_id"] persons_in_zone = self._persons_in_zone cur_count = len(persons_in_zone) diff --git a/homeassistant/components/zone/trigger.py b/homeassistant/components/zone/trigger.py index 8aea25f1f6c..aa4aefe6d95 100644 --- a/homeassistant/components/zone/trigger.py +++ b/homeassistant/components/zone/trigger.py @@ -13,17 +13,21 @@ from homeassistant.const import ( CONF_PLATFORM, CONF_ZONE, ) -from homeassistant.core import CALLBACK_TYPE, Event, HassJob, HomeAssistant, callback +from homeassistant.core import ( + CALLBACK_TYPE, + Event, + EventStateChangedData, + HassJob, + HomeAssistant, + callback, +) from homeassistant.helpers import ( condition, config_validation as cv, entity_registry as er, location, ) -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index cf5051e657a..a4235d1ee2c 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -15,12 +15,11 @@ import pytest from homeassistant.const import MATCH_ALL import homeassistant.core as ha -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, EventStateChangedData, 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 from homeassistant.helpers.event import ( - EventStateChangedData, TrackStates, TrackTemplate, TrackTemplateResult, From 3c5089bc3f3cd83cca33315ed646ba63f27692ca Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 4 Apr 2024 23:48:36 +0200 Subject: [PATCH 275/967] Update import for EventStateChangedData [a-h] (#114899) --- homeassistant/components/alert/__init__.py | 3 +-- homeassistant/components/apache_kafka/__init__.py | 3 +-- homeassistant/components/api/__init__.py | 3 +-- homeassistant/components/bayesian/binary_sensor.py | 3 +-- homeassistant/components/compensation/sensor.py | 11 +++++++---- .../components/conversation/default_agent.py | 9 ++++----- homeassistant/components/derivative/sensor.py | 7 ++----- homeassistant/components/dhcp/__init__.py | 9 +++++++-- homeassistant/components/emulated_hue/config.py | 10 ++++++++-- homeassistant/components/emulated_hue/hue_api.py | 7 ++----- homeassistant/components/esphome/manager.py | 14 +++++++++----- homeassistant/components/filter/sensor.py | 13 ++++++++----- .../components/generic_thermostat/climate.py | 2 +- homeassistant/components/geo_location/trigger.py | 7 ++----- homeassistant/components/group/entity.py | 6 ++---- homeassistant/components/group/event.py | 7 ++----- homeassistant/components/group/media_player.py | 14 +++++++++----- homeassistant/components/history/websocket_api.py | 2 +- .../components/history_stats/coordinator.py | 11 +++++++---- homeassistant/components/history_stats/data.py | 3 +-- .../homeassistant/triggers/numeric_state.py | 2 +- .../components/homeassistant/triggers/state.py | 2 +- .../components/homeassistant/triggers/time.py | 2 +- homeassistant/components/homekit/accessories.py | 6 ++---- homeassistant/components/homekit/type_cameras.py | 9 +++++++-- homeassistant/components/homekit/type_covers.py | 7 ++----- .../components/homekit/type_humidifiers.py | 7 ++----- homeassistant/components/homekit/util.py | 10 ++++++++-- tests/components/esphome/test_entity.py | 7 ++----- 29 files changed, 102 insertions(+), 94 deletions(-) diff --git a/homeassistant/components/alert/__init__.py b/homeassistant/components/alert/__init__.py index 471d32227c2..1ffeb7c73ac 100644 --- a/homeassistant/components/alert/__init__.py +++ b/homeassistant/components/alert/__init__.py @@ -26,13 +26,12 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -from homeassistant.core import Event, HassJob, HomeAssistant +from homeassistant.core import Event, EventStateChangedData, HassJob, HomeAssistant from homeassistant.exceptions import ServiceNotFound import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import ( - EventStateChangedData, async_track_point_in_time, async_track_state_change_event, ) diff --git a/homeassistant/components/apache_kafka/__init__.py b/homeassistant/components/apache_kafka/__init__.py index fb29c0d5e49..454b748dcc2 100644 --- a/homeassistant/components/apache_kafka/__init__.py +++ b/homeassistant/components/apache_kafka/__init__.py @@ -19,10 +19,9 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import Event, HomeAssistant +from homeassistant.core import Event, EventStateChangedData, HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import FILTER_SCHEMA, EntityFilter -from homeassistant.helpers.event import EventStateChangedData from homeassistant.helpers.typing import ConfigType from homeassistant.util import ssl as ssl_util diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py index 82aaefe1288..2a2b55429dd 100644 --- a/homeassistant/components/api/__init__.py +++ b/homeassistant/components/api/__init__.py @@ -37,7 +37,7 @@ from homeassistant.const import ( URL_API_TEMPLATE, ) import homeassistant.core as ha -from homeassistant.core import Event, HomeAssistant +from homeassistant.core import Event, EventStateChangedData, HomeAssistant from homeassistant.exceptions import ( InvalidEntityFormatError, InvalidStateError, @@ -46,7 +46,6 @@ from homeassistant.exceptions import ( Unauthorized, ) from homeassistant.helpers import config_validation as cv, template -from homeassistant.helpers.event import EventStateChangedData from homeassistant.helpers.json import json_dumps, json_fragment from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.helpers.typing import ConfigType diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py index b6298040b6b..470732f36d2 100644 --- a/homeassistant/components/bayesian/binary_sensor.py +++ b/homeassistant/components/bayesian/binary_sensor.py @@ -28,13 +28,12 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback from homeassistant.exceptions import ConditionError, TemplateError from homeassistant.helpers import condition import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( - EventStateChangedData, TrackTemplate, TrackTemplateResult, TrackTemplateResultInfo, diff --git a/homeassistant/components/compensation/sensor.py b/homeassistant/components/compensation/sensor.py index 11d838e2467..95695932540 100644 --- a/homeassistant/components/compensation/sensor.py +++ b/homeassistant/components/compensation/sensor.py @@ -18,12 +18,15 @@ from homeassistant.const import ( CONF_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, ) -from homeassistant.core import Event, HomeAssistant, State, callback -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import ( +from homeassistant.core import ( + Event, EventStateChangedData, - async_track_state_change_event, + HomeAssistant, + State, + callback, ) +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 8202814d347..f652c5ee0eb 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -41,10 +41,7 @@ from homeassistant.helpers import ( translation, ) from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_added_domain, -) +from homeassistant.helpers.event import async_track_state_added_domain from homeassistant.util.json import JsonObjectType, json_loads_object from .const import DEFAULT_EXPOSED_ATTRIBUTES, DOMAIN @@ -134,7 +131,9 @@ async def async_setup_default_agent( async_should_expose(hass, DOMAIN, entity_id) @core.callback - def async_entity_state_listener(event: core.Event[EventStateChangedData]) -> None: + def async_entity_state_listener( + event: core.Event[core.EventStateChangedData], + ) -> None: """Set expose flag on new entities.""" async_should_expose(hass, DOMAIN, event.data["entity_id"]) diff --git a/homeassistant/components/derivative/sensor.py b/homeassistant/components/derivative/sensor.py index ea343288c9c..d5a83035ed5 100644 --- a/homeassistant/components/derivative/sensor.py +++ b/homeassistant/components/derivative/sensor.py @@ -19,7 +19,7 @@ from homeassistant.const import ( STATE_UNKNOWN, UnitOfTime, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback from homeassistant.helpers import ( config_validation as cv, device_registry as dr, @@ -27,10 +27,7 @@ from homeassistant.helpers import ( ) from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index 050bc7a74b2..40cc0c02c84 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -37,7 +37,13 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, STATE_HOME, ) -from homeassistant.core import Event, HomeAssistant, State, callback +from homeassistant.core import ( + Event, + EventStateChangedData, + HomeAssistant, + State, + callback, +) from homeassistant.data_entry_flow import BaseServiceInfo from homeassistant.helpers import config_validation as cv, discovery_flow from homeassistant.helpers.device_registry import ( @@ -48,7 +54,6 @@ from homeassistant.helpers.device_registry import ( ) from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import ( - EventStateChangedData, async_track_state_added_domain, async_track_time_interval, ) diff --git a/homeassistant/components/emulated_hue/config.py b/homeassistant/components/emulated_hue/config.py index b4208c1f3f6..91876d81508 100644 --- a/homeassistant/components/emulated_hue/config.py +++ b/homeassistant/components/emulated_hue/config.py @@ -16,10 +16,16 @@ from homeassistant.components import ( script, ) from homeassistant.const import CONF_ENTITIES, CONF_TYPE -from homeassistant.core import Event, HomeAssistant, State, callback, split_entity_id +from homeassistant.core import ( + Event, + EventStateChangedData, + HomeAssistant, + State, + callback, + split_entity_id, +) from homeassistant.helpers import storage from homeassistant.helpers.event import ( - EventStateChangedData, async_track_state_added_domain, async_track_state_removed_domain, ) diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 91c4440d875..8194d31823d 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -65,11 +65,8 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) -from homeassistant.core import Event, State -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) +from homeassistant.core import Event, EventStateChangedData, State +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.util.json import json_loads from homeassistant.util.network import is_local diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index bbd54154521..ad24a68103d 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -33,16 +33,20 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_CLOSE, EVENT_LOGGING_CHANGED, ) -from homeassistant.core import Event, HomeAssistant, ServiceCall, State, callback +from homeassistant.core import ( + Event, + EventStateChangedData, + HomeAssistant, + ServiceCall, + State, + callback, +) from homeassistant.exceptions import TemplateError from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv import homeassistant.helpers.device_registry as dr from homeassistant.helpers.device_registry import format_mac -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.issue_registry import ( IssueSeverity, async_create_issue, diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index 5ae300f6ec4..decb1f0a33f 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -35,13 +35,16 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import Event, HomeAssistant, State, callback +from homeassistant.core import ( + Event, + EventStateChangedData, + HomeAssistant, + State, + callback, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.start import async_at_started from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index 42fd2ef6f41..4c660bd03e9 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -45,6 +45,7 @@ from homeassistant.core import ( DOMAIN as HA_DOMAIN, CoreState, Event, + EventStateChangedData, HomeAssistant, State, callback, @@ -54,7 +55,6 @@ from homeassistant.helpers import condition import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( - EventStateChangedData, async_track_state_change_event, async_track_time_interval, ) diff --git a/homeassistant/components/geo_location/trigger.py b/homeassistant/components/geo_location/trigger.py index fb6140f707c..96244e08d1b 100644 --- a/homeassistant/components/geo_location/trigger.py +++ b/homeassistant/components/geo_location/trigger.py @@ -11,6 +11,7 @@ from homeassistant.const import CONF_EVENT, CONF_PLATFORM, CONF_SOURCE, CONF_ZON from homeassistant.core import ( CALLBACK_TYPE, Event, + EventStateChangedData, HassJob, HomeAssistant, State, @@ -18,11 +19,7 @@ from homeassistant.core import ( ) from homeassistant.helpers import condition, config_validation as cv from homeassistant.helpers.config_validation import entity_domain -from homeassistant.helpers.event import ( - EventStateChangedData, - TrackStates, - async_track_state_change_filtered, -) +from homeassistant.helpers.event import TrackStates, async_track_state_change_filtered from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType diff --git a/homeassistant/components/group/entity.py b/homeassistant/components/group/entity.py index 24c10fd2e7b..dcb16fd6af3 100644 --- a/homeassistant/components/group/entity.py +++ b/homeassistant/components/group/entity.py @@ -12,6 +12,7 @@ from homeassistant.const import ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, STATE_ON from homeassistant.core import ( CALLBACK_TYPE, Event, + EventStateChangedData, HomeAssistant, State, callback, @@ -20,10 +21,7 @@ from homeassistant.core import ( from homeassistant.helpers import start from homeassistant.helpers.entity import Entity, async_generate_entity_id from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) +from homeassistant.helpers.event import async_track_state_change_event from .const import ATTR_AUTO, ATTR_ORDER, DOMAIN, GROUP_ORDER, REG_KEY from .registry import GroupIntegrationRegistry diff --git a/homeassistant/components/group/event.py b/homeassistant/components/group/event.py index 61ddb3e0645..e5752a7835f 100644 --- a/homeassistant/components/group/event.py +++ b/homeassistant/components/group/event.py @@ -25,13 +25,10 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .entity import GroupEntity diff --git a/homeassistant/components/group/media_player.py b/homeassistant/components/group/media_player.py index ccb7154f7c1..6c49f88a12f 100644 --- a/homeassistant/components/group/media_player.py +++ b/homeassistant/components/group/media_player.py @@ -45,13 +45,17 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, State, callback +from homeassistant.core import ( + CALLBACK_TYPE, + Event, + EventStateChangedData, + HomeAssistant, + State, + callback, +) from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType KEY_ANNOUNCE = "announce" diff --git a/homeassistant/components/history/websocket_api.py b/homeassistant/components/history/websocket_api.py index 462d8464229..03bb7efc561 100644 --- a/homeassistant/components/history/websocket_api.py +++ b/homeassistant/components/history/websocket_api.py @@ -24,6 +24,7 @@ from homeassistant.const import ( from homeassistant.core import ( CALLBACK_TYPE, Event, + EventStateChangedData, HomeAssistant, State, callback, @@ -31,7 +32,6 @@ from homeassistant.core import ( valid_entity_id, ) from homeassistant.helpers.event import ( - EventStateChangedData, async_track_point_in_utc_time, async_track_state_change_event, ) diff --git a/homeassistant/components/history_stats/coordinator.py b/homeassistant/components/history_stats/coordinator.py index 2127f1d3dc5..159de11a9f1 100644 --- a/homeassistant/components/history_stats/coordinator.py +++ b/homeassistant/components/history_stats/coordinator.py @@ -6,12 +6,15 @@ from datetime import timedelta import logging from typing import Any -from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback -from homeassistant.exceptions import TemplateError -from homeassistant.helpers.event import ( +from homeassistant.core import ( + CALLBACK_TYPE, + Event, EventStateChangedData, - async_track_state_change_event, + HomeAssistant, + callback, ) +from homeassistant.exceptions import TemplateError +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.start import async_at_start from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed diff --git a/homeassistant/components/history_stats/data.py b/homeassistant/components/history_stats/data.py index 62ab28dc4f1..544e1772b01 100644 --- a/homeassistant/components/history_stats/data.py +++ b/homeassistant/components/history_stats/data.py @@ -6,8 +6,7 @@ from dataclasses import dataclass import datetime from homeassistant.components.recorder import get_instance, history -from homeassistant.core import Event, HomeAssistant, State -from homeassistant.helpers.event import EventStateChangedData +from homeassistant.core import Event, EventStateChangedData, HomeAssistant, State from homeassistant.helpers.template import Template import homeassistant.util.dt as dt_util diff --git a/homeassistant/components/homeassistant/triggers/numeric_state.py b/homeassistant/components/homeassistant/triggers/numeric_state.py index 2575af41401..43cc3d0918e 100644 --- a/homeassistant/components/homeassistant/triggers/numeric_state.py +++ b/homeassistant/components/homeassistant/triggers/numeric_state.py @@ -22,6 +22,7 @@ from homeassistant.const import ( from homeassistant.core import ( CALLBACK_TYPE, Event, + EventStateChangedData, HassJob, HomeAssistant, State, @@ -34,7 +35,6 @@ from homeassistant.helpers import ( template, ) from homeassistant.helpers.event import ( - EventStateChangedData, async_track_same_state, async_track_state_change_event, ) diff --git a/homeassistant/components/homeassistant/triggers/state.py b/homeassistant/components/homeassistant/triggers/state.py index 6f3183e2b40..e0cbbf09610 100644 --- a/homeassistant/components/homeassistant/triggers/state.py +++ b/homeassistant/components/homeassistant/triggers/state.py @@ -13,6 +13,7 @@ from homeassistant.const import CONF_ATTRIBUTE, CONF_FOR, CONF_PLATFORM, MATCH_A from homeassistant.core import ( CALLBACK_TYPE, Event, + EventStateChangedData, HassJob, HomeAssistant, State, @@ -24,7 +25,6 @@ from homeassistant.helpers import ( template, ) from homeassistant.helpers.event import ( - EventStateChangedData, async_track_same_state, async_track_state_change_event, process_state_match, diff --git a/homeassistant/components/homeassistant/triggers/time.py b/homeassistant/components/homeassistant/triggers/time.py index b1d19d54795..6d035683f71 100644 --- a/homeassistant/components/homeassistant/triggers/time.py +++ b/homeassistant/components/homeassistant/triggers/time.py @@ -16,6 +16,7 @@ from homeassistant.const import ( from homeassistant.core import ( CALLBACK_TYPE, Event, + EventStateChangedData, HassJob, HomeAssistant, State, @@ -23,7 +24,6 @@ from homeassistant.core import ( ) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import ( - EventStateChangedData, async_track_point_in_time, async_track_state_change_event, async_track_time_change, diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 39fa62e3445..f2e1a26b3de 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -45,16 +45,14 @@ from homeassistant.core import ( CALLBACK_TYPE, Context, Event, + EventStateChangedData, HomeAssistant, State, callback as ha_callback, split_entity_id, ) from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.util.decorator import Registry from .const import ( diff --git a/homeassistant/components/homekit/type_cameras.py b/homeassistant/components/homekit/type_cameras.py index 5f1a9428d86..84c834f5cc6 100644 --- a/homeassistant/components/homekit/type_cameras.py +++ b/homeassistant/components/homekit/type_cameras.py @@ -17,9 +17,14 @@ from pyhap.util import callback as pyhap_callback from homeassistant.components import camera from homeassistant.components.ffmpeg import get_ffmpeg_manager from homeassistant.const import STATE_ON -from homeassistant.core import Event, HomeAssistant, State, callback -from homeassistant.helpers.event import ( +from homeassistant.core import ( + Event, EventStateChangedData, + HomeAssistant, + State, + callback, +) +from homeassistant.helpers.event import ( async_track_state_change_event, async_track_time_interval, ) diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index 2452fd65026..d14713b5f05 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -34,11 +34,8 @@ from homeassistant.const import ( STATE_OPEN, STATE_OPENING, ) -from homeassistant.core import Event, State, callback -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) +from homeassistant.core import Event, EventStateChangedData, State, callback +from homeassistant.helpers.event import async_track_state_change_event from .accessories import TYPES, HomeAccessory from .const import ( diff --git a/homeassistant/components/homekit/type_humidifiers.py b/homeassistant/components/homekit/type_humidifiers.py index 2b4de072b6a..1fca441e800 100644 --- a/homeassistant/components/homekit/type_humidifiers.py +++ b/homeassistant/components/homekit/type_humidifiers.py @@ -25,11 +25,8 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_ON, ) -from homeassistant.core import Event, State, callback -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) +from homeassistant.core import Event, EventStateChangedData, State, callback +from homeassistant.helpers.event import async_track_state_change_event from .accessories import TYPES, HomeAccessory from .const import ( diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 642669cfc8d..f63ad9f46ae 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -38,9 +38,15 @@ from homeassistant.const import ( CONF_TYPE, UnitOfTemperature, ) -from homeassistant.core import Event, HomeAssistant, State, callback, split_entity_id +from homeassistant.core import ( + Event, + EventStateChangedData, + HomeAssistant, + State, + callback, + split_entity_id, +) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import EventStateChangedData from homeassistant.helpers.storage import STORAGE_DIR from homeassistant.util.unit_conversion import TemperatureConverter diff --git a/tests/components/esphome/test_entity.py b/tests/components/esphome/test_entity.py index 303d50f3103..bc633d87fae 100644 --- a/tests/components/esphome/test_entity.py +++ b/tests/components/esphome/test_entity.py @@ -23,12 +23,9 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) +from homeassistant.helpers.event import async_track_state_change_event from .conftest import MockESPHomeDevice From 7f198828430e904d1f7b66585d1f9a016d560845 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 5 Apr 2024 00:26:07 +0200 Subject: [PATCH 276/967] Update frontend to 20240404.1 (#114890) --- 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 75c630b4471..028fb28f01b 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==20240404.0"] + "requirements": ["home-assistant-frontend==20240404.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b47828664f9..2c34ee78e6b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -30,7 +30,7 @@ habluetooth==2.4.2 hass-nabucasa==0.79.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240404.0 +home-assistant-frontend==20240404.1 home-assistant-intents==2024.4.3 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 313d4090407..47dcbf2479f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1081,7 +1081,7 @@ hole==0.8.0 holidays==0.46 # homeassistant.components.frontend -home-assistant-frontend==20240404.0 +home-assistant-frontend==20240404.1 # homeassistant.components.conversation home-assistant-intents==2024.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5c9c81f15fa..764174ffd6f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -880,7 +880,7 @@ hole==0.8.0 holidays==0.46 # homeassistant.components.frontend -home-assistant-frontend==20240404.0 +home-assistant-frontend==20240404.1 # homeassistant.components.conversation home-assistant-intents==2024.4.3 From 96149d944466744dad5ee3bb6f2011e4344ade2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 5 Apr 2024 00:48:21 +0200 Subject: [PATCH 277/967] Bump hass-nabucasa from 0.79.0 to 0.80.0 (#114818) Co-authored-by: Paulus Schoutsen --- 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 eed2bda421b..efbb401a0de 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.79.0"] + "requirements": ["hass-nabucasa==0.80.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2c34ee78e6b..610e781faec 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -27,7 +27,7 @@ fnv-hash-fast==0.5.0 ha-av==10.1.1 ha-ffmpeg==3.2.0 habluetooth==2.4.2 -hass-nabucasa==0.79.0 +hass-nabucasa==0.80.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 home-assistant-frontend==20240404.1 diff --git a/pyproject.toml b/pyproject.toml index 1e3e4c86372..0bcb3461729 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,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.79.0", + "hass-nabucasa==0.80.0", # 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 1dd9b1811d3..22bc0743a27 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ bcrypt==4.1.2 certifi>=2021.5.30 ciso8601==2.3.1 fnv-hash-fast==0.5.0 -hass-nabucasa==0.79.0 +hass-nabucasa==0.80.0 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 47dcbf2479f..c106f36f102 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1041,7 +1041,7 @@ habitipy==0.2.0 habluetooth==2.4.2 # homeassistant.components.cloud -hass-nabucasa==0.79.0 +hass-nabucasa==0.80.0 # homeassistant.components.splunk hass-splunk==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 764174ffd6f..444b6cc3379 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -852,7 +852,7 @@ habitipy==0.2.0 habluetooth==2.4.2 # homeassistant.components.cloud -hass-nabucasa==0.79.0 +hass-nabucasa==0.80.0 # homeassistant.components.conversation hassil==1.6.1 From a66ed1d9364979b94ba65e18b6e80ddb9d84c45e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Fri, 5 Apr 2024 02:55:39 +0200 Subject: [PATCH 278/967] Bump myuplink dependency to 0.6.0 (#114767) --- homeassistant/components/myuplink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/myuplink/manifest.json b/homeassistant/components/myuplink/manifest.json index a76f596ade3..0e638a72715 100644 --- a/homeassistant/components/myuplink/manifest.json +++ b/homeassistant/components/myuplink/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["application_credentials"], "documentation": "https://www.home-assistant.io/integrations/myuplink", "iot_class": "cloud_polling", - "requirements": ["myuplink==0.5.0"] + "requirements": ["myuplink==0.6.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index c106f36f102..7b3870db2bc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1353,7 +1353,7 @@ mutesync==0.0.1 mypermobil==0.1.8 # homeassistant.components.myuplink -myuplink==0.5.0 +myuplink==0.6.0 # homeassistant.components.nad nad-receiver==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 444b6cc3379..a6ec537893f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1092,7 +1092,7 @@ mutesync==0.0.1 mypermobil==0.1.8 # homeassistant.components.myuplink -myuplink==0.5.0 +myuplink==0.6.0 # homeassistant.components.keenetic_ndms2 ndms2-client==0.1.2 From 78920e1d71dbfa47ef7a2b836a9a287a16e0b74f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Apr 2024 15:28:36 -1000 Subject: [PATCH 279/967] Reduce august polling frequency (#114904) Co-authored-by: TheJulianJES --- homeassistant/components/august/activity.py | 21 +++++++++++- homeassistant/components/august/const.py | 2 +- homeassistant/components/august/subscriber.py | 33 +++++++++---------- tests/components/august/test_lock.py | 23 ++++++++++++- 4 files changed, 58 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/august/activity.py b/homeassistant/components/august/activity.py index ae920383e40..ee180ab5480 100644 --- a/homeassistant/components/august/activity.py +++ b/homeassistant/components/august/activity.py @@ -5,6 +5,7 @@ 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 @@ -26,9 +27,11 @@ _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 = 3 +ACTIVITY_DEBOUNCE_COOLDOWN = 4 @callback @@ -62,6 +65,7 @@ class ActivityStream(AugustSubscriberMixin): 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: @@ -70,6 +74,7 @@ class ActivityStream(AugustSubscriberMixin): 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: @@ -140,11 +145,25 @@ class ActivityStream(AugustSubscriberMixin): 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( diff --git a/homeassistant/components/august/const.py b/homeassistant/components/august/const.py index 0cbd21f397e..6aa033c62b2 100644 --- a/homeassistant/components/august/const.py +++ b/homeassistant/components/august/const.py @@ -40,7 +40,7 @@ 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=1) +MIN_TIME_BETWEEN_DETAIL_UPDATES = timedelta(hours=24) # Activity needs to be checked more frequently as the # doorbell motion and rings are included here diff --git a/homeassistant/components/august/subscriber.py b/homeassistant/components/august/subscriber.py index e800b5cb604..9332080d9ad 100644 --- a/homeassistant/components/august/subscriber.py +++ b/homeassistant/components/august/subscriber.py @@ -49,9 +49,17 @@ class AugustSubscriberMixin: """Call the refresh method.""" self._hass.async_create_task(self._async_refresh(now), 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, @@ -59,17 +67,12 @@ class AugustSubscriberMixin: name="august refresh", ) - @callback - def _async_cancel_update_interval(_: Event) -> None: - self._stop_interval = None - if self._unsub_interval: - self._unsub_interval() - - self._stop_interval = self._hass.bus.async_listen( - EVENT_HOMEASSISTANT_STOP, - _async_cancel_update_interval, - run_immediately=True, - ) + if not self._stop_interval: + self._stop_interval = self._hass.bus.async_listen( + EVENT_HOMEASSISTANT_STOP, + self._async_cancel_update_interval, + run_immediately=True, + ) @callback def async_unsubscribe_device_id( @@ -82,13 +85,7 @@ class AugustSubscriberMixin: if self._subscriptions: return - - if self._unsub_interval: - self._unsub_interval() - self._unsub_interval = None - if self._stop_interval: - self._stop_interval() - self._stop_interval = None + self._async_cancel_update_interval() @callback def async_signal_device_id_update(self, device_id: str) -> None: diff --git a/tests/components/august/test_lock.py b/tests/components/august/test_lock.py index 39c1745d967..4de931e6979 100644 --- a/tests/components/august/test_lock.py +++ b/tests/components/august/test_lock.py @@ -4,9 +4,11 @@ import datetime from unittest.mock import Mock from aiohttp import ClientResponseError +from freezegun.api import FrozenDateTimeFactory import pytest 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, @@ -155,7 +157,9 @@ async def test_one_lock_operation( async def test_one_lock_operation_pubnub_connected( - hass: HomeAssistant, entity_registry: er.EntityRegistry + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + freezer: FrozenDateTimeFactory, ) -> None: """Test lock and unlock operations are async when pubnub is connected.""" lock_one = await _mock_doorsense_enabled_august_lock_detail(hass) @@ -230,6 +234,23 @@ async def test_one_lock_operation_pubnub_connected( == STATE_UNKNOWN ) + freezer.tick(INITIAL_LOCK_RESYNC_TIME) + + pubnub.message( + pubnub, + Mock( + channel=lock_one.pubsub_channel, + timetoken=(dt_util.utcnow().timestamp() + 2) * 10000000, + message={ + "status": "kAugLockState_Unlocked", + }, + ), + ) + await hass.async_block_till_done() + + lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") + assert lock_online_with_doorsense_name.state == STATE_UNLOCKED + async def test_lock_jammed(hass: HomeAssistant) -> None: """Test lock gets jammed on unlock.""" From d3219063428fd6a7ee2d6b8d503005c04324af9d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Apr 2024 15:30:01 -1000 Subject: [PATCH 280/967] Always run keyed event trackers immediately (#114709) --- homeassistant/helpers/event.py | 8 +---- .../generic_thermostat/test_climate.py | 6 ++-- tests/components/group/test_config_flow.py | 24 ++++++++++++++ tests/components/template/test_config_flow.py | 33 +++++++++++-------- 4 files changed, 47 insertions(+), 24 deletions(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index cfbc40e7ed0..2529b49d263 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -107,7 +107,6 @@ class _KeyedEventTracker(Generic[_TypedDictT]): ], bool, ] - run_immediately: bool @dataclass(slots=True) @@ -339,7 +338,6 @@ _KEYED_TRACK_STATE_CHANGE = _KeyedEventTracker( event_type=EVENT_STATE_CHANGED, dispatcher_callable=_async_dispatch_entity_id_event, filter_callable=_async_state_change_filter, - run_immediately=False, ) @@ -417,7 +415,7 @@ def _async_track_event( tracker.event_type, ft.partial(tracker.dispatcher_callable, hass, callbacks), event_filter=ft.partial(tracker.filter_callable, hass, callbacks), - run_immediately=tracker.run_immediately, + run_immediately=True, ) job = HassJob(action, f"track {tracker.event_type} event {keys}", job_type=job_type) @@ -471,7 +469,6 @@ _KEYED_TRACK_ENTITY_REGISTRY_UPDATED = _KeyedEventTracker( event_type=EVENT_ENTITY_REGISTRY_UPDATED, dispatcher_callable=_async_dispatch_old_entity_id_or_entity_id_event, filter_callable=_async_entity_registry_updated_filter, - run_immediately=True, ) @@ -530,7 +527,6 @@ _KEYED_TRACK_DEVICE_REGISTRY_UPDATED = _KeyedEventTracker( event_type=EVENT_DEVICE_REGISTRY_UPDATED, dispatcher_callable=_async_dispatch_device_id_event, filter_callable=_async_device_registry_updated_filter, - run_immediately=True, ) @@ -599,7 +595,6 @@ _KEYED_TRACK_STATE_ADDED_DOMAIN = _KeyedEventTracker( event_type=EVENT_STATE_CHANGED, dispatcher_callable=_async_dispatch_domain_event, filter_callable=_async_domain_added_filter, - run_immediately=False, ) @@ -635,7 +630,6 @@ _KEYED_TRACK_STATE_REMOVED_DOMAIN = _KeyedEventTracker( event_type=EVENT_STATE_CHANGED, dispatcher_callable=_async_dispatch_domain_event, filter_callable=_async_domain_removed_filter, - run_immediately=False, ) diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py index 8903f4b6606..ff409511221 100644 --- a/tests/components/generic_thermostat/test_climate.py +++ b/tests/components/generic_thermostat/test_climate.py @@ -808,10 +808,10 @@ async def test_heating_cooling_switch_does_not_toggle_when_within_min_cycle_dura # Given await _setup_thermostat_with_min_cycle_duration(hass, ac_mode, initial_hvac_mode) calls = _setup_switch(hass, initial_switch_state) - _setup_sensor(hass, sensor_temperature) # When await common.async_set_temperature(hass, target_temperature) + _setup_sensor(hass, sensor_temperature) await hass.async_block_till_done() # Then @@ -849,10 +849,10 @@ async def test_heating_cooling_switch_toggles_when_outside_min_cycle_duration( fake_changed = datetime.datetime(1970, 11, 11, 11, 11, 11, tzinfo=dt_util.UTC) with freeze_time(fake_changed): calls = _setup_switch(hass, initial_switch_state) - _setup_sensor(hass, sensor_temperature) # When await common.async_set_temperature(hass, target_temperature) + _setup_sensor(hass, sensor_temperature) await hass.async_block_till_done() # Then @@ -894,10 +894,10 @@ async def test_hvac_mode_change_toggles_heating_cooling_switch_even_when_within_ # Given await _setup_thermostat_with_min_cycle_duration(hass, ac_mode, initial_hvac_mode) calls = _setup_switch(hass, initial_switch_state) - _setup_sensor(hass, sensor_temperature) # When await common.async_set_temperature(hass, target_temperature) + _setup_sensor(hass, sensor_temperature) await hass.async_block_till_done() # Then diff --git a/tests/components/group/test_config_flow.py b/tests/components/group/test_config_flow.py index 3de6d9ac32d..3aea9d21f0c 100644 --- a/tests/components/group/test_config_flow.py +++ b/tests/components/group/test_config_flow.py @@ -540,6 +540,7 @@ async def test_config_flow_preview( } ) msg = await client.receive_json() + preview_subscribe_id = msg["id"] assert msg["success"] assert msg["result"] is None @@ -549,9 +550,32 @@ async def test_config_flow_preview( "state": "unavailable", } + await client.send_json_auto_id( + { + "type": "unsubscribe_events", + "subscription": preview_subscribe_id, + } + ) + msg = await client.receive_json() + assert msg["success"] + hass.states.async_set(input_entities[0], input_states[0]) hass.states.async_set(input_entities[1], input_states[1]) + await client.send_json_auto_id( + { + "type": "group/start_preview", + "flow_id": result["flow_id"], + "flow_type": "config_flow", + "user_input": {"name": "My group", "entities": input_entities} + | extra_user_input, + } + ) + msg = await client.receive_json() + preview_subscribe_id = msg["id"] + assert msg["success"] + assert msg["result"] is None + msg = await client.receive_json() assert msg["event"] == { "attributes": { diff --git a/tests/components/template/test_config_flow.py b/tests/components/template/test_config_flow.py index 59a2c4f38a3..8c5dda401dd 100644 --- a/tests/components/template/test_config_flow.py +++ b/tests/components/template/test_config_flow.py @@ -8,6 +8,7 @@ from pytest_unordered import unordered from homeassistant import config_entries 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 @@ -276,7 +277,7 @@ async def test_options( "{{ float(states('sensor.one'), default='') + float(states('sensor.two'), default='') }}", {}, {"one": "30.0", "two": "20.0"}, - ["", "50.0"], + ["", STATE_UNAVAILABLE, "50.0"], [{}, {}], [["one", "two"], ["one", "two"]], ), @@ -342,20 +343,24 @@ async def test_config_flow_preview( hass.states.async_set( f"{template_type}.{input_entity}", input_states[input_entity], {} ) + await hass.async_block_till_done() - msg = await client.receive_json() - assert msg["event"] == { - "attributes": {"friendly_name": "My template"} - | extra_attributes[0] - | extra_attributes[1], - "listeners": { - "all": False, - "domains": [], - "entities": unordered([f"{template_type}.{_id}" for _id in listeners[1]]), - "time": False, - }, - "state": template_states[1], - } + for template_state in template_states[1:]: + msg = await client.receive_json() + assert msg["event"] == { + "attributes": {"friendly_name": "My template"} + | extra_attributes[0] + | extra_attributes[1], + "listeners": { + "all": False, + "domains": [], + "entities": unordered( + [f"{template_type}.{_id}" for _id in listeners[1]] + ), + "time": False, + }, + "state": template_state, + } assert len(hass.states.async_all()) == 2 From 90d502e1611b9bc140146cc43f54ff6fdf419c17 Mon Sep 17 00:00:00 2001 From: Jeef Date: Thu, 4 Apr 2024 19:37:54 -0600 Subject: [PATCH 281/967] Bump weatherflow4py to 0.2.20 (#114888) --- 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 8376bd1b50d..361349dcbe8 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.17"] + "requirements": ["weatherflow4py==0.2.20"] } diff --git a/requirements_all.txt b/requirements_all.txt index 7b3870db2bc..080273b41df 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2848,7 +2848,7 @@ watchdog==2.3.1 waterfurnace==1.1.0 # homeassistant.components.weatherflow_cloud -weatherflow4py==0.2.17 +weatherflow4py==0.2.20 # homeassistant.components.webmin webmin-xmlrpc==0.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a6ec537893f..16967556fb7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2195,7 +2195,7 @@ wallbox==0.6.0 watchdog==2.3.1 # homeassistant.components.weatherflow_cloud -weatherflow4py==0.2.17 +weatherflow4py==0.2.20 # homeassistant.components.webmin webmin-xmlrpc==0.0.2 From 5447a1a0157d72a649f030d80b021cc8b9110be9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Apr 2024 20:22:00 -1000 Subject: [PATCH 282/967] Ensure all tables have the default table args in the db_schema (#114895) --- homeassistant/components/recorder/db_schema.py | 12 +++++++++++- tests/components/recorder/test_init.py | 7 +++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/db_schema.py b/homeassistant/components/recorder/db_schema.py index 5b24448211d..eac743c3d75 100644 --- a/homeassistant/components/recorder/db_schema.py +++ b/homeassistant/components/recorder/db_schema.py @@ -715,6 +715,7 @@ class Statistics(Base, StatisticsBase): "start_ts", unique=True, ), + _DEFAULT_TABLE_ARGS, ) __tablename__ = TABLE_STATISTICS @@ -732,6 +733,7 @@ class StatisticsShortTerm(Base, StatisticsBase): "start_ts", unique=True, ), + _DEFAULT_TABLE_ARGS, ) __tablename__ = TABLE_STATISTICS_SHORT_TERM @@ -760,7 +762,10 @@ class StatisticsMeta(Base): class RecorderRuns(Base): """Representation of recorder run.""" - __table_args__ = (Index("ix_recorder_runs_start_end", "start", "end"),) + __table_args__ = ( + Index("ix_recorder_runs_start_end", "start", "end"), + _DEFAULT_TABLE_ARGS, + ) __tablename__ = TABLE_RECORDER_RUNS run_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) start: Mapped[datetime] = mapped_column(DATETIME_TYPE, default=dt_util.utcnow) @@ -789,6 +794,7 @@ class MigrationChanges(Base): """Representation of migration changes.""" __tablename__ = TABLE_MIGRATION_CHANGES + __table_args__ = (_DEFAULT_TABLE_ARGS,) migration_id: Mapped[str] = mapped_column(String(255), primary_key=True) version: Mapped[int] = mapped_column(SmallInteger) @@ -798,6 +804,8 @@ class SchemaChanges(Base): """Representation of schema version changes.""" __tablename__ = TABLE_SCHEMA_CHANGES + __table_args__ = (_DEFAULT_TABLE_ARGS,) + change_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) schema_version: Mapped[int | None] = mapped_column(Integer) changed: Mapped[datetime] = mapped_column(DATETIME_TYPE, default=dt_util.utcnow) @@ -816,6 +824,8 @@ class StatisticsRuns(Base): """Representation of statistics run.""" __tablename__ = TABLE_STATISTICS_RUNS + __table_args__ = (_DEFAULT_TABLE_ARGS,) + run_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) start: Mapped[datetime] = mapped_column(DATETIME_TYPE, index=True) diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index cde2da3cc83..206c356bad8 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -27,6 +27,7 @@ from homeassistant.components.recorder import ( DOMAIN, SQLITE_URL_PREFIX, Recorder, + db_schema, get_instance, migration, pool, @@ -2598,3 +2599,9 @@ async def test_commit_before_commits_pending_writes( await verify_states_in_queue_future await verify_session_commit_future + + +def test_all_tables_use_default_table_args(hass: HomeAssistant) -> None: + """Test that all tables use the default table args.""" + for table in db_schema.Base.metadata.tables.values(): + assert table.kwargs.items() >= db_schema._DEFAULT_TABLE_ARGS.items() From 0e2fe3b72825daf6c89a17843ba61c2836be2eb2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Apr 2024 20:27:27 -1000 Subject: [PATCH 283/967] Avoid timestamp conversion in core State when equal to last_updated (#114911) --- homeassistant/core.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/core.py b/homeassistant/core.py index f8540ae7e70..f94a7d4c1bb 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1680,11 +1680,15 @@ class State: @cached_property def last_changed_timestamp(self) -> float: """Timestamp of last change.""" + if self.last_changed == self.last_updated: + return self.last_updated_timestamp return self.last_changed.timestamp() @cached_property def last_reported_timestamp(self) -> float: """Timestamp of last report.""" + if self.last_reported == self.last_updated: + return self.last_updated_timestamp return self.last_reported.timestamp() @cached_property From 6040272c04ec1d2cbf52ea1d3fcbfdf03c976764 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 5 Apr 2024 08:28:35 +0200 Subject: [PATCH 284/967] Fix Axis camera platform support HTTPS (#114886) --- homeassistant/components/axis/camera.py | 16 ++++++++-------- homeassistant/components/axis/hub/config.py | 3 +++ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/axis/camera.py b/homeassistant/components/axis/camera.py index 769be676a78..025244fb675 100644 --- a/homeassistant/components/axis/camera.py +++ b/homeassistant/components/axis/camera.py @@ -56,6 +56,7 @@ class AxisCamera(AxisEntity, MjpegCamera): mjpeg_url=self.mjpeg_source, still_image_url=self.image_source, authentication=HTTP_DIGEST_AUTHENTICATION, + verify_ssl=False, unique_id=f"{hub.unique_id}-camera", ) @@ -74,16 +75,18 @@ class AxisCamera(AxisEntity, MjpegCamera): Additionally used when device change IP address. """ + proto = self.hub.config.protocol + host = self.hub.config.host + port = self.hub.config.port + image_options = self.generate_options(skip_stream_profile=True) self._still_image_url = ( - f"http://{self.hub.config.host}:{self.hub.config.port}/axis-cgi" - f"/jpg/image.cgi{image_options}" + f"{proto}://{host}:{port}/axis-cgi/jpg/image.cgi{image_options}" ) mjpeg_options = self.generate_options() self._mjpeg_url = ( - f"http://{self.hub.config.host}:{self.hub.config.port}/axis-cgi" - f"/mjpg/video.cgi{mjpeg_options}" + f"{proto}://{host}:{port}/axis-cgi/mjpg/video.cgi{mjpeg_options}" ) stream_options = self.generate_options(add_video_codec_h264=True) @@ -95,10 +98,7 @@ class AxisCamera(AxisEntity, MjpegCamera): self.hub.additional_diagnostics["camera_sources"] = { "Image": self._still_image_url, "MJPEG": self._mjpeg_url, - "Stream": ( - f"rtsp://user:pass@{self.hub.config.host}/axis-media" - f"/media.amp{stream_options}" - ), + "Stream": (f"rtsp://user:pass@{host}/axis-media/media.amp{stream_options}"), } @property diff --git a/homeassistant/components/axis/hub/config.py b/homeassistant/components/axis/hub/config.py index e6d8378b45c..eba706edc83 100644 --- a/homeassistant/components/axis/hub/config.py +++ b/homeassistant/components/axis/hub/config.py @@ -12,6 +12,7 @@ from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, CONF_PORT, + CONF_PROTOCOL, CONF_TRIGGER_TIME, CONF_USERNAME, ) @@ -31,6 +32,7 @@ class AxisConfig: entry: ConfigEntry + protocol: str host: str port: int username: str @@ -54,6 +56,7 @@ class AxisConfig: options = config_entry.options return cls( entry=config_entry, + protocol=config.get(CONF_PROTOCOL, "http"), host=config[CONF_HOST], username=config[CONF_USERNAME], password=config[CONF_PASSWORD], From 1f37774352f5554137606afcc8a0b7f3280e66a1 Mon Sep 17 00:00:00 2001 From: Lex Li <425130+lextm@users.noreply.github.com> Date: Fri, 5 Apr 2024 02:41:15 -0400 Subject: [PATCH 285/967] Fix type cast in snmp (#114795) --- homeassistant/components/snmp/sensor.py | 2 +- tests/components/snmp/test_negative_sensor.py | 79 +++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 tests/components/snmp/test_negative_sensor.py diff --git a/homeassistant/components/snmp/sensor.py b/homeassistant/components/snmp/sensor.py index f55cd07effb..972b9131935 100644 --- a/homeassistant/components/snmp/sensor.py +++ b/homeassistant/components/snmp/sensor.py @@ -270,7 +270,7 @@ class SnmpData: "SNMP OID %s received type=%s and data %s", self._baseoid, type(value), - bytes(value), + value, ) if isinstance(value, NoSuchObject): _LOGGER.error( diff --git a/tests/components/snmp/test_negative_sensor.py b/tests/components/snmp/test_negative_sensor.py new file mode 100644 index 00000000000..c5ac6460841 --- /dev/null +++ b/tests/components/snmp/test_negative_sensor.py @@ -0,0 +1,79 @@ +"""SNMP sensor tests.""" + +from unittest.mock import patch + +from pysnmp.hlapi import Integer32 +import pytest + +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component + + +@pytest.fixture(autouse=True) +def hlapi_mock(): + """Mock out 3rd party API.""" + mock_data = Integer32(-13) + with patch( + "homeassistant.components.snmp.sensor.getCmd", + return_value=(None, None, None, [[mock_data]]), + ): + yield + + +async def test_basic_config(hass: HomeAssistant) -> None: + """Test basic entity configuration.""" + + config = { + SENSOR_DOMAIN: { + "platform": "snmp", + "host": "192.168.1.32", + "baseoid": "1.3.6.1.4.1.2021.10.1.3.1", + }, + } + + assert await async_setup_component(hass, SENSOR_DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("sensor.snmp") + assert state.state == "-13" + assert state.attributes == {"friendly_name": "SNMP"} + + +async def test_entity_config(hass: HomeAssistant) -> None: + """Test entity configuration.""" + + config = { + SENSOR_DOMAIN: { + # SNMP configuration + "platform": "snmp", + "host": "192.168.1.32", + "baseoid": "1.3.6.1.4.1.2021.10.1.3.1", + # Entity configuration + "icon": "{{'mdi:one_two_three'}}", + "picture": "{{'blabla.png'}}", + "device_class": "temperature", + "name": "{{'SNMP' + ' ' + 'Sensor'}}", + "state_class": "measurement", + "unique_id": "very_unique", + "unit_of_measurement": "°C", + }, + } + + assert await async_setup_component(hass, SENSOR_DOMAIN, config) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + assert entity_registry.async_get("sensor.snmp_sensor").unique_id == "very_unique" + + state = hass.states.get("sensor.snmp_sensor") + assert state.state == "-13" + assert state.attributes == { + "device_class": "temperature", + "entity_picture": "blabla.png", + "friendly_name": "SNMP Sensor", + "icon": "mdi:one_two_three", + "state_class": "measurement", + "unit_of_measurement": "°C", + } From bfe944f6668b32a5349578c0cd68a3a04afead93 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Apr 2024 20:42:57 -1000 Subject: [PATCH 286/967] Handle ambiguous script actions by using action map order (#114825) --- homeassistant/helpers/config_validation.py | 6 ++++++ tests/helpers/test_config_validation.py | 22 ++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index fc39db83658..38287eb6722 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1855,6 +1855,12 @@ def determine_script_action(action: dict[str, Any]) -> str: """Determine action type.""" if not (actions := ACTIONS_SET.intersection(action)): raise ValueError("Unable to determine action") + if len(actions) > 1: + # Ambiguous action, select the first one in the + # order of the ACTIONS_MAP + for action_key, _script_action in ACTIONS_MAP.items(): + if action_key in actions: + return _script_action return ACTIONS_MAP[actions.pop()] diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 133e5e80442..9816dc38189 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -1672,3 +1672,25 @@ def test_color_hex() -> None: with pytest.raises(vol.Invalid, match=msg): cv.color_hex(123456) + + +def test_determine_script_action_ambiguous(): + """Test determine script action with ambiguous actions.""" + assert ( + cv.determine_script_action( + { + "type": "is_power", + "condition": "device", + "device_id": "9c2bda81bc7997c981f811c32cafdb22", + "entity_id": "2ee287ec70dd0c6db187b539bee429b7", + "domain": "sensor", + "below": "15", + } + ) + == "condition" + ) + + +def test_determine_script_action_non_ambiguous(): + """Test determine script action with a non ambiguous action.""" + assert cv.determine_script_action({"delay": "00:00:05"}) == "delay" From b67e9b28d6c6f2ad8b70f51e9410a4acf202f656 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 5 Apr 2024 09:47:49 +0200 Subject: [PATCH 287/967] Fix Axis reconfigure step not providing protocols as alternatives but as string (#114889) --- homeassistant/components/axis/config_flow.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index 30bc653c202..80872fc9be4 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -168,16 +168,13 @@ class AxisFlowHandler(ConfigFlow, domain=AXIS_DOMAIN): self, entry_data: Mapping[str, Any], keep_password: bool ) -> ConfigFlowResult: """Re-run configuration step.""" + protocol = entry_data.get(CONF_PROTOCOL, "http") + password = entry_data[CONF_PASSWORD] if keep_password else "" self.discovery_schema = { - vol.Required( - CONF_PROTOCOL, default=entry_data.get(CONF_PROTOCOL, "http") - ): str, + vol.Required(CONF_PROTOCOL, default=protocol): vol.In(PROTOCOL_CHOICES), vol.Required(CONF_HOST, default=entry_data[CONF_HOST]): str, vol.Required(CONF_USERNAME, default=entry_data[CONF_USERNAME]): str, - vol.Required( - CONF_PASSWORD, - default=entry_data[CONF_PASSWORD] if keep_password else "", - ): str, + vol.Required(CONF_PASSWORD, default=password): str, vol.Required(CONF_PORT, default=entry_data[CONF_PORT]): int, } From 04e5086e010b9a816bab0598e0a3cb5d6c610707 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Fri, 5 Apr 2024 10:01:51 +0200 Subject: [PATCH 288/967] Show correct model string in myuplink (#114921) --- homeassistant/components/myuplink/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/myuplink/__init__.py b/homeassistant/components/myuplink/__init__.py index 5dee46b24cf..42bb9007789 100644 --- a/homeassistant/components/myuplink/__init__.py +++ b/homeassistant/components/myuplink/__init__.py @@ -5,7 +5,7 @@ from __future__ import annotations from http import HTTPStatus from aiohttp import ClientError, ClientResponseError -from myuplink import MyUplinkAPI, get_manufacturer, get_system_name +from myuplink import MyUplinkAPI, get_manufacturer, get_model, get_system_name from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -92,7 +92,7 @@ def create_devices( identifiers={(DOMAIN, device_id)}, name=get_system_name(system), manufacturer=get_manufacturer(device), - model=device.productName, + model=get_model(device), sw_version=device.firmwareCurrent, serial_number=device.product_serial_number, ) From 24f83c5890b03ee085b9028212c889101b9cd59c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 5 Apr 2024 10:40:14 +0200 Subject: [PATCH 289/967] Use is in FlowResultType enum comparison in tests (#114917) * Use is in FlowResultType enum comparison in tests * Adjust auth * Adjust systemmonitor * Once more * Add comment --- tests/components/abode/test_init.py | 4 +- .../application_credentials/test_init.py | 25 +-- .../components/aussie_broadband/test_init.py | 4 +- tests/components/auth/test_mfa_setup_flow.py | 8 +- tests/components/cloud/test_account_link.py | 5 +- .../components/config/test_config_entries.py | 3 +- tests/components/dialogflow/test_init.py | 7 +- tests/components/esphome/test_dashboard.py | 2 +- tests/components/esphome/test_manager.py | 2 +- tests/components/flexit_bacnet/conftest.py | 2 +- tests/components/geofency/test_init.py | 7 +- tests/components/gpslogger/test_init.py | 7 +- tests/components/hisense_aehw4a1/test_init.py | 7 +- .../test_silabs_multiprotocol_addon.py | 154 +++++++++--------- tests/components/ifttt/test_init.py | 7 +- tests/components/locative/test_init.py | 7 +- tests/components/mailgun/test_init.py | 11 +- tests/components/ps4/test_init.py | 7 +- tests/components/sonos/test_init.py | 7 +- tests/components/synology_dsm/test_init.py | 4 +- tests/components/systemmonitor/test_init.py | 4 +- .../components/systemmonitor/test_repairs.py | 6 +- tests/components/traccar/test_init.py | 7 +- tests/components/twilio/test_init.py | 7 +- tests/components/youless/test_config_flows.py | 8 +- 25 files changed, 165 insertions(+), 147 deletions(-) diff --git a/tests/components/abode/test_init.py b/tests/components/abode/test_init.py index e6e5da35a5e..58e9ccb2c41 100644 --- a/tests/components/abode/test_init.py +++ b/tests/components/abode/test_init.py @@ -8,7 +8,6 @@ from jaraco.abode.exceptions import ( Exception as AbodeException, ) -from homeassistant import data_entry_flow from homeassistant.components.abode import ( DOMAIN as ABODE_DOMAIN, SERVICE_CAPTURE_IMAGE, @@ -19,6 +18,7 @@ from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .common import setup_platform @@ -82,7 +82,7 @@ async def test_invalid_credentials(hass: HomeAssistant) -> None: patch( "homeassistant.components.abode.config_flow.AbodeFlowHandler.async_step_reauth", return_value={ - "type": data_entry_flow.FlowResultType.FORM, + "type": FlowResultType.FORM, "flow_id": "mock_flow", "step_id": "reauth_confirm", }, diff --git a/tests/components/application_credentials/test_init.py b/tests/components/application_credentials/test_init.py index 2d44aec4461..af118c82279 100644 --- a/tests/components/application_credentials/test_init.py +++ b/tests/components/application_credentials/test_init.py @@ -26,6 +26,7 @@ from homeassistant.const import ( CONF_NAME, ) 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 @@ -163,7 +164,7 @@ class OAuthFixture: ) result = await self.hass.config_entries.flow.async_configure(result["flow_id"]) - assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == self.title assert "data" in result assert "token" in result["data"] @@ -420,7 +421,7 @@ async def test_config_flow_no_credentials(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "missing_credentials" @@ -447,7 +448,7 @@ async def test_config_flow_other_domain( result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "missing_credentials" @@ -470,7 +471,7 @@ async def test_config_flow( result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.FlowResultType.EXTERNAL_STEP + assert result.get("type") is FlowResultType.EXTERNAL_STEP result = await oauth_fixture.complete_external_step(result) assert ( result["data"].get("auth_implementation") == "fake_integration_some_client_id" @@ -535,14 +536,14 @@ async def test_config_flow_multiple_entries( result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "pick_implementation" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"implementation": "fake_integration_some_client_id2"}, ) - assert result.get("type") == data_entry_flow.FlowResultType.EXTERNAL_STEP + assert result.get("type") is FlowResultType.EXTERNAL_STEP oauth_fixture.client_id = CLIENT_ID + "2" oauth_fixture.title = CLIENT_ID + "2" result = await oauth_fixture.complete_external_step(result) @@ -572,7 +573,7 @@ async def test_config_flow_create_delete_credential( result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "missing_credentials" @@ -589,7 +590,7 @@ async def test_config_flow_with_config_credential( result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.FlowResultType.EXTERNAL_STEP + assert result.get("type") is FlowResultType.EXTERNAL_STEP oauth_fixture.title = DEFAULT_IMPORT_NAME result = await oauth_fixture.complete_external_step(result) # Uses the imported auth domain for compatibility @@ -607,7 +608,7 @@ async def test_import_without_setup(hass: HomeAssistant, config_credential) -> N result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "missing_configuration" @@ -636,7 +637,7 @@ async def test_websocket_without_platform( result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "missing_configuration" @@ -711,7 +712,7 @@ async def test_platform_with_auth_implementation( result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.FlowResultType.EXTERNAL_STEP + assert result.get("type") is FlowResultType.EXTERNAL_STEP oauth_fixture.title = DEFAULT_IMPORT_NAME result = await oauth_fixture.complete_external_step(result) # Uses the imported auth domain for compatibility @@ -769,7 +770,7 @@ async def test_name( result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.FlowResultType.EXTERNAL_STEP + assert result.get("type") is FlowResultType.EXTERNAL_STEP oauth_fixture.title = NAME result = await oauth_fixture.complete_external_step(result) assert ( diff --git a/tests/components/aussie_broadband/test_init.py b/tests/components/aussie_broadband/test_init.py index 00f7e5e7a83..825c10e92ee 100644 --- a/tests/components/aussie_broadband/test_init.py +++ b/tests/components/aussie_broadband/test_init.py @@ -5,9 +5,9 @@ from unittest.mock import patch from aiohttp import ClientConnectionError from aussiebb.exceptions import AuthenticationException, UnrecognisedServiceType -from homeassistant import data_entry_flow from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .common import setup_platform @@ -25,7 +25,7 @@ async def test_auth_failure(hass: HomeAssistant) -> None: with patch( "homeassistant.components.aussie_broadband.config_flow.AussieBroadbandConfigFlow.async_step_reauth", return_value={ - "type": data_entry_flow.FlowResultType.FORM, + "type": FlowResultType.FORM, "flow_id": "mock_flow", "step_id": "reauth_confirm", }, diff --git a/tests/components/auth/test_mfa_setup_flow.py b/tests/components/auth/test_mfa_setup_flow.py index f8c3153fba6..cb8d0d81ffe 100644 --- a/tests/components/auth/test_mfa_setup_flow.py +++ b/tests/components/auth/test_mfa_setup_flow.py @@ -1,9 +1,9 @@ """Tests for the mfa setup flow.""" -from homeassistant import data_entry_flow from homeassistant.auth import auth_manager_from_config from homeassistant.components.auth import mfa_setup_flow from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.setup import async_setup_component from tests.common import CLIENT_ID, MockUser, ensure_auth_manager_loaded @@ -75,7 +75,8 @@ async def test_ws_setup_depose_mfa( assert result["success"] flow = result["result"] - assert flow["type"] == data_entry_flow.FlowResultType.FORM + # Cannot use identity `is` check here as the value is parsed from JSON + assert flow["type"] == FlowResultType.FORM.value assert flow["handler"] == "example_module" assert flow["step_id"] == "init" assert flow["data_schema"][0] == {"type": "string", "name": "pin", "required": True} @@ -94,7 +95,8 @@ async def test_ws_setup_depose_mfa( assert result["success"] flow = result["result"] - assert flow["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + # Cannot use identity `is` check here as the value is parsed from JSON + assert flow["type"] == FlowResultType.CREATE_ENTRY.value assert flow["handler"] == "example_module" assert flow["data"]["result"] is None diff --git a/tests/components/cloud/test_account_link.py b/tests/components/cloud/test_account_link.py index 99a21734588..024118eaabf 100644 --- a/tests/components/cloud/test_account_link.py +++ b/tests/components/cloud/test_account_link.py @@ -7,9 +7,10 @@ from unittest.mock import AsyncMock, Mock, patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.cloud import account_link from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.util.dt import utcnow @@ -203,7 +204,7 @@ async def test_implementation( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP + assert result["type"] is FlowResultType.EXTERNAL_STEP assert result["url"] == "http://example.com/auth" flow_finished.set_result( diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index e2929a0f1d5..dd46921c339 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -13,6 +13,7 @@ from homeassistant.components.config import config_entries from homeassistant.config_entries import HANDLERS, ConfigFlow from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_flow, config_validation as cv from homeassistant.loader import IntegrationNotFound from homeassistant.setup import async_setup_component @@ -1305,7 +1306,7 @@ async def test_ignore_flow( result = await hass.config_entries.flow.async_init( "test", context={"source": core_ce.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM await ws_client.send_json( { diff --git a/tests/components/dialogflow/test_init.py b/tests/components/dialogflow/test_init.py index 7f5fe79d146..a977a414fe4 100644 --- a/tests/components/dialogflow/test_init.py +++ b/tests/components/dialogflow/test_init.py @@ -6,10 +6,11 @@ import json import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import dialogflow, intent_script from homeassistant.config import async_process_ha_core_config from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResultType from homeassistant.setup import async_setup_component SESSION_ID = "a9b84cec-46b6-484e-8f31-f65dba03ae6d" @@ -88,10 +89,10 @@ async def fixture(hass, hass_client_no_auth): result = await hass.config_entries.flow.async_init( "dialogflow", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM, result + assert result["type"] is FlowResultType.FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY webhook_id = result["result"].data["webhook_id"] return await hass_client_no_auth(), webhook_id diff --git a/tests/components/esphome/test_dashboard.py b/tests/components/esphome/test_dashboard.py index 51b9b535caa..e6fca268880 100644 --- a/tests/components/esphome/test_dashboard.py +++ b/tests/components/esphome/test_dashboard.py @@ -156,7 +156,7 @@ async def test_new_dashboard_fix_reauth( "unique_id": mock_config_entry.unique_id, }, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert len(mock_get_encryption_key.mock_calls) == 0 diff --git a/tests/components/esphome/test_manager.py b/tests/components/esphome/test_manager.py index e4d816089b8..e62c85b7f9a 100644 --- a/tests/components/esphome/test_manager.py +++ b/tests/components/esphome/test_manager.py @@ -607,7 +607,7 @@ async def test_connection_aborted_wrong_device( "esphome", context={"source": config_entries.SOURCE_DHCP}, data=service_info ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == "192.168.43.184" await hass.async_block_till_done() diff --git a/tests/components/flexit_bacnet/conftest.py b/tests/components/flexit_bacnet/conftest.py index fb4b2237833..d7e7962003b 100644 --- a/tests/components/flexit_bacnet/conftest.py +++ b/tests/components/flexit_bacnet/conftest.py @@ -22,7 +22,7 @@ async def flow_id(hass: HomeAssistant) -> str: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["errors"] == {} return result["flow_id"] diff --git a/tests/components/geofency/test_init.py b/tests/components/geofency/test_init.py index d5d77c1387a..389a4647e2e 100644 --- a/tests/components/geofency/test_init.py +++ b/tests/components/geofency/test_init.py @@ -5,7 +5,7 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import zone from homeassistant.components.geofency import CONF_MOBILE_BEACONS, DOMAIN from homeassistant.config import async_process_ha_core_config @@ -16,6 +16,7 @@ from homeassistant.const import ( STATE_NOT_HOME, ) 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.setup import async_setup_component from homeassistant.util import slugify @@ -157,10 +158,10 @@ async def webhook_id(hass, geofency_client): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM, result + assert result["type"] is FlowResultType.FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() return result["result"].data["webhook_id"] diff --git a/tests/components/gpslogger/test_init.py b/tests/components/gpslogger/test_init.py index cfe9d050c69..988581c804a 100644 --- a/tests/components/gpslogger/test_init.py +++ b/tests/components/gpslogger/test_init.py @@ -5,13 +5,14 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, data_entry_flow +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.gpslogger import DOMAIN, TRACKER_UPDATE from homeassistant.config import async_process_ha_core_config from homeassistant.const import STATE_HOME, STATE_NOT_HOME 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.dispatcher import DATA_DISPATCHER from homeassistant.setup import async_setup_component @@ -65,10 +66,10 @@ async def webhook_id(hass, gpslogger_client): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM, result + assert result["type"] is FlowResultType.FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() return result["result"].data["webhook_id"] diff --git a/tests/components/hisense_aehw4a1/test_init.py b/tests/components/hisense_aehw4a1/test_init.py index 1abac3421a6..c80e0d28709 100644 --- a/tests/components/hisense_aehw4a1/test_init.py +++ b/tests/components/hisense_aehw4a1/test_init.py @@ -4,9 +4,10 @@ from unittest.mock import patch from pyaehw4a1 import exceptions -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import hisense_aehw4a1 from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.setup import async_setup_component @@ -27,10 +28,10 @@ async def test_creating_entry_sets_up_climate_discovery(hass: HomeAssistant) -> ) # Confirmation form - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() diff --git a/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py b/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py index 82b6fd0c092..d04f725baf6 100644 --- a/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py +++ b/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py @@ -230,7 +230,7 @@ async def test_option_flow_install_multi_pan_addon( config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "addon_not_installed" result = await hass.config_entries.options.async_configure( @@ -239,7 +239,7 @@ async def test_option_flow_install_multi_pan_addon( "enable_multi_pan": True, }, ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "install_addon" assert result["progress_action"] == "install_addon" @@ -247,7 +247,7 @@ async def test_option_flow_install_multi_pan_addon( install_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" set_addon_options.assert_called_once_with( hass, @@ -266,7 +266,7 @@ async def test_option_flow_install_multi_pan_addon( start_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_option_flow_install_multi_pan_addon_zha( @@ -305,7 +305,7 @@ async def test_option_flow_install_multi_pan_addon_zha( zha_config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "addon_not_installed" result = await hass.config_entries.options.async_configure( @@ -314,7 +314,7 @@ async def test_option_flow_install_multi_pan_addon_zha( "enable_multi_pan": True, }, ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "install_addon" assert result["progress_action"] == "install_addon" @@ -330,7 +330,7 @@ async def test_option_flow_install_multi_pan_addon_zha( return_value=11, ): result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" set_addon_options.assert_called_once_with( hass, @@ -361,7 +361,7 @@ async def test_option_flow_install_multi_pan_addon_zha( start_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_option_flow_install_multi_pan_addon_zha_other_radio( @@ -400,7 +400,7 @@ async def test_option_flow_install_multi_pan_addon_zha_other_radio( zha_config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "addon_not_installed" result = await hass.config_entries.options.async_configure( @@ -409,7 +409,7 @@ async def test_option_flow_install_multi_pan_addon_zha_other_radio( "enable_multi_pan": True, }, ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "install_addon" assert result["progress_action"] == "install_addon" @@ -418,7 +418,7 @@ async def test_option_flow_install_multi_pan_addon_zha_other_radio( addon_info.return_value["hostname"] = "core-silabs-multiprotocol" result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" set_addon_options.assert_called_once_with( hass, @@ -437,7 +437,7 @@ async def test_option_flow_install_multi_pan_addon_zha_other_radio( start_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY # Check the ZHA entry data is not changed assert zha_config_entry.data == { @@ -469,7 +469,7 @@ async def test_option_flow_non_hassio( ): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_hassio" @@ -490,11 +490,11 @@ async def test_option_flow_addon_installed_other_device( config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "addon_installed_other_device" result = await hass.config_entries.options.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY @pytest.mark.parametrize( @@ -528,30 +528,30 @@ async def test_option_flow_addon_installed_same_device_reconfigure_unexpected_us config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "addon_menu" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "reconfigure_addon"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "notify_unknown_multipan_user" result = await hass.config_entries.options.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "change_channel" assert get_suggested(result["data_schema"].schema, "channel") == suggested_channel result = await hass.config_entries.options.async_configure( result["flow_id"], {"channel": "14"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "notify_channel_change" assert result["description_placeholders"] == {"delay_minutes": "5"} result = await hass.config_entries.options.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert mock_multiprotocol_platform.change_channel_calls == [(14, 300)] assert multipan_manager._channel == 14 @@ -601,26 +601,26 @@ async def test_option_flow_addon_installed_same_device_reconfigure_expected_user await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "addon_menu" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "reconfigure_addon"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "change_channel" assert get_suggested(result["data_schema"].schema, "channel") == suggested_channel result = await hass.config_entries.options.async_configure( result["flow_id"], {"channel": "14"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "notify_channel_change" assert result["description_placeholders"] == {"delay_minutes": "5"} result = await hass.config_entries.options.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY for domain in ["otbr", "zha"]: assert mock_multiprotocol_platforms[domain].change_channel_calls == [(14, 300)] @@ -664,14 +664,14 @@ async def test_option_flow_addon_installed_same_device_uninstall( zha_config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "addon_menu" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "uninstall_addon"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "uninstall_addon" # Make sure the flasher addon is installed @@ -685,14 +685,14 @@ async def test_option_flow_addon_installed_same_device_uninstall( result["flow_id"], {silabs_multiprotocol_addon.CONF_DISABLE_MULTI_PAN: True} ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "install_flasher_addon" assert result["progress_action"] == "install_addon" await hass.async_block_till_done() result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "uninstall_multiprotocol_addon" assert result["progress_action"] == "uninstall_multiprotocol_addon" @@ -700,7 +700,7 @@ async def test_option_flow_addon_installed_same_device_uninstall( uninstall_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_flasher_addon" assert result["progress_action"] == "start_flasher_addon" assert result["description_placeholders"] == {"addon_name": "Silicon Labs Flasher"} @@ -709,7 +709,7 @@ async def test_option_flow_addon_installed_same_device_uninstall( install_addon.assert_called_once_with(hass, "core_silabs_flasher") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY # Check the ZHA config entry data is updated assert zha_config_entry.data == { @@ -748,21 +748,21 @@ async def test_option_flow_addon_installed_same_device_do_not_uninstall_multi_pa config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "addon_menu" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "uninstall_addon"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "uninstall_addon" result = await hass.config_entries.options.async_configure( result["flow_id"], {silabs_multiprotocol_addon.CONF_DISABLE_MULTI_PAN: False} ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_option_flow_flasher_already_running_failure( @@ -791,14 +791,14 @@ async def test_option_flow_flasher_already_running_failure( config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "addon_menu" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "uninstall_addon"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "uninstall_addon" # The flasher addon is already installed and running, this is bad @@ -808,7 +808,7 @@ async def test_option_flow_flasher_already_running_failure( result = await hass.config_entries.options.async_configure( result["flow_id"], {silabs_multiprotocol_addon.CONF_DISABLE_MULTI_PAN: True} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "addon_already_running" @@ -838,14 +838,14 @@ async def test_option_flow_addon_installed_same_device_flasher_already_installed config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "addon_menu" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "uninstall_addon"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "uninstall_addon" addon_store_info.return_value = { @@ -857,7 +857,7 @@ async def test_option_flow_addon_installed_same_device_flasher_already_installed result = await hass.config_entries.options.async_configure( result["flow_id"], {silabs_multiprotocol_addon.CONF_DISABLE_MULTI_PAN: True} ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "uninstall_multiprotocol_addon" assert result["progress_action"] == "uninstall_multiprotocol_addon" @@ -865,7 +865,7 @@ async def test_option_flow_addon_installed_same_device_flasher_already_installed uninstall_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_flasher_addon" assert result["progress_action"] == "start_flasher_addon" assert result["description_placeholders"] == {"addon_name": "Silicon Labs Flasher"} @@ -879,7 +879,7 @@ async def test_option_flow_addon_installed_same_device_flasher_already_installed install_addon.assert_not_called() result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_option_flow_flasher_install_failure( @@ -919,14 +919,14 @@ async def test_option_flow_flasher_install_failure( zha_config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "addon_menu" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "uninstall_addon"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "uninstall_addon" addon_store_info.return_value = { @@ -939,7 +939,7 @@ async def test_option_flow_flasher_install_failure( result["flow_id"], {silabs_multiprotocol_addon.CONF_DISABLE_MULTI_PAN: True} ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "install_flasher_addon" assert result["progress_action"] == "install_addon" @@ -947,7 +947,7 @@ async def test_option_flow_flasher_install_failure( install_addon.assert_called_once_with(hass, "core_silabs_flasher") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "addon_install_failed" @@ -977,20 +977,20 @@ async def test_option_flow_flasher_addon_flash_failure( config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "addon_menu" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "uninstall_addon"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "uninstall_addon" result = await hass.config_entries.options.async_configure( result["flow_id"], {silabs_multiprotocol_addon.CONF_DISABLE_MULTI_PAN: True} ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "uninstall_multiprotocol_addon" assert result["progress_action"] == "uninstall_multiprotocol_addon" @@ -1000,7 +1000,7 @@ async def test_option_flow_flasher_addon_flash_failure( uninstall_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_flasher_addon" assert result["progress_action"] == "start_flasher_addon" assert result["description_placeholders"] == {"addon_name": "Silicon Labs Flasher"} @@ -1008,7 +1008,7 @@ async def test_option_flow_flasher_addon_flash_failure( await hass.async_block_till_done() result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "addon_start_failed" assert result["description_placeholders"]["addon_name"] == "Silicon Labs Flasher" @@ -1055,21 +1055,21 @@ async def test_option_flow_uninstall_migration_initiate_failure( zha_config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "addon_menu" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "uninstall_addon"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "uninstall_addon" result = await hass.config_entries.options.async_configure( result["flow_id"], {silabs_multiprotocol_addon.CONF_DISABLE_MULTI_PAN: True} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "zha_migration_failed" mock_initiate_migration.assert_called_once() @@ -1116,14 +1116,14 @@ async def test_option_flow_uninstall_migration_finish_failure( zha_config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "addon_menu" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "uninstall_addon"}, ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "uninstall_addon" result = await hass.config_entries.options.async_configure( @@ -1134,7 +1134,7 @@ async def test_option_flow_uninstall_migration_finish_failure( uninstall_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_flasher_addon" assert result["progress_action"] == "start_flasher_addon" assert result["description_placeholders"] == {"addon_name": "Silicon Labs Flasher"} @@ -1142,7 +1142,7 @@ async def test_option_flow_uninstall_migration_finish_failure( await hass.async_block_till_done() result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "zha_migration_failed" @@ -1163,7 +1163,7 @@ async def test_option_flow_do_not_install_multi_pan_addon( config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "addon_not_installed" result = await hass.config_entries.options.async_configure( @@ -1172,7 +1172,7 @@ async def test_option_flow_do_not_install_multi_pan_addon( "enable_multi_pan": False, }, ) - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_option_flow_install_multi_pan_addon_install_fails( @@ -1197,7 +1197,7 @@ async def test_option_flow_install_multi_pan_addon_install_fails( config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "addon_not_installed" result = await hass.config_entries.options.async_configure( @@ -1206,7 +1206,7 @@ async def test_option_flow_install_multi_pan_addon_install_fails( "enable_multi_pan": True, }, ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "install_addon" assert result["progress_action"] == "install_addon" @@ -1214,7 +1214,7 @@ async def test_option_flow_install_multi_pan_addon_install_fails( install_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "addon_install_failed" @@ -1240,7 +1240,7 @@ async def test_option_flow_install_multi_pan_addon_start_fails( config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "addon_not_installed" result = await hass.config_entries.options.async_configure( @@ -1249,7 +1249,7 @@ async def test_option_flow_install_multi_pan_addon_start_fails( "enable_multi_pan": True, }, ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "install_addon" assert result["progress_action"] == "install_addon" @@ -1257,7 +1257,7 @@ async def test_option_flow_install_multi_pan_addon_start_fails( install_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" set_addon_options.assert_called_once_with( hass, @@ -1276,7 +1276,7 @@ async def test_option_flow_install_multi_pan_addon_start_fails( start_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "addon_start_failed" @@ -1302,7 +1302,7 @@ async def test_option_flow_install_multi_pan_addon_set_options_fails( config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "addon_not_installed" result = await hass.config_entries.options.async_configure( @@ -1311,7 +1311,7 @@ async def test_option_flow_install_multi_pan_addon_set_options_fails( "enable_multi_pan": True, }, ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "install_addon" assert result["progress_action"] == "install_addon" @@ -1319,7 +1319,7 @@ async def test_option_flow_install_multi_pan_addon_set_options_fails( install_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "addon_set_config_failed" @@ -1342,7 +1342,7 @@ async def test_option_flow_addon_info_fails( config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "addon_info_failed" @@ -1379,7 +1379,7 @@ async def test_option_flow_install_multi_pan_addon_zha_migration_fails_step_1( zha_config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "addon_not_installed" result = await hass.config_entries.options.async_configure( @@ -1388,7 +1388,7 @@ async def test_option_flow_install_multi_pan_addon_zha_migration_fails_step_1( "enable_multi_pan": True, }, ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "install_addon" assert result["progress_action"] == "install_addon" @@ -1396,7 +1396,7 @@ async def test_option_flow_install_multi_pan_addon_zha_migration_fails_step_1( install_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "zha_migration_failed" set_addon_options.assert_not_called() @@ -1435,7 +1435,7 @@ async def test_option_flow_install_multi_pan_addon_zha_migration_fails_step_2( zha_config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "addon_not_installed" result = await hass.config_entries.options.async_configure( @@ -1444,7 +1444,7 @@ async def test_option_flow_install_multi_pan_addon_zha_migration_fails_step_2( "enable_multi_pan": True, }, ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "install_addon" assert result["progress_action"] == "install_addon" @@ -1452,7 +1452,7 @@ async def test_option_flow_install_multi_pan_addon_zha_migration_fails_step_2( install_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" set_addon_options.assert_called_once_with( hass, @@ -1471,7 +1471,7 @@ async def test_option_flow_install_multi_pan_addon_zha_migration_fails_step_2( start_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "zha_migration_failed" diff --git a/tests/components/ifttt/test_init.py b/tests/components/ifttt/test_init.py index 71bd2bc297f..44896dc0f2c 100644 --- a/tests/components/ifttt/test_init.py +++ b/tests/components/ifttt/test_init.py @@ -1,9 +1,10 @@ """Test the init file of IFTTT.""" -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import ifttt from homeassistant.config import async_process_ha_core_config from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResultType from tests.typing import ClientSessionGenerator @@ -20,10 +21,10 @@ async def test_config_flow_registers_webhook( result = await hass.config_entries.flow.async_init( "ifttt", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM, result + assert result["type"] is FlowResultType.FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY webhook_id = result["result"].data["webhook_id"] ifttt_events = [] diff --git a/tests/components/locative/test_init.py b/tests/components/locative/test_init.py index 938892ad411..fdb38c68d6c 100644 --- a/tests/components/locative/test_init.py +++ b/tests/components/locative/test_init.py @@ -5,12 +5,13 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import locative from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.components.locative import DOMAIN, TRACKER_UPDATE from homeassistant.config import async_process_ha_core_config from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers.dispatcher import DATA_DISPATCHER from homeassistant.setup import async_setup_component @@ -40,10 +41,10 @@ async def webhook_id(hass, locative_client): result = await hass.config_entries.flow.async_init( "locative", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM, result + assert result["type"] is FlowResultType.FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() return result["result"].data["webhook_id"] diff --git a/tests/components/mailgun/test_init.py b/tests/components/mailgun/test_init.py index a36051bd102..e2274f03d23 100644 --- a/tests/components/mailgun/test_init.py +++ b/tests/components/mailgun/test_init.py @@ -5,11 +5,12 @@ import hmac import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import mailgun, webhook from homeassistant.config import async_process_ha_core_config from homeassistant.const import CONF_API_KEY, CONF_DOMAIN from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResultType from homeassistant.setup import async_setup_component API_KEY = "abc123" @@ -38,10 +39,10 @@ async def webhook_id_with_api_key(hass): result = await hass.config_entries.flow.async_init( "mailgun", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM, result + assert result["type"] is FlowResultType.FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY return result["result"].data["webhook_id"] @@ -58,10 +59,10 @@ async def webhook_id_without_api_key(hass): result = await hass.config_entries.flow.async_init( "mailgun", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM, result + assert result["type"] is FlowResultType.FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY return result["result"].data["webhook_id"] diff --git a/tests/components/ps4/test_init.py b/tests/components/ps4/test_init.py index 238c3c15112..180f51295ac 100644 --- a/tests/components/ps4/test_init.py +++ b/tests/components/ps4/test_init.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock, patch -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import ps4 from homeassistant.components.media_player import ( ATTR_MEDIA_CONTENT_TYPE, @@ -27,6 +27,7 @@ from homeassistant.const import ( CONF_TOKEN, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -47,7 +48,7 @@ MOCK_FLOW_RESULT = { "version": VERSION, "minor_version": 1, "handler": DOMAIN, - "type": data_entry_flow.FlowResultType.CREATE_ENTRY, + "type": FlowResultType.CREATE_ENTRY, "title": "test_ps4", "data": MOCK_DATA, "options": {}, @@ -132,7 +133,7 @@ async def test_creating_entry_sets_up_media_player(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() diff --git a/tests/components/sonos/test_init.py b/tests/components/sonos/test_init.py index 97fdc27d461..f8ac5fc6dbf 100644 --- a/tests/components/sonos/test_init.py +++ b/tests/components/sonos/test_init.py @@ -7,7 +7,7 @@ from unittest.mock import Mock, patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import sonos, zeroconf from homeassistant.components.sonos import SonosDiscoveryManager from homeassistant.components.sonos.const import ( @@ -16,6 +16,7 @@ from homeassistant.components.sonos.const import ( ) from homeassistant.components.sonos.exception import SonosUpdateError from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.setup import async_setup_component @@ -46,10 +47,10 @@ async def test_creating_entry_sets_up_media_player( ) # Confirmation form - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done(wait_background_tasks=True) diff --git a/tests/components/synology_dsm/test_init.py b/tests/components/synology_dsm/test_init.py index 25c4d69dfee..13d568e6137 100644 --- a/tests/components/synology_dsm/test_init.py +++ b/tests/components/synology_dsm/test_init.py @@ -4,7 +4,6 @@ from unittest.mock import MagicMock, patch from synology_dsm.exceptions import SynologyDSMLoginInvalidException -from homeassistant import data_entry_flow from homeassistant.components.synology_dsm.const import DOMAIN, SERVICES from homeassistant.const import ( CONF_HOST, @@ -15,6 +14,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .consts import HOST, MACS, PASSWORD, PORT, USE_SSL, USERNAME @@ -57,7 +57,7 @@ async def test_reauth_triggered(hass: HomeAssistant) -> None: patch( "homeassistant.components.synology_dsm.config_flow.SynologyDSMFlowHandler.async_step_reauth", return_value={ - "type": data_entry_flow.FlowResultType.FORM, + "type": FlowResultType.FORM, "flow_id": "mock_flow", "step_id": "reauth_confirm", }, diff --git a/tests/components/systemmonitor/test_init.py b/tests/components/systemmonitor/test_init.py index 2142eecb8b4..705f86f8048 100644 --- a/tests/components/systemmonitor/test_init.py +++ b/tests/components/systemmonitor/test_init.py @@ -38,7 +38,7 @@ async def test_adding_processor_to_options( mock_added_config_entry.entry_id ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -49,7 +49,7 @@ async def test_adding_processor_to_options( ) await hass.async_block_till_done() - assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "binary_sensor": { CONF_PROCESS: ["python3", "pip", "systemd"], diff --git a/tests/components/systemmonitor/test_repairs.py b/tests/components/systemmonitor/test_repairs.py index 4c65f531acc..d054bfa99a4 100644 --- a/tests/components/systemmonitor/test_repairs.py +++ b/tests/components/systemmonitor/test_repairs.py @@ -94,7 +94,8 @@ async def test_migrate_process_sensor( assert resp.status == HTTPStatus.OK data = await resp.json() - assert data["type"] == FlowResultType.CREATE_ENTRY + # Cannot use identity `is` check here as the value is parsed from JSON + assert data["type"] == FlowResultType.CREATE_ENTRY.value await hass.async_block_till_done() state = hass.states.get("binary_sensor.system_monitor_process_python3") @@ -192,5 +193,6 @@ async def test_other_fixable_issues( assert resp.status == HTTPStatus.OK data = await resp.json() - assert data["type"] == FlowResultType.CREATE_ENTRY + # Cannot use identity `is` check here as the value is parsed from JSON + assert data["type"] == FlowResultType.CREATE_ENTRY.value await hass.async_block_till_done() diff --git a/tests/components/traccar/test_init.py b/tests/components/traccar/test_init.py index e0ce876a97f..79e5c877563 100644 --- a/tests/components/traccar/test_init.py +++ b/tests/components/traccar/test_init.py @@ -5,13 +5,14 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, data_entry_flow +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.traccar import DOMAIN, TRACKER_UPDATE from homeassistant.config import async_process_ha_core_config from homeassistant.const import STATE_HOME, STATE_NOT_HOME 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.dispatcher import DATA_DISPATCHER from homeassistant.setup import async_setup_component @@ -65,10 +66,10 @@ async def webhook_id_fixture(hass, client): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM, result + assert result["type"] is FlowResultType.FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() return result["result"].data["webhook_id"] diff --git a/tests/components/twilio/test_init.py b/tests/components/twilio/test_init.py index 77e6afe3a12..8efa1c24742 100644 --- a/tests/components/twilio/test_init.py +++ b/tests/components/twilio/test_init.py @@ -1,9 +1,10 @@ """Test the init file of Twilio.""" -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import twilio from homeassistant.config import async_process_ha_core_config from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResultType from tests.typing import ClientSessionGenerator @@ -19,10 +20,10 @@ async def test_config_flow_registers_webhook( result = await hass.config_entries.flow.async_init( "twilio", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM, result + assert result["type"] is FlowResultType.FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.CREATE_ENTRY webhook_id = result["result"].data["webhook_id"] twilio_events = [] diff --git a/tests/components/youless/test_config_flows.py b/tests/components/youless/test_config_flows.py index bc53c55539b..90f17e04efb 100644 --- a/tests/components/youless/test_config_flows.py +++ b/tests/components/youless/test_config_flows.py @@ -26,7 +26,7 @@ async def test_full_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {} assert result.get("step_id") == "user" @@ -42,7 +42,7 @@ async def test_full_flow(hass: HomeAssistant) -> None: {"host": "localhost"}, ) - assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "localhost" assert len(mocked_youless.mock_calls) == 1 @@ -53,7 +53,7 @@ async def test_not_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == FlowResultType.FORM + assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {} assert result.get("step_id") == "user" @@ -67,5 +67,5 @@ async def test_not_found(hass: HomeAssistant) -> None: {"host": "localhost"}, ) - assert result2.get("type") == FlowResultType.FORM + assert result2.get("type") is FlowResultType.FORM assert len(mocked_youless.mock_calls) == 1 From 772d9f754a000c30d3fdb4944e8a71ad2af91515 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 5 Apr 2024 11:32:59 +0200 Subject: [PATCH 290/967] Fix Downloader YAML import (#114844) --- .../components/downloader/__init__.py | 10 +++- tests/components/downloader/test_init.py | 51 +++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 tests/components/downloader/test_init.py diff --git a/homeassistant/components/downloader/__init__.py b/homeassistant/components/downloader/__init__.py index 3ca503a2167..d110c28785a 100644 --- a/homeassistant/components/downloader/__init__.py +++ b/homeassistant/components/downloader/__init__.py @@ -43,6 +43,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if DOMAIN not in config: return True + hass.async_create_task(_async_import_config(hass, config), eager_start=True) + return True + + +async def _async_import_config(hass: HomeAssistant, config: ConfigType) -> None: + """Import the Downloader component from the YAML file.""" + import_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, @@ -62,7 +69,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass, DOMAIN, f"deprecated_yaml_{DOMAIN}", - breaks_in_ha_version="2024.9.0", + breaks_in_ha_version="2024.10.0", is_fixable=False, issue_domain=DOMAIN, severity=IssueSeverity.WARNING, @@ -72,7 +79,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: "integration_title": "Downloader", }, ) - return True async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/tests/components/downloader/test_init.py b/tests/components/downloader/test_init.py new file mode 100644 index 00000000000..8cd0d00b1ab --- /dev/null +++ b/tests/components/downloader/test_init.py @@ -0,0 +1,51 @@ +"""Tests for the downloader component init.""" + +from unittest.mock import patch + +from homeassistant.components.downloader import ( + CONF_DOWNLOAD_DIR, + DOMAIN, + SERVICE_DOWNLOAD_FILE, +) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +async def test_initialization(hass: HomeAssistant) -> None: + """Test the initialization of the downloader component.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_DOWNLOAD_DIR: "/test_dir", + }, + ) + config_entry.add_to_hass(hass) + with patch("os.path.isdir", return_value=True): + assert await hass.config_entries.async_setup(config_entry.entry_id) + + assert hass.services.has_service(DOMAIN, SERVICE_DOWNLOAD_FILE) + assert config_entry.state is ConfigEntryState.LOADED + + +async def test_import(hass: HomeAssistant) -> None: + """Test the import of the downloader component.""" + with patch("os.path.isdir", return_value=True): + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + CONF_DOWNLOAD_DIR: "/test_dir", + }, + }, + ) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + assert config_entry.data == {CONF_DOWNLOAD_DIR: "/test_dir"} + assert config_entry.state is ConfigEntryState.LOADED + assert hass.services.has_service(DOMAIN, SERVICE_DOWNLOAD_FILE) From 8ca6e12ddd81c579db94c269d8e5822e2f3ee6a7 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 5 Apr 2024 11:44:51 +0200 Subject: [PATCH 291/967] Fix cast dashboard in media browser (#114924) --- homeassistant/components/lovelace/cast.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/lovelace/cast.py b/homeassistant/components/lovelace/cast.py index 02f5d0c0478..82a92b94ae5 100644 --- a/homeassistant/components/lovelace/cast.py +++ b/homeassistant/components/lovelace/cast.py @@ -179,7 +179,7 @@ async def _get_dashboard_info(hass, url_path): "views": views, } - if config is None: + if config is None or "views" not in config: return data for idx, view in enumerate(config["views"]): From 63392ea1c2a079ca2306631bd78c89b312fa82a0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 5 Apr 2024 12:16:27 +0200 Subject: [PATCH 292/967] Bump Wandalen/wretry.action from 3.0.1 to 3.1.0 (#114916) --- .github/workflows/ci.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 71055e0ea72..b0efe812062 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1088,7 +1088,7 @@ jobs: pattern: coverage-* - name: Upload coverage to Codecov (full coverage) if: needs.info.outputs.test_full_suite == 'true' - uses: Wandalen/wretry.action@v3.0.1 + uses: Wandalen/wretry.action@v3.1.0 with: action: codecov/codecov-action@v3.1.3 with: | @@ -1099,7 +1099,7 @@ jobs: attempt_delay: 30000 - name: Upload coverage to Codecov (partial coverage) if: needs.info.outputs.test_full_suite == 'false' - uses: Wandalen/wretry.action@v3.0.1 + uses: Wandalen/wretry.action@v3.1.0 with: action: codecov/codecov-action@v3.1.3 with: | @@ -1234,7 +1234,7 @@ jobs: pattern: coverage-* - name: Upload coverage to Codecov (full coverage) if: needs.info.outputs.test_full_suite == 'true' - uses: Wandalen/wretry.action@v3.0.1 + uses: Wandalen/wretry.action@v3.1.0 with: action: codecov/codecov-action@v3.1.3 with: | @@ -1245,7 +1245,7 @@ jobs: attempt_delay: 30000 - name: Upload coverage to Codecov (partial coverage) if: needs.info.outputs.test_full_suite == 'false' - uses: Wandalen/wretry.action@v3.0.1 + uses: Wandalen/wretry.action@v3.1.0 with: action: codecov/codecov-action@v3.1.3 with: | From 69f8de2c5959c26d960d0698ca534132ceb8a0c2 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 5 Apr 2024 12:32:09 +0200 Subject: [PATCH 293/967] Create right import issues in Downloader (#114922) * Create right import issues in Downloader * Create right import issues in Downloader * Create right import issues in Downloader * Create right import issues in Downloader * Fix * Fix * Fix * Fix * Apply suggestions from code review Co-authored-by: Martin Hjelmare * Fix --------- Co-authored-by: Martin Hjelmare --- .../components/downloader/__init__.py | 57 +++++++++++------ .../components/downloader/config_flow.py | 12 ++-- .../components/downloader/strings.json | 8 +-- .../components/downloader/test_config_flow.py | 16 +++++ tests/components/downloader/test_init.py | 64 ++++++++++++++++++- 5 files changed, 125 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/downloader/__init__.py b/homeassistant/components/downloader/__init__.py index d110c28785a..3fded1215c4 100644 --- a/homeassistant/components/downloader/__init__.py +++ b/homeassistant/components/downloader/__init__.py @@ -11,7 +11,11 @@ import requests import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.core import ( + DOMAIN as HOMEASSISTANT_DOMAIN, + HomeAssistant, + ServiceCall, +) from homeassistant.data_entry_flow import FlowResultType import homeassistant.helpers.config_validation as cv from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue @@ -43,7 +47,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if DOMAIN not in config: return True - hass.async_create_task(_async_import_config(hass, config), eager_start=True) + hass.async_create_task(_async_import_config(hass, config)) return True @@ -58,27 +62,40 @@ async def _async_import_config(hass: HomeAssistant, config: ConfigType) -> None: }, ) - translation_key = "deprecated_yaml" if ( import_result["type"] == FlowResultType.ABORT - and import_result["reason"] == "import_failed" + and import_result["reason"] != "single_instance_allowed" ): - translation_key = "import_failed" - - async_create_issue( - hass, - DOMAIN, - f"deprecated_yaml_{DOMAIN}", - breaks_in_ha_version="2024.10.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key=translation_key, - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "Downloader", - }, - ) + async_create_issue( + hass, + DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.10.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="directory_does_not_exist", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Downloader", + "url": "/config/integrations/dashboard/add?domain=downloader", + }, + ) + else: + async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.10.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Downloader", + }, + ) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/downloader/config_flow.py b/homeassistant/components/downloader/config_flow.py index 15af8b56163..94b33f4e93f 100644 --- a/homeassistant/components/downloader/config_flow.py +++ b/homeassistant/components/downloader/config_flow.py @@ -46,12 +46,16 @@ class DownloaderConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_import( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: + async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult: """Handle a flow initiated by configuration file.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") - return await self.async_step_user(user_input) + try: + await self._validate_input(user_input) + except DirectoryDoesNotExist: + return self.async_abort(reason="directory_does_not_exist") + return self.async_create_entry(title=DEFAULT_NAME, data=user_input) async def _validate_input(self, user_input: dict[str, Any]) -> None: """Validate the user input if the directory exists.""" diff --git a/homeassistant/components/downloader/strings.json b/homeassistant/components/downloader/strings.json index 77dd0abd9d3..4cadabf96c6 100644 --- a/homeassistant/components/downloader/strings.json +++ b/homeassistant/components/downloader/strings.json @@ -37,13 +37,9 @@ } }, "issues": { - "deprecated_yaml": { - "title": "The {integration_title} YAML configuration is being removed", - "description": "Configuring {integration_title} using YAML is being removed.\n\nYour configuration is already imported.\n\nRemove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue." - }, - "import_failed": { + "directory_does_not_exist": { "title": "The {integration_title} failed to import", - "description": "The {integration_title} integration failed to import.\n\nPlease check the logs for more details." + "description": "The {integration_title} integration failed to import because the configured directory does not exist.\n\nEnsure the directory exists and restart Home Assistant to try again or remove the {integration_title} configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." } } } diff --git a/tests/components/downloader/test_config_flow.py b/tests/components/downloader/test_config_flow.py index 45c2302b605..b561fae98e9 100644 --- a/tests/components/downloader/test_config_flow.py +++ b/tests/components/downloader/test_config_flow.py @@ -99,3 +99,19 @@ async def test_import_flow_success(hass: HomeAssistant) -> None: assert result["title"] == "Downloader" assert result["data"] == {} assert result["options"] == {} + + +async def test_import_flow_directory_not_found(hass: HomeAssistant) -> None: + """Test import flow.""" + with patch("os.path.isdir", return_value=False): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_DOWNLOAD_DIR: "download_dir", + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "directory_does_not_exist" diff --git a/tests/components/downloader/test_init.py b/tests/components/downloader/test_init.py index 8cd0d00b1ab..5832c0402b4 100644 --- a/tests/components/downloader/test_init.py +++ b/tests/components/downloader/test_init.py @@ -8,7 +8,8 @@ from homeassistant.components.downloader import ( SERVICE_DOWNLOAD_FILE, ) from homeassistant.config_entries import ConfigEntryState -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.helpers import issue_registry as ir from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -30,7 +31,7 @@ async def test_initialization(hass: HomeAssistant) -> None: assert config_entry.state is ConfigEntryState.LOADED -async def test_import(hass: HomeAssistant) -> None: +async def test_import(hass: HomeAssistant, issue_registry: ir.IssueRegistry) -> None: """Test the import of the downloader component.""" with patch("os.path.isdir", return_value=True): assert await async_setup_component( @@ -49,3 +50,62 @@ async def test_import(hass: HomeAssistant) -> None: assert config_entry.data == {CONF_DOWNLOAD_DIR: "/test_dir"} assert config_entry.state is ConfigEntryState.LOADED assert hass.services.has_service(DOMAIN, SERVICE_DOWNLOAD_FILE) + assert len(issue_registry.issues) == 1 + issue = issue_registry.async_get_issue( + issue_id="deprecated_yaml_downloader", domain=HOMEASSISTANT_DOMAIN + ) + assert issue + + +async def test_import_directory_missing( + hass: HomeAssistant, issue_registry: ir.IssueRegistry +) -> None: + """Test the import of the downloader component.""" + with patch("os.path.isdir", return_value=False): + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + CONF_DOWNLOAD_DIR: "/test_dir", + }, + }, + ) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 0 + assert len(issue_registry.issues) == 1 + issue = issue_registry.async_get_issue( + issue_id="deprecated_yaml_downloader", domain=DOMAIN + ) + assert issue + + +async def test_import_already_exists( + hass: HomeAssistant, issue_registry: ir.IssueRegistry +) -> None: + """Test the import of the downloader component.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_DOWNLOAD_DIR: "/test_dir", + }, + ) + config_entry.add_to_hass(hass) + with patch("os.path.isdir", return_value=True): + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + CONF_DOWNLOAD_DIR: "/test_dir", + }, + }, + ) + await hass.async_block_till_done() + + assert len(issue_registry.issues) == 1 + issue = issue_registry.async_get_issue( + issue_id="deprecated_yaml_downloader", domain=HOMEASSISTANT_DOMAIN + ) + assert issue From 370c902eb9d34a044f9457324afdb61a37cfba75 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 5 Apr 2024 14:21:24 +0200 Subject: [PATCH 294/967] Fix ROVA validation (#114938) * Fix ROVA validation * Fix ROVA validation --- homeassistant/components/rova/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/rova/sensor.py b/homeassistant/components/rova/sensor.py index e510bcf0caf..f63b9893c02 100644 --- a/homeassistant/components/rova/sensor.py +++ b/homeassistant/components/rova/sensor.py @@ -54,7 +54,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_HOUSE_NUMBER_SUFFIX, default=""): cv.string, vol.Optional(CONF_NAME, default="Rova"): cv.string, vol.Optional(CONF_MONITORED_CONDITIONS, default=["bio"]): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)] + cv.ensure_list, [vol.In(["bio", "paper", "plastic", "residual"])] ), } ) From dd8de14cc57b3217c24061413d3130750c9765a3 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Fri, 5 Apr 2024 15:28:33 +0200 Subject: [PATCH 295/967] Update `person` to use `_attr_*` and thus cached properties (#114590) * Update `person` to use `_attr_*` and thus cached properties * Make func * Just use attribute * Move to init --- homeassistant/components/person/__init__.py | 90 ++++++++++----------- 1 file changed, 44 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index 82b3b5ef7bd..cf4059dcc6b 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -402,7 +402,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -class Person(collection.CollectionEntity, RestoreEntity): +class Person( + collection.CollectionEntity, + RestoreEntity, +): """Represent a tracked person.""" _entity_component_unrecorded_attributes = frozenset({ATTR_DEVICE_TRACKERS}) @@ -417,8 +420,18 @@ class Person(collection.CollectionEntity, RestoreEntity): self._longitude: float | None = None self._gps_accuracy: float | None = None self._source: str | None = None - self._state: str | None = None self._unsub_track_device: Callable[[], None] | None = None + self._attr_state: str | None = None + self.device_trackers: list[str] = [] + + self._attr_unique_id = config[CONF_ID] + self._set_attrs_from_config() + + def _set_attrs_from_config(self) -> None: + """Set attributes from config.""" + self._attr_name = self._config[CONF_NAME] + self._attr_entity_picture = self._config.get(CONF_PICTURE) + self.device_trackers = self._config[CONF_DEVICE_TRACKERS] @classmethod def from_storage(cls, config: ConfigType) -> Self: @@ -434,48 +447,6 @@ class Person(collection.CollectionEntity, RestoreEntity): person.editable = False return person - @property - def name(self) -> str: - """Return the name of the entity.""" - return self._config[CONF_NAME] - - @property - def entity_picture(self) -> str | None: - """Return entity picture.""" - return self._config.get(CONF_PICTURE) - - @property - def state(self) -> str | None: - """Return the state of the person.""" - return self._state - - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return the state attributes of the person.""" - data: dict[str, Any] = {ATTR_EDITABLE: self.editable, ATTR_ID: self.unique_id} - if self._latitude is not None: - data[ATTR_LATITUDE] = self._latitude - if self._longitude is not None: - data[ATTR_LONGITUDE] = self._longitude - if self._gps_accuracy is not None: - data[ATTR_GPS_ACCURACY] = self._gps_accuracy - if self._source is not None: - data[ATTR_SOURCE] = self._source - if (user_id := self._config.get(CONF_USER_ID)) is not None: - data[ATTR_USER_ID] = user_id - data[ATTR_DEVICE_TRACKERS] = self.device_trackers - return data - - @property - def unique_id(self) -> str: - """Return a unique ID for the person.""" - return self._config[CONF_ID] - - @property - def device_trackers(self) -> list[str]: - """Return the device trackers for the person.""" - return self._config[CONF_DEVICE_TRACKERS] - async def async_added_to_hass(self) -> None: """Register device trackers.""" await super().async_added_to_hass() @@ -495,6 +466,9 @@ class Person(collection.CollectionEntity, RestoreEntity): self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_START, _async_person_start_hass ) + # Update extra state attributes now + # as there are attributes that can already be set + self._update_extra_state_attributes() async def async_update_config(self, config: ConfigType) -> None: """Handle when the config is updated.""" @@ -504,6 +478,7 @@ class Person(collection.CollectionEntity, RestoreEntity): def _async_update_config(self, config: ConfigType) -> None: """Handle when the config is updated.""" self._config = config + self._set_attrs_from_config() if self._unsub_track_device is not None: self._unsub_track_device() @@ -550,12 +525,13 @@ class Person(collection.CollectionEntity, RestoreEntity): if latest: self._parse_source_state(latest) else: - self._state = None + self._attr_state = None self._source = None self._latitude = None self._longitude = None self._gps_accuracy = None + self._update_extra_state_attributes() self.async_write_ha_state() @callback @@ -564,12 +540,34 @@ class Person(collection.CollectionEntity, RestoreEntity): This is a device tracker state or the restored person state. """ - self._state = state.state + self._attr_state = state.state self._source = state.entity_id self._latitude = state.attributes.get(ATTR_LATITUDE) self._longitude = state.attributes.get(ATTR_LONGITUDE) self._gps_accuracy = state.attributes.get(ATTR_GPS_ACCURACY) + @callback + def _update_extra_state_attributes(self) -> None: + """Update extra state attributes.""" + data: dict[str, Any] = { + ATTR_EDITABLE: self.editable, + ATTR_ID: self.unique_id, + ATTR_DEVICE_TRACKERS: self.device_trackers, + } + + if self._latitude is not None: + data[ATTR_LATITUDE] = self._latitude + if self._longitude is not None: + data[ATTR_LONGITUDE] = self._longitude + if self._gps_accuracy is not None: + data[ATTR_GPS_ACCURACY] = self._gps_accuracy + if self._source is not None: + data[ATTR_SOURCE] = self._source + if (user_id := self._config.get(CONF_USER_ID)) is not None: + data[ATTR_USER_ID] = user_id + + self._attr_extra_state_attributes = data + @websocket_api.websocket_command({vol.Required(CONF_TYPE): "person/list"}) def ws_list_person( From 0b01326f9fd36371f47b3e7028aae5d6df34c8d9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 5 Apr 2024 17:16:55 +0200 Subject: [PATCH 296/967] Use is in ConfigEntryState enum comparison in tests (A-M) (#114925) --- tests/components/agent_dvr/test_init.py | 6 +++--- .../components/aladdin_connect/test_cover.py | 2 +- tests/components/aladdin_connect/test_init.py | 12 +++++------ .../analytics_insights/test_init.py | 4 ++-- .../components/android_ip_webcam/test_init.py | 6 +++--- .../components/androidtv/test_media_player.py | 2 +- tests/components/anthemav/test_init.py | 8 ++++---- tests/components/asuswrt/test_diagnostics.py | 2 +- tests/components/automation/test_blueprint.py | 4 ++-- tests/components/automation/test_init.py | 4 ++-- tests/components/axis/test_init.py | 10 +++++----- tests/components/azure_devops/test_init.py | 10 +++++----- tests/components/azure_event_hub/conftest.py | 4 ++-- tests/components/azure_event_hub/test_init.py | 4 ++-- tests/components/baf/test_init.py | 2 +- tests/components/balboa/test_init.py | 8 ++++---- tests/components/blebox/test_config_flow.py | 5 +++-- tests/components/blue_current/test_init.py | 4 ++-- tests/components/bluetooth/test_init.py | 10 +++++----- tests/components/bluetooth/test_scanner.py | 4 ++-- tests/components/bring/test_init.py | 4 ++-- tests/components/caldav/test_init.py | 8 ++++---- tests/components/coinbase/test_init.py | 6 +++--- tests/components/daikin/test_init.py | 4 ++-- .../components/device_automation/test_init.py | 19 +++++++++--------- .../device_tracker/test_config_entry.py | 8 ++++---- .../devolo_home_control/test_diagnostics.py | 2 +- .../devolo_home_network/test_diagnostics.py | 2 +- tests/components/dlink/test_init.py | 6 +++--- tests/components/dnsip/test_config_flow.py | 3 ++- tests/components/dnsip/test_init.py | 7 +++---- .../components/dremel_3d_printer/test_init.py | 6 +++--- tests/components/dsmr/test_sensor.py | 4 ++-- .../dwd_weather_warnings/test_init.py | 2 +- tests/components/dynalite/test_config_flow.py | 9 +++++---- tests/components/eafm/test_sensor.py | 4 ++-- tests/components/efergy/test_init.py | 6 +++--- .../energenie_power_sockets/test_init.py | 2 +- .../energenie_power_sockets/test_switch.py | 2 +- tests/components/esphome/test_dashboard.py | 8 ++++---- .../components/evil_genius_labs/test_init.py | 4 ++-- tests/components/fastdotcom/test_init.py | 10 +++++----- tests/components/fitbit/test_init.py | 20 +++++++++---------- tests/components/flexit_bacnet/test_init.py | 4 ++-- tests/components/flipr/test_init.py | 2 +- tests/components/flux_led/test_init.py | 16 +++++++-------- .../components/forecast_solar/test_energy.py | 2 +- tests/components/forecast_solar/test_init.py | 2 +- tests/components/freedompro/test_init.py | 6 +++--- tests/components/fritz/test_button.py | 12 +++++------ tests/components/fritz/test_diagnostics.py | 2 +- tests/components/fritz/test_image.py | 6 +++--- tests/components/fritz/test_init.py | 10 +++++----- tests/components/fritz/test_sensor.py | 2 +- tests/components/fritz/test_switch.py | 2 +- tests/components/fritz/test_update.py | 8 ++++---- tests/components/generic/test_config_flow.py | 7 ++++--- tests/components/github/common.py | 4 ++-- tests/components/glances/test_init.py | 4 ++-- tests/components/goalzero/test_init.py | 6 +++--- tests/components/google/test_init.py | 4 ++-- tests/components/google_tasks/test_init.py | 2 +- tests/components/hardkernel/test_init.py | 2 +- tests/components/hassio/test_sensor.py | 3 ++- tests/components/holiday/test_init.py | 4 ++-- .../homeassistant_green/test_init.py | 2 +- .../homeassistant_sky_connect/test_init.py | 6 +++--- .../homeassistant_yellow/test_init.py | 6 +++--- tests/components/homekit/test_init.py | 2 +- .../homekit_controller/test_device_trigger.py | 4 ++-- .../homekit_controller/test_init.py | 8 ++++---- .../husqvarna_automower/test_init.py | 10 +++++----- tests/components/insteon/test_config_flow.py | 3 ++- tests/components/iotawatt/test_init.py | 4 ++-- .../islamic_prayer_times/test_init.py | 10 +++++----- tests/components/kmtronic/test_config_flow.py | 2 +- tests/components/knx/test_init.py | 4 ++-- tests/components/lacrosse_view/test_init.py | 14 ++++++------- tests/components/lacrosse_view/test_sensor.py | 12 +++++------ .../components/lamarzocco/test_config_flow.py | 4 ++-- tests/components/laundrify/test_init.py | 8 ++++---- tests/components/lawn_mower/test_init.py | 4 ++-- tests/components/lcn/test_init.py | 14 ++++++------- tests/components/lidarr/test_init.py | 6 +++--- tests/components/lifx/test_init.py | 12 +++++------ .../linear_garage_door/test_coordinator.py | 4 ++-- .../linear_garage_door/test_cover.py | 2 +- .../linear_garage_door/test_init.py | 4 ++-- tests/components/local_calendar/test_init.py | 6 +++--- tests/components/local_ip/test_init.py | 6 +++--- tests/components/local_todo/test_init.py | 6 +++--- tests/components/lyric/test_config_flow.py | 3 ++- tests/components/matter/test_init.py | 8 ++++---- tests/components/mikrotik/test_init.py | 8 ++++---- .../components/minecraft_server/test_init.py | 16 +++++++-------- tests/components/modem_callerid/test_init.py | 4 ++-- tests/components/mqtt/test_util.py | 16 +++++++-------- tests/components/mystrom/test_init.py | 8 ++++---- tests/components/myuplink/test_init.py | 4 ++-- 99 files changed, 302 insertions(+), 295 deletions(-) diff --git a/tests/components/agent_dvr/test_init.py b/tests/components/agent_dvr/test_init.py index 5b360430b78..7f546a190a7 100644 --- a/tests/components/agent_dvr/test_init.py +++ b/tests/components/agent_dvr/test_init.py @@ -31,7 +31,7 @@ async def test_setup_config_and_unload( ) -> None: """Test setup and unload.""" entry = await init_integration(hass, aioclient_mock) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert entry.data == CONF_DATA @@ -50,7 +50,7 @@ async def test_async_setup_entry_not_ready(hass: HomeAssistant) -> None: side_effect=AgentError, ): await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY with _patch_init_agent(await _create_mocked_agent(available=False)): await hass.config_entries.async_reload(entry.entry_id) - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/aladdin_connect/test_cover.py b/tests/components/aladdin_connect/test_cover.py index 9ad9febc762..082ade75ab9 100644 --- a/tests/components/aladdin_connect/test_cover.py +++ b/tests/components/aladdin_connect/test_cover.py @@ -114,7 +114,7 @@ async def test_cover_operation( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert COVER_DOMAIN in hass.config.components diff --git a/tests/components/aladdin_connect/test_init.py b/tests/components/aladdin_connect/test_init.py index c995fb5074d..623c121957b 100644 --- a/tests/components/aladdin_connect/test_init.py +++ b/tests/components/aladdin_connect/test_init.py @@ -93,7 +93,7 @@ async def test_setup_component_no_error(hass: HomeAssistant) -> None: assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -135,12 +135,12 @@ async def test_load_and_unload( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert await config_entry.async_unload(hass) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED async def test_stale_device_removal( @@ -190,7 +190,7 @@ async def test_stale_device_removal( assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 device_registry = dr.async_get(hass) @@ -220,7 +220,7 @@ async def test_stale_device_removal( assert await config_entry.async_unload(hass) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED mock_aladdinconnect_api.get_doors = AsyncMock(return_value=[DEVICE_CONFIG_OPEN]) with patch( @@ -230,7 +230,7 @@ async def test_stale_device_removal( assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 device_entries = dr.async_entries_for_config_entry( diff --git a/tests/components/analytics_insights/test_init.py b/tests/components/analytics_insights/test_init.py index 4f1ca7cda95..8543a02c025 100644 --- a/tests/components/analytics_insights/test_init.py +++ b/tests/components/analytics_insights/test_init.py @@ -21,9 +21,9 @@ async def test_load_unload_entry( await setup_integration(hass, mock_config_entry) entry = hass.config_entries.async_entries(DOMAIN)[0] - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED await hass.config_entries.async_remove(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/android_ip_webcam/test_init.py b/tests/components/android_ip_webcam/test_init.py index 9aa677b8708..70ecdc9271e 100644 --- a/tests/components/android_ip_webcam/test_init.py +++ b/tests/components/android_ip_webcam/test_init.py @@ -30,7 +30,7 @@ async def test_successful_config_entry( await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED async def test_setup_failed_connection_error( @@ -47,7 +47,7 @@ async def test_setup_failed_connection_error( await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_setup_failed_invalid_auth( @@ -64,7 +64,7 @@ async def test_setup_failed_invalid_auth( await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_unload_entry(hass: HomeAssistant, aioclient_mock_fixture) -> None: diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index 63923a57996..fe6b9962d14 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -591,7 +591,7 @@ async def test_setup_fail( await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY assert state is None diff --git a/tests/components/anthemav/test_init.py b/tests/components/anthemav/test_init.py index 6989ffc69c5..45614a1d885 100644 --- a/tests/components/anthemav/test_init.py +++ b/tests/components/anthemav/test_init.py @@ -6,7 +6,7 @@ from unittest.mock import ANY, AsyncMock, patch from anthemav.device_error import DeviceError import pytest -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant @@ -24,13 +24,13 @@ async def test_load_unload_config_entry( mock_connection_create.assert_called_with( host="1.1.1.1", port=14999, update_callback=ANY ) - assert init_integration.state == config_entries.ConfigEntryState.LOADED + assert init_integration.state is ConfigEntryState.LOADED # unload await hass.config_entries.async_unload(init_integration.entry_id) await hass.async_block_till_done() # assert unload and avr is closed - assert init_integration.state == config_entries.ConfigEntryState.NOT_LOADED + assert init_integration.state is ConfigEntryState.NOT_LOADED mock_anthemav.close.assert_called_once() @@ -46,7 +46,7 @@ async def test_config_entry_not_ready_when_oserror( 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 config_entries.ConfigEntryState.SETUP_RETRY + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY async def test_anthemav_dispatcher_signal( diff --git a/tests/components/asuswrt/test_diagnostics.py b/tests/components/asuswrt/test_diagnostics.py index 1c09dd29adc..207f3ba25f0 100644 --- a/tests/components/asuswrt/test_diagnostics.py +++ b/tests/components/asuswrt/test_diagnostics.py @@ -30,7 +30,7 @@ async def test_diagnostics( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - assert mock_config_entry.state == ConfigEntryState.LOADED + assert mock_config_entry.state is ConfigEntryState.LOADED entry_dict = async_redact_data(mock_config_entry.as_dict(), TO_REDACT) diff --git a/tests/components/automation/test_blueprint.py b/tests/components/automation/test_blueprint.py index 24d77800508..7e29c134462 100644 --- a/tests/components/automation/test_blueprint.py +++ b/tests/components/automation/test_blueprint.py @@ -8,9 +8,9 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries from homeassistant.components import automation from homeassistant.components.blueprint import models +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component @@ -48,7 +48,7 @@ async def test_notify_leaving_zone( ) -> None: """Test notifying leaving a zone blueprint.""" config_entry = MockConfigEntry(domain="fake_integration", data={}) - config_entry.mock_state(hass, config_entries.ConfigEntryState.LOADED) + config_entry.mock_state(hass, ConfigEntryState.LOADED) config_entry.add_to_hass(hass) device = device_registry.async_get_or_create( diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 3e569586a2a..31e74529777 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -8,7 +8,6 @@ from unittest.mock import Mock, patch import pytest -from homeassistant import config_entries from homeassistant.components import automation from homeassistant.components.automation import ( ATTR_SOURCE, @@ -18,6 +17,7 @@ from homeassistant.components.automation import ( SERVICE_TRIGGER, AutomationEntity, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_NAME, @@ -1615,7 +1615,7 @@ async def test_extraction_functions( ) -> None: """Test extraction functions.""" config_entry = MockConfigEntry(domain="fake_integration", data={}) - config_entry.mock_state(hass, config_entries.ConfigEntryState.LOADED) + config_entry.mock_state(hass, ConfigEntryState.LOADED) config_entry.add_to_hass(hass) condition_device = device_registry.async_get_or_create( diff --git a/tests/components/axis/test_init.py b/tests/components/axis/test_init.py index 7a22597197b..607508b985a 100644 --- a/tests/components/axis/test_init.py +++ b/tests/components/axis/test_init.py @@ -11,7 +11,7 @@ from homeassistant.core import HomeAssistant async def test_setup_entry(hass: HomeAssistant, setup_config_entry) -> None: """Test successful setup of entry.""" - assert setup_config_entry.state == ConfigEntryState.LOADED + assert setup_config_entry.state is ConfigEntryState.LOADED async def test_setup_entry_fails(hass: HomeAssistant, config_entry) -> None: @@ -24,15 +24,15 @@ async def test_setup_entry_fails(hass: HomeAssistant, config_entry) -> None: assert not await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR async def test_unload_entry(hass: HomeAssistant, setup_config_entry) -> None: """Test successful unload of entry.""" - assert setup_config_entry.state == ConfigEntryState.LOADED + assert setup_config_entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(setup_config_entry.entry_id) - assert setup_config_entry.state == ConfigEntryState.NOT_LOADED + assert setup_config_entry.state is ConfigEntryState.NOT_LOADED @pytest.mark.parametrize("config_entry_version", [1]) @@ -49,5 +49,5 @@ async def test_migrate_entry(hass: HomeAssistant, config_entry) -> None: with patch("homeassistant.components.axis.async_setup_entry", return_value=True): assert await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert config_entry.version == 3 diff --git a/tests/components/azure_devops/test_init.py b/tests/components/azure_devops/test_init.py index 58e3621914d..a35acb375ec 100644 --- a/tests/components/azure_devops/test_init.py +++ b/tests/components/azure_devops/test_init.py @@ -24,12 +24,12 @@ async def test_load_unload_entry( assert mock_devops_client.authorize.call_count == 1 assert mock_devops_client.get_builds.call_count == 2 - assert mock_config_entry.state == ConfigEntryState.LOADED + 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 == ConfigEntryState.NOT_LOADED + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED async def test_auth_failed( @@ -45,7 +45,7 @@ async def test_auth_failed( assert not mock_devops_client.authorized - assert mock_config_entry.state == ConfigEntryState.SETUP_ERROR + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR async def test_update_failed( @@ -60,7 +60,7 @@ async def test_update_failed( assert mock_devops_client.get_builds.call_count == 1 - assert mock_config_entry.state == ConfigEntryState.SETUP_RETRY + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY async def test_no_builds( @@ -75,4 +75,4 @@ async def test_no_builds( assert mock_devops_client.get_builds.call_count == 1 - assert mock_config_entry.state == ConfigEntryState.SETUP_RETRY + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/azure_event_hub/conftest.py b/tests/components/azure_event_hub/conftest.py index 622b11000d7..99bf054dbb1 100644 --- a/tests/components/azure_event_hub/conftest.py +++ b/tests/components/azure_event_hub/conftest.py @@ -52,7 +52,7 @@ async def mock_entry_fixture(hass, filter_schema, mock_create_batch, mock_send_b assert await async_setup_component( hass, DOMAIN, {DOMAIN: {CONF_FILTER: filter_schema}} ) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED # Clear the component_loaded event from the queue. async_fire_time_changed( @@ -70,7 +70,7 @@ async def mock_entry_fixture(hass, filter_schema, mock_create_batch, mock_send_b @pytest.fixture(name="entry_with_one_event") async def mock_entry_with_one_event(hass, entry): """Use the entry and add a single test event to the queue.""" - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED hass.states.async_set("sensor.test", STATE_ON) return entry diff --git a/tests/components/azure_event_hub/test_init.py b/tests/components/azure_event_hub/test_init.py index 0d5cfff80e9..1440bc2ede9 100644 --- a/tests/components/azure_event_hub/test_init.py +++ b/tests/components/azure_event_hub/test_init.py @@ -69,7 +69,7 @@ async def test_unload_entry(hass: HomeAssistant, entry, mock_create_batch) -> No """ assert await hass.config_entries.async_unload(entry.entry_id) mock_create_batch.add.assert_not_called() - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED async def test_failed_test_connection( @@ -85,7 +85,7 @@ async def test_failed_test_connection( entry.add_to_hass(hass) mock_get_eventhub_properties.side_effect = EventHubError("Test") await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_send_batch_error( diff --git a/tests/components/baf/test_init.py b/tests/components/baf/test_init.py index f2616fdd96d..9de2fc03ed0 100644 --- a/tests/components/baf/test_init.py +++ b/tests/components/baf/test_init.py @@ -37,7 +37,7 @@ async def test_config_entry_wrong_uuid( with _patch_device_init(DeviceUUIDMismatchError): await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() - assert already_migrated_config_entry.state == ConfigEntryState.SETUP_RETRY + assert already_migrated_config_entry.state is ConfigEntryState.SETUP_RETRY assert ( "Unexpected device found at 127.0.0.1; expected 12340, found 1234" in caplog.text diff --git a/tests/components/balboa/test_init.py b/tests/components/balboa/test_init.py index 867339c56ef..ecbadac0c09 100644 --- a/tests/components/balboa/test_init.py +++ b/tests/components/balboa/test_init.py @@ -16,9 +16,9 @@ async def test_setup_entry( hass: HomeAssistant, client: MagicMock, integration: MockConfigEntry ) -> None: """Validate that setup entry also configure the client.""" - assert integration.state == ConfigEntryState.LOADED + assert integration.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(integration.entry_id) - assert integration.state == ConfigEntryState.NOT_LOADED + assert integration.state is ConfigEntryState.NOT_LOADED async def test_setup_entry_fails(hass: HomeAssistant, client: MagicMock) -> None: @@ -36,7 +36,7 @@ async def test_setup_entry_fails(hass: HomeAssistant, client: MagicMock) -> None await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY client.connect.return_value = True client.async_configuration_loaded.return_value = False @@ -44,4 +44,4 @@ async def test_setup_entry_fails(hass: HomeAssistant, client: MagicMock) -> None await hass.config_entries.async_reload(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/blebox/test_config_flow.py b/tests/components/blebox/test_config_flow.py index 7213b33555c..fd6d0ce8fb2 100644 --- a/tests/components/blebox/test_config_flow.py +++ b/tests/components/blebox/test_config_flow.py @@ -9,6 +9,7 @@ import pytest from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.blebox import config_flow +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_IP_ADDRESS from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -203,7 +204,7 @@ async def test_async_setup_entry(hass: HomeAssistant, valid_feature_mock) -> Non await hass.async_block_till_done() assert hass.config_entries.async_entries() == [config] - assert config.state is config_entries.ConfigEntryState.LOADED + assert config.state is ConfigEntryState.LOADED async def test_async_remove_entry(hass: HomeAssistant, valid_feature_mock) -> None: @@ -219,7 +220,7 @@ async def test_async_remove_entry(hass: HomeAssistant, valid_feature_mock) -> No await hass.async_block_till_done() assert hass.config_entries.async_entries() == [] - assert config.state is config_entries.ConfigEntryState.NOT_LOADED + assert config.state is ConfigEntryState.NOT_LOADED async def test_flow_with_zeroconf(hass: HomeAssistant) -> None: diff --git a/tests/components/blue_current/test_init.py b/tests/components/blue_current/test_init.py index 06cc6b27c26..723dd993006 100644 --- a/tests/components/blue_current/test_init.py +++ b/tests/components/blue_current/test_init.py @@ -41,11 +41,11 @@ async def test_load_unload_entry( 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 == ConfigEntryState.LOADED + 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 == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED @pytest.mark.parametrize( diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index e9198362d8f..65962ac8f21 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -288,14 +288,14 @@ async def test_setup_and_retry_adapter_not_yet_available( assert "Failed to start Bluetooth" in caplog.text assert len(bluetooth.async_discovered_service_info(hass)) == 0 - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY with patch( "habluetooth.scanner.OriginalBleakScanner.start", ): async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10)) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED with patch( "habluetooth.scanner.OriginalBleakScanner.stop", @@ -327,7 +327,7 @@ async def test_no_race_during_manual_reload_in_retry_state( assert "Failed to start Bluetooth" in caplog.text assert len(bluetooth.async_discovered_service_info(hass)) == 0 - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY with patch( "habluetooth.scanner.OriginalBleakScanner.start", @@ -335,7 +335,7 @@ async def test_no_race_during_manual_reload_in_retry_state( await hass.config_entries.async_reload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED with patch( "habluetooth.scanner.OriginalBleakScanner.stop", @@ -2866,7 +2866,7 @@ async def test_three_adapters_one_missing( entry.add_to_hass(hass) assert not await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_auto_detect_bluetooth_adapters_linux( diff --git a/tests/components/bluetooth/test_scanner.py b/tests/components/bluetooth/test_scanner.py index 504122fb671..523364e0dfd 100644 --- a/tests/components/bluetooth/test_scanner.py +++ b/tests/components/bluetooth/test_scanner.py @@ -48,7 +48,7 @@ async def test_config_entry_can_be_reloaded_when_stop_raises( ) -> None: """Test we can reload if stopping the scanner raises.""" entry = hass.config_entries.async_entries(bluetooth.DOMAIN)[0] - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED with patch( "habluetooth.scanner.OriginalBleakScanner.stop", @@ -57,7 +57,7 @@ async def test_config_entry_can_be_reloaded_when_stop_raises( await hass.config_entries.async_reload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert "Error stopping scanner" in caplog.text diff --git a/tests/components/bring/test_init.py b/tests/components/bring/test_init.py index 6bf9fd1cb54..8604648d916 100644 --- a/tests/components/bring/test_init.py +++ b/tests/components/bring/test_init.py @@ -37,10 +37,10 @@ async def test_load_unload( entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 - assert bring_config_entry.state == ConfigEntryState.LOADED + assert bring_config_entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(bring_config_entry.entry_id) - assert bring_config_entry.state == ConfigEntryState.NOT_LOADED + assert bring_config_entry.state is ConfigEntryState.NOT_LOADED @pytest.mark.parametrize( diff --git a/tests/components/caldav/test_init.py b/tests/components/caldav/test_init.py index 192c18ef81a..f49b1275dca 100644 --- a/tests/components/caldav/test_init.py +++ b/tests/components/caldav/test_init.py @@ -23,15 +23,15 @@ async def test_load_unload( config_entry: MockConfigEntry, ) -> None: """Test loading and unloading of the config entry.""" - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED with patch("homeassistant.components.caldav.config_flow.caldav.DAVClient"): await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED @pytest.mark.parametrize( @@ -57,7 +57,7 @@ async def test_client_failure( ) -> None: """Test CalDAV client failures in setup.""" - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED with patch( "homeassistant.components.caldav.config_flow.caldav.DAVClient" diff --git a/tests/components/coinbase/test_init.py b/tests/components/coinbase/test_init.py index 5af762f557a..99b6bb4a9bd 100644 --- a/tests/components/coinbase/test_init.py +++ b/tests/components/coinbase/test_init.py @@ -2,13 +2,13 @@ from unittest.mock import patch -from homeassistant import config_entries from homeassistant.components.coinbase.const import ( API_TYPE_VAULT, CONF_CURRENCIES, CONF_EXCHANGE_RATES, DOMAIN, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -45,12 +45,12 @@ async def test_unload_entry(hass: HomeAssistant) -> None: entry = await init_mock_coinbase(hass) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == config_entries.ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) diff --git a/tests/components/daikin/test_init.py b/tests/components/daikin/test_init.py index 01b21ebb6fd..d7d754dacd2 100644 --- a/tests/components/daikin/test_init.py +++ b/tests/components/daikin/test_init.py @@ -212,7 +212,7 @@ async def test_client_connection_error(hass: HomeAssistant, mock_daikin) -> None await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY async def test_timeout_error(hass: HomeAssistant, mock_daikin) -> None: @@ -228,4 +228,4 @@ async def test_timeout_error(hass: HomeAssistant, mock_daikin) -> None: await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index 32e624f1c8c..ac5e490b738 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -7,13 +7,14 @@ import pytest from pytest_unordered import unordered import voluptuous as vol -from homeassistant import config_entries, loader +from homeassistant import loader from homeassistant.components import automation, device_automation from homeassistant.components.device_automation import ( InvalidDeviceAutomationConfig, toggle_entity, ) from homeassistant.components.websocket_api.const import TYPE_RESULT +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -976,7 +977,7 @@ async def test_automation_with_dynamically_validated_action( module.async_validate_action_config = AsyncMock() config_entry = MockConfigEntry(domain="fake_integration", data={}) - config_entry.mock_state(hass, config_entries.ConfigEntryState.LOADED) + config_entry.mock_state(hass, ConfigEntryState.LOADED) config_entry.add_to_hass(hass) device_entry = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, @@ -1078,7 +1079,7 @@ async def test_automation_with_dynamically_validated_condition( module.async_validate_condition_config = AsyncMock() config_entry = MockConfigEntry(domain="fake_integration", data={}) - config_entry.mock_state(hass, config_entries.ConfigEntryState.LOADED) + config_entry.mock_state(hass, ConfigEntryState.LOADED) config_entry.add_to_hass(hass) device_entry = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, @@ -1192,7 +1193,7 @@ async def test_automation_with_dynamically_validated_trigger( module.async_validate_trigger_config = AsyncMock(wraps=lambda hass, config: config) config_entry = MockConfigEntry(domain="fake_integration", data={}) - config_entry.mock_state(hass, config_entries.ConfigEntryState.LOADED) + config_entry.mock_state(hass, ConfigEntryState.LOADED) config_entry.add_to_hass(hass) device_entry = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, @@ -1293,7 +1294,7 @@ async def test_automation_with_bad_action( ) -> None: """Test automation with bad device action.""" config_entry = MockConfigEntry(domain="fake_integration", data={}) - config_entry.mock_state(hass, config_entries.ConfigEntryState.LOADED) + config_entry.mock_state(hass, ConfigEntryState.LOADED) config_entry.add_to_hass(hass) device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, @@ -1327,7 +1328,7 @@ async def test_automation_with_bad_condition_action( ) -> None: """Test automation with bad device action.""" config_entry = MockConfigEntry(domain="fake_integration", data={}) - config_entry.mock_state(hass, config_entries.ConfigEntryState.LOADED) + config_entry.mock_state(hass, ConfigEntryState.LOADED) config_entry.add_to_hass(hass) device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, @@ -1360,7 +1361,7 @@ async def test_automation_with_bad_condition( ) -> None: """Test automation with bad device condition.""" config_entry = MockConfigEntry(domain="fake_integration", data={}) - config_entry.mock_state(hass, config_entries.ConfigEntryState.LOADED) + config_entry.mock_state(hass, ConfigEntryState.LOADED) config_entry.add_to_hass(hass) device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, @@ -1525,7 +1526,7 @@ async def test_automation_with_bad_sub_condition( ) -> None: """Test automation with bad device condition under and/or conditions.""" config_entry = MockConfigEntry(domain="fake_integration", data={}) - config_entry.mock_state(hass, config_entries.ConfigEntryState.LOADED) + config_entry.mock_state(hass, ConfigEntryState.LOADED) config_entry.add_to_hass(hass) device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, @@ -1563,7 +1564,7 @@ async def test_automation_with_bad_trigger( ) -> None: """Test automation with bad device trigger.""" config_entry = MockConfigEntry(domain="fake_integration", data={}) - config_entry.mock_state(hass, config_entries.ConfigEntryState.LOADED) + config_entry.mock_state(hass, ConfigEntryState.LOADED) config_entry.add_to_hass(hass) device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, diff --git a/tests/components/device_tracker/test_config_entry.py b/tests/components/device_tracker/test_config_entry.py index 6a1731d5a77..077e964f0af 100644 --- a/tests/components/device_tracker/test_config_entry.py +++ b/tests/components/device_tracker/test_config_entry.py @@ -336,14 +336,14 @@ async def test_load_unload_entry( ) -> None: """Test loading and unloading a config entry with a device tracker entity.""" config_entry = await create_mock_platform(hass, config_entry, [tracker_entity]) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED state = hass.states.get(entity_id) assert state assert await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED state = hass.states.get(entity_id) assert not state @@ -436,7 +436,7 @@ async def test_tracker_entity_state( ) -> None: """Test tracker entity state and state attributes.""" config_entry = await create_mock_platform(hass, config_entry, [tracker_entity]) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED hass.states.async_set( "zone.home", "0", @@ -482,7 +482,7 @@ async def test_scanner_entity_state( ) config_entry = await create_mock_platform(hass, config_entry, [scanner_entity]) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED entity_state = hass.states.get(entity_id) assert entity_state diff --git a/tests/components/devolo_home_control/test_diagnostics.py b/tests/components/devolo_home_control/test_diagnostics.py index e31bc360845..f52a9d49017 100644 --- a/tests/components/devolo_home_control/test_diagnostics.py +++ b/tests/components/devolo_home_control/test_diagnostics.py @@ -32,7 +32,7 @@ async def test_entry_diagnostics( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED result = await get_diagnostics_for_config_entry(hass, hass_client, entry) assert result == snapshot diff --git a/tests/components/devolo_home_network/test_diagnostics.py b/tests/components/devolo_home_network/test_diagnostics.py index 75794250908..a3580cac954 100644 --- a/tests/components/devolo_home_network/test_diagnostics.py +++ b/tests/components/devolo_home_network/test_diagnostics.py @@ -25,7 +25,7 @@ async def test_entry_diagnostics( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED result = await get_diagnostics_for_config_entry(hass, hass_client, entry) assert result == snapshot diff --git a/tests/components/dlink/test_init.py b/tests/components/dlink/test_init.py index 484927340fa..43055b681e0 100644 --- a/tests/components/dlink/test_init.py +++ b/tests/components/dlink/test_init.py @@ -19,7 +19,7 @@ async def test_setup_config_and_unload( await setup_integration() entry = hass.config_entries.async_entries(DOMAIN)[0] - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert entry.data == CONF_DATA @@ -37,7 +37,7 @@ async def test_legacy_setup_config_and_unload( await setup_integration_legacy() entry = hass.config_entries.async_entries(DOMAIN)[0] - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert entry.data == CONF_DATA @@ -56,7 +56,7 @@ async def test_async_setup_entry_not_ready( """Test that it throws ConfigEntryNotReady when exception occurs during legacy setup.""" with patch_setup(mocked_plug_legacy_no_auth): await hass.config_entries.async_setup(config_entry_with_uid.entry_id) - assert config_entry_with_uid.state == ConfigEntryState.SETUP_RETRY + assert config_entry_with_uid.state is ConfigEntryState.SETUP_RETRY async def test_device_info( diff --git a/tests/components/dnsip/test_config_flow.py b/tests/components/dnsip/test_config_flow.py index 29c8d81dd2d..ff089be0e1e 100644 --- a/tests/components/dnsip/test_config_flow.py +++ b/tests/components/dnsip/test_config_flow.py @@ -17,6 +17,7 @@ from homeassistant.components.dnsip.const import ( CONF_RESOLVER_IPV6, DOMAIN, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -227,7 +228,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: "resolver_ipv6": "2001:4860:4860::8888", } - assert entry.state == config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED async def test_options_flow_empty_return(hass: HomeAssistant) -> None: diff --git a/tests/components/dnsip/test_init.py b/tests/components/dnsip/test_init.py index 37595444c44..3d816bebe60 100644 --- a/tests/components/dnsip/test_init.py +++ b/tests/components/dnsip/test_init.py @@ -4,7 +4,6 @@ from __future__ import annotations from unittest.mock import patch -from homeassistant import config_entries from homeassistant.components.dnsip.const import ( CONF_HOSTNAME, CONF_IPV4, @@ -13,7 +12,7 @@ from homeassistant.components.dnsip.const import ( CONF_RESOLVER_IPV6, DOMAIN, ) -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_USER, ConfigEntryState from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant @@ -49,7 +48,7 @@ async def test_load_unload_entry(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state is config_entries.ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/dremel_3d_printer/test_init.py b/tests/components/dremel_3d_printer/test_init.py index 8216054587d..6b008c7fac1 100644 --- a/tests/components/dremel_3d_printer/test_init.py +++ b/tests/components/dremel_3d_printer/test_init.py @@ -27,7 +27,7 @@ async def test_setup( with patch(MOCKED_MODEL, return_value=model) as mock: await hass.config_entries.async_setup(config_entry.entry_id) assert await async_setup_component(hass, DOMAIN, {}) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert mock.called with patch(MOCKED_MODEL, return_value=model) as mock: @@ -50,7 +50,7 @@ async def test_async_setup_entry_not_ready( await hass.config_entries.async_setup(config_entry.entry_id) assert await async_setup_component(hass, DOMAIN, {}) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY assert not hass.data.get(DOMAIN) @@ -60,7 +60,7 @@ async def test_update_failed( """Test coordinator throws UpdateFailed after failed update.""" await hass.config_entries.async_setup(config_entry.entry_id) assert await async_setup_component(hass, DOMAIN, {}) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED with patch( "homeassistant.components.dremel_3d_printer.Dremel3DPrinter.refresh", diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index f63422e0543..7a38e3010d8 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -13,13 +13,13 @@ from unittest.mock import DEFAULT, MagicMock import pytest -from homeassistant import config_entries from homeassistant.components.sensor import ( ATTR_OPTIONS, ATTR_STATE_CLASS, SensorDeviceClass, SensorStateClass, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, @@ -1325,7 +1325,7 @@ async def test_reconnect(hass: HomeAssistant, dsmr_connection_fixture) -> None: await hass.config_entries.async_unload(mock_entry.entry_id) - assert mock_entry.state == config_entries.ConfigEntryState.NOT_LOADED + assert mock_entry.state is ConfigEntryState.NOT_LOADED async def test_gas_meter_providing_energy_reading( diff --git a/tests/components/dwd_weather_warnings/test_init.py b/tests/components/dwd_weather_warnings/test_init.py index 6967f2ca6b1..db7afaadec9 100644 --- a/tests/components/dwd_weather_warnings/test_init.py +++ b/tests/components/dwd_weather_warnings/test_init.py @@ -28,7 +28,7 @@ async def test_load_unload_entry(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert entry.entry_id in hass.data[DOMAIN] assert await hass.config_entries.async_unload(entry.entry_id) diff --git a/tests/components/dynalite/test_config_flow.py b/tests/components/dynalite/test_config_flow.py index bdbd03faa22..2b56786e4e0 100644 --- a/tests/components/dynalite/test_config_flow.py +++ b/tests/components/dynalite/test_config_flow.py @@ -6,6 +6,7 @@ import pytest from homeassistant import config_entries from homeassistant.components import dynalite +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_PORT from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -21,9 +22,9 @@ from tests.common import MockConfigEntry @pytest.mark.parametrize( ("first_con", "second_con", "exp_type", "exp_result", "exp_reason"), [ - (True, True, "create_entry", config_entries.ConfigEntryState.LOADED, ""), + (True, True, "create_entry", ConfigEntryState.LOADED, ""), (False, False, "abort", None, "cannot_connect"), - (True, False, "create_entry", config_entries.ConfigEntryState.SETUP_RETRY, ""), + (True, False, "create_entry", ConfigEntryState.SETUP_RETRY, ""), ], ) async def test_flow( @@ -138,7 +139,7 @@ async def test_two_entries(hass: HomeAssistant) -> None: data={dynalite.CONF_HOST: host2}, ) assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["result"].state == config_entries.ConfigEntryState.LOADED + assert result["result"].state is ConfigEntryState.LOADED async def test_setup_user(hass): @@ -163,7 +164,7 @@ async def test_setup_user(hass): ) assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["result"].state == config_entries.ConfigEntryState.LOADED + assert result["result"].state is ConfigEntryState.LOADED assert result["title"] == host assert result["data"] == { "host": host, diff --git a/tests/components/eafm/test_sensor.py b/tests/components/eafm/test_sensor.py index 380e1df5f37..082c4e08908 100644 --- a/tests/components/eafm/test_sensor.py +++ b/tests/components/eafm/test_sensor.py @@ -5,7 +5,7 @@ import datetime import aiohttp import pytest -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -37,7 +37,7 @@ async def async_setup_test_fixture(hass, mock_get_station, initial_value): entry.add_to_hass(hass) assert await async_setup_component(hass, "eafm", {}) - assert entry.state is config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED await hass.async_block_till_done() async def poll(value): diff --git a/tests/components/efergy/test_init.py b/tests/components/efergy/test_init.py index 5c72e1a5cfd..151cd50fbc6 100644 --- a/tests/components/efergy/test_init.py +++ b/tests/components/efergy/test_init.py @@ -16,7 +16,7 @@ from tests.test_util.aiohttp import AiohttpClientMocker async def test_setup(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -> None: """Test unload.""" entry = await init_integration(hass, aioclient_mock) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() @@ -32,7 +32,7 @@ async def test_async_setup_entry_not_ready(hass: HomeAssistant) -> None: efergymock.side_effect = (exceptions.ConnectError, exceptions.DataError) await hass.config_entries.async_setup(entry.entry_id) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY assert not hass.data.get(DOMAIN) @@ -43,7 +43,7 @@ async def test_async_setup_entry_auth_failed(hass: HomeAssistant) -> None: efergymock.side_effect = exceptions.InvalidAuth await hass.config_entries.async_setup(entry.entry_id) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR assert not hass.data.get(DOMAIN) diff --git a/tests/components/energenie_power_sockets/test_init.py b/tests/components/energenie_power_sockets/test_init.py index a60949c34cc..4e2fe51665b 100644 --- a/tests/components/energenie_power_sockets/test_init.py +++ b/tests/components/energenie_power_sockets/test_init.py @@ -23,7 +23,7 @@ async def test_load_unload_entry( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert entry.entry_id in hass.data[DOMAIN] assert await hass.config_entries.async_unload(entry.entry_id) diff --git a/tests/components/energenie_power_sockets/test_switch.py b/tests/components/energenie_power_sockets/test_switch.py index b98a3e07f56..4cd2bd60028 100644 --- a/tests/components/energenie_power_sockets/test_switch.py +++ b/tests/components/energenie_power_sockets/test_switch.py @@ -117,7 +117,7 @@ async def test_switch_setup( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert entry.entry_id in hass.data[DOMAIN] state = hass.states.get(f"switch.{entity_name}") diff --git a/tests/components/esphome/test_dashboard.py b/tests/components/esphome/test_dashboard.py index e6fca268880..01c1553cf42 100644 --- a/tests/components/esphome/test_dashboard.py +++ b/tests/components/esphome/test_dashboard.py @@ -60,7 +60,7 @@ async def test_restore_dashboard_storage_end_to_end( ) as mock_dashboard_api: await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - assert mock_config_entry.state == ConfigEntryState.LOADED + assert mock_config_entry.state is ConfigEntryState.LOADED assert mock_dashboard_api.mock_calls[0][1][0] == "http://new-host:6052" @@ -74,7 +74,7 @@ async def test_setup_dashboard_fails( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() await dashboard.async_set_dashboard_info(hass, "test-slug", "test-host", 6052) - assert mock_config_entry.state == ConfigEntryState.LOADED + assert mock_config_entry.state is ConfigEntryState.LOADED assert mock_get_devices.call_count == 1 # The dashboard addon might recover later so we still @@ -109,7 +109,7 @@ async def test_setup_dashboard_fails_when_already_setup( await dashboard.async_set_dashboard_info(hass, "test-slug", "test-host", 6052) await hass.async_block_till_done() - assert mock_config_entry.state == ConfigEntryState.LOADED + assert mock_config_entry.state is ConfigEntryState.LOADED assert mock_get_devices.call_count == 1 # We still setup, and reload, but we do not do the reauths assert dashboard.STORAGE_KEY in hass_storage @@ -120,7 +120,7 @@ async def test_new_info_reload_config_entries( hass: HomeAssistant, init_integration, mock_dashboard ) -> None: """Test config entries are reloaded when new info is set.""" - assert init_integration.state == ConfigEntryState.LOADED + assert init_integration.state is ConfigEntryState.LOADED with patch("homeassistant.components.esphome.async_setup_entry") as mock_setup: await dashboard.async_set_dashboard_info(hass, "test-slug", "test-host", 6052) diff --git a/tests/components/evil_genius_labs/test_init.py b/tests/components/evil_genius_labs/test_init.py index 71b8a6164a6..10b773ead61 100644 --- a/tests/components/evil_genius_labs/test_init.py +++ b/tests/components/evil_genius_labs/test_init.py @@ -2,8 +2,8 @@ import pytest -from homeassistant import config_entries from homeassistant.components.evil_genius_labs import PLATFORMS +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant @@ -14,4 +14,4 @@ async def test_setup_unload_entry( """Test setting up and unloading a config entry.""" assert len(hass.states.async_entity_ids()) == 1 assert await hass.config_entries.async_unload(config_entry.entry_id) - assert config_entry.state == config_entries.ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/fastdotcom/test_init.py b/tests/components/fastdotcom/test_init.py index 12e3902d874..c17b455057b 100644 --- a/tests/components/fastdotcom/test_init.py +++ b/tests/components/fastdotcom/test_init.py @@ -6,8 +6,8 @@ from unittest.mock import patch from freezegun.api import FrozenDateTimeFactory -from homeassistant import config_entries 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 @@ -31,10 +31,10 @@ async def test_unload_entry(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == config_entries.ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state is config_entries.ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED async def test_from_import(hass: HomeAssistant) -> None: @@ -72,7 +72,7 @@ async def test_delayed_speedtest_during_startup( await hass.async_block_till_done() hass.set_state(original_state) - assert config_entry.state == config_entries.ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED state = hass.states.get("sensor.fast_com_download") # Assert state is Unknown as fast.com isn't starting until HA has started assert state.state is STATE_UNKNOWN @@ -87,7 +87,7 @@ async def test_delayed_speedtest_during_startup( assert state is not None assert state.state == "5.0" - assert config_entry.state == config_entries.ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED async def test_service_deprecated( diff --git a/tests/components/fitbit/test_init.py b/tests/components/fitbit/test_init.py index 74312348af1..a4794a63162 100644 --- a/tests/components/fitbit/test_init.py +++ b/tests/components/fitbit/test_init.py @@ -34,15 +34,15 @@ async def test_setup( setup_credentials: None, ) -> None: """Test setting up the integration.""" - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED assert await integration_setup() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED @pytest.mark.parametrize( @@ -68,7 +68,7 @@ async def test_token_refresh_failure( ) assert not await integration_setup() - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY @pytest.mark.parametrize("token_expiration_time", [12345]) @@ -88,7 +88,7 @@ async def test_token_refresh_success( ) assert await integration_setup() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED # Verify token request assert len(aioclient_mock.mock_calls) == 1 @@ -132,7 +132,7 @@ async def test_token_requires_reauth( ) assert not await integration_setup() - assert config_entry.state == ConfigEntryState.SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -147,7 +147,7 @@ async def test_device_update_coordinator_failure( requests_mock: Mocker, ) -> None: """Test case where the device update coordinator fails on the first request.""" - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED requests_mock.register_uri( "GET", @@ -156,7 +156,7 @@ async def test_device_update_coordinator_failure( ) assert not await integration_setup() - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY async def test_device_update_coordinator_reauth( @@ -167,7 +167,7 @@ async def test_device_update_coordinator_reauth( requests_mock: Mocker, ) -> None: """Test case where the device update coordinator fails on the first request.""" - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED requests_mock.register_uri( "GET", @@ -179,7 +179,7 @@ async def test_device_update_coordinator_reauth( ) assert not await integration_setup() - assert config_entry.state == ConfigEntryState.SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 diff --git a/tests/components/flexit_bacnet/test_init.py b/tests/components/flexit_bacnet/test_init.py index 0741120c1ad..4ff52a3bcfc 100644 --- a/tests/components/flexit_bacnet/test_init.py +++ b/tests/components/flexit_bacnet/test_init.py @@ -18,7 +18,7 @@ async def test_loading_and_unloading_config_entry( await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE]) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert mock_config_entry.state == ConfigEntryState.LOADED + 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() @@ -33,4 +33,4 @@ async def test_failed_initialization( mock_flexit_bacnet.update.side_effect = DecodingError mock_config_entry.add_to_hass(hass) assert not await hass.config_entries.async_setup(mock_config_entry.entry_id) - assert mock_config_entry.state == ConfigEntryState.SETUP_RETRY + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/flipr/test_init.py b/tests/components/flipr/test_init.py index 8300ac185ba..6a49b5b7200 100644 --- a/tests/components/flipr/test_init.py +++ b/tests/components/flipr/test_init.py @@ -26,4 +26,4 @@ async def test_unload_entry(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() await hass.config_entries.async_unload(entry.entry_id) - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/flux_led/test_init.py b/tests/components/flux_led/test_init.py index a42ba5dff37..8e3bb03dca2 100644 --- a/tests/components/flux_led/test_init.py +++ b/tests/components/flux_led/test_init.py @@ -98,10 +98,10 @@ async def test_config_entry_reload(hass: HomeAssistant) -> None: with _patch_discovery(), _patch_wifibulb(): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + 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 == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED async def test_config_entry_retry(hass: HomeAssistant) -> None: @@ -113,7 +113,7 @@ async def test_config_entry_retry(hass: HomeAssistant) -> None: with _patch_discovery(no_device=True), _patch_wifibulb(no_device=True): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY async def test_config_entry_retry_right_away_on_discovery(hass: HomeAssistant) -> None: @@ -125,7 +125,7 @@ async def test_config_entry_retry_right_away_on_discovery(hass: HomeAssistant) - with _patch_discovery(no_device=True), _patch_wifibulb(no_device=True): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY with _patch_discovery(), _patch_wifibulb(): await hass.config_entries.flow.async_init( @@ -134,7 +134,7 @@ async def test_config_entry_retry_right_away_on_discovery(hass: HomeAssistant) - data=DHCP_DISCOVERY, ) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED async def test_coordinator_retry_right_away_on_discovery_already_setup( @@ -152,7 +152,7 @@ async def test_coordinator_retry_right_away_on_discovery_already_setup( await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED entity_id = "light.bulb_rgbcw_ddeeff" assert entity_registry.async_get(entity_id).unique_id == MAC_ADDRESS @@ -219,7 +219,7 @@ async def test_config_entry_fills_unique_id_with_directed_discovery( ): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert config_entry.unique_id == MAC_ADDRESS assert config_entry.title == title @@ -235,7 +235,7 @@ async def test_time_sync_startup_and_next_day(hass: HomeAssistant) -> None: with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert len(bulb.async_set_time.mock_calls) == 1 async_fire_time_changed(hass, utcnow() + timedelta(hours=24)) diff --git a/tests/components/forecast_solar/test_energy.py b/tests/components/forecast_solar/test_energy.py index 246ed866506..1ae3c22c870 100644 --- a/tests/components/forecast_solar/test_energy.py +++ b/tests/components/forecast_solar/test_energy.py @@ -25,7 +25,7 @@ async def test_energy_solar_forecast( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - assert mock_config_entry.state == ConfigEntryState.LOADED + assert mock_config_entry.state is ConfigEntryState.LOADED assert await energy.async_get_solar_forecast(hass, mock_config_entry.entry_id) == { "wh_hours": { diff --git a/tests/components/forecast_solar/test_init.py b/tests/components/forecast_solar/test_init.py index b581888547d..481ec3c0c9d 100644 --- a/tests/components/forecast_solar/test_init.py +++ b/tests/components/forecast_solar/test_init.py @@ -29,7 +29,7 @@ async def test_load_unload_config_entry( mock_config_entry.add_to_hass(hass) await async_setup_component(hass, "forecast_solar", {}) - assert mock_config_entry.state == ConfigEntryState.LOADED + assert mock_config_entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(mock_config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/freedompro/test_init.py b/tests/components/freedompro/test_init.py index 58f9c493582..fd8034818b1 100644 --- a/tests/components/freedompro/test_init.py +++ b/tests/components/freedompro/test_init.py @@ -43,7 +43,7 @@ async def test_config_not_ready(hass: HomeAssistant) -> None: ): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_unload_entry( @@ -53,9 +53,9 @@ async def test_unload_entry( entry = init_integration assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/fritz/test_button.py b/tests/components/fritz/test_button.py index f6546296d44..14aa46f30a7 100644 --- a/tests/components/fritz/test_button.py +++ b/tests/components/fritz/test_button.py @@ -26,7 +26,7 @@ async def test_button_setup(hass: HomeAssistant, fc_class_mock, fh_class_mock) - await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED buttons = hass.states.async_all(BUTTON_DOMAIN) assert len(buttons) == 4 @@ -57,7 +57,7 @@ async def test_buttons( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED button = hass.states.get(entity_id) assert button @@ -91,7 +91,7 @@ async def test_wol_button( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED button = hass.states.get("button.printer_wake_on_lan") assert button @@ -125,7 +125,7 @@ async def test_wol_button_new_device( mesh_data = copy.deepcopy(MOCK_MESH_DATA) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert hass.states.get("button.printer_wake_on_lan") assert not hass.states.get("button.server_wake_on_lan") @@ -156,7 +156,7 @@ async def test_wol_button_absent_for_mesh_slave( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED button = hass.states.get("button.printer_wake_on_lan") assert button is None @@ -182,7 +182,7 @@ async def test_wol_button_absent_for_non_lan_device( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED button = hass.states.get("button.printer_wake_on_lan") assert button is None diff --git a/tests/components/fritz/test_diagnostics.py b/tests/components/fritz/test_diagnostics.py index 9dc50cc3378..35d50ff4572 100644 --- a/tests/components/fritz/test_diagnostics.py +++ b/tests/components/fritz/test_diagnostics.py @@ -28,7 +28,7 @@ async def test_entry_diagnostics( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED entry_dict = entry.as_dict() for key in TO_REDACT: diff --git a/tests/components/fritz/test_image.py b/tests/components/fritz/test_image.py index 5d6b9265760..45afe34b6aa 100644 --- a/tests/components/fritz/test_image.py +++ b/tests/components/fritz/test_image.py @@ -105,7 +105,7 @@ async def test_image_entity( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED # test image entity is generated as expected states = hass.states.async_all(IMAGE_DOMAIN) @@ -155,7 +155,7 @@ async def test_image_update( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED client = await hass_client() resp = await client.get("/api/image_proxy/image.mock_title_guestwifi") @@ -191,7 +191,7 @@ async def test_image_update_unavailable( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED state = hass.states.get("image.mock_title_guestwifi") assert state diff --git a/tests/components/fritz/test_init.py b/tests/components/fritz/test_init.py index 0a525192778..be45698e160 100644 --- a/tests/components/fritz/test_init.py +++ b/tests/components/fritz/test_init.py @@ -29,10 +29,10 @@ async def test_setup(hass: HomeAssistant, fc_class_mock, fh_class_mock) -> None: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(entry.entry_id) - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED async def test_options_reload( @@ -53,7 +53,7 @@ async def test_options_reload( ) as mock_reload: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED result = await hass.config_entries.options.async_init(entry.entry_id) await hass.async_block_till_done() @@ -82,7 +82,7 @@ async def test_setup_auth_fail(hass: HomeAssistant, error) -> None: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR @pytest.mark.parametrize( @@ -102,4 +102,4 @@ async def test_setup_fail(hass: HomeAssistant, error) -> None: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/fritz/test_sensor.py b/tests/components/fritz/test_sensor.py index 37116e66719..f8114238376 100644 --- a/tests/components/fritz/test_sensor.py +++ b/tests/components/fritz/test_sensor.py @@ -107,7 +107,7 @@ async def test_sensor_setup(hass: HomeAssistant, fc_class_mock, fh_class_mock) - await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED sensors = hass.states.async_all(SENSOR_DOMAIN) assert len(sensors) == len(SENSOR_TYPES) diff --git a/tests/components/fritz/test_switch.py b/tests/components/fritz/test_switch.py index 91d2d42106b..adb5c3f6799 100644 --- a/tests/components/fritz/test_switch.py +++ b/tests/components/fritz/test_switch.py @@ -180,7 +180,7 @@ async def test_switch_setup( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED switches = hass.states.async_all(Platform.SWITCH) assert len(switches) == 3 diff --git a/tests/components/fritz/test_update.py b/tests/components/fritz/test_update.py index 991b67e6285..c39dd24de02 100644 --- a/tests/components/fritz/test_update.py +++ b/tests/components/fritz/test_update.py @@ -40,7 +40,7 @@ async def test_update_entities_initialized( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED updates = hass.states.async_all(UPDATE_DOMAIN) assert len(updates) == 1 @@ -61,7 +61,7 @@ async def test_update_available( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED update = hass.states.get("update.mock_title_fritz_os") assert update is not None @@ -84,7 +84,7 @@ async def test_no_update_available( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED update = hass.states.get("update.mock_title_fritz_os") assert update is not None @@ -112,7 +112,7 @@ async def test_available_update_can_be_installed( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED update = hass.states.get("update.mock_title_fritz_os") assert update is not None diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py index f22bb4d93da..841fb710717 100644 --- a/tests/components/generic/test_config_flow.py +++ b/tests/components/generic/test_config_flow.py @@ -27,6 +27,7 @@ from homeassistant.components.stream import ( CONF_USE_WALLCLOCK_AS_TIMESTAMPS, ) from homeassistant.components.stream.worker import StreamWorkerError +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( CONF_AUTHENTICATION, CONF_NAME, @@ -804,11 +805,11 @@ async def test_unload_entry(hass: HomeAssistant, fakeimg_png) -> None: await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() - assert mock_entry.state is config_entries.ConfigEntryState.LOADED + assert mock_entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(mock_entry.entry_id) await hass.async_block_till_done() - assert mock_entry.state is config_entries.ConfigEntryState.NOT_LOADED + assert mock_entry.state is ConfigEntryState.NOT_LOADED async def test_reload_on_title_change(hass: HomeAssistant) -> None: @@ -823,7 +824,7 @@ async def test_reload_on_title_change(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() - assert mock_entry.state is config_entries.ConfigEntryState.LOADED + assert mock_entry.state is ConfigEntryState.LOADED assert hass.states.get("camera.my_title").attributes["friendly_name"] == "My Title" hass.config_entries.async_update_entry(mock_entry, title="New Title") diff --git a/tests/components/github/common.py b/tests/components/github/common.py index d850ce1bba8..5007496c9fe 100644 --- a/tests/components/github/common.py +++ b/tests/components/github/common.py @@ -4,8 +4,8 @@ from __future__ import annotations import json -from homeassistant import config_entries from homeassistant.components.github.const import CONF_REPOSITORIES, DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_fixture @@ -50,4 +50,4 @@ async def setup_github_integration( await hass.async_block_till_done() assert setup_result - assert mock_config_entry.state == config_entries.ConfigEntryState.LOADED + assert mock_config_entry.state is ConfigEntryState.LOADED diff --git a/tests/components/glances/test_init.py b/tests/components/glances/test_init.py index aa861dc5518..02fa6960c2f 100644 --- a/tests/components/glances/test_init.py +++ b/tests/components/glances/test_init.py @@ -27,7 +27,7 @@ async def test_successful_config_entry(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED async def test_entry_deprecated_version( @@ -45,7 +45,7 @@ async def test_entry_deprecated_version( await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED issue = issue_registry.async_get_issue(DOMAIN, "deprecated_version") assert issue is not None diff --git a/tests/components/goalzero/test_init.py b/tests/components/goalzero/test_init.py index 1390561785e..1d44c7e808e 100644 --- a/tests/components/goalzero/test_init.py +++ b/tests/components/goalzero/test_init.py @@ -25,7 +25,7 @@ async def test_setup_config_and_unload(hass: HomeAssistant) -> None: with patch("homeassistant.components.goalzero.Yeti", return_value=mocked_yeti): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert entry.data == CONF_DATA @@ -46,7 +46,7 @@ async def test_setup_config_entry_incorrectly_formatted_mac( with patch("homeassistant.components.goalzero.Yeti", return_value=mocked_yeti): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert entry.data == CONF_DATA @@ -64,7 +64,7 @@ async def test_async_setup_entry_not_ready(hass: HomeAssistant) -> None: side_effect=exceptions.ConnectError, ): await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_update_failed( diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index 319f6be5012..2a26776b031 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -116,7 +116,7 @@ async def test_unload_entry( assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED @pytest.mark.parametrize( @@ -959,4 +959,4 @@ async def test_remove_entry( assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_remove(entry.entry_id) - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/google_tasks/test_init.py b/tests/components/google_tasks/test_init.py index 061bf549748..0abfce87133 100644 --- a/tests/components/google_tasks/test_init.py +++ b/tests/components/google_tasks/test_init.py @@ -30,7 +30,7 @@ async def test_setup( await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED assert not hass.services.async_services().get(DOMAIN) diff --git a/tests/components/hardkernel/test_init.py b/tests/components/hardkernel/test_init.py index 90717054ead..af01006afba 100644 --- a/tests/components/hardkernel/test_init.py +++ b/tests/components/hardkernel/test_init.py @@ -101,4 +101,4 @@ async def test_setup_entry_wait_hassio(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(mock_get_os_info.mock_calls) == 1 - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/hassio/test_sensor.py b/tests/components/hassio/test_sensor.py index 82ac3eccdf5..55cec90ec58 100644 --- a/tests/components/hassio/test_sensor.py +++ b/tests/components/hassio/test_sensor.py @@ -14,6 +14,7 @@ from homeassistant.components.hassio import ( HassioAPIError, ) from homeassistant.components.hassio.const import REQUEST_REFRESH_DELAY +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -317,7 +318,7 @@ async def test_stats_addon_sensor( freezer.tick(config_entries.RELOAD_AFTER_UPDATE_DELAY) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) - assert config_entry.state is config_entries.ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED # Verify the entity is still enabled assert entity_registry.async_get(entity_id).disabled_by is None diff --git a/tests/components/holiday/test_init.py b/tests/components/holiday/test_init.py index a044e390a68..38eb51fe925 100644 --- a/tests/components/holiday/test_init.py +++ b/tests/components/holiday/test_init.py @@ -20,10 +20,10 @@ async def test_unload_entry(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() state: ConfigEntryState = entry.state - assert state == ConfigEntryState.NOT_LOADED + assert state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/homeassistant_green/test_init.py b/tests/components/homeassistant_green/test_init.py index 0efb449137a..44cd6da136a 100644 --- a/tests/components/homeassistant_green/test_init.py +++ b/tests/components/homeassistant_green/test_init.py @@ -105,4 +105,4 @@ async def test_setup_entry_wait_hassio(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(mock_get_os_info.mock_calls) == 1 - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/homeassistant_sky_connect/test_init.py b/tests/components/homeassistant_sky_connect/test_init.py index a1fa4a5c743..a6dd5100d7e 100644 --- a/tests/components/homeassistant_sky_connect/test_init.py +++ b/tests/components/homeassistant_sky_connect/test_init.py @@ -299,7 +299,7 @@ async def test_setup_entry_wait_usb(hass: HomeAssistant) -> None: return_value=False, ) as mock_is_plugged_in: await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 # USB discovery starts, config entry should be removed hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -340,7 +340,7 @@ async def test_setup_entry_addon_info_fails( ): assert not await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY async def test_setup_entry_addon_not_running( @@ -373,5 +373,5 @@ async def test_setup_entry_addon_not_running( ): assert not await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY start_addon.assert_called_once() diff --git a/tests/components/homeassistant_yellow/test_init.py b/tests/components/homeassistant_yellow/test_init.py index e94dbbc1438..0631c2cb983 100644 --- a/tests/components/homeassistant_yellow/test_init.py +++ b/tests/components/homeassistant_yellow/test_init.py @@ -294,7 +294,7 @@ async def test_setup_entry_wait_hassio(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(mock_get_os_info.mock_calls) == 1 - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY async def test_setup_entry_addon_info_fails( @@ -325,7 +325,7 @@ async def test_setup_entry_addon_info_fails( assert not await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY async def test_setup_entry_addon_not_running( @@ -355,5 +355,5 @@ async def test_setup_entry_addon_not_running( assert not await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY start_addon.assert_called_once() diff --git a/tests/components/homekit/test_init.py b/tests/components/homekit/test_init.py index e8fb7e1d92e..7e924be1637 100644 --- a/tests/components/homekit/test_init.py +++ b/tests/components/homekit/test_init.py @@ -126,7 +126,7 @@ async def test_bridge_with_triggers( hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/homekit_controller/test_device_trigger.py b/tests/components/homekit_controller/test_device_trigger.py index 239d170a84f..b5a9aee72b1 100644 --- a/tests/components/homekit_controller/test_device_trigger.py +++ b/tests/components/homekit_controller/test_device_trigger.py @@ -370,7 +370,7 @@ async def test_handle_events_late_setup( await hass.config_entries.async_unload(helper.config_entry.entry_id) await hass.async_block_till_done() - assert helper.config_entry.state == ConfigEntryState.NOT_LOADED + assert helper.config_entry.state is ConfigEntryState.NOT_LOADED assert await async_setup_component( hass, @@ -424,7 +424,7 @@ async def test_handle_events_late_setup( await hass.config_entries.async_setup(helper.config_entry.entry_id) await hass.async_block_till_done() - assert helper.config_entry.state == ConfigEntryState.LOADED + assert helper.config_entry.state is ConfigEntryState.LOADED # Make sure first automation (only) fires for single press helper.pairing.testing.update_named_service( diff --git a/tests/components/homekit_controller/test_init.py b/tests/components/homekit_controller/test_init.py index ec3e6216288..59fdf555a50 100644 --- a/tests/components/homekit_controller/test_init.py +++ b/tests/components/homekit_controller/test_init.py @@ -155,13 +155,13 @@ async def test_offline_device_raises(hass: HomeAssistant, controller) -> None: ) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY is_connected = True async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert hass.states.get("light.testdevice").state == STATE_OFF @@ -212,13 +212,13 @@ async def test_ble_device_only_checks_is_available( ) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY is_available = True async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert hass.states.get("light.testdevice").state == STATE_OFF is_available = False diff --git a/tests/components/husqvarna_automower/test_init.py b/tests/components/husqvarna_automower/test_init.py index 3c97a3b2668..dbf1d429eee 100644 --- a/tests/components/husqvarna_automower/test_init.py +++ b/tests/components/husqvarna_automower/test_init.py @@ -31,12 +31,12 @@ async def test_load_unload_entry( await setup_integration(hass, mock_config_entry) entry = hass.config_entries.async_entries(DOMAIN)[0] - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED await hass.config_entries.async_remove(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED @pytest.mark.parametrize( @@ -87,7 +87,7 @@ async def test_update_failed( await setup_integration(hass, mock_config_entry) entry = hass.config_entries.async_entries(DOMAIN)[0] - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_websocket_not_available( @@ -105,13 +105,13 @@ async def test_websocket_not_available( assert "Failed to connect to websocket. Trying to reconnect: Boom" in caplog.text assert mock_automower_client.auth.websocket_connect.call_count == 1 assert mock_automower_client.start_listening.call_count == 1 - assert mock_config_entry.state == ConfigEntryState.LOADED + assert mock_config_entry.state is ConfigEntryState.LOADED freezer.tick(timedelta(seconds=2)) async_fire_time_changed(hass) await hass.async_block_till_done() assert mock_automower_client.auth.websocket_connect.call_count == 2 assert mock_automower_client.start_listening.call_count == 2 - assert mock_config_entry.state == ConfigEntryState.LOADED + assert mock_config_entry.state is ConfigEntryState.LOADED async def test_device_info( diff --git a/tests/components/insteon/test_config_flow.py b/tests/components/insteon/test_config_flow.py index 711290ef3b1..ae21489bd62 100644 --- a/tests/components/insteon/test_config_flow.py +++ b/tests/components/insteon/test_config_flow.py @@ -30,6 +30,7 @@ from homeassistant.components.insteon.const import ( CONF_X10, DOMAIN, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( CONF_ADDRESS, CONF_DEVICE, @@ -136,7 +137,7 @@ async def test_fail_on_existing(hass: HomeAssistant) -> None: options={}, ) config_entry.add_to_hass(hass) - assert config_entry.state is config_entries.ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/iotawatt/test_init.py b/tests/components/iotawatt/test_init.py index c185fec0e4d..8b707780eb4 100644 --- a/tests/components/iotawatt/test_init.py +++ b/tests/components/iotawatt/test_init.py @@ -24,7 +24,7 @@ async def test_setup_connection_failed( mock_iotawatt.connect.side_effect = httpx.ConnectError("") assert await async_setup_component(hass, "iotawatt", {}) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_setup_auth_failed(hass: HomeAssistant, mock_iotawatt, entry) -> None: @@ -32,4 +32,4 @@ async def test_setup_auth_failed(hass: HomeAssistant, mock_iotawatt, entry) -> N mock_iotawatt.connect.return_value = False assert await async_setup_component(hass, "iotawatt", {}) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/islamic_prayer_times/test_init.py b/tests/components/islamic_prayer_times/test_init.py index 3c7565a37ef..aa865ee05a4 100644 --- a/tests/components/islamic_prayer_times/test_init.py +++ b/tests/components/islamic_prayer_times/test_init.py @@ -7,10 +7,10 @@ from freezegun import freeze_time from prayer_times_calculator.exceptions import InvalidResponseError import pytest -from homeassistant import config_entries from homeassistant.components import islamic_prayer_times from homeassistant.components.islamic_prayer_times.const import CONF_CALC_METHOD from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -43,7 +43,7 @@ async def test_successful_config_entry(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state is config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED async def test_setup_failed(hass: HomeAssistant) -> None: @@ -62,7 +62,7 @@ async def test_setup_failed(hass: HomeAssistant) -> None: ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_unload_entry(hass: HomeAssistant) -> None: @@ -81,7 +81,7 @@ async def test_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 config_entries.ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED async def test_options_listener(hass: HomeAssistant) -> None: @@ -122,7 +122,7 @@ async def test_update_failed(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state is config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED with patch( "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times" diff --git a/tests/components/kmtronic/test_config_flow.py b/tests/components/kmtronic/test_config_flow.py index 725058c662a..21468584d81 100644 --- a/tests/components/kmtronic/test_config_flow.py +++ b/tests/components/kmtronic/test_config_flow.py @@ -87,7 +87,7 @@ async def test_form_options( await hass.async_block_till_done() - assert config_entry.state == config_entries.ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED async def test_form_invalid_auth(hass: HomeAssistant) -> None: diff --git a/tests/components/knx/test_init.py b/tests/components/knx/test_init.py index 2d2889e7718..a317a6a298c 100644 --- a/tests/components/knx/test_init.py +++ b/tests/components/knx/test_init.py @@ -11,7 +11,6 @@ from xknx.io import ( SecureConfig, ) -from homeassistant import config_entries from homeassistant.components.knx.config_flow import DEFAULT_ROUTING_IA from homeassistant.components.knx.const import ( CONF_KNX_AUTOMATIC, @@ -40,6 +39,7 @@ from homeassistant.components.knx.const import ( DOMAIN as KNX_DOMAIN, KNXConfigEntryData, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant @@ -287,4 +287,4 @@ async def test_async_remove_entry( await hass.async_block_till_done() assert hass.config_entries.async_entries() == [] - assert config_entry.state is config_entries.ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/lacrosse_view/test_init.py b/tests/components/lacrosse_view/test_init.py index cf11e787ad8..51fa7e5abf4 100644 --- a/tests/components/lacrosse_view/test_init.py +++ b/tests/components/lacrosse_view/test_init.py @@ -35,11 +35,11 @@ async def test_unload_entry(hass: HomeAssistant) -> None: entries = hass.config_entries.async_entries(DOMAIN) assert entries assert len(entries) == 1 - assert entries[0].state == ConfigEntryState.LOADED + assert entries[0].state is ConfigEntryState.LOADED await hass.config_entries.async_unload(entries[0].entry_id) await hass.async_block_till_done() - assert entries[0].state == ConfigEntryState.NOT_LOADED + assert entries[0].state is ConfigEntryState.NOT_LOADED async def test_login_error(hass: HomeAssistant) -> None: @@ -54,7 +54,7 @@ async def test_login_error(hass: HomeAssistant) -> None: entries = hass.config_entries.async_entries(DOMAIN) assert entries assert len(entries) == 1 - assert entries[0].state == ConfigEntryState.SETUP_ERROR + assert entries[0].state is ConfigEntryState.SETUP_ERROR flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN) assert flows assert len(flows) == 1 @@ -76,7 +76,7 @@ async def test_http_error(hass: HomeAssistant) -> None: entries = hass.config_entries.async_entries(DOMAIN) assert entries assert len(entries) == 1 - assert entries[0].state == ConfigEntryState.SETUP_RETRY + assert entries[0].state is ConfigEntryState.SETUP_RETRY async def test_new_token(hass: HomeAssistant, freezer: FrozenDateTimeFactory) -> None: @@ -98,7 +98,7 @@ async def test_new_token(hass: HomeAssistant, freezer: FrozenDateTimeFactory) -> entries = hass.config_entries.async_entries(DOMAIN) assert entries assert len(entries) == 1 - assert entries[0].state == ConfigEntryState.LOADED + assert entries[0].state is ConfigEntryState.LOADED with ( patch("lacrosse_view.LaCrosse.login", return_value=True) as login, @@ -135,7 +135,7 @@ async def test_failed_token( entries = hass.config_entries.async_entries(DOMAIN) assert entries assert len(entries) == 1 - assert entries[0].state == ConfigEntryState.LOADED + assert entries[0].state is ConfigEntryState.LOADED with patch("lacrosse_view.LaCrosse.login", side_effect=LoginError("Test")): freezer.tick(timedelta(hours=1)) @@ -145,7 +145,7 @@ async def test_failed_token( entries = hass.config_entries.async_entries(DOMAIN) assert entries assert len(entries) == 1 - assert entries[0].state == ConfigEntryState.LOADED + assert entries[0].state is ConfigEntryState.LOADED flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN) assert flows diff --git a/tests/components/lacrosse_view/test_sensor.py b/tests/components/lacrosse_view/test_sensor.py index b9140e6173f..11faaf8877e 100644 --- a/tests/components/lacrosse_view/test_sensor.py +++ b/tests/components/lacrosse_view/test_sensor.py @@ -43,7 +43,7 @@ async def test_entities_added(hass: HomeAssistant) -> None: entries = hass.config_entries.async_entries(DOMAIN) assert entries assert len(entries) == 1 - assert entries[0].state == ConfigEntryState.LOADED + assert entries[0].state is ConfigEntryState.LOADED assert hass.states.get("sensor.test_temperature") @@ -67,7 +67,7 @@ async def test_sensor_permission( entries = hass.config_entries.async_entries(DOMAIN) assert entries assert len(entries) == 1 - assert entries[0].state == ConfigEntryState.SETUP_ERROR + assert entries[0].state is ConfigEntryState.SETUP_ERROR assert not hass.states.get("sensor.test_temperature") assert "This account does not have permission to read Test" in caplog.text @@ -92,7 +92,7 @@ async def test_field_not_supported( entries = hass.config_entries.async_entries(DOMAIN) assert entries assert len(entries) == 1 - assert entries[0].state == ConfigEntryState.LOADED + assert entries[0].state is ConfigEntryState.LOADED assert hass.states.get("sensor.test_some_unsupported_field") is None assert "Unsupported sensor field" in caplog.text @@ -128,7 +128,7 @@ async def test_field_types( entries = hass.config_entries.async_entries(DOMAIN) assert entries assert len(entries) == 1 - assert entries[0].state == ConfigEntryState.LOADED + assert entries[0].state is ConfigEntryState.LOADED assert hass.states.get(f"sensor.test_{entity_id}").state == expected @@ -151,7 +151,7 @@ async def test_no_field(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) - entries = hass.config_entries.async_entries(DOMAIN) assert entries assert len(entries) == 1 - assert entries[0].state == ConfigEntryState.LOADED + assert entries[0].state is ConfigEntryState.LOADED assert hass.states.get("sensor.test_temperature").state == "unavailable" @@ -174,5 +174,5 @@ async def test_field_data_missing(hass: HomeAssistant) -> None: entries = hass.config_entries.async_entries(DOMAIN) assert entries assert len(entries) == 1 - assert entries[0].state == ConfigEntryState.LOADED + assert entries[0].state is ConfigEntryState.LOADED assert hass.states.get("sensor.test_temperature").state == "unknown" diff --git a/tests/components/lamarzocco/test_config_flow.py b/tests/components/lamarzocco/test_config_flow.py index 8539f14ce7f..14f794000d8 100644 --- a/tests/components/lamarzocco/test_config_flow.py +++ b/tests/components/lamarzocco/test_config_flow.py @@ -10,7 +10,7 @@ from homeassistant.components.lamarzocco.const import ( CONF_USE_BLUETOOTH, DOMAIN, ) -from homeassistant.config_entries import SOURCE_REAUTH +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 @@ -346,7 +346,7 @@ async def test_options_flow( ) -> None: """Test options flow.""" await async_init_integration(hass, mock_config_entry) - assert mock_config_entry.state is config_entries.ConfigEntryState.LOADED + assert mock_config_entry.state is ConfigEntryState.LOADED result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) diff --git a/tests/components/laundrify/test_init.py b/tests/components/laundrify/test_init.py index 84965af6768..e3ec54a3225 100644 --- a/tests/components/laundrify/test_init.py +++ b/tests/components/laundrify/test_init.py @@ -20,7 +20,7 @@ async def test_setup_entry_api_unauthorized( await hass.async_block_till_done() assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert config_entry.state == ConfigEntryState.SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR assert not hass.data.get(DOMAIN) @@ -35,7 +35,7 @@ async def test_setup_entry_api_cannot_connect( await hass.async_block_till_done() assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY assert not hass.data.get(DOMAIN) @@ -46,7 +46,7 @@ async def test_setup_entry_successful(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED async def test_setup_entry_unload(hass: HomeAssistant) -> None: @@ -56,4 +56,4 @@ async def test_setup_entry_unload(hass: HomeAssistant) -> None: await hass.config_entries.async_unload(config_entry.entry_id) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/lawn_mower/test_init.py b/tests/components/lawn_mower/test_init.py index 1cf6c7f4b24..87115cb1900 100644 --- a/tests/components/lawn_mower/test_init.py +++ b/tests/components/lawn_mower/test_init.py @@ -114,13 +114,13 @@ async def test_lawn_mower_setup(hass: HomeAssistant) -> None: assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert hass.states.get(entity1.entity_id) assert await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED entity_state = hass.states.get(entity1.entity_id) assert entity_state diff --git a/tests/components/lcn/test_init.py b/tests/components/lcn/test_init.py index 292ebc045b2..670735439ce 100644 --- a/tests/components/lcn/test_init.py +++ b/tests/components/lcn/test_init.py @@ -20,12 +20,12 @@ from .conftest import MockPchkConnectionManager, setup_component async def test_async_setup_entry(hass: HomeAssistant, entry, lcn_connection) -> None: """Test a successful setup entry and unload of entry.""" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) @@ -36,7 +36,7 @@ async def test_async_setup_multiple_entries(hass: HomeAssistant, entry, entry2) 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 == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 2 @@ -44,7 +44,7 @@ async def test_async_setup_multiple_entries(hass: HomeAssistant, entry, entry2) assert await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) @@ -96,7 +96,7 @@ async def test_async_setup_entry_raises_authentication_error( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR async def test_async_setup_entry_raises_license_error( @@ -110,7 +110,7 @@ async def test_async_setup_entry_raises_license_error( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR async def test_async_setup_entry_raises_timeout_error( @@ -122,7 +122,7 @@ async def test_async_setup_entry_raises_timeout_error( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR async def test_async_setup_from_configuration_yaml(hass: HomeAssistant) -> None: diff --git a/tests/components/lidarr/test_init.py b/tests/components/lidarr/test_init.py index 48c5e3ff9a6..f10dc117b9c 100644 --- a/tests/components/lidarr/test_init.py +++ b/tests/components/lidarr/test_init.py @@ -14,7 +14,7 @@ async def test_setup( """Test setup.""" await setup_integration() entry = hass.config_entries.async_entries(DOMAIN)[0] - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() @@ -30,7 +30,7 @@ async def test_async_setup_entry_not_ready( await setup_integration() entry = hass.config_entries.async_entries(DOMAIN)[0] assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY assert not hass.data.get(DOMAIN) @@ -41,7 +41,7 @@ async def test_async_setup_entry_auth_failed( await setup_integration() entry = hass.config_entries.async_entries(DOMAIN)[0] assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR assert not hass.data.get(DOMAIN) diff --git a/tests/components/lifx/test_init.py b/tests/components/lifx/test_init.py index 3d0d127bf5c..42ece68a2c5 100644 --- a/tests/components/lifx/test_init.py +++ b/tests/components/lifx/test_init.py @@ -87,10 +87,10 @@ async def test_config_entry_reload(hass: HomeAssistant) -> None: with _patch_discovery(), _patch_config_flow_try_connect(), _patch_device(): await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) await hass.async_block_till_done() - assert already_migrated_config_entry.state == ConfigEntryState.LOADED + assert already_migrated_config_entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(already_migrated_config_entry.entry_id) await hass.async_block_till_done() - assert already_migrated_config_entry.state == ConfigEntryState.NOT_LOADED + assert already_migrated_config_entry.state is ConfigEntryState.NOT_LOADED async def test_config_entry_retry(hass: HomeAssistant) -> None: @@ -106,7 +106,7 @@ async def test_config_entry_retry(hass: HomeAssistant) -> None: ): await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) await hass.async_block_till_done() - assert already_migrated_config_entry.state == ConfigEntryState.SETUP_RETRY + assert already_migrated_config_entry.state is ConfigEntryState.SETUP_RETRY async def test_get_version_fails(hass: HomeAssistant) -> None: @@ -123,7 +123,7 @@ async def test_get_version_fails(hass: HomeAssistant) -> None: with _patch_discovery(device=bulb), _patch_device(device=bulb): await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) await hass.async_block_till_done() - assert already_migrated_config_entry.state == ConfigEntryState.SETUP_RETRY + assert already_migrated_config_entry.state is ConfigEntryState.SETUP_RETRY async def test_dns_error_at_startup(hass: HomeAssistant) -> None: @@ -158,7 +158,7 @@ async def test_dns_error_at_startup(hass: HomeAssistant) -> None: ): await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) await hass.async_block_till_done() - assert already_migrated_config_entry.state == ConfigEntryState.SETUP_RETRY + assert already_migrated_config_entry.state is ConfigEntryState.SETUP_RETRY async def test_config_entry_wrong_serial( @@ -173,7 +173,7 @@ async def test_config_entry_wrong_serial( with _patch_discovery(), _patch_config_flow_try_connect(), _patch_device(): await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) await hass.async_block_till_done() - assert already_migrated_config_entry.state == ConfigEntryState.SETUP_RETRY + assert already_migrated_config_entry.state is ConfigEntryState.SETUP_RETRY assert ( "Unexpected device found at 127.0.0.1; expected aa:bb:cc:dd:ee:c0, found aa:bb:cc:dd:ee:cc" diff --git a/tests/components/linear_garage_door/test_coordinator.py b/tests/components/linear_garage_door/test_coordinator.py index 1e46d294f3f..be38b316c56 100644 --- a/tests/components/linear_garage_door/test_coordinator.py +++ b/tests/components/linear_garage_door/test_coordinator.py @@ -38,7 +38,7 @@ async def test_invalid_password( entries = hass.config_entries.async_entries(DOMAIN) assert entries assert len(entries) == 1 - assert entries[0].state == ConfigEntryState.SETUP_ERROR + assert entries[0].state is ConfigEntryState.SETUP_ERROR flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN) assert flows assert len(flows) == 1 @@ -70,4 +70,4 @@ async def test_invalid_login( entries = hass.config_entries.async_entries(DOMAIN) assert entries assert len(entries) == 1 - assert entries[0].state == ConfigEntryState.SETUP_RETRY + assert entries[0].state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/linear_garage_door/test_cover.py b/tests/components/linear_garage_door/test_cover.py index e692d1867dc..9db7b80fd0e 100644 --- a/tests/components/linear_garage_door/test_cover.py +++ b/tests/components/linear_garage_door/test_cover.py @@ -32,7 +32,7 @@ async def test_data(hass: HomeAssistant) -> None: entries = hass.config_entries.async_entries(DOMAIN) assert entries assert len(entries) == 1 - assert entries[0].state == ConfigEntryState.LOADED + assert entries[0].state is ConfigEntryState.LOADED assert hass.states.get("cover.test_garage_1").state == STATE_OPEN assert hass.states.get("cover.test_garage_2").state == STATE_CLOSED assert hass.states.get("cover.test_garage_3").state == STATE_OPENING diff --git a/tests/components/linear_garage_door/test_init.py b/tests/components/linear_garage_door/test_init.py index 32ebda7e125..63975c8bd3f 100644 --- a/tests/components/linear_garage_door/test_init.py +++ b/tests/components/linear_garage_door/test_init.py @@ -53,7 +53,7 @@ async def test_unload_entry(hass: HomeAssistant) -> None: entries = hass.config_entries.async_entries(DOMAIN) assert entries assert len(entries) == 1 - assert entries[0].state == ConfigEntryState.LOADED + assert entries[0].state is ConfigEntryState.LOADED with patch( "homeassistant.components.linear_garage_door.coordinator.Linear.close", @@ -61,4 +61,4 @@ async def test_unload_entry(hass: HomeAssistant) -> None: ): await hass.config_entries.async_unload(entries[0].entry_id) await hass.async_block_till_done() - assert entries[0].state == ConfigEntryState.NOT_LOADED + assert entries[0].state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/local_calendar/test_init.py b/tests/components/local_calendar/test_init.py index 8e79cccea36..8bb7a26d794 100644 --- a/tests/components/local_calendar/test_init.py +++ b/tests/components/local_calendar/test_init.py @@ -17,7 +17,7 @@ async def test_load_unload( ) -> None: """Test loading and unloading a config entry.""" - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED state = hass.states.get(TEST_ENTITY) assert state @@ -26,7 +26,7 @@ async def test_load_unload( await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED state = hass.states.get(TEST_ENTITY) assert state assert state.state == "unavailable" @@ -54,7 +54,7 @@ async def test_load_failure( ) -> None: """Test failures loading the store.""" - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY state = hass.states.get(TEST_ENTITY) assert not state diff --git a/tests/components/local_ip/test_init.py b/tests/components/local_ip/test_init.py index 54126b21243..cc4f4dd4968 100644 --- a/tests/components/local_ip/test_init.py +++ b/tests/components/local_ip/test_init.py @@ -2,9 +2,9 @@ from __future__ import annotations -from homeassistant import config_entries from homeassistant.components.local_ip import DOMAIN from homeassistant.components.network import MDNS_TARGET_IP, async_get_source_ip +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -17,7 +17,7 @@ async def test_basic_setup(hass: HomeAssistant, mock_get_source_ip) -> None: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED local_ip = await async_get_source_ip(hass, target_ip=MDNS_TARGET_IP) state = hass.states.get(f"sensor.{DOMAIN}") @@ -26,4 +26,4 @@ async def test_basic_setup(hass: HomeAssistant, mock_get_source_ip) -> None: assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state is config_entries.ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/local_todo/test_init.py b/tests/components/local_todo/test_init.py index 98da2ef3c12..c27c65c5706 100644 --- a/tests/components/local_todo/test_init.py +++ b/tests/components/local_todo/test_init.py @@ -17,7 +17,7 @@ async def test_load_unload( ) -> None: """Test loading and unloading a config entry.""" - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED state = hass.states.get(TEST_ENTITY) assert state @@ -26,7 +26,7 @@ async def test_load_unload( await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED state = hass.states.get(TEST_ENTITY) assert state assert state.state == "unavailable" @@ -54,7 +54,7 @@ async def test_load_failure( ) -> None: """Test failures loading the todo store.""" - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY state = hass.states.get(TEST_ENTITY) assert not state diff --git a/tests/components/lyric/test_config_flow.py b/tests/components/lyric/test_config_flow.py index d53b0d57613..73b3aae2d3d 100644 --- a/tests/components/lyric/test_config_flow.py +++ b/tests/components/lyric/test_config_flow.py @@ -11,6 +11,7 @@ from homeassistant.components.application_credentials import ( async_import_client_credential, ) from homeassistant.components.lyric.const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow @@ -105,7 +106,7 @@ async def test_full_flow( assert DOMAIN in hass.config.components entry = hass.config_entries.async_entries(DOMAIN)[0] - assert entry.state is config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(mock_setup.mock_calls) == 1 diff --git a/tests/components/matter/test_init.py b/tests/components/matter/test_init.py index 327e73dd4de..4472e712b20 100644 --- a/tests/components/matter/test_init.py +++ b/tests/components/matter/test_init.py @@ -68,7 +68,7 @@ async def test_entry_setup_unload( await hass.async_block_till_done() assert matter_client.connect.call_count == 1 - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED entity_state = hass.states.get("light.mock_onoff_light") assert entity_state assert entity_state.state != STATE_UNAVAILABLE @@ -76,7 +76,7 @@ async def test_entry_setup_unload( await hass.config_entries.async_unload(entry.entry_id) assert matter_client.disconnect.call_count == 1 - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED entity_state = hass.states.get("light.mock_onoff_light") assert entity_state assert entity_state.state == STATE_UNAVAILABLE @@ -210,12 +210,12 @@ async def test_listen_failure_config_entry_loaded( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED listen_block.set() await hass.async_block_till_done() - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY assert matter_client.disconnect.call_count == 1 diff --git a/tests/components/mikrotik/test_init.py b/tests/components/mikrotik/test_init.py index 96ec0f5771e..cc6a737e75a 100644 --- a/tests/components/mikrotik/test_init.py +++ b/tests/components/mikrotik/test_init.py @@ -34,7 +34,7 @@ async def test_successful_config_entry(hass: HomeAssistant) -> None: entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED async def test_hub_connection_error(hass: HomeAssistant, mock_api: MagicMock) -> None: @@ -49,7 +49,7 @@ async def test_hub_connection_error(hass: HomeAssistant, mock_api: MagicMock) -> await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_hub_authentication_error( @@ -66,7 +66,7 @@ async def test_hub_authentication_error( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR async def test_unload_entry(hass: HomeAssistant) -> None: @@ -83,5 +83,5 @@ async def test_unload_entry(hass: HomeAssistant) -> None: assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert entry.entry_id not in hass.data[DOMAIN] diff --git a/tests/components/minecraft_server/test_init.py b/tests/components/minecraft_server/test_init.py index 9c02fb56d91..8ac2e7ca04d 100644 --- a/tests/components/minecraft_server/test_init.py +++ b/tests/components/minecraft_server/test_init.py @@ -134,12 +134,12 @@ async def test_setup_and_unload_entry( ): assert await hass.config_entries.async_setup(java_mock_config_entry.entry_id) await hass.async_block_till_done() - assert java_mock_config_entry.state == ConfigEntryState.LOADED + assert java_mock_config_entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(java_mock_config_entry.entry_id) await hass.async_block_till_done() assert not hass.data.get(DOMAIN) - assert java_mock_config_entry.state == ConfigEntryState.NOT_LOADED + assert java_mock_config_entry.state is ConfigEntryState.NOT_LOADED async def test_setup_entry_lookup_failure( @@ -157,7 +157,7 @@ async def test_setup_entry_lookup_failure( ) await hass.async_block_till_done() - assert java_mock_config_entry.state == ConfigEntryState.SETUP_RETRY + assert java_mock_config_entry.state is ConfigEntryState.SETUP_RETRY async def test_setup_entry_init_failure( @@ -175,7 +175,7 @@ async def test_setup_entry_init_failure( ) await hass.async_block_till_done() - assert java_mock_config_entry.state == ConfigEntryState.SETUP_RETRY + assert java_mock_config_entry.state is ConfigEntryState.SETUP_RETRY async def test_setup_entry_not_ready( @@ -199,7 +199,7 @@ async def test_setup_entry_not_ready( ) await hass.async_block_till_done() - assert java_mock_config_entry.state == ConfigEntryState.SETUP_RETRY + assert java_mock_config_entry.state is ConfigEntryState.SETUP_RETRY async def test_entry_migration( @@ -246,7 +246,7 @@ async def test_entry_migration( CONF_ADDRESS: TEST_ADDRESS, } assert migrated_config_entry.version == 3 - assert migrated_config_entry.state == ConfigEntryState.LOADED + assert migrated_config_entry.state is ConfigEntryState.LOADED # Test migrated device entry. device_entry = device_registry.async_get(device_entry_id) @@ -305,7 +305,7 @@ async def test_entry_migration_host_only( CONF_ADDRESS: TEST_HOST, } assert v1_mock_config_entry.version == 3 - assert v1_mock_config_entry.state == ConfigEntryState.LOADED + assert v1_mock_config_entry.state is ConfigEntryState.LOADED async def test_entry_migration_v3_failure( @@ -335,4 +335,4 @@ async def test_entry_migration_v3_failure( # Test config entry. assert v1_mock_config_entry.version == 2 - assert v1_mock_config_entry.state == ConfigEntryState.MIGRATION_ERROR + assert v1_mock_config_entry.state is ConfigEntryState.MIGRATION_ERROR diff --git a/tests/components/modem_callerid/test_init.py b/tests/components/modem_callerid/test_init.py index ccf97f60e10..e12850f763d 100644 --- a/tests/components/modem_callerid/test_init.py +++ b/tests/components/modem_callerid/test_init.py @@ -30,7 +30,7 @@ async def test_setup_entry(hass: HomeAssistant) -> None: patch("phone_modem.PhoneModem._modem_sm"), ): await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED async def test_async_setup_entry_not_ready(hass: HomeAssistant) -> None: @@ -45,7 +45,7 @@ async def test_async_setup_entry_not_ready(hass: HomeAssistant) -> None: modemmock.side_effect = exceptions.SerialError await hass.config_entries.async_setup(entry.entry_id) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY assert not hass.data.get(DOMAIN) diff --git a/tests/components/mqtt/test_util.py b/tests/components/mqtt/test_util.py index b07dfc1f642..c485e8a9c27 100644 --- a/tests/components/mqtt/test_util.py +++ b/tests/components/mqtt/test_util.py @@ -162,7 +162,7 @@ async def test_waiting_for_client_not_loaded( for _ in range(4): hass.async_create_task(_async_just_in_time_subscribe()) - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert await hass.config_entries.async_setup(entry.entry_id) assert len(unsubs) == 4 for unsub in unsubs: @@ -182,7 +182,7 @@ async def test_waiting_for_client_loaded( unsub = await mqtt.async_subscribe(hass, "test_topic", lambda msg: None) entry = hass.config_entries.async_entries(mqtt.DATA_MQTT)[0] - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED await _async_just_in_time_subscribe() @@ -209,13 +209,13 @@ async def test_waiting_for_client_entry_fails( assert not await mqtt.async_wait_for_mqtt_client(hass) hass.async_create_task(_async_just_in_time_subscribe()) - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED with patch( "homeassistant.components.mqtt.async_setup_entry", side_effect=Exception, ): await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR async def test_waiting_for_client_setup_fails( @@ -237,12 +237,12 @@ async def test_waiting_for_client_setup_fails( assert not await mqtt.async_wait_for_mqtt_client(hass) hass.async_create_task(_async_just_in_time_subscribe()) - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED # Simulate MQTT setup fails before the client would become available mqtt_client_mock.connect.side_effect = Exception assert not await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR @patch("homeassistant.components.mqtt.util.AVAILABILITY_TIMEOUT", 0.01) @@ -260,7 +260,7 @@ async def test_waiting_for_client_timeout( ) entry.add_to_hass(hass) - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED # returns False after timeout assert not await mqtt.async_wait_for_mqtt_client(hass) @@ -284,7 +284,7 @@ async def test_waiting_for_client_with_disabled_entry( entry.entry_id, ConfigEntryDisabler.USER ) - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED # returns False because entry is disabled assert not await mqtt.async_wait_for_mqtt_client(hass) diff --git a/tests/components/mystrom/test_init.py b/tests/components/mystrom/test_init.py index 0304a0eb270..f22665efb6b 100644 --- a/tests/components/mystrom/test_init.py +++ b/tests/components/mystrom/test_init.py @@ -113,7 +113,7 @@ async def test_init_of_unknown_bulb( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR async def test_init_of_unknown_device( @@ -127,7 +127,7 @@ async def test_init_of_unknown_device( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR async def test_init_cannot_connect_because_of_device_info( @@ -145,7 +145,7 @@ async def test_init_cannot_connect_because_of_device_info( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY async def test_init_cannot_connect_because_of_get_state( @@ -168,4 +168,4 @@ async def test_init_cannot_connect_because_of_get_state( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/myuplink/test_init.py b/tests/components/myuplink/test_init.py index 328dc55d4ad..421eb9b59c2 100644 --- a/tests/components/myuplink/test_init.py +++ b/tests/components/myuplink/test_init.py @@ -26,12 +26,12 @@ async def test_load_unload_entry( await setup_integration(hass, mock_config_entry) entry = hass.config_entries.async_entries(DOMAIN)[0] - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED await hass.config_entries.async_remove(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED @pytest.mark.parametrize( From 35f2287d1ad1a6d59f3ec8ad05cef4f001f83396 Mon Sep 17 00:00:00 2001 From: RJPoelstra <36924801+RJPoelstra@users.noreply.github.com> Date: Fri, 5 Apr 2024 17:20:58 +0200 Subject: [PATCH 297/967] Bump python-MotionMount to 1.0.0 (#114945) --- homeassistant/components/motionmount/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/motionmount/manifest.json b/homeassistant/components/motionmount/manifest.json index bfe7e21fce9..e6a7bd50fba 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==0.3.1"], + "requirements": ["python-MotionMount==1.0.0"], "zeroconf": ["_tvm._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 080273b41df..da52c3b03d9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2197,7 +2197,7 @@ pytfiac==0.4 pythinkingcleaner==0.0.3 # homeassistant.components.motionmount -python-MotionMount==0.3.1 +python-MotionMount==1.0.0 # homeassistant.components.awair python-awair==0.2.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 16967556fb7..43ae9cb445a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1712,7 +1712,7 @@ pytautulli==23.1.1 pytedee-async==0.2.17 # homeassistant.components.motionmount -python-MotionMount==0.3.1 +python-MotionMount==1.0.0 # homeassistant.components.awair python-awair==0.2.4 From f2c091fe0c9d4033989dea31086741df90744a3a Mon Sep 17 00:00:00 2001 From: Ashot Tonoyan Date: Fri, 5 Apr 2024 19:32:23 +0400 Subject: [PATCH 298/967] Include serial number in HomeKit device info (#114688) Very useful when adding a hub that has many identical devices already paired --- .../homekit_controller/connection.py | 1 + .../snapshots/test_init.ambr | 196 +++++++++--------- 2 files changed, 99 insertions(+), 98 deletions(-) diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 883ec4f1a44..7b3f8e45845 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -384,6 +384,7 @@ class HKDevice: model=accessory.model, sw_version=accessory.firmware_revision, hw_version=accessory.hardware_revision, + serial_number=accessory.serial_number, ) if accessory.aid != 1: diff --git a/tests/components/homekit_controller/snapshots/test_init.ambr b/tests/components/homekit_controller/snapshots/test_init.ambr index 10f62920d8e..0507976cd20 100644 --- a/tests/components/homekit_controller/snapshots/test_init.ambr +++ b/tests/components/homekit_controller/snapshots/test_init.ambr @@ -26,7 +26,7 @@ 'model': 'AP2', 'name': 'Airversa AP2 1808', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '1234', 'suggested_area': None, 'sw_version': '0.8.16', }), @@ -622,7 +622,7 @@ 'model': 'T8010', 'name': 'eufy HomeBase2-0AAA', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'A0000A000000000A', 'suggested_area': None, 'sw_version': '2.1.6', }), @@ -695,7 +695,7 @@ 'model': 'T8113', 'name': 'eufyCam2-0000', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'A0000A000000000D', 'suggested_area': None, 'sw_version': '1.6.7', }), @@ -936,7 +936,7 @@ 'model': 'T8113', 'name': 'eufyCam2-000A', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'A0000A000000000B', 'suggested_area': None, 'sw_version': '1.6.7', }), @@ -1177,7 +1177,7 @@ 'model': 'T8113', 'name': 'eufyCam2-000A', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'A0000A000000000C', 'suggested_area': None, 'sw_version': '1.6.7', }), @@ -1422,7 +1422,7 @@ 'model': 'HE1-G01', 'name': 'Aqara-Hub-E1-00A0', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '00aa00000a0', 'suggested_area': None, 'sw_version': '3.3.0', }), @@ -1628,7 +1628,7 @@ 'model': 'AS006', 'name': 'Contact Sensor', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '158d0007c59c6a', 'suggested_area': None, 'sw_version': '0', }), @@ -1792,7 +1792,7 @@ 'model': 'ZHWA11LM', 'name': 'Aqara Hub-1563', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '0000000123456789', 'suggested_area': None, 'sw_version': '1.4.7', }), @@ -2067,7 +2067,7 @@ 'model': 'AR004', 'name': 'Programmable Switch', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '111a1111a1a111', 'suggested_area': None, 'sw_version': '9', }), @@ -2190,7 +2190,7 @@ 'model': 'ABC1000', 'name': 'ArloBabyA0', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '00A0000000000', 'suggested_area': None, 'sw_version': '1.10.931', }), @@ -2674,7 +2674,7 @@ 'model': 'CS-IWO', 'name': 'InWall Outlet-0394DE', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '1020301376', 'suggested_area': None, 'sw_version': '1.0.0', }), @@ -3103,7 +3103,7 @@ 'model': 'REMOTE SENSOR', 'name': 'Basement', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'AB3C', 'suggested_area': None, 'sw_version': '1.0.0', }), @@ -3262,7 +3262,7 @@ 'model': 'ecobee3', 'name': 'HomeW', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '123456789012', 'suggested_area': None, 'sw_version': '4.2.394', }), @@ -3716,7 +3716,7 @@ 'model': 'REMOTE SENSOR', 'name': 'Kitchen', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'AB1C', 'suggested_area': None, 'sw_version': '1.0.0', }), @@ -3875,7 +3875,7 @@ 'model': 'REMOTE SENSOR', 'name': 'Porch', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'AB2C', 'suggested_area': None, 'sw_version': '1.0.0', }), @@ -4038,7 +4038,7 @@ 'model': 'ecobee3', 'name': 'HomeW', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '123456789012', 'suggested_area': None, 'sw_version': '4.2.394', }), @@ -4496,7 +4496,7 @@ 'model': 'REMOTE SENSOR', 'name': 'Basement', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'AB3C', 'suggested_area': None, 'sw_version': '1.0.0', }), @@ -4610,7 +4610,7 @@ 'model': 'ecobee3', 'name': 'HomeW', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '123456789012', 'suggested_area': None, 'sw_version': '4.2.394', }), @@ -4891,7 +4891,7 @@ 'model': 'REMOTE SENSOR', 'name': 'Kitchen', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'AB1C', 'suggested_area': None, 'sw_version': '1.0.0', }), @@ -5050,7 +5050,7 @@ 'model': 'REMOTE SENSOR', 'name': 'Porch', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'AB2C', 'suggested_area': None, 'sw_version': '1.0.0', }), @@ -5213,7 +5213,7 @@ 'model': 'ECB501', 'name': 'My ecobee', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '123456789016', 'suggested_area': None, 'sw_version': '4.7.340214', }), @@ -5680,7 +5680,7 @@ 'model': 'ecobee Switch+', 'name': 'Master Fan', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '111111111111', 'suggested_area': None, 'sw_version': '4.5.130201', }), @@ -5969,7 +5969,7 @@ 'model': 'Eve Degree 00AAA0000', 'name': 'Eve Degree AA11', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'AA00A0A00000', 'suggested_area': None, 'sw_version': '1.2.8', }), @@ -6325,7 +6325,7 @@ 'model': 'Eve Energy 20EAO8601', 'name': 'Eve Energy 50FF', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'AA00A0A00000', 'suggested_area': None, 'sw_version': '1.2.9', }), @@ -6663,7 +6663,7 @@ 'model': 'RavenSystem HAA', 'name': 'HAA-C718B3', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'C718B3-1', 'suggested_area': None, 'sw_version': '5.0.18', }), @@ -6864,7 +6864,7 @@ 'model': 'RavenSystem HAA', 'name': 'HAA-C718B3', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'C718B3-2', 'suggested_area': None, 'sw_version': '5.0.18', }), @@ -6981,7 +6981,7 @@ 'model': 'RYSE Shade', 'name': 'Family Room North', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'cover.family_door_north', 'suggested_area': None, 'sw_version': '3.6.2', }), @@ -7142,7 +7142,7 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', }), @@ -7215,7 +7215,7 @@ 'model': 'RYSE Shade', 'name': 'Kitchen Window', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'cover.kitchen_window', 'suggested_area': None, 'sw_version': '3.6.2', }), @@ -7380,7 +7380,7 @@ 'model': 'Fan', 'name': 'Ceiling Fan', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'fan.ceiling_fan', 'suggested_area': None, 'sw_version': '0.104.0.dev0', }), @@ -7500,7 +7500,7 @@ 'model': 'Bridge', 'name': 'Home Assistant Bridge', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '0.104.0.dev0', }), @@ -7573,7 +7573,7 @@ 'model': 'Fan', 'name': 'Living Room Fan', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'fan.living_room_fan', 'suggested_area': None, 'sw_version': '0.104.0.dev0', }), @@ -7698,7 +7698,7 @@ 'model': 'Climate Control', 'name': '89 Living Room', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'climate.89_living_room', 'suggested_area': None, 'sw_version': '2024.2.0', }), @@ -8020,7 +8020,7 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', }), @@ -8097,7 +8097,7 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', }), @@ -8170,7 +8170,7 @@ 'model': '1039102', 'name': 'Laundry Smoke ED78', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'light.laundry_smoke_ed78', 'suggested_area': None, 'sw_version': '1.4.84', }), @@ -8343,7 +8343,7 @@ 'model': 'RYSE Shade', 'name': 'Family Room North', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'cover.family_door_north', 'suggested_area': None, 'sw_version': '3.6.2', }), @@ -8504,7 +8504,7 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', }), @@ -8577,7 +8577,7 @@ 'model': 'RYSE Shade', 'name': 'Kitchen Window', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'cover.kitchen_window', 'suggested_area': None, 'sw_version': '3.6.2', }), @@ -8742,7 +8742,7 @@ 'model': 'Fan', 'name': 'Ceiling Fan', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'fan.ceiling_fan', 'suggested_area': None, 'sw_version': '0.104.0.dev0', }), @@ -8862,7 +8862,7 @@ 'model': 'Bridge', 'name': 'Home Assistant Bridge', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '0.104.0.dev0', }), @@ -8935,7 +8935,7 @@ 'model': 'Fan', 'name': 'Living Room Fan', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'fan.living_room_fan', 'suggested_area': None, 'sw_version': '0.104.0.dev0', }), @@ -9061,7 +9061,7 @@ 'model': 'Bridge', 'name': 'Home Assistant Bridge', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '0.104.0.dev0', }), @@ -9134,7 +9134,7 @@ 'model': 'Fan', 'name': 'Living Room Fan', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'fan.living_room_fan', 'suggested_area': None, 'sw_version': '0.104.0.dev0', }), @@ -9260,7 +9260,7 @@ 'model': 'Climate Control', 'name': '89 Living Room', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'climate.89_living_room', 'suggested_area': None, 'sw_version': '2024.2.0', }), @@ -9591,7 +9591,7 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', }), @@ -9668,7 +9668,7 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', }), @@ -9741,7 +9741,7 @@ 'model': 'WoHumi', 'name': 'Humidifier 182A', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'humidifier.humidifier_182a', 'suggested_area': None, 'sw_version': '2024.2.0', }), @@ -9921,7 +9921,7 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', }), @@ -9994,7 +9994,7 @@ 'model': 'WoHumi', 'name': 'Humidifier 182A', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'humidifier.humidifier_182a', 'suggested_area': None, 'sw_version': '2024.2.0', }), @@ -10174,7 +10174,7 @@ 'model': 'Bridge', 'name': 'HASS Bridge S6', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'homekit.bridge', 'suggested_area': None, 'sw_version': '2024.2.0', }), @@ -10247,7 +10247,7 @@ 'model': '1039102', 'name': 'Laundry Smoke ED78', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'light.laundry_smoke_ed78', 'suggested_area': None, 'sw_version': '1.4.84', }), @@ -10435,7 +10435,7 @@ 'model': 'Daikin-fwec3a-esp32-homekit-bridge', 'name': 'Air Conditioner', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '00000001', 'suggested_area': None, 'sw_version': '1.0.0', }), @@ -10633,7 +10633,7 @@ 'model': 'LTW012', 'name': 'Hue ambiance candle', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '6623462395276914', 'suggested_area': None, 'sw_version': '1.46.13', }), @@ -10769,7 +10769,7 @@ 'model': 'LTW012', 'name': 'Hue ambiance candle', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '6623462395276939', 'suggested_area': None, 'sw_version': '1.46.13', }), @@ -10905,7 +10905,7 @@ 'model': 'LTW012', 'name': 'Hue ambiance candle', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '6623462403113447', 'suggested_area': None, 'sw_version': '1.46.13', }), @@ -11041,7 +11041,7 @@ 'model': 'LTW012', 'name': 'Hue ambiance candle', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '6623462403233419', 'suggested_area': None, 'sw_version': '1.46.13', }), @@ -11177,7 +11177,7 @@ 'model': 'LTW013', 'name': 'Hue ambiance spot', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '6623462412411853', 'suggested_area': None, 'sw_version': '1.46.13', }), @@ -11323,7 +11323,7 @@ 'model': 'LTW013', 'name': 'Hue ambiance spot', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '6623462412413293', 'suggested_area': None, 'sw_version': '1.46.13', }), @@ -11469,7 +11469,7 @@ 'model': 'RWL021', 'name': 'Hue dimmer switch', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '6623462389072572', 'suggested_area': None, 'sw_version': '45.1.17846', }), @@ -11784,7 +11784,7 @@ 'model': 'LWB010', 'name': 'Hue white lamp', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '6623462378982941', 'suggested_area': None, 'sw_version': '1.46.13', }), @@ -11907,7 +11907,7 @@ 'model': 'LWB010', 'name': 'Hue white lamp', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '6623462378983942', 'suggested_area': None, 'sw_version': '1.46.13', }), @@ -12030,7 +12030,7 @@ 'model': 'LWB010', 'name': 'Hue white lamp', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '6623462379122122', 'suggested_area': None, 'sw_version': '1.46.13', }), @@ -12153,7 +12153,7 @@ 'model': 'LWB010', 'name': 'Hue white lamp', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '6623462379123707', 'suggested_area': None, 'sw_version': '1.46.13', }), @@ -12276,7 +12276,7 @@ 'model': 'LWB010', 'name': 'Hue white lamp', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '6623462383114163', 'suggested_area': None, 'sw_version': '1.46.13', }), @@ -12399,7 +12399,7 @@ 'model': 'LWB010', 'name': 'Hue white lamp', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '6623462383114193', 'suggested_area': None, 'sw_version': '1.46.13', }), @@ -12522,7 +12522,7 @@ 'model': 'LWB010', 'name': 'Hue white lamp', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '6623462385996792', 'suggested_area': None, 'sw_version': '1.46.13', }), @@ -12645,7 +12645,7 @@ 'model': 'BSB002', 'name': 'Philips hue - 482544', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '123456', 'suggested_area': None, 'sw_version': '1.32.1932126170', }), @@ -12722,7 +12722,7 @@ 'model': 'LS1', 'name': 'Koogeek-LS1-20833F', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'AAAA011111111111', 'suggested_area': None, 'sw_version': '2.2.15', }), @@ -12864,7 +12864,7 @@ 'model': 'P1EU', 'name': 'Koogeek-P1-A00AA0', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'EUCP03190xxxxx48', 'suggested_area': None, 'sw_version': '2.3.7', }), @@ -13027,7 +13027,7 @@ 'model': 'KH02CN', 'name': 'Koogeek-SW2-187A91', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'CNNT061751001372', 'suggested_area': None, 'sw_version': '1.0.3', }), @@ -13229,7 +13229,7 @@ 'model': 'E30 2B', 'name': 'Lennox', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'XXXXXXXX', 'suggested_area': None, 'sw_version': '3.40.XX', }), @@ -13509,7 +13509,7 @@ 'model': 'OLED55B9PUA', 'name': 'LG webOS TV AF80', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '999AAAAAA999', 'suggested_area': None, 'sw_version': '04.71.04', }), @@ -13688,7 +13688,7 @@ 'model': 'PD-FSQN-XX', 'name': 'Caséta® Wireless Fan Speed Control', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '39024290', 'suggested_area': None, 'sw_version': '001.005', }), @@ -13808,7 +13808,7 @@ 'model': 'L-BDG2-WH', 'name': 'Smart Bridge 2', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '12344331', 'suggested_area': None, 'sw_version': '08.08', }), @@ -13885,7 +13885,7 @@ 'model': 'MSS425F', 'name': 'MSS425F-15cc', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'HH41234', 'suggested_area': None, 'sw_version': '4.2.3', }), @@ -14162,7 +14162,7 @@ 'model': 'MSS565', 'name': 'MSS565-28da', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'BB1121', 'suggested_area': None, 'sw_version': '4.1.9', }), @@ -14289,7 +14289,7 @@ 'model': 'v1', 'name': 'Mysa-85dda9', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'AAAAAAA000', 'suggested_area': None, 'sw_version': '2.8.1', }), @@ -14617,7 +14617,7 @@ 'model': 'NL55', 'name': 'Nanoleaf Strip 3B32', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'AAAA011111111111', 'suggested_area': None, 'sw_version': '1.4.40', }), @@ -14887,7 +14887,7 @@ 'model': 'Netatmo Doorbell', 'name': 'Netatmo-Doorbell-g738658', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'g738658', 'suggested_area': None, 'sw_version': '80.0.0', }), @@ -15179,7 +15179,7 @@ 'model': 'Smart CO Alarm', 'name': 'Smart CO Alarm', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '1234', 'suggested_area': None, 'sw_version': '1.0.3', }), @@ -15338,7 +15338,7 @@ 'model': 'Healthy Home Coach', 'name': 'Healthy Home Coach', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'AAAAAAAAAAAAA', 'suggested_area': None, 'sw_version': '59', }), @@ -15639,7 +15639,7 @@ 'model': 'SPK5 Pro', 'name': 'RainMachine-00ce4a', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '00aa0000aa0a', 'suggested_area': None, 'sw_version': '1.0.4', }), @@ -16060,7 +16060,7 @@ 'model': 'RYSE Shade', 'name': 'Master Bath South', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '1.0.0', 'suggested_area': None, 'sw_version': '3.0.8', }), @@ -16221,7 +16221,7 @@ 'model': 'RYSE SmartBridge', 'name': 'RYSE SmartBridge', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '0101.3521.0436', 'suggested_area': None, 'sw_version': '1.3.0', }), @@ -16294,7 +16294,7 @@ 'model': 'RYSE Shade', 'name': 'RYSE SmartShade', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '', 'suggested_area': None, 'sw_version': '', }), @@ -16459,7 +16459,7 @@ 'model': 'RYSE Shade', 'name': 'BR Left', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '1.0.0', 'suggested_area': None, 'sw_version': '3.0.8', }), @@ -16620,7 +16620,7 @@ 'model': 'RYSE Shade', 'name': 'LR Left', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '1.0.0', 'suggested_area': None, 'sw_version': '3.0.8', }), @@ -16781,7 +16781,7 @@ 'model': 'RYSE Shade', 'name': 'LR Right', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '1.0.0', 'suggested_area': None, 'sw_version': '3.0.8', }), @@ -16942,7 +16942,7 @@ 'model': 'RYSE SmartBridge', 'name': 'RYSE SmartBridge', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '0401.3521.0679', 'suggested_area': None, 'sw_version': '1.3.0', }), @@ -17015,7 +17015,7 @@ 'model': 'RYSE Shade', 'name': 'RZSS', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '1.0.0', 'suggested_area': None, 'sw_version': '3.0.8', }), @@ -17180,7 +17180,7 @@ 'model': 'BE479CAM619', 'name': 'SENSE ', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'AAAAAAA000', 'suggested_area': None, 'sw_version': '004.027.000', }), @@ -17298,7 +17298,7 @@ 'model': 'SIMPLEconnect', 'name': 'SIMPLEconnect Fan-06F674', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '1234567890abcd', 'suggested_area': None, 'sw_version': '', }), @@ -17473,7 +17473,7 @@ 'model': 'VELUX Gateway', 'name': 'VELUX Gateway', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'a1a11a1', 'suggested_area': None, 'sw_version': '70', }), @@ -17546,7 +17546,7 @@ 'model': 'VELUX Sensor', 'name': 'VELUX Sensor', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'a11b111', 'suggested_area': None, 'sw_version': '16', }), @@ -17754,7 +17754,7 @@ 'model': 'VELUX Window', 'name': 'VELUX Window', 'name_by_user': None, - 'serial_number': None, + 'serial_number': '1111111a114a111a', 'suggested_area': None, 'sw_version': '48', }), @@ -17874,7 +17874,7 @@ 'model': 'Flowerbud', 'name': 'VOCOlinc-Flowerbud-0d324b', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'AM01121849000327', 'suggested_area': None, 'sw_version': '3.121.2', }), @@ -18178,7 +18178,7 @@ 'model': 'VP3', 'name': 'VOCOlinc-VP3-123456', 'name_by_user': None, - 'serial_number': None, + 'serial_number': 'EU0121203xxxxx07', 'suggested_area': None, 'sw_version': '1.101.2', }), From 9204ccfa172a2e3a6ea2c6d032ed69b8faa9eafb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 5 Apr 2024 17:37:00 +0200 Subject: [PATCH 299/967] Use is in ConfigEntryState enum comparison in tests (N-Z) (#114926) --- tests/components/neato/test_config_flow.py | 3 ++- tests/components/nest/test_init.py | 2 +- tests/components/nest/test_media_source.py | 6 ++--- tests/components/netatmo/test_config_flow.py | 5 ++-- tests/components/netatmo/test_init.py | 8 +++---- tests/components/netgear_lte/test_init.py | 4 ++-- tests/components/nexia/test_init.py | 2 +- tests/components/nibe_heatpump/__init__.py | 2 +- tests/components/nina/test_binary_sensor.py | 6 ++--- tests/components/nina/test_init.py | 4 ++-- tests/components/octoprint/__init__.py | 2 +- tests/components/oncue/test_binary_sensor.py | 4 ++-- tests/components/oncue/test_init.py | 8 +++---- tests/components/oncue/test_sensor.py | 4 ++-- .../openweathermap/test_config_flow.py | 10 ++++---- tests/components/ourgroceries/test_init.py | 4 ++-- tests/components/peco/test_init.py | 22 ++++++++--------- tests/components/peco/test_sensor.py | 4 ++-- .../components/powerwall/test_config_flow.py | 3 ++- tests/components/powerwall/test_init.py | 2 +- .../components/private_ble_device/__init__.py | 4 ++-- .../components/proximity/test_diagnostics.py | 2 +- tests/components/prusalink/test_init.py | 12 +++++----- tests/components/pushbullet/test_init.py | 10 ++++---- tests/components/pushover/test_init.py | 10 ++++---- tests/components/radarr/test_init.py | 6 ++--- .../components/rainbird/test_binary_sensor.py | 4 ++-- tests/components/rainbird/test_calendar.py | 6 ++--- tests/components/rainbird/test_config_flow.py | 6 ++--- tests/components/rainbird/test_init.py | 16 ++++++------- tests/components/rainbird/test_number.py | 4 ++-- tests/components/rainbird/test_sensor.py | 4 ++-- tests/components/rainbird/test_switch.py | 4 ++-- tests/components/raspberry_pi/test_init.py | 2 +- tests/components/reolink/test_config_flow.py | 2 +- tests/components/reolink/test_init.py | 4 ++-- tests/components/rfxtrx/test_switch.py | 4 ++-- tests/components/rova/test_init.py | 8 +++---- .../components/samsungtv/test_config_flow.py | 9 +++---- tests/components/samsungtv/test_init.py | 4 ++-- tests/components/scrape/test_init.py | 8 +++---- tests/components/script/test_blueprint.py | 4 ++-- tests/components/script/test_init.py | 4 ++-- tests/components/sensibo/test_init.py | 13 +++++----- tests/components/shelly/test_climate.py | 8 +++---- tests/components/shelly/test_coordinator.py | 18 +++++++------- tests/components/shelly/test_init.py | 6 ++--- tests/components/shelly/test_number.py | 4 ++-- tests/components/shelly/test_switch.py | 8 +++---- tests/components/shelly/test_update.py | 8 +++---- tests/components/slack/test_init.py | 6 ++--- tests/components/speedtestdotnet/test_init.py | 2 +- tests/components/sql/test_init.py | 8 +++---- .../components/srp_energy/test_config_flow.py | 4 ++-- tests/components/srp_energy/test_init.py | 4 ++-- tests/components/srp_energy/test_sensor.py | 2 +- tests/components/starlink/test_init.py | 2 +- tests/components/steam_online/test_init.py | 4 ++-- tests/components/steamist/__init__.py | 2 +- tests/components/steamist/test_init.py | 6 ++--- tests/components/stt/test_init.py | 6 ++--- tests/components/switch_as_x/test_init.py | 4 ++-- tests/components/switchbot_cloud/test_init.py | 4 ++-- tests/components/system_bridge/test_init.py | 4 ++-- tests/components/systemmonitor/test_init.py | 2 +- tests/components/tami4/test_init.py | 4 ++-- .../tankerkoenig/test_coordinator.py | 2 +- .../tesla_wall_connector/test_init.py | 8 +++---- tests/components/todo/test_init.py | 4 ++-- tests/components/todoist/test_init.py | 6 ++--- tests/components/tplink/test_config_flow.py | 21 ++++++++-------- tests/components/tplink/test_init.py | 8 +++---- .../traccar_server/test_config_flow.py | 7 +++--- .../trafikverket_camera/test_coordinator.py | 13 +++++----- .../trafikverket_camera/test_init.py | 15 ++++++------ .../trafikverket_ferry/test_init.py | 9 ++++--- .../trafikverket_train/test_init.py | 13 +++++----- tests/components/transmission/test_init.py | 8 +++---- tests/components/tts/test_init.py | 6 ++--- tests/components/twinkly/test_init.py | 4 ++-- .../unifiprotect/test_config_flow.py | 3 ++- tests/components/unifiprotect/test_init.py | 4 ++-- tests/components/uptimerobot/common.py | 3 ++- tests/components/uptimerobot/test_init.py | 9 +++---- tests/components/valve/test_init.py | 4 ++-- tests/components/velbus/test_init.py | 4 ++-- tests/components/venstar/test_init.py | 6 ++--- tests/components/version/common.py | 3 ++- tests/components/version/test_config_flow.py | 5 ++-- tests/components/voip/conftest.py | 2 +- tests/components/wake_word/test_init.py | 6 ++--- tests/components/wallbox/test_config_flow.py | 5 ++-- tests/components/wallbox/test_init.py | 24 +++++++++---------- tests/components/waqi/test_sensor.py | 2 +- tests/components/weatherkit/test_setup.py | 4 ++-- tests/components/webostv/test_init.py | 4 ++-- tests/components/webostv/test_media_player.py | 4 ++-- .../components/websocket_api/test_commands.py | 5 ++-- tests/components/wiz/test_binary_sensor.py | 6 ++--- tests/components/wiz/test_config_flow.py | 5 ++-- tests/components/wiz/test_init.py | 14 +++++------ tests/components/workday/test_init.py | 6 ++--- tests/components/yolink/test_config_flow.py | 3 ++- tests/components/zha/test_config_flow.py | 7 +++--- tests/components/zha/test_radio_manager.py | 8 +++---- tests/components/zha/test_repairs.py | 10 ++++---- tests/components/zwave_js/test_init.py | 2 +- 107 files changed, 332 insertions(+), 321 deletions(-) diff --git a/tests/components/neato/test_config_flow.py b/tests/components/neato/test_config_flow.py index 506a29b559f..132b23ef157 100644 --- a/tests/components/neato/test_config_flow.py +++ b/tests/components/neato/test_config_flow.py @@ -10,6 +10,7 @@ from homeassistant.components.application_credentials import ( async_import_client_credential, ) from homeassistant.components.neato.const import NEATO_DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow @@ -158,6 +159,6 @@ async def test_reauth( assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" - assert new_entry.state == config_entries.ConfigEntryState.LOADED + assert new_entry.state is ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(NEATO_DOMAIN)) == 1 assert len(mock_setup.mock_calls) == 1 diff --git a/tests/components/nest/test_init.py b/tests/components/nest/test_init.py index 3cac8649c9c..e77ba3bb7e1 100644 --- a/tests/components/nest/test_init.py +++ b/tests/components/nest/test_init.py @@ -206,7 +206,7 @@ async def test_unload_entry(hass: HomeAssistant, setup_platform) -> None: assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED async def test_remove_entry( diff --git a/tests/components/nest/test_media_source.py b/tests/components/nest/test_media_source.py index 4810c8e2ff5..3d0ec521df2 100644 --- a/tests/components/nest/test_media_source.py +++ b/tests/components/nest/test_media_source.py @@ -296,7 +296,7 @@ async def test_integration_unloaded(hass: HomeAssistant, auth, setup_platform) - assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED # No devices returned browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}") @@ -1166,10 +1166,10 @@ async def test_media_store_persistence( await hass.async_block_till_done() # Unload the integration. - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED # Now rebuild the entire integration and verify that all persisted storage # can be re-loaded from disk. diff --git a/tests/components/netatmo/test_config_flow.py b/tests/components/netatmo/test_config_flow.py index c828fae7ba2..933f782c9d9 100644 --- a/tests/components/netatmo/test_config_flow.py +++ b/tests/components/netatmo/test_config_flow.py @@ -15,6 +15,7 @@ from homeassistant.components.netatmo.const import ( OAUTH2_AUTHORIZE, OAUTH2_TOKEN, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow @@ -275,7 +276,7 @@ async def test_reauth( new_entry = hass.config_entries.async_entries(DOMAIN)[0] - assert new_entry.state == config_entries.ConfigEntryState.LOADED + assert new_entry.state is ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(mock_setup.mock_calls) == 1 @@ -322,6 +323,6 @@ async def test_reauth( assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" - assert new_entry2.state == config_entries.ConfigEntryState.LOADED + assert new_entry2.state is ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(mock_setup.mock_calls) == 1 diff --git a/tests/components/netatmo/test_init.py b/tests/components/netatmo/test_init.py index e4869b73e2e..c68bd7df541 100644 --- a/tests/components/netatmo/test_init.py +++ b/tests/components/netatmo/test_init.py @@ -9,9 +9,9 @@ from pyatmo.const import ALL_SCOPES import pytest from syrupy import SnapshotAssertion -from homeassistant import config_entries from homeassistant.components import cloud from homeassistant.components.netatmo import DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_WEBHOOK_ID, Platform from homeassistant.core import CoreState, HomeAssistant import homeassistant.helpers.device_registry as dr @@ -82,7 +82,7 @@ async def test_setup_component( mock_impl.assert_called_once() mock_webhook.assert_called_once() - assert config_entry.state is config_entries.ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert hass.config_entries.async_entries(DOMAIN) assert len(hass.states.async_all()) > 0 @@ -425,7 +425,7 @@ async def test_setup_component_invalid_token_scope(hass: HomeAssistant) -> None: mock_impl.assert_called_once() mock_webhook.assert_not_called() - assert config_entry.state is config_entries.ConfigEntryState.SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR assert hass.config_entries.async_entries(DOMAIN) notifications = async_get_persistent_notifications(hass) @@ -479,7 +479,7 @@ async def test_setup_component_invalid_token( mock_impl.assert_called_once() mock_webhook.assert_not_called() - assert config_entry.state is config_entries.ConfigEntryState.SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR assert hass.config_entries.async_entries(DOMAIN) notifications = async_get_persistent_notifications(hass) assert len(notifications) > 0 diff --git a/tests/components/netgear_lte/test_init.py b/tests/components/netgear_lte/test_init.py index 6a71e6d601c..ef3109123fa 100644 --- a/tests/components/netgear_lte/test_init.py +++ b/tests/components/netgear_lte/test_init.py @@ -13,7 +13,7 @@ from .conftest import CONF_DATA async def test_setup_unload(hass: HomeAssistant, setup_integration: None) -> None: """Test setup and unload.""" entry = hass.config_entries.async_entries(DOMAIN)[0] - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert entry.data == CONF_DATA @@ -29,7 +29,7 @@ async def test_async_setup_entry_not_ready( """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 == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_device( diff --git a/tests/components/nexia/test_init.py b/tests/components/nexia/test_init.py index 8eeb8a9f729..ec84748830a 100644 --- a/tests/components/nexia/test_init.py +++ b/tests/components/nexia/test_init.py @@ -17,7 +17,7 @@ from tests.typing import WebSocketGenerator async def test_setup_retry_client_os_error(hass: HomeAssistant) -> None: """Verify we retry setup on aiohttp.ClientOSError.""" config_entry = await async_init_integration(hass, exception=aiohttp.ClientOSError) - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY async def remove_device(ws_client, device_id, config_entry_id): diff --git a/tests/components/nibe_heatpump/__init__.py b/tests/components/nibe_heatpump/__init__.py index d7c1fa5ebad..1dbef2fe4f2 100644 --- a/tests/components/nibe_heatpump/__init__.py +++ b/tests/components/nibe_heatpump/__init__.py @@ -64,7 +64,7 @@ async def async_add_entry(hass: HomeAssistant, data: dict[str, Any]) -> MockConf entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED return entry diff --git a/tests/components/nina/test_binary_sensor.py b/tests/components/nina/test_binary_sensor.py index 380d16f5101..7f4f000cf3a 100644 --- a/tests/components/nina/test_binary_sensor.py +++ b/tests/components/nina/test_binary_sensor.py @@ -65,7 +65,7 @@ async def test_sensors(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(conf_entry.entry_id) await hass.async_block_till_done() - assert conf_entry.state == ConfigEntryState.LOADED + assert conf_entry.state is ConfigEntryState.LOADED state_w1 = hass.states.get("binary_sensor.warning_aach_stadt_1") entry_w1 = entity_registry.async_get("binary_sensor.warning_aach_stadt_1") @@ -181,7 +181,7 @@ async def test_sensors_without_corona_filter(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(conf_entry.entry_id) await hass.async_block_till_done() - assert conf_entry.state == ConfigEntryState.LOADED + assert conf_entry.state is ConfigEntryState.LOADED state_w1 = hass.states.get("binary_sensor.warning_aach_stadt_1") entry_w1 = entity_registry.async_get("binary_sensor.warning_aach_stadt_1") @@ -309,7 +309,7 @@ async def test_sensors_with_area_filter(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(conf_entry.entry_id) await hass.async_block_till_done() - assert conf_entry.state == ConfigEntryState.LOADED + assert conf_entry.state is ConfigEntryState.LOADED state_w1 = hass.states.get("binary_sensor.warning_aach_stadt_1") entry_w1 = entity_registry.async_get("binary_sensor.warning_aach_stadt_1") diff --git a/tests/components/nina/test_init.py b/tests/components/nina/test_init.py index d7c312a8514..5a6b9ab07dd 100644 --- a/tests/components/nina/test_init.py +++ b/tests/components/nina/test_init.py @@ -64,7 +64,7 @@ async def test_config_entry_not_ready(hass: HomeAssistant) -> None: """Test the configuration entry.""" entry: MockConfigEntry = await init_integration(hass) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED async def test_sensors_connection_error(hass: HomeAssistant) -> None: @@ -82,4 +82,4 @@ async def test_sensors_connection_error(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(conf_entry.entry_id) await hass.async_block_till_done() - assert conf_entry.state == ConfigEntryState.SETUP_RETRY + assert conf_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/octoprint/__init__.py b/tests/components/octoprint/__init__.py index 4a896736329..0a35d0a2267 100644 --- a/tests/components/octoprint/__init__.py +++ b/tests/components/octoprint/__init__.py @@ -85,4 +85,4 @@ async def init_integration( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED diff --git a/tests/components/oncue/test_binary_sensor.py b/tests/components/oncue/test_binary_sensor.py index 12ecb19ebc4..d9fce699d39 100644 --- a/tests/components/oncue/test_binary_sensor.py +++ b/tests/components/oncue/test_binary_sensor.py @@ -25,7 +25,7 @@ async def test_binary_sensors(hass: HomeAssistant) -> None: with _patch_login_and_data(): await async_setup_component(hass, oncue.DOMAIN, {oncue.DOMAIN: {}}) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert len(hass.states.async_all("binary_sensor")) == 1 assert ( @@ -47,7 +47,7 @@ async def test_binary_sensors_not_unavailable(hass: HomeAssistant) -> None: with _patch_login_and_data_unavailable(): await async_setup_component(hass, oncue.DOMAIN, {oncue.DOMAIN: {}}) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert len(hass.states.async_all("binary_sensor")) == 1 assert ( diff --git a/tests/components/oncue/test_init.py b/tests/components/oncue/test_init.py index f10d94d719b..2da3e04e4c3 100644 --- a/tests/components/oncue/test_init.py +++ b/tests/components/oncue/test_init.py @@ -29,10 +29,10 @@ async def test_config_entry_reload(hass: HomeAssistant) -> None: with _patch_login_and_data(): await async_setup_component(hass, oncue.DOMAIN, {oncue.DOMAIN: {}}) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + 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 == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED async def test_config_entry_login_error(hass: HomeAssistant) -> None: @@ -49,7 +49,7 @@ async def test_config_entry_login_error(hass: HomeAssistant) -> None: ): await async_setup_component(hass, oncue.DOMAIN, {oncue.DOMAIN: {}}) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR async def test_config_entry_retry_later(hass: HomeAssistant) -> None: @@ -66,4 +66,4 @@ async def test_config_entry_retry_later(hass: HomeAssistant) -> None: ): await async_setup_component(hass, oncue.DOMAIN, {oncue.DOMAIN: {}}) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/oncue/test_sensor.py b/tests/components/oncue/test_sensor.py index 13f5a8b944d..c124bab3c48 100644 --- a/tests/components/oncue/test_sensor.py +++ b/tests/components/oncue/test_sensor.py @@ -40,7 +40,7 @@ async def test_sensors(hass: HomeAssistant, patcher, connections) -> None: with patcher(): await async_setup_component(hass, oncue.DOMAIN, {oncue.DOMAIN: {}}) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED entity_registry = er.async_get(hass) ent = entity_registry.async_get("sensor.my_generator_latest_firmware") @@ -167,7 +167,7 @@ async def test_sensors_unavailable(hass: HomeAssistant, patcher, connections) -> with patcher(): await async_setup_component(hass, oncue.DOMAIN, {oncue.DOMAIN: {}}) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert len(hass.states.async_all("sensor")) == 25 assert ( diff --git a/tests/components/openweathermap/test_config_flow.py b/tests/components/openweathermap/test_config_flow.py index d6c043b62a8..2715d83f4f0 100644 --- a/tests/components/openweathermap/test_config_flow.py +++ b/tests/components/openweathermap/test_config_flow.py @@ -59,11 +59,11 @@ async def test_form(hass: HomeAssistant) -> None: conf_entries = hass.config_entries.async_entries(DOMAIN) entry = conf_entries[0] - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(conf_entries[0].entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == CONFIG[CONF_NAME] @@ -88,7 +88,7 @@ async def test_form_options(hass: HomeAssistant) -> None: assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED result = await hass.config_entries.options.async_init(config_entry.entry_id) @@ -107,7 +107,7 @@ async def test_form_options(hass: HomeAssistant) -> None: await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED result = await hass.config_entries.options.async_init(config_entry.entry_id) @@ -126,7 +126,7 @@ async def test_form_options(hass: HomeAssistant) -> None: await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED async def test_form_invalid_api_key(hass: HomeAssistant) -> None: diff --git a/tests/components/ourgroceries/test_init.py b/tests/components/ourgroceries/test_init.py index ae8452652ae..99b9204ea2b 100644 --- a/tests/components/ourgroceries/test_init.py +++ b/tests/components/ourgroceries/test_init.py @@ -21,10 +21,10 @@ async def test_load_unload( entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 - assert ourgroceries_config_entry.state == ConfigEntryState.LOADED + assert ourgroceries_config_entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(ourgroceries_config_entry.entry_id) - assert ourgroceries_config_entry.state == ConfigEntryState.NOT_LOADED + assert ourgroceries_config_entry.state is ConfigEntryState.NOT_LOADED @pytest.fixture diff --git a/tests/components/peco/test_init.py b/tests/components/peco/test_init.py index 55dc0a15a4b..22d2233093a 100644 --- a/tests/components/peco/test_init.py +++ b/tests/components/peco/test_init.py @@ -51,11 +51,11 @@ async def test_unload_entry(hass: HomeAssistant) -> None: entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 - assert entries[0].state == ConfigEntryState.LOADED + assert entries[0].state is ConfigEntryState.LOADED await hass.config_entries.async_unload(entries[0].entry_id) await hass.async_block_till_done() - assert entries[0].state == ConfigEntryState.NOT_LOADED + assert entries[0].state is ConfigEntryState.NOT_LOADED @pytest.mark.parametrize( @@ -81,7 +81,7 @@ async def test_update_timeout(hass: HomeAssistant, sensor) -> None: await hass.async_block_till_done() assert hass.states.get(f"sensor.{sensor}") is None - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY @pytest.mark.parametrize( @@ -106,7 +106,7 @@ async def test_total_update_timeout(hass: HomeAssistant, sensor) -> None: await hass.async_block_till_done() assert hass.states.get(f"sensor.{sensor}") is None - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY @pytest.mark.parametrize( @@ -132,7 +132,7 @@ async def test_http_error(hass: HomeAssistant, sensor: str) -> None: await hass.async_block_till_done() assert hass.states.get(f"sensor.{sensor}") is None - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY @pytest.mark.parametrize( @@ -158,7 +158,7 @@ async def test_bad_json(hass: HomeAssistant, sensor: str) -> None: await hass.async_block_till_done() assert hass.states.get(f"sensor.{sensor}") is None - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY async def test_unresponsive_meter_error(hass: HomeAssistant) -> None: @@ -192,7 +192,7 @@ async def test_unresponsive_meter_error(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert hass.states.get("binary_sensor.meter_status") is None - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY async def test_meter_http_error(hass: HomeAssistant) -> None: @@ -226,7 +226,7 @@ async def test_meter_http_error(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert hass.states.get("binary_sensor.meter_status") is None - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY async def test_meter_bad_json(hass: HomeAssistant) -> None: @@ -260,7 +260,7 @@ async def test_meter_bad_json(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert hass.states.get("binary_sensor.meter_status") is None - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY async def test_meter_timeout(hass: HomeAssistant) -> None: @@ -294,7 +294,7 @@ async def test_meter_timeout(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert hass.states.get("binary_sensor.meter_status") is None - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY async def test_meter_data(hass: HomeAssistant) -> None: @@ -329,4 +329,4 @@ async def test_meter_data(hass: HomeAssistant) -> None: assert hass.states.get("binary_sensor.meter_status") is not None assert hass.states.get("binary_sensor.meter_status").state == "on" - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED diff --git a/tests/components/peco/test_sensor.py b/tests/components/peco/test_sensor.py index 2546b7c8996..9cbef9fa1e6 100644 --- a/tests/components/peco/test_sensor.py +++ b/tests/components/peco/test_sensor.py @@ -57,7 +57,7 @@ async def test_sensor_available( entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED sensor_entity = hass.states.get(f"sensor.total_{sensor}") assert sensor_entity is not None @@ -91,7 +91,7 @@ async def test_sensor_available( entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 2 - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED sensor_entity = hass.states.get(f"sensor.bucks_{sensor}") assert sensor_entity is not None diff --git a/tests/components/powerwall/test_config_flow.py b/tests/components/powerwall/test_config_flow.py index 3e13fc7242d..db0ef2e9884 100644 --- a/tests/components/powerwall/test_config_flow.py +++ b/tests/components/powerwall/test_config_flow.py @@ -13,6 +13,7 @@ from tesla_powerwall import ( from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.components.powerwall.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -524,7 +525,7 @@ async def test_dhcp_discovery_updates_unique_id_when_entry_is_failed( unique_id="1.2.3.4", ) entry.add_to_hass(hass) - entry.mock_state(hass, config_entries.ConfigEntryState.SETUP_ERROR) + entry.mock_state(hass, ConfigEntryState.SETUP_ERROR) mock_powerwall = await _mock_powerwall_site_name(hass, "Some site") with ( diff --git a/tests/components/powerwall/test_init.py b/tests/components/powerwall/test_init.py index de8da12ccb5..e271cde0fc4 100644 --- a/tests/components/powerwall/test_init.py +++ b/tests/components/powerwall/test_init.py @@ -66,7 +66,7 @@ async def test_update_data_reauthenticate_on_access_denied(hass: HomeAssistant) async_fire_time_changed(hass, utcnow() + datetime.timedelta(minutes=1)) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED flows = hass.config_entries.flow.async_progress(DOMAIN) assert len(flows) == 1 diff --git a/tests/components/private_ble_device/__init__.py b/tests/components/private_ble_device/__init__.py index 967f422872b..b85f29fc394 100644 --- a/tests/components/private_ble_device/__init__.py +++ b/tests/components/private_ble_device/__init__.py @@ -5,8 +5,8 @@ import time from home_assistant_bluetooth import BluetoothServiceInfoBleak -from homeassistant import config_entries from homeassistant.components.private_ble_device.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util @@ -38,7 +38,7 @@ async def async_mock_config_entry(hass: HomeAssistant, irk: str = DUMMY_IRK) -> entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) - assert entry.state is config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED await hass.async_block_till_done() diff --git a/tests/components/proximity/test_diagnostics.py b/tests/components/proximity/test_diagnostics.py index 26d1d293efd..a60c592fcab 100644 --- a/tests/components/proximity/test_diagnostics.py +++ b/tests/components/proximity/test_diagnostics.py @@ -67,7 +67,7 @@ async def test_entry_diagnostics( mock_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() - assert mock_entry.state == ConfigEntryState.LOADED + assert mock_entry.state is ConfigEntryState.LOADED assert await get_diagnostics_for_config_entry( hass, hass_client, mock_entry diff --git a/tests/components/prusalink/test_init.py b/tests/components/prusalink/test_init.py index 1160143ea11..2cdc6894eeb 100644 --- a/tests/components/prusalink/test_init.py +++ b/tests/components/prusalink/test_init.py @@ -25,12 +25,12 @@ async def test_unloading( ) -> None: """Test unloading prusalink.""" assert await hass.config_entries.async_setup(mock_config_entry.entry_id) - assert mock_config_entry.state == ConfigEntryState.LOADED + assert mock_config_entry.state is ConfigEntryState.LOADED assert hass.states.async_entity_ids_count() > 0 assert await hass.config_entries.async_unload(mock_config_entry.entry_id) - assert mock_config_entry.state == ConfigEntryState.NOT_LOADED + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED for state in hass.states.async_all(): assert state.state == "unavailable" @@ -42,7 +42,7 @@ async def test_failed_update( ) -> None: """Test failed update marks prusalink unavailable.""" assert await hass.config_entries.async_setup(mock_config_entry.entry_id) - assert mock_config_entry.state == ConfigEntryState.LOADED + assert mock_config_entry.state is ConfigEntryState.LOADED with ( patch( @@ -121,7 +121,7 @@ async def test_migration_from_1_1_to_1_2_outdated_firmware( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR assert entry.minor_version == 1 assert (DOMAIN, "firmware_5_1_required") in issue_registry.issues @@ -130,7 +130,7 @@ async def test_migration_from_1_1_to_1_2_outdated_firmware( await hass.async_block_till_done() # Integration should be running now, the issue should be gone - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert entry.minor_version == 2 assert (DOMAIN, "firmware_5_1_required") not in issue_registry.issues @@ -149,4 +149,4 @@ async def test_migration_fails_on_future_version( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.MIGRATION_ERROR + assert entry.state is ConfigEntryState.MIGRATION_ERROR diff --git a/tests/components/pushbullet/test_init.py b/tests/components/pushbullet/test_init.py index 72672f36176..77df6ffa10b 100644 --- a/tests/components/pushbullet/test_init.py +++ b/tests/components/pushbullet/test_init.py @@ -26,7 +26,7 @@ async def test_async_setup_entry_success( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED with patch( "homeassistant.components.pushbullet.api.PushBulletNotificationProvider.start" @@ -49,7 +49,7 @@ async def test_setup_entry_failed_invalid_key(hass: HomeAssistant) -> None: ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR async def test_setup_entry_failed_conn_error(hass: HomeAssistant) -> None: @@ -65,7 +65,7 @@ async def test_setup_entry_failed_conn_error(hass: HomeAssistant) -> None: ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_async_unload_entry(hass: HomeAssistant, requests_mock_fixture) -> None: @@ -78,8 +78,8 @@ async def test_async_unload_entry(hass: HomeAssistant, requests_mock_fixture) -> await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/pushover/test_init.py b/tests/components/pushover/test_init.py index 15e537fd41f..c3a653042ce 100644 --- a/tests/components/pushover/test_init.py +++ b/tests/components/pushover/test_init.py @@ -35,7 +35,7 @@ async def test_async_setup_entry_success( entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED async def test_unique_id_updated(hass: HomeAssistant, mock_pushover: MagicMock) -> None: @@ -44,7 +44,7 @@ async def test_unique_id_updated(hass: HomeAssistant, mock_pushover: MagicMock) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert entry.unique_id is None @@ -60,7 +60,7 @@ async def test_async_setup_entry_failed_invalid_api_key( mock_pushover.side_effect = BadAPIRequestError("400: application token is invalid") await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR async def test_async_setup_entry_failed_conn_error( @@ -75,7 +75,7 @@ async def test_async_setup_entry_failed_conn_error( mock_pushover.side_effect = BadAPIRequestError await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_async_setup_entry_failed_json_error( @@ -92,4 +92,4 @@ async def test_async_setup_entry_failed_json_error( ) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/radarr/test_init.py b/tests/components/radarr/test_init.py index c4226d3f3fb..10ff196bf17 100644 --- a/tests/components/radarr/test_init.py +++ b/tests/components/radarr/test_init.py @@ -16,7 +16,7 @@ from tests.test_util.aiohttp import AiohttpClientMocker async def test_setup(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -> None: """Test unload.""" entry = await setup_integration(hass, aioclient_mock) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() @@ -31,7 +31,7 @@ async def test_async_setup_entry_not_ready( """Test that it throws ConfigEntryNotReady when exception occurs during setup.""" entry = await setup_integration(hass, aioclient_mock, connection_error=True) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY assert not hass.data.get(DOMAIN) @@ -43,7 +43,7 @@ async def test_async_setup_entry_auth_failed( mock_connection_invalid_auth(aioclient_mock) await hass.config_entries.async_setup(entry.entry_id) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR assert not hass.data.get(DOMAIN) diff --git a/tests/components/rainbird/test_binary_sensor.py b/tests/components/rainbird/test_binary_sensor.py index 83a45de93ff..51c1e5dcf9f 100644 --- a/tests/components/rainbird/test_binary_sensor.py +++ b/tests/components/rainbird/test_binary_sensor.py @@ -32,7 +32,7 @@ async def setup_config_entry( ) -> list[Platform]: """Fixture to setup the config entry.""" await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED @pytest.mark.parametrize( @@ -73,7 +73,7 @@ async def test_no_unique_id( responses.insert(0, mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE)) await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED rainsensor = hass.states.get("binary_sensor.rain_bird_controller_rainsensor") assert rainsensor is not None diff --git a/tests/components/rainbird/test_calendar.py b/tests/components/rainbird/test_calendar.py index 9f6dfd9213d..1af6ca7ba7f 100644 --- a/tests/components/rainbird/test_calendar.py +++ b/tests/components/rainbird/test_calendar.py @@ -87,7 +87,7 @@ async def setup_config_entry( ) -> list[Platform]: """Fixture to setup the config entry.""" await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED @pytest.fixture(autouse=True) @@ -191,7 +191,7 @@ async def test_event_state( freezer.move_to(freeze_time) await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED state = hass.states.get(TEST_ENTITY) assert state is not None @@ -295,7 +295,7 @@ async def test_no_unique_id( responses.insert(0, mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE)) await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED state = hass.states.get(TEST_ENTITY) assert state is not None diff --git a/tests/components/rainbird/test_config_flow.py b/tests/components/rainbird/test_config_flow.py index 0e4b04690de..b4cd51d6b3e 100644 --- a/tests/components/rainbird/test_config_flow.py +++ b/tests/components/rainbird/test_config_flow.py @@ -159,7 +159,7 @@ async def test_multiple_config_entries( ) -> None: """Test setting up multiple config entries that refer to different devices.""" await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED responses.clear() responses.extend(config_flow_responses) @@ -234,7 +234,7 @@ async def test_duplicate_config_entries( ) -> None: """Test that a device can not be registered twice.""" await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED responses.clear() responses.extend(config_flow_responses) @@ -299,7 +299,7 @@ async def test_options_flow(hass: HomeAssistant, mock_setup: Mock) -> None: # Assert single config entry is loaded config_entry = next(iter(hass.config_entries.async_entries(DOMAIN))) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED # Initiate the options flow result = await hass.config_entries.options.async_init(config_entry.entry_id) diff --git a/tests/components/rainbird/test_init.py b/tests/components/rainbird/test_init.py index 00cbefc6556..5b2e2ea6d1b 100644 --- a/tests/components/rainbird/test_init.py +++ b/tests/components/rainbird/test_init.py @@ -37,7 +37,7 @@ async def test_init_success( """Test successful setup and unload.""" await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() @@ -112,17 +112,17 @@ async def test_fix_unique_id( entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 - assert entries[0].state == ConfigEntryState.NOT_LOADED + assert entries[0].state is ConfigEntryState.NOT_LOADED assert entries[0].unique_id is None assert entries[0].data.get(CONF_MAC) is None await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED # Verify config entry now has a unique id entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 - assert entries[0].state == ConfigEntryState.LOADED + assert entries[0].state is ConfigEntryState.LOADED assert entries[0].unique_id == MAC_ADDRESS_UNIQUE_ID assert entries[0].data.get(CONF_MAC) == MAC_ADDRESS @@ -170,7 +170,7 @@ async def test_fix_unique_id_failure( await hass.config_entries.async_setup(config_entry.entry_id) # Config entry is loaded, but not updated - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert config_entry.unique_id is None assert expected_warning in caplog.text @@ -204,7 +204,7 @@ async def test_fix_unique_id_duplicate( responses.extend(responses_copy) await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert config_entry.unique_id == MAC_ADDRESS_UNIQUE_ID assert "Unable to fix missing unique id (already exists)" in caplog.text @@ -305,7 +305,7 @@ async def test_fix_entity_unique_ids( ) await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED entity_entry = entity_registry.async_get(entity_entry.id) assert entity_entry @@ -421,7 +421,7 @@ async def test_fix_duplicate_device_ids( assert len(device_entries) == 2 await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED # Only the device with the new format exists device_entries = dr.async_entries_for_config_entry( diff --git a/tests/components/rainbird/test_number.py b/tests/components/rainbird/test_number.py index 0830a238fd7..b3a1860baab 100644 --- a/tests/components/rainbird/test_number.py +++ b/tests/components/rainbird/test_number.py @@ -38,7 +38,7 @@ async def setup_config_entry( ) -> list[Platform]: """Fixture to setup the config entry.""" await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED @pytest.mark.parametrize( @@ -155,7 +155,7 @@ async def test_no_unique_id( responses.insert(0, mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE)) await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED raindelay = hass.states.get("number.rain_bird_controller_rain_delay") assert raindelay is not None diff --git a/tests/components/rainbird/test_sensor.py b/tests/components/rainbird/test_sensor.py index 730e1d50809..34b93f7b411 100644 --- a/tests/components/rainbird/test_sensor.py +++ b/tests/components/rainbird/test_sensor.py @@ -32,7 +32,7 @@ async def setup_config_entry( ) -> list[Platform]: """Fixture to setup the config entry.""" await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED @pytest.mark.parametrize( @@ -85,7 +85,7 @@ async def test_sensor_no_unique_id( responses.insert(0, mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE)) await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED raindelay = hass.states.get("sensor.rain_bird_controller_raindelay") assert raindelay is not None diff --git a/tests/components/rainbird/test_switch.py b/tests/components/rainbird/test_switch.py index 0f9a139a69d..f87b7f121b5 100644 --- a/tests/components/rainbird/test_switch.py +++ b/tests/components/rainbird/test_switch.py @@ -44,7 +44,7 @@ async def setup_config_entry( ) -> list[Platform]: """Fixture to setup the config entry.""" await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED @pytest.mark.parametrize( @@ -294,7 +294,7 @@ async def test_no_unique_id( responses.insert(0, mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE)) await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED zone = hass.states.get("switch.rain_bird_sprinkler_3") assert zone is not None diff --git a/tests/components/raspberry_pi/test_init.py b/tests/components/raspberry_pi/test_init.py index 80b5eedf2af..0c4134bf2be 100644 --- a/tests/components/raspberry_pi/test_init.py +++ b/tests/components/raspberry_pi/test_init.py @@ -118,4 +118,4 @@ async def test_setup_entry_wait_hassio(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(mock_get_os_info.mock_calls) == 1 - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/reolink/test_config_flow.py b/tests/components/reolink/test_config_flow.py index bcbf4fe45a7..92a65fd638b 100644 --- a/tests/components/reolink/test_config_flow.py +++ b/tests/components/reolink/test_config_flow.py @@ -452,7 +452,7 @@ async def test_dhcp_ip_update( assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED if not last_update_success: # ensure the last_update_succes is False for the device_coordinator. diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index 8ebce5d350e..4ec02244c91 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -68,7 +68,7 @@ async def test_failures_parametrized( """Test outcomes when changing errors.""" setattr(reolink_connect, attr, value) assert await hass.config_entries.async_setup(config_entry.entry_id) is ( - expected == ConfigEntryState.LOADED + expected is ConfigEntryState.LOADED ) await hass.async_block_till_done() @@ -88,7 +88,7 @@ async def test_firmware_error_twice( assert await hass.config_entries.async_setup(config_entry.entry_id) is True await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED entity_id = f"{Platform.UPDATE}.{TEST_NVR_NAME}_firmware" assert hass.states.is_state(entity_id, STATE_OFF) diff --git a/tests/components/rfxtrx/test_switch.py b/tests/components/rfxtrx/test_switch.py index 63aacdd5eab..7acc008cc8a 100644 --- a/tests/components/rfxtrx/test_switch.py +++ b/tests/components/rfxtrx/test_switch.py @@ -4,8 +4,8 @@ from unittest.mock import call import pytest -from homeassistant import config_entries from homeassistant.components.rfxtrx import DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNKNOWN from homeassistant.core import HomeAssistant, State @@ -286,4 +286,4 @@ async def test_unknown_event_code(hass: HomeAssistant, rfxtrx) -> None: assert len(conf_entries) == 1 entry = conf_entries[0] - assert entry.state == config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED diff --git a/tests/components/rova/test_init.py b/tests/components/rova/test_init.py index 3dff0cf4c27..e522d5bfb12 100644 --- a/tests/components/rova/test_init.py +++ b/tests/components/rova/test_init.py @@ -24,12 +24,12 @@ async def test_reload( """Test reloading the integration.""" await setup_with_selected_platforms(hass, mock_config_entry, [Platform.SENSOR]) - assert mock_config_entry.state == ConfigEntryState.LOADED + 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 == ConfigEntryState.NOT_LOADED + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED async def test_service( @@ -68,7 +68,7 @@ async def test_retry_after_failure( assert not await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - assert mock_config_entry.state == ConfigEntryState.SETUP_RETRY + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY async def test_issue_if_not_rova_area( @@ -83,5 +83,5 @@ async def test_issue_if_not_rova_area( assert not await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - assert mock_config_entry.state == ConfigEntryState.SETUP_ERROR + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR assert len(issue_registry.issues) == 1 diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 1ca8fc82151..6c325ae3b04 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -43,6 +43,7 @@ from homeassistant.components.ssdp import ( ATTR_UPNP_UDN, SsdpServiceInfo, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( CONF_HOST, CONF_ID, @@ -1770,7 +1771,7 @@ async def test_form_reauth_websocket(hass: HomeAssistant) -> None: """Test reauthenticate websocket.""" entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRYDATA_WS) entry.add_to_hass(hass) - assert entry.state == config_entries.ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED result = await hass.config_entries.flow.async_init( DOMAIN, @@ -1787,7 +1788,7 @@ async def test_form_reauth_websocket(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" - assert entry.state == config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED @pytest.mark.usefixtures("rest_api") @@ -1860,7 +1861,7 @@ async def test_form_reauth_encrypted(hass: HomeAssistant) -> None: entry = MockConfigEntry(domain=DOMAIN, data=encrypted_entry_data) entry.add_to_hass(hass) - assert entry.state == config_entries.ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED result = await hass.config_entries.flow.async_init( DOMAIN, @@ -1908,7 +1909,7 @@ async def test_form_reauth_encrypted(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" - assert entry.state == config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED authenticator_mock.assert_called_once() assert authenticator_mock.call_args[0] == ("fake_host",) diff --git a/tests/components/samsungtv/test_init.py b/tests/components/samsungtv/test_init.py index 5bf8f2cacac..14c85b2c636 100644 --- a/tests/components/samsungtv/test_init.py +++ b/tests/components/samsungtv/test_init.py @@ -95,7 +95,7 @@ async def test_setup_without_port_device_offline(hass: HomeAssistant) -> None: config_entries_domain = hass.config_entries.async_entries(SAMSUNGTV_DOMAIN) assert len(config_entries_domain) == 1 - assert config_entries_domain[0].state == ConfigEntryState.SETUP_RETRY + assert config_entries_domain[0].state is ConfigEntryState.SETUP_RETRY @pytest.mark.usefixtures("remotews", "remoteencws_failing", "rest_api") @@ -166,7 +166,7 @@ async def test_reauth_triggered_encrypted(hass: HomeAssistant) -> None: del encrypted_entry_data[CONF_SESSION_ID] entry = await setup_samsungtv_entry(hass, encrypted_entry_data) - assert entry.state == ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR flows_in_progress = [ flow for flow in hass.config_entries.flow.async_progress() diff --git a/tests/components/scrape/test_init.py b/tests/components/scrape/test_init.py index 8ad766a80bd..db1a89e1ce4 100644 --- a/tests/components/scrape/test_init.py +++ b/tests/components/scrape/test_init.py @@ -6,8 +6,8 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries from homeassistant.components.scrape.const import DEFAULT_SCAN_INTERVAL, 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 homeassistant.setup import async_setup_component @@ -117,16 +117,16 @@ async def test_setup_config_no_sensors( async def test_setup_entry(hass: HomeAssistant, loaded_entry: MockConfigEntry) -> None: """Test setup entry.""" - assert loaded_entry.state == config_entries.ConfigEntryState.LOADED + assert loaded_entry.state is ConfigEntryState.LOADED async def test_unload_entry(hass: HomeAssistant, loaded_entry: MockConfigEntry) -> None: """Test unload an entry.""" - assert loaded_entry.state == config_entries.ConfigEntryState.LOADED + 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 config_entries.ConfigEntryState.NOT_LOADED + assert loaded_entry.state is ConfigEntryState.NOT_LOADED async def remove_device(ws_client, device_id, config_entry_id): diff --git a/tests/components/script/test_blueprint.py b/tests/components/script/test_blueprint.py index bccf1d9aa50..b956aa588cb 100644 --- a/tests/components/script/test_blueprint.py +++ b/tests/components/script/test_blueprint.py @@ -8,9 +8,9 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries from homeassistant.components import script from homeassistant.components.blueprint.models import Blueprint, DomainBlueprints +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import Context, HomeAssistant, callback from homeassistant.helpers import device_registry as dr, template from homeassistant.setup import async_setup_component @@ -48,7 +48,7 @@ async def test_confirmable_notification( ) -> None: """Test confirmable notification blueprint.""" config_entry = MockConfigEntry(domain="fake_integration", data={}) - config_entry.mock_state(hass, config_entries.ConfigEntryState.LOADED) + config_entry.mock_state(hass, ConfigEntryState.LOADED) config_entry.add_to_hass(hass) frodo = device_registry.async_get_or_create( diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py index ba448230c35..790ef7e79bc 100644 --- a/tests/components/script/test_init.py +++ b/tests/components/script/test_init.py @@ -7,9 +7,9 @@ from unittest.mock import Mock, patch import pytest -from homeassistant import config_entries from homeassistant.components import script from homeassistant.components.script import DOMAIN, EVENT_SCRIPT_STARTED, ScriptEntity +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_NAME, @@ -729,7 +729,7 @@ async def test_extraction_functions( ) -> None: """Test extraction functions.""" config_entry = MockConfigEntry(domain="fake_integration", data={}) - config_entry.mock_state(hass, config_entries.ConfigEntryState.LOADED) + config_entry.mock_state(hass, ConfigEntryState.LOADED) config_entry.add_to_hass(hass) device_in_both = device_registry.async_get_or_create( diff --git a/tests/components/sensibo/test_init.py b/tests/components/sensibo/test_init.py index 9698d5241cc..9ab30edf177 100644 --- a/tests/components/sensibo/test_init.py +++ b/tests/components/sensibo/test_init.py @@ -6,10 +6,9 @@ from unittest.mock import patch from pysensibo.model import SensiboData -from homeassistant import config_entries from homeassistant.components.sensibo.const import DOMAIN from homeassistant.components.sensibo.util import NoUsernameError -from homeassistant.config_entries import SOURCE_USER, ConfigEntry +from homeassistant.config_entries import SOURCE_USER, ConfigEntry, ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component @@ -49,7 +48,7 @@ async def test_setup_entry(hass: HomeAssistant, get_data: SensiboData) -> None: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED async def test_migrate_entry(hass: HomeAssistant, get_data: SensiboData) -> None: @@ -81,7 +80,7 @@ async def test_migrate_entry(hass: HomeAssistant, get_data: SensiboData) -> None await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert entry.version == 2 assert entry.unique_id == "username" @@ -113,7 +112,7 @@ async def test_migrate_entry_fails(hass: HomeAssistant, get_data: SensiboData) - await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == config_entries.ConfigEntryState.MIGRATION_ERROR + assert entry.state is ConfigEntryState.MIGRATION_ERROR assert entry.version == 1 assert entry.unique_id == "12" @@ -147,10 +146,10 @@ async def test_unload_entry(hass: HomeAssistant, get_data: SensiboData) -> None: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state is config_entries.ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED async def remove_device(ws_client, device_id, config_entry_id): diff --git a/tests/components/shelly/test_climate.py b/tests/components/shelly/test_climate.py index 7e0e2d1ce46..0bdab979a0e 100644 --- a/tests/components/shelly/test_climate.py +++ b/tests/components/shelly/test_climate.py @@ -479,7 +479,7 @@ async def test_block_set_mode_auth_error( mock_block_device.mock_update() await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED await hass.services.async_call( CLIMATE_DOMAIN, @@ -489,7 +489,7 @@ async def test_block_set_mode_auth_error( ) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -527,7 +527,7 @@ async def test_block_restored_climate_auth_error( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED # Make device online with auth error monkeypatch.setattr(mock_block_device, "initialized", True) @@ -537,7 +537,7 @@ async def test_block_restored_climate_auth_error( mock_block_device.mock_update() await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 diff --git a/tests/components/shelly/test_coordinator.py b/tests/components/shelly/test_coordinator.py index c16f78b83ff..b155176dccd 100644 --- a/tests/components/shelly/test_coordinator.py +++ b/tests/components/shelly/test_coordinator.py @@ -159,14 +159,14 @@ async def test_block_polling_auth_error( ) entry = await init_integration(hass, 1) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED # Move time to generate polling freezer.tick(timedelta(seconds=UPDATE_PERIOD_MULTIPLIER * 15)) async_fire_time_changed(hass) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -198,11 +198,11 @@ async def test_block_rest_update_auth_error( AsyncMock(side_effect=InvalidAuthError), ) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED await mock_rest_update(hass, freezer) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -468,7 +468,7 @@ async def test_rpc_reload_with_invalid_auth( async_fire_time_changed(hass) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -620,14 +620,14 @@ async def test_rpc_reconnect_auth_error( ), ) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED # Move time to generate reconnect freezer.tick(timedelta(seconds=RPC_RECONNECT_INTERVAL)) async_fire_time_changed(hass) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -659,11 +659,11 @@ async def test_rpc_polling_auth_error( ), ) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED await mock_polling_rpc_update(hass, freezer) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 diff --git a/tests/components/shelly/test_init.py b/tests/components/shelly/test_init.py index 754f1111548..de658cd0d16 100644 --- a/tests/components/shelly/test_init.py +++ b/tests/components/shelly/test_init.py @@ -163,7 +163,7 @@ async def test_device_connection_error( ) entry = await init_integration(hass, gen) - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY @pytest.mark.parametrize("gen", [1, 2, 3]) @@ -183,7 +183,7 @@ async def test_mac_mismatch_error( ) entry = await init_integration(hass, gen) - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY @pytest.mark.parametrize("gen", [1, 2, 3]) @@ -203,7 +203,7 @@ async def test_device_auth_error( ) entry = await init_integration(hass, gen) - assert entry.state == ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 diff --git a/tests/components/shelly/test_number.py b/tests/components/shelly/test_number.py index ecc6d7410bf..c138ef71b7d 100644 --- a/tests/components/shelly/test_number.py +++ b/tests/components/shelly/test_number.py @@ -197,7 +197,7 @@ async def test_block_set_value_auth_error( mock_block_device.mock_update() await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED await hass.services.async_call( NUMBER_DOMAIN, @@ -207,7 +207,7 @@ async def test_block_set_value_auth_error( ) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 diff --git a/tests/components/shelly/test_switch.py b/tests/components/shelly/test_switch.py index a57a9890921..fe2c4354afc 100644 --- a/tests/components/shelly/test_switch.py +++ b/tests/components/shelly/test_switch.py @@ -98,7 +98,7 @@ async def test_block_set_state_auth_error( ) entry = await init_integration(hass, 1) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED await hass.services.async_call( SWITCH_DOMAIN, @@ -108,7 +108,7 @@ async def test_block_set_state_auth_error( ) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -242,7 +242,7 @@ async def test_rpc_auth_error( ) entry = await init_integration(hass, 2) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED await hass.services.async_call( SWITCH_DOMAIN, @@ -252,7 +252,7 @@ async def test_rpc_auth_error( ) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 diff --git a/tests/components/shelly/test_update.py b/tests/components/shelly/test_update.py index f3960620a21..73320b2c65f 100644 --- a/tests/components/shelly/test_update.py +++ b/tests/components/shelly/test_update.py @@ -199,7 +199,7 @@ async def test_block_update_auth_error( ) entry = await init_integration(hass, 1) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED await hass.services.async_call( UPDATE_DOMAIN, @@ -209,7 +209,7 @@ async def test_block_update_auth_error( ) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -651,7 +651,7 @@ async def test_rpc_update_auth_error( ) entry = await init_integration(hass, 2) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED await hass.services.async_call( UPDATE_DOMAIN, @@ -661,7 +661,7 @@ async def test_rpc_update_auth_error( ) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 diff --git a/tests/components/slack/test_init.py b/tests/components/slack/test_init.py index e206e066c67..7f36ec01733 100644 --- a/tests/components/slack/test_init.py +++ b/tests/components/slack/test_init.py @@ -13,7 +13,7 @@ async def test_setup(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) - """Test Slack setup.""" entry: ConfigEntry = await async_init_integration(hass, aioclient_mock) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert entry.data == CONF_DATA @@ -26,7 +26,7 @@ async def test_async_setup_entry_not_ready( hass, aioclient_mock, error="cannot_connect" ) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_async_setup_entry_invalid_auth( @@ -37,4 +37,4 @@ async def test_async_setup_entry_invalid_auth( hass, aioclient_mock, error="invalid_auth" ) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR diff --git a/tests/components/speedtestdotnet/test_init.py b/tests/components/speedtestdotnet/test_init.py index 2b0f803eb6f..446ed527df4 100644 --- a/tests/components/speedtestdotnet/test_init.py +++ b/tests/components/speedtestdotnet/test_init.py @@ -46,7 +46,7 @@ async def test_entry_lifecycle(hass: HomeAssistant, mock_api: MagicMock) -> None await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert hass.data[DOMAIN] assert await hass.config_entries.async_unload(entry.entry_id) diff --git a/tests/components/sql/test_init.py b/tests/components/sql/test_init.py index cf5721f52f6..409ebca27c0 100644 --- a/tests/components/sql/test_init.py +++ b/tests/components/sql/test_init.py @@ -7,11 +7,11 @@ from unittest.mock import patch import pytest import voluptuous as vol -from homeassistant import config_entries from homeassistant.components.recorder import Recorder from homeassistant.components.recorder.util import get_instance from homeassistant.components.sql import validate_sql_select from homeassistant.components.sql.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -21,17 +21,17 @@ from . import YAML_CONFIG_INVALID, YAML_CONFIG_NO_DB, init_integration async def test_setup_entry(recorder_mock: Recorder, hass: HomeAssistant) -> None: """Test setup entry.""" config_entry = await init_integration(hass) - assert config_entry.state == config_entries.ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED async def test_unload_entry(recorder_mock: Recorder, hass: HomeAssistant) -> None: """Test unload an entry.""" config_entry = await init_integration(hass) - assert config_entry.state == config_entries.ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state is config_entries.ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED async def test_setup_config(recorder_mock: Recorder, hass: HomeAssistant) -> None: diff --git a/tests/components/srp_energy/test_config_flow.py b/tests/components/srp_energy/test_config_flow.py index bef1acab855..19e21f0e1a0 100644 --- a/tests/components/srp_energy/test_config_flow.py +++ b/tests/components/srp_energy/test_config_flow.py @@ -121,7 +121,7 @@ async def test_flow_entry_already_configured( ) -> None: """Test user input for config_entry that already exists.""" # Verify mock config setup from fixture - assert init_integration.state == ConfigEntryState.LOADED + assert init_integration.state is ConfigEntryState.LOADED assert init_integration.data[CONF_ID] == ACCNT_ID assert init_integration.unique_id == ACCNT_ID @@ -144,7 +144,7 @@ async def test_flow_multiple_configs( ) -> None: """Test multiple config entries.""" # Verify mock config setup from fixture - assert init_integration.state == ConfigEntryState.LOADED + assert init_integration.state is ConfigEntryState.LOADED assert init_integration.data[CONF_ID] == ACCNT_ID assert init_integration.unique_id == ACCNT_ID diff --git a/tests/components/srp_energy/test_init.py b/tests/components/srp_energy/test_init.py index e2411fd4688..2a02e44b9b0 100644 --- a/tests/components/srp_energy/test_init.py +++ b/tests/components/srp_energy/test_init.py @@ -6,12 +6,12 @@ from homeassistant.core import HomeAssistant async def test_setup_entry(hass: HomeAssistant, init_integration) -> None: """Test setup entry.""" - assert init_integration.state == ConfigEntryState.LOADED + assert init_integration.state is ConfigEntryState.LOADED async def test_unload_entry(hass: HomeAssistant, init_integration) -> None: """Test being able to unload an entry.""" - assert init_integration.state == ConfigEntryState.LOADED + assert init_integration.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(init_integration.entry_id) await hass.async_block_till_done() diff --git a/tests/components/srp_energy/test_sensor.py b/tests/components/srp_energy/test_sensor.py index a46caf904b7..7369d07f77a 100644 --- a/tests/components/srp_energy/test_sensor.py +++ b/tests/components/srp_energy/test_sensor.py @@ -21,7 +21,7 @@ from tests.common import MockConfigEntry async def test_loading_sensors(hass: HomeAssistant, init_integration) -> None: """Test the srp energy sensors.""" # Validate the Config Entry was initialized - assert init_integration.state == ConfigEntryState.LOADED + assert init_integration.state is ConfigEntryState.LOADED # Check sensors were loaded assert len(hass.states.async_all()) == 1 diff --git a/tests/components/starlink/test_init.py b/tests/components/starlink/test_init.py index 03e9787b6c0..62a1ee41236 100644 --- a/tests/components/starlink/test_init.py +++ b/tests/components/starlink/test_init.py @@ -31,7 +31,7 @@ async def test_successful_entry(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert entry.entry_id in hass.data[DOMAIN] diff --git a/tests/components/steam_online/test_init.py b/tests/components/steam_online/test_init.py index 48584dab7a5..ccc7690aae3 100644 --- a/tests/components/steam_online/test_init.py +++ b/tests/components/steam_online/test_init.py @@ -17,7 +17,7 @@ async def test_setup(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() @@ -33,7 +33,7 @@ async def test_async_setup_entry_auth_failed(hass: HomeAssistant) -> None: interface.side_effect = steam.api.HTTPError("401") await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR assert not hass.data.get(DOMAIN) diff --git a/tests/components/steamist/__init__.py b/tests/components/steamist/__init__.py index 47fa2236849..77e3efca1f0 100644 --- a/tests/components/steamist/__init__.py +++ b/tests/components/steamist/__init__.py @@ -74,7 +74,7 @@ async def _async_setup_entry_with_status( with _patch_status(status, client): await async_setup_component(hass, steamist.DOMAIN, {steamist.DOMAIN: {}}) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED return client, config_entry diff --git a/tests/components/steamist/test_init.py b/tests/components/steamist/test_init.py index 32400449d0d..96ea59afda2 100644 --- a/tests/components/steamist/test_init.py +++ b/tests/components/steamist/test_init.py @@ -49,7 +49,7 @@ async def test_config_entry_reload(hass: HomeAssistant) -> None: ) await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED async def test_config_entry_retry_later(hass: HomeAssistant) -> None: @@ -65,7 +65,7 @@ async def test_config_entry_retry_later(hass: HomeAssistant) -> None: ): await async_setup_component(hass, steamist.DOMAIN, {steamist.DOMAIN: {}}) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY async def test_config_entry_fills_unique_id_with_directed_discovery( @@ -101,7 +101,7 @@ async def test_config_entry_fills_unique_id_with_directed_discovery( ): await async_setup_component(hass, steamist.DOMAIN, {steamist.DOMAIN: {}}) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert config_entry.unique_id == FORMATTED_MAC_ADDRESS assert config_entry.data[CONF_NAME] == DEVICE_NAME diff --git a/tests/components/stt/test_init.py b/tests/components/stt/test_init.py index a06c635bcfd..165a520c653 100644 --- a/tests/components/stt/test_init.py +++ b/tests/components/stt/test_init.py @@ -370,9 +370,9 @@ async def test_config_entry_unload( ) -> None: """Test we can unload config entry.""" config_entry = await mock_config_entry_setup(hass, tmp_path, mock_provider_entity) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED async def test_restore_state( @@ -388,7 +388,7 @@ async def test_restore_state( config_entry = await mock_config_entry_setup(hass, tmp_path, mock_provider_entity) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED state = hass.states.get(entity_id) assert state assert state.state == timestamp diff --git a/tests/components/switch_as_x/test_init.py b/tests/components/switch_as_x/test_init.py index c74b14cc91c..266d0fd0409 100644 --- a/tests/components/switch_as_x/test_init.py +++ b/tests/components/switch_as_x/test_init.py @@ -949,7 +949,7 @@ async def test_migrate( await hass.async_block_till_done() # Check migration was successful and added invert option - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert config_entry.options == { CONF_ENTITY_ID: "switch.test", CONF_INVERT: False, @@ -988,7 +988,7 @@ async def test_migrate_from_future( await hass.async_block_till_done() # Check migration was not successful and did not add invert option - assert config_entry.state == ConfigEntryState.MIGRATION_ERROR + assert config_entry.state is ConfigEntryState.MIGRATION_ERROR assert config_entry.options == { CONF_ENTITY_ID: "switch.test", CONF_TARGET_DOMAIN: target_domain, diff --git a/tests/components/switchbot_cloud/test_init.py b/tests/components/switchbot_cloud/test_init.py index e9f0a0a475d..25ea370efe5 100644 --- a/tests/components/switchbot_cloud/test_init.py +++ b/tests/components/switchbot_cloud/test_init.py @@ -55,7 +55,7 @@ async def test_setup_entry_success( entry = configure_integration(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() @@ -104,7 +104,7 @@ async def test_setup_entry_fails_when_refreshing( mock_get_status.side_effect = CannotConnect entry = configure_integration(hass) await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() diff --git a/tests/components/system_bridge/test_init.py b/tests/components/system_bridge/test_init.py index 67d8595ba4c..7632a0c8157 100644 --- a/tests/components/system_bridge/test_init.py +++ b/tests/components/system_bridge/test_init.py @@ -46,7 +46,7 @@ async def test_migration_minor_1_to_2(hass: HomeAssistant) -> None: CONF_PORT: FIXTURE_USER_INPUT[CONF_PORT], CONF_TOKEN: FIXTURE_USER_INPUT[CONF_TOKEN], } - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED async def test_migration_minor_future_version(hass: HomeAssistant) -> None: @@ -80,4 +80,4 @@ async def test_migration_minor_future_version(hass: HomeAssistant) -> None: assert config_entry.version == config_entry_version assert config_entry.minor_version == config_entry_minor_version assert config_entry.data == config_entry_data - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED diff --git a/tests/components/systemmonitor/test_init.py b/tests/components/systemmonitor/test_init.py index 705f86f8048..97f4a41b96c 100644 --- a/tests/components/systemmonitor/test_init.py +++ b/tests/components/systemmonitor/test_init.py @@ -21,7 +21,7 @@ async def test_load_unload_entry( ) -> None: """Test load and unload an entry.""" - assert mock_added_config_entry.state == ConfigEntryState.LOADED + assert mock_added_config_entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(mock_added_config_entry.entry_id) await hass.async_block_till_done() assert mock_added_config_entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/tami4/test_init.py b/tests/components/tami4/test_init.py index 62e5861e13c..2e9663c2728 100644 --- a/tests/components/tami4/test_init.py +++ b/tests/components/tami4/test_init.py @@ -13,7 +13,7 @@ async def test_init_success(mock_api, hass: HomeAssistant) -> None: """Test setup and that we can create the entry.""" entry = await create_config_entry(hass) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED @pytest.mark.parametrize( @@ -23,7 +23,7 @@ async def test_init_with_api_error(mock_api, hass: HomeAssistant) -> None: """Test init with api error.""" entry = await create_config_entry(hass) - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY @pytest.mark.parametrize( diff --git a/tests/components/tankerkoenig/test_coordinator.py b/tests/components/tankerkoenig/test_coordinator.py index de65cd921be..1e8991f3f9c 100644 --- a/tests/components/tankerkoenig/test_coordinator.py +++ b/tests/components/tankerkoenig/test_coordinator.py @@ -34,7 +34,7 @@ async def test_rate_limit( caplog: pytest.LogCaptureFixture, ) -> None: """Test detection of API rate limit.""" - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED state = hass.states.get("binary_sensor.station_somewhere_street_1_status") assert state assert state.state == "on" diff --git a/tests/components/tesla_wall_connector/test_init.py b/tests/components/tesla_wall_connector/test_init.py index 152bf18d57e..2b37924b2e4 100644 --- a/tests/components/tesla_wall_connector/test_init.py +++ b/tests/components/tesla_wall_connector/test_init.py @@ -2,7 +2,7 @@ from tesla_wall_connector.exceptions import WallConnectorConnectionError -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from .conftest import create_wall_connector_entry @@ -13,7 +13,7 @@ async def test_init_success(hass: HomeAssistant) -> None: entry = await create_wall_connector_entry(hass) - assert entry.state == config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED async def test_init_while_offline(hass: HomeAssistant) -> None: @@ -22,7 +22,7 @@ async def test_init_while_offline(hass: HomeAssistant) -> None: hass, side_effect=WallConnectorConnectionError ) - assert entry.state == config_entries.ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_load_unload(hass: HomeAssistant) -> None: @@ -30,7 +30,7 @@ async def test_load_unload(hass: HomeAssistant) -> None: entry = await create_wall_connector_entry(hass) - assert entry.state is config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/todo/test_init.py b/tests/components/todo/test_init.py index 4e52c2fff70..95024b71757 100644 --- a/tests/components/todo/test_init.py +++ b/tests/components/todo/test_init.py @@ -180,14 +180,14 @@ async def test_unload_entry( """Test unloading a config entry with a todo entity.""" config_entry = await create_mock_platform(hass, [test_entity]) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED state = hass.states.get("todo.entity1") assert state assert await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED state = hass.states.get("todo.entity1") assert not state diff --git a/tests/components/todoist/test_init.py b/tests/components/todoist/test_init.py index 62915eb0fdd..453276474b3 100644 --- a/tests/components/todoist/test_init.py +++ b/tests/components/todoist/test_init.py @@ -21,10 +21,10 @@ async def test_load_unload( entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 - assert todoist_config_entry.state == ConfigEntryState.LOADED + assert todoist_config_entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(todoist_config_entry.entry_id) - assert todoist_config_entry.state == ConfigEntryState.NOT_LOADED + assert todoist_config_entry.state is ConfigEntryState.NOT_LOADED @pytest.mark.parametrize("todoist_api_status", [HTTPStatus.INTERNAL_SERVER_ERROR]) @@ -35,4 +35,4 @@ async def test_init_failure( todoist_config_entry: MockConfigEntry | None, ) -> None: """Test an initialization error on integration load.""" - assert todoist_config_entry.state == ConfigEntryState.SETUP_RETRY + assert todoist_config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/tplink/test_config_flow.py b/tests/components/tplink/test_config_flow.py index e83ff173701..7bf3b8cce5e 100644 --- a/tests/components/tplink/test_config_flow.py +++ b/tests/components/tplink/test_config_flow.py @@ -15,6 +15,7 @@ from homeassistant.components.tplink import ( SmartDeviceException, ) from homeassistant.components.tplink.const import CONF_DEVICE_CONFIG +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( CONF_ALIAS, CONF_DEVICE, @@ -770,7 +771,7 @@ async def test_integration_discovery_with_ip_change( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - assert mock_config_entry.state == config_entries.ConfigEntryState.SETUP_RETRY + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY flows = hass.config_entries.flow.async_progress() assert len(flows) == 0 @@ -803,7 +804,7 @@ async def test_integration_discovery_with_ip_change( mock_connect["connect"].return_value = bulb await hass.config_entries.async_reload(mock_config_entry.entry_id) await hass.async_block_till_done() - assert mock_config_entry.state == config_entries.ConfigEntryState.LOADED + assert mock_config_entry.state is ConfigEntryState.LOADED # Check that init set the new host correctly before calling connect assert config.host == "127.0.0.1" config.host = "127.0.0.2" @@ -822,7 +823,7 @@ async def test_dhcp_discovery_with_ip_change( 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() - assert mock_config_entry.state == config_entries.ConfigEntryState.SETUP_RETRY + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY flows = hass.config_entries.flow.async_progress() assert len(flows) == 0 @@ -851,7 +852,7 @@ async def test_reauth( mock_added_config_entry.async_start_reauth(hass) await hass.async_block_till_done() - assert mock_added_config_entry.state == config_entries.ConfigEntryState.LOADED + assert mock_added_config_entry.state is ConfigEntryState.LOADED flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 [result] = flows @@ -888,7 +889,7 @@ async def test_reauth_update_from_discovery( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - assert mock_config_entry.state == config_entries.ConfigEntryState.SETUP_ERROR + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -924,7 +925,7 @@ async def test_reauth_update_from_discovery_with_ip_change( 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() - assert mock_config_entry.state == config_entries.ConfigEntryState.SETUP_ERROR + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -967,7 +968,7 @@ async def test_reauth_no_update_if_config_and_ip_the_same( ) await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - assert mock_config_entry.state is config_entries.ConfigEntryState.SETUP_ERROR + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -1013,7 +1014,7 @@ async def test_reauth_errors( mock_added_config_entry.async_start_reauth(hass) await hass.async_block_till_done() - assert mock_added_config_entry.state is config_entries.ConfigEntryState.LOADED + assert mock_added_config_entry.state is ConfigEntryState.LOADED flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 [result] = flows @@ -1155,8 +1156,8 @@ async def test_reauth_update_other_flows( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - assert mock_config_entry2.state == config_entries.ConfigEntryState.SETUP_ERROR - assert mock_config_entry.state == config_entries.ConfigEntryState.SETUP_ERROR + assert mock_config_entry2.state is ConfigEntryState.SETUP_ERROR + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR mock_connect["connect"].side_effect = default_side_effect await hass.async_block_till_done() diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py index 176f2aab7ae..b8f623ac6dc 100644 --- a/tests/components/tplink/test_init.py +++ b/tests/components/tplink/test_init.py @@ -77,10 +77,10 @@ async def test_config_entry_reload(hass: HomeAssistant) -> None: with _patch_discovery(), _patch_single_discovery(), _patch_connect(): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() - assert already_migrated_config_entry.state == ConfigEntryState.LOADED + assert already_migrated_config_entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(already_migrated_config_entry.entry_id) await hass.async_block_till_done() - assert already_migrated_config_entry.state == ConfigEntryState.NOT_LOADED + assert already_migrated_config_entry.state is ConfigEntryState.NOT_LOADED async def test_config_entry_retry(hass: HomeAssistant) -> None: @@ -96,7 +96,7 @@ async def test_config_entry_retry(hass: HomeAssistant) -> None: ): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() - assert already_migrated_config_entry.state == ConfigEntryState.SETUP_RETRY + assert already_migrated_config_entry.state is ConfigEntryState.SETUP_RETRY async def test_dimmer_switch_unique_id_fix_original_entity_still_exists( @@ -154,7 +154,7 @@ async def test_config_entry_wrong_mac_Address( with _patch_discovery(), _patch_single_discovery(), _patch_connect(): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() - assert already_migrated_config_entry.state == ConfigEntryState.SETUP_RETRY + assert already_migrated_config_entry.state is ConfigEntryState.SETUP_RETRY assert ( "Unexpected device found at 127.0.0.1; expected aa:bb:cc:dd:ee:f0, found aa:bb:cc:dd:ee:ff" diff --git a/tests/components/traccar_server/test_config_flow.py b/tests/components/traccar_server/test_config_flow.py index 5652d2c77be..fdc22f9ff97 100644 --- a/tests/components/traccar_server/test_config_flow.py +++ b/tests/components/traccar_server/test_config_flow.py @@ -17,6 +17,7 @@ from homeassistant.components.traccar_server.const import ( DOMAIN, EVENTS, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -62,7 +63,7 @@ async def test_form( CONF_SSL: False, CONF_VERIFY_SSL: True, } - assert result["result"].state == config_entries.ConfigEntryState.LOADED + assert result["result"].state is ConfigEntryState.LOADED @pytest.mark.parametrize( @@ -120,7 +121,7 @@ async def test_form_cannot_connect( CONF_VERIFY_SSL: True, } - assert result["result"].state == config_entries.ConfigEntryState.LOADED + assert result["result"].state is ConfigEntryState.LOADED async def test_options( @@ -242,7 +243,7 @@ async def test_import_from_yaml( assert result["title"] == f"{data[CONF_HOST]}:{data[CONF_PORT]}" assert result["data"] == data assert result["options"] == options - assert result["result"].state == config_entries.ConfigEntryState.LOADED + assert result["result"].state is ConfigEntryState.LOADED async def test_abort_import_already_configured(hass: HomeAssistant) -> None: diff --git a/tests/components/trafikverket_camera/test_coordinator.py b/tests/components/trafikverket_camera/test_coordinator.py index 4f633cb524d..3f37ad05575 100644 --- a/tests/components/trafikverket_camera/test_coordinator.py +++ b/tests/components/trafikverket_camera/test_coordinator.py @@ -12,10 +12,9 @@ from pytrafikverket.exceptions import ( UnknownError, ) -from homeassistant import config_entries from homeassistant.components.trafikverket_camera.const import DOMAIN from homeassistant.components.trafikverket_camera.coordinator import CameraData -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_USER, ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import UpdateFailed @@ -65,22 +64,22 @@ async def test_coordinator( ( InvalidAuthentication, ConfigEntryAuthFailed, - config_entries.ConfigEntryState.SETUP_ERROR, + ConfigEntryState.SETUP_ERROR, ), ( NoCameraFound, UpdateFailed, - config_entries.ConfigEntryState.SETUP_RETRY, + ConfigEntryState.SETUP_RETRY, ), ( MultipleCamerasFound, UpdateFailed, - config_entries.ConfigEntryState.SETUP_RETRY, + ConfigEntryState.SETUP_RETRY, ), ( UnknownError, UpdateFailed, - config_entries.ConfigEntryState.SETUP_RETRY, + ConfigEntryState.SETUP_RETRY, ), ], ) @@ -152,4 +151,4 @@ async def test_coordinator_failed_get_image( mock_data.assert_called_once() state = hass.states.get("camera.test_camera") assert state is None - assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/trafikverket_camera/test_init.py b/tests/components/trafikverket_camera/test_init.py index 688af08fec1..f21d36fda27 100644 --- a/tests/components/trafikverket_camera/test_init.py +++ b/tests/components/trafikverket_camera/test_init.py @@ -9,10 +9,9 @@ import pytest from pytrafikverket.exceptions import UnknownError from pytrafikverket.trafikverket_camera import CameraInfo -from homeassistant import config_entries from homeassistant.components.trafikverket_camera import async_migrate_entry from homeassistant.components.trafikverket_camera.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_USER, ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util @@ -50,7 +49,7 @@ async def test_setup_entry( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state is config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert len(mock_tvt_camera.mock_calls) == 1 @@ -82,10 +81,10 @@ async def test_unload_entry( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state is config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state is config_entries.ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED async def test_migrate_entry( @@ -115,7 +114,7 @@ async def test_migrate_entry( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state is config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert entry.version == 3 assert entry.unique_id == "trafikverket_camera-1234" assert entry.data == ENTRY_CONFIG @@ -165,7 +164,7 @@ async def test_migrate_entry_fails_with_error( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state is config_entries.ConfigEntryState.MIGRATION_ERROR + assert entry.state is ConfigEntryState.MIGRATION_ERROR assert entry.version == version assert entry.unique_id == unique_id assert len(mock_tvt_camera.mock_calls) == 1 @@ -229,7 +228,7 @@ async def test_migrate_entry_fails_no_id( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state is config_entries.ConfigEntryState.MIGRATION_ERROR + assert entry.state is ConfigEntryState.MIGRATION_ERROR assert entry.version == version assert entry.unique_id == unique_id assert len(mock_tvt_camera.mock_calls) == 1 diff --git a/tests/components/trafikverket_ferry/test_init.py b/tests/components/trafikverket_ferry/test_init.py index adfc84d94cb..22ada7e0f40 100644 --- a/tests/components/trafikverket_ferry/test_init.py +++ b/tests/components/trafikverket_ferry/test_init.py @@ -6,9 +6,8 @@ from unittest.mock import patch from pytrafikverket.trafikverket_ferry import FerryStop -from homeassistant import config_entries from homeassistant.components.trafikverket_ferry.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_USER, ConfigEntryState from homeassistant.core import HomeAssistant from . import ENTRY_CONFIG @@ -34,7 +33,7 @@ async def test_setup_entry(hass: HomeAssistant, get_ferries: list[FerryStop]) -> await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state is config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert len(mock_tvt_ferry.mock_calls) == 1 @@ -56,7 +55,7 @@ async def test_unload_entry(hass: HomeAssistant, get_ferries: list[FerryStop]) - await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state is config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state is config_entries.ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/trafikverket_train/test_init.py b/tests/components/trafikverket_train/test_init.py index f68c32b5b90..329d8d716d0 100644 --- a/tests/components/trafikverket_train/test_init.py +++ b/tests/components/trafikverket_train/test_init.py @@ -8,9 +8,8 @@ from pytrafikverket.exceptions import InvalidAuthentication, NoTrainStationFound from pytrafikverket.trafikverket_train import TrainStop from syrupy.assertion import SnapshotAssertion -from homeassistant import config_entries from homeassistant.components.trafikverket_train.const import DOMAIN -from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER, ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_registry import EntityRegistry @@ -43,12 +42,12 @@ async def test_unload_entry(hass: HomeAssistant, get_trains: list[TrainStop]) -> await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state is config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert len(mock_tv_train.mock_calls) == 1 assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state is config_entries.ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED async def test_auth_failed( @@ -74,7 +73,7 @@ async def test_auth_failed( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state is config_entries.ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR active_flows = entry.async_get_active_flows(hass, (SOURCE_REAUTH)) for flow in active_flows: @@ -104,7 +103,7 @@ async def test_no_stations( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_migrate_entity_unique_id( @@ -144,7 +143,7 @@ async def test_migrate_entity_unique_id( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state is config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED entity = entity_registry.async_get(entity.entity_id) assert entity.unique_id == f"{entry.entry_id}-departure_time" diff --git a/tests/components/transmission/test_init.py b/tests/components/transmission/test_init.py index 7efbaad76fb..307576ffdea 100644 --- a/tests/components/transmission/test_init.py +++ b/tests/components/transmission/test_init.py @@ -41,7 +41,7 @@ async def test_successful_config_entry(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED async def test_config_flow_entry_migrate_1_1_to_1_2(hass: HomeAssistant) -> None: @@ -76,7 +76,7 @@ async def test_setup_failed_connection_error( mock_api.side_effect = TransmissionConnectError() await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_setup_failed_auth_error( @@ -90,7 +90,7 @@ async def test_setup_failed_auth_error( mock_api.side_effect = TransmissionAuthError() await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR async def test_setup_failed_unexpected_error( @@ -104,7 +104,7 @@ async def test_setup_failed_unexpected_error( mock_api.side_effect = TransmissionError() await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR async def test_unload_entry(hass: HomeAssistant) -> None: diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index 2c58c25a509..7d308ec0b23 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -82,7 +82,7 @@ async def test_config_entry_unload( assert state is None config_entry = await mock_config_entry_setup(hass, mock_tts_entity) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED state = hass.states.get(entity_id) assert state is not None assert state.state == STATE_UNKNOWN @@ -115,7 +115,7 @@ async def test_config_entry_unload( await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED state = hass.states.get(entity_id) assert state is None @@ -133,7 +133,7 @@ async def test_restore_state( config_entry = await mock_config_entry_setup(hass, mock_tts_entity) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED state = hass.states.get(entity_id) assert state assert state.state == timestamp diff --git a/tests/components/twinkly/test_init.py b/tests/components/twinkly/test_init.py index 33f24a31d8f..794d4d5e773 100644 --- a/tests/components/twinkly/test_init.py +++ b/tests/components/twinkly/test_init.py @@ -34,11 +34,11 @@ async def test_load_unload_entry(hass: HomeAssistant) -> None: with patch("homeassistant.components.twinkly.Twinkly", return_value=client): await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED async def test_config_entry_not_ready(hass: HomeAssistant) -> None: diff --git a/tests/components/unifiprotect/test_config_flow.py b/tests/components/unifiprotect/test_config_flow.py index 58228a08d0d..fda5c91afae 100644 --- a/tests/components/unifiprotect/test_config_flow.py +++ b/tests/components/unifiprotect/test_config_flow.py @@ -18,6 +18,7 @@ from homeassistant.components.unifiprotect.const import ( CONF_OVERRIDE_CHOST, DOMAIN, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -307,7 +308,7 @@ async def test_form_options(hass: HomeAssistant, ufp_client: ProtectApiClient) - await hass.config_entries.async_setup(mock_config.entry_id) await hass.async_block_till_done() - assert mock_config.state == config_entries.ConfigEntryState.LOADED + assert mock_config.state is ConfigEntryState.LOADED result = await hass.config_entries.options.async_init(mock_config.entry_id) assert result["type"] is FlowResultType.FORM diff --git a/tests/components/unifiprotect/test_init.py b/tests/components/unifiprotect/test_init.py index f123abb9861..0e3fd42e28b 100644 --- a/tests/components/unifiprotect/test_init.py +++ b/tests/components/unifiprotect/test_init.py @@ -94,7 +94,7 @@ async def test_setup_multiple( await hass.config_entries.async_setup(mock_config.entry_id) await hass.async_block_till_done() - assert mock_config.state == ConfigEntryState.LOADED + assert mock_config.state is ConfigEntryState.LOADED assert ufp.api.update.called assert mock_config.unique_id == ufp.api.bootstrap.nvr.mac @@ -158,7 +158,7 @@ async def test_setup_cloud_account( await hass.config_entries.async_setup(ufp.entry.entry_id) await hass.async_block_till_done() - assert ufp.entry.state == ConfigEntryState.LOADED + assert ufp.entry.state is ConfigEntryState.LOADED await ws_client.send_json({"id": 1, "type": "repairs/list_issues"}) msg = await ws_client.receive_json() diff --git a/tests/components/uptimerobot/common.py b/tests/components/uptimerobot/common.py index 23c0d3e1ce7..c2d154cd967 100644 --- a/tests/components/uptimerobot/common.py +++ b/tests/components/uptimerobot/common.py @@ -16,6 +16,7 @@ from pyuptimerobot import ( from homeassistant import config_entries from homeassistant.components.uptimerobot.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_ON from homeassistant.core import HomeAssistant @@ -116,6 +117,6 @@ async def setup_uptimerobot_integration(hass: HomeAssistant) -> MockConfigEntry: assert hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY).state == STATE_ON assert hass.states.get(UPTIMEROBOT_SENSOR_TEST_ENTITY).state == STATE_UP - assert mock_entry.state == config_entries.ConfigEntryState.LOADED + assert mock_entry.state is ConfigEntryState.LOADED return mock_entry diff --git a/tests/components/uptimerobot/test_init.py b/tests/components/uptimerobot/test_init.py index a45480e50a5..c0583eddb7d 100644 --- a/tests/components/uptimerobot/test_init.py +++ b/tests/components/uptimerobot/test_init.py @@ -11,6 +11,7 @@ from homeassistant.components.uptimerobot.const import ( COORDINATOR_UPDATE_INTERVAL, DOMAIN, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_ON, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr @@ -44,7 +45,7 @@ async def test_reauthentication_trigger_in_setup( flows = hass.config_entries.flow.async_progress() - assert mock_config_entry.state == config_entries.ConfigEntryState.SETUP_ERROR + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR assert mock_config_entry.reason == "could not authenticate" assert len(flows) == 1 @@ -74,7 +75,7 @@ async def test_reauthentication_trigger_key_read_only( flows = hass.config_entries.flow.async_progress() - assert mock_config_entry.state == config_entries.ConfigEntryState.SETUP_ERROR + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR assert ( mock_config_entry.reason == "Wrong API key type detected, use the 'main' API key" @@ -102,7 +103,7 @@ async def test_reauthentication_trigger_after_setup( mock_config_entry = await setup_uptimerobot_integration(hass) binary_sensor = hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY) - assert mock_config_entry.state == config_entries.ConfigEntryState.LOADED + assert mock_config_entry.state is ConfigEntryState.LOADED assert binary_sensor.state == STATE_ON with patch( @@ -146,7 +147,7 @@ async def test_integration_reload( await hass.async_block_till_done() entry = hass.config_entries.async_get_entry(mock_entry.entry_id) - assert entry.state == config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY).state == STATE_ON diff --git a/tests/components/valve/test_init.py b/tests/components/valve/test_init.py index a00d975f0eb..971e3d04f3e 100644 --- a/tests/components/valve/test_init.py +++ b/tests/components/valve/test_init.py @@ -205,7 +205,7 @@ async def test_valve_setup( assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED for entity in mock_config_entry[1]: entity_id = entity.entity_id state = hass.states.get(entity_id) @@ -215,7 +215,7 @@ async def test_valve_setup( assert await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED for entity in mock_config_entry[1]: entity_id = entity.entity_id diff --git a/tests/components/velbus/test_init.py b/tests/components/velbus/test_init.py index 08efdd0410b..dea246b8a86 100644 --- a/tests/components/velbus/test_init.py +++ b/tests/components/velbus/test_init.py @@ -20,12 +20,12 @@ async def test_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> N await hass.async_block_till_done() assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) diff --git a/tests/components/venstar/test_init.py b/tests/components/venstar/test_init.py index 75250b52f5b..bc8d400df6c 100644 --- a/tests/components/venstar/test_init.py +++ b/tests/components/venstar/test_init.py @@ -54,11 +54,11 @@ async def test_setup_entry(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED async def test_setup_entry_exception(hass: HomeAssistant) -> None: @@ -97,4 +97,4 @@ async def test_setup_entry_exception(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/version/common.py b/tests/components/version/common.py index 33a1747cf0e..c14ec2c4fbf 100644 --- a/tests/components/version/common.py +++ b/tests/components/version/common.py @@ -15,6 +15,7 @@ from homeassistant.components.version.const import ( UPDATE_COORDINATOR_UPDATE_INTERVAL, VERSION_SOURCE_LOCAL, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant @@ -75,6 +76,6 @@ async def setup_version_integration( assert await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() - assert mock_entry.state == config_entries.ConfigEntryState.LOADED + assert mock_entry.state is ConfigEntryState.LOADED return mock_entry diff --git a/tests/components/version/test_config_flow.py b/tests/components/version/test_config_flow.py index 1779b24b45d..edf3439644d 100644 --- a/tests/components/version/test_config_flow.py +++ b/tests/components/version/test_config_flow.py @@ -18,6 +18,7 @@ from homeassistant.components.version.const import ( VERSION_SOURCE_PYPI, VERSION_SOURCE_VERSIONS, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_SOURCE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -31,7 +32,7 @@ from tests.common import async_fire_time_changed async def test_reload_config_entry(hass: HomeAssistant) -> None: """Test reloading the config entry.""" config_entry = await setup_version_integration(hass) - assert config_entry.state == config_entries.ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED with patch( "pyhaversion.HaVersion.get_version", @@ -44,7 +45,7 @@ async def test_reload_config_entry(hass: HomeAssistant) -> None: await hass.async_block_till_done() entry = hass.config_entries.async_get_entry(config_entry.entry_id) - assert entry.state == config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED async def test_basic_form(hass: HomeAssistant) -> None: diff --git a/tests/components/voip/conftest.py b/tests/components/voip/conftest.py index 619a80d86c4..bcd9becbc5a 100644 --- a/tests/components/voip/conftest.py +++ b/tests/components/voip/conftest.py @@ -38,7 +38,7 @@ async def setup_voip(hass: HomeAssistant, config_entry: MockConfigEntry) -> None return_value=(Mock(), AsyncMock()), ): assert await async_setup_component(hass, DOMAIN, {}) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED yield diff --git a/tests/components/wake_word/test_init.py b/tests/components/wake_word/test_init.py index 0aac011d02a..1e957ad7a2c 100644 --- a/tests/components/wake_word/test_init.py +++ b/tests/components/wake_word/test_init.py @@ -165,9 +165,9 @@ async def test_config_entry_unload( ) -> None: """Test we can unload config entry.""" config_entry = await mock_config_entry_setup(hass, tmp_path, mock_provider_entity) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(config_entry.entry_id) - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED @freeze_time("2023-06-22 10:30:00+00:00") @@ -268,7 +268,7 @@ async def test_restore_state( config_entry = await mock_config_entry_setup(hass, tmp_path, mock_provider_entity) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED state = hass.states.get(entity_id) assert state assert state.state == timestamp diff --git a/tests/components/wallbox/test_config_flow.py b/tests/components/wallbox/test_config_flow.py index 0c4497929dc..0a243735cb4 100644 --- a/tests/components/wallbox/test_config_flow.py +++ b/tests/components/wallbox/test_config_flow.py @@ -17,6 +17,7 @@ from homeassistant.components.wallbox.const import ( CHARGER_MAX_CHARGING_CURRENT_KEY, DOMAIN, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -145,7 +146,7 @@ async def test_form_validate_input(hass: HomeAssistant) -> None: async def test_form_reauth(hass: HomeAssistant, entry: MockConfigEntry) -> None: """Test we handle reauth flow.""" await setup_integration(hass, entry) - assert entry.state == config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED with requests_mock.Mocker() as mock_request: mock_request.get( @@ -185,7 +186,7 @@ async def test_form_reauth(hass: HomeAssistant, entry: MockConfigEntry) -> None: async def test_form_reauth_invalid(hass: HomeAssistant, entry: MockConfigEntry) -> None: """Test we handle reauth invalid flow.""" await setup_integration(hass, entry) - assert entry.state == config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED with requests_mock.Mocker() as mock_request: mock_request.get( diff --git a/tests/components/wallbox/test_init.py b/tests/components/wallbox/test_init.py index 3dfc391aa3b..f1362489c50 100644 --- a/tests/components/wallbox/test_init.py +++ b/tests/components/wallbox/test_init.py @@ -28,10 +28,10 @@ async def test_wallbox_setup_unload_entry( """Test Wallbox Unload.""" await setup_integration(hass, entry) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED async def test_wallbox_unload_entry_connection_error( @@ -40,10 +40,10 @@ async def test_wallbox_unload_entry_connection_error( """Test Wallbox Unload Connection Error.""" await setup_integration_connection_error(hass, entry) - assert entry.state == ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR assert await hass.config_entries.async_unload(entry.entry_id) - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED async def test_wallbox_refresh_failed_connection_error_auth( @@ -52,7 +52,7 @@ async def test_wallbox_refresh_failed_connection_error_auth( """Test Wallbox setup with connection error.""" await setup_integration(hass, entry) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED with requests_mock.Mocker() as mock_request: mock_request.get( @@ -71,7 +71,7 @@ async def test_wallbox_refresh_failed_connection_error_auth( await wallbox.async_refresh() assert await hass.config_entries.async_unload(entry.entry_id) - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED async def test_wallbox_refresh_failed_invalid_auth( @@ -80,7 +80,7 @@ async def test_wallbox_refresh_failed_invalid_auth( """Test Wallbox setup with authentication error.""" await setup_integration(hass, entry) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED with requests_mock.Mocker() as mock_request: mock_request.get( @@ -99,7 +99,7 @@ async def test_wallbox_refresh_failed_invalid_auth( await wallbox.async_refresh() assert await hass.config_entries.async_unload(entry.entry_id) - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED async def test_wallbox_refresh_failed_connection_error( @@ -108,7 +108,7 @@ async def test_wallbox_refresh_failed_connection_error( """Test Wallbox setup with connection error.""" await setup_integration(hass, entry) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED with requests_mock.Mocker() as mock_request: mock_request.get( @@ -127,7 +127,7 @@ async def test_wallbox_refresh_failed_connection_error( await wallbox.async_refresh() assert await hass.config_entries.async_unload(entry.entry_id) - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED async def test_wallbox_refresh_failed_read_only( @@ -136,7 +136,7 @@ async def test_wallbox_refresh_failed_read_only( """Test Wallbox setup for read-only user.""" await setup_integration_read_only(hass, entry) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) - assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/waqi/test_sensor.py b/tests/components/waqi/test_sensor.py index 328fe99330e..0825d65cc20 100644 --- a/tests/components/waqi/test_sensor.py +++ b/tests/components/waqi/test_sensor.py @@ -52,4 +52,4 @@ async def test_updating_failed( assert await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() - assert mock_config_entry.state == ConfigEntryState.SETUP_RETRY + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/weatherkit/test_setup.py b/tests/components/weatherkit/test_setup.py index c121f0cc5c1..f198a81b894 100644 --- a/tests/components/weatherkit/test_setup.py +++ b/tests/components/weatherkit/test_setup.py @@ -7,8 +7,8 @@ from apple_weatherkit.client import ( WeatherKitApiClientError, ) -from homeassistant import config_entries from homeassistant.components.weatherkit.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from . import EXAMPLE_CONFIG_DATA @@ -65,4 +65,4 @@ async def test_client_error_handling(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == config_entries.ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/webostv/test_init.py b/tests/components/webostv/test_init.py index 30af1428701..a2961a81a4e 100644 --- a/tests/components/webostv/test_init.py +++ b/tests/components/webostv/test_init.py @@ -18,7 +18,7 @@ async def test_reauth_setup_entry(hass: HomeAssistant, client, monkeypatch) -> N monkeypatch.setattr(client, "connect", Mock(side_effect=WebOsTvPairError)) entry = await setup_webostv(hass) - assert entry.state == ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -37,5 +37,5 @@ async def test_key_update_setup_entry(hass: HomeAssistant, client, monkeypatch) monkeypatch.setattr(client, "client_key", "new_key") entry = await setup_webostv(hass) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert entry.data[CONF_CLIENT_SECRET] == "new_key" diff --git a/tests/components/webostv/test_media_player.py b/tests/components/webostv/test_media_player.py index 2dff9477e50..6608c107599 100644 --- a/tests/components/webostv/test_media_player.py +++ b/tests/components/webostv/test_media_player.py @@ -794,12 +794,12 @@ async def test_reauth_reconnect(hass: HomeAssistant, client, monkeypatch) -> Non monkeypatch.setattr(client, "is_connected", Mock(return_value=False)) monkeypatch.setattr(client, "connect", Mock(side_effect=WebOsTvPairError)) - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=20)) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 52d0e86d828..e96f1c4f903 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -8,7 +8,7 @@ from unittest.mock import ANY, AsyncMock, Mock, patch import pytest import voluptuous as vol -from homeassistant import config_entries, loader +from homeassistant import loader from homeassistant.components.device_automation import toggle_entity from homeassistant.components.websocket_api import const from homeassistant.components.websocket_api.auth import ( @@ -17,6 +17,7 @@ from homeassistant.components.websocket_api.auth import ( TYPE_AUTH_REQUIRED, ) from homeassistant.components.websocket_api.const import FEATURE_COALESCE_MESSAGES, URL +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import SIGNAL_BOOTSTRAP_INTEGRATIONS from homeassistant.core import Context, HomeAssistant, State, SupportsResponse, callback from homeassistant.exceptions import HomeAssistantError, ServiceValidationError @@ -2433,7 +2434,7 @@ async def test_execute_script_with_dynamically_validated_action( ) config_entry = MockConfigEntry(domain="fake_integration", data={}) - config_entry.mock_state(hass, config_entries.ConfigEntryState.LOADED) + config_entry.mock_state(hass, ConfigEntryState.LOADED) config_entry.add_to_hass(hass) device_entry = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, diff --git a/tests/components/wiz/test_binary_sensor.py b/tests/components/wiz/test_binary_sensor.py index adfef066e16..d9e8d7170c7 100644 --- a/tests/components/wiz/test_binary_sensor.py +++ b/tests/components/wiz/test_binary_sensor.py @@ -1,8 +1,8 @@ """Tests for WiZ binary_sensor platform.""" -from homeassistant import config_entries from homeassistant.components import wiz from homeassistant.components.wiz.binary_sensor import OCCUPANCY_UNIQUE_ID +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_HOST, STATE_OFF, STATE_ON, STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -70,7 +70,7 @@ async def test_binary_sensor_restored_from_registry(hass: HomeAssistant) -> None await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == config_entries.ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED async def test_binary_sensor_never_created_no_error_on_unload( @@ -80,4 +80,4 @@ async def test_binary_sensor_never_created_no_error_on_unload( _, entry = await async_setup_integration(hass) await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == config_entries.ConfigEntryState.NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/wiz/test_config_flow.py b/tests/components/wiz/test_config_flow.py index c9ac4b023f7..c60e080f6d4 100644 --- a/tests/components/wiz/test_config_flow.py +++ b/tests/components/wiz/test_config_flow.py @@ -9,6 +9,7 @@ from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.components.wiz.config_flow import CONF_DEVICE from homeassistant.components.wiz.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -344,7 +345,7 @@ async def test_discovered_by_dhcp_or_integration_discovery_avoid_waiting_for_ret bulb.getMac = AsyncMock(side_effect=OSError) _, entry = await async_setup_integration(hass, wizlight=bulb) assert entry.data[CONF_HOST] == FAKE_IP - assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY bulb.getMac = AsyncMock(return_value=FAKE_MAC) with _patch_wizlight(): @@ -355,7 +356,7 @@ async def test_discovered_by_dhcp_or_integration_discovery_avoid_waiting_for_ret assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" - assert entry.state is config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED async def test_setup_via_discovery(hass: HomeAssistant) -> None: diff --git a/tests/components/wiz/test_init.py b/tests/components/wiz/test_init.py index d6813263fcc..3fa369c4d9d 100644 --- a/tests/components/wiz/test_init.py +++ b/tests/components/wiz/test_init.py @@ -3,8 +3,8 @@ import datetime from unittest.mock import AsyncMock, patch -from homeassistant import config_entries from homeassistant.components.wiz.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_FRIENDLY_NAME, CONF_HOST, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -28,21 +28,21 @@ async def test_setup_retry(hass: HomeAssistant) -> None: bulb = _mocked_wizlight(None, None, FAKE_SOCKET) bulb.getMac = AsyncMock(side_effect=OSError) _, entry = await async_setup_integration(hass, wizlight=bulb) - assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY bulb.getMac = AsyncMock(return_value=FAKE_MAC) with _patch_discovery(), _patch_wizlight(device=bulb): await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + datetime.timedelta(minutes=15)) await hass.async_block_till_done() - assert entry.state is config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED async def test_cleanup_on_shutdown(hass: HomeAssistant) -> None: """Test the socket is cleaned up on shutdown.""" bulb = _mocked_wizlight(None, None, FAKE_SOCKET) _, entry = await async_setup_integration(hass, wizlight=bulb) - assert entry.state is config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() bulb.async_close.assert_called_once() @@ -64,7 +64,7 @@ async def test_cleanup_on_failed_first_update(hass: HomeAssistant) -> None: ): await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() - assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY bulb.async_close.assert_called_once() @@ -73,14 +73,14 @@ async def test_wrong_device_now_has_our_ip(hass: HomeAssistant) -> None: bulb = _mocked_wizlight(None, None, FAKE_SOCKET) bulb.mac = "dddddddddddd" _, entry = await async_setup_integration(hass, wizlight=bulb) - assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_reload_on_title_change(hass: HomeAssistant) -> None: """Test the integration gets reloaded when the title is updated.""" bulb = _mocked_wizlight(None, None, FAKE_SOCKET) _, entry = await async_setup_integration(hass, wizlight=bulb) - assert entry.state is config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED await hass.async_block_till_done() with _patch_discovery(), _patch_wizlight(device=bulb): diff --git a/tests/components/workday/test_init.py b/tests/components/workday/test_init.py index e9de315e1d1..1e0c9cbebc6 100644 --- a/tests/components/workday/test_init.py +++ b/tests/components/workday/test_init.py @@ -6,7 +6,7 @@ from datetime import datetime from freezegun.api import FrozenDateTimeFactory -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.util.dt import UTC @@ -35,7 +35,7 @@ async def test_update_options( freezer.move_to(datetime(2023, 4, 12, 12, tzinfo=UTC)) # Monday entry = await init_integration(hass, TEST_CONFIG_WITH_PROVINCE) - assert entry.state == config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert entry.update_listeners is not None state = hass.states.get("binary_sensor.workday_sensor") assert state.state == "on" @@ -47,6 +47,6 @@ async def test_update_options( await hass.async_block_till_done() entry_check = hass.config_entries.async_get_entry("1") - assert entry_check.state == config_entries.ConfigEntryState.LOADED + assert entry_check.state is ConfigEntryState.LOADED state = hass.states.get("binary_sensor.workday_sensor") assert state.state == "off" diff --git a/tests/components/yolink/test_config_flow.py b/tests/components/yolink/test_config_flow.py index db12cecf296..f62bd3ac1ac 100644 --- a/tests/components/yolink/test_config_flow.py +++ b/tests/components/yolink/test_config_flow.py @@ -7,6 +7,7 @@ from yolink.const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN from homeassistant import config_entries, setup from homeassistant.components import application_credentials +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow @@ -108,7 +109,7 @@ async def test_full_flow( assert DOMAIN in hass.config.components entry = hass.config_entries.async_entries(DOMAIN)[0] - assert entry.state is config_entries.ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(mock_setup.mock_calls) == 1 diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index cb61be236bf..0c8414f458f 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -35,6 +35,7 @@ from homeassistant.config_entries import ( SOURCE_USB, SOURCE_USER, SOURCE_ZEROCONF, + ConfigEntryState, ) from homeassistant.const import CONF_SOURCE from homeassistant.core import HomeAssistant @@ -1552,7 +1553,7 @@ async def test_options_flow_defaults( mock_async_unload.assert_called_once_with(entry.entry_id) # Unload it ourselves - entry.mock_state(hass, config_entries.ConfigEntryState.NOT_LOADED) + entry.mock_state(hass, ConfigEntryState.NOT_LOADED) # Reconfigure ZHA assert result1["step_id"] == "prompt_migrate_or_reconfigure" @@ -1735,7 +1736,7 @@ async def test_options_flow_restarts_running_zha_if_cancelled( flow["flow_id"], user_input={} ) - entry.mock_state(hass, config_entries.ConfigEntryState.NOT_LOADED) + entry.mock_state(hass, ConfigEntryState.NOT_LOADED) assert result1["step_id"] == "prompt_migrate_or_reconfigure" result2 = await hass.config_entries.options.async_configure( @@ -1790,7 +1791,7 @@ async def test_options_flow_migration_reset_old_adapter( flow["flow_id"], user_input={} ) - entry.mock_state(hass, config_entries.ConfigEntryState.NOT_LOADED) + entry.mock_state(hass, ConfigEntryState.NOT_LOADED) assert result1["step_id"] == "prompt_migrate_or_reconfigure" result2 = await hass.config_entries.options.async_configure( diff --git a/tests/components/zha/test_radio_manager.py b/tests/components/zha/test_radio_manager.py index dbea454ecb0..0363821ac47 100644 --- a/tests/components/zha/test_radio_manager.py +++ b/tests/components/zha/test_radio_manager.py @@ -10,11 +10,11 @@ import zigpy.config from zigpy.config import CONF_DEVICE_PATH import zigpy.types -from homeassistant import config_entries from homeassistant.components.usb import UsbServiceInfo from homeassistant.components.zha import radio_manager from homeassistant.components.zha.core.const import DOMAIN, RadioType from homeassistant.components.zha.radio_manager import ProbeResult, ZhaRadioManager +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -224,7 +224,7 @@ async def test_migrate_matching_port_config_entry_not_loaded( title="Test", ) config_entry.add_to_hass(hass) - config_entry.mock_state(hass, config_entries.ConfigEntryState.SETUP_IN_PROGRESS) + config_entry.mock_state(hass, ConfigEntryState.SETUP_IN_PROGRESS) migration_data = { "new_discovery_info": { @@ -284,7 +284,7 @@ async def test_migrate_matching_port_retry( title="Test", ) config_entry.add_to_hass(hass) - config_entry.mock_state(hass, config_entries.ConfigEntryState.SETUP_IN_PROGRESS) + config_entry.mock_state(hass, ConfigEntryState.SETUP_IN_PROGRESS) migration_data = { "new_discovery_info": { @@ -389,7 +389,7 @@ async def test_migrate_initiate_failure( title="Test", ) config_entry.add_to_hass(hass) - config_entry.mock_state(hass, config_entries.ConfigEntryState.SETUP_IN_PROGRESS) + config_entry.mock_state(hass, ConfigEntryState.SETUP_IN_PROGRESS) migration_data = { "new_discovery_info": { diff --git a/tests/components/zha/test_repairs.py b/tests/components/zha/test_repairs.py index fea68be86cb..c254a9c15fe 100644 --- a/tests/components/zha/test_repairs.py +++ b/tests/components/zha/test_repairs.py @@ -154,7 +154,7 @@ async def test_multipan_firmware_repair( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR await hass.config_entries.async_unload(config_entry.entry_id) @@ -203,7 +203,7 @@ async def test_multipan_firmware_no_repair_on_probe_failure( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY await hass.config_entries.async_unload(config_entry.entry_id) @@ -241,7 +241,7 @@ async def test_multipan_firmware_retry_on_probe_ezsp( await hass.async_block_till_done() # The config entry state is `SETUP_RETRY`, not `SETUP_ERROR`! - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY await hass.config_entries.async_unload(config_entry.entry_id) @@ -308,7 +308,7 @@ async def test_inconsistent_settings_keep_new( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR await hass.config_entries.async_unload(config_entry.entry_id) @@ -388,7 +388,7 @@ async def test_inconsistent_settings_restore_old( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR await hass.config_entries.async_unload(config_entry.entry_id) diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 822302a9940..85611262214 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -673,7 +673,7 @@ async def test_addon_options_changed( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.LOADED assert entry.data["usb_path"] == new_device assert entry.data["s0_legacy_key"] == new_s0_legacy_key assert entry.data["s2_access_control_key"] == new_s2_access_control_key From f142e902df1853e02c9c9948ad2d81cbc8451298 Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Fri, 5 Apr 2024 18:53:28 +0200 Subject: [PATCH 300/967] SIngle entry for Fastdotcom (#114963) --- homeassistant/components/fastdotcom/config_flow.py | 3 --- homeassistant/components/fastdotcom/manifest.json | 3 ++- homeassistant/generated/integrations.json | 3 ++- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/fastdotcom/config_flow.py b/homeassistant/components/fastdotcom/config_flow.py index ec62c86d787..36b6f81ae5b 100644 --- a/homeassistant/components/fastdotcom/config_flow.py +++ b/homeassistant/components/fastdotcom/config_flow.py @@ -20,9 +20,6 @@ class FastdotcomConfigFlow(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle the initial step.""" - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - if user_input is not None: return self.async_create_entry(title=DEFAULT_NAME, data={}) diff --git a/homeassistant/components/fastdotcom/manifest.json b/homeassistant/components/fastdotcom/manifest.json index 311f3000b1e..9e2e077858c 100644 --- a/homeassistant/components/fastdotcom/manifest.json +++ b/homeassistant/components/fastdotcom/manifest.json @@ -7,5 +7,6 @@ "iot_class": "cloud_polling", "loggers": ["fastdotcom"], "quality_scale": "gold", - "requirements": ["fastdotcom==0.0.3"] + "requirements": ["fastdotcom==0.0.3"], + "single_config_entry": true } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index a0cf46d7f1d..4792046411f 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1760,7 +1760,8 @@ "name": "Fast.com", "integration_type": "hub", "config_flow": true, - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "single_config_entry": true }, "feedreader": { "name": "Feedreader", From e1575678ed6721de5878d6c7f16c3e2cb9cbef4b Mon Sep 17 00:00:00 2001 From: Bengt Sirbelius <95758234+bengtsir@users.noreply.github.com> Date: Fri, 5 Apr 2024 19:06:18 +0200 Subject: [PATCH 301/967] Add new OUI for Axis products (#114923) * Add new OUI for Axis products * Run hassfest --------- Co-authored-by: Robert Svensson --- homeassistant/components/axis/manifest.json | 10 ++++++++++ homeassistant/generated/dhcp.py | 5 +++++ homeassistant/generated/zeroconf.py | 6 ++++++ 3 files changed, 21 insertions(+) diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index 1065783d957..bbea7954be1 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -19,6 +19,10 @@ { "hostname": "axis-b8a44f*", "macaddress": "B8A44F*" + }, + { + "hostname": "axis-e82725*", + "macaddress": "E82725*" } ], "documentation": "https://www.home-assistant.io/integrations/axis", @@ -50,6 +54,12 @@ "properties": { "macaddress": "b8a44f*" } + }, + { + "type": "_axis-video._tcp.local.", + "properties": { + "macaddress": "e82725*" + } } ] } diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 4f9f822e85e..9c5d25a7f22 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -58,6 +58,11 @@ DHCP: list[dict[str, str | bool]] = [ "hostname": "axis-b8a44f*", "macaddress": "B8A44F*", }, + { + "domain": "axis", + "hostname": "axis-e82725*", + "macaddress": "E82725*", + }, { "domain": "blink", "hostname": "blink*", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 060084209fd..68373fa7fe9 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -342,6 +342,12 @@ ZEROCONF = { "macaddress": "b8a44f*", }, }, + { + "domain": "axis", + "properties": { + "macaddress": "e82725*", + }, + }, { "domain": "doorbird", "properties": { From 5194215074ccb7e552d34b8f99956f255a38d9ec Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 5 Apr 2024 19:45:14 +0200 Subject: [PATCH 302/967] Allow single entry in Downloader (#114957) --- homeassistant/components/downloader/config_flow.py | 6 ------ homeassistant/components/downloader/manifest.json | 3 ++- homeassistant/generated/integrations.json | 3 ++- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/downloader/config_flow.py b/homeassistant/components/downloader/config_flow.py index 94b33f4e93f..27101630599 100644 --- a/homeassistant/components/downloader/config_flow.py +++ b/homeassistant/components/downloader/config_flow.py @@ -25,9 +25,6 @@ class DownloaderConfigFlow(ConfigFlow, domain=DOMAIN): """Handle the initial step.""" errors: dict[str, str] = {} - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - if user_input is not None: try: await self._validate_input(user_input) @@ -48,9 +45,6 @@ class DownloaderConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult: """Handle a flow initiated by configuration file.""" - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - try: await self._validate_input(user_input) except DirectoryDoesNotExist: diff --git a/homeassistant/components/downloader/manifest.json b/homeassistant/components/downloader/manifest.json index 876404be889..85434069b87 100644 --- a/homeassistant/components/downloader/manifest.json +++ b/homeassistant/components/downloader/manifest.json @@ -4,5 +4,6 @@ "codeowners": ["@erwindouna"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/downloader", - "quality_scale": "internal" + "quality_scale": "internal", + "single_config_entry": true } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 4792046411f..e6c0588979c 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1299,7 +1299,8 @@ "downloader": { "name": "Downloader", "integration_type": "hub", - "config_flow": true + "config_flow": true, + "single_config_entry": true }, "dremel_3d_printer": { "name": "Dremel 3D Printer", From 850361d1da95d91180becac2068eb7cfbccc0a5d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 5 Apr 2024 19:45:24 +0200 Subject: [PATCH 303/967] Fix Snapcast Config flow (#114952) --- homeassistant/components/snapcast/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/snapcast/config_flow.py b/homeassistant/components/snapcast/config_flow.py index c9f69c48ab5..b37921fd374 100644 --- a/homeassistant/components/snapcast/config_flow.py +++ b/homeassistant/components/snapcast/config_flow.py @@ -45,7 +45,7 @@ class SnapcastConfigFlow(ConfigFlow, domain=DOMAIN): except OSError: errors["base"] = "cannot_connect" else: - await client.stop() + client.stop() return self.async_create_entry(title=DEFAULT_TITLE, data=user_input) return self.async_show_form( step_id="user", data_schema=SNAPCAST_SCHEMA, errors=errors From aeaed8357875e62f6353cab29417be380b05c8c5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 5 Apr 2024 08:14:21 -1000 Subject: [PATCH 304/967] Start async_schedule_update_ha_state task eagerly (#114704) --- homeassistant/helpers/entity.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index eee35fa4cca..66bbc744b70 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -1234,6 +1234,7 @@ class Entity( self.hass.async_create_task( self.async_update_ha_state(force_refresh), f"Entity schedule update ha state {self.entity_id}", + eager_start=True, ) else: self.async_write_ha_state() From 0214511b383204ef42ffaa930b61438a09a28a86 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 5 Apr 2024 21:38:11 +0200 Subject: [PATCH 305/967] Make config flow imports consistent (#114962) * Make config flow imports consistent * Fix --- homeassistant/components/airtouch5/config_flow.py | 6 +++--- homeassistant/components/aprilaire/config_flow.py | 6 +++--- homeassistant/components/bring/config_flow.py | 6 +++--- homeassistant/components/elvia/config_flow.py | 10 +++++----- homeassistant/components/eq3btsmart/config_flow.py | 5 ++--- homeassistant/components/fyta/config_flow.py | 6 +++--- homeassistant/components/huum/config_flow.py | 6 +++--- homeassistant/components/lupusec/config_flow.py | 10 ++++------ homeassistant/components/microbees/config_flow.py | 9 ++++----- homeassistant/components/rabbitair/config_flow.py | 8 ++++---- homeassistant/components/romy/config_flow.py | 14 +++++++------- homeassistant/components/rova/config_flow.py | 10 ++++------ .../components/seventeentrack/config_flow.py | 7 +++---- homeassistant/components/teslemetry/config_flow.py | 6 +++--- .../components/traccar_server/config_flow.py | 7 ++++--- homeassistant/components/velux/config_flow.py | 10 ++++------ .../components/weatherflow_cloud/config_flow.py | 8 ++++---- tests/components/aprilaire/test_config_flow.py | 8 ++++---- 18 files changed, 67 insertions(+), 75 deletions(-) diff --git a/homeassistant/components/airtouch5/config_flow.py b/homeassistant/components/airtouch5/config_flow.py index 65755350b47..3c4671cf54e 100644 --- a/homeassistant/components/airtouch5/config_flow.py +++ b/homeassistant/components/airtouch5/config_flow.py @@ -8,7 +8,7 @@ from typing import Any from airtouch5py.airtouch5_simple_client import Airtouch5SimpleClient import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST from .const import DOMAIN @@ -18,14 +18,14 @@ _LOGGER = logging.getLogger(__name__) STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str}) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class AirTouch5ConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Airtouch 5.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> config_entries.ConfigFlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] | None = None if user_input is not None: diff --git a/homeassistant/components/aprilaire/config_flow.py b/homeassistant/components/aprilaire/config_flow.py index 14437e5f3f2..4acc1b9dd9e 100644 --- a/homeassistant/components/aprilaire/config_flow.py +++ b/homeassistant/components/aprilaire/config_flow.py @@ -8,7 +8,7 @@ from typing import Any from pyaprilaire.const import Attribute import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PORT import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import format_mac @@ -26,14 +26,14 @@ STEP_USER_DATA_SCHEMA = vol.Schema( _LOGGER = logging.getLogger(__name__) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class AprilaireConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Aprilaire.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> config_entries.ConfigFlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: diff --git a/homeassistant/components/bring/config_flow.py b/homeassistant/components/bring/config_flow.py index 0b423f5af36..1fbddeb7bfe 100644 --- a/homeassistant/components/bring/config_flow.py +++ b/homeassistant/components/bring/config_flow.py @@ -9,7 +9,7 @@ from bring_api.bring import Bring from bring_api.exceptions import BringAuthException, BringRequestException import voluptuous as vol -from homeassistant import config_entries +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 homeassistant.helpers.selector import ( @@ -38,14 +38,14 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class BringConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Bring!.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> config_entries.ConfigFlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} if user_input is not None: diff --git a/homeassistant/components/elvia/config_flow.py b/homeassistant/components/elvia/config_flow.py index 4cf311e780e..2db6e4bb2b5 100644 --- a/homeassistant/components/elvia/config_flow.py +++ b/homeassistant/components/elvia/config_flow.py @@ -8,14 +8,14 @@ from typing import TYPE_CHECKING, Any from elvia import Elvia, error as ElviaError import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_TOKEN from homeassistant.util import dt as dt_util from .const import CONF_METERING_POINT_ID, DOMAIN, LOGGER -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class ElviaConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Elvia.""" def __init__(self) -> None: @@ -26,7 +26,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None, - ) -> config_entries.ConfigFlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} if user_input is not None: @@ -75,7 +75,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_select_meter( self, user_input: dict[str, Any] | None = None - ) -> config_entries.ConfigFlowResult: + ) -> ConfigFlowResult: """Handle selecting a metering point ID.""" if TYPE_CHECKING: assert self._metering_point_ids is not None @@ -103,7 +103,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, api_token: str, metering_point_id: str, - ) -> config_entries.ConfigFlowResult: + ) -> ConfigFlowResult: """Store metering point ID and API token.""" if (await self.async_set_unique_id(metering_point_id)) is not None: return self.async_abort( diff --git a/homeassistant/components/eq3btsmart/config_flow.py b/homeassistant/components/eq3btsmart/config_flow.py index 228127d7705..4dccd8a572a 100644 --- a/homeassistant/components/eq3btsmart/config_flow.py +++ b/homeassistant/components/eq3btsmart/config_flow.py @@ -2,9 +2,8 @@ from typing import Any -from homeassistant import config_entries from homeassistant.components.bluetooth import BluetoothServiceInfoBleak -from homeassistant.config_entries import ConfigFlowResult +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_MAC from homeassistant.helpers.device_registry import format_mac from homeassistant.util import slugify @@ -13,7 +12,7 @@ from .const import DOMAIN from .schemas import SCHEMA_MAC -class EQ3ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class EQ3ConfigFlow(ConfigFlow, domain=DOMAIN): """Config flow for eQ-3 Bluetooth Smart thermostats.""" def __init__(self) -> None: diff --git a/homeassistant/components/fyta/config_flow.py b/homeassistant/components/fyta/config_flow.py index 67e46f8125e..8419352dc44 100644 --- a/homeassistant/components/fyta/config_flow.py +++ b/homeassistant/components/fyta/config_flow.py @@ -13,7 +13,7 @@ from fyta_cli.fyta_exceptions import ( ) import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from .const import DOMAIN @@ -26,14 +26,14 @@ DATA_SCHEMA = vol.Schema( ) -class FytaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class FytaConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Fyta.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> config_entries.ConfigFlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors = {} diff --git a/homeassistant/components/huum/config_flow.py b/homeassistant/components/huum/config_flow.py index e2ea2a7dbe1..5de94260a4b 100644 --- a/homeassistant/components/huum/config_flow.py +++ b/homeassistant/components/huum/config_flow.py @@ -9,7 +9,7 @@ from huum.exceptions import Forbidden, NotAuthenticated from huum.huum import Huum import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -25,14 +25,14 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) -class HuumConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class HuumConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for huum.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> config_entries.ConfigFlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors = {} if user_input is not None: diff --git a/homeassistant/components/lupusec/config_flow.py b/homeassistant/components/lupusec/config_flow.py index c3fe7295266..3af823e4fa1 100644 --- a/homeassistant/components/lupusec/config_flow.py +++ b/homeassistant/components/lupusec/config_flow.py @@ -7,7 +7,7 @@ from typing import Any import lupupy import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import ( CONF_HOST, CONF_IP_ADDRESS, @@ -31,12 +31,12 @@ DATA_SCHEMA = vol.Schema( ) -class LupusecConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class LupusecConfigFlowHandler(ConfigFlow, domain=DOMAIN): """Lupusec config flow.""" async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> config_entries.ConfigFlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" errors = {} @@ -66,9 +66,7 @@ class LupusecConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) - async def async_step_import( - self, user_input: dict[str, Any] - ) -> config_entries.ConfigFlowResult: + async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult: """Import the yaml config.""" self._async_abort_entries_match( { diff --git a/homeassistant/components/microbees/config_flow.py b/homeassistant/components/microbees/config_flow.py index dedb6c1f374..c54f8939145 100644 --- a/homeassistant/components/microbees/config_flow.py +++ b/homeassistant/components/microbees/config_flow.py @@ -7,6 +7,7 @@ from typing import Any from microBeesPy import MicroBees, MicroBeesException from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlowResult from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow @@ -32,9 +33,7 @@ class OAuth2FlowHandler( scopes = ["read", "write"] return {"scope": " ".join(scopes)} - async def async_oauth_create_entry( - self, data: dict[str, Any] - ) -> config_entries.ConfigFlowResult: + async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult: """Create an oauth config entry or update existing entry for reauth.""" microbees = MicroBees( @@ -65,7 +64,7 @@ class OAuth2FlowHandler( async def async_step_reauth( self, entry_data: Mapping[str, Any] - ) -> config_entries.ConfigFlowResult: + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" self.reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -74,7 +73,7 @@ class OAuth2FlowHandler( async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> config_entries.ConfigFlowResult: + ) -> ConfigFlowResult: """Confirm reauth dialog.""" if user_input is None: return self.async_show_form(step_id="reauth_confirm") diff --git a/homeassistant/components/rabbitair/config_flow.py b/homeassistant/components/rabbitair/config_flow.py index 30dfac93236..6bf48995412 100644 --- a/homeassistant/components/rabbitair/config_flow.py +++ b/homeassistant/components/rabbitair/config_flow.py @@ -8,8 +8,8 @@ from typing import Any from rabbitair import UdpClient import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import zeroconf +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_MAC from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -49,7 +49,7 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, return {"mac": info.mac} -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class RabbitAirConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Rabbit Air.""" VERSION = 1 @@ -58,7 +58,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> config_entries.ConfigFlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors = {} @@ -100,7 +100,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> config_entries.ConfigFlowResult: + ) -> ConfigFlowResult: """Handle zeroconf discovery.""" mac = dr.format_mac(discovery_info.properties["id"]) await self.async_set_unique_id(mac) diff --git a/homeassistant/components/romy/config_flow.py b/homeassistant/components/romy/config_flow.py index bccae667695..e571ff41c9a 100644 --- a/homeassistant/components/romy/config_flow.py +++ b/homeassistant/components/romy/config_flow.py @@ -5,15 +5,15 @@ from __future__ import annotations import romy import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import zeroconf +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PASSWORD import homeassistant.helpers.config_validation as cv from .const import DOMAIN, LOGGER -class RomyConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class RomyConfigFlow(ConfigFlow, domain=DOMAIN): """Handle config flow for ROMY.""" VERSION = 1 @@ -26,7 +26,7 @@ class RomyConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, str] | None = None - ) -> config_entries.ConfigFlowResult: + ) -> ConfigFlowResult: """Handle the user step.""" errors: dict[str, str] = {} @@ -59,7 +59,7 @@ class RomyConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_password( self, user_input: dict[str, str] | None = None - ) -> config_entries.ConfigFlowResult: + ) -> ConfigFlowResult: """Unlock the robots local http interface with password.""" errors: dict[str, str] = {} @@ -85,7 +85,7 @@ class RomyConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> config_entries.ConfigFlowResult: + ) -> ConfigFlowResult: """Handle zeroconf discovery.""" LOGGER.debug("Zeroconf discovery_info: %s", discovery_info) @@ -125,7 +125,7 @@ class RomyConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf_confirm( self, user_input: dict[str, str] | None = None - ) -> config_entries.ConfigFlowResult: + ) -> ConfigFlowResult: """Handle a confirmation flow initiated by zeroconf.""" if user_input is None: return self.async_show_form( @@ -137,7 +137,7 @@ class RomyConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return await self._async_step_finish_config() - async def _async_step_finish_config(self) -> config_entries.ConfigFlowResult: + async def _async_step_finish_config(self) -> ConfigFlowResult: """Finish the configuration setup.""" return self.async_create_entry( title=self.robot_name_given_by_user, diff --git a/homeassistant/components/rova/config_flow.py b/homeassistant/components/rova/config_flow.py index d618681783e..e5e3a31b8af 100644 --- a/homeassistant/components/rova/config_flow.py +++ b/homeassistant/components/rova/config_flow.py @@ -6,19 +6,19 @@ from requests.exceptions import ConnectTimeout, HTTPError from rova.rova import Rova import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from .const import CONF_HOUSE_NUMBER, CONF_HOUSE_NUMBER_SUFFIX, CONF_ZIP_CODE, DOMAIN -class RovaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class RovaConfigFlow(ConfigFlow, domain=DOMAIN): """Handle Rova config flow.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> config_entries.ConfigFlowResult: + ) -> ConfigFlowResult: """Step when user initializes a integration.""" errors: dict[str, str] = {} @@ -60,9 +60,7 @@ class RovaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_import( - self, user_input: dict[str, Any] - ) -> config_entries.ConfigFlowResult: + async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult: """Import the yaml config.""" zip_code = user_input[CONF_ZIP_CODE] number = user_input[CONF_HOUSE_NUMBER] diff --git a/homeassistant/components/seventeentrack/config_flow.py b/homeassistant/components/seventeentrack/config_flow.py index ae31e1962d7..f54e7e94ac2 100644 --- a/homeassistant/components/seventeentrack/config_flow.py +++ b/homeassistant/components/seventeentrack/config_flow.py @@ -9,8 +9,7 @@ from py17track import Client as SeventeenTrackClient from py17track.errors import SeventeenTrackError import voluptuous as vol -from homeassistant import config_entries -from homeassistant.config_entries import ConfigFlowResult +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback from homeassistant.helpers import aiohttp_client @@ -47,7 +46,7 @@ USER_SCHEMA = vol.Schema( ) -class SeventeenTrackConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class SeventeenTrackConfigFlow(ConfigFlow, domain=DOMAIN): """17track config flow.""" VERSION = 1 @@ -55,7 +54,7 @@ class SeventeenTrackConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> SchemaOptionsFlowHandler: """Get options flow for this handler.""" return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW) diff --git a/homeassistant/components/teslemetry/config_flow.py b/homeassistant/components/teslemetry/config_flow.py index f7fc5bbf805..0803688b1ca 100644 --- a/homeassistant/components/teslemetry/config_flow.py +++ b/homeassistant/components/teslemetry/config_flow.py @@ -14,7 +14,7 @@ from tesla_fleet_api.exceptions import ( ) import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -27,14 +27,14 @@ DESCRIPTION_PLACEHOLDERS = { } -class TeslemetryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class TeslemetryConfigFlow(ConfigFlow, domain=DOMAIN): """Config Teslemetry API connection.""" VERSION = 1 async def async_step_user( self, user_input: Mapping[str, Any] | None = None - ) -> config_entries.ConfigFlowResult: + ) -> ConfigFlowResult: """Get configuration from the user.""" errors: dict[str, str] = {} if user_input: diff --git a/homeassistant/components/traccar_server/config_flow.py b/homeassistant/components/traccar_server/config_flow.py index 0fa97c8100e..678bcc461e7 100644 --- a/homeassistant/components/traccar_server/config_flow.py +++ b/homeassistant/components/traccar_server/config_flow.py @@ -9,6 +9,7 @@ from pytraccar import ApiClient, ServerModel, TraccarException import voluptuous as vol from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -111,7 +112,7 @@ OPTIONS_FLOW = { } -class TraccarServerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class TraccarServerConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Traccar Server.""" async def _get_server_info(self, user_input: dict[str, Any]) -> ServerModel: @@ -130,7 +131,7 @@ class TraccarServerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None, - ) -> config_entries.ConfigFlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} if user_input is not None: @@ -162,7 +163,7 @@ class TraccarServerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import( self, import_info: Mapping[str, Any] - ) -> config_entries.ConfigFlowResult: + ) -> ConfigFlowResult: """Import an entry.""" configured_port = str(import_info[CONF_PORT]) self._async_abort_entries_match( diff --git a/homeassistant/components/velux/config_flow.py b/homeassistant/components/velux/config_flow.py index da6502b86da..679af4bd20a 100644 --- a/homeassistant/components/velux/config_flow.py +++ b/homeassistant/components/velux/config_flow.py @@ -5,7 +5,7 @@ from typing import Any from pyvlx import PyVLX, PyVLXException import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PASSWORD from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN import homeassistant.helpers.config_validation as cv @@ -21,12 +21,10 @@ DATA_SCHEMA = vol.Schema( ) -class VeluxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class VeluxConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for velux.""" - async def async_step_import( - self, config: dict[str, Any] - ) -> config_entries.ConfigFlowResult: + async def async_step_import(self, config: dict[str, Any]) -> ConfigFlowResult: """Import a config entry.""" def create_repair(error: str | None = None) -> None: @@ -81,7 +79,7 @@ class VeluxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, str] | None = None - ) -> config_entries.ConfigFlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} diff --git a/homeassistant/components/weatherflow_cloud/config_flow.py b/homeassistant/components/weatherflow_cloud/config_flow.py index 6e6212042e1..4c905a8451e 100644 --- a/homeassistant/components/weatherflow_cloud/config_flow.py +++ b/homeassistant/components/weatherflow_cloud/config_flow.py @@ -9,7 +9,7 @@ from aiohttp import ClientResponseError import voluptuous as vol from weatherflow4py.api import WeatherFlowRestAPI -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_TOKEN from .const import DOMAIN @@ -27,14 +27,14 @@ async def _validate_api_token(api_token: str) -> dict[str, Any]: return {} -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class WeatherFlowCloudConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for WeatherFlowCloud.""" VERSION = 1 async def async_step_reauth( self, user_input: Mapping[str, Any] - ) -> config_entries.ConfigFlowResult: + ) -> ConfigFlowResult: """Handle a flow for reauth.""" errors = {} @@ -60,7 +60,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> config_entries.ConfigFlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" errors = {} diff --git a/tests/components/aprilaire/test_config_flow.py b/tests/components/aprilaire/test_config_flow.py index 6508379665b..c9cba2b3fd6 100644 --- a/tests/components/aprilaire/test_config_flow.py +++ b/tests/components/aprilaire/test_config_flow.py @@ -8,7 +8,7 @@ import pytest from homeassistant.components.aprilaire.config_flow import ( STEP_USER_DATA_SCHEMA, - ConfigFlow, + AprilaireConfigFlow, ) from homeassistant.core import HomeAssistant @@ -24,7 +24,7 @@ async def test_user_input_step() -> None: show_form_mock = Mock() - config_flow = ConfigFlow() + config_flow = AprilaireConfigFlow() config_flow.async_show_form = show_form_mock await config_flow.async_step_user(None) @@ -41,7 +41,7 @@ async def test_config_flow_invalid_data(client: AprilaireClient) -> None: set_unique_id_mock = AsyncMock() async_abort_entries_match_mock = Mock() - config_flow = ConfigFlow() + config_flow = AprilaireConfigFlow() config_flow.async_show_form = show_form_mock config_flow.async_set_unique_id = set_unique_id_mock config_flow._async_abort_entries_match = async_abort_entries_match_mock @@ -77,7 +77,7 @@ async def test_config_flow_data(client: AprilaireClient, hass: HomeAssistant) -> abort_if_unique_id_configured_mock = Mock() create_entry_mock = Mock() - config_flow = ConfigFlow() + config_flow = AprilaireConfigFlow() config_flow.hass = hass config_flow.async_show_form = show_form_mock config_flow.async_set_unique_id = set_unique_id_mock From c9ce848b4b65f4cf9771d5716adc4fb97c5a3d44 Mon Sep 17 00:00:00 2001 From: Brent Petit Date: Fri, 5 Apr 2024 14:54:40 -0500 Subject: [PATCH 306/967] Add current_humidity to Ecobee humidifier (#114753) * Ecobee: add current_humidity to humidifier * Ecobee: Add test for current_humidity property * Update current_humidity handling in climate and humidifier entity to support the ecobee API not returning actualHumidity, which is an optional value. Also updated tests to add a thermostat which covers a non-populated humidity. In passing, set up the new test thermostat to cover a missing condition where the code doens't recognize the ecobee model number. This gets ecobee humidifier test coverage to 100% --- homeassistant/components/ecobee/climate.py | 5 +- homeassistant/components/ecobee/humidifier.py | 8 ++ .../ecobee/fixtures/ecobee-data.json | 75 +++++++++++++++++++ tests/components/ecobee/test_humidifier.py | 3 + 4 files changed, 90 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index 0df8a42c566..e341f4176ad 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -509,7 +509,10 @@ class Thermostat(ClimateEntity): @property def current_humidity(self) -> int | None: """Return the current humidity.""" - return self.thermostat["runtime"]["actualHumidity"] + try: + return int(self.thermostat["runtime"]["actualHumidity"]) + except KeyError: + return None @property def hvac_action(self): diff --git a/homeassistant/components/ecobee/humidifier.py b/homeassistant/components/ecobee/humidifier.py index 0de7de2e803..d9616383ab6 100644 --- a/homeassistant/components/ecobee/humidifier.py +++ b/homeassistant/components/ecobee/humidifier.py @@ -110,6 +110,14 @@ class EcobeeHumidifier(HumidifierEntity): """Return the desired humidity set point.""" return int(self.thermostat["runtime"]["desiredHumidity"]) + @property + def current_humidity(self) -> int | None: + """Return the current humidity.""" + try: + return int(self.thermostat["runtime"]["actualHumidity"]) + except KeyError: + return None + def set_mode(self, mode): """Set humidifier mode (auto, off, manual).""" if mode.lower() not in (self.available_modes): diff --git a/tests/components/ecobee/fixtures/ecobee-data.json b/tests/components/ecobee/fixtures/ecobee-data.json index d9406c20c3b..d8621bd8c4b 100644 --- a/tests/components/ecobee/fixtures/ecobee-data.json +++ b/tests/components/ecobee/fixtures/ecobee-data.json @@ -139,6 +139,81 @@ ] } ] + }, + { + "identifier": 8675307, + "name": "unknownEcobeeName", + "modelNumber": "unknownEcobeeModel", + "program": { + "climates": [ + { + "name": "Climate1", + "climateRef": "c1" + }, + { + "name": "Climate2", + "climateRef": "c2" + } + ], + "currentClimateRef": "c1" + }, + "runtime": { + "connected": true, + "actualTemperature": 300, + "desiredHeat": 400, + "desiredCool": 200, + "desiredFanMode": "on", + "desiredHumidity": 40 + }, + "settings": { + "hvacMode": "auto", + "heatStages": 1, + "coolStages": 1, + "fanMinOnTime": 10, + "heatCoolMinDelta": 50, + "holdAction": "nextTransition", + "ventilatorType": "none", + "ventilatorMinOnTimeHome": 20, + "ventilatorMinOnTimeAway": 10, + "isVentilatorTimerOn": false, + "hasHumidifier": true, + "humidifierMode": "manual", + "humidity": "30" + }, + "equipmentStatus": "fan", + "events": [ + { + "name": "Event1", + "running": true, + "type": "hold", + "holdClimateRef": "away", + "startDate": "2022-02-02", + "startTime": "11:00:00", + "endDate": "2022-01-01", + "endTime": "10:00:00" + } + ], + "remoteSensors": [ + { + "id": "rs:100", + "name": "Remote Sensor 1", + "type": "ecobee3_remote_sensor", + "code": "WKRP", + "inUse": false, + "capability": [ + { + "id": "1", + "type": "temperature", + "value": "782" + }, + { + "id": "2", + "type": "occupancy", + "value": "false" + } + ] + } + ] } ] } diff --git a/tests/components/ecobee/test_humidifier.py b/tests/components/ecobee/test_humidifier.py index 36b52c9c357..f35a7dc9237 100644 --- a/tests/components/ecobee/test_humidifier.py +++ b/tests/components/ecobee/test_humidifier.py @@ -7,6 +7,7 @@ import pytest from homeassistant.components.ecobee.humidifier import MODE_MANUAL, MODE_OFF from homeassistant.components.humidifier import ( ATTR_AVAILABLE_MODES, + ATTR_CURRENT_HUMIDITY, ATTR_HUMIDITY, ATTR_MAX_HUMIDITY, ATTR_MIN_HUMIDITY, @@ -43,6 +44,8 @@ async def test_attributes(hass: HomeAssistant) -> None: state = hass.states.get(DEVICE_ID) assert state.state == STATE_ON + if state.attributes.get(ATTR_CURRENT_HUMIDITY): + assert state.attributes.get(ATTR_CURRENT_HUMIDITY) == 15 assert state.attributes.get(ATTR_MIN_HUMIDITY) == DEFAULT_MIN_HUMIDITY assert state.attributes.get(ATTR_MAX_HUMIDITY) == DEFAULT_MAX_HUMIDITY assert state.attributes.get(ATTR_HUMIDITY) == 40 From d25ac06326bbdf842293b006f136adc214b18742 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 5 Apr 2024 15:59:11 -1000 Subject: [PATCH 307/967] Run storage final write listener immediately (#114976) This one should not need a call_soon --- homeassistant/helpers/storage.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 93594875ac2..60e464d3985 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -479,7 +479,9 @@ class Store(Generic[_T]): """Ensure that we write if we quit before delay has passed.""" if self._unsub_final_write_listener is None: self._unsub_final_write_listener = self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_FINAL_WRITE, self._async_callback_final_write + EVENT_HOMEASSISTANT_FINAL_WRITE, + self._async_callback_final_write, + run_immediately=True, ) @callback From 00db97a765460f2961d7823a0ed9ec83f38e0bdd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 5 Apr 2024 15:59:27 -1000 Subject: [PATCH 308/967] Run device_registry stop listener immediately (#114978) --- 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 2c160262c50..aa172c7e35b 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -1259,7 +1259,9 @@ def async_setup_cleanup(hass: HomeAssistant, dev_reg: DeviceRegistry) -> None: """Cancel debounced cleanup.""" debounced_cleanup.async_cancel() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _on_homeassistant_stop) + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, _on_homeassistant_stop, run_immediately=True + ) def _normalize_connections(connections: set[tuple[str, str]]) -> set[tuple[str, str]]: From bf5cf382dc083fc5eb32d09d527a52a0675f6ef7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 5 Apr 2024 16:00:09 -1000 Subject: [PATCH 309/967] Avoid useless stat() syscalls for every logger record (#114987) * Avoid useless stat() syscalls for every logger record shouldRollover will always return False since we do not use maxBytes as we are only using RotatingFileHandler for the backupCount option. Since every log record will stat the log file to see if it exists and if its a normal file, we can override the shouldRollover to reduce the logging overhead quite a bit https://github.com/python/cpython/blob/1d3225ae056245da75e4a443ccafcc8f4f982cf2/Lib/logging/handlers.py#L189 * assert False is False --- homeassistant/bootstrap.py | 15 ++++++++++++++- tests/test_bootstrap.py | 10 ++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 373c5c0f38c..afb364e6d2f 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -574,7 +574,7 @@ def async_enable_logging( err_log_path, when="midnight", backupCount=log_rotate_days ) else: - err_handler = logging.handlers.RotatingFileHandler( + err_handler = _RotatingFileHandlerWithoutShouldRollOver( err_log_path, backupCount=1 ) @@ -598,6 +598,19 @@ def async_enable_logging( async_activate_log_queue_handler(hass) +class _RotatingFileHandlerWithoutShouldRollOver(logging.handlers.RotatingFileHandler): + """RotatingFileHandler that does not check if it should roll over on every log.""" + + def shouldRollover(self, record: logging.LogRecord) -> bool: + """Never roll over. + + The shouldRollover check is expensive because it has to stat + the log file for every log record. Since we do not set maxBytes + the result of this check is always False. + """ + return False + + async def async_mount_local_lib_path(config_dir: str) -> str: """Add local library to Python Path. diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index de82aba9911..12eb52c06f4 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -1398,3 +1398,13 @@ async def test_setup_does_base_platforms_first(hass: HomeAssistant) -> None: # only that they are setup before other integrations. assert set(order[1:3]) == {"sensor", "binary_sensor"} assert order[3:] == ["root", "first_dep", "second_dep"] + + +def test_should_rollover_is_always_false(): + """Test that shouldRollover always returns False.""" + assert ( + bootstrap._RotatingFileHandlerWithoutShouldRollOver( + "any.log", delay=True + ).shouldRollover(Mock()) + is False + ) From fb98a6f02661b3397ddbdc18dd3468cc281825a5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 5 Apr 2024 16:14:20 -1000 Subject: [PATCH 310/967] Make run_immediately the default for core EventBus listeners (#113752) * DNM: Make run_immediately the default for listeners This is a test to see how much progress we have made twords this goal https://github.com/home-assistant/core/pull/113727#issuecomment-2004587947 * fix shutdown * Revert "fix shutdown" This reverts commit a8969d7db9fed10040cb8b7b25459dc9d812eb9c. * set false since it break utility meter tests * one more * fix rfxtrx test * test needs to be explict now * fix matrix * fail sooner --- homeassistant/components/matrix/__init__.py | 4 +++- homeassistant/components/rfxtrx/__init__.py | 4 +++- homeassistant/core.py | 4 ++-- homeassistant/helpers/event.py | 5 ++++- homeassistant/helpers/start.py | 4 +++- tests/common.py | 4 +++- tests/components/automation/test_init.py | 4 +++- tests/test_core.py | 6 ++++-- 8 files changed, 25 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/matrix/__init__.py b/homeassistant/components/matrix/__init__.py index b8f1ec08fe0..a283ba20dcb 100644 --- a/homeassistant/components/matrix/__init__.py +++ b/homeassistant/components/matrix/__init__.py @@ -219,7 +219,9 @@ class MatrixBot: loop_sleep_time=1_000, ) # milliseconds. - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, handle_startup) + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, handle_startup, run_immediately=False + ) def _load_commands(self, commands: list[ConfigCommand]) -> None: for command in commands: diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 6f0e5932adc..b40e8d921ed 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -280,7 +280,9 @@ async def async_setup_internal(hass: HomeAssistant, entry: ConfigEntry) -> None: hass.data[DOMAIN][DATA_RFXOBJECT] = rfx_object entry.async_on_unload( - hass.bus.async_listen(dr.EVENT_DEVICE_REGISTRY_UPDATED, _updated_device) + hass.bus.async_listen( + dr.EVENT_DEVICE_REGISTRY_UPDATED, _updated_device, run_immediately=False + ) ) def _shutdown_rfxtrx(event: Event) -> None: diff --git a/homeassistant/core.py b/homeassistant/core.py index f94a7d4c1bb..48036de519e 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1485,7 +1485,7 @@ class EventBus: event_type: str, listener: Callable[[Event[_DataT]], Coroutine[Any, Any, None] | None], event_filter: Callable[[_DataT], bool] | None = None, - run_immediately: bool = False, + run_immediately: bool = True, ) -> CALLBACK_TYPE: """Listen for all events or events of a specific type. @@ -1558,7 +1558,7 @@ class EventBus: self, event_type: str, listener: Callable[[Event[Any]], Coroutine[Any, Any, None] | None], - run_immediately: bool = False, + run_immediately: bool = True, ) -> CALLBACK_TYPE: """Listen once for event of a specific type. diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 2529b49d263..67feb6c48a4 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -272,7 +272,10 @@ def async_track_state_change( return async_track_state_change_event(hass, entity_ids, state_change_listener) return hass.bus.async_listen( - EVENT_STATE_CHANGED, state_change_dispatcher, event_filter=state_change_filter + EVENT_STATE_CHANGED, + state_change_dispatcher, + event_filter=state_change_filter, + run_immediately=False, ) diff --git a/homeassistant/helpers/start.py b/homeassistant/helpers/start.py index 148b416e087..92ce8e8cdde 100644 --- a/homeassistant/helpers/start.py +++ b/homeassistant/helpers/start.py @@ -47,7 +47,9 @@ def _async_at_core_state( if unsub: unsub() - unsub = hass.bus.async_listen_once(event_type, _matched_event) + unsub = hass.bus.async_listen_once( + event_type, _matched_event, run_immediately=False + ) return cancel diff --git a/tests/common.py b/tests/common.py index db96e36f7ec..59b93fc7288 100644 --- a/tests/common.py +++ b/tests/common.py @@ -358,7 +358,9 @@ async def async_test_home_assistant( """Clear global instance.""" INSTANCES.remove(hass) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, clear_instance) + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_CLOSE, clear_instance, run_immediately=False + ) yield hass diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 31e74529777..ba02e61f0a7 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -2512,7 +2512,9 @@ async def test_recursive_automation_starting_script( hass.services.async_register( "test", "automation_started", async_service_handler ) - hass.bus.async_listen("automation_triggered", async_automation_triggered) + hass.bus.async_listen( + "automation_triggered", async_automation_triggered, run_immediately=False + ) hass.bus.async_fire("trigger_automation") await asyncio.wait_for(script_done_event.wait(), 10) diff --git a/tests/test_core.py b/tests/test_core.py index 905d8efe6de..44da9695fdc 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -3361,9 +3361,11 @@ async def test_report_state_listener_restrictions(hass: HomeAssistant) -> None: """Mock filter.""" return False - # run_immediately not set + # run_immediately set to False with pytest.raises(HomeAssistantError): - hass.bus.async_listen(EVENT_STATE_REPORTED, listener, event_filter=filter) + hass.bus.async_listen( + EVENT_STATE_REPORTED, listener, event_filter=filter, run_immediately=False + ) # no filter with pytest.raises(HomeAssistantError): From 90bbfdd53cb83667b25fc23c0d5bbcd9a3b30170 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 5 Apr 2024 16:41:37 -1000 Subject: [PATCH 311/967] Migrate torque to use async platform setup (#114994) --- homeassistant/components/torque/sensor.py | 24 +++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/torque/sensor.py b/homeassistant/components/torque/sensor.py index 8edf4fe49fc..8572a5a0bba 100644 --- a/homeassistant/components/torque/sensor.py +++ b/homeassistant/components/torque/sensor.py @@ -7,10 +7,10 @@ import re from aiohttp import web import voluptuous as vol -from homeassistant.components.http import KEY_HASS, HomeAssistantView +from homeassistant.components.http import HomeAssistantView from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_EMAIL, CONF_NAME, DEGREE -from homeassistant.core import HassJob, HomeAssistant, callback +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 @@ -44,10 +44,10 @@ def convert_pid(value): return int(value, 16) -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 Torque platform.""" @@ -56,7 +56,7 @@ def setup_platform( sensors: dict[int, TorqueSensor] = {} hass.http.register_view( - TorqueReceiveDataView(email, vehicle, sensors, add_entities) + TorqueReceiveDataView(email, vehicle, sensors, async_add_entities) ) @@ -71,18 +71,17 @@ class TorqueReceiveDataView(HomeAssistantView): email: str | None, vehicle: str | None, sensors: dict[int, TorqueSensor], - add_entities: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, ) -> None: """Initialize a Torque view.""" self.email = email self.vehicle = vehicle self.sensors = sensors - self.add_entities_job = HassJob(add_entities) + self.async_add_entities = async_add_entities @callback def get(self, request: web.Request) -> str | None: """Handle Torque data request.""" - hass: HomeAssistant = request.app[KEY_HASS] data = request.query if self.email is not None and self.email != data[SENSOR_EMAIL_FIELD]: @@ -111,12 +110,17 @@ class TorqueReceiveDataView(HomeAssistantView): if pid in self.sensors: self.sensors[pid].async_on_update(data[key]) + new_sensor_entities: list[TorqueSensor] = [] for pid, name in names.items(): if pid not in self.sensors: - self.sensors[pid] = TorqueSensor( + torque_sensor_entity = TorqueSensor( ENTITY_NAME_FORMAT.format(self.vehicle, name), units.get(pid) ) - hass.async_add_hass_job(self.add_entities_job, [self.sensors[pid]]) + new_sensor_entities.append(torque_sensor_entity) + self.sensors[pid] = torque_sensor_entity + + if new_sensor_entities: + self.async_add_entities(new_sensor_entities) return "OK!" From 657bc969a3c73f1b617aa6070b5667728c699a67 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 5 Apr 2024 16:47:17 -1000 Subject: [PATCH 312/967] Improve performance of system_log traceback handling (#114992) --- .../components/system_log/__init__.py | 33 +++++++++++++------ homeassistant/components/zha/core/gateway.py | 5 ++- tests/components/system_log/test_init.py | 27 ++++++++++++++- 3 files changed, 53 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index 423f5c6f5d8..b7222b75b72 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -7,6 +7,7 @@ import logging import re import sys import traceback +from types import FrameType from typing import Any, cast import voluptuous as vol @@ -18,7 +19,7 @@ from homeassistant.core import Event, HomeAssistant, ServiceCall, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType -KeyType = tuple[str, tuple[str, int], str | None] +KeyType = tuple[str, tuple[str, int], tuple[str, int, str] | None] CONF_MAX_ENTRIES = "max_entries" CONF_FIRE_EVENT = "fire_event" @@ -65,16 +66,18 @@ SERVICE_WRITE_SCHEMA = vol.Schema( def _figure_out_source( record: logging.LogRecord, paths_re: re.Pattern[str], - extracted_tb: traceback.StackSummary | None = None, + extracted_tb: list[tuple[FrameType, int]] | None = None, ) -> tuple[str, int]: """Figure out where a log message came from.""" # If a stack trace exists, extract file names from the entire call stack. # The other case is when a regular "log" is made (without an attached # exception). In that case, just use the file where the log was made from. if record.exc_info: + source: list[tuple[FrameType, int]] = extracted_tb or list( + traceback.walk_tb(record.exc_info[2]) + ) stack = [ - (x[0], x[1]) - for x in (extracted_tb or traceback.extract_tb(record.exc_info[2])) + (tb_frame.f_code.co_filename, tb_line_no) for tb_frame, tb_line_no in source ] for i, (filename, _) in enumerate(stack): # Slice the stack to the first frame that matches @@ -176,6 +179,7 @@ class LogEntry: self, record: logging.LogRecord, paths_re: re.Pattern, + formatter: logging.Formatter | None = None, figure_out_source: bool = False, ) -> None: """Initialize a log entry.""" @@ -186,14 +190,21 @@ class LogEntry: # This must be manually tested when changing the code. self.message = deque([_safe_get_message(record)], maxlen=5) self.exception = "" - self.root_cause: str | None = None - extracted_tb: traceback.StackSummary | None = None + self.root_cause: tuple[str, int, str] | None = None + extracted_tb: list[tuple[FrameType, int]] | None = None if record.exc_info: - self.exception = "".join(traceback.format_exception(*record.exc_info)) - if extracted := traceback.extract_tb(record.exc_info[2]): + if formatter and record.exc_text is None: + record.exc_text = formatter.formatException(record.exc_info) + self.exception = record.exc_text or "" + if extracted := list(traceback.walk_tb(record.exc_info[2])): # Last line of traceback contains the root cause of the exception extracted_tb = extracted - self.root_cause = str(extracted[-1]) + tb_frame, tb_line_no = extracted[-1] + self.root_cause = ( + tb_frame.f_code.co_filename, + tb_line_no, + tb_frame.f_code.co_name, + ) if figure_out_source: self.source = _figure_out_source(record, paths_re, extracted_tb) else: @@ -273,7 +284,9 @@ class LogErrorHandler(logging.Handler): default upper limit is set to 50 (older entries are discarded) but can be changed if needed. """ - entry = LogEntry(record, self.paths_re, figure_out_source=True) + entry = LogEntry( + record, self.paths_re, formatter=self.formatter, figure_out_source=True + ) self.records.add_entry(entry) if self.fire_event: self.hass.bus.fire(EVENT_SYSTEM_LOG, entry.to_dict()) diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 83aa12fbfa1..4c41909f660 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -870,7 +870,10 @@ class LogRelayHandler(logging.Handler): def emit(self, record: LogRecord) -> None: """Relay log message via dispatcher.""" entry = LogEntry( - record, self.paths_re, figure_out_source=record.levelno >= logging.WARNING + record, + self.paths_re, + formatter=self.formatter, + figure_out_source=record.levelno >= logging.WARNING, ) async_dispatcher_send( self.hass, diff --git a/tests/components/system_log/test_init.py b/tests/components/system_log/test_init.py index 5e4eda7d643..e3550101dcc 100644 --- a/tests/components/system_log/test_init.py +++ b/tests/components/system_log/test_init.py @@ -471,10 +471,35 @@ async def test__figure_out_source(hass: HomeAssistant) -> None: file, line_no = system_log._figure_out_source( mock_record, paths_re, - traceback.extract_tb(exc_info[2]), + list(traceback.walk_tb(exc_info[2])), ) assert file == __file__ assert line_no != 5 entry = system_log.LogEntry(mock_record, paths_re, figure_out_source=False) assert entry.source == ("figure_out_source is False", 5) + + +async def test_formatting_exception(hass: HomeAssistant) -> None: + """Test that exceptions are formatted correctly.""" + try: + raise ValueError("test") + except ValueError as ex: + exc_info = (type(ex), ex, ex.__traceback__) + mock_record = MagicMock( + pathname="figure_out_source is False", + lineno=5, + exc_info=exc_info, + exc_text=None, + ) + regex_str = f"({__file__})" + paths_re = re.compile(regex_str) + + mock_formatter = MagicMock( + formatException=MagicMock(return_value="formatted exception") + ) + entry = system_log.LogEntry( + mock_record, paths_re, formatter=mock_formatter, figure_out_source=False + ) + assert entry.exception == "formatted exception" + assert mock_record.exc_text == "formatted exception" From 9c27e632fb10ae23cfc399c8ffd45d0553d888f6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 5 Apr 2024 17:02:58 -1000 Subject: [PATCH 313/967] Switch configurator to use async_run_hass_job (#114993) --- homeassistant/components/configurator/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/configurator/__init__.py b/homeassistant/components/configurator/__init__.py index 0579df90dc9..b2cf9a136cc 100644 --- a/homeassistant/components/configurator/__init__.py +++ b/homeassistant/components/configurator/__init__.py @@ -250,7 +250,7 @@ class Configurator: # field validation goes here? if callback and ( - job := self.hass.async_add_hass_job( + job := self.hass.async_run_hass_job( HassJob(callback), call.data.get(ATTR_FIELDS, {}) ) ): From f497c461ed17ae7211454df496c08d219d73e268 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 5 Apr 2024 17:15:51 -1000 Subject: [PATCH 314/967] Switch to using the AsyncResolver with aiohttp (#114529) * Bump aiodns to 3.2.0 changelog: https://github.com/saghul/aiodns/compare/v3.1.1...v3.2.0 * Switch to using the AsyncResolver with aiohttp This avoids creating executor jobs to do DNS resolution AsyncResolver was not usable before https://github.com/aio-libs/aiohttp/pull/8270 because it did not fallback to fallback to A records and only returned AAAA records in most cases when IPv6 was available This is a backport of https://github.com/aio-libs/aiohttp/pull/8270 * Switch to using the AsyncResolver with aiohttp This avoids creating executor jobs to do DNS resolution AsyncResolver was not usable before https://github.com/aio-libs/aiohttp/pull/8270 because it did not fallback to fallback to A records and only returned AAAA records in most cases when IPv6 was available This is a backport of https://github.com/aio-libs/aiohttp/pull/8270 * Switch to using the AsyncResolver with aiohttp This avoids creating executor jobs to do DNS resolution AsyncResolver was not usable before https://github.com/aio-libs/aiohttp/pull/8270 because it did not fallback to fallback to A records and only returned AAAA records in most cases when IPv6 was available This is a backport of https://github.com/aio-libs/aiohttp/pull/8270 * Switch to using the AsyncResolver with aiohttp This avoids creating executor jobs to do DNS resolution AsyncResolver was not usable before https://github.com/aio-libs/aiohttp/pull/8270 because it did not fallback to fallback to A records and only returned AAAA records in most cases when IPv6 was available This is a backport of https://github.com/aio-libs/aiohttp/pull/8270 * fixes * fix mocking in next_dns * fix unmocked calls in blink * more mocking fixes * more fixes * more fixes * Fix missing mocking in nextdns tests extracted from #114539 * extract from context --- .coveragerc | 1 + homeassistant/helpers/aiohttp_client.py | 2 + homeassistant/helpers/backports/__init__.py | 1 + .../helpers/backports/aiohttp_resolver.py | 116 ++++++++++++++++++ homeassistant/package_constraints.txt | 1 + pyproject.toml | 1 + requirements.txt | 1 + 7 files changed, 123 insertions(+) create mode 100644 homeassistant/helpers/backports/__init__.py create mode 100644 homeassistant/helpers/backports/aiohttp_resolver.py diff --git a/.coveragerc b/.coveragerc index ed658f3ca55..68d7629b6c5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -7,6 +7,7 @@ source = homeassistant omit = homeassistant/__main__.py homeassistant/helpers/signal.py + homeassistant/helpers/backports/* homeassistant/scripts/__init__.py homeassistant/scripts/check_config.py homeassistant/scripts/ensure_config.py diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 15437b00183..6278586f469 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -22,6 +22,7 @@ from homeassistant.loader import bind_hass from homeassistant.util import ssl as ssl_util from homeassistant.util.json import json_loads +from .backports.aiohttp_resolver import AsyncResolver from .frame import warn_use from .json import json_dumps @@ -310,6 +311,7 @@ def _async_get_connector( ssl=ssl_context, limit=MAXIMUM_CONNECTIONS, limit_per_host=MAXIMUM_CONNECTIONS_PER_HOST, + resolver=AsyncResolver(), ) connectors[connector_key] = connector diff --git a/homeassistant/helpers/backports/__init__.py b/homeassistant/helpers/backports/__init__.py new file mode 100644 index 00000000000..e672fe1d3d2 --- /dev/null +++ b/homeassistant/helpers/backports/__init__.py @@ -0,0 +1 @@ +"""Backports for helpers.""" diff --git a/homeassistant/helpers/backports/aiohttp_resolver.py b/homeassistant/helpers/backports/aiohttp_resolver.py new file mode 100644 index 00000000000..efa4ba4bb85 --- /dev/null +++ b/homeassistant/helpers/backports/aiohttp_resolver.py @@ -0,0 +1,116 @@ +"""Backport of aiohttp's AsyncResolver for Home Assistant. + +This is a backport of the AsyncResolver class from aiohttp 3.10. + +Before aiohttp 3.10, on system with IPv6 support, AsyncResolver would not fallback +to providing A records when AAAA records were not available. + +Additionally, unlike the ThreadedResolver, AsyncResolver +did not handle link-local addresses correctly. +""" + +from __future__ import annotations + +import asyncio +import socket +import sys +from typing import Any, TypedDict + +import aiodns +from aiohttp.abc import AbstractResolver + +# This is a backport of https://github.com/aio-libs/aiohttp/pull/8270 +# This can be removed once aiohttp 3.10 is the minimum supported version. + +_NUMERIC_SOCKET_FLAGS = socket.AI_NUMERICHOST | socket.AI_NUMERICSERV +_SUPPORTS_SCOPE_ID = sys.version_info >= (3, 9, 0) + + +class ResolveResult(TypedDict): + """Resolve result. + + This is the result returned from an AbstractResolver's + resolve method. + + :param hostname: The hostname that was provided. + :param host: The IP address that was resolved. + :param port: The port that was resolved. + :param family: The address family that was resolved. + :param proto: The protocol that was resolved. + :param flags: The flags that were resolved. + """ + + hostname: str + host: str + port: int + family: int + proto: int + flags: int + + +class AsyncResolver(AbstractResolver): + """Use the `aiodns` package to make asynchronous DNS lookups.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Initialize the resolver.""" + if aiodns is None: + raise RuntimeError("Resolver requires aiodns library") + + self._loop = asyncio.get_running_loop() + self._resolver = aiodns.DNSResolver(*args, loop=self._loop, **kwargs) # type: ignore[misc] + + async def resolve( # type: ignore[override] + self, host: str, port: int = 0, family: int = socket.AF_INET + ) -> list[ResolveResult]: + """Resolve a host name to an IP address.""" + try: + resp = await self._resolver.getaddrinfo( + host, + port=port, + type=socket.SOCK_STREAM, + family=family, # type: ignore[arg-type] + flags=socket.AI_ADDRCONFIG, + ) + except aiodns.error.DNSError as exc: + msg = exc.args[1] if len(exc.args) >= 1 else "DNS lookup failed" + raise OSError(msg) from exc + hosts: list[ResolveResult] = [] + for node in resp.nodes: + address: tuple[bytes, int] | tuple[bytes, int, int, int] = node.addr + family = node.family + if family == socket.AF_INET6: + if len(address) > 3 and address[3] and _SUPPORTS_SCOPE_ID: + # This is essential for link-local IPv6 addresses. + # LL IPv6 is a VERY rare case. Strictly speaking, we should use + # getnameinfo() unconditionally, but performance makes sense. + result = await self._resolver.getnameinfo( + (address[0].decode("ascii"), *address[1:]), + _NUMERIC_SOCKET_FLAGS, + ) + resolved_host = result.node + else: + resolved_host = address[0].decode("ascii") + port = address[1] + else: # IPv4 + assert family == socket.AF_INET + resolved_host = address[0].decode("ascii") + port = address[1] + hosts.append( + ResolveResult( + hostname=host, + host=resolved_host, + port=port, + family=family, + proto=0, + flags=_NUMERIC_SOCKET_FLAGS, + ) + ) + + if not hosts: + raise OSError("DNS lookup failed") + + return hosts + + async def close(self) -> None: + """Close the resolver.""" + self._resolver.cancel() diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 610e781faec..560a1329a32 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -2,6 +2,7 @@ aiodhcpwatcher==1.0.0 aiodiscover==2.0.0 +aiodns==3.2.0 aiohttp-fast-url-dispatcher==0.3.0 aiohttp-zlib-ng==0.3.1 aiohttp==3.9.3 diff --git a/pyproject.toml b/pyproject.toml index 0bcb3461729..e8558524b08 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ classifiers = [ ] requires-python = ">=3.12.0" dependencies = [ + "aiodns==3.2.0", "aiohttp==3.9.3", "aiohttp_cors==0.7.0", "aiohttp-fast-url-dispatcher==0.3.0", diff --git a/requirements.txt b/requirements.txt index 22bc0743a27..7d550bc8c6a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ -c homeassistant/package_constraints.txt # Home Assistant Core +aiodns==3.2.0 aiohttp==3.9.3 aiohttp_cors==0.7.0 aiohttp-fast-url-dispatcher==0.3.0 From 0e202770e32e8d688e74ab73ead9f39c2004a29c Mon Sep 17 00:00:00 2001 From: lunmay <28674102+lunmay@users.noreply.github.com> Date: Sat, 6 Apr 2024 08:12:44 +0200 Subject: [PATCH 315/967] Brand name typo in swiss_public_transport (#115000) --- homeassistant/components/swiss_public_transport/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/swiss_public_transport/strings.json b/homeassistant/components/swiss_public_transport/strings.json index 0a3114c914f..cddc732d3ed 100644 --- a/homeassistant/components/swiss_public_transport/strings.json +++ b/homeassistant/components/swiss_public_transport/strings.json @@ -50,7 +50,7 @@ "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." + "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", From 48b281a5816a0e51421254a5c29ef8856d8d33c1 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 6 Apr 2024 09:01:55 +0200 Subject: [PATCH 316/967] Bump axis to v61 (#114964) --- 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 bbea7954be1..2f057f96286 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==60"], + "requirements": ["axis==61"], "ssdp": [ { "manufacturer": "AXIS" diff --git a/requirements_all.txt b/requirements_all.txt index da52c3b03d9..e24f18f8936 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -514,7 +514,7 @@ aurorapy==0.2.7 # avion==0.10 # homeassistant.components.axis -axis==60 +axis==61 # homeassistant.components.azure_event_hub azure-eventhub==5.11.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 43ae9cb445a..1ffce1b7c51 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -454,7 +454,7 @@ auroranoaa==0.0.3 aurorapy==0.2.7 # homeassistant.components.axis -axis==60 +axis==61 # homeassistant.components.azure_event_hub azure-eventhub==5.11.1 From c3942a7d44d43f66d214f23e2bbe287b1836cf16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Matheson=20Wergeland?= Date: Sat, 6 Apr 2024 09:38:14 +0200 Subject: [PATCH 317/967] Upgrade to pynobo 1.8.1 (#114982) pynobo 1.8.1 --- homeassistant/components/nobo_hub/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nobo_hub/manifest.json b/homeassistant/components/nobo_hub/manifest.json index 4741eb39e29..ce32244e1ce 100644 --- a/homeassistant/components/nobo_hub/manifest.json +++ b/homeassistant/components/nobo_hub/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/nobo_hub", "integration_type": "hub", "iot_class": "local_push", - "requirements": ["pynobo==1.8.0"] + "requirements": ["pynobo==1.8.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index e24f18f8936..e248dcc6ca1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1998,7 +1998,7 @@ pynetgear==0.10.10 pynetio==0.1.9.1 # homeassistant.components.nobo_hub -pynobo==1.8.0 +pynobo==1.8.1 # homeassistant.components.nuki pynuki==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1ffce1b7c51..5ecac38fb93 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1552,7 +1552,7 @@ pymysensors==0.24.0 pynetgear==0.10.10 # homeassistant.components.nobo_hub -pynobo==1.8.0 +pynobo==1.8.1 # homeassistant.components.nuki pynuki==1.6.3 From a28731c29416355553dd1b5813fe320559033e8b Mon Sep 17 00:00:00 2001 From: Claudio Ruggeri - CR-Tech <41435902+crug80@users.noreply.github.com> Date: Sat, 6 Apr 2024 11:01:44 +0200 Subject: [PATCH 318/967] Add Swing Mode Feature to Modbus integration (#113710) --- homeassistant/components/modbus/__init__.py | 23 ++ homeassistant/components/modbus/climate.py | 85 +++++- homeassistant/components/modbus/const.py | 7 + homeassistant/components/modbus/validators.py | 40 ++- tests/components/modbus/test_climate.py | 253 ++++++++++++++++++ tests/components/modbus/test_init.py | 85 ++++++ 6 files changed, 490 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 94a84d3440d..2a82cf89fd5 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -113,6 +113,13 @@ from .const import ( CONF_SWAP_BYTE, CONF_SWAP_WORD, CONF_SWAP_WORD_BYTE, + CONF_SWING_MODE_REGISTER, + CONF_SWING_MODE_SWING_BOTH, + CONF_SWING_MODE_SWING_HORIZ, + CONF_SWING_MODE_SWING_OFF, + CONF_SWING_MODE_SWING_ON, + CONF_SWING_MODE_SWING_VERT, + CONF_SWING_MODE_VALUES, CONF_TARGET_TEMP, CONF_TARGET_TEMP_WRITE_REGISTERS, CONF_VERIFY, @@ -134,6 +141,7 @@ from .modbus import ModbusHub, async_modbus_setup from .validators import ( check_hvac_target_temp_registers, duplicate_fan_mode_validator, + duplicate_swing_mode_validator, hvac_fixedsize_reglist_validator, nan_validator, register_int_list_validator, @@ -296,6 +304,21 @@ CLIMATE_SCHEMA = vol.All( duplicate_fan_mode_validator, ), ), + vol.Optional(CONF_SWING_MODE_REGISTER): vol.Maybe( + vol.All( + { + vol.Required(CONF_ADDRESS): register_int_list_validator, + CONF_SWING_MODE_VALUES: { + vol.Optional(CONF_SWING_MODE_SWING_ON): cv.positive_int, + vol.Optional(CONF_SWING_MODE_SWING_OFF): cv.positive_int, + vol.Optional(CONF_SWING_MODE_SWING_HORIZ): cv.positive_int, + vol.Optional(CONF_SWING_MODE_SWING_VERT): cv.positive_int, + vol.Optional(CONF_SWING_MODE_SWING_BOTH): cv.positive_int, + }, + }, + duplicate_swing_mode_validator, + ) + ), }, ), check_hvac_target_temp_registers, diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 07dd12d3c94..0a4eae341b4 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import datetime +import logging import struct from typing import Any, cast @@ -17,6 +18,11 @@ from homeassistant.components.climate import ( FAN_OFF, FAN_ON, FAN_TOP, + SWING_BOTH, + SWING_HORIZONTAL, + SWING_OFF, + SWING_ON, + SWING_VERTICAL, ClimateEntity, ClimateEntityFeature, HVACMode, @@ -28,6 +34,7 @@ from homeassistant.const import ( CONF_TEMPERATURE_UNIT, PRECISION_TENTHS, PRECISION_WHOLE, + STATE_UNKNOWN, UnitOfTemperature, ) from homeassistant.core import HomeAssistant @@ -67,6 +74,13 @@ from .const import ( CONF_MAX_TEMP, CONF_MIN_TEMP, CONF_STEP, + CONF_SWING_MODE_REGISTER, + CONF_SWING_MODE_SWING_BOTH, + CONF_SWING_MODE_SWING_HORIZ, + CONF_SWING_MODE_SWING_OFF, + CONF_SWING_MODE_SWING_ON, + CONF_SWING_MODE_SWING_VERT, + CONF_SWING_MODE_VALUES, CONF_TARGET_TEMP, CONF_TARGET_TEMP_WRITE_REGISTERS, CONF_WRITE_REGISTERS, @@ -74,6 +88,8 @@ from .const import ( ) from .modbus import ModbusHub +_LOGGER = logging.getLogger(__name__) + PARALLEL_UPDATES = 1 HVACMODE_TO_TARG_TEMP_REG_INDEX_ARRAY = { @@ -204,11 +220,35 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): self._attr_fan_modes.append(fan_mode) else: - # No HVAC modes defined + # No FAN modes defined self._fan_mode_register = None self._attr_fan_mode = FAN_AUTO self._attr_fan_modes = [FAN_AUTO] + # No SWING modes defined + self._swing_mode_register = None + if CONF_SWING_MODE_REGISTER in config: + self._attr_supported_features = ( + self._attr_supported_features | ClimateEntityFeature.SWING_MODE + ) + mode_config = config[CONF_SWING_MODE_REGISTER] + self._swing_mode_register = mode_config[CONF_ADDRESS] + self._attr_swing_modes = cast(list[str], []) + self._attr_swing_mode = None + self._swing_mode_modbus_mapping: list[tuple[int, str]] = [] + mode_value_config = mode_config[CONF_SWING_MODE_VALUES] + for swing_mode_kw, swing_mode in ( + (CONF_SWING_MODE_SWING_ON, SWING_ON), + (CONF_SWING_MODE_SWING_OFF, SWING_OFF), + (CONF_SWING_MODE_SWING_HORIZ, SWING_HORIZONTAL), + (CONF_SWING_MODE_SWING_VERT, SWING_VERTICAL), + (CONF_SWING_MODE_SWING_BOTH, SWING_BOTH), + ): + if swing_mode_kw in mode_value_config: + value = mode_value_config[swing_mode_kw] + self._swing_mode_modbus_mapping.append((value, swing_mode)) + self._attr_swing_modes.append(swing_mode) + if CONF_HVAC_ONOFF_REGISTER in config: self._hvac_onoff_register = config[CONF_HVAC_ONOFF_REGISTER] self._hvac_onoff_write_registers = config[CONF_WRITE_REGISTERS] @@ -287,6 +327,29 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): await self.async_update() + async def async_set_swing_mode(self, swing_mode: str) -> None: + """Set new target swing mode.""" + if self._swing_mode_register: + # Write a value to the mode register for the desired mode. + for value, smode in self._swing_mode_modbus_mapping: + if swing_mode == smode: + if isinstance(self._swing_mode_register, list): + await self._hub.async_pb_call( + self._slave, + self._swing_mode_register[0], + [value], + CALL_TYPE_WRITE_REGISTERS, + ) + else: + await self._hub.async_pb_call( + self._slave, + self._swing_mode_register, + value, + CALL_TYPE_WRITE_REGISTER, + ) + break + await self.async_update() + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" target_temperature = ( @@ -387,6 +450,26 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): int(fan_mode), self._attr_fan_mode ) + # Read the Swing mode register if defined + if self._swing_mode_register: + swing_mode = await self._async_read_register( + CALL_TYPE_REGISTER_HOLDING, + self._swing_mode_register + if isinstance(self._swing_mode_register, int) + else self._swing_mode_register[0], + raw=True, + ) + + self._attr_swing_mode = STATE_UNKNOWN + for value, smode in self._swing_mode_modbus_mapping: + if swing_mode == value: + self._attr_swing_mode = smode + break + + if self._attr_swing_mode is STATE_UNKNOWN: + _err = f"{self.name}: No answer received from Swing mode register. State is Unknown" + _LOGGER.error(_err) + # Read the on/off register if defined. If the value in this # register is "OFF", it will take precedence over the value # in the mode register. diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index 425bd744a1e..02f5d99c72c 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -70,6 +70,13 @@ CONF_HVAC_MODE_AUTO = "state_auto" CONF_HVAC_MODE_DRY = "state_dry" CONF_HVAC_MODE_FAN_ONLY = "state_fan_only" CONF_HVAC_MODE_VALUES = "values" +CONF_SWING_MODE_REGISTER = "swing_mode_register" +CONF_SWING_MODE_SWING_BOTH = "swing_mode_state_both" +CONF_SWING_MODE_SWING_HORIZ = "swing_mode_state_horizontal" +CONF_SWING_MODE_SWING_OFF = "swing_mode_state_off" +CONF_SWING_MODE_SWING_ON = "swing_mode_state_on" +CONF_SWING_MODE_SWING_VERT = "swing_mode_state_vertical" +CONF_SWING_MODE_VALUES = "values" CONF_WRITE_REGISTERS = "write_registers" CONF_VERIFY = "verify" CONF_VIRTUAL_COUNT = "virtual_count" diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py index 7de2ecbe604..5071d098db7 100644 --- a/homeassistant/components/modbus/validators.py +++ b/homeassistant/components/modbus/validators.py @@ -42,6 +42,8 @@ from .const import ( CONF_SWAP_BYTE, CONF_SWAP_WORD, CONF_SWAP_WORD_BYTE, + CONF_SWING_MODE_REGISTER, + CONF_SWING_MODE_VALUES, CONF_TARGET_TEMP, CONF_VIRTUAL_COUNT, CONF_WRITE_TYPE, @@ -256,8 +258,25 @@ def duplicate_fan_mode_validator(config: dict[str, Any]) -> dict: return config +def duplicate_swing_mode_validator(config: dict[str, Any]) -> dict: + """Control modbus climate swing mode values for duplicates.""" + swing_modes: set[int] = set() + errors = [] + for key, value in config[CONF_SWING_MODE_VALUES].items(): + if value in swing_modes: + warn = f"Modbus swing mode {key} has a duplicate value {value}, not loaded, values must be unique!" + _LOGGER.warning(warn) + errors.append(key) + else: + swing_modes.add(value) + + for key in reversed(errors): + del config[CONF_SWING_MODE_VALUES][key] + return config + + def check_hvac_target_temp_registers(config: dict) -> dict: - """Check conflicts among HVAC target temperature registers and HVAC ON/OFF, HVAC register, Fan Modes.""" + """Check conflicts among HVAC target temperature registers and HVAC ON/OFF, HVAC register, Fan Modes, Swing Modes.""" if ( CONF_HVAC_MODE_REGISTER in config @@ -281,6 +300,17 @@ def check_hvac_target_temp_registers(config: dict) -> dict: _LOGGER.warning(wrn) del config[CONF_FAN_MODE_REGISTER] + if CONF_SWING_MODE_REGISTER in config: + regToTest = ( + config[CONF_SWING_MODE_REGISTER][CONF_ADDRESS] + if isinstance(config[CONF_SWING_MODE_REGISTER][CONF_ADDRESS], int) + else config[CONF_SWING_MODE_REGISTER][CONF_ADDRESS][0] + ) + if regToTest in config[CONF_TARGET_TEMP]: + wrn = f"{CONF_SWING_MODE_REGISTER} overlaps CONF_TARGET_TEMP register(s). {CONF_SWING_MODE_REGISTER} is not loaded!" + _LOGGER.warning(wrn) + del config[CONF_SWING_MODE_REGISTER] + return config @@ -294,7 +324,7 @@ def register_int_list_validator(value: Any) -> Any: return value raise vol.Invalid( - f"Invalid {CONF_ADDRESS} register for fan mode. Required type: positive integer, allowed 1 or list of 1 register." + f"Invalid {CONF_ADDRESS} register for fan/swing mode. Required type: positive integer, allowed 1 or list of 1 register." ) @@ -421,6 +451,12 @@ def validate_entity( loc_addr.add(f"{hub_name}{entity[CONF_HVAC_MODE_REGISTER][CONF_ADDRESS]}_{inx}") if CONF_FAN_MODE_REGISTER in entity: loc_addr.add(f"{hub_name}{entity[CONF_FAN_MODE_REGISTER][CONF_ADDRESS]}_{inx}") + if CONF_SWING_MODE_REGISTER in entity: + loc_addr.add( + f"{hub_name}{entity[CONF_SWING_MODE_REGISTER][CONF_ADDRESS] + if isinstance(entity[CONF_SWING_MODE_REGISTER][CONF_ADDRESS],int) + else entity[CONF_SWING_MODE_REGISTER][CONF_ADDRESS][0]}_{inx}" + ) dup_addrs = ent_addr.intersection(loc_addr) if len(dup_addrs) > 0: diff --git a/tests/components/modbus/test_climate.py b/tests/components/modbus/test_climate.py index 3752358c071..093dee67895 100644 --- a/tests/components/modbus/test_climate.py +++ b/tests/components/modbus/test_climate.py @@ -8,6 +8,8 @@ from homeassistant.components.climate.const import ( ATTR_FAN_MODES, ATTR_HVAC_MODE, ATTR_HVAC_MODES, + ATTR_SWING_MODE, + ATTR_SWING_MODES, FAN_AUTO, FAN_DIFFUSE, FAN_FOCUS, @@ -18,6 +20,11 @@ from homeassistant.components.climate.const import ( FAN_OFF, FAN_ON, FAN_TOP, + SWING_BOTH, + SWING_HORIZONTAL, + SWING_OFF, + SWING_ON, + SWING_VERTICAL, HVACMode, ) from homeassistant.components.modbus.const import ( @@ -45,6 +52,13 @@ from homeassistant.components.modbus.const import ( CONF_HVAC_ONOFF_REGISTER, CONF_MAX_TEMP, CONF_MIN_TEMP, + CONF_SWING_MODE_REGISTER, + CONF_SWING_MODE_SWING_BOTH, + CONF_SWING_MODE_SWING_HORIZ, + CONF_SWING_MODE_SWING_OFF, + CONF_SWING_MODE_SWING_ON, + CONF_SWING_MODE_SWING_VERT, + CONF_SWING_MODE_VALUES, CONF_TARGET_TEMP, CONF_TARGET_TEMP_WRITE_REGISTERS, CONF_WRITE_REGISTERS, @@ -58,6 +72,7 @@ from homeassistant.const import ( CONF_SCAN_INTERVAL, CONF_SLAVE, STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant, State from homeassistant.setup import async_setup_component @@ -282,6 +297,41 @@ async def test_config_fan_mode_register(hass: HomeAssistant, mock_modbus) -> Non assert FAN_FOCUS not in state.attributes[ATTR_FAN_MODES] +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 117, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SWING_MODE_REGISTER: { + CONF_ADDRESS: 11, + CONF_SWING_MODE_VALUES: { + CONF_SWING_MODE_SWING_ON: 0, + CONF_SWING_MODE_SWING_OFF: 1, + CONF_SWING_MODE_SWING_BOTH: 2, + CONF_SWING_MODE_SWING_HORIZ: 3, + CONF_SWING_MODE_SWING_VERT: 4, + }, + }, + } + ], + }, + ], +) +async def test_config_swing_mode_register(hass: HomeAssistant, mock_modbus) -> None: + """Run configuration test for Fan mode register.""" + state = hass.states.get(ENTITY_ID) + assert SWING_ON in state.attributes[ATTR_SWING_MODES] + assert SWING_OFF in state.attributes[ATTR_SWING_MODES] + assert SWING_BOTH in state.attributes[ATTR_SWING_MODES] + assert SWING_HORIZONTAL in state.attributes[ATTR_SWING_MODES] + assert SWING_VERTICAL in state.attributes[ATTR_SWING_MODES] + + @pytest.mark.parametrize( "do_config", [ @@ -572,6 +622,146 @@ async def test_service_climate_fan_update( assert hass.states.get(ENTITY_ID).attributes[ATTR_FAN_MODE] == result +@pytest.mark.parametrize( + ("do_config", "result", "register_words"), + [ + ( + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 116, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 0, + CONF_DATA_TYPE: DataType.INT32, + CONF_SWING_MODE_REGISTER: { + CONF_ADDRESS: [118], + CONF_SWING_MODE_VALUES: { + CONF_SWING_MODE_SWING_OFF: 0, + CONF_SWING_MODE_SWING_ON: 1, + CONF_SWING_MODE_SWING_BOTH: 2, + }, + }, + }, + ] + }, + SWING_BOTH, + [0x02], + ), + ( + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 116, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 0, + CONF_DATA_TYPE: DataType.INT32, + CONF_SWING_MODE_REGISTER: { + CONF_ADDRESS: [118], + CONF_SWING_MODE_VALUES: { + CONF_SWING_MODE_SWING_OFF: 0, + CONF_SWING_MODE_SWING_ON: 1, + CONF_SWING_MODE_SWING_VERT: 2, + }, + }, + }, + ] + }, + SWING_ON, + [0x01], + ), + ( + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 116, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 0, + CONF_DATA_TYPE: DataType.INT32, + CONF_SWING_MODE_REGISTER: { + CONF_ADDRESS: [118], + CONF_SWING_MODE_VALUES: { + CONF_SWING_MODE_SWING_OFF: 0, + CONF_SWING_MODE_SWING_ON: 1, + CONF_SWING_MODE_SWING_HORIZ: 3, + }, + }, + CONF_HVAC_ONOFF_REGISTER: 119, + }, + ] + }, + SWING_HORIZONTAL, + [0x03], + ), + ( + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 116, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 0, + CONF_DATA_TYPE: DataType.INT32, + CONF_SWING_MODE_REGISTER: { + CONF_ADDRESS: 118, + CONF_SWING_MODE_VALUES: { + CONF_SWING_MODE_SWING_OFF: 0, + CONF_SWING_MODE_SWING_ON: 1, + CONF_SWING_MODE_SWING_VERT: 2, + CONF_SWING_MODE_SWING_BOTH: 3, + }, + }, + }, + ] + }, + SWING_OFF, + [0x00], + ), + ( + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 116, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 0, + CONF_DATA_TYPE: DataType.INT32, + CONF_SWING_MODE_REGISTER: { + CONF_ADDRESS: 118, + CONF_SWING_MODE_VALUES: { + CONF_SWING_MODE_SWING_OFF: 0, + CONF_SWING_MODE_SWING_ON: 1, + CONF_SWING_MODE_SWING_VERT: 2, + CONF_SWING_MODE_SWING_BOTH: 3, + }, + }, + }, + ] + }, + STATE_UNKNOWN, + [0x05], + ), + ], +) +async def test_service_climate_swing_update( + hass: HomeAssistant, mock_modbus, mock_ha, result, register_words +) -> None: + """Run test for service homeassistant.update_entity.""" + mock_modbus.read_holding_registers.return_value = ReadResult(register_words) + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True + ) + await hass.async_block_till_done() + assert hass.states.get(ENTITY_ID).attributes[ATTR_SWING_MODE] == result + + @pytest.mark.parametrize( ("temperature", "result", "do_config"), [ @@ -843,6 +1033,69 @@ async def test_service_set_fan_mode( ) +@pytest.mark.parametrize( + ("swing_mode", "result", "do_config"), + [ + ( + SWING_OFF, + [0x00], + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 117, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SWING_MODE_REGISTER: { + CONF_ADDRESS: [118], + CONF_SWING_MODE_VALUES: { + CONF_SWING_MODE_SWING_ON: 1, + CONF_SWING_MODE_SWING_OFF: 0, + }, + }, + } + ] + }, + ), + ( + SWING_ON, + [0x01], + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 117, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SWING_MODE_REGISTER: { + CONF_ADDRESS: 118, + CONF_SWING_MODE_VALUES: { + CONF_SWING_MODE_SWING_ON: 1, + CONF_SWING_MODE_SWING_OFF: 0, + }, + }, + } + ] + }, + ), + ], +) +async def test_service_set_swing_mode( + hass: HomeAssistant, swing_mode, result, mock_modbus, mock_ha +) -> None: + """Test set Swing mode.""" + mock_modbus.read_holding_registers.return_value = ReadResult(result) + await hass.services.async_call( + CLIMATE_DOMAIN, + "set_swing_mode", + { + "entity_id": ENTITY_ID, + ATTR_SWING_MODE: swing_mode, + }, + blocking=True, + ) + + test_value = State(ENTITY_ID, 35) test_value.attributes = {ATTR_TEMPERATURE: 37} diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 0ca4703aa5f..922022741b0 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -66,6 +66,11 @@ from homeassistant.components.modbus.const import ( CONF_SWAP_BYTE, CONF_SWAP_WORD, CONF_SWAP_WORD_BYTE, + CONF_SWING_MODE_REGISTER, + CONF_SWING_MODE_SWING_BOTH, + CONF_SWING_MODE_SWING_OFF, + CONF_SWING_MODE_SWING_ON, + CONF_SWING_MODE_VALUES, CONF_TARGET_TEMP, CONF_VIRTUAL_COUNT, DEFAULT_SCAN_INTERVAL, @@ -84,6 +89,7 @@ from homeassistant.components.modbus.validators import ( check_config, check_hvac_target_temp_registers, duplicate_fan_mode_validator, + duplicate_swing_mode_validator, hvac_fixedsize_reglist_validator, nan_validator, register_int_list_validator, @@ -629,6 +635,42 @@ async def test_check_config_sensor(hass: HomeAssistant, do_config) -> None: ], } ], + [ # Testing Swing modes + { + CONF_NAME: TEST_MODBUS_NAME, + CONF_TYPE: TCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, + CONF_TIMEOUT: 3, + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 117, + CONF_SLAVE: 0, + CONF_SWING_MODE_REGISTER: { + CONF_ADDRESS: 120, + CONF_SWING_MODE_VALUES: { + CONF_SWING_MODE_SWING_ON: 0, + CONF_SWING_MODE_SWING_BOTH: 1, + }, + }, + }, + { + CONF_NAME: TEST_ENTITY_NAME + " 2", + CONF_ADDRESS: 119, + CONF_SLAVE: 0, + CONF_TARGET_TEMP: 118, + CONF_SWING_MODE_REGISTER: { + CONF_ADDRESS: [120], + CONF_SWING_MODE_VALUES: { + CONF_SWING_MODE_SWING_ON: 0, + CONF_SWING_MODE_SWING_BOTH: 1, + }, + }, + }, + ], + } + ], [ { CONF_NAME: TEST_MODBUS_NAME, @@ -733,6 +775,29 @@ async def test_check_config_climate(hass: HomeAssistant, do_config) -> None: CONF_FAN_MODE_REGISTER: { CONF_ADDRESS: 117, }, + CONF_SWING_MODE_REGISTER: { + CONF_ADDRESS: 117, + }, + }, + ], + [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 1, + CONF_TARGET_TEMP: [117], + CONF_HVAC_MODE_REGISTER: { + CONF_ADDRESS: 117, + CONF_HVAC_MODE_VALUES: { + CONF_HVAC_MODE_COOL: 0, + CONF_HVAC_MODE_HEAT: 1, + CONF_HVAC_MODE_HEAT_COOL: 2, + CONF_HVAC_MODE_DRY: 3, + }, + }, + CONF_HVAC_ONOFF_REGISTER: 117, + CONF_SWING_MODE_REGISTER: { + CONF_ADDRESS: [117], + }, }, ], ], @@ -743,6 +808,7 @@ async def test_climate_conflict_addresses(do_config) -> None: assert CONF_HVAC_MODE_REGISTER not in do_config[0] assert CONF_HVAC_ONOFF_REGISTER not in do_config[0] assert CONF_FAN_MODE_REGISTER not in do_config[0] + assert CONF_SWING_MODE_REGISTER not in do_config[0] @pytest.mark.parametrize( @@ -764,6 +830,25 @@ async def test_duplicate_fan_mode_validator(do_config) -> None: assert len(do_config[CONF_FAN_MODE_VALUES]) == 2 +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_ADDRESS: 11, + CONF_SWING_MODE_VALUES: { + CONF_SWING_MODE_SWING_ON: 7, + CONF_SWING_MODE_SWING_OFF: 9, + CONF_SWING_MODE_SWING_BOTH: 9, + }, + } + ], +) +async def test_duplicate_swing_mode_validator(do_config) -> None: + """Test duplicate modbus validator.""" + duplicate_swing_mode_validator(do_config) + assert len(do_config[CONF_SWING_MODE_VALUES]) == 2 + + @pytest.mark.parametrize( ("do_config", "sensor_cnt"), [ From 0d66d298ec57b8e50379cc2ec31cbebf771705c3 Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Sat, 6 Apr 2024 11:07:37 +0200 Subject: [PATCH 319/967] Enable Ruff RET504 (#114528) * Enable Ruff RET504 * fix test * Use noqa instead of cast * fix sonos RET504 --------- Co-authored-by: Martin Hjelmare --- homeassistant/__main__.py | 4 +- .../components/accuweather/diagnostics.py | 4 +- homeassistant/components/airly/diagnostics.py | 4 +- .../components/aladdin_connect/diagnostics.py | 4 +- .../components/alexa/capabilities.py | 5 +- homeassistant/components/amcrest/__init__.py | 3 +- homeassistant/components/aprilaire/climate.py | 4 +- .../components/arcam_fmj/media_player.py | 4 +- .../arris_tg2492lg/device_tracker.py | 3 +- homeassistant/components/asuswrt/bridge.py | 6 +- .../components/auth/mfa_setup_flow.py | 3 +- .../components/bluesound/media_player.py | 6 +- .../components/braviatv/diagnostics.py | 4 +- .../components/brother/diagnostics.py | 4 +- homeassistant/components/canary/__init__.py | 4 +- .../components/channels/media_player.py | 3 +- .../cisco_mobility_express/device_tracker.py | 3 +- homeassistant/components/cloud/__init__.py | 4 +- .../components/cloud/account_link.py | 2 +- .../components/coinbase/config_flow.py | 3 +- .../components/conversation/agent_manager.py | 3 +- .../components/conversation/default_agent.py | 7 +-- .../components/cppm_tracker/device_tracker.py | 3 +- homeassistant/components/daikin/__init__.py | 4 +- .../components/device_tracker/legacy.py | 3 +- .../devolo_home_control/diagnostics.py | 4 +- homeassistant/components/dlink/switch.py | 4 +- .../components/dlna_dmr/config_flow.py | 6 +- .../components/dlna_dms/config_flow.py | 6 +- homeassistant/components/dlna_dms/dms.py | 8 +-- .../components/ecowitt/diagnostics.py | 4 +- .../components/egardia/alarm_control_panel.py | 3 +- .../components/electric_kiwi/oauth2.py | 3 +- .../components/enocean/config_flow.py | 5 +- .../environment_canada/diagnostics.py | 4 +- homeassistant/components/ezviz/config_flow.py | 4 +- homeassistant/components/ffmpeg/__init__.py | 3 +- .../fireservicerota/binary_sensor.py | 4 +- .../components/fireservicerota/switch.py | 4 +- homeassistant/components/flipr/config_flow.py | 4 +- homeassistant/components/folder/sensor.py | 3 +- homeassistant/components/fritz/diagnostics.py | 4 +- .../frontier_silicon/browse_media.py | 4 +- homeassistant/components/gios/diagnostics.py | 4 +- .../components/goodwe/diagnostics.py | 4 +- .../components/google_assistant/smart_home.py | 4 +- .../components/google_assistant/trait.py | 8 +-- .../components/greeneye_monitor/sensor.py | 3 +- homeassistant/components/harmony/data.py | 8 +-- homeassistant/components/hko/coordinator.py | 6 +- .../homeassistant/exposed_entities.py | 4 +- .../components/http/data_validator.py | 3 +- homeassistant/components/image/__init__.py | 3 +- homeassistant/components/imap/diagnostics.py | 4 +- .../components/insteon/api/properties.py | 3 +- .../components/integration/sensor.py | 4 +- .../components/intellifire/number.py | 3 +- .../components/jellyfin/media_source.py | 12 +--- homeassistant/components/kodi/config_flow.py | 4 +- .../components/kostal_plenticore/helper.py | 10 +--- .../components/lg_soundbar/__init__.py | 3 +- homeassistant/components/light/__init__.py | 3 +- .../linear_garage_door/config_flow.py | 4 +- .../components/logbook/queries/devices.py | 3 +- .../logbook/queries/entities_and_devices.py | 3 +- .../components/luci/device_tracker.py | 3 +- homeassistant/components/matter/helpers.py | 4 +- .../components/media_source/local_source.py | 4 +- homeassistant/components/melcloud/climate.py | 3 +- .../components/melcloud/water_heater.py | 3 +- .../components/meteoclimatic/__init__.py | 3 +- homeassistant/components/mjpeg/camera.py | 3 +- .../components/mobile_app/helpers.py | 3 +- homeassistant/components/mqtt/client.py | 3 +- homeassistant/components/mqtt/trigger.py | 3 +- homeassistant/components/mqtt/util.py | 3 +- .../components/mysensors/config_flow.py | 3 +- homeassistant/components/mysensors/gateway.py | 3 +- homeassistant/components/mysensors/helpers.py | 3 +- .../components/mystrom/binary_sensor.py | 3 +- .../components/myuplink/binary_sensor.py | 6 +- .../components/myuplink/diagnostics.py | 4 +- homeassistant/components/myuplink/switch.py | 6 +- homeassistant/components/nam/diagnostics.py | 4 +- .../components/nextdns/diagnostics.py | 4 +- .../components/nuki/binary_sensor.py | 6 +- homeassistant/components/numato/sensor.py | 6 +- homeassistant/components/nut/__init__.py | 4 +- homeassistant/components/omnilogic/common.py | 4 +- .../components/opnsense/device_tracker.py | 6 +- .../components/overkiz/config_flow.py | 4 +- homeassistant/components/plaato/__init__.py | 3 +- .../components/proxmoxve/__init__.py | 3 +- homeassistant/components/qvr_pro/camera.py | 4 +- .../components/recorder/history/modern.py | 3 +- .../components/recorder/statistics.py | 3 +- homeassistant/components/roon/config_flow.py | 4 +- .../components/samsungtv/device_trigger.py | 3 +- homeassistant/components/schedule/__init__.py | 3 +- homeassistant/components/sensibo/sensor.py | 3 +- homeassistant/components/sharkiq/vacuum.py | 3 +- .../components/sky_hub/device_tracker.py | 3 +- homeassistant/components/smarttub/light.py | 4 +- homeassistant/components/sms/gateway.py | 4 +- .../components/sonos/media_browser.py | 3 +- .../components/soundtouch/media_player.py | 4 +- .../components/squeezebox/browse_media.py | 3 +- .../components/squeezebox/media_player.py | 4 +- homeassistant/components/statistics/sensor.py | 6 +- homeassistant/components/subaru/__init__.py | 3 +- .../components/subaru/diagnostics.py | 4 +- homeassistant/components/supla/__init__.py | 3 +- .../components/synology_dsm/sensor.py | 7 ++- homeassistant/components/tado/climate.py | 3 +- homeassistant/components/tado/water_heater.py | 4 +- .../components/tankerkoenig/diagnostics.py | 3 +- .../components/telegram_bot/__init__.py | 3 +- homeassistant/components/template/sensor.py | 3 +- .../components/tensorflow/image_processing.py | 4 +- .../components/tractive/diagnostics.py | 4 +- .../components/ubus/device_tracker.py | 3 +- .../components/uk_transport/sensor.py | 3 +- .../components/unifi/device_tracker.py | 4 +- .../components/unifiprotect/media_source.py | 8 +-- .../components/unifiprotect/views.py | 4 +- homeassistant/components/upnp/device.py | 4 +- homeassistant/components/uvc/camera.py | 3 +- .../components/vesync/diagnostics.py | 4 +- .../components/webostv/device_trigger.py | 3 +- homeassistant/components/ws66i/config_flow.py | 4 +- homeassistant/components/xiaomi_aqara/lock.py | 3 +- .../components/xiaomi_miio/__init__.py | 4 +- homeassistant/components/xmpp/notify.py | 8 +-- .../yamaha_musiccast/media_player.py | 4 +- .../zha/core/cluster_handlers/closures.py | 6 +- homeassistant/components/zha/device_action.py | 3 +- homeassistant/components/zha/light.py | 3 +- homeassistant/components/zha/sensor.py | 3 +- .../convert_device_diagnostics_to_fixture.py | 4 +- homeassistant/components/zwave_me/__init__.py | 3 +- homeassistant/config.py | 3 +- homeassistant/helpers/aiohttp_client.py | 4 +- homeassistant/helpers/restore_state.py | 3 +- .../helpers/schema_config_entry_flow.py | 6 +- homeassistant/helpers/script.py | 3 +- homeassistant/util/logging.py | 4 +- homeassistant/util/package.py | 3 +- homeassistant/util/timeout.py | 3 +- pyproject.toml | 1 - script/gen_requirements_all.py | 4 +- script/install_integration_requirements.py | 4 +- script/scaffold/__main__.py | 4 +- tests/components/airthings_ble/__init__.py | 3 +- .../components/alexa/test_smart_home_http.py | 3 +- tests/components/blebox/test_config_flow.py | 3 +- tests/components/cast/conftest.py | 3 +- tests/components/dlna_dmr/conftest.py | 6 +- .../components/dlna_dmr/test_media_player.py | 4 +- tests/components/dlna_dms/conftest.py | 3 +- tests/components/electric_kiwi/conftest.py | 3 +- tests/components/escea/test_config_flow.py | 3 +- .../esphome/test_voice_assistant.py | 2 +- tests/components/feedreader/test_init.py | 3 +- tests/components/generic/conftest.py | 3 +- tests/components/gree/common.py | 3 +- .../here_travel_time/test_config_flow.py | 6 +- tests/components/homekit_controller/common.py | 3 +- .../components/homematicip_cloud/conftest.py | 4 +- tests/components/http/test_data_validator.py | 3 +- tests/components/insteon/test_config_flow.py | 6 +- tests/components/lcn/conftest.py | 3 +- tests/components/lookin/__init__.py | 6 +- .../components/minecraft_server/test_init.py | 4 +- tests/components/modern_forms/__init__.py | 9 +-- tests/components/mysensors/conftest.py | 60 +++++++------------ tests/components/nest/test_media_source.py | 3 +- tests/components/nextcloud/conftest.py | 4 +- tests/components/overkiz/__init__.py | 4 +- tests/components/plex/conftest.py | 3 +- tests/components/ps4/test_media_player.py | 4 +- tests/components/refoss/__init__.py | 3 +- tests/components/shopping_list/test_todo.py | 3 +- tests/components/smtp/test_notify.py | 3 +- tests/components/srp_energy/conftest.py | 3 +- tests/components/stream/common.py | 3 +- .../tesla_wall_connector/conftest.py | 6 +- tests/components/transport_nsw/test_sensor.py | 6 +- tests/components/upb/test_config_flow.py | 3 +- tests/components/vesync/conftest.py | 9 +-- tests/components/zha/conftest.py | 3 +- tests/components/zha/test_cluster_handlers.py | 3 +- tests/components/zha/test_device.py | 3 +- tests/components/zha/test_gateway.py | 3 +- tests/components/zha/test_number.py | 4 +- tests/components/zha/test_select.py | 4 +- tests/components/zha/test_update.py | 4 +- tests/components/zwave_js/conftest.py | 6 +- tests/hassfest/test_requirements.py | 3 +- tests/helpers/test_update_coordinator.py | 3 +- tests/util/test_ssl.py | 3 +- 200 files changed, 252 insertions(+), 595 deletions(-) diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 4ed80c27bf0..0c0d535753c 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -146,9 +146,7 @@ def get_arguments() -> argparse.Namespace: help="Skips validation of operating system", ) - arguments = parser.parse_args() - - return arguments + return parser.parse_args() def check_threads() -> None: diff --git a/homeassistant/components/accuweather/diagnostics.py b/homeassistant/components/accuweather/diagnostics.py index e7bc41eaaf2..c4f04b209cf 100644 --- a/homeassistant/components/accuweather/diagnostics.py +++ b/homeassistant/components/accuweather/diagnostics.py @@ -23,9 +23,7 @@ async def async_get_config_entry_diagnostics( config_entry.entry_id ] - diagnostics_data = { + return { "config_entry_data": async_redact_data(dict(config_entry.data), TO_REDACT), "coordinator_data": coordinator.data, } - - return diagnostics_data diff --git a/homeassistant/components/airly/diagnostics.py b/homeassistant/components/airly/diagnostics.py index 1d63fbc8277..d21d126c60e 100644 --- a/homeassistant/components/airly/diagnostics.py +++ b/homeassistant/components/airly/diagnostics.py @@ -26,9 +26,7 @@ async def async_get_config_entry_diagnostics( """Return diagnostics for a config entry.""" coordinator: AirlyDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - diagnostics_data = { + return { "config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT), "coordinator_data": coordinator.data, } - - return diagnostics_data diff --git a/homeassistant/components/aladdin_connect/diagnostics.py b/homeassistant/components/aladdin_connect/diagnostics.py index b838ff79da3..67a31079f14 100644 --- a/homeassistant/components/aladdin_connect/diagnostics.py +++ b/homeassistant/components/aladdin_connect/diagnostics.py @@ -23,8 +23,6 @@ async def async_get_config_entry_diagnostics( acc: AladdinConnectClient = hass.data[DOMAIN][config_entry.entry_id] - diagnostics_data = { + return { "doors": async_redact_data(acc.doors, TO_REDACT), } - - return diagnostics_data diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index ecb7d5cb5a8..bc9b482109f 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -1764,10 +1764,7 @@ class AlexaRangeController(AlexaCapability): speed_list = self.entity.attributes.get(vacuum.ATTR_FAN_SPEED_LIST) speed = self.entity.attributes.get(vacuum.ATTR_FAN_SPEED) if speed_list is not None and speed is not None: - speed_index = next( - (i for i, v in enumerate(speed_list) if v == speed), None - ) - return speed_index + return next((i for i, v in enumerate(speed_list) if v == speed), None) # Valve Position if self.instance == f"{valve.DOMAIN}.{valve.ATTR_POSITION}": diff --git a/homeassistant/components/amcrest/__init__.py b/homeassistant/components/amcrest/__init__.py index cb6abff3f89..c12aa6d7916 100644 --- a/homeassistant/components/amcrest/__init__.py +++ b/homeassistant/components/amcrest/__init__.py @@ -203,8 +203,7 @@ class AmcrestChecker(ApiWrapper): async def async_command(self, *args: Any, **kwargs: Any) -> httpx.Response: """amcrest.ApiWrapper.command wrapper to catch errors.""" async with self._async_command_wrapper(): - ret = await super().async_command(*args, **kwargs) - return ret + return await super().async_command(*args, **kwargs) @asynccontextmanager async def async_stream_command( diff --git a/homeassistant/components/aprilaire/climate.py b/homeassistant/components/aprilaire/climate.py index 96c1e1ac981..2876d621aef 100644 --- a/homeassistant/components/aprilaire/climate.py +++ b/homeassistant/components/aprilaire/climate.py @@ -107,9 +107,7 @@ class AprilaireClimate(BaseAprilaireEntity, ClimateEntity): features = features | ClimateEntityFeature.PRESET_MODE - features = features | ClimateEntityFeature.FAN_MODE - - return features + return features | ClimateEntityFeature.FAN_MODE @property def current_humidity(self) -> int | None: diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index ac8d389304b..ca08a2b4d16 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -257,7 +257,7 @@ class ArcamFmj(MediaPlayerEntity): for preset in presets.values() ] - root = BrowseMedia( + return BrowseMedia( title="Arcam FMJ Receiver", media_class=MediaClass.DIRECTORY, media_content_id="root", @@ -267,8 +267,6 @@ class ArcamFmj(MediaPlayerEntity): children=radio, ) - return root - @convert_exception async def async_play_media( self, media_type: MediaType | str, media_id: str, **kwargs: Any diff --git a/homeassistant/components/arris_tg2492lg/device_tracker.py b/homeassistant/components/arris_tg2492lg/device_tracker.py index f9485636365..4f674a13c0e 100644 --- a/homeassistant/components/arris_tg2492lg/device_tracker.py +++ b/homeassistant/components/arris_tg2492lg/device_tracker.py @@ -49,11 +49,10 @@ class ArrisDeviceScanner(DeviceScanner): def get_device_name(self, device: str) -> str | None: """Return the name of the given device or None if we don't know.""" - name = next( + return next( (result.hostname for result in self.last_results if result.mac == device), None, ) - return name def _update_info(self) -> None: """Ensure the information from the Arris TG2492LG router is up to date.""" diff --git a/homeassistant/components/asuswrt/bridge.py b/homeassistant/components/asuswrt/bridge.py index 35f3a98251f..f255a3faad4 100644 --- a/homeassistant/components/asuswrt/bridge.py +++ b/homeassistant/components/asuswrt/bridge.py @@ -254,7 +254,7 @@ class AsusWrtLegacyBridge(AsusWrtBridge): async def async_get_available_sensors(self) -> dict[str, dict[str, Any]]: """Return a dictionary of available sensors for this bridge.""" sensors_temperatures = await self._get_available_temperature_sensors() - sensors_types = { + return { SENSORS_TYPE_BYTES: { KEY_SENSORS: SENSORS_BYTES, KEY_METHOD: self._get_bytes, @@ -272,7 +272,6 @@ class AsusWrtLegacyBridge(AsusWrtBridge): KEY_METHOD: self._get_temperatures, }, } - return sensors_types async def _get_available_temperature_sensors(self) -> list[str]: """Check which temperature information is available on the router.""" @@ -351,7 +350,7 @@ class AsusWrtHttpBridge(AsusWrtBridge): """Return a dictionary of available sensors for this bridge.""" sensors_temperatures = await self._get_available_temperature_sensors() sensors_loadavg = await self._get_loadavg_sensors_availability() - sensors_types = { + return { SENSORS_TYPE_BYTES: { KEY_SENSORS: SENSORS_BYTES, KEY_METHOD: self._get_bytes, @@ -369,7 +368,6 @@ class AsusWrtHttpBridge(AsusWrtBridge): KEY_METHOD: self._get_temperatures, }, } - return sensors_types async def _get_available_temperature_sensors(self) -> list[str]: """Check which temperature information is available on the router.""" diff --git a/homeassistant/components/auth/mfa_setup_flow.py b/homeassistant/components/auth/mfa_setup_flow.py index aee08186267..aaa1dbaedbf 100644 --- a/homeassistant/components/auth/mfa_setup_flow.py +++ b/homeassistant/components/auth/mfa_setup_flow.py @@ -147,8 +147,7 @@ def _prepare_result_json( ) -> data_entry_flow.FlowResult: """Convert result to JSON.""" if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY: - data = result.copy() - return data + return result.copy() if result["type"] != data_entry_flow.FlowResultType.FORM: return result diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 9377557d025..cb6f013dbf8 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -863,8 +863,6 @@ class BluesoundPlayer(MediaPlayerEntity): if self._group_name is None: return None - bluesound_group = [] - device_group = self._group_name.split("+") sorted_entities = sorted( @@ -872,14 +870,12 @@ class BluesoundPlayer(MediaPlayerEntity): key=lambda entity: entity.is_master, reverse=True, ) - bluesound_group = [ + return [ entity.name for entity in sorted_entities if entity.bluesound_device_name in device_group ] - return bluesound_group - async def async_unjoin(self): """Unjoin the player from a group.""" if self._master is None: diff --git a/homeassistant/components/braviatv/diagnostics.py b/homeassistant/components/braviatv/diagnostics.py index 917572ffcca..b74a8a3ebdb 100644 --- a/homeassistant/components/braviatv/diagnostics.py +++ b/homeassistant/components/braviatv/diagnostics.py @@ -21,9 +21,7 @@ async def async_get_config_entry_diagnostics( device_info = await coordinator.client.get_system_info() - diagnostics_data = { + return { "config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT), "device_info": async_redact_data(device_info, TO_REDACT), } - - return diagnostics_data diff --git a/homeassistant/components/brother/diagnostics.py b/homeassistant/components/brother/diagnostics.py index a4afb385f8d..ee5eedd84cb 100644 --- a/homeassistant/components/brother/diagnostics.py +++ b/homeassistant/components/brother/diagnostics.py @@ -20,11 +20,9 @@ async def async_get_config_entry_diagnostics( config_entry.entry_id ] - diagnostics_data = { + return { "info": dict(config_entry.data), "data": asdict(coordinator.data), "model": coordinator.brother.model, "firmware": coordinator.brother.firmware, } - - return diagnostics_data diff --git a/homeassistant/components/canary/__init__.py b/homeassistant/components/canary/__init__.py index 60ce50484d8..f879c308a88 100644 --- a/homeassistant/components/canary/__init__.py +++ b/homeassistant/components/canary/__init__.py @@ -140,10 +140,8 @@ async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> Non def _get_canary_api_instance(entry: ConfigEntry) -> Api: """Initialize a new instance of CanaryApi.""" - canary = Api( + return Api( entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], entry.options.get(CONF_TIMEOUT, DEFAULT_TIMEOUT), ) - - return canary diff --git a/homeassistant/components/channels/media_player.py b/homeassistant/components/channels/media_player.py index 002ec8d4efb..2b8fc4a2b3e 100644 --- a/homeassistant/components/channels/media_player.py +++ b/homeassistant/components/channels/media_player.py @@ -167,8 +167,7 @@ class ChannelsPlayer(MediaPlayerEntity): @property def source_list(self): """List of favorite channels.""" - sources = [channel["name"] for channel in self.favorite_channels] - return sources + return [channel["name"] for channel in self.favorite_channels] @property def is_volume_muted(self): diff --git a/homeassistant/components/cisco_mobility_express/device_tracker.py b/homeassistant/components/cisco_mobility_express/device_tracker.py index c156f43942e..d96ab54a68f 100644 --- a/homeassistant/components/cisco_mobility_express/device_tracker.py +++ b/homeassistant/components/cisco_mobility_express/device_tracker.py @@ -72,11 +72,10 @@ class CiscoMEDeviceScanner(DeviceScanner): def get_device_name(self, device): """Return the name of the given device or None if we don't know.""" - name = next( + return next( (result.clId for result in self.last_results if result.macaddr == device), None, ) - return name def get_extra_attributes(self, device): """Get extra attributes of a device. diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 80c2e86a2a3..d85415cf9eb 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -396,6 +396,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.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/cloud/account_link.py b/homeassistant/components/cloud/account_link.py index df2789663c0..784de14e6ad 100644 --- a/homeassistant/components/cloud/account_link.py +++ b/homeassistant/components/cloud/account_link.py @@ -65,7 +65,7 @@ async def _get_services(hass: HomeAssistant) -> list[dict[str, Any]]: services: list[dict[str, Any]] if DATA_SERVICES in hass.data: services = hass.data[DATA_SERVICES] - return services + return services # noqa: RET504 try: services = await account_link.async_fetch_available_services(hass.data[DOMAIN]) diff --git a/homeassistant/components/coinbase/config_flow.py b/homeassistant/components/coinbase/config_flow.py index dafa50bafcb..71ebcec65ee 100644 --- a/homeassistant/components/coinbase/config_flow.py +++ b/homeassistant/components/coinbase/config_flow.py @@ -50,8 +50,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema( def get_user_from_client(api_key, api_token): """Get the user name from Coinbase API credentials.""" client = Client(api_key, api_token) - user = client.get_current_user() - return user + return client.get_current_user() async def validate_api(hass: HomeAssistant, data): diff --git a/homeassistant/components/conversation/agent_manager.py b/homeassistant/components/conversation/agent_manager.py index 838539b4992..9f31ccd6c62 100644 --- a/homeassistant/components/conversation/agent_manager.py +++ b/homeassistant/components/conversation/agent_manager.py @@ -84,7 +84,7 @@ async def async_converse( language = hass.config.language _LOGGER.debug("Processing in %s: %s", language, text) - result = await method( + return await method( ConversationInput( text=text, context=context, @@ -93,7 +93,6 @@ async def async_converse( language=language, ) ) - return result class AgentManager: diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index f652c5ee0eb..32cec18dfef 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -240,7 +240,7 @@ class DefaultAgent(ConversationEntity): slot_lists = self._make_slot_lists() intent_context = self._make_intent_context(user_input) - result = await self.hass.async_add_executor_job( + return await self.hass.async_add_executor_job( self._recognize, user_input, lang_intents, @@ -249,8 +249,6 @@ class DefaultAgent(ConversationEntity): language, ) - return result - async def async_process(self, user_input: ConversationInput) -> ConversationResult: """Process a sentence.""" language = user_input.language or self.hass.config.language @@ -901,8 +899,7 @@ class DefaultAgent(ConversationEntity): # Force rebuild on next use self._trigger_intents = None - unregister = functools.partial(self._unregister_trigger, trigger_data) - return unregister + return functools.partial(self._unregister_trigger, trigger_data) def _rebuild_trigger_intents(self) -> None: """Rebuild the HassIL intents object from the current trigger sentences.""" diff --git a/homeassistant/components/cppm_tracker/device_tracker.py b/homeassistant/components/cppm_tracker/device_tracker.py index 8978028641d..9b1ebbb1ed8 100644 --- a/homeassistant/components/cppm_tracker/device_tracker.py +++ b/homeassistant/components/cppm_tracker/device_tracker.py @@ -64,10 +64,9 @@ class CPPMDeviceScanner(DeviceScanner): def get_device_name(self, device): """Retrieve device name.""" - name = next( + return next( (result["name"] for result in self.results if result["mac"] == device), None ) - return name def get_cppm_data(self): """Retrieve data from Aruba Clearpass and return parsed result.""" diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index f0b62e95b1f..6f1196c7721 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -97,9 +97,7 @@ async def daikin_api_setup( _LOGGER.error("Unexpected error creating device %s", host) return None - api = DaikinApi(device) - - return api + return DaikinApi(device) class DaikinApi: diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index 1d1d4645bb4..e292e97a8ec 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -558,8 +558,7 @@ async def get_tracker(hass: HomeAssistant, config: ConfigType) -> DeviceTracker: track_new = defaults.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW) devices = await async_load_config(yaml_path, hass, consider_home) - tracker = DeviceTracker(hass, consider_home, track_new, defaults, devices) - return tracker + return DeviceTracker(hass, consider_home, track_new, defaults, devices) class DeviceTracker: diff --git a/homeassistant/components/devolo_home_control/diagnostics.py b/homeassistant/components/devolo_home_control/diagnostics.py index 753d04db0a3..33652f8e0bc 100644 --- a/homeassistant/components/devolo_home_control/diagnostics.py +++ b/homeassistant/components/devolo_home_control/diagnostics.py @@ -41,9 +41,7 @@ async def async_get_config_entry_diagnostics( for gateway in gateways ] - diag_data = { + return { "entry": async_redact_data(entry.as_dict(), TO_REDACT), "device_info": device_info, } - - return diag_data diff --git a/homeassistant/components/dlink/switch.py b/homeassistant/components/dlink/switch.py index a37caa6700c..36bfe4fb391 100644 --- a/homeassistant/components/dlink/switch.py +++ b/homeassistant/components/dlink/switch.py @@ -51,13 +51,11 @@ class SmartPlugSwitch(DLinkEntity, SwitchEntity): except ValueError: total_consumption = None - attrs = { + return { ATTR_TOTAL_CONSUMPTION: total_consumption, ATTR_TEMPERATURE: temperature, } - return attrs - @property def is_on(self) -> bool: """Return true if switch is on.""" diff --git a/homeassistant/components/dlna_dmr/config_flow.py b/homeassistant/components/dlna_dmr/config_flow.py index 9d95ba3883e..837bfc456d8 100644 --- a/homeassistant/components/dlna_dmr/config_flow.py +++ b/homeassistant/components/dlna_dmr/config_flow.py @@ -339,11 +339,7 @@ class DlnaDmrFlowHandler(ConfigFlow, domain=DOMAIN): entry.unique_id for entry in self._async_current_entries(include_ignore=False) } - discoveries = [ - disc for disc in discoveries if disc.ssdp_udn not in current_unique_ids - ] - - return discoveries + return [disc for disc in discoveries if disc.ssdp_udn not in current_unique_ids] class DlnaDmrOptionsFlowHandler(OptionsFlow): diff --git a/homeassistant/components/dlna_dms/config_flow.py b/homeassistant/components/dlna_dms/config_flow.py index 480f45ee95b..b50dc7ff227 100644 --- a/homeassistant/components/dlna_dms/config_flow.py +++ b/homeassistant/components/dlna_dms/config_flow.py @@ -179,8 +179,4 @@ class DlnaDmsFlowHandler(ConfigFlow, domain=DOMAIN): entry.unique_id for entry in self._async_current_entries(include_ignore=False) } - discoveries = [ - disc for disc in discoveries if disc.ssdp_udn not in current_unique_ids - ] - - return discoveries + return [disc for disc in discoveries if disc.ssdp_udn not in current_unique_ids] diff --git a/homeassistant/components/dlna_dms/dms.py b/homeassistant/components/dlna_dms/dms.py index 66c328f2e92..2312c7d2e3d 100644 --- a/homeassistant/components/dlna_dms/dms.py +++ b/homeassistant/components/dlna_dms/dms.py @@ -516,7 +516,7 @@ class DmsDeviceSource: if isinstance(child, didl_lite.DidlObject) ] - media_source = BrowseMediaSource( + return BrowseMediaSource( domain=DOMAIN, identifier=self._make_identifier(Action.SEARCH, query), media_class=MediaClass.DIRECTORY, @@ -527,8 +527,6 @@ class DmsDeviceSource: children=children, ) - return media_source - def _didl_to_play_media(self, item: didl_lite.DidlObject) -> DidlPlayMedia: """Return the first playable resource from a DIDL-Lite object.""" assert self._device @@ -583,7 +581,7 @@ class DmsDeviceSource: mime_type = _resource_mime_type(item.res[0]) if item.res else None media_content_type = mime_type or item.upnp_class - media_source = BrowseMediaSource( + return BrowseMediaSource( domain=DOMAIN, identifier=self._make_identifier(Action.OBJECT, item.id), media_class=MEDIA_CLASS_MAP.get(item.upnp_class, ""), @@ -595,8 +593,6 @@ class DmsDeviceSource: thumbnail=self._didl_thumbnail_url(item), ) - return media_source - def _didl_thumbnail_url(self, item: didl_lite.DidlObject) -> str | None: """Return absolute URL of a thumbnail for a DIDL-Lite object. diff --git a/homeassistant/components/ecowitt/diagnostics.py b/homeassistant/components/ecowitt/diagnostics.py index e4aecc1c07b..db7d2e0989d 100644 --- a/homeassistant/components/ecowitt/diagnostics.py +++ b/homeassistant/components/ecowitt/diagnostics.py @@ -22,7 +22,7 @@ async def async_get_device_diagnostics( station = ecowitt.stations[station_id] - data = { + return { "device": { "name": station.station, "model": station.model, @@ -36,5 +36,3 @@ async def async_get_device_diagnostics( if sensor.station.key == station_id }, } - - return data diff --git a/homeassistant/components/egardia/alarm_control_panel.py b/homeassistant/components/egardia/alarm_control_panel.py index c58396ae947..dec4750d219 100644 --- a/homeassistant/components/egardia/alarm_control_panel.py +++ b/homeassistant/components/egardia/alarm_control_panel.py @@ -102,7 +102,7 @@ class EgardiaAlarm(alarm.AlarmControlPanelEntity): def lookupstatusfromcode(self, statuscode): """Look at the rs_codes and returns the status from the code.""" - status = next( + return next( ( status_group.upper() for status_group, codes in self._rs_codes.items() @@ -111,7 +111,6 @@ class EgardiaAlarm(alarm.AlarmControlPanelEntity): ), "UNKNOWN", ) - return status def parsestatus(self, status): """Parse the status.""" diff --git a/homeassistant/components/electric_kiwi/oauth2.py b/homeassistant/components/electric_kiwi/oauth2.py index 864550991f5..9a6c4cd22a5 100644 --- a/homeassistant/components/electric_kiwi/oauth2.py +++ b/homeassistant/components/electric_kiwi/oauth2.py @@ -73,5 +73,4 @@ class ElectricKiwiLocalOAuth2Implementation(AuthImplementation): resp = await session.post(self.token_url, data=data, headers=headers) resp.raise_for_status() - resp_json = cast(dict, await resp.json()) - return resp_json + return cast(dict, await resp.json()) diff --git a/homeassistant/components/enocean/config_flow.py b/homeassistant/components/enocean/config_flow.py index 1137eb23256..157d58bbf23 100644 --- a/homeassistant/components/enocean/config_flow.py +++ b/homeassistant/components/enocean/config_flow.py @@ -81,10 +81,7 @@ class EnOceanFlowHandler(ConfigFlow, domain=DOMAIN): async def validate_enocean_conf(self, user_input) -> bool: """Return True if the user_input contains a valid dongle path.""" dongle_path = user_input[CONF_DEVICE] - path_is_valid = await self.hass.async_add_executor_job( - dongle.validate_path, dongle_path - ) - return path_is_valid + return await self.hass.async_add_executor_job(dongle.validate_path, dongle_path) def create_enocean_entry(self, user_input): """Create an entry for the provided configuration.""" diff --git a/homeassistant/components/environment_canada/diagnostics.py b/homeassistant/components/environment_canada/diagnostics.py index 63f8bb72189..0fb565fda59 100644 --- a/homeassistant/components/environment_canada/diagnostics.py +++ b/homeassistant/components/environment_canada/diagnostics.py @@ -21,9 +21,7 @@ async def async_get_config_entry_diagnostics( coordinators = hass.data[DOMAIN][config_entry.entry_id] weather_coord = coordinators["weather_coordinator"] - diagnostics_data = { + return { "config_entry_data": async_redact_data(dict(config_entry.data), TO_REDACT), "weather_data": dict(weather_coord.ec_data.conditions), } - - return diagnostics_data diff --git a/homeassistant/components/ezviz/config_flow.py b/homeassistant/components/ezviz/config_flow.py index a453398a17a..a17d8312700 100644 --- a/homeassistant/components/ezviz/config_flow.py +++ b/homeassistant/components/ezviz/config_flow.py @@ -70,15 +70,13 @@ def _validate_and_create_auth(data: dict) -> dict[str, Any]: ezviz_token = ezviz_client.login() - auth_data = { + return { CONF_SESSION_ID: ezviz_token[CONF_SESSION_ID], CONF_RFSESSION_ID: ezviz_token[CONF_RFSESSION_ID], CONF_URL: ezviz_token["api_url"], CONF_TYPE: ATTR_TYPE_CLOUD, } - return auth_data - def _test_camera_rtsp_creds(data: dict) -> None: """Try DESCRIBE on RTSP camera with credentials.""" diff --git a/homeassistant/components/ffmpeg/__init__.py b/homeassistant/components/ffmpeg/__init__.py index 09d8e2401f0..e5086166ff5 100644 --- a/homeassistant/components/ffmpeg/__init__.py +++ b/homeassistant/components/ffmpeg/__init__.py @@ -133,10 +133,9 @@ async def async_get_image( else: extra_cmd += " " + size_cmd - image = await asyncio.shield( + return await asyncio.shield( ffmpeg.get_image(input_source, output_format=output_format, extra_cmd=extra_cmd) ) - return image class FFmpegManager: diff --git a/homeassistant/components/fireservicerota/binary_sensor.py b/homeassistant/components/fireservicerota/binary_sensor.py index 9938f6ab096..a22991f2008 100644 --- a/homeassistant/components/fireservicerota/binary_sensor.py +++ b/homeassistant/components/fireservicerota/binary_sensor.py @@ -63,7 +63,7 @@ class ResponseBinarySensor(CoordinatorEntity, BinarySensorEntity): return attr data = self.coordinator.data - attr = { + return { key: data[key] for key in ( "start_time", @@ -77,5 +77,3 @@ class ResponseBinarySensor(CoordinatorEntity, BinarySensorEntity): ) if key in data } - - return attr diff --git a/homeassistant/components/fireservicerota/switch.py b/homeassistant/components/fireservicerota/switch.py index 04e1e4ef5eb..22287653788 100644 --- a/homeassistant/components/fireservicerota/switch.py +++ b/homeassistant/components/fireservicerota/switch.py @@ -71,7 +71,7 @@ class ResponseSwitch(SwitchEntity): return attr data = self._state_attributes - attr = { + return { key: data[key] for key in ( "user_name", @@ -87,8 +87,6 @@ class ResponseSwitch(SwitchEntity): if key in data } - return attr - async def async_turn_on(self, **kwargs: Any) -> None: """Send Acknowledge response status.""" await self.async_set_response(True) diff --git a/homeassistant/components/flipr/config_flow.py b/homeassistant/components/flipr/config_flow.py index fb7985b9602..0b0230f536e 100644 --- a/homeassistant/components/flipr/config_flow.py +++ b/homeassistant/components/flipr/config_flow.py @@ -91,9 +91,7 @@ class FliprConfigFlow(ConfigFlow, domain=DOMAIN): # Instantiates the flipr API that does not require async since it is has no network access. client = FliprAPIRestClient(self._username, self._password) - flipr_ids = await self.hass.async_add_executor_job(client.search_flipr_ids) - - return flipr_ids + return await self.hass.async_add_executor_job(client.search_flipr_ids) async def async_step_flipr_id( self, user_input: dict[str, str] | None = None diff --git a/homeassistant/components/folder/sensor.py b/homeassistant/components/folder/sensor.py index c4454eba800..6c8e4fc63a9 100644 --- a/homeassistant/components/folder/sensor.py +++ b/homeassistant/components/folder/sensor.py @@ -39,8 +39,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def get_files_list(folder_path: str, filter_term: str) -> list[str]: """Return the list of files, applying filter.""" query = folder_path + filter_term - files_list = glob.glob(query) - return files_list + return glob.glob(query) def get_size(files_list: list[str]) -> int: diff --git a/homeassistant/components/fritz/diagnostics.py b/homeassistant/components/fritz/diagnostics.py index 3136f03f95b..c4725b99e43 100644 --- a/homeassistant/components/fritz/diagnostics.py +++ b/homeassistant/components/fritz/diagnostics.py @@ -21,7 +21,7 @@ async def async_get_config_entry_diagnostics( """Return diagnostics for a config entry.""" avm_wrapper: AvmWrapper = hass.data[DOMAIN][entry.entry_id] - diag_data = { + return { "entry": async_redact_data(entry.as_dict(), TO_REDACT), "device_info": { "model": avm_wrapper.model, @@ -51,5 +51,3 @@ async def async_get_config_entry_diagnostics( "wan_link_properties": await avm_wrapper.async_get_wan_link_properties(), }, } - - return diag_data diff --git a/homeassistant/components/frontier_silicon/browse_media.py b/homeassistant/components/frontier_silicon/browse_media.py index da5169b8e7c..0b51cb767c7 100644 --- a/homeassistant/components/frontier_silicon/browse_media.py +++ b/homeassistant/components/frontier_silicon/browse_media.py @@ -94,7 +94,7 @@ async def browse_top_level(current_mode, afsapi: AFSAPI): for top_level_media_content_id, name in TOP_LEVEL_DIRECTORIES.items() ] - library_info = BrowseMedia( + return BrowseMedia( media_class=MediaClass.DIRECTORY, media_content_id="library", media_content_type=MediaType.CHANNELS, @@ -105,8 +105,6 @@ async def browse_top_level(current_mode, afsapi: AFSAPI): children_media_class=MediaClass.DIRECTORY, ) - return library_info - async def browse_node( afsapi: AFSAPI, diff --git a/homeassistant/components/gios/diagnostics.py b/homeassistant/components/gios/diagnostics.py index 1cdd9299a1c..0bdd8f3a7ef 100644 --- a/homeassistant/components/gios/diagnostics.py +++ b/homeassistant/components/gios/diagnostics.py @@ -18,9 +18,7 @@ async def async_get_config_entry_diagnostics( """Return diagnostics for a config entry.""" coordinator: GiosDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - diagnostics_data = { + return { "config_entry": config_entry.as_dict(), "coordinator_data": asdict(coordinator.data), } - - return diagnostics_data diff --git a/homeassistant/components/goodwe/diagnostics.py b/homeassistant/components/goodwe/diagnostics.py index 600f02b9b7e..66806d31589 100644 --- a/homeassistant/components/goodwe/diagnostics.py +++ b/homeassistant/components/goodwe/diagnostics.py @@ -18,7 +18,7 @@ async def async_get_config_entry_diagnostics( """Return diagnostics for a config entry.""" inverter: Inverter = hass.data[DOMAIN][config_entry.entry_id][KEY_INVERTER] - diagnostics_data = { + return { "config_entry": config_entry.as_dict(), "inverter": { "model_name": inverter.model_name, @@ -32,5 +32,3 @@ async def async_get_config_entry_diagnostics( "arm_svn_version": inverter.arm_svn_version, }, } - - return diagnostics_data diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index df55fc0d7c8..bee1c8443fa 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -139,9 +139,7 @@ async def async_devices_sync( await data.config.async_connect_agent_user(agent_user_id) devices = await async_devices_sync_response(hass, data.config, agent_user_id) - response = create_sync_response(agent_user_id, devices) - - return response + return create_sync_response(agent_user_id, devices) @HANDLERS.register("action.devices.QUERY") diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index dd1e0cb3409..3efeabfa778 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -1927,9 +1927,7 @@ class ModesTrait(_Trait): # Shortcut since all domains are currently unique break - payload = {"availableModes": modes} - - return payload + return {"availableModes": modes} def query_attributes(self): """Return current modes.""" @@ -2104,9 +2102,7 @@ class InputSelectorTrait(_Trait): for source in sourcelist ] - payload = {"availableInputs": inputs, "orderedInputs": True} - - return payload + return {"availableInputs": inputs, "orderedInputs": True} def query_attributes(self): """Return current modes.""" diff --git a/homeassistant/components/greeneye_monitor/sensor.py b/homeassistant/components/greeneye_monitor/sensor.py index 1290fc9459a..d9ab6b16960 100644 --- a/homeassistant/components/greeneye_monitor/sensor.py +++ b/homeassistant/components/greeneye_monitor/sensor.py @@ -220,12 +220,11 @@ class PulseCounter(GEMSensor): if self._sensor.pulses_per_second is None: return None - result = ( + return ( self._sensor.pulses_per_second * self._counted_quantity_per_pulse * self._seconds_per_time_unit ) - return result @property def _seconds_per_time_unit(self) -> int: diff --git a/homeassistant/components/harmony/data.py b/homeassistant/components/harmony/data.py index 992eaf52326..cdb31b4388c 100644 --- a/homeassistant/components/harmony/data.py +++ b/homeassistant/components/harmony/data.py @@ -48,17 +48,13 @@ class HarmonyData(HarmonySubscriberMixin): def activity_names(self) -> list[str]: """Names of all the remotes activities.""" activity_infos = self.activities - activities = [activity["label"] for activity in activity_infos] - - return activities + return [activity["label"] for activity in activity_infos] @property def device_names(self): """Names of all of the devices connected to the hub.""" device_infos = self._client.config.get("device", []) - devices = [device["label"] for device in device_infos] - - return devices + return [device["label"] for device in device_infos] @property def unique_id(self): diff --git a/homeassistant/components/hko/coordinator.py b/homeassistant/components/hko/coordinator.py index c7d80ae299e..566ba5dcf5e 100644 --- a/homeassistant/components/hko/coordinator.py +++ b/homeassistant/components/hko/coordinator.py @@ -101,7 +101,7 @@ class HKOUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): def _convert_current(self, data: dict[str, Any]) -> dict[str, Any]: """Return temperature and humidity in the appropriate format.""" - current = { + return { API_HUMIDITY: data[API_HUMIDITY][API_DATA][0][API_VALUE], API_TEMPERATURE: next( ( @@ -112,12 +112,11 @@ class HKOUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): 0, ), } - return current def _convert_forecast(self, data: dict[str, Any]) -> dict[str, Any]: """Return daily forecast in the appropriate format.""" date = data[API_FORECAST_DATE] - forecast = { + return { ATTR_FORECAST_CONDITION: self._convert_icon_condition( data[API_FORECAST_ICON], data[API_FORECAST_WEATHER] ), @@ -125,7 +124,6 @@ class HKOUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): ATTR_FORECAST_TEMP_LOW: data[API_FORECAST_MIN_TEMP][API_VALUE], ATTR_FORECAST_TIME: f"{date[0:4]}-{date[4:6]}-{date[6:8]}T00:00:00+08:00", } - return forecast def _convert_icon_condition(self, icon_code: int, info: str) -> str: """Return the condition corresponding to an icon code.""" diff --git a/homeassistant/components/homeassistant/exposed_entities.py b/homeassistant/components/homeassistant/exposed_entities.py index 135b2847520..4d6d9724ecb 100644 --- a/homeassistant/components/homeassistant/exposed_entities.py +++ b/homeassistant/components/homeassistant/exposed_entities.py @@ -259,7 +259,7 @@ class ExposedEntities: if assistant in registry_entry.options: if "should_expose" in registry_entry.options[assistant]: should_expose = registry_entry.options[assistant]["should_expose"] - return should_expose + return should_expose # noqa: RET504 if self.async_get_expose_new_entities(assistant): should_expose = self._is_default_exposed(entity_id, registry_entry) @@ -286,7 +286,7 @@ class ExposedEntities: ) and assistant in exposed_entity.assistants: if "should_expose" in exposed_entity.assistants[assistant]: should_expose = exposed_entity.assistants[assistant]["should_expose"] - return should_expose + return should_expose # noqa: RET504 if self.async_get_expose_new_entities(assistant): should_expose = self._is_default_exposed(entity_id, None) diff --git a/homeassistant/components/http/data_validator.py b/homeassistant/components/http/data_validator.py index 749c4f63a2f..e1ba1caae56 100644 --- a/homeassistant/components/http/data_validator.py +++ b/homeassistant/components/http/data_validator.py @@ -70,7 +70,6 @@ class RequestDataValidator: f"Message format incorrect: {err}", HTTPStatus.BAD_REQUEST ) - result = await method(view, request, data, *args, **kwargs) - return result + return await method(view, request, data, *args, **kwargs) return wrapper diff --git a/homeassistant/components/image/__init__.py b/homeassistant/components/image/__init__.py index 5b0d1a2a330..f40958a28ea 100644 --- a/homeassistant/components/image/__init__.py +++ b/homeassistant/components/image/__init__.py @@ -82,8 +82,7 @@ async def _async_get_image(image_entity: ImageEntity, timeout: int) -> Image: async with asyncio.timeout(timeout): if image_bytes := await image_entity.async_image(): content_type = valid_image_content_type(image_entity.content_type) - image = Image(content_type, image_bytes) - return image + return Image(content_type, image_bytes) raise HomeAssistantError("Unable to get image") diff --git a/homeassistant/components/imap/diagnostics.py b/homeassistant/components/imap/diagnostics.py index 467f19d6338..8afe3e327ba 100644 --- a/homeassistant/components/imap/diagnostics.py +++ b/homeassistant/components/imap/diagnostics.py @@ -31,9 +31,7 @@ def _async_get_diagnostics( redacted_config = async_redact_data(entry.data, REDACT_CONFIG) coordinator: ImapDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - data = { + return { "config": redacted_config, "event": coordinator.diagnostics_data, } - - return data diff --git a/homeassistant/components/insteon/api/properties.py b/homeassistant/components/insteon/api/properties.py index 7fac5439f56..20e798dded0 100644 --- a/homeassistant/components/insteon/api/properties.py +++ b/homeassistant/components/insteon/api/properties.py @@ -139,8 +139,7 @@ def property_to_dict(prop): modified = value == prop.new_value if prop.value_type in [ToggleMode, RelayMode] or prop.name == RAMP_RATE_IN_SEC: value = str(value).lower() - prop_dict = {"name": prop.name, "value": value, "modified": modified} - return prop_dict + return {"name": prop.name, "value": value, "modified": modified} def update_property(device, prop_name, value): diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index cf9ba5f2950..65e967d2af7 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -466,12 +466,10 @@ class IntegrationSensor(RestoreSensor): @property def extra_state_attributes(self) -> dict[str, str] | None: """Return the state attributes of the sensor.""" - state_attr = { + return { ATTR_SOURCE_ID: self._source_entity, } - return state_attr - @property def extra_restore_state_data(self) -> IntegrationSensorExtraStoredData: """Return sensor specific state data to be restored.""" diff --git a/homeassistant/components/intellifire/number.py b/homeassistant/components/intellifire/number.py index dca7a74c78e..17ed3b7bd27 100644 --- a/homeassistant/components/intellifire/number.py +++ b/homeassistant/components/intellifire/number.py @@ -61,8 +61,7 @@ class IntellifireFlameControlEntity(IntellifireEntity, NumberEntity): def native_value(self) -> float | None: """Return the current Flame Height segment number value.""" # UI uses 1-5 for flame height, backing lib uses 0-4 - value = self.coordinator.read_api.data.flameheight + 1 - return value + return self.coordinator.read_api.data.flameheight + 1 async def async_set_native_value(self, value: float) -> None: """Slider change.""" diff --git a/homeassistant/components/jellyfin/media_source.py b/homeassistant/components/jellyfin/media_source.py index add04d1a1ec..6d982458378 100644 --- a/homeassistant/components/jellyfin/media_source.py +++ b/homeassistant/components/jellyfin/media_source.py @@ -286,7 +286,7 @@ class JellyfinSource(MediaSource): mime_type = _media_mime_type(track) thumbnail_url = self._get_thumbnail_url(track) - result = BrowseMediaSource( + return BrowseMediaSource( domain=DOMAIN, identifier=track_id, media_class=MediaClass.TRACK, @@ -297,8 +297,6 @@ class JellyfinSource(MediaSource): thumbnail=thumbnail_url, ) - return result - async def _build_movie_library( self, library: dict[str, Any], include_children: bool ) -> BrowseMediaSource: @@ -347,7 +345,7 @@ class JellyfinSource(MediaSource): mime_type = _media_mime_type(movie) thumbnail_url = self._get_thumbnail_url(movie) - result = BrowseMediaSource( + return BrowseMediaSource( domain=DOMAIN, identifier=movie_id, media_class=MediaClass.MOVIE, @@ -358,8 +356,6 @@ class JellyfinSource(MediaSource): thumbnail=thumbnail_url, ) - return result - async def _build_tv_library( self, library: dict[str, Any], include_children: bool ) -> BrowseMediaSource: @@ -486,7 +482,7 @@ class JellyfinSource(MediaSource): mime_type = _media_mime_type(episode) thumbnail_url = self._get_thumbnail_url(episode) - result = BrowseMediaSource( + return BrowseMediaSource( domain=DOMAIN, identifier=episode_id, media_class=MediaClass.EPISODE, @@ -497,8 +493,6 @@ class JellyfinSource(MediaSource): thumbnail=thumbnail_url, ) - return result - async def _get_children( self, parent_id: str, item_type: str ) -> list[dict[str, Any]]: diff --git a/homeassistant/components/kodi/config_flow.py b/homeassistant/components/kodi/config_flow.py index 1d9d1ca4f7c..b4d9c575122 100644 --- a/homeassistant/components/kodi/config_flow.py +++ b/homeassistant/components/kodi/config_flow.py @@ -300,7 +300,7 @@ class KodiConfigFlow(ConfigFlow, domain=DOMAIN): @callback def _get_data(self): - data = { + return { CONF_NAME: self._name, CONF_HOST: self._host, CONF_PORT: self._port, @@ -311,8 +311,6 @@ class KodiConfigFlow(ConfigFlow, domain=DOMAIN): CONF_TIMEOUT: DEFAULT_TIMEOUT, } - return data - class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/kostal_plenticore/helper.py b/homeassistant/components/kostal_plenticore/helper.py index 4a4e6539f03..37666557eff 100644 --- a/homeassistant/components/kostal_plenticore/helper.py +++ b/homeassistant/components/kostal_plenticore/helper.py @@ -235,8 +235,7 @@ class SettingDataUpdateCoordinator( _LOGGER.debug("Fetching %s for %s", self.name, self._fetch) - fetched_data = await client.get_setting_values(self._fetch) - return fetched_data + return await client.get_setting_values(self._fetch) class PlenticoreSelectUpdateCoordinator(DataUpdateCoordinator[_DataT]): # pylint: disable=hass-enforce-coordinator-module @@ -295,9 +294,7 @@ class SelectDataUpdateCoordinator( _LOGGER.debug("Fetching select %s for %s", self.name, self._fetch) - fetched_data = await self._async_get_current_option(self._fetch) - - return fetched_data + return await self._async_get_current_option(self._fetch) async def _async_get_current_option( self, @@ -313,8 +310,7 @@ class SelectDataUpdateCoordinator( continue for option in val.values(): if option[all_option] == "1": - fetched = {mid: {cast(str, pids[0]): all_option}} - return fetched + return {mid: {cast(str, pids[0]): all_option}} return {mid: {cast(str, pids[0]): "None"}} return {} diff --git a/homeassistant/components/lg_soundbar/__init__.py b/homeassistant/components/lg_soundbar/__init__.py index cd1ce1c8139..250cba887c1 100644 --- a/homeassistant/components/lg_soundbar/__init__.py +++ b/homeassistant/components/lg_soundbar/__init__.py @@ -35,5 +35,4 @@ async def async_unload_entry( hass: core.HomeAssistant, entry: config_entries.ConfigEntry ) -> bool: """Unload a config entry.""" - result = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - return result + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 726aef73c01..332d701148e 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -957,8 +957,7 @@ class LightEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): @property def _light_internal_rgbw_color(self) -> tuple[int, int, int, int] | None: """Return the rgbw color value [int, int, int, int].""" - rgbw_color = self.rgbw_color - return rgbw_color + return self.rgbw_color @cached_property def rgbww_color(self) -> tuple[int, int, int, int, int] | None: diff --git a/homeassistant/components/linear_garage_door/config_flow.py b/homeassistant/components/linear_garage_door/config_flow.py index bfb6f825030..31629f8e3b0 100644 --- a/homeassistant/components/linear_garage_door/config_flow.py +++ b/homeassistant/components/linear_garage_door/config_flow.py @@ -53,15 +53,13 @@ async def validate_input( finally: await hub.close() - info = { + return { "email": data["email"], "password": data["password"], "sites": sites, "device_id": device_id, } - return info - class LinearGarageDoorConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Linear Garage Door.""" diff --git a/homeassistant/components/logbook/queries/devices.py b/homeassistant/components/logbook/queries/devices.py index f4b1c06c40c..0e67ad23381 100644 --- a/homeassistant/components/logbook/queries/devices.py +++ b/homeassistant/components/logbook/queries/devices.py @@ -82,7 +82,7 @@ def devices_stmt( json_quotable_device_ids: list[str], ) -> StatementLambdaElement: """Generate a logbook query for multiple devices.""" - stmt = lambda_stmt( + return lambda_stmt( lambda: _apply_devices_context_union( select_events_without_states(start_day, end_day, event_type_ids).where( apply_event_device_id_matchers(json_quotable_device_ids) @@ -93,7 +93,6 @@ def devices_stmt( json_quotable_device_ids, ).order_by(Events.time_fired_ts) ) - return stmt def apply_event_device_id_matchers( diff --git a/homeassistant/components/logbook/queries/entities_and_devices.py b/homeassistant/components/logbook/queries/entities_and_devices.py index 383bb71e223..bef34f0858b 100644 --- a/homeassistant/components/logbook/queries/entities_and_devices.py +++ b/homeassistant/components/logbook/queries/entities_and_devices.py @@ -110,7 +110,7 @@ def entities_devices_stmt( json_quoted_device_ids: list[str], ) -> StatementLambdaElement: """Generate a logbook query for multiple entities.""" - stmt = lambda_stmt( + return lambda_stmt( lambda: _apply_entities_devices_context_union( select_events_without_states(start_day, end_day, event_type_ids).where( _apply_event_entity_id_device_id_matchers( @@ -125,7 +125,6 @@ def entities_devices_stmt( json_quoted_device_ids, ).order_by(Events.time_fired_ts) ) - return stmt def _apply_event_entity_id_device_id_matchers( diff --git a/homeassistant/components/luci/device_tracker.py b/homeassistant/components/luci/device_tracker.py index d62c1b07b5c..183f383e7e4 100644 --- a/homeassistant/components/luci/device_tracker.py +++ b/homeassistant/components/luci/device_tracker.py @@ -71,11 +71,10 @@ class LuciDeviceScanner(DeviceScanner): def get_device_name(self, device): """Return the name of the given device or None if we don't know.""" - name = next( + return next( (result.hostname for result in self.last_results if result.mac == device), None, ) - return name def get_extra_attributes(self, device): """Get extra attributes of a device. diff --git a/homeassistant/components/matter/helpers.py b/homeassistant/components/matter/helpers.py index 9aa58879214..cab9b602753 100644 --- a/homeassistant/components/matter/helpers.py +++ b/homeassistant/components/matter/helpers.py @@ -109,7 +109,7 @@ def get_node_from_device_entry( if server_info is None: raise RuntimeError("Matter server information is not available") - node = next( + return next( ( node for node in matter_client.get_nodes() @@ -118,5 +118,3 @@ def get_node_from_device_entry( ), None, ) - - return node diff --git a/homeassistant/components/media_source/local_source.py b/homeassistant/components/media_source/local_source.py index 8a67ae4a5b4..a1685df285e 100644 --- a/homeassistant/components/media_source/local_source.py +++ b/homeassistant/components/media_source/local_source.py @@ -93,12 +93,10 @@ class LocalSource(MediaSource): else: source_dir_id, location = None, "" - result = await self.hass.async_add_executor_job( + return await self.hass.async_add_executor_job( self._browse_media, source_dir_id, location ) - return result - def _browse_media( self, source_dir_id: str | None, location: str ) -> BrowseMediaSource: diff --git a/homeassistant/components/melcloud/climate.py b/homeassistant/components/melcloud/climate.py index 4bf12650b82..08b3658c270 100644 --- a/homeassistant/components/melcloud/climate.py +++ b/homeassistant/components/melcloud/climate.py @@ -330,12 +330,11 @@ class AtwDeviceZoneClimate(MelCloudClimate): @property def extra_state_attributes(self) -> dict[str, Any]: """Return the optional state attributes with device specific additions.""" - data = { + return { ATTR_STATUS: ATW_ZONE_HVAC_MODE_LOOKUP.get( self._zone.status, self._zone.status ) } - return data @property def hvac_mode(self) -> HVACMode: diff --git a/homeassistant/components/melcloud/water_heater.py b/homeassistant/components/melcloud/water_heater.py index 7d170430b04..8de1ac53311 100644 --- a/homeassistant/components/melcloud/water_heater.py +++ b/homeassistant/components/melcloud/water_heater.py @@ -73,8 +73,7 @@ class AtwWaterHeater(WaterHeaterEntity): @property def extra_state_attributes(self) -> dict[str, Any] | None: """Return the optional state attributes with device specific additions.""" - data = {ATTR_STATUS: self._device.status} - return data + return {ATTR_STATUS: self._device.status} @property def temperature_unit(self) -> str: diff --git a/homeassistant/components/meteoclimatic/__init__.py b/homeassistant/components/meteoclimatic/__init__.py index 2c371ebdcfd..f81d60c3d00 100644 --- a/homeassistant/components/meteoclimatic/__init__.py +++ b/homeassistant/components/meteoclimatic/__init__.py @@ -49,5 +49,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.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/mjpeg/camera.py b/homeassistant/components/mjpeg/camera.py index ab8c67f2ca9..dcb2eff2fd6 100644 --- a/homeassistant/components/mjpeg/camera.py +++ b/homeassistant/components/mjpeg/camera.py @@ -146,8 +146,7 @@ class MjpegCamera(Camera): async with asyncio.timeout(TIMEOUT): response = await websession.get(self._still_image_url, auth=self._auth) - image = await response.read() - return image + return await response.read() except TimeoutError: LOGGER.error("Timeout getting camera image from %s", self.name) diff --git a/homeassistant/components/mobile_app/helpers.py b/homeassistant/components/mobile_app/helpers.py index 13d50b7984f..7f88074bf34 100644 --- a/homeassistant/components/mobile_app/helpers.py +++ b/homeassistant/components/mobile_app/helpers.py @@ -104,8 +104,7 @@ def _convert_legacy_encryption_key(key: str) -> bytes: keylen = SecretBox.KEY_SIZE key_bytes = key.encode("utf-8") key_bytes = key_bytes[:keylen] - key_bytes = key_bytes.ljust(keylen, b"\0") - return key_bytes + return key_bytes.ljust(keylen, b"\0") def decrypt_payload_legacy(key: str, ciphertext: bytes) -> JsonValueType | None: diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 83830de4963..978123e169c 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -187,7 +187,7 @@ async def async_subscribe( translation_domain=DOMAIN, translation_placeholders={"topic": topic}, ) from exc - async_remove = await mqtt_data.client.async_subscribe( + return await mqtt_data.client.async_subscribe( topic, catch_log_exception( msg_callback, @@ -199,7 +199,6 @@ async def async_subscribe( qos, encoding, ) - return async_remove @bind_hass diff --git a/homeassistant/components/mqtt/trigger.py b/homeassistant/components/mqtt/trigger.py index d7086885b24..7aa798a7a3c 100644 --- a/homeassistant/components/mqtt/trigger.py +++ b/homeassistant/components/mqtt/trigger.py @@ -99,7 +99,6 @@ async def async_attach_trigger( "Attaching MQTT trigger for topic: '%s', payload: '%s'", topic, wanted_payload ) - remove = await mqtt.async_subscribe( + return await mqtt.async_subscribe( hass, topic, mqtt_automation_listener, encoding=encoding, qos=qos ) - return remove diff --git a/homeassistant/components/mqtt/util.py b/homeassistant/components/mqtt/util.py index a4635d1e4cc..ab21ab56f1b 100644 --- a/homeassistant/components/mqtt/util.py +++ b/homeassistant/components/mqtt/util.py @@ -218,8 +218,7 @@ def valid_birth_will(config: ConfigType) -> ConfigType: def get_mqtt_data(hass: HomeAssistant) -> MqttData: """Return typed MqttData from hass.data[DATA_MQTT].""" - mqtt_data: MqttData - mqtt_data = hass.data[DATA_MQTT] + mqtt_data: MqttData = hass.data[DATA_MQTT] return mqtt_data diff --git a/homeassistant/components/mysensors/config_flow.py b/homeassistant/components/mysensors/config_flow.py index b4347a39e12..9a8d79ca3a7 100644 --- a/homeassistant/components/mysensors/config_flow.py +++ b/homeassistant/components/mysensors/config_flow.py @@ -63,7 +63,7 @@ def is_persistence_file(value: str) -> str: def _get_schema_common(user_input: dict[str, str]) -> dict: """Create a schema with options common to all gateway types.""" - schema = { + return { vol.Required( CONF_VERSION, description={ @@ -72,7 +72,6 @@ def _get_schema_common(user_input: dict[str, str]) -> dict: ): str, vol.Optional(CONF_PERSISTENCE_FILE): str, } - return schema def _validate_version(version: str) -> dict[str, str]: diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py index 8e9fb5442ea..0a037dfce31 100644 --- a/homeassistant/components/mysensors/gateway.py +++ b/homeassistant/components/mysensors/gateway.py @@ -129,7 +129,7 @@ async def setup_gateway( ) -> BaseAsyncGateway | None: """Set up the Gateway for the given ConfigEntry.""" - ready_gateway = await _get_gateway( + return await _get_gateway( hass, gateway_type=entry.data[CONF_GATEWAY_TYPE], device=entry.data[CONF_DEVICE], @@ -144,7 +144,6 @@ async def setup_gateway( topic_out_prefix=entry.data.get(CONF_TOPIC_OUT_PREFIX), retain=entry.data.get(CONF_RETAIN, False), ) - return ready_gateway async def _get_gateway( diff --git a/homeassistant/components/mysensors/helpers.py b/homeassistant/components/mysensors/helpers.py index cb075b8f485..c456cfd1f11 100644 --- a/homeassistant/components/mysensors/helpers.py +++ b/homeassistant/components/mysensors/helpers.py @@ -152,7 +152,7 @@ def get_child_schema( """Return a child schema.""" set_req = gateway.const.SetReq child_schema = child.get_schema(gateway.protocol_version) - schema = child_schema.extend( + return child_schema.extend( { vol.Required( set_req[name].value, msg=invalid_msg(gateway, child, name) @@ -161,7 +161,6 @@ def get_child_schema( }, extra=vol.ALLOW_EXTRA, ) - return schema def invalid_msg( diff --git a/homeassistant/components/mystrom/binary_sensor.py b/homeassistant/components/mystrom/binary_sensor.py index 2201eb778d6..66ea2cc9679 100644 --- a/homeassistant/components/mystrom/binary_sensor.py +++ b/homeassistant/components/mystrom/binary_sensor.py @@ -38,8 +38,7 @@ class MyStromView(HomeAssistantView): async def get(self, request): """Handle the GET request received from a myStrom button.""" - res = await self._handle(request.app[KEY_HASS], request.query) - return res + return await self._handle(request.app[KEY_HASS], request.query) async def _handle(self, hass, data): """Handle requests to the myStrom endpoint.""" diff --git a/homeassistant/components/myuplink/binary_sensor.py b/homeassistant/components/myuplink/binary_sensor.py index 38b8c9c5fd3..6b7ec66a7b4 100644 --- a/homeassistant/components/myuplink/binary_sensor.py +++ b/homeassistant/components/myuplink/binary_sensor.py @@ -34,11 +34,7 @@ def get_description(device_point: DevicePoint) -> BinarySensorEntityDescription 2. Default to None """ prefix, _, _ = device_point.category.partition(" ") - description = CATEGORY_BASED_DESCRIPTIONS.get(prefix, {}).get( - device_point.parameter_id - ) - - return description + return CATEGORY_BASED_DESCRIPTIONS.get(prefix, {}).get(device_point.parameter_id) async def async_setup_entry( diff --git a/homeassistant/components/myuplink/diagnostics.py b/homeassistant/components/myuplink/diagnostics.py index d108db595a1..15b643ffd92 100644 --- a/homeassistant/components/myuplink/diagnostics.py +++ b/homeassistant/components/myuplink/diagnostics.py @@ -39,9 +39,7 @@ async def async_get_config_entry_diagnostics( } ) - diagnostics_data = { + return { "config_entry_data": async_redact_data(dict(config_entry.data), TO_REDACT), "myuplink_data": async_redact_data(myuplink_data, TO_REDACT), } - - return diagnostics_data diff --git a/homeassistant/components/myuplink/switch.py b/homeassistant/components/myuplink/switch.py index d26695f4cbe..11dca1e2ac0 100644 --- a/homeassistant/components/myuplink/switch.py +++ b/homeassistant/components/myuplink/switch.py @@ -39,11 +39,7 @@ def get_description(device_point: DevicePoint) -> SwitchEntityDescription | None 2. Default to None """ prefix, _, _ = device_point.category.partition(" ") - description = CATEGORY_BASED_DESCRIPTIONS.get(prefix, {}).get( - device_point.parameter_id - ) - - return description + return CATEGORY_BASED_DESCRIPTIONS.get(prefix, {}).get(device_point.parameter_id) async def async_setup_entry( diff --git a/homeassistant/components/nam/diagnostics.py b/homeassistant/components/nam/diagnostics.py index 8ce885f0297..db1a97d8fb1 100644 --- a/homeassistant/components/nam/diagnostics.py +++ b/homeassistant/components/nam/diagnostics.py @@ -22,9 +22,7 @@ async def async_get_config_entry_diagnostics( """Return diagnostics for a config entry.""" coordinator: NAMDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - diagnostics_data = { + return { "info": async_redact_data(config_entry.data, TO_REDACT), "data": asdict(coordinator.data), } - - return diagnostics_data diff --git a/homeassistant/components/nextdns/diagnostics.py b/homeassistant/components/nextdns/diagnostics.py index c0a9071bb9d..cade6476d82 100644 --- a/homeassistant/components/nextdns/diagnostics.py +++ b/homeassistant/components/nextdns/diagnostics.py @@ -37,7 +37,7 @@ async def async_get_config_entry_diagnostics( settings_coordinator = coordinators[ATTR_SETTINGS] status_coordinator = coordinators[ATTR_STATUS] - diagnostics_data = { + return { "config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT), "dnssec_coordinator_data": asdict(dnssec_coordinator.data), "encryption_coordinator_data": asdict(encryption_coordinator.data), @@ -46,5 +46,3 @@ async def async_get_config_entry_diagnostics( "settings_coordinator_data": asdict(settings_coordinator.data), "status_coordinator_data": asdict(status_coordinator.data), } - - return diagnostics_data diff --git a/homeassistant/components/nuki/binary_sensor.py b/homeassistant/components/nuki/binary_sensor.py index f2a98599e27..9b4772ee108 100644 --- a/homeassistant/components/nuki/binary_sensor.py +++ b/homeassistant/components/nuki/binary_sensor.py @@ -55,10 +55,9 @@ class NukiDoorsensorEntity(NukiEntity[NukiDevice], BinarySensorEntity): @property def extra_state_attributes(self): """Return the device specific state attributes.""" - data = { + return { ATTR_NUKI_ID: self._nuki_device.nuki_id, } - return data @property def available(self) -> bool: @@ -96,10 +95,9 @@ class NukiRingactionEntity(NukiEntity[NukiDevice], BinarySensorEntity): @property def extra_state_attributes(self): """Return the device specific state attributes.""" - data = { + return { ATTR_NUKI_ID: self._nuki_device.nuki_id, } - return data @property def is_on(self) -> bool: diff --git a/homeassistant/components/numato/sensor.py b/homeassistant/components/numato/sensor.py index 6efc3f6160f..ef71e00bc73 100644 --- a/homeassistant/components/numato/sensor.py +++ b/homeassistant/components/numato/sensor.py @@ -116,8 +116,7 @@ class NumatoGpioAdc(SensorEntity): def _clamp_to_source_range(self, val): # clamp to source range val = max(val, self._src_range[0]) - val = min(val, self._src_range[1]) - return val + return min(val, self._src_range[1]) def _linear_scale_to_dest_range(self, val): # linear scale to dest range @@ -125,5 +124,4 @@ class NumatoGpioAdc(SensorEntity): adc_val_rel = val - self._src_range[0] ratio = float(adc_val_rel) / float(src_len) dst_len = self._dst_range[1] - self._dst_range[0] - dest_val = self._dst_range[0] + ratio * dst_len - return dest_val + return self._dst_range[0] + ratio * dst_len diff --git a/homeassistant/components/nut/__init__.py b/homeassistant/components/nut/__init__.py index 575def8bf0f..8b715237e01 100644 --- a/homeassistant/components/nut/__init__.py +++ b/homeassistant/components/nut/__init__.py @@ -265,9 +265,7 @@ class PyNUTData: manufacturer = _manufacturer_from_status(self._status) model = _model_from_status(self._status) firmware = _firmware_from_status(self._status) - device_info = NUTDeviceInfo(manufacturer, model, firmware) - - return device_info + return NUTDeviceInfo(manufacturer, model, firmware) async def _async_get_status(self) -> dict[str, str]: """Get the ups status from NUT.""" diff --git a/homeassistant/components/omnilogic/common.py b/homeassistant/components/omnilogic/common.py index adc87e7be26..0484c889ba3 100644 --- a/homeassistant/components/omnilogic/common.py +++ b/homeassistant/components/omnilogic/common.py @@ -69,9 +69,7 @@ class OmniLogicUpdateCoordinator(DataUpdateCoordinator[dict[tuple, dict[str, Any return data - parsed_data = get_item_data(data, "Backyard", (), parsed_data) - - return parsed_data + return get_item_data(data, "Backyard", (), parsed_data) class OmniLogicEntity(CoordinatorEntity[OmniLogicUpdateCoordinator]): diff --git a/homeassistant/components/opnsense/device_tracker.py b/homeassistant/components/opnsense/device_tracker.py index 7c018e20a36..6357ce38e1d 100644 --- a/homeassistant/components/opnsense/device_tracker.py +++ b/homeassistant/components/opnsense/device_tracker.py @@ -14,10 +14,9 @@ async def async_get_scanner( ) -> OPNSenseDeviceScanner: """Configure the OPNSense device_tracker.""" interface_client = hass.data[OPNSENSE_DATA]["interfaces"] - scanner = OPNSenseDeviceScanner( + return OPNSenseDeviceScanner( interface_client, hass.data[OPNSENSE_DATA][CONF_TRACKER_INTERFACE] ) - return scanner class OPNSenseDeviceScanner(DeviceScanner): @@ -46,8 +45,7 @@ class OPNSenseDeviceScanner(DeviceScanner): """Return the name of the given device or None if we don't know.""" if device not in self.last_results: return None - hostname = self.last_results[device].get("hostname") or None - return hostname + return self.last_results[device].get("hostname") or None def update_info(self): """Ensure the information from the OPNSense router is up to date. diff --git a/homeassistant/components/overkiz/config_flow.py b/homeassistant/components/overkiz/config_flow.py index f95f885f7ef..eb79910d63f 100644 --- a/homeassistant/components/overkiz/config_flow.py +++ b/homeassistant/components/overkiz/config_flow.py @@ -368,12 +368,10 @@ class OverkizConfigFlow(ConfigFlow, domain=DOMAIN): self, username: str, password: str, server: OverkizServer ) -> OverkizClient: session = async_create_clientsession(self.hass) - client = OverkizClient( + return OverkizClient( username=username, password=password, server=server, session=session ) - return client - async def _create_local_api_token( self, cloud_client: OverkizClient, host: str, verify_ssl: bool ) -> str: diff --git a/homeassistant/components/plaato/__init__.py b/homeassistant/components/plaato/__init__.py index 0d8678d95ef..c68e2c8ad75 100644 --- a/homeassistant/components/plaato/__init__.py +++ b/homeassistant/components/plaato/__init__.py @@ -234,8 +234,7 @@ class PlaatoCoordinator(DataUpdateCoordinator): # pylint: disable=hass-enforce- async def _async_update_data(self): """Update data via library.""" - data = await self.api.get_data( + return await self.api.get_data( session=aiohttp_client.async_get_clientsession(self.hass), device_type=self.device_type, ) - return data diff --git a/homeassistant/components/proxmoxve/__init__.py b/homeassistant/components/proxmoxve/__init__.py index e0b8f91088d..6d6771debc4 100644 --- a/homeassistant/components/proxmoxve/__init__.py +++ b/homeassistant/components/proxmoxve/__init__.py @@ -200,8 +200,7 @@ def create_coordinator_container_vm( def poll_api() -> dict[str, Any] | None: """Call the api.""" - vm_status = call_api_container_vm(proxmox, node_name, vm_id, vm_type) - return vm_status + return call_api_container_vm(proxmox, node_name, vm_id, vm_type) vm_status = await hass.async_add_executor_job(poll_api) diff --git a/homeassistant/components/qvr_pro/camera.py b/homeassistant/components/qvr_pro/camera.py index 2754ab3a1ec..38221f89cfd 100644 --- a/homeassistant/components/qvr_pro/camera.py +++ b/homeassistant/components/qvr_pro/camera.py @@ -90,9 +90,7 @@ class QVRProCamera(Camera): @property def extra_state_attributes(self): """Get the state attributes.""" - attrs = {"qvr_guid": self.guid} - - return attrs + return {"qvr_guid": self.guid} def camera_image( self, width: int | None = None, height: int | None = None diff --git a/homeassistant/components/recorder/history/modern.py b/homeassistant/components/recorder/history/modern.py index 5fd4f415e02..675bb1b8cf8 100644 --- a/homeassistant/components/recorder/history/modern.py +++ b/homeassistant/components/recorder/history/modern.py @@ -174,8 +174,7 @@ def _significant_states_stmt( StateAttributes, States.attributes_id == StateAttributes.attributes_id ) if not include_start_time_state or not run_start_ts: - stmt = stmt.order_by(States.metadata_id, States.last_updated_ts) - return stmt + return stmt.order_by(States.metadata_id, States.last_updated_ts) unioned_subquery = union_all( _select_from_subquery( _get_start_time_state_stmt( diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 0c127d079ad..e70a52c36f1 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -349,8 +349,7 @@ def get_start_time() -> datetime: now = dt_util.utcnow() current_period_minutes = now.minute - now.minute % 5 current_period = now.replace(minute=current_period_minutes, second=0, microsecond=0) - last_period = current_period - timedelta(minutes=5) - return last_period + return current_period - timedelta(minutes=5) def _compile_hourly_statistics_summary_mean_stmt( diff --git a/homeassistant/components/roon/config_flow.py b/homeassistant/components/roon/config_flow.py index d24cdb0c98d..2dc0bf71cd4 100644 --- a/homeassistant/components/roon/config_flow.py +++ b/homeassistant/components/roon/config_flow.py @@ -97,9 +97,7 @@ async def discover(hass): """Connect and authenticate home assistant.""" hub = RoonHub(hass) - servers = await hub.discover() - - return servers + return await hub.discover() async def authenticate(hass: HomeAssistant, host, port, servers): diff --git a/homeassistant/components/samsungtv/device_trigger.py b/homeassistant/components/samsungtv/device_trigger.py index e47cde785eb..5b8ff3ebdb8 100644 --- a/homeassistant/components/samsungtv/device_trigger.py +++ b/homeassistant/components/samsungtv/device_trigger.py @@ -55,8 +55,7 @@ async def async_get_triggers( _hass: HomeAssistant, device_id: str ) -> list[dict[str, str]]: """List device triggers for device.""" - triggers = [async_get_turn_on_trigger(device_id)] - return triggers + return [async_get_turn_on_trigger(device_id)] async def async_attach_trigger( diff --git a/homeassistant/components/schedule/__init__.py b/homeassistant/components/schedule/__init__.py index 2dc2ff2d035..e69a6761bc7 100644 --- a/homeassistant/components/schedule/__init__.py +++ b/homeassistant/components/schedule/__init__.py @@ -256,8 +256,7 @@ class Schedule(CollectionEntity): @classmethod def from_storage(cls, config: ConfigType) -> Schedule: """Return entity instance initialized from storage.""" - schedule = cls(config, editable=True) - return schedule + return cls(config, editable=True) @classmethod def from_yaml(cls, config: ConfigType) -> Schedule: diff --git a/homeassistant/components/sensibo/sensor.py b/homeassistant/components/sensibo/sensor.py index 0a2b23b2cd9..81ab3a06067 100644 --- a/homeassistant/components/sensibo/sensor.py +++ b/homeassistant/components/sensibo/sensor.py @@ -311,8 +311,7 @@ class SensiboDeviceSensor(SensiboDeviceBaseEntity, SensorEntity): @property def native_value(self) -> StateType | datetime: """Return value of sensor.""" - state = self.entity_description.value_fn(self.device_data) - return state + return self.entity_description.value_fn(self.device_data) @property def extra_state_attributes(self) -> Mapping[str, Any] | None: diff --git a/homeassistant/components/sharkiq/vacuum.py b/homeassistant/components/sharkiq/vacuum.py index 6647b79c892..d028b0b8b87 100644 --- a/homeassistant/components/sharkiq/vacuum.py +++ b/homeassistant/components/sharkiq/vacuum.py @@ -267,11 +267,10 @@ class SharkVacuumEntity(CoordinatorEntity[SharkIqUpdateCoordinator], StateVacuum @property def extra_state_attributes(self) -> dict[str, Any]: """Return a dictionary of device state attributes specific to sharkiq.""" - data = { + return { ATTR_ERROR_CODE: self.error_code, ATTR_ERROR_MSG: self.sharkiq.error_text, ATTR_LOW_LIGHT: self.low_light, ATTR_RECHARGE_RESUME: self.recharge_resume, ATTR_ROOMS: self.available_rooms, } - return data diff --git a/homeassistant/components/sky_hub/device_tracker.py b/homeassistant/components/sky_hub/device_tracker.py index 52c56993be0..bc4a0fdc743 100644 --- a/homeassistant/components/sky_hub/device_tracker.py +++ b/homeassistant/components/sky_hub/device_tracker.py @@ -54,11 +54,10 @@ class SkyHubDeviceScanner(DeviceScanner): async def async_get_device_name(self, device): """Return the name of the given device.""" - name = next( + return next( (result.name for result in self.last_results if result.mac == device), None, ) - return name async def async_get_extra_attributes(self, device): """Get extra attributes of a device.""" diff --git a/homeassistant/components/smarttub/light.py b/homeassistant/components/smarttub/light.py index b5fac0b34f6..532234f4059 100644 --- a/homeassistant/components/smarttub/light.py +++ b/homeassistant/components/smarttub/light.py @@ -96,14 +96,12 @@ class SmartTubLight(SmartTubEntity, LightEntity): @property def effect_list(self): """Return the list of supported effects.""" - effects = [ + return [ effect for effect in map(self._light_mode_to_effect, SpaLight.LightMode) if effect is not None ] - return effects - @staticmethod def _light_mode_to_effect(light_mode: SpaLight.LightMode): if light_mode == SpaLight.LightMode.OFF: diff --git a/homeassistant/components/sms/gateway.py b/homeassistant/components/sms/gateway.py index 9b5b8c1f51e..e0cbf78dba4 100644 --- a/homeassistant/components/sms/gateway.py +++ b/homeassistant/components/sms/gateway.py @@ -134,9 +134,7 @@ class Gateway: _LOGGER.info("Failed to read messages!") # Link all SMS when there are concatenated messages - entries = gammu.LinkSMS(entries) - - return entries + return gammu.LinkSMS(entries) @callback def _notify_incoming_sms(self, messages): diff --git a/homeassistant/components/sonos/media_browser.py b/homeassistant/components/sonos/media_browser.py index 6e6f388ed50..b6fc250ab23 100644 --- a/homeassistant/components/sonos/media_browser.py +++ b/homeassistant/components/sonos/media_browser.py @@ -501,7 +501,7 @@ def get_media( # Format is S:TITLE or S:ITEM_ID splits = item_id.split(":") title = splits[1] if len(splits) > 1 else None - playlist = next( + return next( ( p for p in media_library.get_playlists() @@ -509,7 +509,6 @@ def get_media( ), None, ) - return playlist if not item_id.startswith("A:ALBUM") and search_type == SONOS_ALBUM: item_id = "A:ALBUMARTIST/" + "/".join(item_id.split("/")[2:]) diff --git a/homeassistant/components/soundtouch/media_player.py b/homeassistant/components/soundtouch/media_player.py index 0843cc1a826..c09c4ed72c4 100644 --- a/homeassistant/components/soundtouch/media_player.py +++ b/homeassistant/components/soundtouch/media_player.py @@ -409,10 +409,8 @@ class SoundTouchMediaPlayer(MediaPlayerEntity): if slave_instance and slave_instance.entity_id != master: slaves.append(slave_instance.entity_id) - attributes = { + return { "master": master, "is_master": master == self.entity_id, "slaves": slaves, } - - return attributes diff --git a/homeassistant/components/squeezebox/browse_media.py b/homeassistant/components/squeezebox/browse_media.py index b770a3e22a3..bc63bcb7f2f 100644 --- a/homeassistant/components/squeezebox/browse_media.py +++ b/homeassistant/components/squeezebox/browse_media.py @@ -170,8 +170,7 @@ async def library_payload(hass, player): else: library_info["children"].append(item) - response = BrowseMedia(**library_info) - return response + return BrowseMedia(**library_info) def media_source_content_filter(item: BrowseMedia) -> bool: diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index 007d880a263..7d072fa2570 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -258,14 +258,12 @@ class SqueezeBoxEntity(MediaPlayerEntity): @property def extra_state_attributes(self): """Return device-specific attributes.""" - squeezebox_attr = { + return { attr: getattr(self, attr) for attr in ATTR_TO_PROPERTY if getattr(self, attr) is not None } - return squeezebox_attr - @callback def rediscovered(self, unique_id, connected): """Make a player available again.""" diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index 5c10768a408..36513dfd851 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -762,19 +762,17 @@ class StatisticsSensor(SensorEntity): def _stat_sum_differences(self) -> StateType: if len(self.states) >= 2: - diff_sum = sum( + return sum( abs(j - i) for i, j in zip(list(self.states), list(self.states)[1:]) ) - return diff_sum return None def _stat_sum_differences_nonnegative(self) -> StateType: if len(self.states) >= 2: - diff_sum_nn = sum( + return sum( (j - i if j >= i else j - 0) for i, j in zip(list(self.states), list(self.states)[1:]) ) - return diff_sum_nn return None def _stat_total(self) -> StateType: diff --git a/homeassistant/components/subaru/__init__.py b/homeassistant/components/subaru/__init__.py index d7169fc181e..db2ee7fdbbc 100644 --- a/homeassistant/components/subaru/__init__.py +++ b/homeassistant/components/subaru/__init__.py @@ -149,7 +149,7 @@ async def update_subaru(vehicle, controller): def get_vehicle_info(controller, vin): """Obtain vehicle identifiers and capabilities.""" - info = { + return { VEHICLE_VIN: vin, VEHICLE_MODEL_NAME: controller.get_model_name(vin), VEHICLE_MODEL_YEAR: controller.get_model_year(vin), @@ -161,7 +161,6 @@ def get_vehicle_info(controller, vin): VEHICLE_HAS_SAFETY_SERVICE: controller.get_safety_status(vin), VEHICLE_LAST_UPDATE: 0, } - return info def get_device_info(vehicle_info): diff --git a/homeassistant/components/subaru/diagnostics.py b/homeassistant/components/subaru/diagnostics.py index 0a26387d1c2..726457aa341 100644 --- a/homeassistant/components/subaru/diagnostics.py +++ b/homeassistant/components/subaru/diagnostics.py @@ -25,7 +25,7 @@ async def async_get_config_entry_diagnostics( """Return diagnostics for a config entry.""" coordinator = hass.data[DOMAIN][config_entry.entry_id][ENTRY_COORDINATOR] - diagnostics_data = { + return { "config_entry": async_redact_data(config_entry.data, CONFIG_FIELDS_TO_REDACT), "options": async_redact_data(config_entry.options, []), "data": [ @@ -34,8 +34,6 @@ async def async_get_config_entry_diagnostics( ], } - return diagnostics_data - async def async_get_device_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry, device: DeviceEntry diff --git a/homeassistant/components/supla/__init__.py b/homeassistant/components/supla/__init__.py index 46a3ec2b2c0..8f04b5b662e 100644 --- a/homeassistant/components/supla/__init__.py +++ b/homeassistant/components/supla/__init__.py @@ -101,13 +101,12 @@ async def discover_devices(hass, hass_config): async def _fetch_channels(): async with asyncio.timeout(SCAN_INTERVAL.total_seconds()): - channels = { + return { channel["id"]: channel for channel in await server.get_channels( # noqa: B023 include=["iodevice", "state", "connected"] ) } - return channels coordinator = DataUpdateCoordinator( hass, diff --git a/homeassistant/components/synology_dsm/sensor.py b/homeassistant/components/synology_dsm/sensor.py index 47483ee4a63..b742669712e 100644 --- a/homeassistant/components/synology_dsm/sensor.py +++ b/homeassistant/components/synology_dsm/sensor.py @@ -4,6 +4,7 @@ from __future__ import annotations from dataclasses import dataclass from datetime import datetime, timedelta +from typing import cast from synology_dsm.api.core.utilization import SynoCoreUtilization from synology_dsm.api.dsm.information import SynoDSMInformation @@ -387,8 +388,10 @@ class SynoDSMStorageSensor(SynologyDSMDeviceEntity, SynoDSMSensor): @property def native_value(self) -> StateType: """Return the state.""" - attr = getattr(self._api.storage, self.entity_description.key)(self._device_id) - return attr # type: ignore[no-any-return] + return cast( + StateType, + getattr(self._api.storage, self.entity_description.key)(self._device_id), + ) class SynoDSMInfoSensor(SynoDSMSensor): diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 621f5a1ad61..6d298a80e79 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -206,7 +206,7 @@ def create_climate_entity( cool_max_temp = float(cool_temperatures["celsius"]["max"]) cool_step = cool_temperatures["celsius"].get("step", PRECISION_TENTHS) - entity = TadoClimate( + return TadoClimate( tado, name, zone_id, @@ -222,7 +222,6 @@ def create_climate_entity( cool_step, supported_fan_modes, ) - return entity class TadoClimate(TadoZoneEntity, ClimateEntity): diff --git a/homeassistant/components/tado/water_heater.py b/homeassistant/components/tado/water_heater.py index 99172228973..f1257f097eb 100644 --- a/homeassistant/components/tado/water_heater.py +++ b/homeassistant/components/tado/water_heater.py @@ -106,7 +106,7 @@ def create_water_heater_entity(tado: TadoConnector, name: str, zone_id: int, zon min_temp = None max_temp = None - entity = TadoWaterHeater( + return TadoWaterHeater( tado, name, zone_id, @@ -115,8 +115,6 @@ def create_water_heater_entity(tado: TadoConnector, name: str, zone_id: int, zon max_temp, ) - return entity - class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity): """Representation of a Tado water heater.""" diff --git a/homeassistant/components/tankerkoenig/diagnostics.py b/homeassistant/components/tankerkoenig/diagnostics.py index 4846d2687a2..0af5b29c5a8 100644 --- a/homeassistant/components/tankerkoenig/diagnostics.py +++ b/homeassistant/components/tankerkoenig/diagnostics.py @@ -27,11 +27,10 @@ async def async_get_config_entry_diagnostics( """Return diagnostics for a config entry.""" coordinator: TankerkoenigDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - diag_data = { + return { "entry": async_redact_data(entry.as_dict(), TO_REDACT), "data": { station_id: asdict(price_info) for station_id, price_info in coordinator.data.items() }, } - return diag_data diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index 4e47be8b807..897fd6a9bac 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -983,10 +983,9 @@ class TelegramNotificationService: """Remove bot from chat.""" chat_id = self._get_target_chat_ids(chat_id)[0] _LOGGER.debug("Leave from chat ID %s", chat_id) - leaved = await self._send_msg( + return await self._send_msg( self.bot.leave_chat, "Error leaving chat", None, chat_id ) - return leaved class BaseTelegramBotEntity: diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index a6dbedc6161..a341fdd5f87 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -237,8 +237,7 @@ def async_create_preview_sensor( ) -> SensorTemplate: """Create a preview sensor.""" validated_config = SENSOR_SCHEMA(config | {CONF_NAME: name}) - entity = SensorTemplate(hass, validated_config, None) - return entity + return SensorTemplate(hass, validated_config, None) class SensorTemplate(TemplateEntity, SensorEntity): diff --git a/homeassistant/components/tensorflow/image_processing.py b/homeassistant/components/tensorflow/image_processing.py index 632db28ca3a..40e30ca3848 100644 --- a/homeassistant/components/tensorflow/image_processing.py +++ b/homeassistant/components/tensorflow/image_processing.py @@ -96,9 +96,7 @@ def get_model_detection_function(model): image, shapes = model.preprocess(image) prediction_dict = model.predict(image, shapes) - detections = model.postprocess(prediction_dict, shapes) - - return detections + return model.postprocess(prediction_dict, shapes) return detect_fn diff --git a/homeassistant/components/tractive/diagnostics.py b/homeassistant/components/tractive/diagnostics.py index f2bc80c51a1..cd1f5632f46 100644 --- a/homeassistant/components/tractive/diagnostics.py +++ b/homeassistant/components/tractive/diagnostics.py @@ -20,12 +20,10 @@ async def async_get_config_entry_diagnostics( """Return diagnostics for a config entry.""" trackables = hass.data[DOMAIN][config_entry.entry_id][TRACKABLES] - diagnostics_data = async_redact_data( + return async_redact_data( { "config_entry": config_entry.as_dict(), "trackables": [item.trackable for item in trackables], }, TO_REDACT, ) - - return diagnostics_data diff --git a/homeassistant/components/ubus/device_tracker.py b/homeassistant/components/ubus/device_tracker.py index 0aebea84c7d..b728059d0be 100644 --- a/homeassistant/components/ubus/device_tracker.py +++ b/homeassistant/components/ubus/device_tracker.py @@ -106,8 +106,7 @@ class UbusDeviceScanner(DeviceScanner): if self.mac2name is None: # Generation of mac2name dictionary failed return None - name = self.mac2name.get(device.upper(), None) - return name + return self.mac2name.get(device.upper(), None) async def async_get_extra_attributes(self, device: str) -> dict[str, str]: """Return the host to distinguish between multiple routers.""" diff --git a/homeassistant/components/uk_transport/sensor.py b/homeassistant/components/uk_transport/sensor.py index 24a88724add..134dd675163 100644 --- a/homeassistant/components/uk_transport/sensor.py +++ b/homeassistant/components/uk_transport/sensor.py @@ -284,5 +284,4 @@ def _delta_mins(hhmm_time_str): if hhmm_datetime < now: hhmm_datetime += timedelta(days=1) - delta_mins = (hhmm_datetime - now).total_seconds() // 60 - return delta_mins + return (hhmm_datetime - now).total_seconds() // 60 diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 96a8a5dc1f8..a41d1942536 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -359,6 +359,4 @@ class UnifiScannerEntity(UnifiEntity[HandlerT, ApiItemT], ScannerEntity): if self.is_connected: attributes_to_check = CLIENT_CONNECTED_ALL_ATTRIBUTES - attributes = {k: raw[k] for k in attributes_to_check if k in raw} - - return attributes + return {k: raw[k] for k in attributes_to_check if k in raw} diff --git a/homeassistant/components/unifiprotect/media_source.py b/homeassistant/components/unifiprotect/media_source.py index 32cac04797f..ba962891454 100644 --- a/homeassistant/components/unifiprotect/media_source.py +++ b/homeassistant/components/unifiprotect/media_source.py @@ -419,9 +419,7 @@ class ProtectMediaSource(MediaSource): if camera is not None: title = f"{camera.display_name} > {title}" - title = f"{data.api.bootstrap.nvr.display_name} > {title}" - - return title + return f"{data.api.bootstrap.nvr.display_name} > {title}" async def _build_event( self, @@ -868,7 +866,7 @@ class ProtectMediaSource(MediaSource): async def _build_console(self, data: ProtectData) -> BrowseMediaSource: """Build media source for a single UniFi Protect NVR.""" - base = BrowseMediaSource( + return BrowseMediaSource( domain=DOMAIN, identifier=f"{data.api.bootstrap.nvr.id}:browse", media_class=MediaClass.DIRECTORY, @@ -880,8 +878,6 @@ class ProtectMediaSource(MediaSource): children=await self._build_cameras(data), ) - return base - async def _build_sources(self) -> BrowseMediaSource: """Return all media source for all UniFi Protect NVRs.""" diff --git a/homeassistant/components/unifiprotect/views.py b/homeassistant/components/unifiprotect/views.py index 0aa7056976b..0f9bff63689 100644 --- a/homeassistant/components/unifiprotect/views.py +++ b/homeassistant/components/unifiprotect/views.py @@ -52,15 +52,13 @@ def async_generate_event_video_url(event: Event) -> str: raise ValueError("Event is ongoing") url_format = VideoProxyView.url or "{nvr_id}/{camera_id}/{start}/{end}" - url = url_format.format( + return url_format.format( nvr_id=event.api.bootstrap.nvr.id, camera_id=event.camera_id, start=event.start.replace(microsecond=0).isoformat(), end=event.end.replace(microsecond=0).isoformat(), ) - return url - @callback def _client_error(message: Any, code: HTTPStatus) -> web.Response: diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index 4dff753ac6a..0b9eecb1b15 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -74,9 +74,7 @@ async def async_create_device(hass: HomeAssistant, location: str) -> Device: # Create profile wrapper. igd_device = IgdDevice(upnp_device, None) - device = Device(hass, igd_device) - - return device + return Device(hass, igd_device) class Device: diff --git a/homeassistant/components/uvc/camera.py b/homeassistant/components/uvc/camera.py index 307db17c2b8..4615bc2990a 100644 --- a/homeassistant/components/uvc/camera.py +++ b/homeassistant/components/uvc/camera.py @@ -243,7 +243,7 @@ class UnifiVideoCamera(Camera): """Return the source of the stream.""" for channel in self._caminfo["channels"]: if channel["isRtspEnabled"]: - uri = next( + return next( ( uri for i, uri in enumerate(channel["rtspUris"]) @@ -251,7 +251,6 @@ class UnifiVideoCamera(Camera): if re.search(self._nvr._host, uri) ) ) - return uri return None diff --git a/homeassistant/components/vesync/diagnostics.py b/homeassistant/components/vesync/diagnostics.py index b56c8fc5db6..9af8a7fed67 100644 --- a/homeassistant/components/vesync/diagnostics.py +++ b/homeassistant/components/vesync/diagnostics.py @@ -24,7 +24,7 @@ async def async_get_config_entry_diagnostics( """Return diagnostics for a config entry.""" manager: VeSync = hass.data[DOMAIN][VS_MANAGER] - data = { + return { DOMAIN: { "bulb_count": len(manager.bulbs), "fan_count": len(manager.fans), @@ -40,8 +40,6 @@ async def async_get_config_entry_diagnostics( }, } - return data - async def async_get_device_diagnostics( hass: HomeAssistant, entry: ConfigEntry, device: DeviceEntry diff --git a/homeassistant/components/webostv/device_trigger.py b/homeassistant/components/webostv/device_trigger.py index 0175df5d828..17d92b1abf3 100644 --- a/homeassistant/components/webostv/device_trigger.py +++ b/homeassistant/components/webostv/device_trigger.py @@ -55,8 +55,7 @@ async def async_get_triggers( _hass: HomeAssistant, device_id: str ) -> list[dict[str, str]]: """List device triggers for device.""" - triggers = [async_get_turn_on_trigger(device_id)] - return triggers + return [async_get_turn_on_trigger(device_id)] async def async_attach_trigger( diff --git a/homeassistant/components/ws66i/config_flow.py b/homeassistant/components/ws66i/config_flow.py index 5692ffcb81b..0cf0b557f35 100644 --- a/homeassistant/components/ws66i/config_flow.py +++ b/homeassistant/components/ws66i/config_flow.py @@ -128,12 +128,10 @@ class WS66iConfigFlow(ConfigFlow, domain=DOMAIN): @callback def _key_for_source(index, source, previous_sources): - key = vol.Required( + return vol.Required( source, description={"suggested_value": previous_sources[str(index)]} ) - return key - class Ws66iOptionsFlowHandler(OptionsFlow): """Handle a WS66i options flow.""" diff --git a/homeassistant/components/xiaomi_aqara/lock.py b/homeassistant/components/xiaomi_aqara/lock.py index 90afbe15911..8499864576a 100644 --- a/homeassistant/components/xiaomi_aqara/lock.py +++ b/homeassistant/components/xiaomi_aqara/lock.py @@ -61,8 +61,7 @@ class XiaomiAqaraLock(LockEntity, XiaomiDevice): @property def extra_state_attributes(self) -> dict[str, int]: """Return the state attributes.""" - attributes = {ATTR_VERIFIED_WRONG_TIMES: self._verified_wrong_times} - return attributes + return {ATTR_VERIFIED_WRONG_TIMES: self._verified_wrong_times} @callback def clear_unlock_state(self, _): diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index 5384bd93a7e..35ee017286f 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -248,7 +248,7 @@ def _async_update_data_vacuum( fan_speeds = device.fan_speed_presets() - data = VacuumCoordinatorData( + return VacuumCoordinatorData( device.status(), device.dnd_status(), device.last_clean_details(), @@ -259,8 +259,6 @@ def _async_update_data_vacuum( {v: k for k, v in fan_speeds.items()}, ) - return data - async def update_async() -> VacuumCoordinatorData: """Fetch data from the device using async_add_executor_job.""" diff --git a/homeassistant/components/xmpp/notify.py b/homeassistant/components/xmpp/notify.py index 50797536aee..4f7af2be7ee 100644 --- a/homeassistant/components/xmpp/notify.py +++ b/homeassistant/components/xmpp/notify.py @@ -297,7 +297,7 @@ async def async_send_message( # noqa: C901 _LOGGER.info("Uploading file from URL, %s", filename) - url = await self["xep_0363"].upload_file( + return await self["xep_0363"].upload_file( filename, size=filesize, input_file=result.content, @@ -305,8 +305,6 @@ async def async_send_message( # noqa: C901 timeout=timeout, ) - return url - async def upload_file_from_path(self, path, timeout=None): """Upload a file from a local file path via XEP_0363.""" _LOGGER.info("Uploading file from path, %s", path) @@ -328,7 +326,7 @@ async def async_send_message( # noqa: C901 filename = self.get_random_filename(data.get(ATTR_PATH)) _LOGGER.debug("Uploading file with random filename %s", filename) - url = await self["xep_0363"].upload_file( + return await self["xep_0363"].upload_file( filename, size=filesize, input_file=input_file, @@ -336,8 +334,6 @@ async def async_send_message( # noqa: C901 timeout=timeout, ) - return url - def send_text_message(self): """Send a text only message to a room or a recipient.""" try: diff --git a/homeassistant/components/yamaha_musiccast/media_player.py b/homeassistant/components/yamaha_musiccast/media_player.py index 3edf524c8ad..a068ac6ddca 100644 --- a/homeassistant/components/yamaha_musiccast/media_player.py +++ b/homeassistant/components/yamaha_musiccast/media_player.py @@ -385,7 +385,7 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): else: children.append(item) - overview = BrowseMedia( + return BrowseMedia( title=media_content_provider.title, media_class=MEDIA_CLASS_MAPPING.get(media_content_provider.content_type), media_content_id=media_content_provider.content_id, @@ -395,8 +395,6 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): children=children, ) - return overview - async def async_select_sound_mode(self, sound_mode: str) -> None: """Select sound mode.""" await self.coordinator.musiccast.select_sound_mode(self._zone_id, sound_mode) diff --git a/homeassistant/components/zha/core/cluster_handlers/closures.py b/homeassistant/components/zha/core/cluster_handlers/closures.py index c57ad507317..e96d6492beb 100644 --- a/homeassistant/components/zha/core/cluster_handlers/closures.py +++ b/homeassistant/components/zha/core/cluster_handlers/closures.py @@ -96,8 +96,7 @@ class DoorLockClusterHandler(ClusterHandler): async def async_get_user_code(self, code_slot: int) -> int: """Get the user code from the code slot.""" - result = await self.get_pin_code(code_slot - 1) - return result + return await self.get_pin_code(code_slot - 1) async def async_clear_user_code(self, code_slot: int) -> None: """Clear the code slot.""" @@ -117,8 +116,7 @@ class DoorLockClusterHandler(ClusterHandler): async def async_get_user_type(self, code_slot: int) -> str: """Get user type.""" - result = await self.get_user_type(code_slot - 1) - return result + return await self.get_user_type(code_slot - 1) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(Shade.cluster_id) diff --git a/homeassistant/components/zha/device_action.py b/homeassistant/components/zha/device_action.py index 076cb1d420e..8f5a03a7fe5 100644 --- a/homeassistant/components/zha/device_action.py +++ b/homeassistant/components/zha/device_action.py @@ -136,8 +136,7 @@ async def async_validate_action_config( ) -> ConfigType: """Validate config.""" schema = ACTION_SCHEMA_MAP.get(config[CONF_TYPE], DEFAULT_ACTION_SCHEMA) - config = schema(config) - return config + return schema(config) async def async_get_actions( diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 8d65899707e..5e729a74f0d 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -147,11 +147,10 @@ class BaseLight(LogMixin, light.LightEntity): @property def extra_state_attributes(self) -> dict[str, Any]: """Return state attributes.""" - attributes = { + return { "off_with_transition": self._off_with_transition, "off_brightness": self._off_brightness, } - return attributes @property def is_on(self) -> bool: diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 91fe302291a..e8507a96e2c 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -434,8 +434,7 @@ class Battery(Sensor): # per zcl specs battery percent is reported at 200% ¯\_(ツ)_/¯ if not isinstance(value, numbers.Number) or value == -1 or value == 255: return None - value = round(value / 2) - return value + return round(value / 2) @property def extra_state_attributes(self) -> dict[str, Any]: diff --git a/homeassistant/components/zwave_js/scripts/convert_device_diagnostics_to_fixture.py b/homeassistant/components/zwave_js/scripts/convert_device_diagnostics_to_fixture.py index 1005c3bb4db..6b73d1362f9 100644 --- a/homeassistant/components/zwave_js/scripts/convert_device_diagnostics_to_fixture.py +++ b/homeassistant/components/zwave_js/scripts/convert_device_diagnostics_to_fixture.py @@ -25,9 +25,7 @@ def get_arguments() -> argparse.Namespace: ), ) - arguments = parser.parse_args() - - return arguments + return parser.parse_args() def get_fixtures_dir_path(data: dict) -> Path: diff --git a/homeassistant/components/zwave_me/__init__.py b/homeassistant/components/zwave_me/__init__.py index 66f02246792..7e00924c221 100644 --- a/homeassistant/components/zwave_me/__init__.py +++ b/homeassistant/components/zwave_me/__init__.py @@ -63,8 +63,7 @@ class ZWaveMeController: async def async_establish_connection(self): """Get connection status.""" - is_connected = await self.zwave_api.get_connection() - return is_connected + return await self.zwave_api.get_connection() def add_device(self, device: ZWaveMeData) -> None: """Send signal to create device.""" diff --git a/homeassistant/config.py b/homeassistant/config.py index d3f30f84a68..61b346944fa 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -329,8 +329,7 @@ def _validate_currency(data: Any) -> Any: return cv.currency(data) except vol.InInvalid: with suppress(vol.InInvalid): - currency = cv.historic_currency(data) - return currency + return cv.historic_currency(data) raise diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 6278586f469..2437d42da59 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -126,7 +126,7 @@ def async_create_clientsession( if auto_cleanup: auto_cleanup_method = _async_register_clientsession_shutdown - clientsession = _async_create_clientsession( + return _async_create_clientsession( hass, verify_ssl, auto_cleanup_method=auto_cleanup_method, @@ -134,8 +134,6 @@ def async_create_clientsession( **kwargs, ) - return clientsession - @callback def _async_create_clientsession( diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index 7979247c8b0..4970b89db1f 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -73,12 +73,11 @@ class StoredState: def as_dict(self) -> dict[str, Any]: """Return a dict representation of the stored state to be JSON serialized.""" - result = { + return { "state": self.state.json_fragment, "extra_data": self.extra_data.as_dict() if self.extra_data else None, "last_seen": self.last_seen, } - return result @classmethod def from_dict(cls, json_dict: dict) -> Self: diff --git a/homeassistant/helpers/schema_config_entry_flow.py b/homeassistant/helpers/schema_config_entry_flow.py index 0486c7b6f8c..978ce949eb3 100644 --- a/homeassistant/helpers/schema_config_entry_flow.py +++ b/homeassistant/helpers/schema_config_entry_flow.py @@ -357,8 +357,7 @@ class SchemaConfigFlowHandler(ConfigFlow, ABC): ) -> ConfigFlowResult: """Handle a config flow step.""" # pylint: disable-next=protected-access - result = await self._common_handler.async_step(step_id, user_input) - return result + return await self._common_handler.async_step(step_id, user_input) return _async_step @@ -452,8 +451,7 @@ class SchemaOptionsFlowHandler(OptionsFlowWithConfigEntry): ) -> ConfigFlowResult: """Handle an options flow step.""" # pylint: disable-next=protected-access - result = await self._common_handler.async_step(step_id, user_input) - return result + return await self._common_handler.async_step(step_id, user_input) return _async_step diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 0b054307702..0fee9fae322 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -817,8 +817,7 @@ class _ScriptRun: return True - result = traced_test_conditions(self._hass, self._variables) - return result + return traced_test_conditions(self._hass, self._variables) @async_trace_path("repeat") async def _async_repeat_step(self): # noqa: C901 diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index 489b6493ef1..09ece063dd0 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -198,12 +198,10 @@ def async_create_catching_coro( target: target coroutine. """ trace = traceback.extract_stack() - wrapped_target = catch_log_coro_exception( + return catch_log_coro_exception( target, lambda: "Exception in {} called from\n {}".format( target.__name__, "".join(traceback.format_list(trace[:-1])), ), ) - - return wrapped_target diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index 44f9be3272f..067bf5ff36d 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -147,5 +147,4 @@ async def async_get_user_site(deps_dir: str) -> str: close_fds=False, # required for posix_spawn ) stdout, _ = await process.communicate() - lib_dir = stdout.decode().strip() - return lib_dir + return stdout.decode().strip() diff --git a/homeassistant/util/timeout.py b/homeassistant/util/timeout.py index cf73ee6b220..94baa57e4d8 100644 --- a/homeassistant/util/timeout.py +++ b/homeassistant/util/timeout.py @@ -472,8 +472,7 @@ class TimeoutManager: # Global Zone if zone_name == ZONE_GLOBAL: - task = _GlobalTaskContext(self, current_task, timeout, cool_down) - return task + return _GlobalTaskContext(self, current_task, timeout, cool_down) # Zone Handling if zone_name in self.zones: diff --git a/pyproject.toml b/pyproject.toml index e8558524b08..f35aa12221f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -719,7 +719,6 @@ ignore = [ # temporarily disabled "PT019", - "RET504", "RET503", "RET502", "RET501", diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 9a9ff6821c7..d8fffac1a06 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -263,9 +263,7 @@ def normalize_package_name(requirement: str) -> str: return "" # pipdeptree needs lowercase and dash instead of underscore as separator - package = match.group(1).lower().replace("_", "-") - - return package + return match.group(1).lower().replace("_", "-") def comment_requirement(req: str) -> bool: diff --git a/script/install_integration_requirements.py b/script/install_integration_requirements.py index 1a87be04f7e..fec893c008a 100644 --- a/script/install_integration_requirements.py +++ b/script/install_integration_requirements.py @@ -18,9 +18,7 @@ def get_arguments() -> argparse.Namespace: "integration", type=valid_integration, help="Integration to target." ) - arguments = parser.parse_args() - - return arguments + return parser.parse_args() def main() -> int | None: diff --git a/script/scaffold/__main__.py b/script/scaffold/__main__.py index f81f9144f98..45dbed790e6 100644 --- a/script/scaffold/__main__.py +++ b/script/scaffold/__main__.py @@ -25,9 +25,7 @@ def get_arguments() -> argparse.Namespace: "--integration", type=valid_integration, help="Integration to target." ) - arguments = parser.parse_args() - - return arguments + return parser.parse_args() def main(): diff --git a/tests/components/airthings_ble/__init__.py b/tests/components/airthings_ble/__init__.py index ba213e0c2e7..3622a21f633 100644 --- a/tests/components/airthings_ble/__init__.py +++ b/tests/components/airthings_ble/__init__.py @@ -235,11 +235,10 @@ def create_entry(hass): def create_device(entry: ConfigEntry, device_registry: DeviceRegistry): """Create a device for the given entry.""" - device = device_registry.async_get_or_create( + return device_registry.async_get_or_create( config_entry_id=entry.entry_id, connections={(CONNECTION_BLUETOOTH, WAVE_SERVICE_INFO.address)}, manufacturer="Airthings AS", name="Airthings Wave Plus (123456)", model="Wave Plus", ) - return device diff --git a/tests/components/alexa/test_smart_home_http.py b/tests/components/alexa/test_smart_home_http.py index 153442552a4..1c30c72e72c 100644 --- a/tests/components/alexa/test_smart_home_http.py +++ b/tests/components/alexa/test_smart_home_http.py @@ -23,12 +23,11 @@ async def do_http_discovery(config, hass, hass_client): http_client = await hass_client() request = get_new_request("Alexa.Discovery", "Discover") - response = await http_client.post( + return await http_client.post( smart_home.SMART_HOME_HTTP_ENDPOINT, data=json.dumps(request), headers={"content-type": CONTENT_TYPE_JSON}, ) - return response @pytest.mark.parametrize( diff --git a/tests/components/blebox/test_config_flow.py b/tests/components/blebox/test_config_flow.py index fd6d0ce8fb2..612c4f09424 100644 --- a/tests/components/blebox/test_config_flow.py +++ b/tests/components/blebox/test_config_flow.py @@ -88,8 +88,7 @@ async def test_flow_works( def product_class_mock_fixture(): """Return a mocked feature.""" path = "homeassistant.components.blebox.config_flow.Box" - patcher = patch(path, DEFAULT, blebox_uniapi.box.Box, True, True) - return patcher + return patch(path, DEFAULT, blebox_uniapi.box.Box, True, True) async def test_flow_with_connection_failure( diff --git a/tests/components/cast/conftest.py b/tests/components/cast/conftest.py index d9ebb24696e..66a55ba7efd 100644 --- a/tests/components/cast/conftest.py +++ b/tests/components/cast/conftest.py @@ -18,8 +18,7 @@ def get_multizone_status_mock(): @pytest.fixture def get_cast_type_mock(): """Mock pychromecast dial.""" - mock = MagicMock(spec_set=pychromecast.dial.get_cast_type) - return mock + return MagicMock(spec_set=pychromecast.dial.get_cast_type) @pytest.fixture diff --git a/tests/components/dlna_dmr/conftest.py b/tests/components/dlna_dmr/conftest.py index bb47a468dc4..59b1af546f2 100644 --- a/tests/components/dlna_dmr/conftest.py +++ b/tests/components/dlna_dmr/conftest.py @@ -82,7 +82,7 @@ def domain_data_mock(hass: HomeAssistant) -> Iterable[Mock]: @pytest.fixture def config_entry_mock() -> MockConfigEntry: """Mock a config entry for this platform.""" - mock_entry = MockConfigEntry( + return MockConfigEntry( unique_id=MOCK_DEVICE_UDN, domain=DLNA_DOMAIN, data={ @@ -94,13 +94,12 @@ def config_entry_mock() -> MockConfigEntry: title=MOCK_DEVICE_NAME, options={}, ) - return mock_entry @pytest.fixture def config_entry_mock_no_mac() -> MockConfigEntry: """Mock a config entry that does not already contain a MAC address.""" - mock_entry = MockConfigEntry( + return MockConfigEntry( unique_id=MOCK_DEVICE_UDN, domain=DLNA_DOMAIN, data={ @@ -111,7 +110,6 @@ def config_entry_mock_no_mac() -> MockConfigEntry: title=MOCK_DEVICE_NAME, options={}, ) - return mock_entry @pytest.fixture diff --git a/tests/components/dlna_dmr/test_media_player.py b/tests/components/dlna_dmr/test_media_player.py index 9ead49f0955..87c54c2956b 100644 --- a/tests/components/dlna_dmr/test_media_player.py +++ b/tests/components/dlna_dmr/test_media_player.py @@ -85,9 +85,7 @@ async def setup_mock_component(hass: HomeAssistant, mock_entry: MockConfigEntry) entries = async_entries_for_config_entry(async_get_er(hass), mock_entry.entry_id) assert len(entries) == 1 - entity_id = entries[0].entity_id - - return entity_id + return entries[0].entity_id async def get_attrs(hass: HomeAssistant, entity_id: str) -> Mapping[str, Any]: diff --git a/tests/components/dlna_dms/conftest.py b/tests/components/dlna_dms/conftest.py index eacf03e9da7..c1bee224c5a 100644 --- a/tests/components/dlna_dms/conftest.py +++ b/tests/components/dlna_dms/conftest.py @@ -93,7 +93,7 @@ def aiohttp_session_requester_mock() -> Iterable[Mock]: @pytest.fixture def config_entry_mock() -> MockConfigEntry: """Mock a config entry for this platform.""" - mock_entry = MockConfigEntry( + return MockConfigEntry( unique_id=MOCK_DEVICE_USN, domain=DOMAIN, version=CONFIG_VERSION, @@ -104,7 +104,6 @@ def config_entry_mock() -> MockConfigEntry: }, title=MOCK_DEVICE_NAME, ) - return mock_entry @pytest.fixture diff --git a/tests/components/electric_kiwi/conftest.py b/tests/components/electric_kiwi/conftest.py index 0a1d32f0ec0..8819b1e134d 100644 --- a/tests/components/electric_kiwi/conftest.py +++ b/tests/components/electric_kiwi/conftest.py @@ -63,7 +63,7 @@ def component_setup( @pytest.fixture(name="config_entry") def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry: """Create mocked config entry.""" - entry = MockConfigEntry( + return MockConfigEntry( title="Electric Kiwi", domain=DOMAIN, data={ @@ -79,7 +79,6 @@ def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry: }, unique_id=DOMAIN, ) - return entry @pytest.fixture diff --git a/tests/components/escea/test_config_flow.py b/tests/components/escea/test_config_flow.py index 3cb914bfaf3..aa863bd4371 100644 --- a/tests/components/escea/test_config_flow.py +++ b/tests/components/escea/test_config_flow.py @@ -25,8 +25,7 @@ def mock_discovery_service_fixture() -> AsyncMock: @pytest.fixture(name="mock_controller") def mock_controller_fixture() -> MagicMock: """Mock controller.""" - controller = MagicMock() - return controller + return MagicMock() def _mock_start_discovery( diff --git a/tests/components/esphome/test_voice_assistant.py b/tests/components/esphome/test_voice_assistant.py index 427cd1dbc8f..9882419ed5a 100644 --- a/tests/components/esphome/test_voice_assistant.py +++ b/tests/components/esphome/test_voice_assistant.py @@ -47,7 +47,7 @@ def voice_assistant_udp_server( server.close() server = VoiceAssistantUDPServer(hass, entry_data, Mock(), handle_finished) - return server + return server # noqa: RET504 return _voice_assistant_udp_server diff --git a/tests/components/feedreader/test_init.py b/tests/components/feedreader/test_init.py index f836d233670..67ce95811a0 100644 --- a/tests/components/feedreader/test_init.py +++ b/tests/components/feedreader/test_init.py @@ -36,8 +36,7 @@ VALID_CONFIG_5 = {feedreader.DOMAIN: {CONF_URLS: [URL], CONF_MAX_ENTRIES: 1}} def load_fixture_bytes(src: str) -> bytes: """Return byte stream of fixture.""" feed_data = load_fixture(src, DOMAIN) - raw = bytes(feed_data, "utf-8") - return raw + return bytes(feed_data, "utf-8") @pytest.fixture(name="feed_one_event") diff --git a/tests/components/generic/conftest.py b/tests/components/generic/conftest.py index e9233ffc559..92a9298cbd5 100644 --- a/tests/components/generic/conftest.py +++ b/tests/components/generic/conftest.py @@ -68,11 +68,10 @@ def mock_create_stream(): mock_stream.add_provider.return_value = mock_provider mock_stream.start = AsyncMock() mock_stream.stop = AsyncMock() - fake_create_stream = patch( + return patch( "homeassistant.components.generic.config_flow.create_stream", return_value=mock_stream, ) - return fake_create_stream @pytest.fixture diff --git a/tests/components/gree/common.py b/tests/components/gree/common.py index 97656596ce6..ca217168b18 100644 --- a/tests/components/gree/common.py +++ b/tests/components/gree/common.py @@ -69,7 +69,7 @@ def build_device_info_mock( def build_device_mock(name="fake-device-1", ipAddress="1.1.1.1", mac="aabbcc112233"): """Build mock device object.""" - mock = Mock( + return Mock( device_info=build_device_info_mock(name, ipAddress, mac), name=name, bind=AsyncMock(), @@ -89,7 +89,6 @@ def build_device_mock(name="fake-device-1", ipAddress="1.1.1.1", mac="aabbcc1122 power_save=False, steady_heat=False, ) - return mock async def async_setup_gree(hass: HomeAssistant) -> MockConfigEntry: diff --git a/tests/components/here_travel_time/test_config_flow.py b/tests/components/here_travel_time/test_config_flow.py index 002e2ed8fdb..eb958991c71 100644 --- a/tests/components/here_travel_time/test_config_flow.py +++ b/tests/components/here_travel_time/test_config_flow.py @@ -84,13 +84,12 @@ async def option_init_result_fixture(hass: HomeAssistant) -> FlowResultType: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() flow = await hass.config_entries.options.async_init(entry.entry_id) - result = await hass.config_entries.options.async_configure( + return await hass.config_entries.options.async_configure( flow["flow_id"], user_input={ CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, }, ) - return result @pytest.fixture(name="origin_step_result") @@ -102,7 +101,7 @@ async def origin_step_result_fixture( user_step_result["flow_id"], {"next_step_id": "origin_coordinates"} ) - location_selector_result = await hass.config_entries.flow.async_configure( + return await hass.config_entries.flow.async_configure( origin_menu_result["flow_id"], { "origin": { @@ -112,7 +111,6 @@ async def origin_step_result_fixture( } }, ) - return location_selector_result @pytest.mark.parametrize( diff --git a/tests/components/homekit_controller/common.py b/tests/components/homekit_controller/common.py index 39466cc51e4..8c45080c786 100644 --- a/tests/components/homekit_controller/common.py +++ b/tests/components/homekit_controller/common.py @@ -195,8 +195,7 @@ async def setup_accessories_from_file(hass: HomeAssistant, path: str) -> Accesso load_fixture, os.path.join("homekit_controller", path) ) accessories_json = hkloads(accessories_fixture) - accessories = Accessories.from_list(accessories_json) - return accessories + return Accessories.from_list(accessories_json) async def setup_platform(hass): diff --git a/tests/components/homematicip_cloud/conftest.py b/tests/components/homematicip_cloud/conftest.py index 88298521f75..3f87f12d9fc 100644 --- a/tests/components/homematicip_cloud/conftest.py +++ b/tests/components/homematicip_cloud/conftest.py @@ -54,7 +54,7 @@ def hmip_config_entry_fixture() -> config_entries.ConfigEntry: HMIPC_NAME: "", HMIPC_PIN: HAPPIN, } - config_entry = MockConfigEntry( + return MockConfigEntry( version=1, domain=HMIPC_DOMAIN, title="Home Test SN", @@ -63,8 +63,6 @@ def hmip_config_entry_fixture() -> config_entries.ConfigEntry: source=SOURCE_IMPORT, ) - return config_entry - @pytest.fixture(name="default_mock_hap_factory") async def default_mock_hap_factory_fixture( diff --git a/tests/components/http/test_data_validator.py b/tests/components/http/test_data_validator.py index af55e0b8597..9a4e80052f6 100644 --- a/tests/components/http/test_data_validator.py +++ b/tests/components/http/test_data_validator.py @@ -30,8 +30,7 @@ async def get_client(aiohttp_client, validator): return b"" TestView().register(app[KEY_HASS], app, app.router) - client = await aiohttp_client(app) - return client + return await aiohttp_client(app) async def test_validator(aiohttp_client: ClientSessionGenerator) -> None: diff --git a/tests/components/insteon/test_config_flow.py b/tests/components/insteon/test_config_flow.py index ae21489bd62..7cc0eefc0b5 100644 --- a/tests/components/insteon/test_config_flow.py +++ b/tests/components/insteon/test_config_flow.py @@ -95,11 +95,10 @@ async def _init_form(hass, modem_type): ) assert result["type"] is FlowResultType.MENU - result2 = await hass.config_entries.flow.async_configure( + return await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": modem_type}, ) - return result2 async def _device_form(hass, flow_id, connection, user_input): @@ -303,11 +302,10 @@ async def _options_init_form(hass, entry_id, step): assert result["type"] is FlowResultType.MENU assert result["step_id"] == "init" - result2 = await hass.config_entries.options.async_configure( + return await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": step}, ) - return result2 async def _options_form( diff --git a/tests/components/lcn/conftest.py b/tests/components/lcn/conftest.py index 2e50f20561e..6571b63ddf1 100644 --- a/tests/components/lcn/conftest.py +++ b/tests/components/lcn/conftest.py @@ -68,14 +68,13 @@ def create_config_entry(name): title = entry_data[CONF_HOST] unique_id = fixture_filename - entry = MockConfigEntry( + return MockConfigEntry( domain=DOMAIN, title=title, unique_id=unique_id, data=entry_data, options=options, ) - return entry @pytest.fixture diff --git a/tests/components/lookin/__init__.py b/tests/components/lookin/__init__.py index e19bdc9fd73..cea0f969893 100644 --- a/tests/components/lookin/__init__.py +++ b/tests/components/lookin/__init__.py @@ -31,13 +31,11 @@ ZEROCONF_DATA = ZeroconfServiceInfo( def _mocked_climate() -> Climate: - climate = MagicMock(auto_spec=Climate) - return climate + return MagicMock(auto_spec=Climate) def _mocked_remote() -> Remote: - remote = MagicMock(auto_spec=Remote) - return remote + return MagicMock(auto_spec=Remote) def _mocked_device() -> Device: diff --git a/tests/components/minecraft_server/test_init.py b/tests/components/minecraft_server/test_init.py index 8ac2e7ca04d..6f7a49a190c 100644 --- a/tests/components/minecraft_server/test_init.py +++ b/tests/components/minecraft_server/test_init.py @@ -108,13 +108,11 @@ def create_v1_mock_binary_sensor_entity_entry( device_id=device_entry_id, ) assert entity_entry.unique_id == entity_unique_id - binary_sensor_entity_id_key_mapping = { + return { "entity_id": entity_entry.entity_id, "key": BINARY_SENSOR_KEYS["v2"], } - return binary_sensor_entity_id_key_mapping - async def test_setup_and_unload_entry( hass: HomeAssistant, java_mock_config_entry: MockConfigEntry diff --git a/tests/components/modern_forms/__init__.py b/tests/components/modern_forms/__init__.py index 65de87c333d..ae4e5bd9862 100644 --- a/tests/components/modern_forms/__init__.py +++ b/tests/components/modern_forms/__init__.py @@ -19,10 +19,9 @@ async def modern_forms_call_mock(method, url, data): fixture = "modern_forms/device_info.json" else: fixture = "modern_forms/device_status.json" - response = AiohttpClientMockResponse( + return AiohttpClientMockResponse( method=method, url=url, json=json.loads(load_fixture(fixture)) ) - return response async def modern_forms_no_light_call_mock(method, url, data): @@ -31,10 +30,9 @@ async def modern_forms_no_light_call_mock(method, url, data): fixture = "modern_forms/device_info_no_light.json" else: fixture = "modern_forms/device_status_no_light.json" - response = AiohttpClientMockResponse( + return AiohttpClientMockResponse( method=method, url=url, json=json.loads(load_fixture(fixture)) ) - return response async def modern_forms_timers_set_mock(method, url, data): @@ -43,10 +41,9 @@ async def modern_forms_timers_set_mock(method, url, data): fixture = "modern_forms/device_info.json" else: fixture = "modern_forms/device_status_timers_active.json" - response = AiohttpClientMockResponse( + return AiohttpClientMockResponse( method=method, url=url, json=json.loads(load_fixture(fixture)) ) - return response async def init_integration( diff --git a/tests/components/mysensors/conftest.py b/tests/components/mysensors/conftest.py index bcf852e1368..e18043fda1f 100644 --- a/tests/components/mysensors/conftest.py +++ b/tests/components/mysensors/conftest.py @@ -116,7 +116,7 @@ def transport_write(transport: MagicMock) -> MagicMock: @pytest.fixture(name="serial_entry") async def serial_entry_fixture(hass: HomeAssistant) -> MockConfigEntry: """Create a config entry for a serial gateway.""" - entry = MockConfigEntry( + return MockConfigEntry( domain=DOMAIN, data={ CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_SERIAL, @@ -125,7 +125,6 @@ async def serial_entry_fixture(hass: HomeAssistant) -> MockConfigEntry: CONF_BAUD_RATE: DEFAULT_BAUD_RATE, }, ) - return entry @pytest.fixture(name="config_entry") @@ -219,8 +218,7 @@ def cover_node_binary( ) -> Sensor: """Load the cover child node.""" nodes = update_gateway_nodes(gateway_nodes, deepcopy(cover_node_binary_state)) - node = nodes[1] - return node + return nodes[1] @pytest.fixture(name="cover_node_percentage_state", scope="session") @@ -235,8 +233,7 @@ def cover_node_percentage( ) -> Sensor: """Load the cover child node.""" nodes = update_gateway_nodes(gateway_nodes, deepcopy(cover_node_percentage_state)) - node = nodes[1] - return node + return nodes[1] @pytest.fixture(name="door_sensor_state", scope="session") @@ -249,8 +246,7 @@ def door_sensor_state_fixture() -> dict: def door_sensor(gateway_nodes: dict[int, Sensor], door_sensor_state: dict) -> Sensor: """Load the door sensor.""" nodes = update_gateway_nodes(gateway_nodes, deepcopy(door_sensor_state)) - node = nodes[1] - return node + return nodes[1] @pytest.fixture(name="gps_sensor_state", scope="session") @@ -263,8 +259,7 @@ def gps_sensor_state_fixture() -> dict: def gps_sensor(gateway_nodes: dict[int, Sensor], gps_sensor_state: dict) -> Sensor: """Load the gps sensor.""" nodes = update_gateway_nodes(gateway_nodes, deepcopy(gps_sensor_state)) - node = nodes[1] - return node + return nodes[1] @pytest.fixture(name="dimmer_node_state", scope="session") @@ -277,8 +272,7 @@ def dimmer_node_state_fixture() -> dict: def dimmer_node(gateway_nodes: dict[int, Sensor], dimmer_node_state: dict) -> Sensor: """Load the dimmer child node.""" nodes = update_gateway_nodes(gateway_nodes, deepcopy(dimmer_node_state)) - node = nodes[1] - return node + return nodes[1] @pytest.fixture(name="hvac_node_auto_state", scope="session") @@ -293,8 +287,7 @@ def hvac_node_auto( ) -> Sensor: """Load the hvac auto child node.""" nodes = update_gateway_nodes(gateway_nodes, deepcopy(hvac_node_auto_state)) - node = nodes[1] - return node + return nodes[1] @pytest.fixture(name="hvac_node_cool_state", scope="session") @@ -309,8 +302,7 @@ def hvac_node_cool( ) -> Sensor: """Load the hvac cool child node.""" nodes = update_gateway_nodes(gateway_nodes, deepcopy(hvac_node_cool_state)) - node = nodes[1] - return node + return nodes[1] @pytest.fixture(name="hvac_node_heat_state", scope="session") @@ -325,8 +317,7 @@ def hvac_node_heat( ) -> Sensor: """Load the hvac heat child node.""" nodes = update_gateway_nodes(gateway_nodes, deepcopy(hvac_node_heat_state)) - node = nodes[1] - return node + return nodes[1] @pytest.fixture(name="power_sensor_state", scope="session") @@ -339,8 +330,7 @@ def power_sensor_state_fixture() -> dict: def power_sensor(gateway_nodes: dict[int, Sensor], power_sensor_state: dict) -> Sensor: """Load the power sensor.""" nodes = update_gateway_nodes(gateway_nodes, power_sensor_state) - node = nodes[1] - return node + return nodes[1] @pytest.fixture(name="rgb_node_state", scope="session") @@ -353,8 +343,7 @@ def rgb_node_state_fixture() -> dict: def rgb_node(gateway_nodes: dict[int, Sensor], rgb_node_state: dict) -> Sensor: """Load the rgb child node.""" nodes = update_gateway_nodes(gateway_nodes, deepcopy(rgb_node_state)) - node = nodes[1] - return node + return nodes[1] @pytest.fixture(name="rgbw_node_state", scope="session") @@ -367,8 +356,7 @@ def rgbw_node_state_fixture() -> dict: def rgbw_node(gateway_nodes: dict[int, Sensor], rgbw_node_state: dict) -> Sensor: """Load the rgbw child node.""" nodes = update_gateway_nodes(gateway_nodes, deepcopy(rgbw_node_state)) - node = nodes[1] - return node + return nodes[1] @pytest.fixture(name="energy_sensor_state", scope="session") @@ -383,8 +371,7 @@ def energy_sensor( ) -> Sensor: """Load the energy sensor.""" nodes = update_gateway_nodes(gateway_nodes, energy_sensor_state) - node = nodes[1] - return node + return nodes[1] @pytest.fixture(name="sound_sensor_state", scope="session") @@ -397,8 +384,7 @@ def sound_sensor_state_fixture() -> dict: def sound_sensor(gateway_nodes: dict[int, Sensor], sound_sensor_state: dict) -> Sensor: """Load the sound sensor.""" nodes = update_gateway_nodes(gateway_nodes, sound_sensor_state) - node = nodes[1] - return node + return nodes[1] @pytest.fixture(name="distance_sensor_state", scope="session") @@ -413,8 +399,7 @@ def distance_sensor( ) -> Sensor: """Load the distance sensor.""" nodes = update_gateway_nodes(gateway_nodes, distance_sensor_state) - node = nodes[1] - return node + return nodes[1] @pytest.fixture(name="ir_transceiver_state", scope="session") @@ -429,8 +414,7 @@ def ir_transceiver( ) -> Sensor: """Load the ir transceiver child node.""" nodes = update_gateway_nodes(gateway_nodes, deepcopy(ir_transceiver_state)) - node = nodes[1] - return node + return nodes[1] @pytest.fixture(name="relay_node_state", scope="session") @@ -443,8 +427,7 @@ def relay_node_state_fixture() -> dict: def relay_node(gateway_nodes: dict[int, Sensor], relay_node_state: dict) -> Sensor: """Load the relay child node.""" nodes = update_gateway_nodes(gateway_nodes, deepcopy(relay_node_state)) - node = nodes[1] - return node + return nodes[1] @pytest.fixture(name="temperature_sensor_state", scope="session") @@ -459,8 +442,7 @@ def temperature_sensor( ) -> Sensor: """Load the temperature sensor.""" nodes = update_gateway_nodes(gateway_nodes, temperature_sensor_state) - node = nodes[1] - return node + return nodes[1] @pytest.fixture(name="text_node_state", scope="session") @@ -473,8 +455,7 @@ def text_node_state_fixture() -> dict: def text_node(gateway_nodes: dict[int, Sensor], text_node_state: dict) -> Sensor: """Load the text child node.""" nodes = update_gateway_nodes(gateway_nodes, text_node_state) - node = nodes[1] - return node + return nodes[1] @pytest.fixture(name="battery_sensor_state", scope="session") @@ -489,5 +470,4 @@ def battery_sensor( ) -> Sensor: """Load the battery sensor.""" nodes = update_gateway_nodes(gateway_nodes, deepcopy(battery_sensor_state)) - node = nodes[1] - return node + return nodes[1] diff --git a/tests/components/nest/test_media_source.py b/tests/components/nest/test_media_source.py index 3d0ec521df2..def99633435 100644 --- a/tests/components/nest/test_media_source.py +++ b/tests/components/nest/test_media_source.py @@ -85,8 +85,7 @@ def frame_image_data(frame_i, total_frames): img[:, :, 2] = 0.5 + 0.5 * np.sin(2 * np.pi * (2 / 3 + frame_i / total_frames)) img = np.round(255 * img).astype(np.uint8) - img = np.clip(img, 0, 255) - return img + return np.clip(img, 0, 255) @pytest.fixture diff --git a/tests/components/nextcloud/conftest.py b/tests/components/nextcloud/conftest.py index 0ea281abb49..58b37359d42 100644 --- a/tests/components/nextcloud/conftest.py +++ b/tests/components/nextcloud/conftest.py @@ -9,12 +9,10 @@ import pytest @pytest.fixture def mock_nextcloud_monitor() -> Mock: """Mock of NextcloudMonitor.""" - ncm = Mock( + return Mock( update=Mock(return_value=True), ) - return ncm - @pytest.fixture def mock_setup_entry() -> Generator[AsyncMock, None, None]: diff --git a/tests/components/overkiz/__init__.py b/tests/components/overkiz/__init__.py index e7d729ea41c..48d6055f23a 100644 --- a/tests/components/overkiz/__init__.py +++ b/tests/components/overkiz/__init__.py @@ -11,6 +11,4 @@ def load_setup_fixture( ) -> Setup: """Return setup from fixture.""" setup_json = load_json_object_fixture(fixture) - setup = Setup(**humps.decamelize(setup_json)) - - return setup + return Setup(**humps.decamelize(setup_json)) diff --git a/tests/components/plex/conftest.py b/tests/components/plex/conftest.py index d6c91a9d9a8..7e82b1c9d26 100644 --- a/tests/components/plex/conftest.py +++ b/tests/components/plex/conftest.py @@ -599,8 +599,7 @@ def setup_plex_server( websocket_connected(mock_websocket) await hass.async_block_till_done() - plex_server = hass.data[DOMAIN][SERVERS][entry.unique_id] - return plex_server + return hass.data[DOMAIN][SERVERS][entry.unique_id] return _wrapper diff --git a/tests/components/ps4/test_media_player.py b/tests/components/ps4/test_media_player.py index 6adcad03016..e0be9d508fc 100644 --- a/tests/components/ps4/test_media_player.py +++ b/tests/components/ps4/test_media_player.py @@ -147,9 +147,7 @@ async def setup_mock_component(hass, entry=None): mock_entities = hass.states.async_entity_ids() - mock_entity_id = mock_entities[0] - - return mock_entity_id + return mock_entities[0] async def mock_ddp_response(hass, mock_status_data): diff --git a/tests/components/refoss/__init__.py b/tests/components/refoss/__init__.py index e7c160ef0af..51c4261b954 100644 --- a/tests/components/refoss/__init__.py +++ b/tests/components/refoss/__init__.py @@ -62,7 +62,7 @@ class FakeDiscovery: def build_device_mock(name="r10", ip="1.1.1.1", mac="aabbcc112233"): """Build mock device object.""" - mock = Mock( + return Mock( uuid="abc", dev_name=name, device_type="r10", @@ -74,7 +74,6 @@ def build_device_mock(name="r10", ip="1.1.1.1", mac="aabbcc112233"): sub_type="eu", channels=[0], ) - return mock def build_base_device_mock(name="r10", ip="1.1.1.1", mac="aabbcc112233"): diff --git a/tests/components/shopping_list/test_todo.py b/tests/components/shopping_list/test_todo.py index fb6f61d4edf..173544d0be2 100644 --- a/tests/components/shopping_list/test_todo.py +++ b/tests/components/shopping_list/test_todo.py @@ -53,8 +53,7 @@ async def ws_move_item( if previous_uid is not None: data["previous_uid"] = previous_uid await client.send_json_auto_id(data) - resp = await client.receive_json() - return resp + return await client.receive_json() return move diff --git a/tests/components/smtp/test_notify.py b/tests/components/smtp/test_notify.py index dd51cf15992..15be7b66d27 100644 --- a/tests/components/smtp/test_notify.py +++ b/tests/components/smtp/test_notify.py @@ -72,7 +72,7 @@ async def test_reload_notify(hass: HomeAssistant) -> None: @pytest.fixture def message(): """Return MockSMTP object with test data.""" - mailer = MockSMTP( + return MockSMTP( "localhost", 25, 5, @@ -85,7 +85,6 @@ def message(): 0, True, ) - return mailer HTML = """ diff --git a/tests/components/srp_energy/conftest.py b/tests/components/srp_energy/conftest.py index c2cc6d5e1a4..12fa7ffd6d6 100644 --- a/tests/components/srp_energy/conftest.py +++ b/tests/components/srp_energy/conftest.py @@ -36,8 +36,7 @@ def fixture_hass_tz_info(hass: HomeAssistant, setup_hass_config) -> dt.tzinfo | @pytest.fixture(name="test_date") def fixture_test_date(hass: HomeAssistant, hass_tz_info) -> dt.datetime | None: """Return test datetime for the hass timezone.""" - test_date = dt.datetime(2022, 8, 2, 0, 0, 0, 0, tzinfo=hass_tz_info) - return test_date + return dt.datetime(2022, 8, 2, 0, 0, 0, 0, tzinfo=hass_tz_info) @pytest.fixture(name="mock_config_entry") diff --git a/tests/components/stream/common.py b/tests/components/stream/common.py index ab42141c667..e642b209146 100644 --- a/tests/components/stream/common.py +++ b/tests/components/stream/common.py @@ -59,8 +59,7 @@ def frame_image_data(frame_i, total_frames): img[:, :, 2] = 0.5 + 0.5 * np.sin(2 * np.pi * (2 / 3 + frame_i / total_frames)) img = np.round(255 * img).astype(np.uint8) - img = np.clip(img, 0, 255) - return img + return np.clip(img, 0, 255) def generate_video(encoder, container_format, duration): diff --git a/tests/components/tesla_wall_connector/conftest.py b/tests/components/tesla_wall_connector/conftest.py index a31df11dcd0..e10ae190a59 100644 --- a/tests/components/tesla_wall_connector/conftest.py +++ b/tests/components/tesla_wall_connector/conftest.py @@ -88,14 +88,12 @@ async def create_wall_connector_entry( def get_vitals_mock() -> Vitals: """Get mocked vitals object.""" - vitals = MagicMock(auto_spec=Vitals) - return vitals + return MagicMock(auto_spec=Vitals) def get_lifetime_mock() -> Lifetime: """Get mocked lifetime object.""" - lifetime = MagicMock(auto_spec=Lifetime) - return lifetime + return MagicMock(auto_spec=Lifetime) @dataclass diff --git a/tests/components/transport_nsw/test_sensor.py b/tests/components/transport_nsw/test_sensor.py index 9ecff818592..80df947dbe7 100644 --- a/tests/components/transport_nsw/test_sensor.py +++ b/tests/components/transport_nsw/test_sensor.py @@ -19,7 +19,7 @@ VALID_CONFIG = { def get_departuresMock(_stop_id, route, destination, api_key): """Mock TransportNSW departures loading.""" - data = { + return { "stop_id": "209516", "route": "199", "due": 16, @@ -28,7 +28,6 @@ def get_departuresMock(_stop_id, route, destination, api_key): "destination": "Palm Beach", "mode": "Bus", } - return data @patch("TransportNSW.TransportNSW.get_departures", side_effect=get_departuresMock) @@ -50,7 +49,7 @@ async def test_transportnsw_config(mocked_get_departures, hass: HomeAssistant) - def get_departuresMock_notFound(_stop_id, route, destination, api_key): """Mock TransportNSW departures loading.""" - data = { + return { "stop_id": "n/a", "route": "n/a", "due": "n/a", @@ -59,7 +58,6 @@ def get_departuresMock_notFound(_stop_id, route, destination, api_key): "destination": "n/a", "mode": "n/a", } - return data @patch( diff --git a/tests/components/upb/test_config_flow.py b/tests/components/upb/test_config_flow.py index 3a82eaa0ae7..5eaed2e3a24 100644 --- a/tests/components/upb/test_config_flow.py +++ b/tests/components/upb/test_config_flow.py @@ -34,11 +34,10 @@ async def valid_tcp_flow(hass, sync_complete=True, config_ok=True): flow = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - result = await hass.config_entries.flow.async_configure( + return await hass.config_entries.flow.async_configure( flow["flow_id"], {"protocol": "TCP", "address": "1.2.3.4", "file_path": "upb.upe"}, ) - return result async def test_full_upb_flow_with_serial_port(hass: HomeAssistant) -> None: diff --git a/tests/components/vesync/conftest.py b/tests/components/vesync/conftest.py index 23e0938cce6..5500ef1a55f 100644 --- a/tests/components/vesync/conftest.py +++ b/tests/components/vesync/conftest.py @@ -71,15 +71,13 @@ def manager_fixture() -> VeSync: @pytest.fixture(name="fan") def fan_fixture(): """Create a mock VeSync fan fixture.""" - mock_fixture = Mock(VeSyncAirBypass) - return mock_fixture + return Mock(VeSyncAirBypass) @pytest.fixture(name="bulb") def bulb_fixture(): """Create a mock VeSync bulb fixture.""" - mock_fixture = Mock(VeSyncBulb) - return mock_fixture + return Mock(VeSyncBulb) @pytest.fixture(name="switch") @@ -101,5 +99,4 @@ def dimmable_switch_fixture(): @pytest.fixture(name="outlet") def outlet_fixture(): """Create a mock VeSync outlet fixture.""" - mock_fixture = Mock(VeSyncOutlet) - return mock_fixture + return Mock(VeSyncOutlet) diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index 254cf13c556..7d3722b5037 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -422,12 +422,11 @@ def zha_device_mock( zigpy_device = zigpy_device_mock( endpoints, ieee, manufacturer, model, node_desc, patch_cluster=patch_cluster ) - zha_device = zha_core_device.ZHADevice( + return zha_core_device.ZHADevice( hass, zigpy_device, ZHAGateway(hass, {}, config_entry), ) - return zha_device return _zha_device diff --git a/tests/components/zha/test_cluster_handlers.py b/tests/components/zha/test_cluster_handlers.py index b8fbd071a6d..ca21b74e106 100644 --- a/tests/components/zha/test_cluster_handlers.py +++ b/tests/components/zha/test_cluster_handlers.py @@ -119,8 +119,7 @@ async def poll_control_device(zha_device_restored, zigpy_device_mock): "test model", ) - zha_device = await zha_device_restored(zigpy_dev) - return zha_device + return await zha_device_restored(zigpy_dev) @pytest.mark.parametrize( diff --git a/tests/components/zha/test_device.py b/tests/components/zha/test_device.py index 289442b3466..fefc68a8d94 100644 --- a/tests/components/zha/test_device.py +++ b/tests/components/zha/test_device.py @@ -115,8 +115,7 @@ async def ota_zha_device(zha_device_restored, zigpy_device_mock): "test model", ) - zha_device = await zha_device_restored(zigpy_dev) - return zha_device + return await zha_device_restored(zigpy_dev) def _send_time_changed(hass, seconds): diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py index d090ac8aba0..666594bd854 100644 --- a/tests/components/zha/test_gateway.py +++ b/tests/components/zha/test_gateway.py @@ -60,8 +60,7 @@ def required_platform_only(): async def zha_dev_basic(hass, zha_device_restored, zigpy_dev_basic): """ZHA device with just a basic cluster.""" - zha_device = await zha_device_restored(zigpy_dev_basic) - return zha_device + return await zha_device_restored(zigpy_dev_basic) @pytest.fixture diff --git a/tests/components/zha/test_number.py b/tests/components/zha/test_number.py index 6bb1703a229..b3fc42c35df 100644 --- a/tests/components/zha/test_number.py +++ b/tests/components/zha/test_number.py @@ -61,7 +61,7 @@ def zigpy_analog_output_device(zigpy_device_mock): async def light(zigpy_device_mock): """Siren fixture.""" - zigpy_device = zigpy_device_mock( + return zigpy_device_mock( { 1: { SIG_EP_PROFILE: zha.PROFILE_ID, @@ -79,8 +79,6 @@ async def light(zigpy_device_mock): node_descriptor=b"\x02@\x84_\x11\x7fd\x00\x00,d\x00\x00", ) - return zigpy_device - async def test_number( hass: HomeAssistant, zha_device_joined_restored, zigpy_analog_output_device diff --git a/tests/components/zha/test_select.py b/tests/components/zha/test_select.py index 97aed05dcd3..1d3811d0293 100644 --- a/tests/components/zha/test_select.py +++ b/tests/components/zha/test_select.py @@ -72,7 +72,7 @@ async def siren(hass, zigpy_device_mock, zha_device_joined_restored): async def light(hass, zigpy_device_mock): """Siren fixture.""" - zigpy_device = zigpy_device_mock( + return zigpy_device_mock( { 1: { SIG_EP_PROFILE: zha.PROFILE_ID, @@ -88,8 +88,6 @@ async def light(hass, zigpy_device_mock): node_descriptor=b"\x02@\x84_\x11\x7fd\x00\x00,d\x00\x00", ) - return zigpy_device - @pytest.fixture def core_rs(hass_storage): diff --git a/tests/components/zha/test_update.py b/tests/components/zha/test_update.py index 60cd5bf9ff9..32be013e673 100644 --- a/tests/components/zha/test_update.py +++ b/tests/components/zha/test_update.py @@ -229,7 +229,7 @@ def make_packet(zigpy_device, cluster, cmd_name: str, **kwargs): kwargs=kwargs, ) - ota_packet = t.ZigbeePacket( + return t.ZigbeePacket( src=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=zigpy_device.nwk), src_ep=1, dst=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=0x0000), @@ -242,8 +242,6 @@ def make_packet(zigpy_device, cluster, cmd_name: str, **kwargs): rssi=-30, ) - return ota_packet - @patch("zigpy.device.AFTER_OTA_ATTR_READ_DELAY", 0.01) async def test_firmware_update_success( diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 98453071bc1..dbf7357d4a0 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -966,8 +966,7 @@ def aeotec_radiator_thermostat_fixture(client, aeotec_radiator_thermostat_state) def nortek_thermostat_added_event_fixture(client): """Mock a Nortek thermostat node added event.""" event_data = json.loads(load_fixture("zwave_js/nortek_thermostat_added_event.json")) - event = Event("node added", event_data) - return event + return Event("node added", event_data) @pytest.fixture(name="nortek_thermostat_removed_event") @@ -976,8 +975,7 @@ def nortek_thermostat_removed_event_fixture(client): event_data = json.loads( load_fixture("zwave_js/nortek_thermostat_removed_event.json") ) - event = Event("node removed", event_data) - return event + return Event("node removed", event_data) @pytest.fixture(name="integration") diff --git a/tests/hassfest/test_requirements.py b/tests/hassfest/test_requirements.py index 7cb08621e83..f3b008a6113 100644 --- a/tests/hassfest/test_requirements.py +++ b/tests/hassfest/test_requirements.py @@ -11,7 +11,7 @@ from script.hassfest.requirements import validate_requirements_format @pytest.fixture def integration(): """Fixture for hassfest integration model.""" - integration = Integration( + return Integration( path=Path("homeassistant/components/test"), _manifest={ "domain": "test", @@ -21,7 +21,6 @@ def integration(): "requirements": [], }, ) - return integration def test_validate_requirements_format_with_space(integration: Integration) -> None: diff --git a/tests/helpers/test_update_coordinator.py b/tests/helpers/test_update_coordinator.py index 25f72d76e3c..be1bbf0580e 100644 --- a/tests/helpers/test_update_coordinator.py +++ b/tests/helpers/test_update_coordinator.py @@ -63,14 +63,13 @@ def get_crd( calls += 1 return calls - crd = update_coordinator.DataUpdateCoordinator[int]( + return update_coordinator.DataUpdateCoordinator[int]( hass, _LOGGER, name="test", update_method=refresh, update_interval=update_interval, ) - return crd DEFAULT_UPDATE_INTERVAL = timedelta(seconds=10) diff --git a/tests/util/test_ssl.py b/tests/util/test_ssl.py index 4a88e061cbc..d0c7ce3bfb6 100644 --- a/tests/util/test_ssl.py +++ b/tests/util/test_ssl.py @@ -15,8 +15,7 @@ from homeassistant.util.ssl import ( @pytest.fixture def mock_sslcontext(): """Mock the ssl lib.""" - ssl_mock = MagicMock(set_ciphers=Mock(return_value=True)) - return ssl_mock + return MagicMock(set_ciphers=Mock(return_value=True)) def test_client_context(mock_sslcontext) -> None: From e85b9faa00b77a4ce919b6a1bd1b7c6181493ba2 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Sat, 6 Apr 2024 10:49:30 +0100 Subject: [PATCH 320/967] Bump ring_doorbell to 0.8.10 (#114865) --- homeassistant/components/ring/button.py | 2 +- homeassistant/components/ring/camera.py | 4 ++-- homeassistant/components/ring/light.py | 4 ++-- homeassistant/components/ring/manifest.json | 2 +- homeassistant/components/ring/siren.py | 8 +++----- homeassistant/components/ring/switch.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 8 files changed, 12 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/ring/button.py b/homeassistant/components/ring/button.py index 343c0d68257..d739dc29841 100644 --- a/homeassistant/components/ring/button.py +++ b/homeassistant/components/ring/button.py @@ -52,6 +52,6 @@ class RingDoorButton(RingEntity, ButtonEntity): self._attr_unique_id = f"{device.id}-{description.key}" @exception_wrap - def press(self) -> None: + def press(self): """Open the door.""" self._device.open_door() diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index 7cbe3559ab2..ec0f4ca3fab 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -132,7 +132,7 @@ class RingCam(RingEntity, Camera): finally: await stream.close() - async def async_update(self) -> None: + async def async_update(self): """Update camera entity and refresh attributes.""" if ( self._device.has_capability(MOTION_DETECTION_CAPABILITY) @@ -160,7 +160,7 @@ class RingCam(RingEntity, Camera): self._expires_at = FORCE_REFRESH_INTERVAL + utcnow @exception_wrap - def _get_video(self) -> str: + def _get_video(self): return self._device.recording_url(self._last_event["id"]) @exception_wrap diff --git a/homeassistant/components/ring/light.py b/homeassistant/components/ring/light.py index 31e22c2084c..10d13e59810 100644 --- a/homeassistant/components/ring/light.py +++ b/homeassistant/components/ring/light.py @@ -4,7 +4,7 @@ from datetime import timedelta import logging from typing import Any -from ring_doorbell import RingGeneric, RingStickUpCam +from ring_doorbell import RingStickUpCam from homeassistant.components.light import ColorMode, LightEntity from homeassistant.config_entries import ConfigEntry @@ -55,7 +55,7 @@ class RingLight(RingEntity, LightEntity): _attr_supported_color_modes = {ColorMode.ONOFF} _attr_translation_key = "light" - def __init__(self, device: RingGeneric, coordinator) -> None: + def __init__(self, device, coordinator): """Initialize the light.""" super().__init__(device, coordinator) self._attr_unique_id = device.id diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index 05c6dcd5ab1..1abc9a99e63 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -14,5 +14,5 @@ "iot_class": "cloud_polling", "loggers": ["ring_doorbell"], "quality_scale": "silver", - "requirements": ["ring-doorbell[listen]==0.8.9"] + "requirements": ["ring-doorbell[listen]==0.8.10"] } diff --git a/homeassistant/components/ring/siren.py b/homeassistant/components/ring/siren.py index 4e53ab8a006..4b7d9243dbf 100644 --- a/homeassistant/components/ring/siren.py +++ b/homeassistant/components/ring/siren.py @@ -1,9 +1,7 @@ """Component providing HA Siren support for Ring Chimes.""" import logging -from typing import Any -from ring_doorbell import RingGeneric from ring_doorbell.const import CHIME_TEST_SOUND_KINDS, KIND_DING from homeassistant.components.siren import ATTR_TONE, SirenEntity, SirenEntityFeature @@ -37,18 +35,18 @@ async def async_setup_entry( class RingChimeSiren(RingEntity, SirenEntity): """Creates a siren to play the test chimes of a Chime device.""" - _attr_available_tones = CHIME_TEST_SOUND_KINDS + _attr_available_tones = list(CHIME_TEST_SOUND_KINDS) _attr_supported_features = SirenEntityFeature.TURN_ON | SirenEntityFeature.TONES _attr_translation_key = "siren" - def __init__(self, device: RingGeneric, coordinator: RingDataCoordinator) -> None: + def __init__(self, device, coordinator: RingDataCoordinator) -> None: """Initialize a Ring Chime siren.""" super().__init__(device, coordinator) # Entity class attributes self._attr_unique_id = f"{self._device.id}-siren" @exception_wrap - def turn_on(self, **kwargs: Any) -> None: + def turn_on(self, **kwargs): """Play the test sound on a Ring Chime device.""" tone = kwargs.get(ATTR_TONE) or KIND_DING diff --git a/homeassistant/components/ring/switch.py b/homeassistant/components/ring/switch.py index 15aa0a787bb..2221eeb7705 100644 --- a/homeassistant/components/ring/switch.py +++ b/homeassistant/components/ring/switch.py @@ -62,7 +62,7 @@ class SirenSwitch(BaseRingSwitch): _attr_translation_key = "siren" - def __init__(self, device: RingGeneric, coordinator: RingDataCoordinator) -> None: + def __init__(self, device, coordinator: RingDataCoordinator) -> None: """Initialize the switch for a device with a siren.""" super().__init__(device, coordinator, "siren") self._no_updates_until = dt_util.utcnow() diff --git a/requirements_all.txt b/requirements_all.txt index e248dcc6ca1..17988445501 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2451,7 +2451,7 @@ rfk101py==0.0.1 rflink==0.0.66 # homeassistant.components.ring -ring-doorbell[listen]==0.8.9 +ring-doorbell[listen]==0.8.10 # homeassistant.components.fleetgo ritassist==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5ecac38fb93..a834f5849cd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1894,7 +1894,7 @@ reolink-aio==0.8.9 rflink==0.0.66 # homeassistant.components.ring -ring-doorbell[listen]==0.8.9 +ring-doorbell[listen]==0.8.10 # homeassistant.components.roku rokuecp==0.19.2 From 7d5b39b9de2213ddbe023dbbe62fc985e46d8cc1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 5 Apr 2024 23:50:29 -1000 Subject: [PATCH 321/967] Fix dictionary changed size during iteration in prometheus (#115005) Fixes #104803 --- homeassistant/components/prometheus/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index 09c65c35f5f..c02cbeabd84 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -257,7 +257,7 @@ class PrometheusMetrics: self, entity_id: str, friendly_name: str | None = None ) -> None: """Remove labelsets matching the given entity id from all metrics.""" - for metric in self._metrics.values(): + for metric in list(self._metrics.values()): for sample in cast(list[prometheus_client.Metric], metric.collect())[ 0 ].samples: From 34bf4dc9625889a258e4accf364557352c6252f0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 5 Apr 2024 23:57:30 -1000 Subject: [PATCH 322/967] Migrate generic_hygrostat to use async_track_state_change_event (#115001) --- .../generic_hygrostat/humidifier.py | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/generic_hygrostat/humidifier.py b/homeassistant/components/generic_hygrostat/humidifier.py index 02641acccae..32ad34773bd 100644 --- a/homeassistant/components/generic_hygrostat/humidifier.py +++ b/homeassistant/components/generic_hygrostat/humidifier.py @@ -34,6 +34,7 @@ from homeassistant.const import ( from homeassistant.core import ( DOMAIN as HA_DOMAIN, Event, + EventStateChangedData, HomeAssistant, State, callback, @@ -41,7 +42,7 @@ from homeassistant.core import ( from homeassistant.helpers import condition from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( - async_track_state_change, + async_track_state_change_event, async_track_time_interval, ) from homeassistant.helpers.restore_state import RestoreEntity @@ -179,11 +180,15 @@ class GenericHygrostat(HumidifierEntity, RestoreEntity): await super().async_added_to_hass() # Add listener - async_track_state_change( - self.hass, self._sensor_entity_id, self._async_sensor_changed + self.async_on_remove( + async_track_state_change_event( + self.hass, self._sensor_entity_id, self._async_sensor_changed_event + ) ) - async_track_state_change( - self.hass, self._switch_entity_id, self._async_switch_changed + self.async_on_remove( + async_track_state_change_event( + self.hass, self._switch_entity_id, self._async_switch_changed_event + ) ) if self._keep_alive: @@ -343,6 +348,15 @@ class GenericHygrostat(HumidifierEntity, RestoreEntity): # Get default humidity from super class return super().max_humidity + async def _async_sensor_changed_event( + self, event: Event[EventStateChangedData] + ) -> None: + """Handle ambient humidity changes.""" + data = event.data + await self._async_sensor_changed( + data["entity_id"], data["old_state"], data["new_state"] + ) + async def _async_sensor_changed( self, entity_id: str, old_state: State | None, new_state: State | None ) -> None: @@ -374,6 +388,14 @@ class GenericHygrostat(HumidifierEntity, RestoreEntity): _LOGGER.warning("Sensor is stalled, call the emergency stop") await self._async_update_humidity("Stalled") + @callback + def _async_switch_changed_event(self, event: Event[EventStateChangedData]) -> None: + """Handle humidifier switch state changes.""" + data = event.data + self._async_switch_changed( + data["entity_id"], data["old_state"], data["new_state"] + ) + @callback def _async_switch_changed( self, entity_id: str, old_state: State | None, new_state: State | None From b6d0c9d1c35573aea7907ec2faede26fe97879a0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 6 Apr 2024 00:00:30 -1000 Subject: [PATCH 323/967] Migrate proximity to use async_track_state_change_event (#115002) --- homeassistant/components/proximity/__init__.py | 4 ++-- .../components/proximity/coordinator.py | 16 +++++++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/proximity/__init__.py b/homeassistant/components/proximity/__init__.py index f6c67fc088f..d739efe39e7 100644 --- a/homeassistant/components/proximity/__init__.py +++ b/homeassistant/components/proximity/__init__.py @@ -20,7 +20,7 @@ from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import ( async_track_entity_registry_updated_event, - async_track_state_change, + async_track_state_change_event, ) from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType @@ -142,7 +142,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = ProximityDataUpdateCoordinator(hass, entry.title, dict(entry.data)) entry.async_on_unload( - async_track_state_change( + async_track_state_change_event( hass, entry.data[CONF_TRACKED_ENTITIES], coordinator.async_check_proximity_state_change, diff --git a/homeassistant/components/proximity/coordinator.py b/homeassistant/components/proximity/coordinator.py index ea33c1f8121..ff7eedb5cd0 100644 --- a/homeassistant/components/proximity/coordinator.py +++ b/homeassistant/components/proximity/coordinator.py @@ -15,7 +15,13 @@ from homeassistant.const import ( CONF_ZONE, UnitOfLength, ) -from homeassistant.core import Event, HomeAssistant, State, callback +from homeassistant.core import ( + Event, + EventStateChangedData, + HomeAssistant, + State, + callback, +) from homeassistant.helpers import entity_registry as er from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType @@ -100,10 +106,14 @@ class ProximityDataUpdateCoordinator(DataUpdateCoordinator[ProximityData]): self.entity_mapping[tracked_entity_id].append(entity_id) async def async_check_proximity_state_change( - self, entity: str, old_state: State | None, new_state: State | None + self, + event: Event[EventStateChangedData], ) -> None: """Fetch and process state change event.""" - self.state_change_data = StateChangedData(entity, old_state, new_state) + data = event.data + self.state_change_data = StateChangedData( + data["entity_id"], data["old_state"], data["new_state"] + ) await self.async_refresh() async def async_check_tracked_entity_change( From d9573bb7dc8f72c58e885007222ad4a20081452c Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 6 Apr 2024 12:06:16 +0200 Subject: [PATCH 324/967] Move Color extractor service to async_setup (#115013) --- .../components/color_extractor/__init__.py | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/color_extractor/__init__.py b/homeassistant/components/color_extractor/__init__.py index 56270ce0f75..be9b80e8f52 100644 --- a/homeassistant/components/color_extractor/__init__.py +++ b/homeassistant/components/color_extractor/__init__.py @@ -66,19 +66,6 @@ def _get_color(file_handler) -> tuple: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Color extractor component.""" - if DOMAIN in config: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data={} - ) - ) - - return True - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Load a config entry.""" - async def async_handle_service(service_call: ServiceCall) -> None: """Decide which color_extractor method to call based on service.""" service_data = dict(service_call.data) @@ -169,4 +156,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _file = _get_file(file_path) return _get_color(_file) + if DOMAIN in config: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data={} + ) + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Load a config entry.""" return True From 6594d022ba3f499a5778c8062f3ef71dfb33e8d7 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Sat, 6 Apr 2024 03:16:00 -0700 Subject: [PATCH 325/967] Bump pylitterbot to 2023.4.11 (#114918) --- .../components/litterrobot/manifest.json | 2 +- .../components/litterrobot/vacuum.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/litterrobot/common.py | 3 ++- tests/components/litterrobot/test_sensor.py | 2 +- tests/components/litterrobot/test_vacuum.py | 26 +++++++++++++++++++ 7 files changed, 33 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index ea096a908fc..66ade5f356c 100644 --- a/homeassistant/components/litterrobot/manifest.json +++ b/homeassistant/components/litterrobot/manifest.json @@ -12,5 +12,5 @@ "integration_type": "hub", "iot_class": "cloud_push", "loggers": ["pylitterbot"], - "requirements": ["pylitterbot==2023.4.9"] + "requirements": ["pylitterbot==2023.4.11"] } diff --git a/homeassistant/components/litterrobot/vacuum.py b/homeassistant/components/litterrobot/vacuum.py index 4f9efa2dff7..d752609d7de 100644 --- a/homeassistant/components/litterrobot/vacuum.py +++ b/homeassistant/components/litterrobot/vacuum.py @@ -35,6 +35,7 @@ LITTER_BOX_STATUS_STATE_MAP = { LitterBoxStatus.CLEAN_CYCLE: STATE_CLEANING, LitterBoxStatus.EMPTY_CYCLE: STATE_CLEANING, LitterBoxStatus.CLEAN_CYCLE_COMPLETE: STATE_DOCKED, + LitterBoxStatus.CAT_DETECTED: STATE_DOCKED, LitterBoxStatus.CAT_SENSOR_TIMING: STATE_DOCKED, LitterBoxStatus.DRAWER_FULL_1: STATE_DOCKED, LitterBoxStatus.DRAWER_FULL_2: STATE_DOCKED, diff --git a/requirements_all.txt b/requirements_all.txt index 17988445501..1cc56c06807 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1950,7 +1950,7 @@ pylibrespot-java==0.1.1 pylitejet==0.6.2 # homeassistant.components.litterrobot -pylitterbot==2023.4.9 +pylitterbot==2023.4.11 # homeassistant.components.lutron_caseta pylutron-caseta==0.20.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a834f5849cd..28925e18d88 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1516,7 +1516,7 @@ pylibrespot-java==0.1.1 pylitejet==0.6.2 # homeassistant.components.litterrobot -pylitterbot==2023.4.9 +pylitterbot==2023.4.11 # homeassistant.components.lutron_caseta pylutron-caseta==0.20.0 diff --git a/tests/components/litterrobot/common.py b/tests/components/litterrobot/common.py index fe6202edc47..cac81aad4ef 100644 --- a/tests/components/litterrobot/common.py +++ b/tests/components/litterrobot/common.py @@ -33,6 +33,7 @@ ROBOT_4_DATA = { "wifiRssi": -53.0, "unitPowerType": "AC", "catWeight": 12.0, + "displayCode": "DC_MODE_IDLE", "unitTimezone": "America/New_York", "unitTime": None, "cleanCycleWaitTime": 15, @@ -66,7 +67,7 @@ ROBOT_4_DATA = { "isDFIResetPending": False, "DFINumberOfCycles": 104, "DFILevelPercent": 76, - "isDFIFull": True, + "isDFIFull": False, "DFIFullCounter": 3, "DFITriggerCount": 42, "litterLevel": 460, diff --git a/tests/components/litterrobot/test_sensor.py b/tests/components/litterrobot/test_sensor.py index 9002894d0ab..8d1f2b68e05 100644 --- a/tests/components/litterrobot/test_sensor.py +++ b/tests/components/litterrobot/test_sensor.py @@ -86,7 +86,7 @@ async def test_litter_robot_sensor( assert sensor.state == "2022-09-17T12:06:37+00:00" assert sensor.attributes["device_class"] == SensorDeviceClass.TIMESTAMP sensor = hass.states.get("sensor.test_status_code") - assert sensor.state == "dfs" + assert sensor.state == "rdy" assert sensor.attributes["device_class"] == SensorDeviceClass.ENUM sensor = hass.states.get("sensor.test_litter_level") assert sensor.state == "70.0" diff --git a/tests/components/litterrobot/test_vacuum.py b/tests/components/litterrobot/test_vacuum.py index 9013d6e83eb..68ebae1e239 100644 --- a/tests/components/litterrobot/test_vacuum.py +++ b/tests/components/litterrobot/test_vacuum.py @@ -5,6 +5,7 @@ from __future__ import annotations from typing import Any from unittest.mock import MagicMock +from pylitterbot import Robot import pytest from homeassistant.components.litterrobot import DOMAIN @@ -16,6 +17,7 @@ from homeassistant.components.vacuum import ( SERVICE_STOP, STATE_DOCKED, STATE_ERROR, + STATE_PAUSED, ) from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant @@ -96,6 +98,30 @@ async def test_vacuum_with_error( assert vacuum.state == STATE_ERROR +@pytest.mark.parametrize( + ("robot_data", "expected_state"), + [ + ({"displayCode": "DC_CAT_DETECT"}, STATE_DOCKED), + ({"isDFIFull": True}, STATE_ERROR), + ({"robotCycleState": "CYCLE_STATE_CAT_DETECT"}, STATE_PAUSED), + ], +) +async def test_vacuum_states( + hass: HomeAssistant, + mock_account_with_litterrobot_4: MagicMock, + robot_data: dict[str, str | bool], + expected_state: str, +) -> None: + """Test sending commands to the switch.""" + await setup_integration(hass, mock_account_with_litterrobot_4, PLATFORM_DOMAIN) + robot: Robot = mock_account_with_litterrobot_4.robots[0] + robot._update_data(robot_data, partial=True) + + vacuum = hass.states.get(VACUUM_ENTITY_ID) + assert vacuum + assert vacuum.state == expected_state + + @pytest.mark.parametrize( ("service", "command", "extra"), [ From 0ccd3346088a7cdf7fb9a1f7b98fca79659a4f12 Mon Sep 17 00:00:00 2001 From: wittypluck Date: Sat, 6 Apr 2024 12:20:00 +0200 Subject: [PATCH 326/967] Update glances-api to 0.6.0 (#114929) --- 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 d022995b786..2fb5cf16996 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.5.0"] + "requirements": ["glances-api==0.6.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1cc56c06807..e3d6640dc78 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -949,7 +949,7 @@ gios==3.2.2 gitterpy==0.1.7 # homeassistant.components.glances -glances-api==0.5.0 +glances-api==0.6.0 # homeassistant.components.goalzero goalzero==0.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 28925e18d88..757eec2b642 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -775,7 +775,7 @@ getmac==0.9.4 gios==3.2.2 # homeassistant.components.glances -glances-api==0.5.0 +glances-api==0.6.0 # homeassistant.components.goalzero goalzero==0.2.2 From bd9070be11e8d9f46efd5c49e24dc9b7d530c27b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Sat, 6 Apr 2024 12:24:00 +0200 Subject: [PATCH 327/967] Update aioairzone-cloud to v0.5.0 (#114928) --- .../components/airzone_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../snapshots/test_diagnostics.ambr | 31 ++++++++++++++++++- tests/components/airzone_cloud/util.py | 31 ++++++++++++++++++- 5 files changed, 63 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/airzone_cloud/manifest.json b/homeassistant/components/airzone_cloud/manifest.json index b4445f6fe45..2b7615c01f4 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.4.7"] + "requirements": ["aioairzone-cloud==0.5.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index e3d6640dc78..8e78b920b28 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -185,7 +185,7 @@ aio-georss-gdacs==0.9 aioairq==0.3.2 # homeassistant.components.airzone_cloud -aioairzone-cloud==0.4.7 +aioairzone-cloud==0.5.0 # homeassistant.components.airzone aioairzone==0.7.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 757eec2b642..62fe74f5600 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -164,7 +164,7 @@ aio-georss-gdacs==0.9 aioairq==0.3.2 # homeassistant.components.airzone_cloud -aioairzone-cloud==0.4.7 +aioairzone-cloud==0.5.0 # homeassistant.components.airzone aioairzone==0.7.6 diff --git a/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr b/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr index d1a8d74cc08..0edd17d513a 100644 --- a/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr +++ b/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr @@ -109,6 +109,7 @@ 'action': 6, 'active': False, 'available': True, + 'double-set-point': False, 'id': 'aidoo1', 'installation': 'installation1', 'is-connected': True, @@ -150,6 +151,7 @@ 'action': 1, 'active': True, 'available': True, + 'double-set-point': True, 'id': 'aidoo_pro', 'installation': 'installation1', 'is-connected': True, @@ -177,7 +179,7 @@ 'temperature': 20.0, 'temperature-setpoint': 22.0, 'temperature-setpoint-cool-air': 22.0, - 'temperature-setpoint-hot-air': 22.0, + 'temperature-setpoint-hot-air': 18.0, 'temperature-setpoint-max': 30.0, 'temperature-setpoint-max-auto-air': 30.0, 'temperature-setpoint-max-cool-air': 30.0, @@ -196,6 +198,9 @@ 'action': 1, 'active': True, 'available': True, + 'hot-water': list([ + 'dhw1', + ]), 'humidity': 27, 'id': 'group1', 'installation': 'installation1', @@ -275,6 +280,25 @@ 'temperature-step': 0.5, }), }), + 'hot-water': dict({ + 'dhw1': dict({ + 'active': False, + 'available': True, + 'id': 'dhw1', + 'installation': 'installation1', + 'is-connected': True, + 'name': 'DHW', + 'power': False, + 'problems': False, + 'temperature': 45.5, + 'temperature-setpoint': 48, + 'temperature-setpoint-max': 60, + 'temperature-setpoint-min': 40, + 'temperature-step': 1, + 'web-server': 'webserver1', + 'ws-connected': True, + }), + }), 'installations': dict({ 'installation1': dict({ 'action': 1, @@ -289,6 +313,9 @@ 'group2', 'group3', ]), + 'hot-water': list([ + 'dhw1', + ]), 'humidity': 27, 'id': 'installation1', 'mode': 2, @@ -418,6 +445,7 @@ 'aq-present': True, 'aq-status': 'good', 'available': True, + 'double-set-point': False, 'humidity': 30, 'id': 'zone1', 'installation': 'installation1', @@ -478,6 +506,7 @@ 'aq-present': True, 'aq-status': 'good', 'available': True, + 'double-set-point': False, 'humidity': 24, 'id': 'zone2', 'installation': 'installation1', diff --git a/tests/components/airzone_cloud/util.py b/tests/components/airzone_cloud/util.py index ea0dbf9f736..02c3e18eed2 100644 --- a/tests/components/airzone_cloud/util.py +++ b/tests/components/airzone_cloud/util.py @@ -14,6 +14,7 @@ from aioairzone_cloud.const import ( API_AQ_PM_10, API_AQ_PRESENT, API_AQ_QUALITY, + API_AZ_ACS, API_AZ_AIDOO, API_AZ_AIDOO_PRO, API_AZ_SYSTEM, @@ -24,6 +25,7 @@ from aioairzone_cloud.const import ( API_DEVICE_ID, API_DEVICES, API_DISCONNECTION_DATE, + API_DOUBLE_SET_POINT, API_ERRORS, API_FAH, API_GROUP_ID, @@ -41,6 +43,7 @@ from aioairzone_cloud.const import ( API_POWER, API_RANGE_MAX_AIR, API_RANGE_MIN_AIR, + API_RANGE_SP_MAX_ACS, API_RANGE_SP_MAX_AUTO_AIR, API_RANGE_SP_MAX_COOL_AIR, API_RANGE_SP_MAX_DRY_AIR, @@ -48,6 +51,7 @@ from aioairzone_cloud.const import ( API_RANGE_SP_MAX_HOT_AIR, API_RANGE_SP_MAX_STOP_AIR, API_RANGE_SP_MAX_VENT_AIR, + API_RANGE_SP_MIN_ACS, API_RANGE_SP_MIN_AUTO_AIR, API_RANGE_SP_MIN_COOL_AIR, API_RANGE_SP_MIN_DRY_AIR, @@ -55,6 +59,7 @@ from aioairzone_cloud.const import ( API_RANGE_SP_MIN_HOT_AIR, API_RANGE_SP_MIN_STOP_AIR, API_RANGE_SP_MIN_VENT_AIR, + API_SETPOINT, API_SP_AIR_AUTO, API_SP_AIR_COOL, API_SP_AIR_DRY, @@ -70,7 +75,9 @@ from aioairzone_cloud.const import ( API_STAT_RSSI, API_STAT_SSID, API_STATUS, + API_STEP, API_SYSTEM_NUMBER, + API_TANK_TEMP, API_TYPE, API_WARNINGS, API_WS_CONNECTED, @@ -105,6 +112,11 @@ GET_INSTALLATION_MOCK = { API_GROUP_ID: "grp1", API_NAME: "Group", API_DEVICES: [ + { + API_DEVICE_ID: "dhw1", + API_TYPE: API_AZ_ACS, + API_WS_ID: WS_ID, + }, { API_DEVICE_ID: "system1", API_TYPE: API_AZ_SYSTEM, @@ -268,6 +280,7 @@ def mock_get_device_status(device: Device) -> dict[str, Any]: if device.get_id() == "aidoo_pro": return { API_ACTIVE: True, + API_DOUBLE_SET_POINT: True, API_ERRORS: [], API_MODE: OperationMode.COOLING.value, API_MODE_AVAIL: [ @@ -279,7 +292,7 @@ def mock_get_device_status(device: Device) -> dict[str, Any]: ], API_SP_AIR_AUTO: {API_CELSIUS: 22, API_FAH: 72}, API_SP_AIR_COOL: {API_CELSIUS: 22, API_FAH: 72}, - API_SP_AIR_HEAT: {API_CELSIUS: 22, API_FAH: 72}, + API_SP_AIR_HEAT: {API_CELSIUS: 18, API_FAH: 64}, API_RANGE_MAX_AIR: {API_CELSIUS: 30, API_FAH: 86}, API_RANGE_SP_MAX_AUTO_AIR: {API_CELSIUS: 30, API_FAH: 86}, API_RANGE_SP_MAX_COOL_AIR: {API_CELSIUS: 30, API_FAH: 86}, @@ -297,6 +310,20 @@ def mock_get_device_status(device: Device) -> dict[str, Any]: API_LOCAL_TEMP: {API_CELSIUS: 20, API_FAH: 68}, API_WARNINGS: [], } + if device.get_id() == "dhw1": + return { + API_ACTIVE: False, + API_ERRORS: [], + API_POWER: False, + API_SETPOINT: {API_CELSIUS: 48, API_FAH: 118}, + API_RANGE_SP_MAX_ACS: {API_CELSIUS: 60, API_FAH: 140}, + API_RANGE_SP_MIN_ACS: {API_CELSIUS: 40, API_FAH: 104}, + API_STEP: {API_CELSIUS: 1, API_FAH: 1}, + API_TANK_TEMP: {API_CELSIUS: 45.5, API_FAH: 114}, + API_IS_CONNECTED: True, + API_WS_CONNECTED: True, + API_WARNINGS: [], + } if device.get_id() == "system1": return { API_AQ_MODE_VALUES: ["off", "on", "auto"], @@ -332,6 +359,7 @@ def mock_get_device_status(device: Device) -> dict[str, Any]: API_AQ_PM_10: 3, API_AQ_PRESENT: True, API_AQ_QUALITY: "good", + API_DOUBLE_SET_POINT: False, API_HUMIDITY: 30, API_MODE: OperationMode.COOLING.value, API_MODE_AVAIL: [ @@ -376,6 +404,7 @@ def mock_get_device_status(device: Device) -> dict[str, Any]: API_AQ_PM_10: 3, API_AQ_PRESENT: True, API_AQ_QUALITY: "good", + API_DOUBLE_SET_POINT: False, API_HUMIDITY: 24, API_MODE: OperationMode.COOLING.value, API_MODE_AVAIL: [], From fdef3ece134ec3fcfe4c0bcf61d91af4f0dd160e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Matheson=20Wergeland?= Date: Sat, 6 Apr 2024 13:01:56 +0200 Subject: [PATCH 328/967] Fix placeholder quotes (#114974) * When quoting placeholders, always use double quotes so Lokalise recognizes the placeholder. * Ensure that strings does not contain placeholders in single quotes. * Avoid redefining value * Moved string_with_no_placeholders_in_single_quotes * Define regex once * Fix tests --- homeassistant/components/blink/strings.json | 2 +- homeassistant/components/ecovacs/strings.json | 2 +- .../components/homeworks/strings.json | 2 +- homeassistant/components/imap/strings.json | 8 +++---- homeassistant/components/mailbox/strings.json | 2 +- homeassistant/components/mqtt/strings.json | 4 ++-- homeassistant/components/reolink/strings.json | 2 +- .../components/rest_command/strings.json | 6 +++--- script/hassfest/translations.py | 12 +++++++++++ tests/components/rest_command/test_init.py | 8 +++---- tests/hassfest/test_translations.py | 21 +++++++++++++++++++ 11 files changed, 51 insertions(+), 18 deletions(-) create mode 100644 tests/hassfest/test_translations.py diff --git a/homeassistant/components/blink/strings.json b/homeassistant/components/blink/strings.json index 09bbba4c226..2260acede1c 100644 --- a/homeassistant/components/blink/strings.json +++ b/homeassistant/components/blink/strings.json @@ -106,7 +106,7 @@ }, "exceptions": { "integration_not_found": { - "message": "Integration '{target}' not found in registry" + "message": "Integration \"{target}\" not found in registry" }, "no_path": { "message": "Can't write to directory {target}, no access to path!" diff --git a/homeassistant/components/ecovacs/strings.json b/homeassistant/components/ecovacs/strings.json index a21f57a7a24..50afd21deb3 100644 --- a/homeassistant/components/ecovacs/strings.json +++ b/homeassistant/components/ecovacs/strings.json @@ -227,7 +227,7 @@ }, "deprecated_yaml_import_issue_continent_not_match": { "title": "The Ecovacs YAML configuration import failed", - "description": "Configuring Ecovacs using YAML is being removed but there is an unexpected continent specified in the YAML configuration.\n\nFrom the given country, the continent '{continent}' is expected. Change the continent and restart Home Assistant to try again or remove the Ecovacs YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually.\n\nIf the contintent '{continent}' is not applicable, please open an issue on [GitHub]({github_issue_url})." + "description": "Configuring Ecovacs using YAML is being removed but there is an unexpected continent specified in the YAML configuration.\n\nFrom the given country, the continent \"{continent}\" is expected. Change the continent and restart Home Assistant to try again or remove the Ecovacs YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually.\n\nIf the contintent \"{continent}\" is not applicable, please open an issue on [GitHub]({github_issue_url})." } }, "selector": { diff --git a/homeassistant/components/homeworks/strings.json b/homeassistant/components/homeworks/strings.json index 46c58515f39..b0d0f6e61e1 100644 --- a/homeassistant/components/homeworks/strings.json +++ b/homeassistant/components/homeworks/strings.json @@ -41,7 +41,7 @@ }, "exceptions": { "invalid_controller_id": { - "message": "Invalid controller_id '{controller_id}', expected one of '{controller_ids}'" + "message": "Invalid controller_id \"{controller_id}\", expected one of \"{controller_ids}\"" } }, "options": { diff --git a/homeassistant/components/imap/strings.json b/homeassistant/components/imap/strings.json index 8c06889361c..378a1172788 100644 --- a/homeassistant/components/imap/strings.json +++ b/homeassistant/components/imap/strings.json @@ -37,13 +37,13 @@ }, "exceptions": { "copy_failed": { - "message": "Copying the message failed with '{error}'." + "message": "Copying the message failed with \"{error}\"." }, "delete_failed": { - "message": "Marking the the message for deletion failed with '{error}'." + "message": "Marking the the message for deletion failed with \"{error}\"." }, "expunge_failed": { - "message": "Expungling the the message failed with '{error}'." + "message": "Expungling the the message failed with \"{error}\"." }, "invalid_entry": { "message": "No valid IMAP entry was found." @@ -58,7 +58,7 @@ "message": "The IMAP server failed to connect: {error}." }, "seen_failed": { - "message": "Marking message as seen failed with '{error}'." + "message": "Marking message as seen failed with \"{error}\"." } }, "options": { diff --git a/homeassistant/components/mailbox/strings.json b/homeassistant/components/mailbox/strings.json index 44f1ad08d39..01746e3e98d 100644 --- a/homeassistant/components/mailbox/strings.json +++ b/homeassistant/components/mailbox/strings.json @@ -3,7 +3,7 @@ "issues": { "deprecated_mailbox": { "title": "The mailbox platform is being removed", - "description": "The mailbox platform is being removed. Please report it to the author of the '{integration_domain}' custom integration." + "description": "The mailbox platform is being removed. Please report it to the author of the \"{integration_domain}\" custom integration." } } } diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 87fe0bd033a..2bd47db63bc 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -264,10 +264,10 @@ "message": "Unable to publish: topic template `{topic_template}` produced an invalid topic `{topic}` after rendering ({error})" }, "mqtt_not_setup_cannot_subscribe": { - "message": "Cannot subscribe to topic '{topic}', make sure MQTT is set up correctly." + "message": "Cannot subscribe to topic \"{topic}\", make sure MQTT is set up correctly." }, "mqtt_not_setup_cannot_publish": { - "message": "Cannot publish to topic '{topic}', make sure MQTT is set up correctly." + "message": "Cannot publish to topic \"{topic}\", make sure MQTT is set up correctly." } } } diff --git a/homeassistant/components/reolink/strings.json b/homeassistant/components/reolink/strings.json index 2282289bdbc..ec81893d846 100644 --- a/homeassistant/components/reolink/strings.json +++ b/homeassistant/components/reolink/strings.json @@ -28,7 +28,7 @@ "api_error": "API error occurred", "cannot_connect": "Failed to connect, check the IP address of the camera", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "not_admin": "User needs to be admin, user ''{username}'' has authorisation level ''{userlevel}''", + "not_admin": "User needs to be admin, user \"{username}\" has authorisation level \"{userlevel}\"", "unknown": "[%key:common::config_flow::error::unknown%]", "webhook_exception": "Home Assistant URL is not available, go to Settings > System > Network > Home Assistant URL and correct the URLs, see {more_info}" }, diff --git a/homeassistant/components/rest_command/strings.json b/homeassistant/components/rest_command/strings.json index 2d658ad8b20..fd0b26f6499 100644 --- a/homeassistant/components/rest_command/strings.json +++ b/homeassistant/components/rest_command/strings.json @@ -7,13 +7,13 @@ }, "exceptions": { "timeout": { - "message": "Timeout when calling resource '{request_url}'" + "message": "Timeout when calling resource \"{request_url}\"" }, "client_error": { - "message": "Client error occurred when calling resource '{request_url}'" + "message": "Client error occurred when calling resource \"{request_url}\"" }, "decoding_error": { - "message": "The response of '{request_url}' could not be decoded as {decoding_type}" + "message": "The response of \"{request_url}\" could not be decoded as {decoding_type}" } } } diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index 6c20246b396..b893902af69 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -24,6 +24,7 @@ REMOVED = 2 RE_REFERENCE = r"\[\%key:(.+)\%\]" RE_TRANSLATION_KEY = re.compile(r"^(?!.+[_-]{2})(?![_-])[a-z0-9-_]+(? str: """Validate that the value is a valid translation. - prevents string with HTML + - prevents strings with single quoted placeholders - prevents combined translations """ value = cv.string_with_no_html(value) + value = string_no_single_quoted_placeholders(value) if RE_COMBINED_REFERENCE.search(value): raise vol.Invalid("the string should not contain combined translations") return str(value) +def string_no_single_quoted_placeholders(value: str) -> str: + """Validate that the value does not contain placeholders inside single quotes.""" + if RE_PLACEHOLDER_IN_SINGLE_QUOTES.search(value): + raise vol.Invalid( + "the string should not contain placeholders inside single quotes" + ) + return value + + def gen_data_entry_schema( *, config: Config, diff --git a/tests/components/rest_command/test_init.py b/tests/components/rest_command/test_init.py index 567391a4b32..4f88e1b9d34 100644 --- a/tests/components/rest_command/test_init.py +++ b/tests/components/rest_command/test_init.py @@ -68,7 +68,7 @@ async def test_rest_command_timeout( with pytest.raises(HomeAssistantError) as exc: await hass.services.async_call(DOMAIN, "get_test", {}, blocking=True) - assert str(exc.value) == "Timeout when calling resource 'https://example.com/'" + assert str(exc.value) == 'Timeout when calling resource "https://example.com/"' assert len(aioclient_mock.mock_calls) == 1 @@ -88,7 +88,7 @@ async def test_rest_command_aiohttp_error( assert ( str(exc.value) - == "Client error occurred when calling resource 'https://example.com/'" + == 'Client error occurred when calling resource "https://example.com/"' ) assert len(aioclient_mock.mock_calls) == 1 @@ -341,7 +341,7 @@ async def test_rest_command_get_response_malformed_json( ) assert ( str(exc.value) - == "The response of 'https://example.com/' could not be decoded as JSON" + == 'The response of "https://example.com/" could not be decoded as JSON' ) @@ -375,7 +375,7 @@ async def test_rest_command_get_response_none( ) assert ( str(exc.value) - == "The response of 'https://example.com/' could not be decoded as text" + == 'The response of "https://example.com/" could not be decoded as text' ) assert not response diff --git a/tests/hassfest/test_translations.py b/tests/hassfest/test_translations.py new file mode 100644 index 00000000000..526320a5044 --- /dev/null +++ b/tests/hassfest/test_translations.py @@ -0,0 +1,21 @@ +"""Tests for hassfest translations.""" + +import pytest +import voluptuous as vol + +from script.hassfest import translations + + +def test_string_with_no_placeholders_in_single_quotes() -> None: + """Test string with no placeholders in single quotes.""" + schema = vol.Schema(translations.string_no_single_quoted_placeholders) + + with pytest.raises(vol.Invalid): + schema("This has '{placeholder}' in single quotes") + + for value in ( + 'This has "{placeholder}" in double quotes', + "Simple {placeholder}", + "No placeholder", + ): + schema(value) From 082af6e0aee6103719ea06535ed118209246827e Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 6 Apr 2024 13:57:09 +0200 Subject: [PATCH 329/967] Improve generic event typing [voip] (#114738) --- homeassistant/components/voip/devices.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/voip/devices.py b/homeassistant/components/voip/devices.py index 9acc04f6879..4e2dca15308 100644 --- a/homeassistant/components/voip/devices.py +++ b/homeassistant/components/voip/devices.py @@ -84,7 +84,7 @@ class VoIPDevices: ) @callback - def async_device_removed(ev: Event) -> None: + def async_device_removed(ev: Event[dr.EventDeviceRegistryUpdatedData]) -> None: """Handle device removed.""" removed_id = ev.data["device_id"] self.devices = { @@ -97,7 +97,7 @@ class VoIPDevices: self.hass.bus.async_listen( dr.EVENT_DEVICE_REGISTRY_UPDATED, async_device_removed, - callback(lambda event_data: event_data.get("action") == "remove"), + callback(lambda event_data: event_data["action"] == "remove"), ) ) From d24cf9e4ec1c26646f6a0421874fb8b80aef4e0f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 6 Apr 2024 13:59:12 +0200 Subject: [PATCH 330/967] Improve generic event typing [cloud] (#114728) --- homeassistant/components/cloud/alexa_config.py | 15 ++++++++------- homeassistant/components/cloud/google_config.py | 8 ++++++-- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index 467d1589398..5b77a02384d 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -517,7 +517,9 @@ class CloudAlexaConfig(alexa_config.AbstractConfig): return False return True - async def _handle_entity_registry_updated(self, event: Event) -> None: + async def _handle_entity_registry_updated( + self, event: Event[er.EventEntityRegistryUpdatedData] + ) -> None: """Handle when entity registry updated.""" if not self.enabled or not self._cloud.is_logged_in: return @@ -527,15 +529,14 @@ class CloudAlexaConfig(alexa_config.AbstractConfig): if not self.should_expose(entity_id): return - action = event.data["action"] - to_update = [] - to_remove = [] + to_update: list[str] = [] + to_remove: list[str] = [] - if action == "create": + if event.data["action"] == "create": to_update.append(entity_id) - elif action == "remove": + elif event.data["action"] == "remove": to_remove.append(entity_id) - elif action == "update" and bool( + elif event.data["action"] == "update" and bool( set(event.data["changes"]) & er.ENTITY_DESCRIBING_ATTRIBUTES ): to_update.append(entity_id) diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 1ba2fab717f..3586823ca11 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -453,7 +453,9 @@ class CloudGoogleConfig(AbstractConfig): self.async_schedule_google_sync_all() @callback - def _handle_entity_registry_updated(self, event: Event) -> None: + def _handle_entity_registry_updated( + self, event: Event[er.EventEntityRegistryUpdatedData] + ) -> None: """Handle when entity registry updated.""" if ( not self.enabled @@ -476,7 +478,9 @@ class CloudGoogleConfig(AbstractConfig): self.async_schedule_google_sync_all() @callback - async def _handle_device_registry_updated(self, event: Event) -> None: + async def _handle_device_registry_updated( + self, event: Event[dr.EventDeviceRegistryUpdatedData] + ) -> None: """Handle when device registry updated.""" if ( not self.enabled From 7898bdcd4e35fcd8c3ab0acd9dbd3ef2a8d4074e Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 6 Apr 2024 14:00:13 +0200 Subject: [PATCH 331/967] Improve generic event typing [conversation] (#114729) --- homeassistant/components/conversation/default_agent.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 32cec18dfef..731b6ead527 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -175,14 +175,16 @@ class DefaultAgent(ConversationEntity): return get_languages() @core.callback - def _filter_entity_registry_changes(self, event_data: dict[str, Any]) -> bool: + def _filter_entity_registry_changes( + self, event_data: er.EventEntityRegistryUpdatedData + ) -> bool: """Filter entity registry changed events.""" return event_data["action"] == "update" and any( field in event_data["changes"] for field in _ENTITY_REGISTRY_UPDATE_FIELDS ) @core.callback - def _filter_state_changes(self, event_data: dict[str, Any]) -> bool: + def _filter_state_changes(self, event_data: EventStateChangedData) -> bool: """Filter state changed events.""" return not event_data["old_state"] or not event_data["new_state"] @@ -752,9 +754,7 @@ class DefaultAgent(ConversationEntity): return lang_intents @core.callback - def _async_clear_slot_list( - self, event: core.Event[dict[str, Any]] | None = None - ) -> None: + def _async_clear_slot_list(self, event: core.Event[Any] | None = None) -> None: """Clear slot lists when a registry has changed.""" self._slot_lists = None assert self._unsub_clear_slot_list is not None From 289700dcf5519aeab2966d46d553259d887665e8 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 6 Apr 2024 14:01:05 +0200 Subject: [PATCH 332/967] Improve generic event typing [device_tracker] (#114730) --- homeassistant/components/device_tracker/config_entry.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_tracker/config_entry.py b/homeassistant/components/device_tracker/config_entry.py index 99c152cd449..33c753c41e1 100644 --- a/homeassistant/components/device_tracker/config_entry.py +++ b/homeassistant/components/device_tracker/config_entry.py @@ -18,7 +18,10 @@ from homeassistant.const import ( ) from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.device_registry import ( + DeviceInfo, + EventDeviceRegistryUpdatedData, +) from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent @@ -101,7 +104,7 @@ def _async_register_mac( data = hass.data[data_key] = {mac: (domain, unique_id)} @callback - def handle_device_event(ev: Event) -> None: + def handle_device_event(ev: Event[EventDeviceRegistryUpdatedData]) -> None: """Enable the online status entity for the mac of a newly created device.""" # Only for new devices if ev.data["action"] != "create": From bf142aef5f2712f02f7f2b03502a131b7a56b7e2 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 6 Apr 2024 14:13:20 +0200 Subject: [PATCH 333/967] Fix ruff error (#115023) --- 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 731b6ead527..57ac5e2cc58 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -184,7 +184,7 @@ class DefaultAgent(ConversationEntity): ) @core.callback - def _filter_state_changes(self, event_data: EventStateChangedData) -> bool: + def _filter_state_changes(self, event_data: core.EventStateChangedData) -> bool: """Filter state changed events.""" return not event_data["old_state"] or not event_data["new_state"] From fa7da34298edf954d11de8b967b8aa62b55d2c9b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 6 Apr 2024 14:16:25 +0200 Subject: [PATCH 334/967] Improve generic event typing [tasmota] (#114737) --- .../components/tasmota/device_automation.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/tasmota/device_automation.py b/homeassistant/components/tasmota/device_automation.py index ef05585dd87..af14efbd65c 100644 --- a/homeassistant/components/tasmota/device_automation.py +++ b/homeassistant/components/tasmota/device_automation.py @@ -1,7 +1,6 @@ """Provides device automations for Tasmota.""" -from collections.abc import Mapping -from typing import Any +from __future__ import annotations from hatasmota.const import AUTOMATION_TYPE_TRIGGER from hatasmota.models import DiscoveryHashType @@ -9,7 +8,10 @@ from hatasmota.trigger import TasmotaTrigger from homeassistant.config_entries import ConfigEntry from homeassistant.core import Event, HomeAssistant, callback -from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED +from homeassistant.helpers.device_registry import ( + EVENT_DEVICE_REGISTRY_UPDATED, + EventDeviceRegistryUpdatedData, +) from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import device_trigger @@ -25,12 +27,16 @@ async def async_remove_automations(hass: HomeAssistant, device_id: str) -> None: async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Set up Tasmota device automation dynamically through discovery.""" - async def async_device_removed(event: Event) -> None: + async def async_device_removed( + event: Event[EventDeviceRegistryUpdatedData], + ) -> None: """Handle the removal of a device.""" await async_remove_automations(hass, event.data["device_id"]) @callback - def _async_device_removed_filter(event_data: Mapping[str, Any]) -> bool: + def _async_device_removed_filter( + event_data: EventDeviceRegistryUpdatedData, + ) -> bool: """Filter device registry events.""" return event_data["action"] == "remove" From e0f5559c8f455812dfa37452dd94b815c398da7a Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 6 Apr 2024 14:18:03 +0200 Subject: [PATCH 335/967] Improve generic event typing [EventComponentLoaded] (#114739) --- homeassistant/components/config/__init__.py | 6 ++++-- homeassistant/components/homeassistant_alerts/__init__.py | 3 ++- homeassistant/setup.py | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index d71a00ce3bd..1f6dc2c2122 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -7,7 +7,7 @@ from homeassistant.const import EVENT_COMPONENT_LOADED from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import ConfigType -from homeassistant.setup import ATTR_COMPONENT +from homeassistant.setup import EventComponentLoaded from . import ( area_registry, @@ -56,6 +56,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if panel.async_setup(hass): name = panel.__name__.split(".")[-1] key = f"{DOMAIN}.{name}" - hass.bus.async_fire(EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: key}) + hass.bus.async_fire( + EVENT_COMPONENT_LOADED, EventComponentLoaded(component=key) + ) return True diff --git a/homeassistant/components/homeassistant_alerts/__init__.py b/homeassistant/components/homeassistant_alerts/__init__.py index 7d0a7c588dd..b0eefad053e 100644 --- a/homeassistant/components/homeassistant_alerts/__init__.py +++ b/homeassistant/components/homeassistant_alerts/__init__.py @@ -23,6 +23,7 @@ from homeassistant.helpers.issue_registry import ( from homeassistant.helpers.start import async_at_start from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.setup import EventComponentLoaded COMPONENT_LOADED_COOLDOWN = 30 DOMAIN = "homeassistant_alerts" @@ -99,7 +100,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) @callback - def _component_loaded(_: Event) -> None: + def _component_loaded(_: Event[EventComponentLoaded]) -> None: refresh_debouncer.async_schedule_call() await coordinator.async_refresh() diff --git a/homeassistant/setup.py b/homeassistant/setup.py index b1fc080a429..0516f78b198 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -455,7 +455,7 @@ async def _async_setup_component( # noqa: C901 if domain in hass.data[DATA_SETUP]: hass.data[DATA_SETUP].pop(domain) - hass.bus.async_fire(EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: domain}) + hass.bus.async_fire(EVENT_COMPONENT_LOADED, EventComponentLoaded(component=domain)) return True From bf0309a7226da721692a196b1f4e0459bd0af477 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 6 Apr 2024 14:19:39 +0200 Subject: [PATCH 336/967] Improve generic event typing [mqtt_statestream] (#114732) --- .../components/mqtt_statestream/__init__.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/mqtt_statestream/__init__.py b/homeassistant/components/mqtt_statestream/__init__.py index a9b86c4bf8f..6a1a791d7ac 100644 --- a/homeassistant/components/mqtt_statestream/__init__.py +++ b/homeassistant/components/mqtt_statestream/__init__.py @@ -1,16 +1,14 @@ """Publish simple item state changes via MQTT.""" -from collections.abc import Mapping import json import logging -from typing import Any import voluptuous as vol from homeassistant.components import mqtt from homeassistant.components.mqtt import valid_publish_topic from homeassistant.const import EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED -from homeassistant.core import Event, HomeAssistant, State, callback +from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import ( INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA, @@ -57,9 +55,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if not base_topic.endswith("/"): base_topic = f"{base_topic}/" - async def _state_publisher(evt: Event) -> None: - entity_id: str = evt.data["entity_id"] - new_state: State = evt.data["new_state"] + async def _state_publisher(evt: Event[EventStateChangedData]) -> None: + entity_id = evt.data["entity_id"] + assert (new_state := evt.data["new_state"]) payload = new_state.state @@ -92,9 +90,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @callback def _ha_started(hass: HomeAssistant) -> None: @callback - def _event_filter(event_data: Mapping[str, Any]) -> bool: - entity_id: str = event_data["entity_id"] - new_state: State | None = event_data["new_state"] + def _event_filter(event_data: EventStateChangedData) -> bool: + entity_id = event_data["entity_id"] + new_state = event_data["new_state"] if new_state is None: return False if not publish_filter(entity_id): From 856567d67408b207940f4c8711a21310a8ac69d6 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 6 Apr 2024 14:20:31 +0200 Subject: [PATCH 337/967] Improve generic event typing [google_pubsub] (#114731) --- homeassistant/components/google_pubsub/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google_pubsub/__init__.py b/homeassistant/components/google_pubsub/__init__.py index 74e2a297ff4..f289fae2456 100644 --- a/homeassistant/components/google_pubsub/__init__.py +++ b/homeassistant/components/google_pubsub/__init__.py @@ -11,7 +11,7 @@ from google.cloud.pubsub_v1 import PublisherClient import voluptuous as vol from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNAVAILABLE, STATE_UNKNOWN -from homeassistant.core import Event, HomeAssistant +from homeassistant.core import Event, EventStateChangedData, HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import FILTER_SCHEMA from homeassistant.helpers.typing import ConfigType @@ -59,9 +59,9 @@ def setup(hass: HomeAssistant, yaml_config: ConfigType) -> bool: encoder = DateTimeJSONEncoder() - def send_to_pubsub(event: Event): + def send_to_pubsub(event: Event[EventStateChangedData]): """Send states to Pub/Sub.""" - state = event.data.get("new_state") + state = event.data["new_state"] if ( state is None or state.state in (STATE_UNKNOWN, "", STATE_UNAVAILABLE) From 81d682874f5c56e5e2f0a8fba4fd49ecba2d7328 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 6 Apr 2024 14:23:32 +0200 Subject: [PATCH 338/967] Update typing extensions to 4.11.0 (#114985) --- homeassistant/components/zwave_js/config_flow.py | 2 +- homeassistant/config_entries.py | 14 +++++++------- homeassistant/helpers/data_entry_flow.py | 2 +- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index ca05dc2117b..3470f64f79f 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -183,7 +183,7 @@ class BaseZwaveJSFlow(ConfigEntryBaseFlow, ABC): @property @abstractmethod - def flow_manager(self) -> FlowManager[ConfigFlowResult, str]: + def flow_manager(self) -> FlowManager[ConfigFlowResult]: """Return the flow manager of the flow.""" async def async_step_install_addon( diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index dbc300891b1..a31728e7121 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1111,7 +1111,7 @@ class FlowCancelledError(Exception): """Error to indicate that a flow has been cancelled.""" -class ConfigEntriesFlowManager(data_entry_flow.FlowManager[ConfigFlowResult, str]): +class ConfigEntriesFlowManager(data_entry_flow.FlowManager[ConfigFlowResult]): """Manage all the config entry flows that are in progress.""" _flow_result = ConfigFlowResult @@ -1237,7 +1237,7 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager[ConfigFlowResult, str async def async_finish_flow( self, - flow: data_entry_flow.FlowHandler[ConfigFlowResult, str], + flow: data_entry_flow.FlowHandler[ConfigFlowResult], result: ConfigFlowResult, ) -> ConfigFlowResult: """Finish a config flow and add an entry.""" @@ -1359,7 +1359,7 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager[ConfigFlowResult, str async def async_post_init( self, - flow: data_entry_flow.FlowHandler[ConfigFlowResult, str], + flow: data_entry_flow.FlowHandler[ConfigFlowResult], result: ConfigFlowResult, ) -> None: """After a flow is initialised trigger new flow notifications.""" @@ -2027,7 +2027,7 @@ def _async_abort_entries_match( raise data_entry_flow.AbortFlow("already_configured") -class ConfigEntryBaseFlow(data_entry_flow.FlowHandler[ConfigFlowResult, str]): +class ConfigEntryBaseFlow(data_entry_flow.FlowHandler[ConfigFlowResult]): """Base class for config and option flows.""" _flow_result = ConfigFlowResult @@ -2379,7 +2379,7 @@ class ConfigFlow(ConfigEntryBaseFlow): return self.async_abort(reason=reason) -class OptionsFlowManager(data_entry_flow.FlowManager[ConfigFlowResult, str]): +class OptionsFlowManager(data_entry_flow.FlowManager[ConfigFlowResult]): """Flow to set options for a configuration entry.""" _flow_result = ConfigFlowResult @@ -2409,7 +2409,7 @@ class OptionsFlowManager(data_entry_flow.FlowManager[ConfigFlowResult, str]): async def async_finish_flow( self, - flow: data_entry_flow.FlowHandler[ConfigFlowResult, str], + flow: data_entry_flow.FlowHandler[ConfigFlowResult], result: ConfigFlowResult, ) -> ConfigFlowResult: """Finish an options flow and update options for configuration entry. @@ -2431,7 +2431,7 @@ class OptionsFlowManager(data_entry_flow.FlowManager[ConfigFlowResult, str]): return result async def _async_setup_preview( - self, flow: data_entry_flow.FlowHandler[ConfigFlowResult, str] + self, flow: data_entry_flow.FlowHandler[ConfigFlowResult] ) -> None: """Set up preview for an option flow handler.""" entry = self._async_get_config_entry(flow.handler) diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py index 1edeb28d88f..2adab32195b 100644 --- a/homeassistant/helpers/data_entry_flow.py +++ b/homeassistant/helpers/data_entry_flow.py @@ -18,7 +18,7 @@ from . import config_validation as cv _FlowManagerT = TypeVar( "_FlowManagerT", - bound="data_entry_flow.FlowManager[Any]", + bound=data_entry_flow.FlowManager[Any], default=data_entry_flow.FlowManager, ) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 560a1329a32..36432285e84 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -54,7 +54,7 @@ pyudev==0.24.1 PyYAML==6.0.1 requests==2.31.0 SQLAlchemy==2.0.29 -typing-extensions>=4.10.0,<5.0 +typing-extensions>=4.11.0,<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 f35aa12221f..7fe54efcbe5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,7 +60,7 @@ dependencies = [ "PyYAML==6.0.1", "requests==2.31.0", "SQLAlchemy==2.0.29", - "typing-extensions>=4.10.0,<5.0", + "typing-extensions>=4.11.0,<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 7d550bc8c6a..5221f8152c2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -35,7 +35,7 @@ python-slugify==8.0.4 PyYAML==6.0.1 requests==2.31.0 SQLAlchemy==2.0.29 -typing-extensions>=4.10.0,<5.0 +typing-extensions>=4.11.0,<5.0 ulid-transform==0.9.0 urllib3>=1.26.5,<2 voluptuous==0.13.1 From b743a86aa95d3b8d5ad81d09b6636ebb18566a23 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 6 Apr 2024 15:48:52 +0200 Subject: [PATCH 339/967] Refactor Vilfo tests (#115020) * Refactor Vilfo tests * Patch is_host_valid --- tests/components/vilfo/conftest.py | 61 ++++ tests/components/vilfo/test_config_flow.py | 332 ++++++++++----------- 2 files changed, 213 insertions(+), 180 deletions(-) create mode 100644 tests/components/vilfo/conftest.py diff --git a/tests/components/vilfo/conftest.py b/tests/components/vilfo/conftest.py new file mode 100644 index 00000000000..75ed352c839 --- /dev/null +++ b/tests/components/vilfo/conftest.py @@ -0,0 +1,61 @@ +"""Vilfo tests conftest.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest + +from homeassistant.components.vilfo import DOMAIN +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.vilfo.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +def mock_vilfo_client() -> Generator[AsyncMock, None, None]: + """Mock a Vilfo client.""" + with patch( + "homeassistant.components.vilfo.config_flow.VilfoClient", + autospec=True, + ) as mock_client: + client = mock_client.return_value + client.get_board_information.return_value = None + client.ping.return_value = None + client.resolve_firmware_version.return_value = "1.1.0" + client.resolve_mac_address.return_value = "FF-00-00-00-00-00" + client.mac = "FF-00-00-00-00-00" + yield client + + +@pytest.fixture +def mock_is_valid_host() -> Generator[AsyncMock, None, None]: + """Mock is_valid_host.""" + with patch( + "homeassistant.components.vilfo.config_flow.is_host_valid", + return_value=True, + ) as mock_is_valid_host: + yield mock_is_valid_host + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Mock a config entry.""" + return MockConfigEntry( + domain=DOMAIN, + title="testadmin.vilfo.com", + unique_id="FF-00-00-00-00-00", + data={ + CONF_HOST: "testadmin.vilfo.com", + CONF_ACCESS_TOKEN: "test-token", + }, + ) diff --git a/tests/components/vilfo/test_config_flow.py b/tests/components/vilfo/test_config_flow.py index 51c2698e241..c4fdb2fe22c 100644 --- a/tests/components/vilfo/test_config_flow.py +++ b/tests/components/vilfo/test_config_flow.py @@ -1,219 +1,191 @@ """Test the Vilfo Router config flow.""" -from unittest.mock import Mock, patch +from typing import Any +from unittest.mock import AsyncMock -import vilfo +import pytest +from vilfo.exceptions import AuthenticationException, VilfoException -from homeassistant import config_entries -from homeassistant.components.vilfo import config_flow from homeassistant.components.vilfo.const import DOMAIN -from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_ID, CONF_MAC +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from tests.common import MockConfigEntry -async def test_form(hass: HomeAssistant) -> None: - """Test we get the form.""" - mock_mac = "FF-00-00-00-00-00" - firmware_version = "1.1.0" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] is FlowResultType.FORM - assert result["errors"] == {} - - with ( - patch("vilfo.Client.ping", return_value=None), - patch("vilfo.Client.get_board_information", return_value=None), - patch("vilfo.Client.resolve_firmware_version", return_value=firmware_version), - patch("vilfo.Client.resolve_mac_address", return_value=mock_mac), - patch("homeassistant.components.vilfo.async_setup_entry") as mock_setup_entry, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], +@pytest.mark.parametrize( + ("user_input", "expected_unique_id", "mac"), + [ + ( {CONF_HOST: "testadmin.vilfo.com", CONF_ACCESS_TOKEN: "test-token"}, - ) - await hass.async_block_till_done() + "testadmin.vilfo.com", + None, + ), + ( + {CONF_HOST: "testadmin.vilfo.com", CONF_ACCESS_TOKEN: "test-token"}, + "FF-00-00-00-00-00", + "FF-00-00-00-00-00", + ), + ( + {CONF_HOST: "192.168.0.1", CONF_ACCESS_TOKEN: "test-token"}, + "FF-00-00-00-00-00", + "FF-00-00-00-00-00", + ), + ( + {CONF_HOST: "2001:db8::1428:57ab", CONF_ACCESS_TOKEN: "test-token"}, + "FF-00-00-00-00-00", + "FF-00-00-00-00-00", + ), + ], +) +async def test_full_flow( + hass: HomeAssistant, + mock_vilfo_client: AsyncMock, + mock_setup_entry: AsyncMock, + mock_is_valid_host: AsyncMock, + user_input: dict[str, Any], + expected_unique_id: str, + mac: str | None, +) -> None: + """Test we can finish a config flow.""" - assert result2["type"] is FlowResultType.CREATE_ENTRY - assert result2["title"] == "testadmin.vilfo.com" - assert result2["data"] == { - "host": "testadmin.vilfo.com", - "access_token": "test-token", - } + mock_vilfo_client.resolve_mac_address.return_value = mac + mock_vilfo_client.mac = mac + + 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 not result["errors"] + + 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 + assert result["title"] == user_input[CONF_HOST] + assert result["data"] == user_input + assert result["result"].unique_id == expected_unique_id assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_invalid_auth(hass: HomeAssistant) -> None: +async def test_form_invalid_auth( + hass: HomeAssistant, + mock_vilfo_client: AsyncMock, + mock_is_valid_host: AsyncMock, + mock_setup_entry: AsyncMock, +) -> None: """Test we handle invalid auth.""" + mock_vilfo_client.get_board_information.side_effect = AuthenticationException + mock_vilfo_client.resolve_mac_address.return_value = None + result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM + assert not result["errors"] + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "testadmin.vilfo.com", CONF_ACCESS_TOKEN: "test-token"}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "invalid_auth"} + + mock_vilfo_client.get_board_information.side_effect = None + mock_vilfo_client.resolve_mac_address.return_value = "FF-00-00-00-00-00" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "testadmin.vilfo.com", CONF_ACCESS_TOKEN: "test-token"}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + + +@pytest.mark.parametrize( + ("side_effect", "error"), + [(VilfoException, "cannot_connect"), (Exception, "unknown")], +) +async def test_form_exceptions( + hass: HomeAssistant, + mock_vilfo_client: AsyncMock, + mock_is_valid_host: AsyncMock, + mock_setup_entry: AsyncMock, + side_effect: Exception, + error: str, +) -> None: + """Test we handle exceptions.""" + mock_vilfo_client.ping.side_effect = side_effect + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} ) - with ( - patch("vilfo.Client.ping", return_value=None), - patch("vilfo.Client.resolve_mac_address", return_value=None), - patch( - "vilfo.Client.get_board_information", - side_effect=vilfo.exceptions.AuthenticationException, - ), - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"host": "testadmin.vilfo.com", "access_token": "test-token"}, - ) - - assert result2["type"] is FlowResultType.FORM - assert result2["errors"] == {"base": "invalid_auth"} - - -async def test_form_cannot_connect(hass: HomeAssistant) -> None: - """Test we handle cannot connect error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "testadmin.vilfo.com", CONF_ACCESS_TOKEN: "test-token"}, ) - with ( - patch("vilfo.Client.ping", side_effect=vilfo.exceptions.VilfoException), - patch("vilfo.Client.resolve_mac_address"), - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"host": "testadmin.vilfo.com", "access_token": "test-token"}, - ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error} - assert result2["type"] is FlowResultType.FORM - assert result2["errors"] == {"base": "cannot_connect"} + mock_vilfo_client.ping.side_effect = None - with ( - patch("vilfo.Client.ping", side_effect=vilfo.exceptions.VilfoException), - patch("vilfo.Client.resolve_mac_address"), - ): - result3 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"host": "testadmin.vilfo.com", "access_token": "test-token"}, - ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "testadmin.vilfo.com", CONF_ACCESS_TOKEN: "test-token"}, + ) + await hass.async_block_till_done() - assert result3["type"] is FlowResultType.FORM - assert result3["errors"] == {"base": "cannot_connect"} + assert result["type"] is FlowResultType.CREATE_ENTRY -async def test_form_wrong_host(hass: HomeAssistant) -> None: +async def test_form_wrong_host( + hass: HomeAssistant, + mock_is_valid_host: AsyncMock, +) -> None: """Test we handle wrong host errors.""" + mock_is_valid_host.return_value = False result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_USER}, - data={"host": "this is an invalid hostname", "access_token": "test-token"}, + context={"source": SOURCE_USER}, + data={ + CONF_HOST: "this is an invalid hostname", + CONF_ACCESS_TOKEN: "test-token", + }, ) assert result["errors"] == {"host": "wrong_host"} -async def test_form_already_configured(hass: HomeAssistant) -> None: +async def test_form_already_configured( + hass: HomeAssistant, + mock_vilfo_client: AsyncMock, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, + mock_is_valid_host: AsyncMock, +) -> None: """Test that we handle already configured exceptions appropriately.""" - first_flow_result1 = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - firmware_version = "1.1.0" - with ( - patch("vilfo.Client.ping", return_value=None), - patch( - "vilfo.Client.get_board_information", - return_value=None, - ), - patch("vilfo.Client.resolve_firmware_version", return_value=firmware_version), - patch("vilfo.Client.resolve_mac_address", return_value=None), - ): - first_flow_result2 = await hass.config_entries.flow.async_configure( - first_flow_result1["flow_id"], - {CONF_HOST: "testadmin.vilfo.com", CONF_ACCESS_TOKEN: "test-token"}, - ) + mock_config_entry.add_to_hass(hass) - second_flow_result1 = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with ( - patch("vilfo.Client.ping", return_value=None), - patch( - "vilfo.Client.get_board_information", - return_value=None, - ), - patch("vilfo.Client.resolve_firmware_version", return_value=firmware_version), - patch("vilfo.Client.resolve_mac_address", return_value=None), - ): - second_flow_result2 = await hass.config_entries.flow.async_configure( - second_flow_result1["flow_id"], - {CONF_HOST: "testadmin.vilfo.com", CONF_ACCESS_TOKEN: "test-token"}, - ) - - assert first_flow_result2["type"] is FlowResultType.CREATE_ENTRY - assert second_flow_result2["type"] is FlowResultType.ABORT - assert second_flow_result2["reason"] == "already_configured" - - -async def test_form_unexpected_exception(hass: HomeAssistant) -> None: - """Test that we handle unexpected exceptions.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) - - with patch( - "homeassistant.components.vilfo.config_flow.VilfoClient", - ) as mock_client: - mock_client.return_value.ping = Mock(side_effect=Exception) - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"host": "testadmin.vilfo.com", "access_token": "test-token"}, - ) - - assert result2["errors"] == {"base": "unknown"} - - -async def test_validate_input_returns_data(hass: HomeAssistant) -> None: - """Test we handle the MAC address being resolved or not.""" - mock_data = {"host": "testadmin.vilfo.com", "access_token": "test-token"} - mock_data_with_ip = {"host": "192.168.0.1", "access_token": "test-token"} - mock_data_with_ipv6 = {"host": "2001:db8::1428:57ab", "access_token": "test-token"} - mock_mac = "FF-00-00-00-00-00" - firmware_version = "1.1.0" - - with ( - patch("vilfo.Client.ping", return_value=None), - patch("vilfo.Client.get_board_information", return_value=None), - patch("vilfo.Client.resolve_firmware_version", return_value=firmware_version), - patch("vilfo.Client.resolve_mac_address", return_value=None), - ): - result = await config_flow.validate_input(hass, data=mock_data) - - assert result["title"] == mock_data["host"] - assert result[CONF_HOST] == mock_data["host"] - assert result[CONF_MAC] is None - assert result[CONF_ID] == mock_data["host"] - - with ( - patch("vilfo.Client.ping", return_value=None), - patch("vilfo.Client.get_board_information", return_value=None), - patch("vilfo.Client.resolve_firmware_version", return_value=firmware_version), - patch("vilfo.Client.resolve_mac_address", return_value=mock_mac), - ): - result2 = await config_flow.validate_input(hass, data=mock_data) - result3 = await config_flow.validate_input(hass, data=mock_data_with_ip) - result4 = await config_flow.validate_input(hass, data=mock_data_with_ipv6) - - assert result2["title"] == mock_data["host"] - assert result2[CONF_HOST] == mock_data["host"] - assert result2[CONF_MAC] == mock_mac - assert result2[CONF_ID] == mock_mac - - assert result3["title"] == mock_data_with_ip["host"] - assert result3[CONF_HOST] == mock_data_with_ip["host"] - assert result3[CONF_MAC] == mock_mac - assert result3[CONF_ID] == mock_mac - - assert result4["title"] == mock_data_with_ipv6["host"] - assert result4[CONF_HOST] == mock_data_with_ipv6["host"] - assert result4[CONF_MAC] == mock_mac - assert result4[CONF_ID] == mock_mac + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "testadmin.vilfo.com", CONF_ACCESS_TOKEN: "test-token"}, + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" From 57cc3495c2a26b7ba29d8f9f252238865e6132e1 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 6 Apr 2024 15:51:56 +0200 Subject: [PATCH 340/967] Sort coveragerc again (#115017) Co-authored-by: Jan Bouwhuis --- .coveragerc | 2 +- script/hassfest/coverage.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index 68d7629b6c5..63a55d8d5b1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -6,8 +6,8 @@ source = homeassistant omit = homeassistant/__main__.py - homeassistant/helpers/signal.py homeassistant/helpers/backports/* + homeassistant/helpers/signal.py homeassistant/scripts/__init__.py homeassistant/scripts/check_config.py homeassistant/scripts/ensure_config.py diff --git a/script/hassfest/coverage.py b/script/hassfest/coverage.py index 264960a42e1..6be41fa43b8 100644 --- a/script/hassfest/coverage.py +++ b/script/hassfest/coverage.py @@ -28,6 +28,7 @@ PREFIX = """# Sorted by hassfest. source = homeassistant omit = homeassistant/__main__.py + homeassistant/helpers/backports/* homeassistant/helpers/signal.py homeassistant/scripts/__init__.py homeassistant/scripts/check_config.py From a5c82f37133e619ab0e750d31e52b7618515ee3e Mon Sep 17 00:00:00 2001 From: larsvinc Date: Sat, 6 Apr 2024 17:09:48 +0200 Subject: [PATCH 341/967] Add adax on/off functionality for local heaters (#114557) * Add adax on/off functionality for local heaters * Fixed ruff / newline * Ran Ruff * Update homeassistant/components/adax/climate.py Removed unecessary return Co-authored-by: G Johansson * Update homeassistant/components/adax/climate.py Co-authored-by: G Johansson * Update homeassistant/components/adax/climate.py Co-authored-by: G Johansson * Fixed bug with internal temperature state. * Apply suggestions from code review Added target_temp for code clarity Co-authored-by: G Johansson --------- Co-authored-by: G Johansson --- homeassistant/components/adax/climate.py | 26 +++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/adax/climate.py b/homeassistant/components/adax/climate.py index 69b89cfe8cc..ac381ff46d5 100644 --- a/homeassistant/components/adax/climate.py +++ b/homeassistant/components/adax/climate.py @@ -135,11 +135,15 @@ class AdaxDevice(ClimateEntity): class LocalAdaxDevice(ClimateEntity): """Representation of a heater.""" - _attr_hvac_modes = [HVACMode.HEAT] + _attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF] _attr_hvac_mode = HVACMode.HEAT _attr_max_temp = 35 _attr_min_temp = 5 - _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ) _attr_target_temperature_step = PRECISION_WHOLE _attr_temperature_unit = UnitOfTemperature.CELSIUS @@ -152,6 +156,14 @@ class LocalAdaxDevice(ClimateEntity): manufacturer="Adax", ) + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set hvac mode.""" + if hvac_mode == HVACMode.HEAT: + temperature = self._attr_target_temperature or self._attr_min_temp + await self._adax_data_handler.set_target_temperature(temperature) + elif hvac_mode == HVACMode.OFF: + await self._adax_data_handler.set_target_temperature(0) + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: @@ -161,6 +173,14 @@ class LocalAdaxDevice(ClimateEntity): async def async_update(self) -> None: """Get the latest data.""" data = await self._adax_data_handler.get_status() - self._attr_target_temperature = data["target_temperature"] self._attr_current_temperature = data["current_temperature"] self._attr_available = self._attr_current_temperature is not None + if (target_temp := data["target_temperature"]) == 0: + self._attr_hvac_mode = HVACMode.OFF + self._attr_icon = "mdi:radiator-off" + if target_temp == 0: + self._attr_target_temperature = self._attr_min_temp + else: + self._attr_hvac_mode = HVACMode.HEAT + self._attr_icon = "mdi:radiator" + self._attr_target_temperature = target_temp From 7f8341e03a564fee8562576849f2a22c5e13060f Mon Sep 17 00:00:00 2001 From: mkmer Date: Sat, 6 Apr 2024 11:22:56 -0400 Subject: [PATCH 342/967] Deprecate aux heat from Honeywell (#114110) * Remove aux heat * Add switch entity for emheat * Optimized async_setup_entry * Fix errors in comments * Fix new ruff failuer * Use constant for EM * Protect EM mode - must be in heat to turn on/off * Restore aux_heat * Add repair issue * Add missing place holder to issue * Better placeholder "option" --- .../components/honeywell/__init__.py | 2 +- homeassistant/components/honeywell/climate.py | 25 ++++- .../components/honeywell/strings.json | 26 +++++ homeassistant/components/honeywell/switch.py | 97 +++++++++++++++++++ .../components/honeywell/test_diagnostics.py | 2 +- tests/components/honeywell/test_init.py | 16 +-- tests/components/honeywell/test_switch.py | 87 +++++++++++++++++ 7 files changed, 244 insertions(+), 11 deletions(-) create mode 100644 homeassistant/components/honeywell/switch.py create mode 100644 tests/components/honeywell/test_switch.py diff --git a/homeassistant/components/honeywell/__init__.py b/homeassistant/components/honeywell/__init__.py index c1c46e2b7af..8349c383e9f 100644 --- a/homeassistant/components/honeywell/__init__.py +++ b/homeassistant/components/honeywell/__init__.py @@ -21,7 +21,7 @@ from .const import ( ) UPDATE_LOOP_SLEEP_TIME = 5 -PLATFORMS = [Platform.CLIMATE, Platform.SENSOR] +PLATFORMS = [Platform.CLIMATE, Platform.SENSOR, Platform.SWITCH] MIGRATE_OPTIONS_KEYS = {CONF_COOL_AWAY_TEMPERATURE, CONF_HEAT_AWAY_TEMPERATURE} diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index 5ac5e8a2472..bd32ee0a23d 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -35,7 +35,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, issue_registry as ir from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -169,6 +169,7 @@ class HoneywellUSThermostat(ClimateEntity): manufacturer="Honeywell", ) + self._attr_translation_placeholders = {"name": device.name} self._attr_temperature_unit = UnitOfTemperature.FAHRENHEIT if device.temperature_unit == "C": self._attr_temperature_unit = UnitOfTemperature.CELSIUS @@ -480,6 +481,16 @@ class HoneywellUSThermostat(ClimateEntity): async def async_turn_aux_heat_on(self) -> None: """Turn auxiliary heater on.""" + ir.async_create_issue( + self.hass, + DOMAIN, + "service_deprecation", + breaks_in_ha_version="2024.10.0", + is_fixable=True, + is_persistent=True, + severity=ir.IssueSeverity.WARNING, + translation_key="service_deprecation", + ) try: await self._device.set_system_mode("emheat") except SomeComfortError as err: @@ -489,6 +500,18 @@ class HoneywellUSThermostat(ClimateEntity): async def async_turn_aux_heat_off(self) -> None: """Turn auxiliary heater off.""" + + ir.async_create_issue( + self.hass, + DOMAIN, + "service_deprecation", + breaks_in_ha_version="2024.10.0", + is_fixable=True, + is_persistent=True, + severity=ir.IssueSeverity.WARNING, + translation_key="service_deprecation", + ) + try: if HVACMode.HEAT in self.hvac_modes: await self.async_set_hvac_mode(HVACMode.HEAT) diff --git a/homeassistant/components/honeywell/strings.json b/homeassistant/components/honeywell/strings.json index 6f855828e01..7506a7fda7c 100644 --- a/homeassistant/components/honeywell/strings.json +++ b/homeassistant/components/honeywell/strings.json @@ -41,6 +41,11 @@ "name": "Outdoor humidity" } }, + "switch": { + "emergency_heat": { + "name": "Emergency heat" + } + }, "climate": { "honeywell": { "state_attributes": { @@ -54,5 +59,26 @@ } } } + }, + "exceptions": { + "switch_failed_off": { + "message": "Honeywell could turn off emergency heat mode." + }, + "switch_failed_on": { + "message": "Honeywell could not set system mode to emergency heat mode." + } + }, + "issues": { + "service_deprecation": { + "title": "Honeywell aux heat is being removed", + "fix_flow": { + "step": { + "confirm": { + "title": "[%key:component::honeywell::issues::service_deprecation::title%]", + "description": "Use `switch.{name}_emergency_heat` instead to change mode.\n\nPlease adjust your automations and scripts and select **submit** to fix this issue." + } + } + } + } } } diff --git a/homeassistant/components/honeywell/switch.py b/homeassistant/components/honeywell/switch.py new file mode 100644 index 00000000000..4aebde76727 --- /dev/null +++ b/homeassistant/components/honeywell/switch.py @@ -0,0 +1,97 @@ +"""Support for Honeywell switches.""" + +from __future__ import annotations + +from typing import Any + +from aiosomecomfort import SomeComfortError +from aiosomecomfort.device import Device as SomeComfortDevice + +from homeassistant.components.switch import ( + SwitchDeviceClass, + SwitchEntity, + SwitchEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import HoneywellData +from .const import DOMAIN + +EMERGENCY_HEAT_KEY = "emergency_heat" + +SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = ( + SwitchEntityDescription( + key=EMERGENCY_HEAT_KEY, + translation_key=EMERGENCY_HEAT_KEY, + device_class=SwitchDeviceClass.SWITCH, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Honeywell switches.""" + data: HoneywellData = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities( + HoneywellSwitch(hass, config_entry, device, description) + for device in data.devices.values() + if device.raw_ui_data.get("SwitchEmergencyHeatAllowed") + for description in SWITCH_TYPES + ) + + +class HoneywellSwitch(SwitchEntity): + """Representation of a honeywell switch.""" + + _attr_has_entity_name = True + + def __init__( + self, + hass: HomeAssistant, + config_entry: ConfigEntry, + device: SomeComfortDevice, + description: SwitchEntityDescription, + ) -> None: + """Initialize the switch.""" + self._data = hass.data[DOMAIN][config_entry.entry_id] + self._device = device + self.entity_description = description + self._attr_unique_id = f"{device.deviceid}_{description.key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, device.deviceid)}, + name=device.name, + manufacturer="Honeywell", + ) + + 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 + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the switch off if on.""" + if self.is_on: + try: + await self._device.set_system_mode("off") + + except SomeComfortError as err: + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="switch_failed_off" + ) from err + + @property + def is_on(self) -> bool: + """Return true if Emergency heat is enabled.""" + return self._device.system_mode == "emheat" diff --git a/tests/components/honeywell/test_diagnostics.py b/tests/components/honeywell/test_diagnostics.py index b180bf0e5bc..06c41d3d055 100644 --- a/tests/components/honeywell/test_diagnostics.py +++ b/tests/components/honeywell/test_diagnostics.py @@ -29,7 +29,7 @@ async def test_entry_diagnostics( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED - assert hass.states.async_entity_ids_count() == 6 + assert hass.states.async_entity_ids_count() == 8 result = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) diff --git a/tests/components/honeywell/test_init.py b/tests/components/honeywell/test_init.py index d27428fcf65..a77c0aaed7e 100644 --- a/tests/components/honeywell/test_init.py +++ b/tests/components/honeywell/test_init.py @@ -30,7 +30,7 @@ async def test_setup_entry(hass: HomeAssistant, config_entry: MockConfigEntry) - await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED assert ( - hass.states.async_entity_ids_count() == 3 + hass.states.async_entity_ids_count() == 4 ) # 1 climate entity; 2 sensor entities @@ -63,8 +63,8 @@ async def test_setup_multiple_thermostats( await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED assert ( - hass.states.async_entity_ids_count() == 6 - ) # 2 climate entities; 4 sensor entities + hass.states.async_entity_ids_count() == 8 + ) # 2 climate entities; 4 sensor entities; 2 switch entities async def test_setup_multiple_thermostats_with_same_deviceid( @@ -84,8 +84,8 @@ async def test_setup_multiple_thermostats_with_same_deviceid( await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED assert ( - hass.states.async_entity_ids_count() == 3 - ) # 1 climate entity; 2 sensor entities + hass.states.async_entity_ids_count() == 4 + ) # 1 climate entity; 2 sensor entities; 1 switch enitiy assert "Platform honeywell does not generate unique IDs" not in caplog.text @@ -171,7 +171,7 @@ async def test_remove_stale_device( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED - assert hass.states.async_entity_ids_count() == 6 + assert hass.states.async_entity_ids_count() == 8 device_entries = dr.async_entries_for_config_entry( device_registry, config_entry.entry_id @@ -209,8 +209,8 @@ async def test_remove_stale_device( assert config_entry.state is ConfigEntryState.LOADED assert ( - hass.states.async_entity_ids_count() == 3 - ) # 1 climate entities; 2 sensor entities + hass.states.async_entity_ids_count() == 4 + ) # 1 climate entities; 2 sensor entities; 1 switch entity device_entries = dr.async_entries_for_config_entry( device_registry, config_entry.entry_id diff --git a/tests/components/honeywell/test_switch.py b/tests/components/honeywell/test_switch.py new file mode 100644 index 00000000000..73052871ef1 --- /dev/null +++ b/tests/components/honeywell/test_switch.py @@ -0,0 +1,87 @@ +"""Tests for Honeywell switch component.""" + +from unittest.mock import MagicMock + +from aiosomecomfort.exceptions import SomeComfortError +import pytest + +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 +from homeassistant.exceptions import HomeAssistantError + +from . import init_integration + +from tests.common import MockConfigEntry + + +async def test_emheat_switch( + hass: HomeAssistant, + config_entry: MockConfigEntry, + device: MagicMock, +) -> None: + """Test emergency heat 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, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + 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, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + device.set_system_mode.assert_called_once_with("off") + + device.set_system_mode.reset_mock() + device.system_mode = "heat" + device.set_system_mode.side_effect = SomeComfortError + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + device.set_system_mode.assert_called_once_with("emheat") + + device.set_system_mode.reset_mock() + device.system_mode = "emheat" + device.set_system_mode.side_effect = SomeComfortError + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + device.set_system_mode.assert_called_once_with("off") From fa47e792929e18185f4fe2c3fa849e3adb16a59b Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sat, 6 Apr 2024 18:37:54 +0200 Subject: [PATCH 343/967] Correct typo in IMAP translation (#115032) --- homeassistant/components/imap/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/imap/strings.json b/homeassistant/components/imap/strings.json index 378a1172788..ac06d833f55 100644 --- a/homeassistant/components/imap/strings.json +++ b/homeassistant/components/imap/strings.json @@ -43,7 +43,7 @@ "message": "Marking the the message for deletion failed with \"{error}\"." }, "expunge_failed": { - "message": "Expungling the the message failed with \"{error}\"." + "message": "Expunging the the message failed with \"{error}\"." }, "invalid_entry": { "message": "No valid IMAP entry was found." From c33a23404805bfd1a49d109be7d40cede48b9d8a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 6 Apr 2024 09:15:40 -1000 Subject: [PATCH 344/967] Make eager_start default to True for async_create_task (#114995) --- homeassistant/core.py | 2 +- tests/test_core.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 48036de519e..7e09363178d 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -719,7 +719,7 @@ class HomeAssistant: self, target: Coroutine[Any, Any, _R], name: str | None = None, - eager_start: bool = False, + eager_start: bool = True, ) -> asyncio.Task[_R]: """Create a task from within the event loop. diff --git a/tests/test_core.py b/tests/test_core.py index 44da9695fdc..e9d8d39ce18 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -327,7 +327,7 @@ async def test_async_create_task_schedule_coroutine() -> None: async def job(): pass - ha.HomeAssistant.async_create_task(hass, job()) + ha.HomeAssistant.async_create_task(hass, job(), eager_start=False) assert len(hass.loop.call_soon.mock_calls) == 0 assert len(hass.loop.create_task.mock_calls) == 1 assert len(hass.add_job.mock_calls) == 0 @@ -353,7 +353,9 @@ async def test_async_create_task_schedule_coroutine_with_name() -> None: async def job(): pass - task = ha.HomeAssistant.async_create_task(hass, job(), "named task") + task = ha.HomeAssistant.async_create_task( + hass, job(), "named task", eager_start=False + ) assert len(hass.loop.call_soon.mock_calls) == 0 assert len(hass.loop.create_task.mock_calls) == 1 assert len(hass.add_job.mock_calls) == 0 From 2e3cb1a767b5a927cb2f02066317402de8321674 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sat, 6 Apr 2024 21:17:44 +0200 Subject: [PATCH 345/967] Correct changes hassfest coverage backports (#115044) --- .coveragerc | 1 - script/hassfest/coverage.py | 1 - 2 files changed, 2 deletions(-) diff --git a/.coveragerc b/.coveragerc index 63a55d8d5b1..ed658f3ca55 100644 --- a/.coveragerc +++ b/.coveragerc @@ -6,7 +6,6 @@ source = homeassistant omit = homeassistant/__main__.py - homeassistant/helpers/backports/* homeassistant/helpers/signal.py homeassistant/scripts/__init__.py homeassistant/scripts/check_config.py diff --git a/script/hassfest/coverage.py b/script/hassfest/coverage.py index 6be41fa43b8..264960a42e1 100644 --- a/script/hassfest/coverage.py +++ b/script/hassfest/coverage.py @@ -28,7 +28,6 @@ PREFIX = """# Sorted by hassfest. source = homeassistant omit = homeassistant/__main__.py - homeassistant/helpers/backports/* homeassistant/helpers/signal.py homeassistant/scripts/__init__.py homeassistant/scripts/check_config.py From 8f425b9ea725c192a6e42045d9b0d45eec460728 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 6 Apr 2024 21:35:42 +0200 Subject: [PATCH 346/967] Improve generic event typing [recorder] (#114736) --- .../components/recorder/entity_registry.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/recorder/entity_registry.py b/homeassistant/components/recorder/entity_registry.py index 5bf1856316a..1c0299fc8da 100644 --- a/homeassistant/components/recorder/entity_registry.py +++ b/homeassistant/components/recorder/entity_registry.py @@ -1,8 +1,7 @@ """Recorder entity registry helper.""" -from collections.abc import Mapping import logging -from typing import Any +from typing import TYPE_CHECKING from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import entity_registry as er @@ -19,10 +18,14 @@ def async_setup(hass: HomeAssistant) -> None: """Set up the entity hooks.""" @callback - def _async_entity_id_changed(event: Event) -> None: + def _async_entity_id_changed( + event: Event[er.EventEntityRegistryUpdatedData], + ) -> None: instance = get_instance(hass) - old_entity_id: str = event.data["old_entity_id"] - new_entity_id: str = event.data["entity_id"] + if TYPE_CHECKING: + assert event.data["action"] == "update" and "old_entity_id" in event.data + old_entity_id = event.data["old_entity_id"] + new_entity_id = event.data["entity_id"] instance.async_update_statistics_metadata( old_entity_id, new_statistic_id=new_entity_id ) @@ -31,7 +34,9 @@ def async_setup(hass: HomeAssistant) -> None: ) @callback - def entity_registry_changed_filter(event_data: Mapping[str, Any]) -> bool: + def entity_registry_changed_filter( + event_data: er.EventEntityRegistryUpdatedData, + ) -> bool: """Handle entity_id changed filter.""" return event_data["action"] == "update" and "old_entity_id" in event_data From 166910f587f91c5bd468df28644ce124211e611d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 6 Apr 2024 09:53:50 -1000 Subject: [PATCH 347/967] Make eager_start default to True for async_create_background_task (#114996) --- homeassistant/components/imap/coordinator.py | 2 +- homeassistant/components/ollama/config_flow.py | 4 +++- homeassistant/components/zeroconf/__init__.py | 1 + homeassistant/core.py | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/imap/coordinator.py b/homeassistant/components/imap/coordinator.py index 997bff13534..59ceb2b3b3d 100644 --- a/homeassistant/components/imap/coordinator.py +++ b/homeassistant/components/imap/coordinator.py @@ -448,7 +448,7 @@ class ImapPushDataUpdateCoordinator(ImapDataUpdateCoordinator): async def async_start(self) -> None: """Start coordinator.""" self._push_wait_task = self.hass.async_create_background_task( - self._async_wait_push_loop(), "Wait for IMAP data push" + self._async_wait_push_loop(), "Wait for IMAP data push", eager_start=False ) async def _async_wait_push_loop(self) -> None: diff --git a/homeassistant/components/ollama/config_flow.py b/homeassistant/components/ollama/config_flow.py index 50d0667803f..4c59a38bfe0 100644 --- a/homeassistant/components/ollama/config_flow.py +++ b/homeassistant/components/ollama/config_flow.py @@ -149,7 +149,9 @@ class OllamaConfigFlow(ConfigFlow, domain=DOMAIN): # The task will block until the model and metadata are fully # downloaded. self.download_task = self.hass.async_create_background_task( - self.client.pull(self.model), f"Downloading {self.model}" + self.client.pull(self.model), + f"Downloading {self.model}", + eager_start=False, ) if self.download_task.done(): diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 66c41c19474..5ebe1bb769e 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -432,6 +432,7 @@ class ZeroconfDiscovery: zeroconf, async_service_info, service_type, name ), name=f"zeroconf lookup {name}.{service_type}", + eager_start=False, ) async def _async_lookup_and_process_service_update( diff --git a/homeassistant/core.py b/homeassistant/core.py index 7e09363178d..aa3b5c8434f 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -742,7 +742,7 @@ class HomeAssistant: @callback def async_create_background_task( - self, target: Coroutine[Any, Any, _R], name: str, eager_start: bool = False + self, target: Coroutine[Any, Any, _R], name: str, eager_start: bool = True ) -> asyncio.Task[_R]: """Create a task from within the event loop. From 52957849cf4207f276139032bf06ad316415a389 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 6 Apr 2024 10:59:24 -1000 Subject: [PATCH 348/967] Make eager_start default to True for config entry async_create_background_task (#115050) --- homeassistant/config_entries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index a31728e7121..413637fd726 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1080,7 +1080,7 @@ class ConfigEntry: hass: HomeAssistant, target: Coroutine[Any, Any, _R], name: str, - eager_start: bool = False, + eager_start: bool = True, ) -> asyncio.Task[_R]: """Create a background task tied to the config entry lifecycle. From b0fd3d0b8990fba32f11c94c636e4af7870344f1 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 6 Apr 2024 23:09:46 +0200 Subject: [PATCH 349/967] Bump `brother` to version 4.1.0 (#115021) 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 9ca18a95a1e..3bbaf40f686 100644 --- a/homeassistant/components/brother/manifest.json +++ b/homeassistant/components/brother/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_polling", "loggers": ["brother", "pyasn1", "pysmi", "pysnmp"], "quality_scale": "platinum", - "requirements": ["brother==4.0.2"], + "requirements": ["brother==4.1.0"], "zeroconf": [ { "type": "_printer._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 8e78b920b28..2b76cd42831 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -610,7 +610,7 @@ bring-api==0.5.7 broadlink==0.18.3 # homeassistant.components.brother -brother==4.0.2 +brother==4.1.0 # homeassistant.components.brottsplatskartan brottsplatskartan==1.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 62fe74f5600..f39962eff3a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -521,7 +521,7 @@ bring-api==0.5.7 broadlink==0.18.3 # homeassistant.components.brother -brother==4.0.2 +brother==4.1.0 # homeassistant.components.brottsplatskartan brottsplatskartan==1.0.5 From 2290362dfb94da02ad0a8f552005860f399e53a7 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sat, 6 Apr 2024 23:18:52 +0200 Subject: [PATCH 350/967] Update xknxproject to 3.7.1 (#115053) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 99c150a8346..af0c6b8d01c 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -12,7 +12,7 @@ "quality_scale": "platinum", "requirements": [ "xknx==2.12.2", - "xknxproject==3.7.0", + "xknxproject==3.7.1", "knx-frontend==2024.1.20.105944" ], "single_config_entry": true diff --git a/requirements_all.txt b/requirements_all.txt index 2b76cd42831..7202e326f58 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2887,7 +2887,7 @@ xiaomi-ble==0.28.0 xknx==2.12.2 # homeassistant.components.knx -xknxproject==3.7.0 +xknxproject==3.7.1 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f39962eff3a..6d69e839acc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2231,7 +2231,7 @@ xiaomi-ble==0.28.0 xknx==2.12.2 # homeassistant.components.knx -xknxproject==3.7.0 +xknxproject==3.7.1 # homeassistant.components.bluesound # homeassistant.components.fritz From 29bc67234e7ac023f23338ada3db0625a63cc20a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 6 Apr 2024 11:22:02 -1000 Subject: [PATCH 351/967] Make eager_start default to True for config entry async_create_task (#115047) --- homeassistant/components/weather/__init__.py | 4 ++-- homeassistant/config_entries.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 95655f439c9..aa4989de2fe 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -1163,7 +1163,7 @@ class CoordinatorWeatherEntity( assert coordinator.config_entry is not None getattr(self, f"_handle_{forecast_type}_forecast_coordinator_update")() coordinator.config_entry.async_create_task( - self.hass, self.async_update_listeners((forecast_type,)) + self.hass, self.async_update_listeners((forecast_type,)), eager_start=False ) @callback @@ -1273,5 +1273,5 @@ class SingleCoordinatorWeatherEntity( super()._handle_coordinator_update() assert self.coordinator.config_entry self.coordinator.config_entry.async_create_task( - self.hass, self.async_update_listeners(None) + self.hass, self.async_update_listeners(None), eager_start=False ) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 413637fd726..adbb99b9d3c 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1056,7 +1056,7 @@ class ConfigEntry: hass: HomeAssistant, target: Coroutine[Any, Any, _R], name: str | None = None, - eager_start: bool = False, + eager_start: bool = True, ) -> asyncio.Task[_R]: """Create a task from within the event loop. From 9a1b0874fd03840473f9eefe5fd7eeed5cf3c17c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Sat, 6 Apr 2024 23:37:22 +0200 Subject: [PATCH 352/967] Update aioairzone-cloud to v0.5.1 (#115029) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update aioairzone-cloud to v0.5.1 Signed-off-by: Álvaro Fernández Rojas * tests: airzone_cloud: fix diagnostics Signed-off-by: Álvaro Fernández Rojas --------- Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/airzone_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../airzone_cloud/snapshots/test_diagnostics.ambr | 7 ++++++- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/airzone_cloud/manifest.json b/homeassistant/components/airzone_cloud/manifest.json index 2b7615c01f4..366f8214bc1 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.0"] + "requirements": ["aioairzone-cloud==0.5.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 7202e326f58..32ff63923b0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -185,7 +185,7 @@ aio-georss-gdacs==0.9 aioairq==0.3.2 # homeassistant.components.airzone_cloud -aioairzone-cloud==0.5.0 +aioairzone-cloud==0.5.1 # homeassistant.components.airzone aioairzone==0.7.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6d69e839acc..6bbcffb27fc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -164,7 +164,7 @@ aio-georss-gdacs==0.9 aioairq==0.3.2 # homeassistant.components.airzone_cloud -aioairzone-cloud==0.5.0 +aioairzone-cloud==0.5.1 # homeassistant.components.airzone aioairzone==0.7.6 diff --git a/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr b/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr index 0edd17d513a..7ec1c2eb2fe 100644 --- a/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr +++ b/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr @@ -287,7 +287,12 @@ 'id': 'dhw1', 'installation': 'installation1', 'is-connected': True, - 'name': 'DHW', + 'name': 'Airzone Cloud DHW', + 'operation': 0, + 'operations': list([ + 0, + 1, + ]), 'power': False, 'problems': False, 'temperature': 45.5, From 78bb21138ce44954bc440da19f92393c58730380 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Sat, 6 Apr 2024 23:38:05 +0200 Subject: [PATCH 353/967] Remove @StevenLooman from dlna_dmr codeowners (#115028) * Remove myself from dlna_dmr codeowners * Update codeowners file --------- Co-authored-by: jbouwh --- CODEOWNERS | 4 ++-- homeassistant/components/dlna_dmr/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index fa06757896c..31e97d9e511 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -316,8 +316,8 @@ build.json @home-assistant/supervisor /tests/components/discovergy/ @jpbede /homeassistant/components/dlink/ @tkdrob /tests/components/dlink/ @tkdrob -/homeassistant/components/dlna_dmr/ @StevenLooman @chishm -/tests/components/dlna_dmr/ @StevenLooman @chishm +/homeassistant/components/dlna_dmr/ @chishm +/tests/components/dlna_dmr/ @chishm /homeassistant/components/dlna_dms/ @chishm /tests/components/dlna_dms/ @chishm /homeassistant/components/dnsip/ @gjohansson-ST diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 41fa49f1a94..ebbab957700 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -2,7 +2,7 @@ "domain": "dlna_dmr", "name": "DLNA Digital Media Renderer", "after_dependencies": ["media_source"], - "codeowners": ["@StevenLooman", "@chishm"], + "codeowners": ["@chishm"], "config_flow": true, "dependencies": ["ssdp"], "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", From ed451cab3b76ab8ae76e37f1d74e13ba42c0ce6e Mon Sep 17 00:00:00 2001 From: William Easton Date: Sat, 6 Apr 2024 16:39:43 -0500 Subject: [PATCH 354/967] Update Ambient Weather to include Lightning Strike Time and Distance (#114255) * Update sensor.py to include Lightning Distance * Update strings.json to include Lightning Strikes distance * Update homeassistant/components/ambient_station/sensor.py * Update homeassistant/components/ambient_station/strings.json * Update device class lightning distance * Also add Last Lightning strike datetime --------- Co-authored-by: Aaron Bach Co-authored-by: J. Nick Koston --- .../components/ambient_station/sensor.py | 21 ++++++++++++++++++- .../components/ambient_station/strings.json | 6 ++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py index db729197a59..229ebee4fbf 100644 --- a/homeassistant/components/ambient_station/sensor.py +++ b/homeassistant/components/ambient_station/sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations -from datetime import datetime +from datetime import UTC, datetime from homeassistant.components.sensor import ( SensorDeviceClass, @@ -19,6 +19,7 @@ from homeassistant.const import ( LIGHT_LUX, PERCENTAGE, UnitOfIrradiance, + UnitOfLength, UnitOfPrecipitationDepth, UnitOfPressure, UnitOfSpeed, @@ -61,6 +62,8 @@ TYPE_HUMIDITYIN = "humidityin" TYPE_LASTRAIN = "lastRain" TYPE_LIGHTNING_PER_DAY = "lightning_day" TYPE_LIGHTNING_PER_HOUR = "lightning_hour" +TYPE_LASTLIGHTNING_DISTANCE = "lightning_distance" +TYPE_LASTLIGHTNING = "lightning_time" TYPE_MAXDAILYGUST = "maxdailygust" TYPE_MONTHLYRAININ = "monthlyrainin" TYPE_PM25 = "pm25" @@ -296,6 +299,18 @@ SENSOR_DESCRIPTIONS = ( native_unit_of_measurement="strikes", state_class=SensorStateClass.TOTAL, ), + SensorEntityDescription( + key=TYPE_LASTLIGHTNING, + translation_key="last_lightning_strike", + device_class=SensorDeviceClass.TIMESTAMP, + ), + SensorEntityDescription( + key=TYPE_LASTLIGHTNING_DISTANCE, + translation_key="last_lightning_strike_distance", + native_unit_of_measurement=UnitOfLength.MILES, + device_class=SensorDeviceClass.DISTANCE, + state_class=SensorStateClass.MEASUREMENT, + ), SensorEntityDescription( key=TYPE_MAXDAILYGUST, translation_key="max_gust", @@ -685,5 +700,9 @@ class AmbientWeatherSensor(AmbientWeatherEntity, SensorEntity): raw = self._ambient.stations[self._mac_address][ATTR_LAST_DATA][key] if key == TYPE_LASTRAIN: self._attr_native_value = datetime.strptime(raw, "%Y-%m-%dT%H:%M:%S.%f%z") + elif key == TYPE_LASTLIGHTNING: + self._attr_native_value = datetime.fromtimestamp( + raw / 1000, tz=UTC + ) # Ambient uses millisecond epoch else: self._attr_native_value = raw diff --git a/homeassistant/components/ambient_station/strings.json b/homeassistant/components/ambient_station/strings.json index 02bceda500f..25006dce0e9 100644 --- a/homeassistant/components/ambient_station/strings.json +++ b/homeassistant/components/ambient_station/strings.json @@ -219,6 +219,12 @@ "last_rain": { "name": "Last rain" }, + "last_lightning_strike": { + "name": "Last Lightning strike" + }, + "last_lightning_strike_distance": { + "name": "Last Lightning strike distance" + }, "lightning_strikes_per_day": { "name": "Lightning strikes per day" }, From cce8e4839aae6f04c78fdd94f9be066152826f73 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Sun, 7 Apr 2024 00:06:30 +0200 Subject: [PATCH 355/967] Add reconfigure step for waze_travel_time (#114885) * Add reconfigure step for waze_travel_time * Add reconfigure_successful string * Update tests/components/waze_travel_time/test_config_flow.py --------- Co-authored-by: G Johansson --- .../waze_travel_time/config_flow.py | 32 +++++++++++++- .../components/waze_travel_time/strings.json | 3 +- .../waze_travel_time/test_config_flow.py | 44 +++++++++++++++++++ 3 files changed, 77 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/waze_travel_time/config_flow.py b/homeassistant/components/waze_travel_time/config_flow.py index a196c5f4f57..d0f63b97b78 100644 --- a/homeassistant/components/waze_travel_time/config_flow.py +++ b/homeassistant/components/waze_travel_time/config_flow.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import Any + import voluptuous as vol from homeassistant.config_entries import ( @@ -119,6 +121,10 @@ class WazeConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 + def __init__(self) -> None: + """Init Config Flow.""" + self._entry: ConfigEntry | None = None + @staticmethod @callback def async_get_options_flow( @@ -127,7 +133,9 @@ class WazeConfigFlow(ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return WazeOptionsFlow(config_entry) - async def async_step_user(self, user_input=None) -> ConfigFlowResult: + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: """Handle the initial step.""" errors = {} user_input = user_input or {} @@ -140,6 +148,13 @@ class WazeConfigFlow(ConfigFlow, domain=DOMAIN): user_input[CONF_DESTINATION], user_input[CONF_REGION], ): + if self._entry: + return self.async_update_reload_and_abort( + self._entry, + title=user_input[CONF_NAME], + data=user_input, + reason="reconfigure_successful", + ) return self.async_create_entry( title=user_input.get(CONF_NAME, DEFAULT_NAME), data=user_input, @@ -155,3 +170,18 @@ class WazeConfigFlow(ConfigFlow, domain=DOMAIN): data_schema=self.add_suggested_values_to_schema(CONFIG_SCHEMA, user_input), errors=errors, ) + + async def async_step_reconfigure( + self, _: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle reconfiguration.""" + self._entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + assert self._entry + + data = self._entry.data.copy() + data[CONF_REGION] = data[CONF_REGION].lower() + + return self.async_show_form( + step_id="user", + data_schema=self.add_suggested_values_to_schema(CONFIG_SCHEMA, data), + ) diff --git a/homeassistant/components/waze_travel_time/strings.json b/homeassistant/components/waze_travel_time/strings.json index 2a5017a5b9f..e6dd3c3a22e 100644 --- a/homeassistant/components/waze_travel_time/strings.json +++ b/homeassistant/components/waze_travel_time/strings.json @@ -16,7 +16,8 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_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/waze_travel_time/test_config_flow.py b/tests/components/waze_travel_time/test_config_flow.py index 6d155c4e79b..5b1e3417bfc 100644 --- a/tests/components/waze_travel_time/test_config_flow.py +++ b/tests/components/waze_travel_time/test_config_flow.py @@ -53,6 +53,50 @@ async def test_minimum_fields(hass: HomeAssistant) -> None: } +@pytest.mark.usefixtures("mock_update") +async def test_reconfigure(hass: HomeAssistant) -> None: + """Test reconfigure flow.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + options=DEFAULT_OPTIONS, + ) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + reconfigure_result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_RECONFIGURE, + "entry_id": entry.entry_id, + }, + ) + assert reconfigure_result["type"] is FlowResultType.FORM + assert reconfigure_result["step_id"] == "user" + + user_step_result = await hass.config_entries.flow.async_configure( + reconfigure_result["flow_id"], + { + CONF_NAME: DEFAULT_NAME, + CONF_ORIGIN: "location3", + CONF_DESTINATION: "location4", + CONF_REGION: "us", + }, + ) + assert user_step_result["type"] is FlowResultType.ABORT + assert user_step_result["reason"] == "reconfigure_successful" + await hass.async_block_till_done() + + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert entry.data == { + CONF_NAME: DEFAULT_NAME, + CONF_ORIGIN: "location3", + CONF_DESTINATION: "location4", + CONF_REGION: "US", + } + + async def test_options(hass: HomeAssistant) -> None: """Test options flow.""" entry = MockConfigEntry( From 3ef2c464ac477e398efaafe501d43e232534d1d0 Mon Sep 17 00:00:00 2001 From: wittypluck Date: Sun, 7 Apr 2024 00:13:02 +0200 Subject: [PATCH 356/967] Add Glances DiskIO read/write sensors (#114933) --- homeassistant/components/glances/icons.json | 6 + homeassistant/components/glances/sensor.py | 21 +- homeassistant/components/glances/strings.json | 6 + tests/components/glances/__init__.py | 4 + .../glances/snapshots/test_sensor.ambr | 216 ++++++++++++++++++ 5 files changed, 252 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/glances/icons.json b/homeassistant/components/glances/icons.json index 06f8cd98a07..92ef28ad325 100644 --- a/homeassistant/components/glances/icons.json +++ b/homeassistant/components/glances/icons.json @@ -10,6 +10,12 @@ "disk_free": { "default": "mdi:harddisk" }, + "diskio_read": { + "default": "mdi:harddisk" + }, + "diskio_write": { + "default": "mdi:harddisk" + }, "memory_usage": { "default": "mdi:memory" }, diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index 5c22154aeef..fc83d297645 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -14,6 +14,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( PERCENTAGE, REVOLUTIONS_PER_MINUTE, + UnitOfDataRate, UnitOfInformation, UnitOfTemperature, ) @@ -57,6 +58,24 @@ SENSOR_TYPES = { device_class=SensorDeviceClass.DATA_SIZE, state_class=SensorStateClass.MEASUREMENT, ), + ("diskio", "read"): GlancesSensorEntityDescription( + key="read", + type="diskio", + translation_key="diskio_read", + native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, + suggested_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND, + device_class=SensorDeviceClass.DATA_RATE, + state_class=SensorStateClass.MEASUREMENT, + ), + ("diskio", "write"): GlancesSensorEntityDescription( + key="write", + type="diskio", + translation_key="diskio_write", + native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, + suggested_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND, + device_class=SensorDeviceClass.DATA_RATE, + state_class=SensorStateClass.MEASUREMENT, + ), ("mem", "memory_use_percent"): GlancesSensorEntityDescription( key="memory_use_percent", type="mem", @@ -230,7 +249,7 @@ async def async_setup_entry( entities: list[GlancesSensor] = [] for sensor_type, sensors in coordinator.data.items(): - if sensor_type in ["fs", "sensors", "raid"]: + if sensor_type in ["fs", "diskio", "sensors", "raid"]: entities.extend( GlancesSensor( coordinator, diff --git a/homeassistant/components/glances/strings.json b/homeassistant/components/glances/strings.json index 10a4cb7ed00..e2ef185727c 100644 --- a/homeassistant/components/glances/strings.json +++ b/homeassistant/components/glances/strings.json @@ -41,6 +41,12 @@ "disk_free": { "name": "{sensor_label} disk free" }, + "diskio_read": { + "name": "{sensor_label} disk read" + }, + "diskio_write": { + "name": "{sensor_label} disk write" + }, "memory_usage": { "name": "Memory usage" }, diff --git a/tests/components/glances/__init__.py b/tests/components/glances/__init__.py index fd0df3be3a9..c7f2657fb37 100644 --- a/tests/components/glances/__init__.py +++ b/tests/components/glances/__init__.py @@ -181,6 +181,10 @@ HA_SENSOR_DATA: dict[str, Any] = { "/ssl": {"disk_use": 30.7, "disk_use_percent": 6.7, "disk_free": 426.5}, "/media": {"disk_use": 30.7, "disk_use_percent": 6.7, "disk_free": 426.5}, }, + "diskio": { + "nvme0n1": {"read": 184320, "write": 23863296}, + "sda": {"read": 3859, "write": 25954}, + }, "sensors": { "cpu_thermal 1": {"temperature_core": 59}, "err_temp": {"temperature_hdd": "unavailable"}, diff --git a/tests/components/glances/snapshots/test_sensor.ambr b/tests/components/glances/snapshots/test_sensor.ambr index cf74e91f613..d6cccdab4ee 100644 --- a/tests/components/glances/snapshots/test_sensor.ambr +++ b/tests/components/glances/snapshots/test_sensor.ambr @@ -802,6 +802,222 @@ 'state': 'unavailable', }) # --- +# name: test_sensor_states[sensor.0_0_0_0_nvme0n1_disk_read-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.0_0_0_0_nvme0n1_disk_read', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'nvme0n1 disk read', + 'platform': 'glances', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'diskio_read', + 'unique_id': 'test-nvme0n1-read', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_nvme0n1_disk_read-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': '0.0.0.0 nvme0n1 disk read', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.0_0_0_0_nvme0n1_disk_read', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.184320', + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_nvme0n1_disk_write-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.0_0_0_0_nvme0n1_disk_write', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'nvme0n1 disk write', + 'platform': 'glances', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'diskio_write', + 'unique_id': 'test-nvme0n1-write', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_nvme0n1_disk_write-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': '0.0.0.0 nvme0n1 disk write', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.0_0_0_0_nvme0n1_disk_write', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '23.863296', + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_sda_disk_read-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.0_0_0_0_sda_disk_read', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'sda disk read', + 'platform': 'glances', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'diskio_read', + 'unique_id': 'test-sda-read', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_sda_disk_read-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': '0.0.0.0 sda disk read', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.0_0_0_0_sda_disk_read', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.003859', + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_sda_disk_write-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.0_0_0_0_sda_disk_write', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'sda disk write', + 'platform': 'glances', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'diskio_write', + 'unique_id': 'test-sda-write', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_sda_disk_write-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': '0.0.0.0 sda disk write', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.0_0_0_0_sda_disk_write', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.025954', + }) +# --- # name: test_sensor_states[sensor.0_0_0_0_ssl_disk_free-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From c4b5a7c027806735927c6f640904c0331d758235 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 6 Apr 2024 12:35:46 -1000 Subject: [PATCH 357/967] Migrate start helper to use run_immediately (#115055) --- homeassistant/helpers/start.py | 4 +--- tests/components/utility_meter/test_sensor.py | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/helpers/start.py b/homeassistant/helpers/start.py index 92ce8e8cdde..4d07ec213bb 100644 --- a/homeassistant/helpers/start.py +++ b/homeassistant/helpers/start.py @@ -47,9 +47,7 @@ def _async_at_core_state( if unsub: unsub() - unsub = hass.bus.async_listen_once( - event_type, _matched_event, run_immediately=False - ) + unsub = hass.bus.async_listen_once(event_type, _matched_event, run_immediately=True) return cancel diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index 99a63809329..e6abd086a78 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -1206,7 +1206,7 @@ async def test_delta_values( await hass.async_block_till_done() state = hass.states.get("sensor.energy_bill") - assert state.attributes.get("status") == PAUSED + assert state.attributes.get("status") == COLLECTING now += timedelta(seconds=30) with freeze_time(now): @@ -1249,7 +1249,7 @@ async def test_delta_values( state = hass.states.get("sensor.energy_bill") assert state is not None - assert state.state == "9" + assert state.state == "10" @pytest.mark.parametrize( @@ -1316,7 +1316,7 @@ async def test_non_periodically_resetting( await hass.async_block_till_done() state = hass.states.get("sensor.energy_bill") - assert state.attributes.get("status") == PAUSED + assert state.attributes.get("status") == COLLECTING now += timedelta(seconds=30) with freeze_time(now): From 6f783d75bcc93b5f022f584f13232eec33abeaee Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Sun, 7 Apr 2024 08:48:37 +1000 Subject: [PATCH 358/967] Bump aiolifx to 1.0.2 and aiolifx-themes to 0.4.15 (#115059) --- homeassistant/components/lifx/manifest.json | 10 +++++++-- homeassistant/generated/zeroconf.py | 24 +++++++++++++++++++++ requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 4 files changed, 36 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index 39412780331..6aa7fdc6305 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -16,9 +16,11 @@ "homekit": { "models": [ "LIFX A19", + "LIFX A21", "LIFX Beam", "LIFX BR30", "LIFX Candle", + "LIFX Ceiling", "LIFX Clean", "LIFX Color", "LIFX DLCOL", @@ -27,12 +29,16 @@ "LIFX Downlight", "LIFX Filament", "LIFX GU10", + "LIFX Indoor Neon", "LIFX Lightstrip", "LIFX Mini", "LIFX Neon", "LIFX Nightvision", + "LIFX PAR38", "LIFX Pls", "LIFX Plus", + "LIFX Round", + "LIFX Square", "LIFX String", "LIFX Tile", "LIFX White", @@ -42,8 +48,8 @@ "iot_class": "local_polling", "loggers": ["aiolifx", "aiolifx_effects", "bitstring"], "requirements": [ - "aiolifx==1.0.0", + "aiolifx==1.0.2", "aiolifx-effects==0.3.2", - "aiolifx-themes==0.4.10" + "aiolifx-themes==0.4.15" ] } diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 68373fa7fe9..3441026994b 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -64,6 +64,10 @@ HOMEKIT = { "always_discover": True, "domain": "lifx", }, + "LIFX A21": { + "always_discover": True, + "domain": "lifx", + }, "LIFX BR30": { "always_discover": True, "domain": "lifx", @@ -76,6 +80,10 @@ HOMEKIT = { "always_discover": True, "domain": "lifx", }, + "LIFX Ceiling": { + "always_discover": True, + "domain": "lifx", + }, "LIFX Clean": { "always_discover": True, "domain": "lifx", @@ -108,6 +116,10 @@ HOMEKIT = { "always_discover": True, "domain": "lifx", }, + "LIFX Indoor Neon": { + "always_discover": True, + "domain": "lifx", + }, "LIFX Lightstrip": { "always_discover": True, "domain": "lifx", @@ -124,6 +136,10 @@ HOMEKIT = { "always_discover": True, "domain": "lifx", }, + "LIFX PAR38": { + "always_discover": True, + "domain": "lifx", + }, "LIFX Pls": { "always_discover": True, "domain": "lifx", @@ -132,6 +148,14 @@ HOMEKIT = { "always_discover": True, "domain": "lifx", }, + "LIFX Round": { + "always_discover": True, + "domain": "lifx", + }, + "LIFX Square": { + "always_discover": True, + "domain": "lifx", + }, "LIFX String": { "always_discover": True, "domain": "lifx", diff --git a/requirements_all.txt b/requirements_all.txt index 32ff63923b0..18115ed20eb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -288,10 +288,10 @@ aiokef==0.2.16 aiolifx-effects==0.3.2 # homeassistant.components.lifx -aiolifx-themes==0.4.10 +aiolifx-themes==0.4.15 # homeassistant.components.lifx -aiolifx==1.0.0 +aiolifx==1.0.2 # homeassistant.components.livisi aiolivisi==0.0.19 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6bbcffb27fc..08bb89ce0a7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -261,10 +261,10 @@ aiokafka==0.10.0 aiolifx-effects==0.3.2 # homeassistant.components.lifx -aiolifx-themes==0.4.10 +aiolifx-themes==0.4.15 # homeassistant.components.lifx -aiolifx==1.0.0 +aiolifx==1.0.2 # homeassistant.components.livisi aiolivisi==0.0.19 From 553c1479330f3cd836d33c2cc2d453c6660e20ff Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Sat, 6 Apr 2024 18:51:21 -0400 Subject: [PATCH 359/967] Fix sonos switch test failures (#115052) --- tests/components/sonos/conftest.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 4c469028e9a..218ca90a26b 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -8,6 +8,7 @@ from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest from soco import SoCo +from soco.alarms import Alarms from soco.events_base import Event as SonosEvent from homeassistant.components import ssdp, zeroconf @@ -122,6 +123,8 @@ def async_setup_sonos(hass, config_entry, fire_zgs_event): async def _wrapper(): config_entry.add_to_hass(hass) + sonos_alarms = Alarms() + sonos_alarms.last_alarm_list_version = "RINCON_test:0" assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done(wait_background_tasks=True) await fire_zgs_event() From 8e645c9b32854b4a96282002752877d7b4ab6c9e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 6 Apr 2024 13:15:14 -1000 Subject: [PATCH 360/967] Fix flakey cast discovery test (#115063) --- tests/components/cast/test_media_player.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 72eea4c5ff4..5481459b715 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -271,9 +271,11 @@ async def test_start_discovery_called_once( ) -> None: """Test pychromecast.start_discovery called exactly once.""" await async_setup_cast(hass) + await hass.async_block_till_done(wait_background_tasks=True) assert castbrowser_mock.return_value.start_discovery.call_count == 1 await async_setup_cast(hass) + await hass.async_block_till_done(wait_background_tasks=True) assert castbrowser_mock.return_value.start_discovery.call_count == 1 From 8324fd5d1dddda808e9687afb6fb79490cf63855 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 7 Apr 2024 01:15:30 +0200 Subject: [PATCH 361/967] Deprecated old backports and typing aliases (#114883) --- homeassistant/backports/enum.py | 21 +++++-- homeassistant/backports/functools.py | 22 +++++-- homeassistant/helpers/category_registry.py | 6 +- homeassistant/helpers/deprecation.py | 16 ++++- homeassistant/helpers/typing.py | 33 ++++++++-- tests/common.py | 34 +++++++++++ tests/helpers/test_deprecation.py | 70 ++++++++++++++++++---- tests/helpers/test_typing.py | 37 ++++++++++++ tests/test_backports.py | 41 +++++++++++++ 9 files changed, 250 insertions(+), 30 deletions(-) create mode 100644 tests/helpers/test_typing.py create mode 100644 tests/test_backports.py diff --git a/homeassistant/backports/enum.py b/homeassistant/backports/enum.py index 3c09d8e7f57..8b823f47e22 100644 --- a/homeassistant/backports/enum.py +++ b/homeassistant/backports/enum.py @@ -9,8 +9,21 @@ import it. from __future__ import annotations -from enum import StrEnum +from enum import StrEnum as _StrEnum +from functools import partial -__all__ = [ - "StrEnum", -] +from homeassistant.helpers.deprecation import ( + DeprecatedAlias, + all_with_deprecated_constants, + check_if_deprecated_constant, + dir_with_deprecated_constants, +) + +# StrEnum deprecated as of 2024.5 use enum.StrEnum instead. +_DEPRECATED_StrEnum = DeprecatedAlias(_StrEnum, "enum.StrEnum", "2025.5") + +__getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) +__dir__ = partial( + dir_with_deprecated_constants, module_globals_keys=[*globals().keys()] +) +__all__ = all_with_deprecated_constants(globals()) diff --git a/homeassistant/backports/functools.py b/homeassistant/backports/functools.py index 96c9888bd80..bad4236f9c8 100644 --- a/homeassistant/backports/functools.py +++ b/homeassistant/backports/functools.py @@ -9,8 +9,22 @@ import it. from __future__ import annotations -from functools import cached_property +from functools import cached_property as _cached_property, partial -__all__ = [ - "cached_property", -] +from homeassistant.helpers.deprecation import ( + DeprecatedAlias, + all_with_deprecated_constants, + check_if_deprecated_constant, + dir_with_deprecated_constants, +) + +# cached_property deprecated as of 2024.5 use functools.cached_property instead. +_DEPRECATED_cached_property = DeprecatedAlias( + _cached_property, "functools.cached_property", "2025.5" +) + +__getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) +__dir__ = partial( + dir_with_deprecated_constants, module_globals_keys=[*globals().keys()] +) +__all__ = all_with_deprecated_constants(globals()) diff --git a/homeassistant/helpers/category_registry.py b/homeassistant/helpers/category_registry.py index fec87262374..6c7a11cf854 100644 --- a/homeassistant/helpers/category_registry.py +++ b/homeassistant/helpers/category_registry.py @@ -7,12 +7,12 @@ import dataclasses from dataclasses import dataclass, field from typing import Literal, TypedDict, cast -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.util.ulid import ulid_now from .registry import BaseRegistry from .storage import Store -from .typing import UNDEFINED, EventType, UndefinedType +from .typing import UNDEFINED, UndefinedType DATA_REGISTRY = "category_registry" EVENT_CATEGORY_REGISTRY_UPDATED = "category_registry_updated" @@ -28,7 +28,7 @@ class EventCategoryRegistryUpdatedData(TypedDict): category_id: str -EventCategoryRegistryUpdated = EventType[EventCategoryRegistryUpdatedData] +EventCategoryRegistryUpdated = Event[EventCategoryRegistryUpdatedData] @dataclass(slots=True, kw_only=True, frozen=True) diff --git a/homeassistant/helpers/deprecation.py b/homeassistant/helpers/deprecation.py index 6e70bbc7635..93520866142 100644 --- a/homeassistant/helpers/deprecation.py +++ b/homeassistant/helpers/deprecation.py @@ -243,6 +243,14 @@ class DeprecatedConstantEnum(NamedTuple): breaks_in_ha_version: str | None +class DeprecatedAlias(NamedTuple): + """Deprecated alias.""" + + value: Any + replacement: str + breaks_in_ha_version: str | None + + _PREFIX_DEPRECATED = "_DEPRECATED_" @@ -254,6 +262,7 @@ def check_if_deprecated_constant(name: str, module_globals: dict[str, Any]) -> A """ module_name = module_globals.get("__name__") value = replacement = None + description = "constant" if (deprecated_const := module_globals.get(_PREFIX_DEPRECATED + name)) is None: raise AttributeError(f"Module {module_name!r} has no attribute {name!r}") if isinstance(deprecated_const, DeprecatedConstant): @@ -266,6 +275,11 @@ 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): + description = "alias" + value = deprecated_const.value + replacement = deprecated_const.replacement + breaks_in_ha_version = deprecated_const.breaks_in_ha_version if value is None or replacement is None: msg = ( @@ -284,7 +298,7 @@ def check_if_deprecated_constant(name: str, module_globals: dict[str, Any]) -> A name, module_name or __name__, replacement, - "constant", + description, "used", breaks_in_ha_version, log_when_no_integration_is_found=False, diff --git a/homeassistant/helpers/typing.py b/homeassistant/helpers/typing.py index 0f372689809..8b1b4addcdb 100644 --- a/homeassistant/helpers/typing.py +++ b/homeassistant/helpers/typing.py @@ -2,15 +2,22 @@ from collections.abc import Mapping from enum import Enum +from functools import partial from typing import Any, TypeVar import homeassistant.core +from .deprecation import ( + DeprecatedAlias, + all_with_deprecated_constants, + check_if_deprecated_constant, + dir_with_deprecated_constants, +) + _DataT = TypeVar("_DataT") GPSType = tuple[float, float] ConfigType = dict[str, Any] -ContextType = homeassistant.core.Context DiscoveryInfoType = dict[str, Any] ServiceDataType = dict[str, Any] StateType = str | int | float | None @@ -33,7 +40,23 @@ UNDEFINED = UndefinedType._singleton # pylint: disable=protected-access # are not present in the core code base. # They are kept in order not to break custom integrations # that may rely on them. -# In due time they will be removed. -EventType = homeassistant.core.Event -HomeAssistantType = homeassistant.core.HomeAssistant -ServiceCallType = homeassistant.core.ServiceCall +# 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" +) + +# These can be removed if no deprecated constant are in this module anymore +__getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) +__dir__ = partial( + dir_with_deprecated_constants, module_globals_keys=[*globals().keys()] +) +__all__ = all_with_deprecated_constants(globals()) diff --git a/tests/common.py b/tests/common.py index 59b93fc7288..3472da6d1ef 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1642,6 +1642,40 @@ def import_and_test_deprecated_constant( assert constant_name in module.__all__ +def import_and_test_deprecated_alias( + caplog: pytest.LogCaptureFixture, + module: ModuleType, + alias_name: str, + replacement: Any, + breaks_in_ha_version: str, +) -> None: + """Import and test deprecated alias replaced by a value. + + - Import deprecated alias + - Assert value is the same as the replacement + - Assert a warning is logged + - Assert the deprecated alias is included in the modules.__dir__() + - Assert the deprecated alias is included in the modules.__all__() + """ + replacement_name = f"{replacement.__module__}.{replacement.__name__}" + value = import_deprecated_constant(module, alias_name) + assert value == replacement + assert ( + module.__name__, + logging.WARNING, + ( + f"{alias_name} was used from test_constant_deprecation," + f" this is a deprecated alias which will be removed in HA Core {breaks_in_ha_version}. " + f"Use {replacement_name} instead, please report " + "it to the author of the 'test_constant_deprecation' custom integration" + ), + ) in caplog.record_tuples + + # verify deprecated alias is included in dir() + assert alias_name in dir(module) + assert alias_name in module.__all__ + + def help_test_all(module: ModuleType) -> None: """Test module.__all__ is correctly set.""" assert set(module.__all__) == { diff --git a/tests/helpers/test_deprecation.py b/tests/helpers/test_deprecation.py index b53a6d5ec1d..fed48c5735b 100644 --- a/tests/helpers/test_deprecation.py +++ b/tests/helpers/test_deprecation.py @@ -10,6 +10,7 @@ import pytest from homeassistant.core import HomeAssistant from homeassistant.helpers.deprecation import ( + DeprecatedAlias, DeprecatedConstant, DeprecatedConstantEnum, check_if_deprecated_constant, @@ -283,38 +284,59 @@ class TestDeprecatedConstantEnum(StrEnum): TEST = "value" -def _get_value(obj: DeprecatedConstant | DeprecatedConstantEnum | tuple) -> Any: - if isinstance(obj, tuple): - if len(obj) == 2: - return obj[0].value - - return obj[0] - +def _get_value( + obj: DeprecatedConstant + | DeprecatedConstantEnum + | DeprecatedAlias + | tuple[Any, ...], +) -> Any: if isinstance(obj, DeprecatedConstant): return obj.value if isinstance(obj, DeprecatedConstantEnum): return obj.enum.value + if isinstance(obj, DeprecatedAlias): + return obj.value + + if len(obj) == 2: + return obj[0].value + + return obj[0] + @pytest.mark.parametrize( - ("deprecated_constant", "extra_msg"), + ("deprecated_constant", "extra_msg", "description"), [ ( DeprecatedConstant("value", "NEW_CONSTANT", None), ". Use NEW_CONSTANT instead", + "constant", ), ( DeprecatedConstant(1, "NEW_CONSTANT", "2099.1"), " which will be removed in HA Core 2099.1. Use NEW_CONSTANT instead", + "constant", ), ( DeprecatedConstantEnum(TestDeprecatedConstantEnum.TEST, None), ". Use TestDeprecatedConstantEnum.TEST instead", + "constant", ), ( DeprecatedConstantEnum(TestDeprecatedConstantEnum.TEST, "2099.1"), " which will be removed in HA Core 2099.1. Use TestDeprecatedConstantEnum.TEST instead", + "constant", + ), + ( + DeprecatedAlias(1, "new_alias", None), + ". Use new_alias instead", + "alias", + ), + ( + DeprecatedAlias(1, "new_alias", "2099.1"), + " which will be removed in HA Core 2099.1. Use new_alias instead", + "alias", ), ], ) @@ -330,10 +352,14 @@ def _get_value(obj: DeprecatedConstant | DeprecatedConstantEnum | tuple) -> Any: ) def test_check_if_deprecated_constant( caplog: pytest.LogCaptureFixture, - deprecated_constant: DeprecatedConstant | DeprecatedConstantEnum | tuple, + deprecated_constant: DeprecatedConstant + | DeprecatedConstantEnum + | DeprecatedAlias + | tuple, extra_msg: str, module_name: str, extra_extra_msg: str, + description: str, ) -> None: """Test check_if_deprecated_constant.""" module_globals = { @@ -378,28 +404,42 @@ def test_check_if_deprecated_constant( assert ( module_name, logging.WARNING, - f"TEST_CONSTANT was used from hue, this is a deprecated constant{extra_msg}{extra_extra_msg}", + f"TEST_CONSTANT was used from hue, this is a deprecated {description}{extra_msg}{extra_extra_msg}", ) in caplog.record_tuples @pytest.mark.parametrize( - ("deprecated_constant", "extra_msg"), + ("deprecated_constant", "extra_msg", "description"), [ ( DeprecatedConstant("value", "NEW_CONSTANT", None), ". Use NEW_CONSTANT instead", + "constant", ), ( DeprecatedConstant(1, "NEW_CONSTANT", "2099.1"), " which will be removed in HA Core 2099.1. Use NEW_CONSTANT instead", + "constant", ), ( DeprecatedConstantEnum(TestDeprecatedConstantEnum.TEST, None), ". Use TestDeprecatedConstantEnum.TEST instead", + "constant", ), ( DeprecatedConstantEnum(TestDeprecatedConstantEnum.TEST, "2099.1"), " which will be removed in HA Core 2099.1. Use TestDeprecatedConstantEnum.TEST instead", + "constant", + ), + ( + DeprecatedAlias(1, "new_alias", None), + ". Use new_alias instead", + "alias", + ), + ( + DeprecatedAlias(1, "new_alias", "2099.1"), + " which will be removed in HA Core 2099.1. Use new_alias instead", + "alias", ), ], ) @@ -412,9 +452,13 @@ def test_check_if_deprecated_constant( ) def test_check_if_deprecated_constant_integration_not_found( caplog: pytest.LogCaptureFixture, - deprecated_constant: DeprecatedConstant | DeprecatedConstantEnum | tuple, + deprecated_constant: DeprecatedConstant + | DeprecatedConstantEnum + | DeprecatedAlias + | tuple, extra_msg: str, module_name: str, + description: str, ) -> None: """Test check_if_deprecated_constant.""" module_globals = { @@ -432,7 +476,7 @@ def test_check_if_deprecated_constant_integration_not_found( assert ( module_name, logging.WARNING, - f"TEST_CONSTANT is a deprecated constant{extra_msg}", + f"TEST_CONSTANT is a deprecated {description}{extra_msg}", ) not in caplog.record_tuples diff --git a/tests/helpers/test_typing.py b/tests/helpers/test_typing.py new file mode 100644 index 00000000000..5b50a8864de --- /dev/null +++ b/tests/helpers/test_typing.py @@ -0,0 +1,37 @@ +"""Test typing helper module.""" + +from __future__ import annotations + +from typing import Any + +import pytest + +from homeassistant.core import Context, Event, HomeAssistant, ServiceCall +from homeassistant.helpers import typing as ha_typing + +from tests.common import import_and_test_deprecated_alias + + +@pytest.mark.parametrize( + ("alias_name", "replacement", "breaks_in_ha_version"), + [ + ("ContextType", Context, "2025.5"), + ("EventType", Event, "2025.5"), + ("HomeAssistantType", HomeAssistant, "2025.5"), + ("ServiceCallType", ServiceCall, "2025.5"), + ], +) +def test_deprecated_aliases( + caplog: pytest.LogCaptureFixture, + alias_name: str, + replacement: Any, + breaks_in_ha_version: str, +) -> None: + """Test deprecated aliases.""" + import_and_test_deprecated_alias( + caplog, + ha_typing, + alias_name, + replacement, + breaks_in_ha_version, + ) diff --git a/tests/test_backports.py b/tests/test_backports.py new file mode 100644 index 00000000000..09c11da37cb --- /dev/null +++ b/tests/test_backports.py @@ -0,0 +1,41 @@ +"""Test backports package.""" + +from __future__ import annotations + +from enum import StrEnum +from functools import cached_property +from types import ModuleType +from typing import Any + +import pytest + +from homeassistant.backports import ( + enum as backports_enum, + functools as backports_functools, +) + +from tests.common import import_and_test_deprecated_alias + + +@pytest.mark.parametrize( + ("module", "replacement", "breaks_in_ha_version"), + [ + (backports_enum, StrEnum, "2025.5"), + (backports_functools, cached_property, "2025.5"), + ], +) +def test_deprecated_aliases( + caplog: pytest.LogCaptureFixture, + module: ModuleType, + replacement: Any, + breaks_in_ha_version: str, +) -> None: + """Test deprecated aliases.""" + alias_name = replacement.__name__ + import_and_test_deprecated_alias( + caplog, + module, + alias_name, + replacement, + breaks_in_ha_version, + ) From 164d29d4d9703a090db83e295dcc6a6b22868f7b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 6 Apr 2024 13:53:36 -1000 Subject: [PATCH 362/967] Remove prepare override in HomeAssistantQueueHandler (#115064) --- homeassistant/util/logging.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index 09ece063dd0..8709186face 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -23,15 +23,6 @@ class HomeAssistantQueueHandler(logging.handlers.QueueHandler): listener: logging.handlers.QueueListener | None = None - def prepare(self, record: logging.LogRecord) -> logging.LogRecord: - """Prepare a record for queuing. - - This is added as a workaround for https://bugs.python.org/issue46755 - """ - record = super().prepare(record) - record.stack_info = None - return record - def handle(self, record: logging.LogRecord) -> Any: """Conditionally emit the specified logging record. From a0936902c2b11b645da8921a11f92a6192762e8b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 6 Apr 2024 14:31:23 -1000 Subject: [PATCH 363/967] Use identity checks for EntityPlatformState enum (#115067) --- homeassistant/helpers/entity.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 66bbc744b70..c0b301b6fb6 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -1092,7 +1092,7 @@ class Entity( @callback def _async_write_ha_state(self) -> None: """Write the state to the state machine.""" - if self._platform_state == EntityPlatformState.REMOVED: + if self._platform_state is EntityPlatformState.REMOVED: # Polling returned after the entity has already been removed return @@ -1305,7 +1305,7 @@ class Entity( parallel_updates: asyncio.Semaphore | None, ) -> None: """Start adding an entity to a platform.""" - if self._platform_state != EntityPlatformState.NOT_ADDED: + if self._platform_state is not EntityPlatformState.NOT_ADDED: raise HomeAssistantError( f"Entity '{self.entity_id}' cannot be added a second time to an entity" " platform" @@ -1572,7 +1572,7 @@ class Entity( If the entity is not added to a platform it's not safe to call _stringify_state. """ - if self._platform_state != EntityPlatformState.ADDED: + if self._platform_state is not EntityPlatformState.ADDED: return f"" return f"" From cb9352110cab2f16b9d20b98ebc310b8059ce9ba Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 7 Apr 2024 02:34:49 +0200 Subject: [PATCH 364/967] Improve registry store data typing (#115066) --- homeassistant/helpers/area_registry.py | 58 ++++++++++++++-------- homeassistant/helpers/category_registry.py | 20 ++++++-- homeassistant/helpers/device_registry.py | 2 +- homeassistant/helpers/floor_registry.py | 41 ++++++++------- homeassistant/helpers/label_registry.py | 26 +++++++--- homeassistant/helpers/registry.py | 11 ++-- 6 files changed, 104 insertions(+), 54 deletions(-) diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py index 24f58c56d2f..69b405c6af0 100644 --- a/homeassistant/helpers/area_registry.py +++ b/homeassistant/helpers/area_registry.py @@ -26,6 +26,24 @@ STORAGE_VERSION_MAJOR = 1 STORAGE_VERSION_MINOR = 6 +class _AreaStoreData(TypedDict): + """Data type for individual area. Used in AreasRegistryStoreData.""" + + aliases: list[str] + floor_id: str | None + icon: str | None + id: str + labels: list[str] + name: str + picture: str | None + + +class AreasRegistryStoreData(TypedDict): + """Store data type for AreaRegistry.""" + + areas: list[_AreaStoreData] + + class EventAreaRegistryUpdatedData(TypedDict): """EventAreaRegistryUpdated data.""" @@ -45,7 +63,7 @@ class AreaEntry(NormalizedNameBaseRegistryEntry): picture: str | None -class AreaRegistryStore(Store[dict[str, list[dict[str, Any]]]]): +class AreaRegistryStore(Store[AreasRegistryStoreData]): """Store area registry data.""" async def _async_migrate_func( @@ -53,7 +71,7 @@ class AreaRegistryStore(Store[dict[str, list[dict[str, Any]]]]): old_major_version: int, old_minor_version: int, old_data: dict[str, list[dict[str, Any]]], - ) -> dict[str, Any]: + ) -> AreasRegistryStoreData: """Migrate to the new version.""" if old_major_version < 2: if old_minor_version < 2: @@ -84,7 +102,7 @@ class AreaRegistryStore(Store[dict[str, list[dict[str, Any]]]]): if old_major_version > 1: raise NotImplementedError - return old_data + return old_data # type: ignore[return-value] class AreaRegistryItems(NormalizedNameBaseRegistryItems[AreaEntry]): @@ -126,7 +144,7 @@ class AreaRegistryItems(NormalizedNameBaseRegistryItems[AreaEntry]): return [data[key] for key in self._floors_index.get(floor, ())] -class AreaRegistry(BaseRegistry): +class AreaRegistry(BaseRegistry[AreasRegistryStoreData]): """Class to hold a registry of areas.""" areas: AreaRegistryItems @@ -314,24 +332,22 @@ class AreaRegistry(BaseRegistry): self._area_data = areas.data @callback - def _data_to_save(self) -> dict[str, list[dict[str, Any]]]: + def _data_to_save(self) -> AreasRegistryStoreData: """Return data of area registry to store in a file.""" - data = {} - - data["areas"] = [ - { - "aliases": list(entry.aliases), - "floor_id": entry.floor_id, - "icon": entry.icon, - "id": entry.id, - "labels": list(entry.labels), - "name": entry.name, - "picture": entry.picture, - } - for entry in self.areas.values() - ] - - return data + return { + "areas": [ + { + "aliases": list(entry.aliases), + "floor_id": entry.floor_id, + "icon": entry.icon, + "id": entry.id, + "labels": list(entry.labels), + "name": entry.name, + "picture": entry.picture, + } + for entry in self.areas.values() + ] + } def _generate_area_id(self, name: str) -> str: """Generate area ID.""" diff --git a/homeassistant/helpers/category_registry.py b/homeassistant/helpers/category_registry.py index 6c7a11cf854..7d559477f57 100644 --- a/homeassistant/helpers/category_registry.py +++ b/homeassistant/helpers/category_registry.py @@ -20,6 +20,20 @@ STORAGE_KEY = "core.category_registry" STORAGE_VERSION_MAJOR = 1 +class _CategoryStoreData(TypedDict): + """Data type for individual category. Used in CategoryRegistryStoreData.""" + + category_id: str + icon: str | None + name: str + + +class CategoryRegistryStoreData(TypedDict): + """Store data type for CategoryRegistry.""" + + categories: dict[str, list[_CategoryStoreData]] + + class EventCategoryRegistryUpdatedData(TypedDict): """Event data for when the category registry is updated.""" @@ -40,14 +54,14 @@ class CategoryEntry: name: str -class CategoryRegistry(BaseRegistry): +class CategoryRegistry(BaseRegistry[CategoryRegistryStoreData]): """Class to hold a registry of categories by scope.""" def __init__(self, hass: HomeAssistant) -> None: """Initialize the category registry.""" self.hass = hass self.categories: dict[str, dict[str, CategoryEntry]] = {} - self._store: Store[dict[str, dict[str, list[dict[str, str]]]]] = Store( + self._store = Store( hass, STORAGE_VERSION_MAJOR, STORAGE_KEY, @@ -167,7 +181,7 @@ class CategoryRegistry(BaseRegistry): self.categories = category_entries @callback - def _data_to_save(self) -> dict[str, dict[str, list[dict[str, str | None]]]]: + def _data_to_save(self) -> CategoryRegistryStoreData: """Return data of category registry to store in a file.""" return { "categories": { diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index aa172c7e35b..dc3f4bff434 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -551,7 +551,7 @@ class ActiveDeviceRegistryItems(DeviceRegistryItems[DeviceEntry]): ] -class DeviceRegistry(BaseRegistry): +class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): """Class to hold a registry of devices.""" devices: ActiveDeviceRegistryItems diff --git a/homeassistant/helpers/floor_registry.py b/homeassistant/helpers/floor_registry.py index b168b81c1a9..a10d3af6101 100644 --- a/homeassistant/helpers/floor_registry.py +++ b/homeassistant/helpers/floor_registry.py @@ -5,7 +5,7 @@ from __future__ import annotations from collections.abc import Iterable import dataclasses from dataclasses import dataclass -from typing import TYPE_CHECKING, Literal, TypedDict, cast +from typing import Literal, TypedDict, cast from homeassistant.core import Event, HomeAssistant, callback from homeassistant.util import slugify @@ -25,6 +25,22 @@ STORAGE_KEY = "core.floor_registry" STORAGE_VERSION_MAJOR = 1 +class _FloorStoreData(TypedDict): + """Data type for individual floor. Used in FloorRegistryStoreData.""" + + aliases: list[str] + floor_id: str + icon: str | None + level: int | None + name: str + + +class FloorRegistryStoreData(TypedDict): + """Store data type for FloorRegistry.""" + + floors: list[_FloorStoreData] + + class EventFloorRegistryUpdatedData(TypedDict): """Event data for when the floor registry is updated.""" @@ -45,7 +61,7 @@ class FloorEntry(NormalizedNameBaseRegistryEntry): level: int | None = None -class FloorRegistry(BaseRegistry): +class FloorRegistry(BaseRegistry[FloorRegistryStoreData]): """Class to hold a registry of floors.""" floors: NormalizedNameBaseRegistryItems[FloorEntry] @@ -54,13 +70,11 @@ class FloorRegistry(BaseRegistry): def __init__(self, hass: HomeAssistant) -> None: """Initialize the floor registry.""" self.hass = hass - self._store: Store[dict[str, list[dict[str, str | int | list[str] | None]]]] = ( - Store( - hass, - STORAGE_VERSION_MAJOR, - STORAGE_KEY, - atomic_writes=True, - ) + self._store = Store( + hass, + STORAGE_VERSION_MAJOR, + STORAGE_KEY, + atomic_writes=True, ) @callback @@ -190,13 +204,6 @@ class FloorRegistry(BaseRegistry): if data is not None: for floor in data["floors"]: - if TYPE_CHECKING: - assert isinstance(floor["aliases"], list) - assert isinstance(floor["icon"], str) - assert isinstance(floor["level"], int) - assert isinstance(floor["name"], str) - assert isinstance(floor["floor_id"], str) - normalized_name = normalize_name(floor["name"]) floors[floor["floor_id"]] = FloorEntry( aliases=set(floor["aliases"]), @@ -211,7 +218,7 @@ class FloorRegistry(BaseRegistry): self._floor_data = floors.data @callback - def _data_to_save(self) -> dict[str, list[dict[str, str | int | list[str] | None]]]: + def _data_to_save(self) -> FloorRegistryStoreData: """Return data of floor registry to store in a file.""" return { "floors": [ diff --git a/homeassistant/helpers/label_registry.py b/homeassistant/helpers/label_registry.py index b3ca89140a1..e409b41b26f 100644 --- a/homeassistant/helpers/label_registry.py +++ b/homeassistant/helpers/label_registry.py @@ -25,6 +25,22 @@ STORAGE_KEY = "core.label_registry" STORAGE_VERSION_MAJOR = 1 +class _LabelStoreData(TypedDict): + """Data type for individual label. Used in LabelRegistryStoreData.""" + + color: str | None + description: str | None + icon: str | None + label_id: str + name: str + + +class LabelRegistryStoreData(TypedDict): + """Store data type for LabelRegistry.""" + + labels: list[_LabelStoreData] + + class EventLabelRegistryUpdatedData(TypedDict): """Event data for when the label registry is updated.""" @@ -45,7 +61,7 @@ class LabelEntry(NormalizedNameBaseRegistryEntry): icon: str | None = None -class LabelRegistry(BaseRegistry): +class LabelRegistry(BaseRegistry[LabelRegistryStoreData]): """Class to hold a registry of labels.""" labels: NormalizedNameBaseRegistryItems[LabelEntry] @@ -54,7 +70,7 @@ class LabelRegistry(BaseRegistry): def __init__(self, hass: HomeAssistant) -> None: """Initialize the label registry.""" self.hass = hass - self._store: Store[dict[str, list[dict[str, str | None]]]] = Store( + self._store = Store( hass, STORAGE_VERSION_MAJOR, STORAGE_KEY, @@ -189,10 +205,6 @@ class LabelRegistry(BaseRegistry): if data is not None: for label in data["labels"]: - # Check if the necessary keys are present - if label["label_id"] is None or label["name"] is None: - continue - normalized_name = normalize_name(label["name"]) labels[label["label_id"]] = LabelEntry( color=label["color"], @@ -207,7 +219,7 @@ class LabelRegistry(BaseRegistry): self._label_data = labels.data @callback - def _data_to_save(self) -> dict[str, list[dict[str, str | None]]]: + def _data_to_save(self) -> LabelRegistryStoreData: """Return data of label registry to store in a file.""" return { "labels": [ diff --git a/homeassistant/helpers/registry.py b/homeassistant/helpers/registry.py index 0057190848a..832f50661ae 100644 --- a/homeassistant/helpers/registry.py +++ b/homeassistant/helpers/registry.py @@ -4,8 +4,8 @@ from __future__ import annotations from abc import ABC, abstractmethod from collections import UserDict -from collections.abc import ValuesView -from typing import TYPE_CHECKING, Any, Literal, TypeVar +from collections.abc import Mapping, Sequence, ValuesView +from typing import TYPE_CHECKING, Any, Generic, Literal, TypeVar from homeassistant.core import CoreState, HomeAssistant, callback @@ -17,6 +17,7 @@ SAVE_DELAY_LONG = 180 _DataT = TypeVar("_DataT") +_StoreDataT = TypeVar("_StoreDataT", bound=Mapping[str, Any] | Sequence[Any]) class BaseRegistryItems(UserDict[str, _DataT], ABC): @@ -64,11 +65,11 @@ class BaseRegistryItems(UserDict[str, _DataT], ABC): super().__delitem__(key) -class BaseRegistry(ABC): +class BaseRegistry(ABC, Generic[_StoreDataT]): """Class to implement a registry.""" hass: HomeAssistant - _store: Store + _store: Store[_StoreDataT] @callback def async_schedule_save(self) -> None: @@ -80,5 +81,5 @@ class BaseRegistry(ABC): @callback @abstractmethod - def _data_to_save(self) -> dict[str, Any]: + def _data_to_save(self) -> _StoreDataT: """Return data of registry to store in a file.""" From b456a212eb774c2e366369fd0f5aebcd18e8f147 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 6 Apr 2024 21:58:23 -1000 Subject: [PATCH 365/967] Fix flakey tests using the _get_diagnostics_for_config_entry helper (#115069) --- tests/components/diagnostics/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/diagnostics/__init__.py b/tests/components/diagnostics/__init__.py index 81d62e7c2fe..d241ca09f41 100644 --- a/tests/components/diagnostics/__init__.py +++ b/tests/components/diagnostics/__init__.py @@ -19,6 +19,7 @@ async def _get_diagnostics_for_config_entry( ) -> JsonObjectType: """Return the diagnostics config entry for the specified domain.""" assert await async_setup_component(hass, "diagnostics", {}) + await hass.async_block_till_done() client = await hass_client() response = await client.get( From f2fe2c45109b01d779f479a9e866f3c11ec868f7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 6 Apr 2024 22:54:40 -1000 Subject: [PATCH 366/967] Fix synology_dsm availablity (#115073) * Remove reload on update failure from synology_dsm fixes #115062 The coordinator will retry on its own later, there is no reason to reload here. This was added in #42697 * fix available checks --- .../components/synology_dsm/binary_sensor.py | 2 +- homeassistant/components/synology_dsm/camera.py | 2 +- homeassistant/components/synology_dsm/common.py | 13 +------------ homeassistant/components/synology_dsm/sensor.py | 2 +- homeassistant/components/synology_dsm/switch.py | 2 +- homeassistant/components/synology_dsm/update.py | 2 +- 6 files changed, 6 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/synology_dsm/binary_sensor.py b/homeassistant/components/synology_dsm/binary_sensor.py index 7579f350774..28dc750bc91 100644 --- a/homeassistant/components/synology_dsm/binary_sensor.py +++ b/homeassistant/components/synology_dsm/binary_sensor.py @@ -116,7 +116,7 @@ class SynoDSMSecurityBinarySensor(SynoDSMBinarySensor): @property def available(self) -> bool: """Return True if entity is available.""" - return bool(self._api.security) + return bool(self._api.security) and super().available @property def extra_state_attributes(self) -> dict[str, str]: diff --git a/homeassistant/components/synology_dsm/camera.py b/homeassistant/components/synology_dsm/camera.py index 19f95c710d0..82d15138f05 100644 --- a/homeassistant/components/synology_dsm/camera.py +++ b/homeassistant/components/synology_dsm/camera.py @@ -108,7 +108,7 @@ class SynoDSMCamera(SynologyDSMBaseEntity[SynologyDSMCameraUpdateCoordinator], C @property def available(self) -> bool: """Return the availability of the camera.""" - return self.camera_data.is_enabled and self.coordinator.last_update_success + return self.camera_data.is_enabled and super().available @property def is_recording(self) -> bool: diff --git a/homeassistant/components/synology_dsm/common.py b/homeassistant/components/synology_dsm/common.py index d8a2f1ede62..42ec45e94a4 100644 --- a/homeassistant/components/synology_dsm/common.py +++ b/homeassistant/components/synology_dsm/common.py @@ -286,18 +286,7 @@ class SynoApi: async def async_update(self) -> None: """Update function for updating API information.""" - try: - await self._update() - except SYNOLOGY_CONNECTION_EXCEPTIONS as err: - LOGGER.debug( - "Connection error during update of '%s' with exception: %s", - self._entry.unique_id, - err, - ) - LOGGER.warning( - "Connection error during update, fallback by reloading the entry" - ) - await self._hass.config_entries.async_reload(self._entry.entry_id) + await self._update() async def _update(self) -> None: """Update function for updating API information.""" diff --git a/homeassistant/components/synology_dsm/sensor.py b/homeassistant/components/synology_dsm/sensor.py index b742669712e..6769c1e4901 100644 --- a/homeassistant/components/synology_dsm/sensor.py +++ b/homeassistant/components/synology_dsm/sensor.py @@ -367,7 +367,7 @@ class SynoDSMUtilSensor(SynoDSMSensor): @property def available(self) -> bool: """Return True if entity is available.""" - return bool(self._api.utilisation) + return bool(self._api.utilisation) and super().available class SynoDSMStorageSensor(SynologyDSMDeviceEntity, SynoDSMSensor): diff --git a/homeassistant/components/synology_dsm/switch.py b/homeassistant/components/synology_dsm/switch.py index 6e1e38675a0..c19cdb8c815 100644 --- a/homeassistant/components/synology_dsm/switch.py +++ b/homeassistant/components/synology_dsm/switch.py @@ -98,7 +98,7 @@ class SynoDSMSurveillanceHomeModeToggle( @property def available(self) -> bool: """Return True if entity is available.""" - return bool(self._api.surveillance_station) + return bool(self._api.surveillance_station) and super().available @property def device_info(self) -> DeviceInfo: diff --git a/homeassistant/components/synology_dsm/update.py b/homeassistant/components/synology_dsm/update.py index 7b1a36c57b3..c7bcff48cea 100644 --- a/homeassistant/components/synology_dsm/update.py +++ b/homeassistant/components/synology_dsm/update.py @@ -59,7 +59,7 @@ class SynoDSMUpdateEntity( @property def available(self) -> bool: """Return True if entity is available.""" - return bool(self._api.upgrade) + return bool(self._api.upgrade) and super().available @property def installed_version(self) -> str | None: From 32004973c8243a5deca6c4e0ec5cd55cd1c6b02e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 7 Apr 2024 00:17:18 -1000 Subject: [PATCH 367/967] Simplify invalidating the User cache (#115074) --- homeassistant/auth/models.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/auth/models.py b/homeassistant/auth/models.py index 9242c6a67c6..7192f6345e1 100644 --- a/homeassistant/auth/models.py +++ b/homeassistant/auth/models.py @@ -86,11 +86,7 @@ class User: def invalidate_cache(self) -> None: """Invalidate permission and is_admin cache.""" for attr_to_invalidate in ("permissions", "is_admin"): - # try is must more efficient than suppress - try: # noqa: SIM105 - delattr(self, attr_to_invalidate) - except AttributeError: - pass + self.__dict__.pop(attr_to_invalidate, None) @attr.s(slots=True) From 9f2fa7ec19d90db5a6c42a2cb5b5d3f8e62e41ee Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 7 Apr 2024 13:19:40 +0200 Subject: [PATCH 368/967] Add snapshot tests to Bluemaestro (#115094) --- .../bluemaestro/snapshots/test_sensor.ambr | 256 ++++++++++++++++++ tests/components/bluemaestro/test_sensor.py | 28 +- 2 files changed, 274 insertions(+), 10 deletions(-) create mode 100644 tests/components/bluemaestro/snapshots/test_sensor.ambr diff --git a/tests/components/bluemaestro/snapshots/test_sensor.ambr b/tests/components/bluemaestro/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..a86758c709a --- /dev/null +++ b/tests/components/bluemaestro/snapshots/test_sensor.ambr @@ -0,0 +1,256 @@ +# serializer version: 1 +# name: test_sensors[sensor.tempo_disc_thd_eeff_battery-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.tempo_disc_thd_eeff_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': 'bluemaestro', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'aa:bb:cc:dd:ee:ff-battery', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.tempo_disc_thd_eeff_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Tempo Disc THD EEFF Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.tempo_disc_thd_eeff_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_sensors[sensor.tempo_disc_thd_eeff_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.tempo_disc_thd_eeff_dew_point', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Dew Point', + 'platform': 'bluemaestro', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'aa:bb:cc:dd:ee:ff-dew_point', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.tempo_disc_thd_eeff_dew_point-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tempo Disc THD EEFF Dew Point', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tempo_disc_thd_eeff_dew_point', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '13.1', + }) +# --- +# name: test_sensors[sensor.tempo_disc_thd_eeff_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.tempo_disc_thd_eeff_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': 'bluemaestro', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'aa:bb:cc:dd:ee:ff-humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.tempo_disc_thd_eeff_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'Tempo Disc THD EEFF Humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.tempo_disc_thd_eeff_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '49.8', + }) +# --- +# name: test_sensors[sensor.tempo_disc_thd_eeff_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.tempo_disc_thd_eeff_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': 'bluemaestro', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'aa:bb:cc:dd:ee:ff-signal_strength', + 'unit_of_measurement': 'dBm', + }) +# --- +# name: test_sensors[sensor.tempo_disc_thd_eeff_signal_strength-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'signal_strength', + 'friendly_name': 'Tempo Disc THD EEFF Signal Strength', + 'state_class': , + 'unit_of_measurement': 'dBm', + }), + 'context': , + 'entity_id': 'sensor.tempo_disc_thd_eeff_signal_strength', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-60', + }) +# --- +# name: test_sensors[sensor.tempo_disc_thd_eeff_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.tempo_disc_thd_eeff_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': 'bluemaestro', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'aa:bb:cc:dd:ee:ff-temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.tempo_disc_thd_eeff_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Tempo Disc THD EEFF Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.tempo_disc_thd_eeff_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '24.2', + }) +# --- diff --git a/tests/components/bluemaestro/test_sensor.py b/tests/components/bluemaestro/test_sensor.py index fdcb16730ff..a75e390c781 100644 --- a/tests/components/bluemaestro/test_sensor.py +++ b/tests/components/bluemaestro/test_sensor.py @@ -1,9 +1,11 @@ """Test the BlueMaestro sensors.""" +import pytest +from syrupy import SnapshotAssertion + from homeassistant.components.bluemaestro.const import DOMAIN -from homeassistant.components.sensor import ATTR_STATE_CLASS -from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from . import BLUEMAESTRO_SERVICE_INFO @@ -11,7 +13,12 @@ from tests.common import MockConfigEntry from tests.components.bluetooth import inject_bluetooth_service_info -async def test_sensors(hass: HomeAssistant) -> None: +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_sensors( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: """Test setting up creates the sensors.""" entry = MockConfigEntry( domain=DOMAIN, @@ -25,14 +32,15 @@ async def test_sensors(hass: HomeAssistant) -> None: assert len(hass.states.async_all("sensor")) == 0 inject_bluetooth_service_info(hass, BLUEMAESTRO_SERVICE_INFO) await hass.async_block_till_done() - assert len(hass.states.async_all("sensor")) == 4 + assert len(hass.states.async_all("sensor")) == 5 + entity_entries = er.async_entries_for_config_entry(entity_registry, entry.entry_id) - humid_sensor = hass.states.get("sensor.tempo_disc_thd_eeff_temperature") - humid_sensor_attrs = humid_sensor.attributes - assert humid_sensor.state == "24.2" - assert humid_sensor_attrs[ATTR_FRIENDLY_NAME] == "Tempo Disc THD EEFF Temperature" - assert humid_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "°C" - assert humid_sensor_attrs[ATTR_STATE_CLASS] == "measurement" + assert entity_entries + for entity_entry in entity_entries: + assert hass.states.get(entity_entry.entity_id) == snapshot( + name=f"{entity_entry.entity_id}-state" + ) + assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry") assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() From 021eed66f3f292b9706993c9d0c9a1b43a2ab2d1 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 7 Apr 2024 13:36:12 +0200 Subject: [PATCH 369/967] Add more base entities to netatmo (#107862) * Rename netatmo base entity file * Add more Netatmo base entities * Add more Netatmo base entities * Add more Netatmo base entities * Add more Netatmo base entities * Apply suggestions from code review * Add more Netatmo base entities * Add snapshot tests to Netatmo platforms * Add snapshot tests to Netatmo platforms * Fix snapshots * Fix tests * Update snapshots * Add fans * Add fans * Update homeassistant/components/netatmo/select.py Co-authored-by: Tobias Sauerwein * Add snapshot tests to Netatmo platforms * Update snapshots * minor clean up * Fix tests * Fix * Fix * Fix * Move dot split to weather station sensors --------- Co-authored-by: Tobias Sauerwein Co-authored-by: Tobias Sauerwein --- homeassistant/components/netatmo/camera.py | 99 ++++++----- homeassistant/components/netatmo/climate.py | 131 +++++++-------- homeassistant/components/netatmo/const.py | 1 + homeassistant/components/netatmo/cover.py | 39 ++--- homeassistant/components/netatmo/entity.py | 121 ++++++++++---- homeassistant/components/netatmo/fan.py | 34 ++-- homeassistant/components/netatmo/light.py | 92 ++++------- homeassistant/components/netatmo/select.py | 57 ++++--- homeassistant/components/netatmo/sensor.py | 154 +++++++----------- homeassistant/components/netatmo/switch.py | 36 ++-- .../netatmo/snapshots/test_climate.ambr | 20 +-- .../netatmo/snapshots/test_cover.ambr | 8 +- .../netatmo/snapshots/test_fan.ambr | 4 +- .../netatmo/snapshots/test_init.ambr | 94 +++++------ .../netatmo/snapshots/test_light.ambr | 8 +- .../netatmo/snapshots/test_select.ambr | 4 +- .../netatmo/snapshots/test_sensor.ambr | 116 ++++++------- .../netatmo/snapshots/test_switch.ambr | 4 +- 18 files changed, 497 insertions(+), 525 deletions(-) diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index bd12e757359..3bd7bcd859d 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -40,7 +40,7 @@ from .const import ( WEBHOOK_PUSH_TYPE, ) from .data_handler import EVENT, HOME, SIGNAL_NAME, NetatmoDevice -from .entity import NetatmoBaseEntity +from .entity import NetatmoModuleEntity _LOGGER = logging.getLogger(__name__) @@ -80,12 +80,16 @@ async def async_setup_entry( ) -class NetatmoCamera(NetatmoBaseEntity, Camera): +class NetatmoCamera(NetatmoModuleEntity, Camera): """Representation of a Netatmo camera.""" _attr_brand = MANUFACTURER - _attr_has_entity_name = True _attr_supported_features = CameraEntityFeature.STREAM + _attr_configuration_url = CONF_URL_SECURITY + device: NaModules.Camera + _quality = DEFAULT_QUALITY + _monitoring: bool | None = None + _attr_name = None def __init__( self, @@ -93,30 +97,22 @@ class NetatmoCamera(NetatmoBaseEntity, Camera): ) -> None: """Set up for access to the Netatmo camera images.""" Camera.__init__(self) - super().__init__(netatmo_device.data_handler) + super().__init__(netatmo_device) - self._camera = cast(NaModules.Camera, netatmo_device.device) - self._id = self._camera.entity_id - self._home_id = self._camera.home.entity_id - self._device_name = self._camera.name - self._model = self._camera.device_type - self._config_url = CONF_URL_SECURITY - self._attr_unique_id = f"{self._id}-{self._model}" - self._quality = DEFAULT_QUALITY - self._monitoring: bool | None = None + self._attr_unique_id = f"{netatmo_device.device.entity_id}-{self.device_type}" self._light_state = None self._publishers.extend( [ { "name": HOME, - "home_id": self._home_id, - SIGNAL_NAME: f"{HOME}-{self._home_id}", + "home_id": self.home.entity_id, + SIGNAL_NAME: f"{HOME}-{self.home.entity_id}", }, { "name": EVENT, - "home_id": self._home_id, - SIGNAL_NAME: f"{EVENT}-{self._home_id}", + "home_id": self.home.entity_id, + SIGNAL_NAME: f"{EVENT}-{self.home.entity_id}", }, ] ) @@ -134,7 +130,7 @@ class NetatmoCamera(NetatmoBaseEntity, Camera): ) ) - self.hass.data[DOMAIN][DATA_CAMERAS][self._id] = self._device_name + self.hass.data[DOMAIN][DATA_CAMERAS][self.device.entity_id] = self.device.name @callback def handle_event(self, event: dict) -> None: @@ -144,7 +140,10 @@ class NetatmoCamera(NetatmoBaseEntity, Camera): if not data.get("camera_id"): return - if data["home_id"] == self._home_id and data["camera_id"] == self._id: + if ( + data["home_id"] == self.home.entity_id + and data["camera_id"] == self.device.entity_id + ): if data[WEBHOOK_PUSH_TYPE] in ("NACamera-off", "NACamera-disconnection"): self._attr_is_streaming = False self._monitoring = False @@ -168,7 +167,7 @@ class NetatmoCamera(NetatmoBaseEntity, Camera): ) -> bytes | None: """Return a still image response from the camera.""" try: - return cast(bytes, await self._camera.async_get_live_snapshot()) + return cast(bytes, await self.device.async_get_live_snapshot()) except ( aiohttp.ClientPayloadError, aiohttp.ContentTypeError, @@ -183,50 +182,50 @@ class NetatmoCamera(NetatmoBaseEntity, Camera): def supported_features(self) -> CameraEntityFeature: """Return supported features.""" supported_features = CameraEntityFeature.ON_OFF - if self._model != "NDB": + if self.device_type != "NDB": supported_features |= CameraEntityFeature.STREAM return supported_features async def async_turn_off(self) -> None: """Turn off camera.""" - await self._camera.async_monitoring_off() + await self.device.async_monitoring_off() async def async_turn_on(self) -> None: """Turn on camera.""" - await self._camera.async_monitoring_on() + await self.device.async_monitoring_on() async def stream_source(self) -> str: """Return the stream source.""" - if self._camera.is_local: - await self._camera.async_update_camera_urls() + if self.device.is_local: + await self.device.async_update_camera_urls() - if self._camera.local_url: - return f"{self._camera.local_url}/live/files/{self._quality}/index.m3u8" - return f"{self._camera.vpn_url}/live/files/{self._quality}/index.m3u8" + if self.device.local_url: + return f"{self.device.local_url}/live/files/{self._quality}/index.m3u8" + return f"{self.device.vpn_url}/live/files/{self._quality}/index.m3u8" @callback def async_update_callback(self) -> None: """Update the entity's state.""" - self._attr_is_on = self._camera.alim_status is not None - self._attr_available = self._camera.alim_status is not None + self._attr_is_on = self.device.alim_status is not None + self._attr_available = self.device.alim_status is not None - if self._camera.monitoring is not None: - self._attr_is_streaming = self._camera.monitoring - self._attr_motion_detection_enabled = self._camera.monitoring + if self.device.monitoring is not None: + self._attr_is_streaming = self.device.monitoring + self._attr_motion_detection_enabled = self.device.monitoring - self.hass.data[DOMAIN][DATA_EVENTS][self._id] = self.process_events( - self._camera.events + self.hass.data[DOMAIN][DATA_EVENTS][self.device.entity_id] = ( + self.process_events(self.device.events) ) self._attr_extra_state_attributes.update( { - "id": self._id, + "id": self.device.entity_id, "monitoring": self._monitoring, - "sd_status": self._camera.sd_status, - "alim_status": self._camera.alim_status, - "is_local": self._camera.is_local, - "vpn_url": self._camera.vpn_url, - "local_url": self._camera.local_url, + "sd_status": self.device.sd_status, + "alim_status": self.device.alim_status, + "is_local": self.device.is_local, + "vpn_url": self.device.vpn_url, + "local_url": self.device.local_url, "light_state": self._light_state, } ) @@ -249,9 +248,9 @@ class NetatmoCamera(NetatmoBaseEntity, Camera): def get_video_url(self, video_id: str) -> str: """Get video url.""" - if self._camera.is_local: - return f"{self._camera.local_url}/vod/{video_id}/files/{self._quality}/index.m3u8" - return f"{self._camera.vpn_url}/vod/{video_id}/files/{self._quality}/index.m3u8" + if self.device.is_local: + return f"{self.device.local_url}/vod/{video_id}/files/{self._quality}/index.m3u8" + return f"{self.device.vpn_url}/vod/{video_id}/files/{self._quality}/index.m3u8" def fetch_person_ids(self, persons: list[str | None]) -> list[str]: """Fetch matching person ids for given list of persons.""" @@ -260,7 +259,7 @@ class NetatmoCamera(NetatmoBaseEntity, Camera): for person in persons: person_id = None - for pid, data in self._camera.home.persons.items(): + for pid, data in self.home.persons.items(): if data.pseudo == person: person_ids.append(pid) person_id = pid @@ -279,7 +278,7 @@ class NetatmoCamera(NetatmoBaseEntity, Camera): persons = kwargs.get(ATTR_PERSONS, []) person_ids = self.fetch_person_ids(persons) - await self._camera.home.async_set_persons_home(person_ids=person_ids) + await self.home.async_set_persons_home(person_ids=person_ids) _LOGGER.debug("Set %s as at home", persons) async def _service_set_person_away(self, **kwargs: Any) -> None: @@ -288,7 +287,7 @@ class NetatmoCamera(NetatmoBaseEntity, Camera): person_ids = self.fetch_person_ids([person] if person else []) person_id = next(iter(person_ids), None) - await self._camera.home.async_set_persons_away( + await self.home.async_set_persons_away( person_id=person_id, ) @@ -299,11 +298,11 @@ class NetatmoCamera(NetatmoBaseEntity, Camera): async def _service_set_camera_light(self, **kwargs: Any) -> None: """Service to set light mode.""" - if not isinstance(self._camera, NaModules.netatmo.NOC): + if not isinstance(self.device, NaModules.netatmo.NOC): raise HomeAssistantError( - f"{self._model} <{self._device_name}> does not have a floodlight" + f"{self.device_type} <{self.device.name}> does not have a floodlight" ) mode = str(kwargs.get(ATTR_CAMERA_LIGHT_MODE)) _LOGGER.debug("Turn %s camera light for '%s'", mode, self._attr_name) - await self._camera.async_set_floodlight_state(mode) + await self.device.async_set_floodlight_state(mode) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 15bf3291618..e257c7a89ea 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -22,7 +22,6 @@ from homeassistant.components.climate import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_SUGGESTED_AREA, ATTR_TEMPERATURE, PRECISION_HALVES, STATE_OFF, @@ -30,7 +29,6 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_platform -from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt as dt_util @@ -42,7 +40,6 @@ from .const import ( ATTR_SELECTED_SCHEDULE, ATTR_TARGET_TEMPERATURE, ATTR_TIME_PERIOD, - CONF_URL_ENERGY, DATA_SCHEDULES, DOMAIN, EVENT_TYPE_CANCEL_SET_POINT, @@ -57,7 +54,7 @@ from .const import ( SERVICE_SET_TEMPERATURE_WITH_TIME_PERIOD, ) from .data_handler import HOME, SIGNAL_NAME, NetatmoRoom -from .entity import NetatmoBaseEntity +from .entity import NetatmoRoomEntity _LOGGER = logging.getLogger(__name__) @@ -182,7 +179,7 @@ async def async_setup_entry( ) -class NetatmoThermostat(NetatmoBaseEntity, ClimateEntity): +class NetatmoThermostat(NetatmoRoomEntity, ClimateEntity): """Representation a Netatmo thermostat.""" _attr_hvac_mode = HVACMode.AUTO @@ -191,47 +188,37 @@ class NetatmoThermostat(NetatmoBaseEntity, ClimateEntity): _attr_supported_features = SUPPORT_FLAGS _attr_target_temperature_step = PRECISION_HALVES _attr_temperature_unit = UnitOfTemperature.CELSIUS + _attr_name = None + _away: bool | None = None + _connected: bool | None = None _enable_turn_on_off_backwards_compatibility = False - def __init__(self, netatmo_device: NetatmoRoom) -> None: + _away_temperature: float | None = None + _hg_temperature: float | None = None + _boilerstatus: bool | None = None + + def __init__(self, room: NetatmoRoom) -> None: """Initialize the sensor.""" - ClimateEntity.__init__(self) - super().__init__(netatmo_device.data_handler) + super().__init__(room) - self._room = netatmo_device.room - self._id = self._room.entity_id - self._home_id = self._room.home.entity_id - - self._signal_name = f"{HOME}-{self._home_id}" + self._signal_name = f"{HOME}-{self.home.entity_id}" self._publishers.extend( [ { "name": HOME, - "home_id": self._room.home.entity_id, + "home_id": self.home.entity_id, SIGNAL_NAME: self._signal_name, }, ] ) - assert self._room.climate_type - self._model: DeviceType = self._room.climate_type - - self._config_url = CONF_URL_ENERGY - - self._attr_name = self._room.name - self._away: bool | None = None - self._connected: bool | None = None - - self._away_temperature: float | None = None - self._hg_temperature: float | None = None - self._boilerstatus: bool | None = None self._selected_schedule = None self._attr_hvac_modes = [HVACMode.AUTO, HVACMode.HEAT] - if self._model is NA_THERM: + if self.device_type is NA_THERM: self._attr_hvac_modes.append(HVACMode.OFF) - self._attr_unique_id = f"{self._room.entity_id}-{self._model}" + self._attr_unique_id = f"{self.device.entity_id}-{self.device_type}" async def async_added_to_hass(self) -> None: """Entity created.""" @@ -256,12 +243,12 @@ class NetatmoThermostat(NetatmoBaseEntity, ClimateEntity): """Handle webhook events.""" data = event["data"] - if self._room.home.entity_id != data["home_id"]: + if self.home.entity_id != data["home_id"]: return if data["event_type"] == EVENT_TYPE_SCHEDULE and "schedule_id" in data: self._selected_schedule = getattr( - self.hass.data[DOMAIN][DATA_SCHEDULES][self._room.home.entity_id].get( + self.hass.data[DOMAIN][DATA_SCHEDULES][self.home.entity_id].get( data["schedule_id"] ), "name", @@ -276,7 +263,7 @@ class NetatmoThermostat(NetatmoBaseEntity, ClimateEntity): home = data["home"] - if self._room.home.entity_id != home["id"]: + if self.home.entity_id != home["id"]: return if data["event_type"] == EVENT_TYPE_THERM_MODE: @@ -295,7 +282,7 @@ class NetatmoThermostat(NetatmoBaseEntity, ClimateEntity): for room in home.get("rooms", []): if ( data["event_type"] == EVENT_TYPE_SET_POINT - and self._room.entity_id == room["id"] + and self.device.entity_id == room["id"] ): if room["therm_setpoint_mode"] == STATE_NETATMO_OFF: self._attr_hvac_mode = HVACMode.OFF @@ -317,7 +304,7 @@ class NetatmoThermostat(NetatmoBaseEntity, ClimateEntity): if ( data["event_type"] == EVENT_TYPE_CANCEL_SET_POINT - and self._room.entity_id == room["id"] + and self.device.entity_id == room["id"] ): if self._attr_hvac_mode == HVACMode.OFF: self._attr_hvac_mode = HVACMode.AUTO @@ -330,11 +317,11 @@ class NetatmoThermostat(NetatmoBaseEntity, ClimateEntity): @property def hvac_action(self) -> HVACAction: """Return the current running hvac operation if supported.""" - if self._model != NA_VALVE and self._boilerstatus is not None: + if self.device_type != NA_VALVE and self._boilerstatus is not None: return CURRENT_HVAC_MAP_NETATMO[self._boilerstatus] # Maybe it is a valve if ( - heating_req := getattr(self._room, "heating_power_request", 0) + heating_req := getattr(self.device, "heating_power_request", 0) ) is not None and heating_req > 0: return HVACAction.HEATING return HVACAction.IDLE @@ -352,16 +339,17 @@ class NetatmoThermostat(NetatmoBaseEntity, ClimateEntity): """Set new preset mode.""" if ( preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX) - and self._model == NA_VALVE + and self.device_type == NA_VALVE and self._attr_hvac_mode == HVACMode.HEAT ): - await self._room.async_therm_set( + await self.device.async_therm_set( STATE_NETATMO_HOME, ) elif ( - preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX) and self._model == NA_VALVE + preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX) + and self.device_type == NA_VALVE ): - await self._room.async_therm_set( + await self.device.async_therm_set( STATE_NETATMO_MANUAL, DEFAULT_MAX_TEMP, ) @@ -369,11 +357,11 @@ class NetatmoThermostat(NetatmoBaseEntity, ClimateEntity): preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX) and self._attr_hvac_mode == HVACMode.HEAT ): - await self._room.async_therm_set(STATE_NETATMO_HOME) + await self.device.async_therm_set(STATE_NETATMO_HOME) elif preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX): - await self._room.async_therm_set(PRESET_MAP_NETATMO[preset_mode]) + await self.device.async_therm_set(PRESET_MAP_NETATMO[preset_mode]) elif preset_mode in THERM_MODES: - await self._room.home.async_set_thermmode(PRESET_MAP_NETATMO[preset_mode]) + await self.device.home.async_set_thermmode(PRESET_MAP_NETATMO[preset_mode]) else: _LOGGER.error("Preset mode '%s' not available", preset_mode) @@ -381,25 +369,25 @@ class NetatmoThermostat(NetatmoBaseEntity, ClimateEntity): async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature for 2 hours.""" - await self._room.async_therm_set( + await self.device.async_therm_set( STATE_NETATMO_MANUAL, min(kwargs[ATTR_TEMPERATURE], DEFAULT_MAX_TEMP) ) self.async_write_ha_state() async def async_turn_off(self) -> None: """Turn the entity off.""" - if self._model == NA_VALVE: - await self._room.async_therm_set( + if self.device_type == NA_VALVE: + await self.device.async_therm_set( STATE_NETATMO_MANUAL, DEFAULT_MIN_TEMP, ) elif self._attr_hvac_mode != HVACMode.OFF: - await self._room.async_therm_set(STATE_NETATMO_OFF) + await self.device.async_therm_set(STATE_NETATMO_OFF) self.async_write_ha_state() async def async_turn_on(self) -> None: """Turn the entity on.""" - await self._room.async_therm_set(STATE_NETATMO_HOME) + await self.device.async_therm_set(STATE_NETATMO_HOME) self.async_write_ha_state() @property @@ -410,36 +398,36 @@ class NetatmoThermostat(NetatmoBaseEntity, ClimateEntity): @callback def async_update_callback(self) -> None: """Update the entity's state.""" - if not self._room.reachable: + if not self.device.reachable: if self.available: self._connected = False return self._connected = True - self._away_temperature = self._room.home.get_away_temp() - self._hg_temperature = self._room.home.get_hg_temp() - self._attr_current_temperature = self._room.therm_measured_temperature - self._attr_target_temperature = self._room.therm_setpoint_temperature + self._away_temperature = self.home.get_away_temp() + self._hg_temperature = self.home.get_hg_temp() + self._attr_current_temperature = self.device.therm_measured_temperature + self._attr_target_temperature = self.device.therm_setpoint_temperature self._attr_preset_mode = NETATMO_MAP_PRESET[ - getattr(self._room, "therm_setpoint_mode", STATE_NETATMO_SCHEDULE) + getattr(self.device, "therm_setpoint_mode", STATE_NETATMO_SCHEDULE) ] self._attr_hvac_mode = HVAC_MAP_NETATMO[self._attr_preset_mode] self._away = self._attr_hvac_mode == HVAC_MAP_NETATMO[STATE_NETATMO_AWAY] self._selected_schedule = getattr( - self._room.home.get_selected_schedule(), "name", None + self.home.get_selected_schedule(), "name", None ) self._attr_extra_state_attributes[ATTR_SELECTED_SCHEDULE] = ( self._selected_schedule ) - if self._model == NA_VALVE: + if self.device_type == NA_VALVE: self._attr_extra_state_attributes[ATTR_HEATING_POWER_REQUEST] = ( - self._room.heating_power_request + self.device.heating_power_request ) else: - for module in self._room.modules.values(): + for module in self.device.modules.values(): if hasattr(module, "boiler_status"): module = cast(NATherm1, module) if module.boiler_status is not None: @@ -450,7 +438,7 @@ class NetatmoThermostat(NetatmoBaseEntity, ClimateEntity): schedule_name = kwargs.get(ATTR_SCHEDULE_NAME) schedule_id = None for sid, schedule in self.hass.data[DOMAIN][DATA_SCHEDULES][ - self._room.home.entity_id + self.home.entity_id ].items(): if schedule.name == schedule_name: schedule_id = sid @@ -460,10 +448,10 @@ class NetatmoThermostat(NetatmoBaseEntity, ClimateEntity): _LOGGER.error("%s is not a valid schedule", kwargs.get(ATTR_SCHEDULE_NAME)) return - await self._room.home.async_switch_schedule(schedule_id=schedule_id) + await self.home.async_switch_schedule(schedule_id=schedule_id) _LOGGER.debug( "Setting %s schedule to %s (%s)", - self._room.home.entity_id, + self.home.entity_id, kwargs.get(ATTR_SCHEDULE_NAME), schedule_id, ) @@ -475,12 +463,12 @@ class NetatmoThermostat(NetatmoBaseEntity, ClimateEntity): end_datetime = kwargs[ATTR_END_DATETIME] end_timestamp = int(dt_util.as_timestamp(end_datetime)) - await self._room.home.async_set_thermmode( + await self.home.async_set_thermmode( mode=PRESET_MAP_NETATMO[preset_mode], end_time=end_timestamp ) _LOGGER.debug( "Setting %s preset to %s with end datetime %s", - self._room.home.entity_id, + self.home.entity_id, preset_mode, end_timestamp, ) @@ -494,11 +482,11 @@ class NetatmoThermostat(NetatmoBaseEntity, ClimateEntity): _LOGGER.debug( "Setting %s to target temperature %s with end datetime %s", - self._room.entity_id, + self.device.entity_id, target_temperature, end_timestamp, ) - await self._room.async_therm_manual(target_temperature, end_timestamp) + await self.device.async_therm_manual(target_temperature, end_timestamp) async def _async_service_set_temperature_with_time_period( self, **kwargs: Any @@ -508,22 +496,15 @@ class NetatmoThermostat(NetatmoBaseEntity, ClimateEntity): _LOGGER.debug( "Setting %s to target temperature %s with time period %s", - self._room.entity_id, + self.device.entity_id, target_temperature, time_period, ) now_timestamp = dt_util.as_timestamp(dt_util.utcnow()) end_timestamp = int(now_timestamp + time_period.seconds) - await self._room.async_therm_manual(target_temperature, end_timestamp) + await self.device.async_therm_manual(target_temperature, end_timestamp) async def _async_service_clear_temperature_setting(self, **kwargs: Any) -> None: - _LOGGER.debug("Clearing %s temperature setting", self._room.entity_id) - await self._room.async_therm_home() - - @property - def device_info(self) -> DeviceInfo: - """Return the device info for the thermostat.""" - device_info: DeviceInfo = super().device_info - device_info[ATTR_SUGGESTED_AREA] = self._room.name - return device_info + _LOGGER.debug("Clearing %s temperature setting", self.device.entity_id) + await self.device.async_therm_home() diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index 34a5c42038e..8109b418066 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -12,6 +12,7 @@ PLATFORMS = [ Platform.CAMERA, Platform.CLIMATE, Platform.COVER, + Platform.FAN, Platform.LIGHT, Platform.SELECT, Platform.SENSOR, diff --git a/homeassistant/components/netatmo/cover.py b/homeassistant/components/netatmo/cover.py index f2b5c801eec..c34b3a1b47b 100644 --- a/homeassistant/components/netatmo/cover.py +++ b/homeassistant/components/netatmo/cover.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from typing import Any, cast +from typing import Any from pyatmo import modules as NaModules @@ -20,7 +20,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import CONF_URL_CONTROL, NETATMO_CREATE_COVER from .data_handler import HOME, SIGNAL_NAME, NetatmoDevice -from .entity import NetatmoBaseEntity +from .entity import NetatmoModuleEntity _LOGGER = logging.getLogger(__name__) @@ -43,7 +43,7 @@ async def async_setup_entry( ) -class NetatmoCover(NetatmoBaseEntity, CoverEntity): +class NetatmoCover(NetatmoModuleEntity, CoverEntity): """Representation of a Netatmo cover device.""" _attr_supported_features = ( @@ -52,56 +52,51 @@ class NetatmoCover(NetatmoBaseEntity, CoverEntity): | CoverEntityFeature.STOP | CoverEntityFeature.SET_POSITION ) + _attr_configuration_url = CONF_URL_CONTROL _attr_device_class = CoverDeviceClass.SHUTTER + _attr_name = None + device: NaModules.Shutter def __init__(self, netatmo_device: NetatmoDevice) -> None: """Initialize the Netatmo device.""" - super().__init__(netatmo_device.data_handler) + super().__init__(netatmo_device) - self._cover = cast(NaModules.Shutter, netatmo_device.device) + self._attr_is_closed = self.device.current_position == 0 - self._id = self._cover.entity_id - self._attr_name = self._device_name = self._cover.name - self._model = self._cover.device_type - self._config_url = CONF_URL_CONTROL - - self._home_id = self._cover.home.entity_id - self._attr_is_closed = self._cover.current_position == 0 - - self._signal_name = f"{HOME}-{self._home_id}" + self._signal_name = f"{HOME}-{self.home.entity_id}" self._publishers.extend( [ { "name": HOME, - "home_id": self._home_id, + "home_id": self.home.entity_id, SIGNAL_NAME: self._signal_name, }, ] ) - self._attr_unique_id = f"{self._id}-{self._model}" + self._attr_unique_id = f"{self.device.entity_id}-{self.device_type}" async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" - await self._cover.async_close() + await self.device.async_close() self._attr_is_closed = True self.async_write_ha_state() async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" - await self._cover.async_open() + await self.device.async_open() self._attr_is_closed = False self.async_write_ha_state() async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" - await self._cover.async_stop() + await self.device.async_stop() async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the cover shutter to a specific position.""" - await self._cover.async_set_target_position(kwargs[ATTR_POSITION]) + await self.device.async_set_target_position(kwargs[ATTR_POSITION]) @callback def async_update_callback(self) -> None: """Update the entity's state.""" - self._attr_is_closed = self._cover.current_position == 0 - self._attr_current_cover_position = self._cover.current_position + self._attr_is_closed = self.device.current_position == 0 + self._attr_current_cover_position = self.device.current_position diff --git a/homeassistant/components/netatmo/entity.py b/homeassistant/components/netatmo/entity.py index 579d2177824..5f08cb941d6 100644 --- a/homeassistant/components/netatmo/entity.py +++ b/homeassistant/components/netatmo/entity.py @@ -2,39 +2,38 @@ from __future__ import annotations +from abc import abstractmethod from typing import Any -from pyatmo import DeviceType -from pyatmo.modules.device_types import ( - DEVICE_DESCRIPTION_MAP, - DeviceType as NetatmoDeviceType, -) +from pyatmo import DeviceType, Home, Module, Room +from pyatmo.modules.base_class import NetatmoBase +from pyatmo.modules.device_types import DEVICE_DESCRIPTION_MAP 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 .const import DATA_DEVICE_IDS, DEFAULT_ATTRIBUTION, DOMAIN, SIGNAL_NAME -from .data_handler import PUBLIC, NetatmoDataHandler +from .const import ( + CONF_URL_ENERGY, + DATA_DEVICE_IDS, + DEFAULT_ATTRIBUTION, + DOMAIN, + SIGNAL_NAME, +) +from .data_handler import PUBLIC, NetatmoDataHandler, NetatmoDevice, NetatmoRoom class NetatmoBaseEntity(Entity): """Netatmo entity base class.""" _attr_attribution = DEFAULT_ATTRIBUTION + _attr_has_entity_name = True def __init__(self, data_handler: NetatmoDataHandler) -> None: """Set up Netatmo entity base.""" self.data_handler = data_handler self._publishers: list[dict[str, Any]] = [] - - self._device_name: str = "" - self._id: str = "" - self._model: DeviceType - self._config_url: str | None = None - self._attr_name = None - self._attr_unique_id = None self._attr_extra_state_attributes = {} async def async_added_to_hass(self) -> None: @@ -72,10 +71,6 @@ class NetatmoBaseEntity(Entity): ): await self.data_handler.unsubscribe(signal_name, None) - registry = dr.async_get(self.hass) - if device := registry.async_get_device(identifiers={(DOMAIN, self._id)}): - self.hass.data[DOMAIN][DATA_DEVICE_IDS][self._id] = device.id - self.async_update_callback() async def async_will_remove_from_hass(self) -> None: @@ -92,18 +87,82 @@ class NetatmoBaseEntity(Entity): """Update the entity's state.""" raise NotImplementedError + +class NetatmoDeviceEntity(NetatmoBaseEntity): + """Netatmo entity base class.""" + + def __init__(self, data_handler: NetatmoDataHandler, device: NetatmoBase) -> None: + """Set up Netatmo entity base.""" + super().__init__(data_handler) + self.device = device + @property - def device_info(self) -> DeviceInfo: - """Return the device info for the sensor.""" - if "." in self._model: - netatmo_device = NetatmoDeviceType(self._model.partition(".")[2]) - else: - netatmo_device = getattr(NetatmoDeviceType, self._model) - manufacturer, model = DEVICE_DESCRIPTION_MAP[netatmo_device] - return DeviceInfo( - configuration_url=self._config_url, - identifiers={(DOMAIN, self._id)}, - name=self._device_name, - manufacturer=manufacturer, - model=model, + @abstractmethod + def device_type(self) -> DeviceType: + """Return the device type.""" + + @property + def device_description(self) -> tuple[str, str]: + """Return the model of this device.""" + return DEVICE_DESCRIPTION_MAP[self.device_type] + + @property + def home(self) -> Home: + """Return the home this room belongs to.""" + return self.device.home + + +class NetatmoRoomEntity(NetatmoDeviceEntity): + """Netatmo room entity base class.""" + + device: Room + + def __init__(self, room: NetatmoRoom) -> None: + """Set up a Netatmo room entity.""" + super().__init__(room.data_handler, room.room) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, room.room.entity_id)}, + name=room.room.name, + manufacturer=self.device_description[0], + model=self.device_description[1], + configuration_url=CONF_URL_ENERGY, + suggested_area=room.room.name, ) + + async def async_added_to_hass(self) -> None: + """Entity created.""" + await super().async_added_to_hass() + registry = dr.async_get(self.hass) + if device := registry.async_get_device( + identifiers={(DOMAIN, self.device.entity_id)} + ): + self.hass.data[DOMAIN][DATA_DEVICE_IDS][self.device.entity_id] = device.id + + @property + def device_type(self) -> DeviceType: + """Return the device type.""" + assert self.device.climate_type + return self.device.climate_type + + +class NetatmoModuleEntity(NetatmoDeviceEntity): + """Netatmo module entity base class.""" + + device: Module + _attr_configuration_url: str + + def __init__(self, device: NetatmoDevice) -> None: + """Set up a Netatmo module entity.""" + super().__init__(device.data_handler, device.device) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, device.device.entity_id)}, + name=device.device.name, + manufacturer=self.device_description[0], + model=self.device_description[1], + configuration_url=self._attr_configuration_url, + ) + + @property + def device_type(self) -> DeviceType: + """Return the device type.""" + return self.device.device_type diff --git a/homeassistant/components/netatmo/fan.py b/homeassistant/components/netatmo/fan.py index 1b2798dd118..71a8c548622 100644 --- a/homeassistant/components/netatmo/fan.py +++ b/homeassistant/components/netatmo/fan.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from typing import Final, cast +from typing import Final from pyatmo import modules as NaModules @@ -15,7 +15,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import CONF_URL_CONTROL, NETATMO_CREATE_FAN from .data_handler import HOME, SIGNAL_NAME, NetatmoDevice -from .entity import NetatmoBaseEntity +from .entity import NetatmoModuleEntity _LOGGER = logging.getLogger(__name__) @@ -43,46 +43,38 @@ async def async_setup_entry( ) -class NetatmoFan(NetatmoBaseEntity, FanEntity): +class NetatmoFan(NetatmoModuleEntity, FanEntity): """Representation of a Netatmo fan.""" _attr_preset_modes = ["slow", "fast"] _attr_supported_features = FanEntityFeature.PRESET_MODE + _attr_configuration_url = CONF_URL_CONTROL + _attr_name = None + device: NaModules.Fan def __init__(self, netatmo_device: NetatmoDevice) -> None: """Initialize of Netatmo fan.""" - super().__init__(netatmo_device.data_handler) - - self._fan = cast(NaModules.Fan, netatmo_device.device) - - self._id = self._fan.entity_id - self._attr_name = self._device_name = self._fan.name - self._model = self._fan.device_type - self._config_url = CONF_URL_CONTROL - - self._home_id = self._fan.home.entity_id - - self._signal_name = f"{HOME}-{self._home_id}" + super().__init__(netatmo_device) self._publishers.extend( [ { "name": HOME, - "home_id": self._home_id, - SIGNAL_NAME: self._signal_name, + "home_id": self.home.entity_id, + SIGNAL_NAME: f"{HOME}-{self.home.entity_id}", }, ] ) - self._attr_unique_id = f"{self._id}-{self._model}" + self._attr_unique_id = f"{self.device.entity_id}-{self.device_type}" async def async_set_preset_mode(self, preset_mode: str) -> None: """Set the preset mode of the fan.""" - await self._fan.async_set_fan_speed(PRESET_MAPPING[preset_mode]) + await self.device.async_set_fan_speed(PRESET_MAPPING[preset_mode]) @callback def async_update_callback(self) -> None: """Update the entity's state.""" - if self._fan.fan_speed is None: + if self.device.fan_speed is None: self._attr_preset_mode = None return - self._attr_preset_mode = PRESETS.get(self._fan.fan_speed) + self._attr_preset_mode = PRESETS.get(self.device.fan_speed) diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py index 9ccab51f792..b1871e9dabb 100644 --- a/homeassistant/components/netatmo/light.py +++ b/homeassistant/components/netatmo/light.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from typing import Any, cast +from typing import Any from pyatmo import modules as NaModules @@ -24,7 +24,7 @@ from .const import ( WEBHOOK_PUSH_TYPE, ) from .data_handler import HOME, SIGNAL_NAME, NetatmoDevice -from .entity import NetatmoBaseEntity +from .entity import NetatmoModuleEntity _LOGGER = logging.getLogger(__name__) @@ -62,36 +62,28 @@ async def async_setup_entry( ) -class NetatmoCameraLight(NetatmoBaseEntity, LightEntity): +class NetatmoCameraLight(NetatmoModuleEntity, LightEntity): """Representation of a Netatmo Presence camera light.""" + device: NaModules.NOC + _attr_is_on = False + _attr_name = None + _attr_configuration_url = CONF_URL_SECURITY _attr_color_mode = ColorMode.ONOFF _attr_has_entity_name = True _attr_supported_color_modes = {ColorMode.ONOFF} - def __init__( - self, - netatmo_device: NetatmoDevice, - ) -> None: + def __init__(self, netatmo_device: NetatmoDevice) -> None: """Initialize a Netatmo Presence camera light.""" - LightEntity.__init__(self) - super().__init__(netatmo_device.data_handler) + super().__init__(netatmo_device) + self._attr_unique_id = f"{self.device.entity_id}-light" - self._camera = cast(NaModules.NOC, netatmo_device.device) - self._id = self._camera.entity_id - self._home_id = self._camera.home.entity_id - self._device_name = self._camera.name - self._model = self._camera.device_type - self._config_url = CONF_URL_SECURITY - self._is_on = False - self._attr_unique_id = f"{self._id}-light" - - self._signal_name = f"{HOME}-{self._home_id}" + self._signal_name = f"{HOME}-{self.home.entity_id}" self._publishers.extend( [ { "name": HOME, - "home_id": self._camera.home.entity_id, + "home_id": self.home.entity_id, SIGNAL_NAME: self._signal_name, }, ] @@ -118,11 +110,11 @@ class NetatmoCameraLight(NetatmoBaseEntity, LightEntity): return if ( - data["home_id"] == self._home_id - and data["camera_id"] == self._id + data["home_id"] == self.home.entity_id + and data["camera_id"] == self.device.entity_id and data[WEBHOOK_PUSH_TYPE] == WEBHOOK_LIGHT_MODE ): - self._is_on = bool(data["sub_type"] == "on") + self._attr_is_on = bool(data["sub_type"] == "on") self.async_write_ha_state() return @@ -132,59 +124,47 @@ class NetatmoCameraLight(NetatmoBaseEntity, LightEntity): """If the webhook is not established, mark as unavailable.""" return bool(self.data_handler.webhook) - @property - def is_on(self) -> bool: - """Return true if light is on.""" - return self._is_on - async def async_turn_on(self, **kwargs: Any) -> None: """Turn camera floodlight on.""" _LOGGER.debug("Turn camera '%s' on", self.name) - await self._camera.async_floodlight_on() + await self.device.async_floodlight_on() async def async_turn_off(self, **kwargs: Any) -> None: """Turn camera floodlight into auto mode.""" _LOGGER.debug("Turn camera '%s' to auto mode", self.name) - await self._camera.async_floodlight_auto() + await self.device.async_floodlight_auto() @callback def async_update_callback(self) -> None: """Update the entity's state.""" - self._is_on = bool(self._camera.floodlight == "on") + self._attr_is_on = bool(self.device.floodlight == "on") -class NetatmoLight(NetatmoBaseEntity, LightEntity): +class NetatmoLight(NetatmoModuleEntity, LightEntity): """Representation of a dimmable light by Legrand/BTicino.""" - def __init__( - self, - netatmo_device: NetatmoDevice, - ) -> None: + _attr_name = None + _attr_configuration_url = CONF_URL_CONTROL + _attr_brightness: int | None = 0 + device: NaModules.NLFN + + def __init__(self, netatmo_device: NetatmoDevice) -> None: """Initialize a Netatmo light.""" - super().__init__(netatmo_device.data_handler) + super().__init__(netatmo_device) + self._attr_unique_id = f"{self.device.entity_id}-light" - self._dimmer = cast(NaModules.NLFN, netatmo_device.device) - self._id = self._dimmer.entity_id - self._home_id = self._dimmer.home.entity_id - self._device_name = self._dimmer.name - self._attr_name = f"{self._device_name}" - self._model = self._dimmer.device_type - self._config_url = CONF_URL_CONTROL - self._attr_brightness = 0 - self._attr_unique_id = f"{self._id}-light" - - if self._dimmer.brightness is not None: + if self.device.brightness is not None: self._attr_color_mode = ColorMode.BRIGHTNESS else: self._attr_color_mode = ColorMode.ONOFF self._attr_supported_color_modes = {self._attr_color_mode} - self._signal_name = f"{HOME}-{self._home_id}" + self._signal_name = f"{HOME}-{self.home.entity_id}" self._publishers.extend( [ { "name": HOME, - "home_id": self._dimmer.home.entity_id, + "home_id": self.home.entity_id, SIGNAL_NAME: self._signal_name, }, ] @@ -193,27 +173,27 @@ class NetatmoLight(NetatmoBaseEntity, LightEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn light on.""" if ATTR_BRIGHTNESS in kwargs: - await self._dimmer.async_set_brightness(kwargs[ATTR_BRIGHTNESS]) + await self.device.async_set_brightness(kwargs[ATTR_BRIGHTNESS]) else: - await self._dimmer.async_on() + await self.device.async_on() self._attr_is_on = True self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: """Turn light off.""" - await self._dimmer.async_off() + await self.device.async_off() self._attr_is_on = False self.async_write_ha_state() @callback def async_update_callback(self) -> None: """Update the entity's state.""" - self._attr_is_on = self._dimmer.on is True + self._attr_is_on = self.device.on is True - if self._dimmer.brightness is not None: + if (brightness := self.device.brightness) is not None: # Netatmo uses a range of [0, 100] to control brightness - self._attr_brightness = round((self._dimmer.brightness / 100) * 255) + self._attr_brightness = round((brightness / 100) * 255) else: self._attr_brightness = None diff --git a/homeassistant/components/netatmo/select.py b/homeassistant/components/netatmo/select.py index 6680242f579..3fe098a75a9 100644 --- a/homeassistant/components/netatmo/select.py +++ b/homeassistant/components/netatmo/select.py @@ -4,11 +4,10 @@ from __future__ import annotations import logging -from pyatmo import DeviceType - from homeassistant.components.select import SelectEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -17,6 +16,7 @@ from .const import ( DATA_SCHEDULES, DOMAIN, EVENT_TYPE_SCHEDULE, + MANUFACTURER, NETATMO_CREATE_SELECT, ) from .data_handler import HOME, SIGNAL_NAME, NetatmoHome @@ -43,39 +43,36 @@ async def async_setup_entry( class NetatmoScheduleSelect(NetatmoBaseEntity, SelectEntity): """Representation a Netatmo thermostat schedule selector.""" - def __init__( - self, - netatmo_home: NetatmoHome, - ) -> None: + _attr_name = None + + def __init__(self, netatmo_home: NetatmoHome) -> None: """Initialize the select entity.""" - SelectEntity.__init__(self) super().__init__(netatmo_home.data_handler) - self._home = netatmo_home.home - self._home_id = self._home.entity_id + self.home = netatmo_home.home - self._signal_name = netatmo_home.signal_name self._publishers.extend( [ { "name": HOME, - "home_id": self._home.entity_id, - SIGNAL_NAME: self._signal_name, + "home_id": self.home.entity_id, + SIGNAL_NAME: netatmo_home.signal_name, }, ] ) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self.home.entity_id)}, + name=self.home.name, + manufacturer=MANUFACTURER, + model="Climate", + configuration_url=CONF_URL_ENERGY, + ) - self._device_name = self._home.name - self._attr_name = f"{self._device_name}" + self._attr_unique_id = f"{self.home.entity_id}-schedule-select" - self._model = DeviceType.NATherm1 - self._config_url = CONF_URL_ENERGY - - self._attr_unique_id = f"{self._home_id}-schedule-select" - - self._attr_current_option = getattr(self._home.get_selected_schedule(), "name") + self._attr_current_option = getattr(self.home.get_selected_schedule(), "name") self._attr_options = [ - schedule.name for schedule in self._home.schedules.values() + schedule.name for schedule in self.home.schedules.values() ] async def async_added_to_hass(self) -> None: @@ -95,12 +92,12 @@ class NetatmoScheduleSelect(NetatmoBaseEntity, SelectEntity): """Handle webhook events.""" data = event["data"] - if self._home_id != data["home_id"]: + if self.home.entity_id != data["home_id"]: return if data["event_type"] == EVENT_TYPE_SCHEDULE and "schedule_id" in data: self._attr_current_option = getattr( - self.hass.data[DOMAIN][DATA_SCHEDULES][self._home_id].get( + self.hass.data[DOMAIN][DATA_SCHEDULES][self.home.entity_id].get( data["schedule_id"] ), "name", @@ -110,24 +107,26 @@ class NetatmoScheduleSelect(NetatmoBaseEntity, SelectEntity): async def async_select_option(self, option: str) -> None: """Change the selected option.""" for sid, schedule in self.hass.data[DOMAIN][DATA_SCHEDULES][ - self._home_id + self.home.entity_id ].items(): if schedule.name != option: continue _LOGGER.debug( "Setting %s schedule to %s (%s)", - self._home_id, + self.home.entity_id, option, sid, ) - await self._home.async_switch_schedule(schedule_id=sid) + await self.home.async_switch_schedule(schedule_id=sid) break @callback def async_update_callback(self) -> None: """Update the entity's state.""" - self._attr_current_option = getattr(self._home.get_selected_schedule(), "name") - self.hass.data[DOMAIN][DATA_SCHEDULES][self._home_id] = self._home.schedules + self._attr_current_option = getattr(self.home.get_selected_schedule(), "name") + self.hass.data[DOMAIN][DATA_SCHEDULES][self.home.entity_id] = ( + self.home.schedules + ) self._attr_options = [ - schedule.name for schedule in self._home.schedules.values() + schedule.name for schedule in self.home.schedules.values() ] diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 481b0ba86aa..8fe3b79fbac 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -7,6 +7,7 @@ import logging from typing import cast import pyatmo +from pyatmo import DeviceType from homeassistant.components.sensor import ( SensorDeviceClass, @@ -31,7 +32,10 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.device_registry import async_entries_for_config_entry +from homeassistant.helpers.device_registry import ( + DeviceInfo, + async_entries_for_config_entry, +) from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -52,7 +56,7 @@ from .const import ( SIGNAL_NAME, ) from .data_handler import HOME, PUBLIC, NetatmoDataHandler, NetatmoDevice, NetatmoRoom -from .entity import NetatmoBaseEntity +from .entity import NetatmoBaseEntity, NetatmoModuleEntity, NetatmoRoomEntity from .helper import NetatmoArea _LOGGER = logging.getLogger(__name__) @@ -305,11 +309,9 @@ async def async_setup_entry( netatmo_device.device.name, ) async_add_entities( - [ - NetatmoSensor(netatmo_device, description) - for description in SENSOR_TYPES - if description.key in netatmo_device.device.features - ] + NetatmoSensor(netatmo_device, description) + for description in SENSOR_TYPES + if description.key in netatmo_device.device.features ) entry.async_on_unload( @@ -395,11 +397,11 @@ async def async_setup_entry( await add_public_entities(False) -class NetatmoWeatherSensor(NetatmoBaseEntity, SensorEntity): +class NetatmoWeatherSensor(NetatmoModuleEntity, SensorEntity): """Implementation of a Netatmo weather/home coach sensor.""" - _attr_has_entity_name = True entity_description: NetatmoSensorEntityDescription + _attr_configuration_url = CONF_URL_WEATHER def __init__( self, @@ -407,16 +409,9 @@ class NetatmoWeatherSensor(NetatmoBaseEntity, SensorEntity): description: NetatmoSensorEntityDescription, ) -> None: """Initialize the sensor.""" - super().__init__(netatmo_device.data_handler) + super().__init__(netatmo_device) self.entity_description = description - - self._module = netatmo_device.device - self._id = self._module.entity_id - self._station_id = ( - self._module.bridge if self._module.bridge is not None else self._id - ) - self._device_name = self._module.name - category = getattr(self._module.device_category, "name") + category = getattr(self.device.device_category, "name") self._publishers.extend( [ { @@ -425,16 +420,10 @@ class NetatmoWeatherSensor(NetatmoBaseEntity, SensorEntity): }, ] ) + self._attr_unique_id = f"{self.device.entity_id}-{description.key}" - self._attr_name = f"{description.name}" - self._model = self._module.device_type - self._config_url = CONF_URL_WEATHER - self._attr_unique_id = f"{self._id}-{description.key}" - - if hasattr(self._module, "place"): - place = cast( - pyatmo.modules.base_class.Place, getattr(self._module, "place") - ) + if hasattr(self.device, "place"): + place = cast(pyatmo.modules.base_class.Place, getattr(self.device, "place")) if hasattr(place, "location") and place.location is not None: self._attr_extra_state_attributes.update( { @@ -443,12 +432,19 @@ class NetatmoWeatherSensor(NetatmoBaseEntity, SensorEntity): } ) + @property + def device_type(self) -> DeviceType: + """Return the Netatmo device type.""" + if "." not in self.device.device_type: + return super().device_type + return DeviceType(self.device.device_type.partition(".")[2]) + @callback def async_update_callback(self) -> None: """Update the entity's state.""" if ( - not self._module.reachable - or (state := getattr(self._module, self.entity_description.netatmo_name)) + not self.device.reachable + or (state := getattr(self.device, self.entity_description.netatmo_name)) is None ): if self.available: @@ -474,22 +470,18 @@ class NetatmoWeatherSensor(NetatmoBaseEntity, SensorEntity): self.async_write_ha_state() -class NetatmoClimateBatterySensor(NetatmoBaseEntity, SensorEntity): +class NetatmoClimateBatterySensor(NetatmoModuleEntity, SensorEntity): """Implementation of a Netatmo sensor.""" entity_description: NetatmoSensorEntityDescription + device: pyatmo.modules.NRV + _attr_configuration_url = CONF_URL_ENERGY - def __init__( - self, - netatmo_device: NetatmoDevice, - ) -> None: + def __init__(self, netatmo_device: NetatmoDevice) -> None: """Initialize the sensor.""" - super().__init__(netatmo_device.data_handler) + super().__init__(netatmo_device) self.entity_description = BATTERY_SENSOR_DESCRIPTION - self._module = cast(pyatmo.modules.NRV, netatmo_device.device) - self._id = netatmo_device.parent_id - self._publishers.extend( [ { @@ -500,31 +492,32 @@ class NetatmoClimateBatterySensor(NetatmoBaseEntity, SensorEntity): ] ) - self._attr_name = f"{self._module.name} {self.entity_description.name}" - self._room_id = self._module.room_id - self._model = getattr(self._module.device_type, "value") - self._config_url = CONF_URL_ENERGY - - self._attr_unique_id = ( - f"{self._id}-{self._module.entity_id}-{self.entity_description.key}" + self._attr_unique_id = f"{netatmo_device.parent_id}-{self.device.entity_id}-{self.entity_description.key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, netatmo_device.parent_id)}, + name=netatmo_device.device.name, + manufacturer=self.device_description[0], + model=self.device_description[1], + configuration_url=self._attr_configuration_url, ) @callback def async_update_callback(self) -> None: """Update the entity's state.""" - if not self._module.reachable: + if not self.device.reachable: if self.available: self._attr_available = False return self._attr_available = True - self._attr_native_value = self._module.battery + self._attr_native_value = self.device.battery -class NetatmoSensor(NetatmoBaseEntity, SensorEntity): +class NetatmoSensor(NetatmoModuleEntity, SensorEntity): """Implementation of a Netatmo sensor.""" entity_description: NetatmoSensorEntityDescription + _attr_configuration_url = CONF_URL_ENERGY def __init__( self, @@ -532,40 +525,32 @@ class NetatmoSensor(NetatmoBaseEntity, SensorEntity): description: NetatmoSensorEntityDescription, ) -> None: """Initialize the sensor.""" - super().__init__(netatmo_device.data_handler) + super().__init__(netatmo_device) self.entity_description = description - self._module = netatmo_device.device - self._id = self._module.entity_id - self._publishers.extend( [ { "name": HOME, - "home_id": netatmo_device.device.home.entity_id, + "home_id": self.home.entity_id, SIGNAL_NAME: netatmo_device.signal_name, }, ] ) - self._attr_name = f"{self._module.name} {self.entity_description.name}" - self._room_id = self._module.room_id - self._model = getattr(self._module.device_type, "value") - self._config_url = CONF_URL_ENERGY - self._attr_unique_id = ( - f"{self._id}-{self._module.entity_id}-{self.entity_description.key}" + f"{self.device.entity_id}-{self.device.entity_id}-{description.key}" ) @callback def async_update_callback(self) -> None: """Update the entity's state.""" - if not self._module.reachable: + if not self.device.reachable: if self.available: self._attr_available = False return - if (state := getattr(self._module, self.entity_description.key)) is None: + if (state := getattr(self.device, self.entity_description.key)) is None: return self._attr_available = True @@ -609,7 +594,7 @@ def process_wifi(strength: int) -> str: return "Full" -class NetatmoRoomSensor(NetatmoBaseEntity, SensorEntity): +class NetatmoRoomSensor(NetatmoRoomEntity, SensorEntity): """Implementation of a Netatmo room sensor.""" entity_description: NetatmoSensorEntityDescription @@ -620,37 +605,27 @@ class NetatmoRoomSensor(NetatmoBaseEntity, SensorEntity): description: NetatmoSensorEntityDescription, ) -> None: """Initialize the sensor.""" - super().__init__(netatmo_room.data_handler) + super().__init__(netatmo_room) self.entity_description = description - self._room = netatmo_room.room - self._id = self._room.entity_id - self._publishers.extend( [ { "name": HOME, - "home_id": netatmo_room.room.home.entity_id, + "home_id": self.home.entity_id, SIGNAL_NAME: netatmo_room.signal_name, }, ] ) - self._attr_name = f"{self._room.name} {self.entity_description.name}" - self._room_id = self._room.entity_id - self._config_url = CONF_URL_ENERGY - - assert self._room.climate_type - self._model = self._room.climate_type - self._attr_unique_id = ( - f"{self._id}-{self._room.entity_id}-{self.entity_description.key}" + f"{self.device.entity_id}-{self.device.entity_id}-{description.key}" ) @callback def async_update_callback(self) -> None: """Update the entity's state.""" - if (state := getattr(self._room, self.entity_description.key)) is None: + if (state := getattr(self.device, self.entity_description.key)) is None: return self._attr_native_value = state @@ -661,7 +636,6 @@ class NetatmoRoomSensor(NetatmoBaseEntity, SensorEntity): class NetatmoPublicSensor(NetatmoBaseEntity, SensorEntity): """Represent a single sensor in a Netatmo.""" - _attr_has_entity_name = True entity_description: NetatmoSensorEntityDescription def __init__( @@ -691,33 +665,31 @@ class NetatmoPublicSensor(NetatmoBaseEntity, SensorEntity): self.area = area self._mode = area.mode - self._area_name = area.area_name - self._id = self._area_name - self._device_name = f"{self._area_name}" - self._attr_name = f"{description.name}" self._show_on_map = area.show_on_map - self._config_url = CONF_URL_PUBLIC_WEATHER - self._attr_unique_id = ( - f"{self._device_name.replace(' ', '-')}-{description.key}" - ) - self._model = PUBLIC + self._attr_unique_id = f"{area.area_name.replace(' ', '-')}-{description.key}" self._attr_extra_state_attributes.update( { - ATTR_LATITUDE: (self.area.lat_ne + self.area.lat_sw) / 2, - ATTR_LONGITUDE: (self.area.lon_ne + self.area.lon_sw) / 2, + ATTR_LATITUDE: (area.lat_ne + area.lat_sw) / 2, + ATTR_LONGITUDE: (area.lon_ne + area.lon_sw) / 2, } ) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, area.area_name)}, + name=area.area_name, + model="Public Weather station", + manufacturer="Netatmo", + configuration_url=CONF_URL_PUBLIC_WEATHER, + ) async def async_added_to_hass(self) -> None: """Entity created.""" await super().async_added_to_hass() - assert self.device_info and "name" in self.device_info self.async_on_remove( async_dispatcher_connect( self.hass, - f"netatmo-config-{self.device_info['name']}", + f"netatmo-config-{self.area.area_name}", self.async_config_update_callback, ) ) @@ -776,7 +748,7 @@ class NetatmoPublicSensor(NetatmoBaseEntity, SensorEntity): _LOGGER.error( "No station provides %s data in the area %s", self.entity_description.key, - self._area_name, + self.area.area_name, ) self._attr_available = False diff --git a/homeassistant/components/netatmo/switch.py b/homeassistant/components/netatmo/switch.py index 6677adec4b0..6ba4628a358 100644 --- a/homeassistant/components/netatmo/switch.py +++ b/homeassistant/components/netatmo/switch.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from typing import Any, cast +from typing import Any from pyatmo import modules as NaModules @@ -15,7 +15,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import CONF_URL_CONTROL, NETATMO_CREATE_SWITCH from .data_handler import HOME, SIGNAL_NAME, NetatmoDevice -from .entity import NetatmoBaseEntity +from .entity import NetatmoModuleEntity _LOGGER = logging.getLogger(__name__) @@ -38,51 +38,45 @@ async def async_setup_entry( ) -class NetatmoSwitch(NetatmoBaseEntity, SwitchEntity): +class NetatmoSwitch(NetatmoModuleEntity, SwitchEntity): """Representation of a Netatmo switch device.""" + _attr_name = None + _attr_configuration_url = CONF_URL_CONTROL + device: NaModules.Switch + def __init__( self, netatmo_device: NetatmoDevice, ) -> None: """Initialize the Netatmo device.""" - super().__init__(netatmo_device.data_handler) - - self._switch = cast(NaModules.Switch, netatmo_device.device) - - self._id = self._switch.entity_id - self._attr_name = self._device_name = self._switch.name - self._model = self._switch.device_type - self._config_url = CONF_URL_CONTROL - - self._home_id = self._switch.home.entity_id - - self._signal_name = f"{HOME}-{self._home_id}" + super().__init__(netatmo_device) + self._signal_name = f"{HOME}-{self.home.entity_id}" self._publishers.extend( [ { "name": HOME, - "home_id": self._home_id, + "home_id": self.home.entity_id, SIGNAL_NAME: self._signal_name, }, ] ) - self._attr_unique_id = f"{self._id}-{self._model}" - self._attr_is_on = self._switch.on + self._attr_unique_id = f"{self.device.entity_id}-{self.device_type}" + self._attr_is_on = self.device.on @callback def async_update_callback(self) -> None: """Update the entity's state.""" - self._attr_is_on = self._switch.on + self._attr_is_on = self.device.on async def async_turn_on(self, **kwargs: Any) -> None: """Turn the zone on.""" - await self._switch.async_on() + await self.device.async_on() self._attr_is_on = True self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: """Turn the zone off.""" - await self._switch.async_off() + await self.device.async_off() self._attr_is_on = False self.async_write_ha_state() diff --git a/tests/components/netatmo/snapshots/test_climate.ambr b/tests/components/netatmo/snapshots/test_climate.ambr index 327595e90a5..b9a92882b9e 100644 --- a/tests/components/netatmo/snapshots/test_climate.ambr +++ b/tests/components/netatmo/snapshots/test_climate.ambr @@ -26,7 +26,7 @@ 'domain': 'climate', 'entity_category': None, 'entity_id': 'climate.bureau', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -37,7 +37,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Bureau', + 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': , @@ -101,7 +101,7 @@ 'domain': 'climate', 'entity_category': None, 'entity_id': 'climate.cocina', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -112,7 +112,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Cocina', + 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': , @@ -182,7 +182,7 @@ 'domain': 'climate', 'entity_category': None, 'entity_id': 'climate.corridor', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -193,7 +193,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Corridor', + 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': , @@ -262,7 +262,7 @@ 'domain': 'climate', 'entity_category': None, 'entity_id': 'climate.entrada', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -273,7 +273,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Entrada', + 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': , @@ -344,7 +344,7 @@ 'domain': 'climate', 'entity_category': None, 'entity_id': 'climate.livingroom', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -355,7 +355,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Livingroom', + 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': , diff --git a/tests/components/netatmo/snapshots/test_cover.ambr b/tests/components/netatmo/snapshots/test_cover.ambr index e907985ab39..7ea016f5ae8 100644 --- a/tests/components/netatmo/snapshots/test_cover.ambr +++ b/tests/components/netatmo/snapshots/test_cover.ambr @@ -12,7 +12,7 @@ 'domain': 'cover', 'entity_category': None, 'entity_id': 'cover.bubendorff_blind', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -23,7 +23,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Bubendorff blind', + 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': , @@ -62,7 +62,7 @@ 'domain': 'cover', 'entity_category': None, 'entity_id': 'cover.entrance_blinds', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -73,7 +73,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Entrance Blinds', + 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': , diff --git a/tests/components/netatmo/snapshots/test_fan.ambr b/tests/components/netatmo/snapshots/test_fan.ambr index 958a8f79704..ba882d68e50 100644 --- a/tests/components/netatmo/snapshots/test_fan.ambr +++ b/tests/components/netatmo/snapshots/test_fan.ambr @@ -17,7 +17,7 @@ 'domain': 'fan', 'entity_category': None, 'entity_id': 'fan.centralized_ventilation_controler', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -28,7 +28,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Centralized ventilation controler', + 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': , diff --git a/tests/components/netatmo/snapshots/test_init.ambr b/tests/components/netatmo/snapshots/test_init.ambr index febc6f95bc6..8f4b357fc5f 100644 --- a/tests/components/netatmo/snapshots/test_init.ambr +++ b/tests/components/netatmo/snapshots/test_init.ambr @@ -111,7 +111,7 @@ }), 'manufacturer': 'Smarther', 'model': 'Smarther with Netatmo', - 'name': '', + 'name': 'Corridor', 'name_by_user': None, 'serial_number': None, 'suggested_area': 'Corridor', @@ -141,7 +141,7 @@ }), 'manufacturer': 'Legrand', 'model': 'Connected Energy Meter', - 'name': '', + 'name': 'Consumption meter', 'name_by_user': None, 'serial_number': None, 'suggested_area': None, @@ -201,7 +201,7 @@ }), 'manufacturer': 'Legrand', 'model': 'Connected Ecometer', - 'name': '', + 'name': 'Line 1', 'name_by_user': None, 'serial_number': None, 'suggested_area': None, @@ -231,7 +231,7 @@ }), 'manufacturer': 'Legrand', 'model': 'Connected Ecometer', - 'name': '', + 'name': 'Line 2', 'name_by_user': None, 'serial_number': None, 'suggested_area': None, @@ -261,7 +261,7 @@ }), 'manufacturer': 'Legrand', 'model': 'Connected Ecometer', - 'name': '', + 'name': 'Line 3', 'name_by_user': None, 'serial_number': None, 'suggested_area': None, @@ -291,7 +291,7 @@ }), 'manufacturer': 'Legrand', 'model': 'Connected Ecometer', - 'name': '', + 'name': 'Line 4', 'name_by_user': None, 'serial_number': None, 'suggested_area': None, @@ -321,7 +321,7 @@ }), 'manufacturer': 'Legrand', 'model': 'Connected Ecometer', - 'name': '', + 'name': 'Line 5', 'name_by_user': None, 'serial_number': None, 'suggested_area': None, @@ -351,7 +351,7 @@ }), 'manufacturer': 'Legrand', 'model': 'Connected Ecometer', - 'name': '', + 'name': 'Total', 'name_by_user': None, 'serial_number': None, 'suggested_area': None, @@ -381,7 +381,7 @@ }), 'manufacturer': 'Legrand', 'model': 'Connected Ecometer', - 'name': '', + 'name': 'Gas', 'name_by_user': None, 'serial_number': None, 'suggested_area': None, @@ -411,7 +411,7 @@ }), 'manufacturer': 'Legrand', 'model': 'Connected Ecometer', - 'name': '', + 'name': 'Hot water', 'name_by_user': None, 'serial_number': None, 'suggested_area': None, @@ -441,7 +441,7 @@ }), 'manufacturer': 'Legrand', 'model': 'Connected Ecometer', - 'name': '', + 'name': 'Cold water', 'name_by_user': None, 'serial_number': None, 'suggested_area': None, @@ -471,7 +471,7 @@ }), 'manufacturer': 'Legrand', 'model': 'Connected Ecometer', - 'name': '', + 'name': 'Écocompteur', 'name_by_user': None, 'serial_number': None, 'suggested_area': None, @@ -771,7 +771,7 @@ }), 'manufacturer': 'Legrand', 'model': 'Plug', - 'name': '', + 'name': 'Prise', 'name_by_user': None, 'serial_number': None, 'suggested_area': None, @@ -951,7 +951,7 @@ }), 'manufacturer': 'Netatmo', 'model': 'OpenTherm Modulating Thermostat', - 'name': '', + 'name': 'Bureau Modulate', 'name_by_user': None, 'serial_number': None, 'suggested_area': 'Bureau', @@ -981,7 +981,7 @@ }), 'manufacturer': 'Netatmo', 'model': 'Smart Thermostat', - 'name': '', + 'name': 'Livingroom', 'name_by_user': None, 'serial_number': None, 'suggested_area': 'Livingroom', @@ -1011,7 +1011,7 @@ }), 'manufacturer': 'Netatmo', 'model': 'Smart Valve', - 'name': '', + 'name': 'Valve1', 'name_by_user': None, 'serial_number': None, 'suggested_area': 'Entrada', @@ -1041,7 +1041,7 @@ }), 'manufacturer': 'Netatmo', 'model': 'Smart Valve', - 'name': '', + 'name': 'Valve2', 'name_by_user': None, 'serial_number': None, 'suggested_area': 'Cocina', @@ -1049,6 +1049,36 @@ 'via_device_id': None, }) # --- +# name: test_devices[netatmo-91763b24c43d3e344f424e8b] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': 'https://my.netatmo.com/app/energy', + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'netatmo', + '91763b24c43d3e344f424e8b', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Netatmo', + 'model': 'Climate', + 'name': 'MYHOME', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- # name: test_devices[netatmo-Home avg] DeviceRegistryEntrySnapshot({ 'area_id': None, @@ -1109,33 +1139,3 @@ 'via_device_id': None, }) # --- -# name: test_devices[netatmo-] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': 'https://my.netatmo.com/app/energy', - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'netatmo', - '', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Netatmo', - 'model': 'Smart Thermostat', - 'name': 'MYHOME', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, - }) -# --- diff --git a/tests/components/netatmo/snapshots/test_light.ambr b/tests/components/netatmo/snapshots/test_light.ambr index dabc7f8528f..fe5a8aac7d0 100644 --- a/tests/components/netatmo/snapshots/test_light.ambr +++ b/tests/components/netatmo/snapshots/test_light.ambr @@ -16,7 +16,7 @@ 'domain': 'light', 'entity_category': None, 'entity_id': 'light.bathroom_light', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -27,7 +27,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Bathroom light', + 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -127,7 +127,7 @@ 'domain': 'light', 'entity_category': None, 'entity_id': 'light.unknown_00_11_22_33_00_11_45_fe', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -138,7 +138,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Unknown 00:11:22:33:00:11:45:fe', + 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, diff --git a/tests/components/netatmo/snapshots/test_select.ambr b/tests/components/netatmo/snapshots/test_select.ambr index 0a95049957e..ff68fc71c09 100644 --- a/tests/components/netatmo/snapshots/test_select.ambr +++ b/tests/components/netatmo/snapshots/test_select.ambr @@ -17,7 +17,7 @@ 'domain': 'select', 'entity_category': None, 'entity_id': 'select.myhome', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -28,7 +28,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'MYHOME', + 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, diff --git a/tests/components/netatmo/snapshots/test_sensor.ambr b/tests/components/netatmo/snapshots/test_sensor.ambr index df92c644588..0d894ee74f0 100644 --- a/tests/components/netatmo/snapshots/test_sensor.ambr +++ b/tests/components/netatmo/snapshots/test_sensor.ambr @@ -936,7 +936,7 @@ 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.bureau_modulate_battery_percent', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -947,7 +947,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Bureau Modulate Battery Percent', + 'original_name': 'Battery Percent', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -988,7 +988,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.cold_water_power', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -999,7 +999,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Cold water Power', + 'original_name': 'Power', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -1038,7 +1038,7 @@ 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.cold_water_reachability', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -1049,7 +1049,7 @@ }), 'original_device_class': None, 'original_icon': 'mdi:signal', - 'original_name': 'Cold water Reachability', + 'original_name': 'Reachability', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -1076,7 +1076,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.consumption_meter_power', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -1087,7 +1087,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Consumption meter Power', + 'original_name': 'Power', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -1126,7 +1126,7 @@ 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.consumption_meter_reachability', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -1137,7 +1137,7 @@ }), 'original_device_class': None, 'original_icon': 'mdi:signal', - 'original_name': 'Consumption meter Reachability', + 'original_name': 'Reachability', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -1164,7 +1164,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.corridor_humidity', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -1175,7 +1175,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Corridor Humidity', + 'original_name': 'Humidity', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -1216,7 +1216,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.ecocompteur_power', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -1227,7 +1227,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Écocompteur Power', + 'original_name': 'Power', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -1266,7 +1266,7 @@ 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.ecocompteur_reachability', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -1277,7 +1277,7 @@ }), 'original_device_class': None, 'original_icon': 'mdi:signal', - 'original_name': 'Écocompteur Reachability', + 'original_name': 'Reachability', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -1304,7 +1304,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.gas_power', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -1315,7 +1315,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Gas Power', + 'original_name': 'Power', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -1354,7 +1354,7 @@ 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.gas_reachability', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -1365,7 +1365,7 @@ }), 'original_device_class': None, 'original_icon': 'mdi:signal', - 'original_name': 'Gas Reachability', + 'original_name': 'Reachability', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -2350,7 +2350,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.hot_water_power', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -2361,7 +2361,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Hot water Power', + 'original_name': 'Power', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -2400,7 +2400,7 @@ 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.hot_water_reachability', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -2411,7 +2411,7 @@ }), 'original_device_class': None, 'original_icon': 'mdi:signal', - 'original_name': 'Hot water Reachability', + 'original_name': 'Reachability', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -2893,7 +2893,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.line_1_power', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -2904,7 +2904,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Line 1 Power', + 'original_name': 'Power', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -2943,7 +2943,7 @@ 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.line_1_reachability', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -2954,7 +2954,7 @@ }), 'original_device_class': None, 'original_icon': 'mdi:signal', - 'original_name': 'Line 1 Reachability', + 'original_name': 'Reachability', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -2981,7 +2981,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.line_2_power', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -2992,7 +2992,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Line 2 Power', + 'original_name': 'Power', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -3031,7 +3031,7 @@ 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.line_2_reachability', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -3042,7 +3042,7 @@ }), 'original_device_class': None, 'original_icon': 'mdi:signal', - 'original_name': 'Line 2 Reachability', + 'original_name': 'Reachability', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -3069,7 +3069,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.line_3_power', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -3080,7 +3080,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Line 3 Power', + 'original_name': 'Power', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -3119,7 +3119,7 @@ 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.line_3_reachability', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -3130,7 +3130,7 @@ }), 'original_device_class': None, 'original_icon': 'mdi:signal', - 'original_name': 'Line 3 Reachability', + 'original_name': 'Reachability', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -3157,7 +3157,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.line_4_power', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -3168,7 +3168,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Line 4 Power', + 'original_name': 'Power', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -3207,7 +3207,7 @@ 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.line_4_reachability', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -3218,7 +3218,7 @@ }), 'original_device_class': None, 'original_icon': 'mdi:signal', - 'original_name': 'Line 4 Reachability', + 'original_name': 'Reachability', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -3245,7 +3245,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.line_5_power', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -3256,7 +3256,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Line 5 Power', + 'original_name': 'Power', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -3295,7 +3295,7 @@ 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.line_5_reachability', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -3306,7 +3306,7 @@ }), 'original_device_class': None, 'original_icon': 'mdi:signal', - 'original_name': 'Line 5 Reachability', + 'original_name': 'Reachability', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -3333,7 +3333,7 @@ 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.livingroom_battery_percent', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -3344,7 +3344,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Livingroom Battery Percent', + 'original_name': 'Battery Percent', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -4307,7 +4307,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.prise_power', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -4318,7 +4318,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Prise Power', + 'original_name': 'Power', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -4357,7 +4357,7 @@ 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.prise_reachability', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -4368,7 +4368,7 @@ }), 'original_device_class': None, 'original_icon': 'mdi:signal', - 'original_name': 'Prise Reachability', + 'original_name': 'Reachability', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -4395,7 +4395,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.total_power', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -4406,7 +4406,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Total Power', + 'original_name': 'Power', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -4445,7 +4445,7 @@ 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.total_reachability', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -4456,7 +4456,7 @@ }), 'original_device_class': None, 'original_icon': 'mdi:signal', - 'original_name': 'Total Reachability', + 'original_name': 'Reachability', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -4483,7 +4483,7 @@ 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.valve1_battery_percent', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -4494,7 +4494,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Valve1 Battery Percent', + 'original_name': 'Battery Percent', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -4535,7 +4535,7 @@ 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.valve2_battery_percent', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -4546,7 +4546,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Valve2 Battery Percent', + 'original_name': 'Battery Percent', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, diff --git a/tests/components/netatmo/snapshots/test_switch.ambr b/tests/components/netatmo/snapshots/test_switch.ambr index 22c41aefd42..4244917d86f 100644 --- a/tests/components/netatmo/snapshots/test_switch.ambr +++ b/tests/components/netatmo/snapshots/test_switch.ambr @@ -12,7 +12,7 @@ 'domain': 'switch', 'entity_category': None, 'entity_id': 'switch.prise', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -23,7 +23,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Prise', + 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, From df6997bfa9072303d083b64b50b1a75fe2185cdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sun, 7 Apr 2024 14:20:58 +0200 Subject: [PATCH 370/967] Downgrade hass-nabucasa from 0.80.0 to 0.78.0 (#115078) --- 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 efbb401a0de..49a3fc0bf5c 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.80.0"] + "requirements": ["hass-nabucasa==0.78.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 36432285e84..19fb7549150 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -28,7 +28,7 @@ fnv-hash-fast==0.5.0 ha-av==10.1.1 ha-ffmpeg==3.2.0 habluetooth==2.4.2 -hass-nabucasa==0.80.0 +hass-nabucasa==0.78.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 home-assistant-frontend==20240404.1 diff --git a/pyproject.toml b/pyproject.toml index 7fe54efcbe5..f9b289063ed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,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.80.0", + "hass-nabucasa==0.78.0", # 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 5221f8152c2..c5b5e54046d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,7 +17,7 @@ bcrypt==4.1.2 certifi>=2021.5.30 ciso8601==2.3.1 fnv-hash-fast==0.5.0 -hass-nabucasa==0.80.0 +hass-nabucasa==0.78.0 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 18115ed20eb..ccb9ad11b29 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1041,7 +1041,7 @@ habitipy==0.2.0 habluetooth==2.4.2 # homeassistant.components.cloud -hass-nabucasa==0.80.0 +hass-nabucasa==0.78.0 # homeassistant.components.splunk hass-splunk==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 08bb89ce0a7..40d0fd6788d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -852,7 +852,7 @@ habitipy==0.2.0 habluetooth==2.4.2 # homeassistant.components.cloud -hass-nabucasa==0.80.0 +hass-nabucasa==0.78.0 # homeassistant.components.conversation hassil==1.6.1 From 1b07d3ecfa2412f950018f02860fe62badfe44dd Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 7 Apr 2024 14:38:40 +0200 Subject: [PATCH 371/967] Enable entities in Netatmo snapshot test (#115105) --- .../netatmo/snapshots/test_sensor.ambr | 1056 +++++++++++++++-- tests/components/netatmo/test_sensor.py | 1 + 2 files changed, 933 insertions(+), 124 deletions(-) diff --git a/tests/components/netatmo/snapshots/test_sensor.ambr b/tests/components/netatmo/snapshots/test_sensor.ambr index 0d894ee74f0..8a670140617 100644 --- a/tests/components/netatmo/snapshots/test_sensor.ambr +++ b/tests/components/netatmo/snapshots/test_sensor.ambr @@ -277,7 +277,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.baby_bedroom_pressure_trend', @@ -302,7 +302,19 @@ }) # --- # name: test_entity[sensor.baby_bedroom_pressure_trend-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Baby Bedroom Pressure trend', + 'icon': 'mdi:trending-up', + }), + 'context': , + 'entity_id': 'sensor.baby_bedroom_pressure_trend', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) # --- # name: test_entity[sensor.baby_bedroom_reachability-entry] EntityRegistryEntrySnapshot({ @@ -313,7 +325,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.baby_bedroom_reachability', @@ -338,7 +350,21 @@ }) # --- # name: test_entity[sensor.baby_bedroom_reachability-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Baby Bedroom Reachability', + 'icon': 'mdi:signal', + 'latitude': 13.377726, + 'longitude': 52.516263, + }), + 'context': , + 'entity_id': 'sensor.baby_bedroom_reachability', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'True', + }) # --- # name: test_entity[sensor.baby_bedroom_temperature-entry] EntityRegistryEntrySnapshot({ @@ -403,7 +429,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.baby_bedroom_temperature_trend', @@ -428,7 +454,19 @@ }) # --- # name: test_entity[sensor.baby_bedroom_temperature_trend-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Baby Bedroom Temperature trend', + 'icon': 'mdi:trending-up', + }), + 'context': , + 'entity_id': 'sensor.baby_bedroom_temperature_trend', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) # --- # name: test_entity[sensor.baby_bedroom_wifi-entry] EntityRegistryEntrySnapshot({ @@ -439,7 +477,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.baby_bedroom_wifi', @@ -464,7 +502,21 @@ }) # --- # name: test_entity[sensor.baby_bedroom_wifi-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Baby Bedroom Wifi', + 'icon': 'mdi:wifi', + 'latitude': 13.377726, + 'longitude': 52.516263, + }), + 'context': , + 'entity_id': 'sensor.baby_bedroom_wifi', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'High', + }) # --- # name: test_entity[sensor.bedroom_co2-entry] EntityRegistryEntrySnapshot({ @@ -734,7 +786,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.bedroom_pressure_trend', @@ -759,7 +811,19 @@ }) # --- # name: test_entity[sensor.bedroom_pressure_trend-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Bedroom Pressure trend', + 'icon': 'mdi:trending-up', + }), + 'context': , + 'entity_id': 'sensor.bedroom_pressure_trend', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) # --- # name: test_entity[sensor.bedroom_reachability-entry] EntityRegistryEntrySnapshot({ @@ -770,7 +834,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.bedroom_reachability', @@ -795,7 +859,19 @@ }) # --- # name: test_entity[sensor.bedroom_reachability-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Bedroom Reachability', + 'icon': 'mdi:signal', + }), + 'context': , + 'entity_id': 'sensor.bedroom_reachability', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) # --- # name: test_entity[sensor.bedroom_temperature-entry] EntityRegistryEntrySnapshot({ @@ -858,7 +934,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.bedroom_temperature_trend', @@ -883,7 +959,19 @@ }) # --- # name: test_entity[sensor.bedroom_temperature_trend-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Bedroom Temperature trend', + 'icon': 'mdi:trending-up', + }), + 'context': , + 'entity_id': 'sensor.bedroom_temperature_trend', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) # --- # name: test_entity[sensor.bedroom_wifi-entry] EntityRegistryEntrySnapshot({ @@ -894,7 +982,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.bedroom_wifi', @@ -919,7 +1007,19 @@ }) # --- # name: test_entity[sensor.bedroom_wifi-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Bedroom Wifi', + 'icon': 'mdi:wifi', + }), + 'context': , + 'entity_id': 'sensor.bedroom_wifi', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) # --- # name: test_entity[sensor.bureau_modulate_battery_percent-entry] EntityRegistryEntrySnapshot({ @@ -1034,7 +1134,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.cold_water_reachability', @@ -1059,7 +1159,19 @@ }) # --- # name: test_entity[sensor.cold_water_reachability-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Cold water Reachability', + 'icon': 'mdi:signal', + }), + 'context': , + 'entity_id': 'sensor.cold_water_reachability', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) # --- # name: test_entity[sensor.consumption_meter_power-entry] EntityRegistryEntrySnapshot({ @@ -1122,7 +1234,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.consumption_meter_reachability', @@ -1147,7 +1259,19 @@ }) # --- # name: test_entity[sensor.consumption_meter_reachability-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Consumption meter Reachability', + 'icon': 'mdi:signal', + }), + 'context': , + 'entity_id': 'sensor.consumption_meter_reachability', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'True', + }) # --- # name: test_entity[sensor.corridor_humidity-entry] EntityRegistryEntrySnapshot({ @@ -1262,7 +1386,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.ecocompteur_reachability', @@ -1287,7 +1411,19 @@ }) # --- # name: test_entity[sensor.ecocompteur_reachability-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Écocompteur Reachability', + 'icon': 'mdi:signal', + }), + 'context': , + 'entity_id': 'sensor.ecocompteur_reachability', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) # --- # name: test_entity[sensor.gas_power-entry] EntityRegistryEntrySnapshot({ @@ -1350,7 +1486,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.gas_reachability', @@ -1375,7 +1511,19 @@ }) # --- # name: test_entity[sensor.gas_reachability-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Gas Reachability', + 'icon': 'mdi:signal', + }), + 'context': , + 'entity_id': 'sensor.gas_reachability', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) # --- # name: test_entity[sensor.home_avg_angle-entry] EntityRegistryEntrySnapshot({ @@ -1388,7 +1536,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.home_avg_angle', @@ -1413,7 +1561,23 @@ }) # --- # name: test_entity[sensor.home_avg_angle-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Home avg Angle', + 'icon': 'mdi:compass-outline', + 'latitude': 32.17901225, + 'longitude': -117.17901225, + 'state_class': , + 'unit_of_measurement': '°', + }), + 'context': , + 'entity_id': 'sensor.home_avg_angle', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '17.0', + }) # --- # name: test_entity[sensor.home_avg_gust_angle-entry] EntityRegistryEntrySnapshot({ @@ -1426,7 +1590,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.home_avg_gust_angle', @@ -1451,7 +1615,23 @@ }) # --- # name: test_entity[sensor.home_avg_gust_angle-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Home avg Gust Angle', + 'icon': 'mdi:compass-outline', + 'latitude': 32.17901225, + 'longitude': -117.17901225, + 'state_class': , + 'unit_of_measurement': '°', + }), + 'context': , + 'entity_id': 'sensor.home_avg_gust_angle', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '217.0', + }) # --- # name: test_entity[sensor.home_avg_gust_strength-entry] EntityRegistryEntrySnapshot({ @@ -1464,7 +1644,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.home_avg_gust_strength', @@ -1489,7 +1669,23 @@ }) # --- # name: test_entity[sensor.home_avg_gust_strength-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'wind_speed', + 'friendly_name': 'Home avg Gust Strength', + 'latitude': 32.17901225, + 'longitude': -117.17901225, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_avg_gust_strength', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '31.0', + }) # --- # name: test_entity[sensor.home_avg_humidity-entry] EntityRegistryEntrySnapshot({ @@ -1667,7 +1863,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.home_avg_rain_last_hour', @@ -1692,7 +1888,23 @@ }) # --- # name: test_entity[sensor.home_avg_rain_last_hour-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'precipitation', + 'friendly_name': 'Home avg Rain last hour', + 'latitude': 32.17901225, + 'longitude': -117.17901225, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_avg_rain_last_hour', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.1', + }) # --- # name: test_entity[sensor.home_avg_rain_today-entry] EntityRegistryEntrySnapshot({ @@ -1867,7 +2079,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.home_max_angle', @@ -1892,7 +2104,23 @@ }) # --- # name: test_entity[sensor.home_max_angle-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Home max Angle', + 'icon': 'mdi:compass-outline', + 'latitude': 32.17901225, + 'longitude': -117.17901225, + 'state_class': , + 'unit_of_measurement': '°', + }), + 'context': , + 'entity_id': 'sensor.home_max_angle', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '17', + }) # --- # name: test_entity[sensor.home_max_gust_angle-entry] EntityRegistryEntrySnapshot({ @@ -1905,7 +2133,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.home_max_gust_angle', @@ -1930,7 +2158,23 @@ }) # --- # name: test_entity[sensor.home_max_gust_angle-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Home max Gust Angle', + 'icon': 'mdi:compass-outline', + 'latitude': 32.17901225, + 'longitude': -117.17901225, + 'state_class': , + 'unit_of_measurement': '°', + }), + 'context': , + 'entity_id': 'sensor.home_max_gust_angle', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '217', + }) # --- # name: test_entity[sensor.home_max_gust_strength-entry] EntityRegistryEntrySnapshot({ @@ -1943,7 +2187,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.home_max_gust_strength', @@ -1968,7 +2212,23 @@ }) # --- # name: test_entity[sensor.home_max_gust_strength-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'wind_speed', + 'friendly_name': 'Home max Gust Strength', + 'latitude': 32.17901225, + 'longitude': -117.17901225, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_max_gust_strength', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '31', + }) # --- # name: test_entity[sensor.home_max_humidity-entry] EntityRegistryEntrySnapshot({ @@ -2146,7 +2406,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.home_max_rain_last_hour', @@ -2171,7 +2431,23 @@ }) # --- # name: test_entity[sensor.home_max_rain_last_hour-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'precipitation', + 'friendly_name': 'Home max Rain last hour', + 'latitude': 32.17901225, + 'longitude': -117.17901225, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_max_rain_last_hour', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.2', + }) # --- # name: test_entity[sensor.home_max_rain_today-entry] EntityRegistryEntrySnapshot({ @@ -2396,7 +2672,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.hot_water_reachability', @@ -2421,7 +2697,19 @@ }) # --- # name: test_entity[sensor.hot_water_reachability-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Hot water Reachability', + 'icon': 'mdi:signal', + }), + 'context': , + 'entity_id': 'sensor.hot_water_reachability', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) # --- # name: test_entity[sensor.kitchen_co2-entry] EntityRegistryEntrySnapshot({ @@ -2691,7 +2979,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.kitchen_pressure_trend', @@ -2716,7 +3004,19 @@ }) # --- # name: test_entity[sensor.kitchen_pressure_trend-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Kitchen Pressure trend', + 'icon': 'mdi:trending-up', + }), + 'context': , + 'entity_id': 'sensor.kitchen_pressure_trend', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) # --- # name: test_entity[sensor.kitchen_reachability-entry] EntityRegistryEntrySnapshot({ @@ -2727,7 +3027,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.kitchen_reachability', @@ -2752,7 +3052,21 @@ }) # --- # name: test_entity[sensor.kitchen_reachability-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Kitchen Reachability', + 'icon': 'mdi:signal', + 'latitude': 13.377726, + 'longitude': 52.516263, + }), + 'context': , + 'entity_id': 'sensor.kitchen_reachability', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'True', + }) # --- # name: test_entity[sensor.kitchen_temperature-entry] EntityRegistryEntrySnapshot({ @@ -2815,7 +3129,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.kitchen_temperature_trend', @@ -2840,7 +3154,19 @@ }) # --- # name: test_entity[sensor.kitchen_temperature_trend-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Kitchen Temperature trend', + 'icon': 'mdi:trending-up', + }), + 'context': , + 'entity_id': 'sensor.kitchen_temperature_trend', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) # --- # name: test_entity[sensor.kitchen_wifi-entry] EntityRegistryEntrySnapshot({ @@ -2851,7 +3177,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.kitchen_wifi', @@ -2876,7 +3202,21 @@ }) # --- # name: test_entity[sensor.kitchen_wifi-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Kitchen Wifi', + 'icon': 'mdi:wifi', + 'latitude': 13.377726, + 'longitude': 52.516263, + }), + 'context': , + 'entity_id': 'sensor.kitchen_wifi', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Full', + }) # --- # name: test_entity[sensor.line_1_power-entry] EntityRegistryEntrySnapshot({ @@ -2939,7 +3279,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.line_1_reachability', @@ -2964,7 +3304,19 @@ }) # --- # name: test_entity[sensor.line_1_reachability-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Line 1 Reachability', + 'icon': 'mdi:signal', + }), + 'context': , + 'entity_id': 'sensor.line_1_reachability', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) # --- # name: test_entity[sensor.line_2_power-entry] EntityRegistryEntrySnapshot({ @@ -3027,7 +3379,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.line_2_reachability', @@ -3052,7 +3404,19 @@ }) # --- # name: test_entity[sensor.line_2_reachability-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Line 2 Reachability', + 'icon': 'mdi:signal', + }), + 'context': , + 'entity_id': 'sensor.line_2_reachability', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) # --- # name: test_entity[sensor.line_3_power-entry] EntityRegistryEntrySnapshot({ @@ -3115,7 +3479,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.line_3_reachability', @@ -3140,7 +3504,19 @@ }) # --- # name: test_entity[sensor.line_3_reachability-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Line 3 Reachability', + 'icon': 'mdi:signal', + }), + 'context': , + 'entity_id': 'sensor.line_3_reachability', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) # --- # name: test_entity[sensor.line_4_power-entry] EntityRegistryEntrySnapshot({ @@ -3203,7 +3579,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.line_4_reachability', @@ -3228,7 +3604,19 @@ }) # --- # name: test_entity[sensor.line_4_reachability-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Line 4 Reachability', + 'icon': 'mdi:signal', + }), + 'context': , + 'entity_id': 'sensor.line_4_reachability', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) # --- # name: test_entity[sensor.line_5_power-entry] EntityRegistryEntrySnapshot({ @@ -3291,7 +3679,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.line_5_reachability', @@ -3316,7 +3704,19 @@ }) # --- # name: test_entity[sensor.line_5_reachability-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Line 5 Reachability', + 'icon': 'mdi:signal', + }), + 'context': , + 'entity_id': 'sensor.line_5_reachability', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) # --- # name: test_entity[sensor.livingroom_battery_percent-entry] EntityRegistryEntrySnapshot({ @@ -3638,7 +4038,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.livingroom_pressure_trend', @@ -3663,7 +4063,19 @@ }) # --- # name: test_entity[sensor.livingroom_pressure_trend-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Livingroom Pressure trend', + 'icon': 'mdi:trending-up', + }), + 'context': , + 'entity_id': 'sensor.livingroom_pressure_trend', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) # --- # name: test_entity[sensor.livingroom_reachability-entry] EntityRegistryEntrySnapshot({ @@ -3674,7 +4086,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.livingroom_reachability', @@ -3699,7 +4111,21 @@ }) # --- # name: test_entity[sensor.livingroom_reachability-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Livingroom Reachability', + 'icon': 'mdi:signal', + 'latitude': 13.377726, + 'longitude': 52.516263, + }), + 'context': , + 'entity_id': 'sensor.livingroom_reachability', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'True', + }) # --- # name: test_entity[sensor.livingroom_temperature-entry] EntityRegistryEntrySnapshot({ @@ -3762,7 +4188,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.livingroom_temperature_trend', @@ -3787,7 +4213,19 @@ }) # --- # name: test_entity[sensor.livingroom_temperature_trend-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Livingroom Temperature trend', + 'icon': 'mdi:trending-up', + }), + 'context': , + 'entity_id': 'sensor.livingroom_temperature_trend', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) # --- # name: test_entity[sensor.livingroom_wifi-entry] EntityRegistryEntrySnapshot({ @@ -3798,7 +4236,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.livingroom_wifi', @@ -3823,7 +4261,21 @@ }) # --- # name: test_entity[sensor.livingroom_wifi-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Livingroom Wifi', + 'icon': 'mdi:wifi', + 'latitude': 13.377726, + 'longitude': 52.516263, + }), + 'context': , + 'entity_id': 'sensor.livingroom_wifi', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'High', + }) # --- # name: test_entity[sensor.parents_bedroom_co2-entry] EntityRegistryEntrySnapshot({ @@ -4103,7 +4555,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.parents_bedroom_pressure_trend', @@ -4128,7 +4580,19 @@ }) # --- # name: test_entity[sensor.parents_bedroom_pressure_trend-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Parents Bedroom Pressure trend', + 'icon': 'mdi:trending-up', + }), + 'context': , + 'entity_id': 'sensor.parents_bedroom_pressure_trend', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) # --- # name: test_entity[sensor.parents_bedroom_reachability-entry] EntityRegistryEntrySnapshot({ @@ -4139,7 +4603,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.parents_bedroom_reachability', @@ -4164,7 +4628,21 @@ }) # --- # name: test_entity[sensor.parents_bedroom_reachability-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Parents Bedroom Reachability', + 'icon': 'mdi:signal', + 'latitude': 13.377726, + 'longitude': 52.516263, + }), + 'context': , + 'entity_id': 'sensor.parents_bedroom_reachability', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'True', + }) # --- # name: test_entity[sensor.parents_bedroom_temperature-entry] EntityRegistryEntrySnapshot({ @@ -4229,7 +4707,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.parents_bedroom_temperature_trend', @@ -4254,7 +4732,19 @@ }) # --- # name: test_entity[sensor.parents_bedroom_temperature_trend-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Parents Bedroom Temperature trend', + 'icon': 'mdi:trending-up', + }), + 'context': , + 'entity_id': 'sensor.parents_bedroom_temperature_trend', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) # --- # name: test_entity[sensor.parents_bedroom_wifi-entry] EntityRegistryEntrySnapshot({ @@ -4265,7 +4755,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.parents_bedroom_wifi', @@ -4290,7 +4780,21 @@ }) # --- # name: test_entity[sensor.parents_bedroom_wifi-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Parents Bedroom Wifi', + 'icon': 'mdi:wifi', + 'latitude': 13.377726, + 'longitude': 52.516263, + }), + 'context': , + 'entity_id': 'sensor.parents_bedroom_wifi', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'High', + }) # --- # name: test_entity[sensor.prise_power-entry] EntityRegistryEntrySnapshot({ @@ -4353,7 +4857,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.prise_reachability', @@ -4378,7 +4882,19 @@ }) # --- # name: test_entity[sensor.prise_reachability-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Prise Reachability', + 'icon': 'mdi:signal', + }), + 'context': , + 'entity_id': 'sensor.prise_reachability', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'True', + }) # --- # name: test_entity[sensor.total_power-entry] EntityRegistryEntrySnapshot({ @@ -4441,7 +4957,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.total_reachability', @@ -4466,7 +4982,19 @@ }) # --- # name: test_entity[sensor.total_reachability-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Total Reachability', + 'icon': 'mdi:signal', + }), + 'context': , + 'entity_id': 'sensor.total_reachability', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) # --- # name: test_entity[sensor.valve1_battery_percent-entry] EntityRegistryEntrySnapshot({ @@ -4737,7 +5265,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.villa_bathroom_radio', @@ -4762,7 +5290,19 @@ }) # --- # name: test_entity[sensor.villa_bathroom_radio-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Villa Bathroom Radio', + 'icon': 'mdi:signal', + }), + 'context': , + 'entity_id': 'sensor.villa_bathroom_radio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Full', + }) # --- # name: test_entity[sensor.villa_bathroom_reachability-entry] EntityRegistryEntrySnapshot({ @@ -4773,7 +5313,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.villa_bathroom_reachability', @@ -4798,7 +5338,19 @@ }) # --- # name: test_entity[sensor.villa_bathroom_reachability-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Villa Bathroom Reachability', + 'icon': 'mdi:signal', + }), + 'context': , + 'entity_id': 'sensor.villa_bathroom_reachability', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'True', + }) # --- # name: test_entity[sensor.villa_bathroom_temperature-entry] EntityRegistryEntrySnapshot({ @@ -4861,7 +5413,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.villa_bathroom_temperature_trend', @@ -4886,7 +5438,19 @@ }) # --- # name: test_entity[sensor.villa_bathroom_temperature_trend-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Villa Bathroom Temperature trend', + 'icon': 'mdi:trending-up', + }), + 'context': , + 'entity_id': 'sensor.villa_bathroom_temperature_trend', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'stable', + }) # --- # name: test_entity[sensor.villa_bedroom_battery_percent-entry] EntityRegistryEntrySnapshot({ @@ -5053,7 +5617,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.villa_bedroom_radio', @@ -5078,7 +5642,19 @@ }) # --- # name: test_entity[sensor.villa_bedroom_radio-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Villa Bedroom Radio', + 'icon': 'mdi:signal', + }), + 'context': , + 'entity_id': 'sensor.villa_bedroom_radio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'High', + }) # --- # name: test_entity[sensor.villa_bedroom_reachability-entry] EntityRegistryEntrySnapshot({ @@ -5089,7 +5665,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.villa_bedroom_reachability', @@ -5114,7 +5690,19 @@ }) # --- # name: test_entity[sensor.villa_bedroom_reachability-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Villa Bedroom Reachability', + 'icon': 'mdi:signal', + }), + 'context': , + 'entity_id': 'sensor.villa_bedroom_reachability', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'True', + }) # --- # name: test_entity[sensor.villa_bedroom_temperature-entry] EntityRegistryEntrySnapshot({ @@ -5177,7 +5765,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.villa_bedroom_temperature_trend', @@ -5202,7 +5790,19 @@ }) # --- # name: test_entity[sensor.villa_bedroom_temperature_trend-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Villa Bedroom Temperature trend', + 'icon': 'mdi:trending-up', + }), + 'context': , + 'entity_id': 'sensor.villa_bedroom_temperature_trend', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'stable', + }) # --- # name: test_entity[sensor.villa_co2-entry] EntityRegistryEntrySnapshot({ @@ -5269,7 +5869,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.villa_garden_angle', @@ -5294,7 +5894,21 @@ }) # --- # name: test_entity[sensor.villa_garden_angle-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Villa Garden Angle', + 'icon': 'mdi:compass-outline', + 'state_class': , + 'unit_of_measurement': '°', + }), + 'context': , + 'entity_id': 'sensor.villa_garden_angle', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '217', + }) # --- # name: test_entity[sensor.villa_garden_battery_percent-entry] EntityRegistryEntrySnapshot({ @@ -5407,7 +6021,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.villa_garden_gust_angle', @@ -5432,7 +6046,21 @@ }) # --- # name: test_entity[sensor.villa_garden_gust_angle-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Villa Garden Gust Angle', + 'icon': 'mdi:compass-outline', + 'state_class': , + 'unit_of_measurement': '°', + }), + 'context': , + 'entity_id': 'sensor.villa_garden_gust_angle', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '206', + }) # --- # name: test_entity[sensor.villa_garden_gust_direction-entry] EntityRegistryEntrySnapshot({ @@ -5443,7 +6071,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.villa_garden_gust_direction', @@ -5468,7 +6096,19 @@ }) # --- # name: test_entity[sensor.villa_garden_gust_direction-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Villa Garden Gust Direction', + 'icon': 'mdi:compass-outline', + }), + 'context': , + 'entity_id': 'sensor.villa_garden_gust_direction', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'S', + }) # --- # name: test_entity[sensor.villa_garden_gust_strength-entry] EntityRegistryEntrySnapshot({ @@ -5481,7 +6121,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.villa_garden_gust_strength', @@ -5506,7 +6146,21 @@ }) # --- # name: test_entity[sensor.villa_garden_gust_strength-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'wind_speed', + 'friendly_name': 'Villa Garden Gust Strength', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.villa_garden_gust_strength', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '9', + }) # --- # name: test_entity[sensor.villa_garden_radio-entry] EntityRegistryEntrySnapshot({ @@ -5517,7 +6171,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.villa_garden_radio', @@ -5542,7 +6196,19 @@ }) # --- # name: test_entity[sensor.villa_garden_radio-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Villa Garden Radio', + 'icon': 'mdi:signal', + }), + 'context': , + 'entity_id': 'sensor.villa_garden_radio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Full', + }) # --- # name: test_entity[sensor.villa_garden_reachability-entry] EntityRegistryEntrySnapshot({ @@ -5553,7 +6219,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.villa_garden_reachability', @@ -5578,7 +6244,19 @@ }) # --- # name: test_entity[sensor.villa_garden_reachability-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Villa Garden Reachability', + 'icon': 'mdi:signal', + }), + 'context': , + 'entity_id': 'sensor.villa_garden_reachability', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'True', + }) # --- # name: test_entity[sensor.villa_garden_wind_strength-entry] EntityRegistryEntrySnapshot({ @@ -5853,7 +6531,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.villa_outdoor_radio', @@ -5878,7 +6556,19 @@ }) # --- # name: test_entity[sensor.villa_outdoor_radio-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Villa Outdoor Radio', + 'icon': 'mdi:signal', + }), + 'context': , + 'entity_id': 'sensor.villa_outdoor_radio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) # --- # name: test_entity[sensor.villa_outdoor_reachability-entry] EntityRegistryEntrySnapshot({ @@ -5889,7 +6579,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.villa_outdoor_reachability', @@ -5914,7 +6604,19 @@ }) # --- # name: test_entity[sensor.villa_outdoor_reachability-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Villa Outdoor Reachability', + 'icon': 'mdi:signal', + }), + 'context': , + 'entity_id': 'sensor.villa_outdoor_reachability', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) # --- # name: test_entity[sensor.villa_outdoor_temperature-entry] EntityRegistryEntrySnapshot({ @@ -5977,7 +6679,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.villa_outdoor_temperature_trend', @@ -6002,7 +6704,19 @@ }) # --- # name: test_entity[sensor.villa_outdoor_temperature_trend-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Villa Outdoor Temperature trend', + 'icon': 'mdi:trending-up', + }), + 'context': , + 'entity_id': 'sensor.villa_outdoor_temperature_trend', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) # --- # name: test_entity[sensor.villa_pressure-entry] EntityRegistryEntrySnapshot({ @@ -6070,7 +6784,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.villa_pressure_trend', @@ -6095,7 +6809,21 @@ }) # --- # name: test_entity[sensor.villa_pressure_trend-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Villa Pressure trend', + 'icon': 'mdi:trending-up', + 'latitude': 46.123456, + 'longitude': 6.1234567, + }), + 'context': , + 'entity_id': 'sensor.villa_pressure_trend', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'up', + }) # --- # name: test_entity[sensor.villa_rain_battery_percent-entry] EntityRegistryEntrySnapshot({ @@ -6158,7 +6886,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.villa_rain_radio', @@ -6183,7 +6911,19 @@ }) # --- # name: test_entity[sensor.villa_rain_radio-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Villa Rain Radio', + 'icon': 'mdi:signal', + }), + 'context': , + 'entity_id': 'sensor.villa_rain_radio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Medium', + }) # --- # name: test_entity[sensor.villa_rain_rain-entry] EntityRegistryEntrySnapshot({ @@ -6248,7 +6988,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.villa_rain_rain_last_hour', @@ -6273,7 +7013,21 @@ }) # --- # name: test_entity[sensor.villa_rain_rain_last_hour-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'precipitation', + 'friendly_name': 'Villa Rain Rain last hour', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.villa_rain_rain_last_hour', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) # --- # name: test_entity[sensor.villa_rain_rain_today-entry] EntityRegistryEntrySnapshot({ @@ -6336,7 +7090,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.villa_rain_reachability', @@ -6361,7 +7115,19 @@ }) # --- # name: test_entity[sensor.villa_rain_reachability-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Villa Rain Reachability', + 'icon': 'mdi:signal', + }), + 'context': , + 'entity_id': 'sensor.villa_rain_reachability', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'True', + }) # --- # name: test_entity[sensor.villa_reachability-entry] EntityRegistryEntrySnapshot({ @@ -6372,7 +7138,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.villa_reachability', @@ -6397,7 +7163,21 @@ }) # --- # name: test_entity[sensor.villa_reachability-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Villa Reachability', + 'icon': 'mdi:signal', + 'latitude': 46.123456, + 'longitude': 6.1234567, + }), + 'context': , + 'entity_id': 'sensor.villa_reachability', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'True', + }) # --- # name: test_entity[sensor.villa_temperature-entry] EntityRegistryEntrySnapshot({ @@ -6462,7 +7242,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.villa_temperature_trend', @@ -6487,7 +7267,21 @@ }) # --- # name: test_entity[sensor.villa_temperature_trend-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Villa Temperature trend', + 'icon': 'mdi:trending-up', + 'latitude': 46.123456, + 'longitude': 6.1234567, + }), + 'context': , + 'entity_id': 'sensor.villa_temperature_trend', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'stable', + }) # --- # name: test_entity[sensor.villa_wifi-entry] EntityRegistryEntrySnapshot({ @@ -6498,7 +7292,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.villa_wifi', @@ -6523,5 +7317,19 @@ }) # --- # name: test_entity[sensor.villa_wifi-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Villa Wifi', + 'icon': 'mdi:wifi', + 'latitude': 46.123456, + 'longitude': 6.1234567, + }), + 'context': , + 'entity_id': 'sensor.villa_wifi', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'High', + }) # --- diff --git a/tests/components/netatmo/test_sensor.py b/tests/components/netatmo/test_sensor.py index 073b9faf485..fa3ff41c3fb 100644 --- a/tests/components/netatmo/test_sensor.py +++ b/tests/components/netatmo/test_sensor.py @@ -15,6 +15,7 @@ from .common import selected_platforms, snapshot_platform_entities from tests.common import MockConfigEntry +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_entity( hass: HomeAssistant, config_entry: MockConfigEntry, From e4fbe539aafb8c8029ca3064fc4b68847e4b59eb Mon Sep 17 00:00:00 2001 From: fhoekstra <32362869+fhoekstra@users.noreply.github.com> Date: Sun, 7 Apr 2024 15:05:22 +0200 Subject: [PATCH 372/967] Add GPU sensor to Glances (#106322) * feat: add GPU sensor to Glances * Add translations to Glances GPU sensor * Fix translations of GPU processor and memory usage * PR feedback: move icons to icons.json thanks to @wittypluck * Update glances snapshot with added GPU * Remove JSON trailing comma Co-authored-by: Joost Lekkerkerker * Limit precision of Glance GPU mem usage sensor Co-authored-by: Joost Lekkerkerker * Clean up outdated snapshots --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/glances/icons.json | 6 + homeassistant/components/glances/sensor.py | 32 ++- homeassistant/components/glances/strings.json | 6 + tests/components/glances/__init__.py | 8 + .../glances/snapshots/test_sensor.ambr | 204 ++++++++++++++++++ 5 files changed, 255 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/glances/icons.json b/homeassistant/components/glances/icons.json index 92ef28ad325..9f3e00540de 100644 --- a/homeassistant/components/glances/icons.json +++ b/homeassistant/components/glances/icons.json @@ -54,6 +54,12 @@ }, "uptime": { "default": "mdi:clock-time-eight-outline" + }, + "gpu_processor_usage": { + "default": "mdi:expansion-card" + }, + "gpu_memory_usage": { + "default": "mdi:memory" } } } diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index fc83d297645..80671c0642e 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -235,6 +235,36 @@ SENSOR_TYPES = { translation_key="uptime", device_class=SensorDeviceClass.TIMESTAMP, ), + ("gpu", "mem"): GlancesSensorEntityDescription( + key="mem", + type="gpu", + translation_key="gpu_memory_usage", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + ("gpu", "proc"): GlancesSensorEntityDescription( + key="proc", + type="gpu", + translation_key="gpu_processor_usage", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + ), + ("gpu", "temperature"): GlancesSensorEntityDescription( + key="temperature", + type="gpu", + translation_key="temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + ("gpu", "fan_speed"): GlancesSensorEntityDescription( + key="fan_speed", + type="gpu", + translation_key="fan_speed", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), } @@ -249,7 +279,7 @@ async def async_setup_entry( entities: list[GlancesSensor] = [] for sensor_type, sensors in coordinator.data.items(): - if sensor_type in ["fs", "diskio", "sensors", "raid"]: + if sensor_type in ["fs", "diskio", "sensors", "raid", "gpu"]: entities.extend( GlancesSensor( coordinator, diff --git a/homeassistant/components/glances/strings.json b/homeassistant/components/glances/strings.json index e2ef185727c..208d14563df 100644 --- a/homeassistant/components/glances/strings.json +++ b/homeassistant/components/glances/strings.json @@ -109,6 +109,12 @@ }, "uptime": { "name": "Uptime" + }, + "gpu_memory_usage": { + "name": "{sensor_label} memory usage" + }, + "gpu_processor_usage": { + "name": "{sensor_label} processor usage" } } }, diff --git a/tests/components/glances/__init__.py b/tests/components/glances/__init__.py index c7f2657fb37..6fbfc73b7be 100644 --- a/tests/components/glances/__init__.py +++ b/tests/components/glances/__init__.py @@ -215,4 +215,12 @@ HA_SENSOR_DATA: dict[str, Any] = { }, }, "uptime": "3 days, 10:25:20", + "gpu": { + "NVIDIA GeForce RTX 3080 (GPU 0)": { + "temperature": 51, + "mem": 8.41064453125, + "proc": 26, + "fan_speed": 0, + } + }, } diff --git a/tests/components/glances/snapshots/test_sensor.ambr b/tests/components/glances/snapshots/test_sensor.ambr index d6cccdab4ee..64ea8dd1052 100644 --- a/tests/components/glances/snapshots/test_sensor.ambr +++ b/tests/components/glances/snapshots/test_sensor.ambr @@ -802,6 +802,210 @@ 'state': 'unavailable', }) # --- +# name: test_sensor_states[sensor.0_0_0_0_nvidia_geforce_rtx_3080_gpu_0_fan_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.0_0_0_0_nvidia_geforce_rtx_3080_gpu_0_fan_speed', + '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': 'NVIDIA GeForce RTX 3080 (GPU 0) fan speed', + 'platform': 'glances', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'fan_speed', + 'unique_id': 'test-NVIDIA GeForce RTX 3080 (GPU 0)-fan_speed', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_nvidia_geforce_rtx_3080_gpu_0_fan_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': '0.0.0.0 NVIDIA GeForce RTX 3080 (GPU 0) fan speed', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.0_0_0_0_nvidia_geforce_rtx_3080_gpu_0_fan_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_nvidia_geforce_rtx_3080_gpu_0_memory_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.0_0_0_0_nvidia_geforce_rtx_3080_gpu_0_memory_usage', + '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': 'NVIDIA GeForce RTX 3080 (GPU 0) memory usage', + 'platform': 'glances', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'gpu_memory_usage', + 'unique_id': 'test-NVIDIA GeForce RTX 3080 (GPU 0)-mem', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_nvidia_geforce_rtx_3080_gpu_0_memory_usage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': '0.0.0.0 NVIDIA GeForce RTX 3080 (GPU 0) memory usage', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.0_0_0_0_nvidia_geforce_rtx_3080_gpu_0_memory_usage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '8.41064453125', + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_nvidia_geforce_rtx_3080_gpu_0_processor_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.0_0_0_0_nvidia_geforce_rtx_3080_gpu_0_processor_usage', + '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': 'NVIDIA GeForce RTX 3080 (GPU 0) processor usage', + 'platform': 'glances', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'gpu_processor_usage', + 'unique_id': 'test-NVIDIA GeForce RTX 3080 (GPU 0)-proc', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_nvidia_geforce_rtx_3080_gpu_0_processor_usage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': '0.0.0.0 NVIDIA GeForce RTX 3080 (GPU 0) processor usage', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.0_0_0_0_nvidia_geforce_rtx_3080_gpu_0_processor_usage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '26', + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_nvidia_geforce_rtx_3080_gpu_0_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.0_0_0_0_nvidia_geforce_rtx_3080_gpu_0_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': 'NVIDIA GeForce RTX 3080 (GPU 0) temperature', + 'platform': 'glances', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'temperature', + 'unique_id': 'test-NVIDIA GeForce RTX 3080 (GPU 0)-temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_nvidia_geforce_rtx_3080_gpu_0_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': '0.0.0.0 NVIDIA GeForce RTX 3080 (GPU 0) temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.0_0_0_0_nvidia_geforce_rtx_3080_gpu_0_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '51', + }) +# --- # name: test_sensor_states[sensor.0_0_0_0_nvme0n1_disk_read-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From d4d06c0bc3ddd375982415498f6ca1dc9a7c22ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Sun, 7 Apr 2024 15:07:40 +0200 Subject: [PATCH 373/967] Add Water Heater support for Airzone Cloud (#115097) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * airzone_cloud: add Water Heater support Signed-off-by: Álvaro Fernández Rojas * run CI --------- Signed-off-by: Álvaro Fernández Rojas --- .../components/airzone_cloud/__init__.py | 1 + .../components/airzone_cloud/entity.py | 42 ++++ .../components/airzone_cloud/water_heater.py | 164 +++++++++++++++ .../snapshots/test_diagnostics.ambr | 2 + .../airzone_cloud/test_water_heater.py | 186 ++++++++++++++++++ tests/components/airzone_cloud/util.py | 2 + 6 files changed, 397 insertions(+) create mode 100644 homeassistant/components/airzone_cloud/water_heater.py create mode 100644 tests/components/airzone_cloud/test_water_heater.py diff --git a/homeassistant/components/airzone_cloud/__init__.py b/homeassistant/components/airzone_cloud/__init__.py index 83be481a4de..c6908b191d7 100644 --- a/homeassistant/components/airzone_cloud/__init__.py +++ b/homeassistant/components/airzone_cloud/__init__.py @@ -17,6 +17,7 @@ PLATFORMS: list[Platform] = [ Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.SENSOR, + Platform.WATER_HEATER, ] diff --git a/homeassistant/components/airzone_cloud/entity.py b/homeassistant/components/airzone_cloud/entity.py index f53321ce353..8e8a7aff1bc 100644 --- a/homeassistant/components/airzone_cloud/entity.py +++ b/homeassistant/components/airzone_cloud/entity.py @@ -11,6 +11,7 @@ from aioairzone_cloud.const import ( AZD_AVAILABLE, AZD_FIRMWARE, AZD_GROUPS, + AZD_HOT_WATERS, AZD_INSTALLATIONS, AZD_NAME, AZD_SYSTEM_ID, @@ -136,6 +137,47 @@ class AirzoneGroupEntity(AirzoneEntity): self.coordinator.async_set_updated_data(self.coordinator.airzone.data()) +class AirzoneHotWaterEntity(AirzoneEntity): + """Define an Airzone Cloud Hot Water entity.""" + + def __init__( + self, + coordinator: AirzoneUpdateCoordinator, + dhw_id: str, + dhw_data: dict[str, Any], + ) -> None: + """Initialize.""" + super().__init__(coordinator) + + self.dhw_id = dhw_id + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, dhw_id)}, + manufacturer=MANUFACTURER, + name=dhw_data[AZD_NAME], + via_device=(DOMAIN, dhw_data[AZD_WEBSERVER]), + ) + + def get_airzone_value(self, key: str) -> Any: + """Return DHW value by key.""" + value = None + if dhw := self.coordinator.data[AZD_HOT_WATERS].get(self.dhw_id): + value = dhw.get(key) + return value + + async def _async_update_params(self, params: dict[str, Any]) -> None: + """Send DHW parameters to Cloud API.""" + _LOGGER.debug("dhw=%s: update_params=%s", self.entity_id, params) + try: + await self.coordinator.airzone.api_set_dhw_id_params(self.dhw_id, params) + except AirzoneCloudError as error: + raise HomeAssistantError( + f"Failed to set {self.entity_id} params: {error}" + ) from error + + self.coordinator.async_set_updated_data(self.coordinator.airzone.data()) + + class AirzoneInstallationEntity(AirzoneEntity): """Define an Airzone Cloud Installation entity.""" diff --git a/homeassistant/components/airzone_cloud/water_heater.py b/homeassistant/components/airzone_cloud/water_heater.py new file mode 100644 index 00000000000..fd1c772b38a --- /dev/null +++ b/homeassistant/components/airzone_cloud/water_heater.py @@ -0,0 +1,164 @@ +"""Support for the Airzone Cloud water heater.""" + +from __future__ import annotations + +from typing import Any, Final + +from aioairzone_cloud.common import HotWaterOperation, TemperatureUnit +from aioairzone_cloud.const import ( + API_OPTS, + API_POWER, + API_POWERFUL_MODE, + API_SETPOINT, + API_UNITS, + API_VALUE, + AZD_HOT_WATERS, + AZD_OPERATION, + AZD_OPERATIONS, + AZD_TEMP, + AZD_TEMP_SET, + AZD_TEMP_SET_MAX, + AZD_TEMP_SET_MIN, +) + +from homeassistant.components.water_heater import ( + STATE_ECO, + STATE_PERFORMANCE, + 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 .coordinator import AirzoneUpdateCoordinator +from .entity import AirzoneHotWaterEntity + +OPERATION_LIB_TO_HASS: Final[dict[HotWaterOperation, str]] = { + HotWaterOperation.Off: STATE_OFF, + HotWaterOperation.On: STATE_ECO, + HotWaterOperation.Powerful: STATE_PERFORMANCE, +} + +OPERATION_MODE_TO_DHW_PARAMS: Final[dict[str, dict[str, Any]]] = { + STATE_OFF: { + API_POWER: { + API_VALUE: False, + }, + }, + STATE_ECO: { + API_POWER: { + API_VALUE: True, + }, + API_POWERFUL_MODE: { + API_VALUE: False, + }, + }, + STATE_PERFORMANCE: { + API_POWER: { + API_VALUE: True, + }, + API_POWERFUL_MODE: { + API_VALUE: True, + }, + }, +} + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Add Airzone Cloud Water Heater from a config_entry.""" + coordinator: AirzoneUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + AirzoneWaterHeater( + coordinator, + dhw_id, + dhw_data, + ) + for dhw_id, dhw_data in coordinator.data.get(AZD_HOT_WATERS, {}).items() + ) + + +class AirzoneWaterHeater(AirzoneHotWaterEntity, WaterHeaterEntity): + """Define an Airzone Cloud Water Heater.""" + + _attr_name = None + _attr_supported_features = ( + WaterHeaterEntityFeature.TARGET_TEMPERATURE + | WaterHeaterEntityFeature.ON_OFF + | WaterHeaterEntityFeature.OPERATION_MODE + ) + _attr_temperature_unit = UnitOfTemperature.CELSIUS + + def __init__( + self, + coordinator: AirzoneUpdateCoordinator, + dhw_id: str, + dhw_data: dict, + ) -> None: + """Initialize Airzone Cloud Water Heater.""" + super().__init__(coordinator, dhw_id, dhw_data) + + self._attr_unique_id = dhw_id + self._attr_operation_list = [ + OPERATION_LIB_TO_HASS[operation] + for operation in self.get_airzone_value(AZD_OPERATIONS) + ] + + self._async_update_attrs() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the water heater off.""" + params = { + API_POWER: { + API_VALUE: False, + }, + } + await self._async_update_params(params) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the water heater off.""" + params = { + API_POWER: { + API_VALUE: True, + }, + } + await self._async_update_params(params) + + async def async_set_operation_mode(self, operation_mode: str) -> None: + """Set new target operation mode.""" + params = OPERATION_MODE_TO_DHW_PARAMS.get(operation_mode, {}) + await self._async_update_params(params) + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new target temperature.""" + params: dict[str, Any] = {} + if ATTR_TEMPERATURE in kwargs: + params[API_SETPOINT] = { + API_VALUE: kwargs[ATTR_TEMPERATURE], + API_OPTS: { + API_UNITS: TemperatureUnit.CELSIUS.value, + }, + } + await self._async_update_params(params) + + @callback + def _handle_coordinator_update(self) -> None: + """Update attributes when the coordinator updates.""" + self._async_update_attrs() + super()._handle_coordinator_update() + + @callback + def _async_update_attrs(self) -> None: + """Update water heater attributes.""" + self._attr_current_temperature = self.get_airzone_value(AZD_TEMP) + self._attr_current_operation = OPERATION_LIB_TO_HASS[ + self.get_airzone_value(AZD_OPERATION) + ] + self._attr_max_temp = self.get_airzone_value(AZD_TEMP_SET_MAX) + self._attr_min_temp = self.get_airzone_value(AZD_TEMP_SET_MIN) + self._attr_target_temperature = self.get_airzone_value(AZD_TEMP_SET) diff --git a/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr b/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr index 7ec1c2eb2fe..3309c175543 100644 --- a/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr +++ b/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr @@ -292,8 +292,10 @@ 'operations': list([ 0, 1, + 2, ]), 'power': False, + 'power-mode': False, 'problems': False, 'temperature': 45.5, 'temperature-setpoint': 48, diff --git a/tests/components/airzone_cloud/test_water_heater.py b/tests/components/airzone_cloud/test_water_heater.py new file mode 100644 index 00000000000..98b1d85be48 --- /dev/null +++ b/tests/components/airzone_cloud/test_water_heater.py @@ -0,0 +1,186 @@ +"""The water heater tests for the Airzone Cloud platform.""" + +from unittest.mock import patch + +from aioairzone_cloud.exceptions import AirzoneCloudError +import pytest + +from homeassistant.components.water_heater import ( + ATTR_CURRENT_TEMPERATURE, + ATTR_MAX_TEMP, + ATTR_MIN_TEMP, + ATTR_OPERATION_MODE, + DOMAIN as WATER_HEATER_DOMAIN, + SERVICE_SET_OPERATION_MODE, + SERVICE_SET_TEMPERATURE, + STATE_ECO, + STATE_PERFORMANCE, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_TEMPERATURE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError + +from .util import async_init_integration + + +async def test_airzone_create_water_heater(hass: HomeAssistant) -> None: + """Test creation of water heater.""" + + await async_init_integration(hass) + + state = hass.states.get("water_heater.airzone_cloud_dhw") + assert state.state == STATE_OFF + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 45.5 + assert state.attributes[ATTR_MAX_TEMP] == 60 + assert state.attributes[ATTR_MIN_TEMP] == 40 + assert state.attributes[ATTR_TEMPERATURE] == 48 + + +async def test_airzone_water_heater_turn_on_off(hass: HomeAssistant) -> None: + """Test turning on/off.""" + + await async_init_integration(hass) + + with patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_patch_device", + return_value=None, + ): + await hass.services.async_call( + WATER_HEATER_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "water_heater.airzone_cloud_dhw", + }, + blocking=True, + ) + + state = hass.states.get("water_heater.airzone_cloud_dhw") + assert state.state == STATE_ECO + + with patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_patch_device", + return_value=None, + ): + await hass.services.async_call( + WATER_HEATER_DOMAIN, + SERVICE_TURN_OFF, + { + ATTR_ENTITY_ID: "water_heater.airzone_cloud_dhw", + }, + blocking=True, + ) + + state = hass.states.get("water_heater.airzone_cloud_dhw") + assert state.state == STATE_OFF + + +async def test_airzone_water_heater_set_operation(hass: HomeAssistant) -> None: + """Test setting the Operation mode.""" + + await async_init_integration(hass) + + with patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_patch_device", + return_value=None, + ): + await hass.services.async_call( + WATER_HEATER_DOMAIN, + SERVICE_SET_OPERATION_MODE, + { + ATTR_ENTITY_ID: "water_heater.airzone_cloud_dhw", + ATTR_OPERATION_MODE: STATE_ECO, + }, + blocking=True, + ) + + state = hass.states.get("water_heater.airzone_cloud_dhw") + assert state.state == STATE_ECO + + with patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_patch_device", + return_value=None, + ): + await hass.services.async_call( + WATER_HEATER_DOMAIN, + SERVICE_SET_OPERATION_MODE, + { + ATTR_ENTITY_ID: "water_heater.airzone_cloud_dhw", + ATTR_OPERATION_MODE: STATE_PERFORMANCE, + }, + blocking=True, + ) + + state = hass.states.get("water_heater.airzone_cloud_dhw") + assert state.state == STATE_PERFORMANCE + + with patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_patch_device", + return_value=None, + ): + await hass.services.async_call( + WATER_HEATER_DOMAIN, + SERVICE_SET_OPERATION_MODE, + { + ATTR_ENTITY_ID: "water_heater.airzone_cloud_dhw", + ATTR_OPERATION_MODE: STATE_OFF, + }, + blocking=True, + ) + + state = hass.states.get("water_heater.airzone_cloud_dhw") + assert state.state == STATE_OFF + + +async def test_airzone_water_heater_set_temp(hass: HomeAssistant) -> None: + """Test setting the target temperature.""" + + await async_init_integration(hass) + + with patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_patch_device", + return_value=None, + ): + await hass.services.async_call( + WATER_HEATER_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: "water_heater.airzone_cloud_dhw", + ATTR_TEMPERATURE: 50, + }, + blocking=True, + ) + + state = hass.states.get("water_heater.airzone_cloud_dhw") + assert state.attributes[ATTR_TEMPERATURE] == 50 + + +async def test_airzone_water_heater_set_temp_error(hass: HomeAssistant) -> None: + """Test error when setting the target temperature.""" + + await async_init_integration(hass) + + with ( + patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_patch_device", + side_effect=AirzoneCloudError, + ), + pytest.raises(HomeAssistantError), + ): + await hass.services.async_call( + WATER_HEATER_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: "water_heater.airzone_cloud_dhw", + ATTR_TEMPERATURE: 80, + }, + blocking=True, + ) + + state = hass.states.get("water_heater.airzone_cloud_dhw") + assert state.attributes[ATTR_TEMPERATURE] == 48 diff --git a/tests/components/airzone_cloud/util.py b/tests/components/airzone_cloud/util.py index 02c3e18eed2..0583fad7c0e 100644 --- a/tests/components/airzone_cloud/util.py +++ b/tests/components/airzone_cloud/util.py @@ -41,6 +41,7 @@ from aioairzone_cloud.const import ( API_NAME, API_OLD_ID, API_POWER, + API_POWERFUL_MODE, API_RANGE_MAX_AIR, API_RANGE_MIN_AIR, API_RANGE_SP_MAX_ACS, @@ -315,6 +316,7 @@ def mock_get_device_status(device: Device) -> dict[str, Any]: API_ACTIVE: False, API_ERRORS: [], API_POWER: False, + API_POWERFUL_MODE: False, API_SETPOINT: {API_CELSIUS: 48, API_FAH: 118}, API_RANGE_SP_MAX_ACS: {API_CELSIUS: 60, API_FAH: 140}, API_RANGE_SP_MIN_ACS: {API_CELSIUS: 40, API_FAH: 104}, From f617000920be9d39104b50f6f6506c7cba2485a6 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Sun, 7 Apr 2024 15:10:00 +0200 Subject: [PATCH 374/967] Add device removal capability for Netatmo (#107630) * Add device removal capability for Netatmo * Update tests/components/netatmo/test_init.py Co-authored-by: Joost Lekkerkerker * Update tests/components/netatmo/test_init.py Co-authored-by: Joost Lekkerkerker * Update tests/components/netatmo/test_init.py Co-authored-by: Joost Lekkerkerker --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/netatmo/__init__.py | 18 ++++++ tests/components/netatmo/test_init.py | 59 +++++++++++++++++++- 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index 322af8cf3ac..f402009e13b 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -25,6 +25,7 @@ from homeassistant.helpers import ( config_entry_oauth2_flow, config_validation as cv, ) +from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later from homeassistant.helpers.start import async_at_started @@ -243,3 +244,20 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: await cloud.async_delete_cloudhook(hass, entry.data[CONF_WEBHOOK_ID]) except cloud.CloudNotAvailable: pass + + +async def async_remove_config_entry_device( + hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry +) -> bool: + """Remove a config entry from a device.""" + data = hass.data[DOMAIN][config_entry.entry_id][DATA_HANDLER] + modules = [m for h in data.account.homes.values() for m in h.modules] + rooms = [r for h in data.account.homes.values() for r in h.rooms] + + return not any( + identifier + for identifier in device_entry.identifiers + if identifier[0] == DOMAIN + and identifier[1] in modules + or identifier[1] in rooms + ) diff --git a/tests/components/netatmo/test_init.py b/tests/components/netatmo/test_init.py index c68bd7df541..672084d644d 100644 --- a/tests/components/netatmo/test_init.py +++ b/tests/components/netatmo/test_init.py @@ -14,7 +14,7 @@ from homeassistant.components.netatmo import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_WEBHOOK_ID, Platform from homeassistant.core import CoreState, HomeAssistant -import homeassistant.helpers.device_registry as dr +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 @@ -31,6 +31,7 @@ from tests.common import ( async_get_persistent_notifications, ) from tests.components.cloud import mock_cloud +from tests.typing import MockHAClientWebSocket, WebSocketGenerator # Fake webhook thermostat mode change to "Max" FAKE_WEBHOOK = { @@ -520,3 +521,59 @@ async def test_devices( for device_entry in device_entries: identifier = list(device_entry.identifiers)[0] assert device_entry == snapshot(name=f"{identifier[0]}-{identifier[1]}") + + +async def remove_device( + ws_client: MockHAClientWebSocket, device_id: str, config_entry_id: str +) -> bool: + """Remove config entry from a device.""" + await ws_client.send_json( + { + "id": 1, + "type": "config/device_registry/remove_config_entry", + "config_entry_id": config_entry_id, + "device_id": device_id, + } + ) + response = await ws_client.receive_json() + return response["success"] + + +async def test_device_remove_devices( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + config_entry: MockConfigEntry, + netatmo_auth: AsyncMock, +) -> None: + """Test we can only remove a device that no longer exists.""" + + assert await async_setup_component(hass, "config", {}) + + with selected_platforms([Platform.CLIMATE]): + assert await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + climate_entity_livingroom = "climate.livingroom" + entity = entity_registry.async_get(climate_entity_livingroom) + + device_entry = device_registry.async_get(entity.device_id) + assert ( + await remove_device( + await hass_ws_client(hass), device_entry.id, config_entry.entry_id + ) + is False + ) + + dead_device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={(DOMAIN, "remove-device-id")}, + ) + assert ( + await remove_device( + await hass_ws_client(hass), dead_device_entry.id, config_entry.entry_id + ) + is True + ) From 5630b3611daa260431e630c27810b16e2c8c0592 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 7 Apr 2024 03:14:18 -1000 Subject: [PATCH 375/967] Add an event filter to the alexa state report state change listener (#115076) Co-authored-by: jbouwh --- .../components/alexa/state_report.py | 49 +++++++++++++------ tests/components/alexa/test_state_report.py | 44 ++++++++--------- 2 files changed, 56 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index 9c640d76dd4..978879b5d88 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -13,10 +13,16 @@ from uuid import uuid4 import aiohttp from homeassistant.components import event -from homeassistant.const import MATCH_ALL, STATE_ON -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, State, callback +from homeassistant.const import EVENT_STATE_CHANGED, STATE_ON +from homeassistant.core import ( + CALLBACK_TYPE, + Event, + EventStateChangedData, + HomeAssistant, + State, + callback, +) from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.significant_change import create_checker import homeassistant.util.dt as dt_util from homeassistant.util.json import JsonObjectType, json_loads_object @@ -265,28 +271,35 @@ async def async_enable_proactive_mode( checker = await create_checker(hass, DOMAIN, extra_significant_check) - async def async_entity_state_listener( - changed_entity: str, - old_state: State | None, - new_state: State | None, - ) -> None: + @callback + def _async_entity_state_filter(data: EventStateChangedData) -> bool: if not hass.is_running: - return + return False - if not new_state: - return + if not (new_state := data["new_state"]): + return False if new_state.domain not in ENTITY_ADAPTERS: - return + return False + changed_entity = data["entity_id"] if not smart_home_config.should_expose(changed_entity): _LOGGER.debug("Not exposing %s because filtered by config", changed_entity) - return + return False + + return True + + async def _async_entity_state_listener( + event_: Event[EventStateChangedData], + ) -> None: + data = event_.data + new_state = data["new_state"] + if TYPE_CHECKING: + assert new_state is not None alexa_changed_entity: AlexaEntity = ENTITY_ADAPTERS[new_state.domain]( hass, smart_home_config, new_state ) - # Determine how entity should be reported on should_report = False should_doorbell = False @@ -303,6 +316,7 @@ async def async_enable_proactive_mode( return if should_doorbell: + old_state = data["old_state"] if ( new_state.domain == event.DOMAIN or new_state.state == STATE_ON @@ -324,7 +338,12 @@ async def async_enable_proactive_mode( hass, smart_home_config, alexa_changed_entity, alexa_properties ) - return async_track_state_change(hass, MATCH_ALL, async_entity_state_listener) + return hass.bus.async_listen( + EVENT_STATE_CHANGED, + _async_entity_state_listener, + event_filter=_async_entity_state_filter, + run_immediately=True, + ) async def async_send_changereport_message( diff --git a/tests/components/alexa/test_state_report.py b/tests/components/alexa/test_state_report.py index 6bd7caccc38..92410ae9de9 100644 --- a/tests/components/alexa/test_state_report.py +++ b/tests/components/alexa/test_state_report.py @@ -185,14 +185,14 @@ async def test_report_state_unsets_authorized_on_error( config = get_default_config(hass) await state_report.async_enable_proactive_mode(hass, config) + config._store.set_authorized.assert_not_called() + hass.states.async_set( "binary_sensor.test_contact", "off", {"friendly_name": "Test Contact Sensor", "device_class": "door"}, ) - config._store.set_authorized.assert_not_called() - # To trigger event listener await hass.async_block_till_done() config._store.set_authorized.assert_called_once_with(False) @@ -215,15 +215,15 @@ async def test_report_state_unsets_authorized_on_access_token_error( await state_report.async_enable_proactive_mode(hass, config) - hass.states.async_set( - "binary_sensor.test_contact", - "off", - {"friendly_name": "Test Contact Sensor", "device_class": "door"}, - ) - config._store.set_authorized.assert_not_called() with patch.object(config, "async_get_access_token", AsyncMock(side_effect=exc)): + hass.states.async_set( + "binary_sensor.test_contact", + "off", + {"friendly_name": "Test Contact Sensor", "device_class": "door"}, + ) + # To trigger event listener await hass.async_block_till_done() config._store.set_authorized.assert_called_once_with(False) @@ -731,39 +731,39 @@ async def test_proactive_mode_filter_states( assert len(aioclient_mock.mock_calls) == 0 # hass not running should not report + current_state = hass.state + hass.set_state(core.CoreState.stopping) + await hass.async_block_till_done() + await hass.async_block_till_done() hass.states.async_set( "binary_sensor.test_contact", "off", {"friendly_name": "Test Contact Sensor", "device_class": "door"}, ) - current_state = hass.state - hass.set_state(core.CoreState.stopping) - await hass.async_block_till_done() - await hass.async_block_till_done() hass.set_state(current_state) assert len(aioclient_mock.mock_calls) == 0 # unsupported entity should not report - hass.states.async_set( - "binary_sensor.test_contact", - "on", - {"friendly_name": "Test Contact Sensor", "device_class": "door"}, - ) with patch.dict( "homeassistant.components.alexa.state_report.ENTITY_ADAPTERS", {}, clear=True ): + hass.states.async_set( + "binary_sensor.test_contact", + "on", + {"friendly_name": "Test Contact Sensor", "device_class": "door"}, + ) await hass.async_block_till_done() await hass.async_block_till_done() assert len(aioclient_mock.mock_calls) == 0 # Not exposed by config should not report - hass.states.async_set( - "binary_sensor.test_contact", - "off", - {"friendly_name": "Test Contact Sensor", "device_class": "door"}, - ) with patch.object(config, "should_expose", return_value=False): + hass.states.async_set( + "binary_sensor.test_contact", + "off", + {"friendly_name": "Test Contact Sensor", "device_class": "door"}, + ) await hass.async_block_till_done() await hass.async_block_till_done() assert len(aioclient_mock.mock_calls) == 0 From 32d62e477afe4a1c97f3bd61564e1eb6b140f188 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 7 Apr 2024 15:54:22 +0200 Subject: [PATCH 376/967] Add entity translations to Bluemaestro (#102424) * Add entity translations to Bluemaestro * Fix * yes --- homeassistant/components/bluemaestro/sensor.py | 6 ++---- homeassistant/components/bluemaestro/strings.json | 7 +++++++ .../components/bluemaestro/snapshots/test_sensor.ambr | 10 +++++----- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/bluemaestro/sensor.py b/homeassistant/components/bluemaestro/sensor.py index 4024b8b3326..f8529a4103b 100644 --- a/homeassistant/components/bluemaestro/sensor.py +++ b/homeassistant/components/bluemaestro/sensor.py @@ -75,6 +75,7 @@ SENSOR_DESCRIPTIONS = { ): SensorEntityDescription( key=f"{BlueMaestroSensorDeviceClass.DEW_POINT}_{Units.TEMP_CELSIUS}", device_class=SensorDeviceClass.TEMPERATURE, + translation_key="dew_point", native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, ), @@ -110,10 +111,7 @@ def sensor_update_to_bluetooth_data_update( device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value for device_key, sensor_values in sensor_update.entity_values.items() }, - entity_names={ - device_key_to_bluetooth_entity_key(device_key): sensor_values.name - for device_key, sensor_values in sensor_update.entity_values.items() - }, + entity_names={}, ) diff --git a/homeassistant/components/bluemaestro/strings.json b/homeassistant/components/bluemaestro/strings.json index d1d544c2381..9dc500980a6 100644 --- a/homeassistant/components/bluemaestro/strings.json +++ b/homeassistant/components/bluemaestro/strings.json @@ -18,5 +18,12 @@ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "entity": { + "sensor": { + "dew_point": { + "name": "Dew point" + } + } } } diff --git a/tests/components/bluemaestro/snapshots/test_sensor.ambr b/tests/components/bluemaestro/snapshots/test_sensor.ambr index a86758c709a..2b777ec6f09 100644 --- a/tests/components/bluemaestro/snapshots/test_sensor.ambr +++ b/tests/components/bluemaestro/snapshots/test_sensor.ambr @@ -76,11 +76,11 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Dew Point', + 'original_name': 'Dew point', 'platform': 'bluemaestro', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'dew_point', 'unique_id': 'aa:bb:cc:dd:ee:ff-dew_point', 'unit_of_measurement': , }) @@ -89,7 +89,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', - 'friendly_name': 'Tempo Disc THD EEFF Dew Point', + 'friendly_name': 'Tempo Disc THD EEFF Dew point', 'state_class': , 'unit_of_measurement': , }), @@ -178,7 +178,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Signal Strength', + 'original_name': 'Signal strength', 'platform': 'bluemaestro', 'previous_unique_id': None, 'supported_features': 0, @@ -191,7 +191,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'signal_strength', - 'friendly_name': 'Tempo Disc THD EEFF Signal Strength', + 'friendly_name': 'Tempo Disc THD EEFF Signal strength', 'state_class': , 'unit_of_measurement': 'dBm', }), From 63d43a938465d238ffbf8f86583eabc1db4cd9b6 Mon Sep 17 00:00:00 2001 From: wittypluck Date: Sun, 7 Apr 2024 16:01:00 +0200 Subject: [PATCH 377/967] Add Glances network sensors (#114546) * Add Glances network sensors * Add unit test for network and remove unused MOCK_DATA * Add network icons --- homeassistant/components/glances/icons.json | 6 + homeassistant/components/glances/sensor.py | 20 +- homeassistant/components/glances/strings.json | 6 + tests/components/glances/__init__.py | 167 +-------- .../glances/snapshots/test_sensor.ambr | 324 ++++++++++++++++++ 5 files changed, 360 insertions(+), 163 deletions(-) diff --git a/homeassistant/components/glances/icons.json b/homeassistant/components/glances/icons.json index 9f3e00540de..f6a1dcfea39 100644 --- a/homeassistant/components/glances/icons.json +++ b/homeassistant/components/glances/icons.json @@ -60,6 +60,12 @@ }, "gpu_memory_usage": { "default": "mdi:memory" + }, + "network_rx": { + "default": "mdi:transmission-tower" + }, + "network_tx": { + "default": "mdi:transmission-tower" } } } diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index 80671c0642e..c5706757725 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -265,6 +265,24 @@ SENSOR_TYPES = { native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), + ("network", "rx"): GlancesSensorEntityDescription( + key="rx", + type="network", + translation_key="network_rx", + native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, + suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND, + device_class=SensorDeviceClass.DATA_RATE, + state_class=SensorStateClass.MEASUREMENT, + ), + ("network", "tx"): GlancesSensorEntityDescription( + key="tx", + type="network", + translation_key="network_tx", + native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, + suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND, + device_class=SensorDeviceClass.DATA_RATE, + state_class=SensorStateClass.MEASUREMENT, + ), } @@ -279,7 +297,7 @@ async def async_setup_entry( entities: list[GlancesSensor] = [] for sensor_type, sensors in coordinator.data.items(): - if sensor_type in ["fs", "diskio", "sensors", "raid", "gpu"]: + if sensor_type in ["fs", "diskio", "sensors", "raid", "gpu", "network"]: entities.extend( GlancesSensor( coordinator, diff --git a/homeassistant/components/glances/strings.json b/homeassistant/components/glances/strings.json index 208d14563df..11735601ce9 100644 --- a/homeassistant/components/glances/strings.json +++ b/homeassistant/components/glances/strings.json @@ -115,6 +115,12 @@ }, "gpu_processor_usage": { "name": "{sensor_label} processor usage" + }, + "network_rx": { + "name": "{sensor_label} RX" + }, + "network_tx": { + "name": "{sensor_label} TX" } } }, diff --git a/tests/components/glances/__init__.py b/tests/components/glances/__init__.py index 6fbfc73b7be..beba7163bc2 100644 --- a/tests/components/glances/__init__.py +++ b/tests/components/glances/__init__.py @@ -12,168 +12,6 @@ MOCK_USER_INPUT: dict[str, Any] = { "verify_ssl": True, } -MOCK_DATA = { - "cpu": { - "total": 10.6, - "user": 7.6, - "system": 2.1, - "idle": 88.8, - "nice": 0.0, - "iowait": 0.6, - }, - "diskio": [ - { - "time_since_update": 1, - "disk_name": "nvme0n1", - "read_count": 12, - "write_count": 466, - "read_bytes": 184320, - "write_bytes": 23863296, - "key": "disk_name", - }, - ], - "docker": { - "containers": [ - { - "key": "name", - "name": "container1", - "Status": "running", - "cpu": {"total": 50.94973493230174}, - "cpu_percent": 50.94973493230174, - "memory": { - "usage": 1120321536, - "limit": 3976318976, - "rss": 480641024, - "cache": 580915200, - "max_usage": 1309597696, - }, - "memory_usage": 539406336, - }, - { - "key": "name", - "name": "container2", - "Status": "running", - "cpu": {"total": 26.23567931034483}, - "cpu_percent": 26.23567931034483, - "memory": { - "usage": 85139456, - "limit": 3976318976, - "rss": 33677312, - "cache": 35012608, - "max_usage": 87650304, - }, - "memory_usage": 50126848, - }, - ] - }, - "fs": [ - { - "device_name": "/dev/sda8", - "fs_type": "ext4", - "mnt_point": "/ssl", - "size": 511320748032, - "used": 32910458880, - "free": 457917374464, - "percent": 6.7, - "key": "mnt_point", - }, - { - "device_name": "/dev/sda8", - "fs_type": "ext4", - "mnt_point": "/media", - "size": 511320748032, - "used": 32910458880, - "free": 457917374464, - "percent": 6.7, - "key": "mnt_point", - }, - ], - "mem": { - "total": 3976318976, - "available": 2878337024, - "percent": 27.6, - "used": 1097981952, - "free": 2878337024, - "active": 567971840, - "inactive": 1679704064, - "buffers": 149807104, - "cached": 1334816768, - "shared": 1499136, - }, - "sensors": [ - { - "label": "cpu_thermal 1", - "value": 59, - "warning": None, - "critical": None, - "unit": "C", - "type": "temperature_core", - "key": "label", - }, - { - "label": "err_temp", - "value": "ERR", - "warning": None, - "critical": None, - "unit": "C", - "type": "temperature_hdd", - "key": "label", - }, - { - "label": "na_temp", - "value": "NA", - "warning": None, - "critical": None, - "unit": "C", - "type": "temperature_hdd", - "key": "label", - }, - ], - "system": { - "os_name": "Linux", - "hostname": "fedora-35", - "platform": "64bit", - "linux_distro": "Fedora Linux 35", - "os_version": "5.15.6-200.fc35.x86_64", - "hr_name": "Fedora Linux 35 64bit", - }, - "raid": { - "md3": { - "status": "active", - "type": "raid1", - "components": {"sdh1": "2", "sdi1": "0"}, - "available": "2", - "used": "2", - "config": "UU", - }, - "md1": { - "status": "active", - "type": "raid1", - "components": {"sdg": "0", "sde": "1"}, - "available": "2", - "used": "2", - "config": "UU", - }, - "md4": { - "status": "active", - "type": "raid1", - "components": {"sdf1": "1", "sdb1": "0"}, - "available": "2", - "used": "2", - "config": "UU", - }, - "md0": { - "status": "active", - "type": "raid1", - "components": {"sdc": "2", "sdd": "3"}, - "available": "2", - "used": "2", - "config": "UU", - }, - }, - "uptime": "3 days, 10:25:20", -} - MOCK_REFERENCE_DATE: datetime = datetime.fromisoformat("2024-02-13T14:13:12") HA_SENSOR_DATA: dict[str, Any] = { @@ -214,6 +52,11 @@ HA_SENSOR_DATA: dict[str, Any] = { "config": "UU", }, }, + "network": { + "lo": {"is_up": True, "rx": 7646, "tx": 7646, "speed": 0.0}, + "dummy0": {"is_up": False, "rx": 0.0, "tx": 0.0, "speed": 0.0}, + "eth0": {"is_up": True, "rx": 3953, "tx": 5995, "speed": 9.8}, + }, "uptime": "3 days, 10:25:20", "gpu": { "NVIDIA GeForce RTX 3080 (GPU 0)": { diff --git a/tests/components/glances/snapshots/test_sensor.ambr b/tests/components/glances/snapshots/test_sensor.ambr index 64ea8dd1052..662e95c6a1c 100644 --- a/tests/components/glances/snapshots/test_sensor.ambr +++ b/tests/components/glances/snapshots/test_sensor.ambr @@ -200,6 +200,114 @@ 'state': '59', }) # --- +# name: test_sensor_states[sensor.0_0_0_0_dummy0_rx-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.0_0_0_0_dummy0_rx', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'dummy0 RX', + 'platform': 'glances', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'network_rx', + 'unique_id': 'test-dummy0-rx', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_dummy0_rx-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': '0.0.0.0 dummy0 RX', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.0_0_0_0_dummy0_rx', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.000000', + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_dummy0_tx-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.0_0_0_0_dummy0_tx', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'dummy0 TX', + 'platform': 'glances', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'network_tx', + 'unique_id': 'test-dummy0-tx', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_dummy0_tx-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': '0.0.0.0 dummy0 TX', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.0_0_0_0_dummy0_tx', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.000000', + }) +# --- # name: test_sensor_states[sensor.0_0_0_0_err_temp_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -251,6 +359,222 @@ 'state': 'unavailable', }) # --- +# name: test_sensor_states[sensor.0_0_0_0_eth0_rx-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.0_0_0_0_eth0_rx', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'eth0 RX', + 'platform': 'glances', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'network_rx', + 'unique_id': 'test-eth0-rx', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_eth0_rx-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': '0.0.0.0 eth0 RX', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.0_0_0_0_eth0_rx', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.03162', + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_eth0_tx-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.0_0_0_0_eth0_tx', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'eth0 TX', + 'platform': 'glances', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'network_tx', + 'unique_id': 'test-eth0-tx', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_eth0_tx-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': '0.0.0.0 eth0 TX', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.0_0_0_0_eth0_tx', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.04796', + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_lo_rx-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.0_0_0_0_lo_rx', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'lo RX', + 'platform': 'glances', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'network_rx', + 'unique_id': 'test-lo-rx', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_lo_rx-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': '0.0.0.0 lo RX', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.0_0_0_0_lo_rx', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.06117', + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_lo_tx-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.0_0_0_0_lo_tx', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'lo TX', + 'platform': 'glances', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'network_tx', + 'unique_id': 'test-lo-tx', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[sensor.0_0_0_0_lo_tx-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_rate', + 'friendly_name': '0.0.0.0 lo TX', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.0_0_0_0_lo_tx', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.06117', + }) +# --- # name: test_sensor_states[sensor.0_0_0_0_md1_available-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From c270ab00599646aa413337584c6002c2f80d5066 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 7 Apr 2024 19:29:19 +0200 Subject: [PATCH 378/967] Correct duplicate word in IMAP translations (#115132) --- homeassistant/components/imap/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/imap/strings.json b/homeassistant/components/imap/strings.json index ac06d833f55..3a20fc244c6 100644 --- a/homeassistant/components/imap/strings.json +++ b/homeassistant/components/imap/strings.json @@ -40,10 +40,10 @@ "message": "Copying the message failed with \"{error}\"." }, "delete_failed": { - "message": "Marking the the message for deletion failed with \"{error}\"." + "message": "Marking the message for deletion failed with \"{error}\"." }, "expunge_failed": { - "message": "Expunging the the message failed with \"{error}\"." + "message": "Expunging the message failed with \"{error}\"." }, "invalid_entry": { "message": "No valid IMAP entry was found." From 3239351f186a90181b445ee3e26204657da9f6d5 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Sun, 7 Apr 2024 19:32:15 +0200 Subject: [PATCH 379/967] Bump velbus-aio to 2024.4.1 (#115109) bump velbusaio to 2024.4.1 --- homeassistant/components/velbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index 1c51c58d238..6f817a23325 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -13,7 +13,7 @@ "velbus-packet", "velbus-protocol" ], - "requirements": ["velbus-aio==2024.4.0"], + "requirements": ["velbus-aio==2024.4.1"], "usb": [ { "vid": "10CF", diff --git a/requirements_all.txt b/requirements_all.txt index ccb9ad11b29..393b9188d96 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2805,7 +2805,7 @@ vallox-websocket-api==5.1.1 vehicle==2.2.1 # homeassistant.components.velbus -velbus-aio==2024.4.0 +velbus-aio==2024.4.1 # homeassistant.components.venstar venstarcolortouch==0.19 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 40d0fd6788d..4edb38b9e8b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2161,7 +2161,7 @@ vallox-websocket-api==5.1.1 vehicle==2.2.1 # homeassistant.components.velbus -velbus-aio==2024.4.0 +velbus-aio==2024.4.1 # homeassistant.components.venstar venstarcolortouch==0.19 From 9ec4e9a1a90e4c8663487835192eb0697bc47dcb Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 7 Apr 2024 19:38:56 +0200 Subject: [PATCH 380/967] Remove Color extractor import flow (#115015) * Remove Color extractor import flow * Remove Color extractor import flow --- .../components/color_extractor/__init__.py | 14 +------ .../components/color_extractor/config_flow.py | 38 ------------------- .../components/color_extractor/strings.json | 6 --- .../color_extractor/test_config_flow.py | 28 ++------------ tests/components/color_extractor/test_init.py | 18 --------- 5 files changed, 5 insertions(+), 99 deletions(-) delete mode 100644 tests/components/color_extractor/test_init.py diff --git a/homeassistant/components/color_extractor/__init__.py b/homeassistant/components/color_extractor/__init__.py index be9b80e8f52..81cd55564b9 100644 --- a/homeassistant/components/color_extractor/__init__.py +++ b/homeassistant/components/color_extractor/__init__.py @@ -14,7 +14,7 @@ from homeassistant.components.light import ( DOMAIN as LIGHT_DOMAIN, LIGHT_TURN_ON_SCHEMA, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import SERVICE_TURN_ON as LIGHT_SERVICE_TURN_ON from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import aiohttp_client @@ -25,10 +25,7 @@ from .const import ATTR_PATH, ATTR_URL, DOMAIN, SERVICE_TURN_ON _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = vol.Schema( - {vol.Optional(DOMAIN): {}}, - extra=vol.ALLOW_EXTRA, -) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) # Extend the existing light.turn_on service schema SERVICE_SCHEMA = vol.All( @@ -156,13 +153,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: _file = _get_file(file_path) return _get_color(_file) - if DOMAIN in config: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data={} - ) - ) - return True diff --git a/homeassistant/components/color_extractor/config_flow.py b/homeassistant/components/color_extractor/config_flow.py index de1f9cb35be..aacb07d8982 100644 --- a/homeassistant/components/color_extractor/config_flow.py +++ b/homeassistant/components/color_extractor/config_flow.py @@ -5,9 +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.data_entry_flow import FlowResultType -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from .const import DEFAULT_NAME, DOMAIN @@ -28,38 +25,3 @@ class ColorExtractorConfigFlow(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]) -> ConfigFlowResult: - """Handle import from configuration.yaml.""" - result = await self.async_step_user(user_input) - if result["type"] == FlowResultType.CREATE_ENTRY: - async_create_issue( - self.hass, - HOMEASSISTANT_DOMAIN, - f"deprecated_yaml_{DOMAIN}", - breaks_in_ha_version="2024.4.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "Color extractor", - }, - ) - else: - async_create_issue( - self.hass, - DOMAIN, - "deprecated_yaml", - breaks_in_ha_version="2024.4.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "Color extractor", - }, - ) - return result diff --git a/homeassistant/components/color_extractor/strings.json b/homeassistant/components/color_extractor/strings.json index aa5fd5f4ef7..f66c448f7c2 100644 --- a/homeassistant/components/color_extractor/strings.json +++ b/homeassistant/components/color_extractor/strings.json @@ -9,12 +9,6 @@ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" } }, - "issues": { - "deprecated_yaml": { - "title": "The {integration_title} YAML configuration is being removed", - "description": "Configuring {integration_title} using YAML is being removed.\n\nYour configuration is already imported.\n\nRemove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue." - } - }, "services": { "turn_on": { "name": "[%key:common::action::turn_on%]", diff --git a/tests/components/color_extractor/test_config_flow.py b/tests/components/color_extractor/test_config_flow.py index 886c7991177..972b78b3f59 100644 --- a/tests/components/color_extractor/test_config_flow.py +++ b/tests/components/color_extractor/test_config_flow.py @@ -2,10 +2,8 @@ from unittest.mock import patch -import pytest - from homeassistant.components.color_extractor.const import DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -37,35 +35,15 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 1 -@pytest.mark.parametrize("source", [SOURCE_USER, SOURCE_IMPORT]) -async def test_single_instance_allowed( - hass: HomeAssistant, - source: str, -) -> None: +async def test_single_instance_allowed(hass: HomeAssistant) -> None: """Test we abort if already setup.""" mock_config_entry = MockConfigEntry(domain=DOMAIN) mock_config_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": source}, data={} + DOMAIN, context={"source": SOURCE_USER}, data={} ) assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" - - -async def test_import_flow( - hass: HomeAssistant, -) -> None: - """Test the import configuration flow.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={}, - ) - - assert result.get("type") is FlowResultType.CREATE_ENTRY - assert result.get("title") == "Color extractor" - assert result.get("data") == {} - assert result.get("options") == {} diff --git a/tests/components/color_extractor/test_init.py b/tests/components/color_extractor/test_init.py deleted file mode 100644 index cf4354db48d..00000000000 --- a/tests/components/color_extractor/test_init.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Test Color extractor component setup process.""" - -from homeassistant.components.color_extractor import DOMAIN -from homeassistant.config_entries import ConfigEntryState -from homeassistant.core import HomeAssistant -from homeassistant.helpers import issue_registry as ir -from homeassistant.setup import async_setup_component - - -async def test_legacy_migration(hass: HomeAssistant) -> None: - """Test migration from yaml to config flow.""" - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - await hass.async_block_till_done() - entries = hass.config_entries.async_entries(DOMAIN) - assert len(entries) == 1 - assert entries[0].state is ConfigEntryState.LOADED - issue_registry = ir.async_get(hass) - assert len(issue_registry.issues) == 1 From 80ec9d43941690b29b50979869c16f8ba215a6b1 Mon Sep 17 00:00:00 2001 From: dontinelli <73341522+dontinelli@users.noreply.github.com> Date: Sun, 7 Apr 2024 21:07:51 +0200 Subject: [PATCH 381/967] improve handling of incorrect values in fyta integration (#115134) * improve handling of incorrect values * Changes based on review comment * Apply suggestions from code review Co-authored-by: Joost Lekkerkerker * update value_fn * ruff --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/fyta/sensor.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/fyta/sensor.py b/homeassistant/components/fyta/sensor.py index 0643c69981e..2b9e8e3de07 100644 --- a/homeassistant/components/fyta/sensor.py +++ b/homeassistant/components/fyta/sensor.py @@ -46,35 +46,35 @@ SENSORS: Final[list[FytaSensorEntityDescription]] = [ translation_key="plant_status", device_class=SensorDeviceClass.ENUM, options=PLANT_STATUS_LIST, - value_fn=lambda value: PLANT_STATUS[value], + value_fn=PLANT_STATUS.get, ), FytaSensorEntityDescription( key="temperature_status", translation_key="temperature_status", device_class=SensorDeviceClass.ENUM, options=PLANT_STATUS_LIST, - value_fn=lambda value: PLANT_STATUS[value], + value_fn=PLANT_STATUS.get, ), FytaSensorEntityDescription( key="light_status", translation_key="light_status", device_class=SensorDeviceClass.ENUM, options=PLANT_STATUS_LIST, - value_fn=lambda value: PLANT_STATUS[value], + value_fn=PLANT_STATUS.get, ), FytaSensorEntityDescription( key="moisture_status", translation_key="moisture_status", device_class=SensorDeviceClass.ENUM, options=PLANT_STATUS_LIST, - value_fn=lambda value: PLANT_STATUS[value], + value_fn=PLANT_STATUS.get, ), FytaSensorEntityDescription( key="salinity_status", translation_key="salinity_status", device_class=SensorDeviceClass.ENUM, options=PLANT_STATUS_LIST, - value_fn=lambda value: PLANT_STATUS[value], + value_fn=PLANT_STATUS.get, ), FytaSensorEntityDescription( key="temperature", From 05440ec04cd146035d4682ab8ac9de7c4e696b6c Mon Sep 17 00:00:00 2001 From: dontinelli <73341522+dontinelli@users.noreply.github.com> Date: Sun, 7 Apr 2024 21:38:13 +0200 Subject: [PATCH 382/967] Bump fyta_cli to 0.3.5 (#115143) bump fyta_cli to 0.3.5 --- homeassistant/components/fyta/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/fyta/manifest.json b/homeassistant/components/fyta/manifest.json index a93a76a9e1d..55255777994 100644 --- a/homeassistant/components/fyta/manifest.json +++ b/homeassistant/components/fyta/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/fyta", "integration_type": "hub", "iot_class": "cloud_polling", - "requirements": ["fyta_cli==0.3.3"] + "requirements": ["fyta_cli==0.3.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index 393b9188d96..eea298047e5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -903,7 +903,7 @@ freesms==0.2.0 fritzconnection[qr]==1.13.2 # homeassistant.components.fyta -fyta_cli==0.3.3 +fyta_cli==0.3.5 # homeassistant.components.google_translate gTTS==2.2.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4edb38b9e8b..33862281865 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -735,7 +735,7 @@ freebox-api==1.1.0 fritzconnection[qr]==1.13.2 # homeassistant.components.fyta -fyta_cli==0.3.3 +fyta_cli==0.3.5 # homeassistant.components.google_translate gTTS==2.2.4 From 771fe57e328e953f118c16caa0c6558d3a733058 Mon Sep 17 00:00:00 2001 From: rappenze Date: Sun, 7 Apr 2024 22:53:30 +0200 Subject: [PATCH 383/967] Fix fibaro sensor additional sensor lookup (#115148) --- homeassistant/components/fibaro/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/fibaro/sensor.py b/homeassistant/components/fibaro/sensor.py index 6e672e9cc97..fd6ec74050d 100644 --- a/homeassistant/components/fibaro/sensor.py +++ b/homeassistant/components/fibaro/sensor.py @@ -121,6 +121,7 @@ async def async_setup_entry( Platform.COVER, Platform.LIGHT, Platform.LOCK, + Platform.SENSOR, Platform.SWITCH, ) for device in controller.fibaro_devices[platform] From 569f54d8e35124149dca2285c463d49e413affd3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 7 Apr 2024 11:02:53 -1000 Subject: [PATCH 384/967] Terminate scripts with until and while conditions that execute more than 10000 times (#115110) --- homeassistant/helpers/script.py | 63 +++++++++++++++++++++++++++++++++ tests/helpers/test_script.py | 52 +++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 0fee9fae322..3c364ed8892 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -280,6 +280,9 @@ STATIC_VALIDATION_ACTION_TYPES = ( cv.SCRIPT_ACTION_WAIT_TEMPLATE, ) +REPEAT_WARN_ITERATIONS = 5000 +REPEAT_TERMINATE_ITERATIONS = 10000 + async def async_validate_actions_config( hass: HomeAssistant, actions: list[ConfigType] @@ -839,6 +842,7 @@ class _ScriptRun: # pylint: disable-next=protected-access script = self._script._get_repeat_script(self._step) + warned_too_many_loops = False async def async_run_sequence(iteration, extra_msg=""): self._log("Repeating %s: Iteration %i%s", description, iteration, extra_msg) @@ -909,6 +913,36 @@ class _ScriptRun: _LOGGER.warning("Error in 'while' evaluation:\n%s", ex) break + if iteration > 1: + if iteration > REPEAT_WARN_ITERATIONS: + if not warned_too_many_loops: + warned_too_many_loops = True + _LOGGER.warning( + "While condition %s in script `%s` looped %s times", + repeat[CONF_WHILE], + self._script.name, + REPEAT_WARN_ITERATIONS, + ) + + if iteration > REPEAT_TERMINATE_ITERATIONS: + _LOGGER.critical( + "While condition %s in script `%s` " + "terminated because it looped %s times", + repeat[CONF_WHILE], + self._script.name, + REPEAT_TERMINATE_ITERATIONS, + ) + raise _AbortScript( + f"While condition {repeat[CONF_WHILE]} " + "terminated because it looped " + f" {REPEAT_TERMINATE_ITERATIONS} times" + ) + + # If the user creates a script with a tight loop, + # yield to the event loop so the system stays + # responsive while all the cpu time is consumed. + await asyncio.sleep(0) + await async_run_sequence(iteration) elif CONF_UNTIL in repeat: @@ -927,6 +961,35 @@ class _ScriptRun: _LOGGER.warning("Error in 'until' evaluation:\n%s", ex) break + if iteration >= REPEAT_WARN_ITERATIONS: + if not warned_too_many_loops: + warned_too_many_loops = True + _LOGGER.warning( + "Until condition %s in script `%s` looped %s times", + repeat[CONF_UNTIL], + self._script.name, + REPEAT_WARN_ITERATIONS, + ) + + if iteration >= REPEAT_TERMINATE_ITERATIONS: + _LOGGER.critical( + "Until condition %s in script `%s` " + "terminated because it looped %s times", + repeat[CONF_UNTIL], + self._script.name, + REPEAT_TERMINATE_ITERATIONS, + ) + raise _AbortScript( + f"Until condition {repeat[CONF_UNTIL]} " + "terminated because it looped " + f"{REPEAT_TERMINATE_ITERATIONS} times" + ) + + # If the user creates a script with a tight loop, + # yield to the event loop so the system stays responsive + # while all the cpu time is consumed. + await asyncio.sleep(0) + if saved_repeat_vars: self._variables["repeat"] = saved_repeat_vars else: diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 53499c4f88c..cdc5a6092c4 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -2837,6 +2837,58 @@ async def test_repeat_nested( assert_action_trace(expected_trace) +@pytest.mark.parametrize( + ("condition", "check"), [("while", "above"), ("until", "below")] +) +async def test_repeat_limits( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, condition: str, check: str +) -> None: + """Test limits on repeats prevent the system from hanging.""" + event = "test_event" + events = async_capture_events(hass, event) + hass.states.async_set("sensor.test", "0.5") + + sequence = { + "repeat": { + "sequence": [ + { + "event": event, + }, + ], + } + } + sequence["repeat"][condition] = { + "condition": "numeric_state", + "entity_id": "sensor.test", + check: "0", + } + + with ( + patch.object(script, "REPEAT_WARN_ITERATIONS", 5), + patch.object(script, "REPEAT_TERMINATE_ITERATIONS", 10), + ): + script_obj = script.Script( + hass, cv.SCRIPT_SCHEMA(sequence), f"Test {condition}", "test_domain" + ) + + caplog.clear() + caplog.set_level(logging.WARNING) + + hass.async_create_task(script_obj.async_run(context=Context())) + await asyncio.wait_for(hass.async_block_till_done(), 1) + + title_condition = condition.title() + + assert f"{title_condition} condition" in caplog.text + assert f"in script `Test {condition}` looped 5 times" in caplog.text + assert ( + f"script `Test {condition}` terminated because it looped 10 times" + in caplog.text + ) + + assert len(events) == 10 + + async def test_choose_warning( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: From 8e98ba731239e0f9e050b62bbf6ff336c92eb48a Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Sun, 7 Apr 2024 23:30:50 +0200 Subject: [PATCH 385/967] Add first batch of Ruff PYI rules (#115100) Co-authored-by: Jan Bouwhuis --- homeassistant/auth/permissions/__init__.py | 3 +-- homeassistant/components/assist_pipeline/pipeline.py | 2 +- homeassistant/components/control4/director_utils.py | 5 ++--- homeassistant/components/deconz/binary_sensor.py | 2 -- .../components/here_travel_time/__init__.py | 2 +- .../components/homekit_controller/switch.py | 2 +- homeassistant/components/mobile_app/helpers.py | 6 +++--- homeassistant/components/mqtt/__init__.py | 4 +--- homeassistant/components/noaa_tides/sensor.py | 2 +- homeassistant/components/rainbird/coordinator.py | 3 --- homeassistant/components/recorder/statistics.py | 8 ++++---- .../recorder/table_managers/statistics_meta.py | 6 +++--- homeassistant/components/recorder/websocket_api.py | 2 +- homeassistant/components/zha/core/helpers.py | 12 ++++-------- homeassistant/core.py | 6 +++--- homeassistant/helpers/httpx_client.py | 2 +- homeassistant/helpers/intent.py | 3 +-- homeassistant/helpers/reload.py | 2 +- homeassistant/helpers/template.py | 5 +++-- homeassistant/helpers/typing.py | 4 +--- homeassistant/util/dt.py | 2 +- homeassistant/util/signal_type.py | 2 +- homeassistant/util/yaml/loader.py | 2 +- pyproject.toml | 4 ++++ tests/common.py | 4 ++-- tests/components/recorder/db_schema_25.py | 2 +- tests/components/recorder/db_schema_28.py | 2 +- 27 files changed, 44 insertions(+), 55 deletions(-) diff --git a/homeassistant/auth/permissions/__init__.py b/homeassistant/auth/permissions/__init__.py index c0574e9f0ea..9c2c7e500ca 100644 --- a/homeassistant/auth/permissions/__init__.py +++ b/homeassistant/auth/permissions/__init__.py @@ -3,7 +3,6 @@ from __future__ import annotations from collections.abc import Callable -from typing import Any import voluptuous as vol @@ -64,7 +63,7 @@ class PolicyPermissions(AbstractPermissions): """Return a function that can test entity access.""" return compile_entities(self._policy.get(CAT_ENTITIES), self._perm_lookup) - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: """Equals check.""" return isinstance(other, PolicyPermissions) and other._policy == self._policy diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py index 33e1b8c2f76..0d25950d65b 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -587,7 +587,7 @@ class PipelineRun: self.audio_settings.noise_suppression_level, ) - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: """Compare pipeline runs by id.""" if isinstance(other, PipelineRun): return self.id == other.id diff --git a/homeassistant/components/control4/director_utils.py b/homeassistant/components/control4/director_utils.py index 2ce03c2e635..10e9486ee89 100644 --- a/homeassistant/components/control4/director_utils.py +++ b/homeassistant/components/control4/director_utils.py @@ -1,7 +1,6 @@ """Provides data updates from the Control4 controller for platforms.""" from collections import defaultdict -from collections.abc import Set import logging from typing import Any @@ -20,7 +19,7 @@ _LOGGER = logging.getLogger(__name__) async def _update_variables_for_config_entry( - hass: HomeAssistant, entry: ConfigEntry, variable_names: Set[str] + hass: HomeAssistant, entry: ConfigEntry, variable_names: set[str] ) -> dict[int, dict[str, Any]]: """Retrieve data from the Control4 director.""" director: C4Director = hass.data[DOMAIN][entry.entry_id][CONF_DIRECTOR] @@ -32,7 +31,7 @@ async def _update_variables_for_config_entry( async def update_variables_for_config_entry( - hass: HomeAssistant, entry: ConfigEntry, variable_names: Set[str] + hass: HomeAssistant, entry: ConfigEntry, variable_names: set[str] ) -> dict[int, dict[str, Any]]: """Try to Retrieve data from the Control4 director for update_coordinator.""" try: diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index eaa89c6eb9c..02f6ada8fc8 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -33,8 +33,6 @@ from .const import ATTR_DARK, ATTR_ON from .deconz_device import DeconzDevice from .hub import DeconzHub -_SensorDeviceT = TypeVar("_SensorDeviceT", bound=PydeconzSensorBase) - ATTR_ORIENTATION = "orientation" ATTR_TILTANGLE = "tiltangle" ATTR_VIBRATIONSTRENGTH = "vibrationstrength" diff --git a/homeassistant/components/here_travel_time/__init__.py b/homeassistant/components/here_travel_time/__init__.py index 9da1ce491f0..1b99ba64827 100644 --- a/homeassistant/components/here_travel_time/__init__.py +++ b/homeassistant/components/here_travel_time/__init__.py @@ -50,7 +50,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b departure=departure, ) - cls: type[HERETransitDataUpdateCoordinator] | type[HERERoutingDataUpdateCoordinator] + cls: type[HERETransitDataUpdateCoordinator | HERERoutingDataUpdateCoordinator] if config_entry.data[CONF_MODE] in {TRAVEL_MODE_PUBLIC, "publicTransportTimeTable"}: cls = HERETransitDataUpdateCoordinator else: diff --git a/homeassistant/components/homekit_controller/switch.py b/homeassistant/components/homekit_controller/switch.py index 65f98ed8f5e..9fa4782e061 100644 --- a/homeassistant/components/homekit_controller/switch.py +++ b/homeassistant/components/homekit_controller/switch.py @@ -192,7 +192,7 @@ class DeclarativeCharacteristicSwitch(CharacteristicEntity, SwitchEntity): ) -ENTITY_TYPES: dict[str, type[HomeKitSwitch] | type[HomeKitValve]] = { +ENTITY_TYPES: dict[str, type[HomeKitSwitch | HomeKitValve]] = { ServicesTypes.SWITCH: HomeKitSwitch, ServicesTypes.OUTLET: HomeKitSwitch, ServicesTypes.VALVE: HomeKitValve, diff --git a/homeassistant/components/mobile_app/helpers.py b/homeassistant/components/mobile_app/helpers.py index 7f88074bf34..0ecfe207277 100644 --- a/homeassistant/components/mobile_app/helpers.py +++ b/homeassistant/components/mobile_app/helpers.py @@ -38,7 +38,7 @@ _LOGGER = logging.getLogger(__name__) def setup_decrypt( - key_encoder: type[RawEncoder] | type[HexEncoder], + key_encoder: type[RawEncoder | HexEncoder], ) -> Callable[[bytes, bytes], bytes]: """Return decryption function and length of key. @@ -55,7 +55,7 @@ def setup_decrypt( def setup_encrypt( - key_encoder: type[RawEncoder] | type[HexEncoder], + key_encoder: type[RawEncoder | HexEncoder], ) -> Callable[[bytes, bytes], bytes]: """Return encryption function and length of key. @@ -75,7 +75,7 @@ def _decrypt_payload_helper( key: str | bytes, ciphertext: bytes, key_bytes: bytes, - key_encoder: type[RawEncoder] | type[HexEncoder], + key_encoder: type[RawEncoder | HexEncoder], ) -> JsonValueType | None: """Decrypt encrypted payload.""" try: diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 8e866776a41..28cb7d0944b 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -6,7 +6,7 @@ import asyncio from collections.abc import Callable from datetime import datetime import logging -from typing import TYPE_CHECKING, Any, TypeVar, cast +from typing import TYPE_CHECKING, Any, cast import voluptuous as vol @@ -143,8 +143,6 @@ CONFIG_ENTRY_CONFIG_KEYS = [ CONF_WILL_MESSAGE, ] -_T = TypeVar("_T") - REMOVED_OPTIONS = vol.All( cv.removed(CONF_BIRTH_MESSAGE), # Removed in HA Core 2023.4 cv.removed(CONF_BROKER), # Removed in HA Core 2023.4 diff --git a/homeassistant/components/noaa_tides/sensor.py b/homeassistant/components/noaa_tides/sensor.py index 235263345e6..5e213e847ba 100644 --- a/homeassistant/components/noaa_tides/sensor.py +++ b/homeassistant/components/noaa_tides/sensor.py @@ -85,7 +85,7 @@ class NOAATidesData(TypedDict): """Representation of a single tide.""" time_stamp: list[Timestamp] - hi_lo: list[Literal["L"] | Literal["H"]] + hi_lo: list[Literal["L", "H"]] predicted_wl: list[float] diff --git a/homeassistant/components/rainbird/coordinator.py b/homeassistant/components/rainbird/coordinator.py index 70365c2f095..83db2d584d2 100644 --- a/homeassistant/components/rainbird/coordinator.py +++ b/homeassistant/components/rainbird/coordinator.py @@ -7,7 +7,6 @@ from dataclasses import dataclass import datetime from functools import cached_property import logging -from typing import TypeVar import aiohttp from pyrainbird.async_client import ( @@ -39,8 +38,6 @@ CONECTION_LIMIT = 1 _LOGGER = logging.getLogger(__name__) -_T = TypeVar("_T") - @dataclass class RainbirdDeviceState: diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index e70a52c36f1..41cf4e22b53 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -684,7 +684,7 @@ def get_metadata_with_session( session: Session, *, statistic_ids: set[str] | None = None, - statistic_type: Literal["mean"] | Literal["sum"] | None = None, + statistic_type: Literal["mean", "sum"] | None = None, statistic_source: str | None = None, ) -> dict[str, tuple[int, StatisticMetaData]]: """Fetch meta data. @@ -705,7 +705,7 @@ def get_metadata( hass: HomeAssistant, *, statistic_ids: set[str] | None = None, - statistic_type: Literal["mean"] | Literal["sum"] | None = None, + statistic_type: Literal["mean", "sum"] | None = None, statistic_source: str | None = None, ) -> dict[str, tuple[int, StatisticMetaData]]: """Return metadata for statistic_ids.""" @@ -753,7 +753,7 @@ def update_statistics_metadata( async def async_list_statistic_ids( hass: HomeAssistant, statistic_ids: set[str] | None = None, - statistic_type: Literal["mean"] | Literal["sum"] | None = None, + statistic_type: Literal["mean", "sum"] | None = None, ) -> list[dict]: """Return all statistic_ids (or filtered one) and unit of measurement. @@ -823,7 +823,7 @@ def _flatten_list_statistic_ids_metadata_result( def list_statistic_ids( hass: HomeAssistant, statistic_ids: set[str] | None = None, - statistic_type: Literal["mean"] | Literal["sum"] | None = None, + statistic_type: Literal["mean", "sum"] | None = None, ) -> list[dict]: """Return all statistic_ids (or filtered one) and unit of measurement. diff --git a/homeassistant/components/recorder/table_managers/statistics_meta.py b/homeassistant/components/recorder/table_managers/statistics_meta.py index 32e989b0e3d..9b33eff0c9b 100644 --- a/homeassistant/components/recorder/table_managers/statistics_meta.py +++ b/homeassistant/components/recorder/table_managers/statistics_meta.py @@ -36,7 +36,7 @@ QUERY_STATISTIC_META = ( def _generate_get_metadata_stmt( statistic_ids: set[str] | None = None, - statistic_type: Literal["mean"] | Literal["sum"] | None = None, + statistic_type: Literal["mean", "sum"] | None = None, statistic_source: str | None = None, ) -> StatementLambdaElement: """Generate a statement to fetch metadata.""" @@ -88,7 +88,7 @@ class StatisticsMetaManager: self, session: Session, statistic_ids: set[str] | None = None, - statistic_type: Literal["mean"] | Literal["sum"] | None = None, + statistic_type: Literal["mean", "sum"] | None = None, statistic_source: str | None = None, ) -> dict[str, tuple[int, StatisticMetaData]]: """Fetch meta data and process it into results and/or cache.""" @@ -202,7 +202,7 @@ class StatisticsMetaManager: self, session: Session, statistic_ids: set[str] | None = None, - statistic_type: Literal["mean"] | Literal["sum"] | None = None, + statistic_type: Literal["mean", "sum"] | None = None, statistic_source: str | None = None, ) -> dict[str, tuple[int, StatisticMetaData]]: """Fetch meta data. diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index 79104485e19..58c362df62e 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -235,7 +235,7 @@ async def ws_get_statistics_during_period( def _ws_get_list_statistic_ids( hass: HomeAssistant, msg_id: int, - statistic_type: Literal["mean"] | Literal["sum"] | None = None, + statistic_type: Literal["mean", "sum"] | None = None, ) -> bytes: """Fetch a list of available statistic_id and convert them to JSON. diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index b060d56cb04..b44fa9e83e1 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -14,7 +14,7 @@ from dataclasses import dataclass import enum import logging import re -from typing import TYPE_CHECKING, Any, ParamSpec, TypeVar, overload +from typing import TYPE_CHECKING, Any, TypeVar, overload import voluptuous as vol import zigpy.exceptions @@ -59,14 +59,10 @@ from .const import CLUSTER_TYPE_IN, CLUSTER_TYPE_OUT, CUSTOM_CONFIGURATION, DATA from .registries import BINDABLE_CLUSTERS if TYPE_CHECKING: - from .cluster_handlers import ClusterHandler from .device import ZHADevice from .gateway import ZHAGateway -_ClusterHandlerT = TypeVar("_ClusterHandlerT", bound="ClusterHandler") _T = TypeVar("_T") -_R = TypeVar("_R") -_P = ParamSpec("_P") _LOGGER = logging.getLogger(__name__) @@ -508,9 +504,9 @@ def validate_device_class( def validate_device_class( - device_class_enum: type[BinarySensorDeviceClass] - | type[SensorDeviceClass] - | type[NumberDeviceClass], + device_class_enum: type[ + BinarySensorDeviceClass | SensorDeviceClass | NumberDeviceClass + ], metadata_value: enum.Enum, platform: str, logger: logging.Logger, diff --git a/homeassistant/core.py b/homeassistant/core.py index aa3b5c8434f..ccea82a7eb9 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -381,7 +381,7 @@ class HomeAssistant: http: HomeAssistantHTTP = None # type: ignore[assignment] config_entries: ConfigEntries = None # type: ignore[assignment] - def __new__(cls, config_dir: str) -> HomeAssistant: + def __new__(cls, config_dir: str) -> Self: """Set the _hass thread local data.""" hass = super().__new__(cls) _hass.hass = hass @@ -1168,9 +1168,9 @@ class Context: self.parent_id = parent_id self.origin_event: Event[Any] | None = None - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: """Compare contexts.""" - return bool(self.__class__ == other.__class__ and self.id == other.id) + return isinstance(other, Context) and self.id == other.id @cached_property def _as_dict(self) -> dict[str, str | None]: diff --git a/homeassistant/helpers/httpx_client.py b/homeassistant/helpers/httpx_client.py index 2855705b9c1..a0112ae0843 100644 --- a/homeassistant/helpers/httpx_client.py +++ b/homeassistant/helpers/httpx_client.py @@ -57,7 +57,7 @@ class HassHttpXAsyncClient(httpx.AsyncClient): """Prevent an integration from reopen of the client via context manager.""" return self - async def __aexit__(self, *args: Any) -> None: + async def __aexit__(self, *args: object) -> None: """Prevent an integration from close of the client via context manager.""" diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index 5fc80bedbed..ab747043a04 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -10,7 +10,7 @@ from dataclasses import dataclass from enum import Enum from functools import cached_property import logging -from typing import Any, TypeVar +from typing import Any import voluptuous as vol @@ -34,7 +34,6 @@ from . import ( _LOGGER = logging.getLogger(__name__) _SlotsType = dict[str, Any] -_T = TypeVar("_T") INTENT_TURN_OFF = "HassTurnOff" INTENT_TURN_ON = "HassTurnOn" diff --git a/homeassistant/helpers/reload.py b/homeassistant/helpers/reload.py index ffd6bdeb50d..cdd53731d6e 100644 --- a/homeassistant/helpers/reload.py +++ b/homeassistant/helpers/reload.py @@ -156,7 +156,7 @@ async def async_integration_yaml_config( hass: HomeAssistant, integration_name: str, *, - raise_on_failure: Literal[False] | bool, + raise_on_failure: Literal[False], ) -> ConfigType | None: ... diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 0f2dd735a66..63a700e031b 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -28,6 +28,7 @@ from typing import ( Literal, NoReturn, ParamSpec, + Self, TypeVar, cast, overload, @@ -310,7 +311,7 @@ class TupleWrapper(tuple, ResultWrapper): # This is all magic to be allowed to subclass a tuple. - def __new__(cls, value: tuple, *, render_result: str | None = None) -> TupleWrapper: + def __new__(cls, value: tuple, *, render_result: str | None = None) -> Self: """Create a new tuple class.""" return super().__new__(cls, tuple(value)) @@ -1102,7 +1103,7 @@ class TemplateStateBase(State): return f"{state} {unit}" return state - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: """Ensure we collect on equality check.""" self._collect_state() return self._state.__eq__(other) diff --git a/homeassistant/helpers/typing.py b/homeassistant/helpers/typing.py index 8b1b4addcdb..9bc34a09066 100644 --- a/homeassistant/helpers/typing.py +++ b/homeassistant/helpers/typing.py @@ -3,7 +3,7 @@ from collections.abc import Mapping from enum import Enum from functools import partial -from typing import Any, TypeVar +from typing import Any import homeassistant.core @@ -14,8 +14,6 @@ from .deprecation import ( dir_with_deprecated_constants, ) -_DataT = TypeVar("_DataT") - GPSType = tuple[float, float] ConfigType = dict[str, Any] DiscoveryInfoType = dict[str, Any] diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index e85a302f371..2f2b415144f 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -188,7 +188,7 @@ def parse_datetime(dt_str: str, *, raise_on_error: Literal[True]) -> dt.datetime @overload def parse_datetime( - dt_str: str, *, raise_on_error: Literal[False] | bool + dt_str: str, *, raise_on_error: Literal[False] ) -> dt.datetime | None: ... diff --git a/homeassistant/util/signal_type.py b/homeassistant/util/signal_type.py index be634ce6ba9..e2730c969c4 100644 --- a/homeassistant/util/signal_type.py +++ b/homeassistant/util/signal_type.py @@ -19,7 +19,7 @@ class _SignalTypeBase(Generic[*_Ts]): return hash(self.name) - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: """Check equality for dict keys to be compatible with str.""" if isinstance(other, str): diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index d07b578628c..79ee2797ad9 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -276,7 +276,7 @@ def _parse_yaml_python( def _parse_yaml( - loader: type[FastSafeLoader] | type[PythonSafeLoader], + loader: type[FastSafeLoader | PythonSafeLoader], content: str | TextIO, secrets: Secrets | None = None, ) -> JSON_TYPE: diff --git a/pyproject.toml b/pyproject.toml index f9b289063ed..b7aca115f44 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -638,6 +638,7 @@ select = [ "PIE", # flake8-pie "PL", # pylint "PT", # flake8-pytest-style + "PYI", # flake8-pyi "RET", # flake8-return "RSE", # flake8-raise "RUF005", # Consider iterable unpacking instead of concatenation @@ -719,6 +720,9 @@ ignore = [ # temporarily disabled "PT019", + "PYI024", # Use typing.NamedTuple instead of collections.namedtuple + "PYI036", + "PYI041", "RET503", "RET502", "RET501", diff --git a/tests/common.py b/tests/common.py index 3472da6d1ef..3ed0f9e2008 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1527,12 +1527,12 @@ class _HA_ANY: _other = _SENTINEL - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: """Test equal.""" self._other = other return True - def __ne__(self, other: Any) -> bool: + def __ne__(self, other: object) -> bool: """Test not equal.""" self._other = other return False diff --git a/tests/components/recorder/db_schema_25.py b/tests/components/recorder/db_schema_25.py index 0d763f91b67..d989cacb76a 100644 --- a/tests/components/recorder/db_schema_25.py +++ b/tests/components/recorder/db_schema_25.py @@ -653,7 +653,7 @@ class LazyState(State): "last_updated": last_updated_isoformat, } - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: """Return the comparison.""" return ( other.__class__ in [self.__class__, State] diff --git a/tests/components/recorder/db_schema_28.py b/tests/components/recorder/db_schema_28.py index feaf877b36f..8c984b61f6c 100644 --- a/tests/components/recorder/db_schema_28.py +++ b/tests/components/recorder/db_schema_28.py @@ -818,7 +818,7 @@ class LazyState(State): "last_updated": last_updated_isoformat, } - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: """Return the comparison.""" return ( other.__class__ in [self.__class__, State] From 3a28125470d2613f700b7c8626f8348a3bb99549 Mon Sep 17 00:00:00 2001 From: Matrix Date: Mon, 8 Apr 2024 05:31:52 +0800 Subject: [PATCH 386/967] Bump yolink-api to 0.4.2 (#115026) --- homeassistant/components/yolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yolink/manifest.json b/homeassistant/components/yolink/manifest.json index 8b3b071161c..cd6759b5864 100644 --- a/homeassistant/components/yolink/manifest.json +++ b/homeassistant/components/yolink/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["auth", "application_credentials"], "documentation": "https://www.home-assistant.io/integrations/yolink", "iot_class": "cloud_push", - "requirements": ["yolink-api==0.4.1"] + "requirements": ["yolink-api==0.4.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index eea298047e5..6054c3080af 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2917,7 +2917,7 @@ yeelight==0.7.14 yeelightsunflower==0.0.10 # homeassistant.components.yolink -yolink-api==0.4.1 +yolink-api==0.4.2 # homeassistant.components.youless youless-api==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 33862281865..cab390bc8a1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2255,7 +2255,7 @@ yalexs==3.0.1 yeelight==0.7.14 # homeassistant.components.yolink -yolink-api==0.4.1 +yolink-api==0.4.2 # homeassistant.components.youless youless-api==1.0.1 From f96c5a2905b25709385ae90c69c610890d8bda2e Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Sun, 7 Apr 2024 18:51:44 -0400 Subject: [PATCH 387/967] Add additional Sonos integration code-owner (#115157) --- CODEOWNERS | 4 ++-- homeassistant/components/sonos/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 31e97d9e511..946caef629e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1285,8 +1285,8 @@ build.json @home-assistant/supervisor /tests/components/sonarr/ @ctalkington /homeassistant/components/songpal/ @rytilahti @shenxn /tests/components/songpal/ @rytilahti @shenxn -/homeassistant/components/sonos/ @jjlawren -/tests/components/sonos/ @jjlawren +/homeassistant/components/sonos/ @jjlawren @peterager +/tests/components/sonos/ @jjlawren @peterager /homeassistant/components/soundtouch/ @kroimon /tests/components/soundtouch/ @kroimon /homeassistant/components/spaceapi/ @fabaff diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 58a0ec3b7ee..b6375eb7f16 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -2,7 +2,7 @@ "domain": "sonos", "name": "Sonos", "after_dependencies": ["plex", "spotify", "zeroconf", "media_source"], - "codeowners": ["@jjlawren"], + "codeowners": ["@jjlawren", "@peterager"], "config_flow": true, "dependencies": ["ssdp"], "documentation": "https://www.home-assistant.io/integrations/sonos", From d007b175c5901adfd3d4c6e2091b48a1d6c13b4a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 7 Apr 2024 12:51:59 -1000 Subject: [PATCH 388/967] Write timer entity state before firing events (#115151) --- homeassistant/components/timer/__init__.py | 12 ++++++------ tests/components/timer/test_init.py | 17 ++++++++++++----- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index 72e93f5655a..5da68d99dd6 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -325,12 +325,12 @@ class Timer(collection.CollectionEntity, RestoreEntity): self._end = start + self._remaining + self.async_write_ha_state() self.hass.bus.async_fire(event, {ATTR_ENTITY_ID: self.entity_id}) self._listener = async_track_point_in_utc_time( self.hass, self._async_finished, self._end ) - self.async_write_ha_state() @callback def async_change(self, duration: timedelta) -> None: @@ -351,11 +351,11 @@ class Timer(collection.CollectionEntity, RestoreEntity): self._listener() self._end += duration self._remaining = self._end - dt_util.utcnow().replace(microsecond=0) + self.async_write_ha_state() self.hass.bus.async_fire(EVENT_TIMER_CHANGED, {ATTR_ENTITY_ID: self.entity_id}) self._listener = async_track_point_in_utc_time( self.hass, self._async_finished, self._end ) - self.async_write_ha_state() @callback def async_pause(self) -> None: @@ -368,8 +368,8 @@ class Timer(collection.CollectionEntity, RestoreEntity): self._remaining = self._end - dt_util.utcnow().replace(microsecond=0) self._state = STATUS_PAUSED self._end = None - self.hass.bus.async_fire(EVENT_TIMER_PAUSED, {ATTR_ENTITY_ID: self.entity_id}) self.async_write_ha_state() + self.hass.bus.async_fire(EVENT_TIMER_PAUSED, {ATTR_ENTITY_ID: self.entity_id}) @callback def async_cancel(self) -> None: @@ -381,10 +381,10 @@ class Timer(collection.CollectionEntity, RestoreEntity): self._end = None self._remaining = None self._running_duration = self._configured_duration + self.async_write_ha_state() self.hass.bus.async_fire( EVENT_TIMER_CANCELLED, {ATTR_ENTITY_ID: self.entity_id} ) - self.async_write_ha_state() @callback def async_finish(self) -> None: @@ -400,11 +400,11 @@ class Timer(collection.CollectionEntity, RestoreEntity): self._end = None self._remaining = None self._running_duration = self._configured_duration + self.async_write_ha_state() self.hass.bus.async_fire( EVENT_TIMER_FINISHED, {ATTR_ENTITY_ID: self.entity_id, ATTR_FINISHED_AT: end.isoformat()}, ) - self.async_write_ha_state() @callback def _async_finished(self, time: datetime) -> None: @@ -418,11 +418,11 @@ class Timer(collection.CollectionEntity, RestoreEntity): self._end = None self._remaining = None self._running_duration = self._configured_duration + self.async_write_ha_state() self.hass.bus.async_fire( EVENT_TIMER_FINISHED, {ATTR_ENTITY_ID: self.entity_id, ATTR_FINISHED_AT: end.isoformat()}, ) - self.async_write_ha_state() async def async_update_config(self, config: ConfigType) -> None: """Handle when the config is updated.""" diff --git a/tests/components/timer/test_init.py b/tests/components/timer/test_init.py index 5aca1625d1f..c1c9f56094b 100644 --- a/tests/components/timer/test_init.py +++ b/tests/components/timer/test_init.py @@ -45,7 +45,7 @@ from homeassistant.const import ( EVENT_STATE_CHANGED, SERVICE_RELOAD, ) -from homeassistant.core import Context, CoreState, HomeAssistant, State +from homeassistant.core import Context, CoreState, Event, HomeAssistant, State, callback from homeassistant.exceptions import HomeAssistantError, Unauthorized from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.restore_state import StoredState, async_get @@ -156,11 +156,12 @@ async def test_methods_and_events(hass: HomeAssistant) -> None: assert state assert state.state == STATUS_IDLE - results = [] + results: list[tuple[Event, str]] = [] - def fake_event_listener(event): + @callback + def fake_event_listener(event: Event): """Fake event listener for trigger.""" - results.append(event) + results.append((event, hass.states.get("timer.test1").state)) hass.bus.async_listen(EVENT_TIMER_STARTED, fake_event_listener) hass.bus.async_listen(EVENT_TIMER_RESTARTED, fake_event_listener) @@ -262,7 +263,10 @@ async def test_methods_and_events(hass: HomeAssistant) -> None: if step["event"] is not None: expected_events += 1 - assert results[-1].event_type == step["event"] + last_result = results[-1] + event, state = last_result + assert event.event_type == step["event"] + assert state == step["state"] assert len(results) == expected_events @@ -404,6 +408,7 @@ async def test_wait_till_timer_expires(hass: HomeAssistant) -> None: results = [] + @callback def fake_event_listener(event): """Fake event listener for trigger.""" results.append(event) @@ -580,6 +585,7 @@ async def test_timer_restarted_event(hass: HomeAssistant) -> None: results = [] + @callback def fake_event_listener(event): """Fake event listener for trigger.""" results.append(event) @@ -647,6 +653,7 @@ async def test_state_changed_when_timer_restarted(hass: HomeAssistant) -> None: results = [] + @callback def fake_event_listener(event): """Fake event listener for trigger.""" results.append(event) From a0e6fd6ec5a64033c8f9551cb05f6f0e34c47390 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 8 Apr 2024 01:28:24 +0200 Subject: [PATCH 389/967] Add improved typing for event fire and listen methods (#114906) * Add EventType implementation * Update integrations for EventType * Change state_changed to EventType * Fix tests * Remove runtime impact * Add tests * Move to stub file * Apply pre-commit to stub files * Fix ruff PYI checks --------- Co-authored-by: J. Nick Koston --- .pre-commit-config.yaml | 4 +- homeassistant/auth/permissions/events.py | 5 +- .../components/homeassistant/logbook.py | 4 +- homeassistant/components/logbook/__init__.py | 4 +- homeassistant/components/logbook/helpers.py | 7 ++- homeassistant/components/logbook/models.py | 8 ++- homeassistant/components/logbook/processor.py | 6 +- .../components/mobile_app/logbook.py | 4 +- homeassistant/components/recorder/__init__.py | 5 +- homeassistant/components/recorder/core.py | 3 +- .../components/recorder/models/event.py | 6 +- .../recorder/table_managers/__init__.py | 10 ++-- .../recorder/table_managers/event_types.py | 27 ++++++--- homeassistant/components/recorder/tasks.py | 3 +- homeassistant/const.py | 8 ++- homeassistant/core.py | 57 +++++++++++-------- homeassistant/exceptions.py | 10 +++- homeassistant/helpers/event.py | 3 +- homeassistant/util/event_type.py | 20 +++++++ homeassistant/util/event_type.pyi | 25 ++++++++ tests/util/test_event_type.py | 25 ++++++++ 21 files changed, 182 insertions(+), 62 deletions(-) create mode 100644 homeassistant/util/event_type.py create mode 100644 homeassistant/util/event_type.pyi create mode 100644 tests/util/test_event_type.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8280ac326a7..760e7e20676 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,7 @@ repos: args: - --fix - id: ruff-format - files: ^((homeassistant|pylint|script|tests)/.+)?[^/]+\.py$ + files: ^((homeassistant|pylint|script|tests)/.+)?[^/]+\.(py|pyi)$ - repo: https://github.com/codespell-project/codespell rev: v2.2.6 hooks: @@ -63,7 +63,7 @@ repos: language: script types: [python] require_serial: true - files: ^(homeassistant|pylint)/.+\.py$ + files: ^(homeassistant|pylint)/.+\.(py|pyi)$ - id: pylint name: pylint entry: script/run-in-env.sh pylint -j 0 --ignore-missing-annotations=y diff --git a/homeassistant/auth/permissions/events.py b/homeassistant/auth/permissions/events.py index 3146cd99787..9f2fb45f9f0 100644 --- a/homeassistant/auth/permissions/events.py +++ b/homeassistant/auth/permissions/events.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Final +from typing import Any, Final from homeassistant.const import ( EVENT_COMPONENT_LOADED, @@ -21,10 +21,11 @@ from homeassistant.helpers.area_registry import EVENT_AREA_REGISTRY_UPDATED from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED from homeassistant.helpers.issue_registry import EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED +from homeassistant.util.event_type import EventType # These are events that do not contain any sensitive data # Except for state_changed, which is handled accordingly. -SUBSCRIBE_ALLOWLIST: Final[set[str]] = { +SUBSCRIBE_ALLOWLIST: Final[set[EventType[Any] | str]] = { EVENT_AREA_REGISTRY_UPDATED, EVENT_COMPONENT_LOADED, EVENT_CORE_CONFIG_UPDATE, diff --git a/homeassistant/components/homeassistant/logbook.py b/homeassistant/components/homeassistant/logbook.py index 12d6c66b69c..1c67075b671 100644 --- a/homeassistant/components/homeassistant/logbook.py +++ b/homeassistant/components/homeassistant/logbook.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Callable +from typing import Any from homeassistant.components.logbook import ( LOGBOOK_ENTRY_ICON, @@ -11,10 +12,11 @@ from homeassistant.components.logbook import ( ) from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.util.event_type import EventType from . import DOMAIN -EVENT_TO_NAME = { +EVENT_TO_NAME: dict[EventType[Any] | str, str] = { EVENT_HOMEASSISTANT_STOP: "stopped", EVENT_HOMEASSISTANT_START: "started", } diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index f19e64aa6f0..d520cafb80e 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -31,6 +31,7 @@ from homeassistant.helpers.integration_platform import ( ) from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass +from homeassistant.util.event_type import EventType from . import rest_api, websocket_api from .const import ( # noqa: F401 @@ -134,7 +135,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: entities_filter = None external_events: dict[ - str, tuple[str, Callable[[LazyEventPartialState], dict[str, Any]]] + EventType[Any] | str, + tuple[str, Callable[[LazyEventPartialState], dict[str, Any]]], ] = {} hass.data[DOMAIN] = LogbookConfig(external_events, filters, entities_filter) websocket_api.async_setup(hass) diff --git a/homeassistant/components/logbook/helpers.py b/homeassistant/components/logbook/helpers.py index 5c25056c041..8ec953a0afd 100644 --- a/homeassistant/components/logbook/helpers.py +++ b/homeassistant/components/logbook/helpers.py @@ -26,6 +26,7 @@ from homeassistant.core import ( ) from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.util.event_type import EventType from .const import ALWAYS_CONTINUOUS_DOMAINS, AUTOMATION_EVENTS, BUILT_IN_EVENTS, DOMAIN from .models import LogbookConfig @@ -63,7 +64,7 @@ def _async_config_entries_for_ids( def async_determine_event_types( hass: HomeAssistant, entity_ids: list[str] | None, device_ids: list[str] | None -) -> tuple[str, ...]: +) -> tuple[EventType[Any] | str, ...]: """Reduce the event types based on the entity ids and device ids.""" logbook_config: LogbookConfig = hass.data[DOMAIN] external_events = logbook_config.external_events @@ -81,7 +82,7 @@ def async_determine_event_types( # to add them since we have historically included # them when matching only on entities # - intrested_event_types: set[str] = { + intrested_event_types: set[EventType[Any] | str] = { external_event for external_event, domain_call in external_events.items() if domain_call[0] in interested_domains @@ -160,7 +161,7 @@ def async_subscribe_events( hass: HomeAssistant, subscriptions: list[CALLBACK_TYPE], target: Callable[[Event[Any]], None], - event_types: tuple[str, ...], + event_types: tuple[EventType[Any] | str, ...], entities_filter: Callable[[str], bool] | None, entity_ids: list[str] | None, device_ids: list[str] | None, diff --git a/homeassistant/components/logbook/models.py b/homeassistant/components/logbook/models.py index 9409c59985c..2f9b2c8e289 100644 --- a/homeassistant/components/logbook/models.py +++ b/homeassistant/components/logbook/models.py @@ -18,6 +18,7 @@ from homeassistant.components.recorder.models import ( ) from homeassistant.const import ATTR_ICON, EVENT_STATE_CHANGED from homeassistant.core import Context, Event, State, callback +from homeassistant.util.event_type import EventType from homeassistant.util.json import json_loads from homeassistant.util.ulid import ulid_to_bytes @@ -27,7 +28,8 @@ class LogbookConfig: """Configuration for the logbook integration.""" external_events: dict[ - str, tuple[str, Callable[[LazyEventPartialState], dict[str, Any]]] + EventType[Any] | str, + tuple[str, Callable[[LazyEventPartialState], dict[str, Any]]], ] sqlalchemy_filter: Filters | None = None entity_filter: Callable[[str], bool] | None = None @@ -66,7 +68,7 @@ class LazyEventPartialState: ) @cached_property - def event_type(self) -> str | None: + def event_type(self) -> EventType[Any] | str | None: """Return the event type.""" return self.row.event_type @@ -110,7 +112,7 @@ class EventAsRow: icon: str | None = None context_user_id_bin: bytes | None = None context_parent_id_bin: bytes | None = None - event_type: str | None = None + event_type: EventType[Any] | str | None = None state: str | None = None context_only: None = None diff --git a/homeassistant/components/logbook/processor.py b/homeassistant/components/logbook/processor.py index 2180a63b74f..df1eb6a15f2 100644 --- a/homeassistant/components/logbook/processor.py +++ b/homeassistant/components/logbook/processor.py @@ -38,6 +38,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, split_entity_id from homeassistant.helpers import entity_registry as er import homeassistant.util.dt as dt_util +from homeassistant.util.event_type import EventType from .const import ( ATTR_MESSAGE, @@ -75,7 +76,8 @@ class LogbookRun: context_lookup: dict[bytes | None, Row | EventAsRow | None] external_events: dict[ - str, tuple[str, Callable[[LazyEventPartialState], dict[str, Any]]] + EventType[Any] | str, + tuple[str, Callable[[LazyEventPartialState], dict[str, Any]]], ] event_cache: EventCache entity_name_cache: EntityNameCache @@ -90,7 +92,7 @@ class EventProcessor: def __init__( self, hass: HomeAssistant, - event_types: tuple[str, ...], + event_types: tuple[EventType[Any] | str, ...], entity_ids: list[str] | None = None, device_ids: list[str] | None = None, context_id: str | None = None, diff --git a/homeassistant/components/mobile_app/logbook.py b/homeassistant/components/mobile_app/logbook.py index 6a863e6a75b..d9f7f4f04e1 100644 --- a/homeassistant/components/mobile_app/logbook.py +++ b/homeassistant/components/mobile_app/logbook.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Callable +from typing import Any from homeassistant.components.logbook import ( LOGBOOK_ENTRY_ENTITY_ID, @@ -12,6 +13,7 @@ from homeassistant.components.logbook import ( ) from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_ICON from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.util.event_type import EventType from .const import DOMAIN @@ -21,7 +23,7 @@ IOS_EVENT_ZONE_EXITED = "ios.zone_exited" ATTR_ZONE = "zone" ATTR_SOURCE_DEVICE_NAME = "sourceDeviceName" ATTR_SOURCE_DEVICE_ID = "sourceDeviceID" -EVENT_TO_DESCRIPTION = { +EVENT_TO_DESCRIPTION: dict[EventType[Any] | str, str] = { IOS_EVENT_ZONE_ENTERED: "entered zone", IOS_EVENT_ZONE_EXITED: "exited zone", } diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index de75207389f..26b9f471b9e 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -25,6 +25,7 @@ from homeassistant.helpers.integration_platform import ( ) from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass +from homeassistant.util.event_type import EventType from . import entity_registry, websocket_api from .const import ( # noqa: F401 @@ -146,7 +147,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass_config_path=hass.config.path(DEFAULT_DB_FILE) ) exclude = conf[CONF_EXCLUDE] - exclude_event_types: set[str] = set(exclude.get(CONF_EVENT_TYPES, [])) + exclude_event_types: set[EventType[Any] | str] = set( + exclude.get(CONF_EVENT_TYPES, []) + ) if EVENT_STATE_CHANGED in exclude_event_types: _LOGGER.error("State change events cannot be excluded, use a filter instead") exclude_event_types.remove(EVENT_STATE_CHANGED) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 4ae61a0c4ba..1780436168d 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -40,6 +40,7 @@ from homeassistant.helpers.start import async_at_started from homeassistant.helpers.typing import UNDEFINED, UndefinedType import homeassistant.util.dt as dt_util from homeassistant.util.enum import try_parse_enum +from homeassistant.util.event_type import EventType from . import migration, statistics from .const import ( @@ -173,7 +174,7 @@ class Recorder(threading.Thread): db_max_retries: int, db_retry_wait: int, entity_filter: Callable[[str], bool], - exclude_event_types: set[str], + exclude_event_types: set[EventType[Any] | str], ) -> None: """Initialize the recorder.""" threading.Thread.__init__(self, name="Recorder") diff --git a/homeassistant/components/recorder/models/event.py b/homeassistant/components/recorder/models/event.py index 379a6fddb1d..4e5030bfde7 100644 --- a/homeassistant/components/recorder/models/event.py +++ b/homeassistant/components/recorder/models/event.py @@ -2,9 +2,13 @@ from __future__ import annotations +from typing import Any + +from homeassistant.util.event_type import EventType + def extract_event_type_ids( - event_type_to_event_type_id: dict[str, int | None], + event_type_to_event_type_id: dict[EventType[Any] | str, int | None], ) -> list[int]: """Extract event_type ids from event_type_to_event_type_id.""" return [ diff --git a/homeassistant/components/recorder/table_managers/__init__.py b/homeassistant/components/recorder/table_managers/__init__.py index 9a0945dc4d9..c064987ddcb 100644 --- a/homeassistant/components/recorder/table_managers/__init__.py +++ b/homeassistant/components/recorder/table_managers/__init__.py @@ -1,9 +1,11 @@ """Managers for each table.""" -from typing import TYPE_CHECKING, Generic, TypeVar +from typing import TYPE_CHECKING, Any, Generic, TypeVar from lru import LRU +from homeassistant.util.event_type import EventType + if TYPE_CHECKING: from ..core import Recorder @@ -13,7 +15,7 @@ _DataT = TypeVar("_DataT") class BaseTableManager(Generic[_DataT]): """Base class for table managers.""" - _id_map: "LRU[str, int]" + _id_map: "LRU[EventType[Any] | str, int]" def __init__(self, recorder: "Recorder") -> None: """Initialize the table manager. @@ -24,7 +26,7 @@ class BaseTableManager(Generic[_DataT]): """ self.active = False self.recorder = recorder - self._pending: dict[str, _DataT] = {} + self._pending: dict[EventType[Any] | str, _DataT] = {} def get_from_cache(self, data: str) -> int | None: """Resolve data to the id without accessing the underlying database. @@ -34,7 +36,7 @@ class BaseTableManager(Generic[_DataT]): """ return self._id_map.get(data) - def get_pending(self, shared_data: str) -> _DataT | None: + def get_pending(self, shared_data: EventType[Any] | str) -> _DataT | None: """Get pending data that have not be assigned ids yet. This call is not thread-safe and must be called from the diff --git a/homeassistant/components/recorder/table_managers/event_types.py b/homeassistant/components/recorder/table_managers/event_types.py index 94ceab7bf68..73401e8df56 100644 --- a/homeassistant/components/recorder/table_managers/event_types.py +++ b/homeassistant/components/recorder/table_managers/event_types.py @@ -3,12 +3,13 @@ from __future__ import annotations from collections.abc import Iterable -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING, Any, cast from lru import LRU from sqlalchemy.orm.session import Session from homeassistant.core import Event +from homeassistant.util.event_type import EventType from ..db_schema import EventTypes from ..queries import find_event_type_ids @@ -29,7 +30,9 @@ class EventTypeManager(BaseLRUTableManager[EventTypes]): def __init__(self, recorder: Recorder) -> None: """Initialize the event type manager.""" super().__init__(recorder, CACHE_SIZE) - self._non_existent_event_types: LRU[str, None] = LRU(CACHE_SIZE) + self._non_existent_event_types: LRU[EventType[Any] | str, None] = LRU( + CACHE_SIZE + ) def load(self, events: list[Event], session: Session) -> None: """Load the event_type to event_type_ids mapping into memory. @@ -44,7 +47,10 @@ class EventTypeManager(BaseLRUTableManager[EventTypes]): ) def get( - self, event_type: str, session: Session, from_recorder: bool = False + self, + event_type: EventType[Any] | str, + session: Session, + from_recorder: bool = False, ) -> int | None: """Resolve event_type to the event_type_id. @@ -54,16 +60,19 @@ class EventTypeManager(BaseLRUTableManager[EventTypes]): return self.get_many((event_type,), session)[event_type] def get_many( - self, event_types: Iterable[str], session: Session, from_recorder: bool = False - ) -> dict[str, int | None]: + self, + event_types: Iterable[EventType[Any] | str], + session: Session, + from_recorder: bool = False, + ) -> dict[EventType[Any] | str, int | None]: """Resolve event_types to event_type_ids. This call is not thread-safe and must be called from the recorder thread. """ - results: dict[str, int | None] = {} - missing: list[str] = [] - non_existent: list[str] = [] + results: dict[EventType[Any] | str, int | None] = {} + missing: list[EventType[Any] | str] = [] + non_existent: list[EventType[Any] | str] = [] for event_type in event_types: if (event_type_id := self._id_map.get(event_type)) is None: @@ -123,7 +132,7 @@ class EventTypeManager(BaseLRUTableManager[EventTypes]): self.clear_non_existent(event_type) self._pending.clear() - def clear_non_existent(self, event_type: str) -> None: + def clear_non_existent(self, event_type: EventType[Any] | str) -> None: """Clear a non-existent event type from the cache. This call is not thread-safe and must be called from the diff --git a/homeassistant/components/recorder/tasks.py b/homeassistant/components/recorder/tasks.py index 1b81d7a983f..2d980c849e5 100644 --- a/homeassistant/components/recorder/tasks.py +++ b/homeassistant/components/recorder/tasks.py @@ -12,6 +12,7 @@ import threading from typing import TYPE_CHECKING, Any from homeassistant.helpers.typing import UndefinedType +from homeassistant.util.event_type import EventType from . import entity_registry, purge, statistics from .const import DOMAIN @@ -459,7 +460,7 @@ class EventIdMigrationTask(RecorderTask): class RefreshEventTypesTask(RecorderTask): """An object to insert into the recorder queue to refresh event types.""" - event_types: list[str] + event_types: list[EventType[Any] | str] def run(self, instance: Recorder) -> None: """Refresh event types.""" diff --git a/homeassistant/const.py b/homeassistant/const.py index d52deb98d5b..0eed33c48d7 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -4,7 +4,7 @@ from __future__ import annotations from enum import StrEnum from functools import partial -from typing import Final +from typing import TYPE_CHECKING, Final from .helpers.deprecation import ( DeprecatedConstant, @@ -13,8 +13,12 @@ from .helpers.deprecation import ( check_if_deprecated_constant, dir_with_deprecated_constants, ) +from .util.event_type import EventType from .util.signal_type import SignalType +if TYPE_CHECKING: + from .core import EventStateChangedData + APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 5 @@ -306,7 +310,7 @@ EVENT_LOGBOOK_ENTRY: Final = "logbook_entry" EVENT_LOGGING_CHANGED: Final = "logging_changed" EVENT_SERVICE_REGISTERED: Final = "service_registered" EVENT_SERVICE_REMOVED: Final = "service_removed" -EVENT_STATE_CHANGED: Final = "state_changed" +EVENT_STATE_CHANGED: EventType[EventStateChangedData] = EventType("state_changed") EVENT_STATE_REPORTED: Final = "state_reported" EVENT_THEMES_UPDATED: Final = "themes_updated" EVENT_PANELS_UPDATED: Final = "panels_updated" diff --git a/homeassistant/core.py b/homeassistant/core.py index ccea82a7eb9..574edf34c9b 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -102,6 +102,7 @@ from .util.async_ import ( run_callback_threadsafe, shutdown_run_callback_threadsafe, ) +from .util.event_type import EventType from .util.executor import InterruptibleThreadPoolExecutor from .util.json import JsonObjectType from .util.read_only_dict import ReadOnlyDict @@ -1216,7 +1217,7 @@ class Event(Generic[_DataT]): def __init__( self, - event_type: str, + event_type: EventType[_DataT] | str, data: _DataT | None = None, origin: EventOrigin = EventOrigin.local, time_fired_timestamp: float | None = None, @@ -1290,7 +1291,7 @@ class Event(Generic[_DataT]): def _event_repr( - event_type: str, origin: EventOrigin, data: Mapping[str, Any] | None + event_type: EventType[_DataT] | str, origin: EventOrigin, data: _DataT | None ) -> str: """Return the representation.""" if data: @@ -1307,13 +1308,13 @@ _FilterableJobType = tuple[ @dataclass(slots=True) -class _OneTimeListener: +class _OneTimeListener(Generic[_DataT]): hass: HomeAssistant - listener_job: HassJob[[Event], Coroutine[Any, Any, None] | None] + listener_job: HassJob[[Event[_DataT]], Coroutine[Any, Any, None] | None] remove: CALLBACK_TYPE | None = None @callback - def __call__(self, event: Event) -> None: + def __call__(self, event: Event[_DataT]) -> None: """Remove listener from event bus and then fire listener.""" if not self.remove: # If the listener was already removed, we don't need to do anything @@ -1341,7 +1342,7 @@ class EventBus: def __init__(self, hass: HomeAssistant) -> None: """Initialize a new event bus.""" - self._listeners: dict[str, list[_FilterableJobType[Any]]] = {} + self._listeners: dict[EventType[Any] | str, list[_FilterableJobType[Any]]] = {} self._match_all_listeners: list[_FilterableJobType[Any]] = [] self._listeners[MATCH_ALL] = self._match_all_listeners self._hass = hass @@ -1356,7 +1357,7 @@ class EventBus: self._debug = _LOGGER.isEnabledFor(logging.DEBUG) @callback - def async_listeners(self) -> dict[str, int]: + def async_listeners(self) -> dict[EventType[Any] | str, int]: """Return dictionary with events and the number of listeners. This method must be run in the event loop. @@ -1364,14 +1365,14 @@ class EventBus: return {key: len(listeners) for key, listeners in self._listeners.items()} @property - def listeners(self) -> dict[str, int]: + def listeners(self) -> dict[EventType[Any] | str, int]: """Return dictionary with events and the number of listeners.""" return run_callback_threadsafe(self._hass.loop, self.async_listeners).result() def fire( self, - event_type: str, - event_data: Mapping[str, Any] | None = None, + event_type: EventType[_DataT] | str, + event_data: _DataT | None = None, origin: EventOrigin = EventOrigin.local, context: Context | None = None, ) -> None: @@ -1383,8 +1384,8 @@ class EventBus: @callback def async_fire( self, - event_type: str, - event_data: Mapping[str, Any] | None = None, + event_type: EventType[_DataT] | str, + event_data: _DataT | None = None, origin: EventOrigin = EventOrigin.local, context: Context | None = None, time_fired: float | None = None, @@ -1402,8 +1403,8 @@ class EventBus: @callback def _async_fire( self, - event_type: str, - event_data: Mapping[str, Any] | None = None, + event_type: EventType[_DataT] | str, + event_data: _DataT | None = None, origin: EventOrigin = EventOrigin.local, context: Context | None = None, time_fired: float | None = None, @@ -1431,7 +1432,7 @@ class EventBus: if not listeners: return - event: Event | None = None + event: Event[_DataT] | None = None for job, event_filter, run_immediately in listeners: if event_filter is not None: @@ -1461,8 +1462,8 @@ class EventBus: def listen( self, - event_type: str, - listener: Callable[[Event[Any]], Coroutine[Any, Any, None] | None], + event_type: EventType[_DataT] | str, + listener: Callable[[Event[_DataT]], Coroutine[Any, Any, None] | None], ) -> CALLBACK_TYPE: """Listen for all events or events of a specific type. @@ -1482,7 +1483,7 @@ class EventBus: @callback def async_listen( self, - event_type: str, + event_type: EventType[_DataT] | str, listener: Callable[[Event[_DataT]], Coroutine[Any, Any, None] | None], event_filter: Callable[[_DataT], bool] | None = None, run_immediately: bool = True, @@ -1524,7 +1525,9 @@ class EventBus: @callback def _async_listen_filterable_job( - self, event_type: str, filterable_job: _FilterableJobType[Any] + self, + event_type: EventType[_DataT] | str, + filterable_job: _FilterableJobType[_DataT], ) -> CALLBACK_TYPE: self._listeners.setdefault(event_type, []).append(filterable_job) return functools.partial( @@ -1533,8 +1536,8 @@ class EventBus: def listen_once( self, - event_type: str, - listener: Callable[[Event[Any]], Coroutine[Any, Any, None] | None], + event_type: EventType[_DataT] | str, + listener: Callable[[Event[_DataT]], Coroutine[Any, Any, None] | None], ) -> CALLBACK_TYPE: """Listen once for event of a specific type. @@ -1556,8 +1559,8 @@ class EventBus: @callback def async_listen_once( self, - event_type: str, - listener: Callable[[Event[Any]], Coroutine[Any, Any, None] | None], + event_type: EventType[_DataT] | str, + listener: Callable[[Event[_DataT]], Coroutine[Any, Any, None] | None], run_immediately: bool = True, ) -> CALLBACK_TYPE: """Listen once for event of a specific type. @@ -1569,7 +1572,9 @@ class EventBus: This method must be run in the event loop. """ - one_time_listener = _OneTimeListener(self._hass, HassJob(listener)) + one_time_listener: _OneTimeListener[_DataT] = _OneTimeListener( + self._hass, HassJob(listener) + ) remove = self._async_listen_filterable_job( event_type, ( @@ -1587,7 +1592,9 @@ class EventBus: @callback def _async_remove_listener( - self, event_type: str, filterable_job: _FilterableJobType + self, + event_type: EventType[_DataT] | str, + filterable_job: _FilterableJobType[_DataT], ) -> None: """Remove a listener of a specific event_type. diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index bdf4d8c060b..1eb964d82b1 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -4,7 +4,9 @@ from __future__ import annotations from collections.abc import Callable, Generator, Sequence from dataclasses import dataclass -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any + +from .util.event_type import EventType if TYPE_CHECKING: from .core import Context @@ -271,8 +273,12 @@ class ServiceNotFound(HomeAssistantError): class MaxLengthExceeded(HomeAssistantError): """Raised when a property value has exceeded the max character length.""" - def __init__(self, value: str, property_name: str, max_length: int) -> None: + def __init__( + self, value: EventType[Any] | str, property_name: str, max_length: int + ) -> None: """Initialize error.""" + if TYPE_CHECKING: + value = str(value) super().__init__( translation_domain="homeassistant", translation_key="max_length_exceeded", diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 67feb6c48a4..e3f91320c7b 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -38,6 +38,7 @@ from homeassistant.exceptions import TemplateError from homeassistant.loader import bind_hass from homeassistant.util import dt as dt_util from homeassistant.util.async_ import run_callback_threadsafe +from homeassistant.util.event_type import EventType from .device_registry import ( EVENT_DEVICE_REGISTRY_UPDATED, @@ -90,7 +91,7 @@ class _KeyedEventTracker(Generic[_TypedDictT]): listeners_key: str callbacks_key: str - event_type: str + event_type: EventType[_TypedDictT] | str dispatcher_callable: Callable[ [ HomeAssistant, diff --git a/homeassistant/util/event_type.py b/homeassistant/util/event_type.py new file mode 100644 index 00000000000..e96d45c80a3 --- /dev/null +++ b/homeassistant/util/event_type.py @@ -0,0 +1,20 @@ +"""Implementation for EventType. + +Custom for type checking. See stub file. +""" + +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Generic + +from typing_extensions import TypeVar + +_DataT = TypeVar("_DataT", bound=Mapping[str, Any], default=Mapping[str, Any]) + + +class EventType(str, Generic[_DataT]): + """Custom type for Event.event_type. + + At runtime this is a generic subclass of str. + """ diff --git a/homeassistant/util/event_type.pyi b/homeassistant/util/event_type.pyi new file mode 100644 index 00000000000..4285e54e8c9 --- /dev/null +++ b/homeassistant/util/event_type.pyi @@ -0,0 +1,25 @@ +"""Stub file for event_type. Provide overload for type checking.""" +# ruff: noqa: PYI021 # Allow docstrings + +from collections.abc import Mapping +from typing import Any, Generic + +from typing_extensions import TypeVar + +__all__ = [ + "EventType", +] + +_DataT = TypeVar("_DataT", bound=Mapping[str, Any], default=Mapping[str, Any]) + +class EventType(Generic[_DataT]): + """Custom type for Event.event_type. At runtime delegated to str. + + For type checkers pretend to be its own separate class. + """ + + def __init__(self, value: str, /) -> None: ... + def __len__(self) -> int: ... + def __hash__(self) -> int: ... + def __eq__(self, value: object, /) -> bool: ... + def __getitem__(self, index: int) -> str: ... diff --git a/tests/util/test_event_type.py b/tests/util/test_event_type.py new file mode 100644 index 00000000000..3086c8ea075 --- /dev/null +++ b/tests/util/test_event_type.py @@ -0,0 +1,25 @@ +"""Test EventType implementation.""" + +from __future__ import annotations + +import orjson + +from homeassistant.util.event_type import EventType + + +def test_compatibility_with_str() -> None: + """Test EventType. At runtime it should be (almost) fully compatible with str.""" + + event = EventType("Hello World") + assert event == "Hello World" + assert len(event) == 11 + assert hash(event) == hash("Hello World") + d: dict[str | EventType, int] = {EventType("key"): 2} + assert d["key"] == 2 + + +def test_json_dump() -> None: + """Test EventType json dump with orjson.""" + + event = EventType("state_changed") + assert orjson.dumps({"event_type": event}) == b'{"event_type":"state_changed"}' From 94a2352b419d101b6a8e502f574329b664ceff2f Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Mon, 8 Apr 2024 01:39:11 +0200 Subject: [PATCH 390/967] Bump pyoverkiz to 1.13.10 (#115154) --- 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 2ef0f0ebef4..dc2f0df4783 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.9"], + "requirements": ["pyoverkiz==1.13.10"], "zeroconf": [ { "type": "_kizbox._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 6054c3080af..0c65976d815 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2042,7 +2042,7 @@ pyotgw==2.1.3 pyotp==2.8.0 # homeassistant.components.overkiz -pyoverkiz==1.13.9 +pyoverkiz==1.13.10 # homeassistant.components.openweathermap pyowm==3.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cab390bc8a1..1e8cec938f0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1590,7 +1590,7 @@ pyotgw==2.1.3 pyotp==2.8.0 # homeassistant.components.overkiz -pyoverkiz==1.13.9 +pyoverkiz==1.13.10 # homeassistant.components.openweathermap pyowm==3.2.0 From 213cf76781fd2d06d826ff225b75242704597520 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 7 Apr 2024 13:46:38 -1000 Subject: [PATCH 391/967] Fix flakey fritz image test (#115161) --- tests/components/fritz/test_image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/fritz/test_image.py b/tests/components/fritz/test_image.py index 45afe34b6aa..a22ab76fdb6 100644 --- a/tests/components/fritz/test_image.py +++ b/tests/components/fritz/test_image.py @@ -164,7 +164,7 @@ async def test_image_update( fc_class_mock().override_services({**MOCK_FB_SERVICES, **GUEST_WIFI_CHANGED}) async_fire_time_changed(hass, utcnow() + timedelta(seconds=60)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) resp = await client.get("/api/image_proxy/image.mock_title_guestwifi") resp_body_new = await resp.read() From f044dd54cfe5ecc8ef38563366f5f6218bf1d20e Mon Sep 17 00:00:00 2001 From: rappenze Date: Mon, 8 Apr 2024 01:49:20 +0200 Subject: [PATCH 392/967] Bump fibaro to 0.7.7 (#115152) --- homeassistant/components/fibaro/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/fibaro/manifest.json b/homeassistant/components/fibaro/manifest.json index 68763228f82..bb1558f998b 100644 --- a/homeassistant/components/fibaro/manifest.json +++ b/homeassistant/components/fibaro/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["pyfibaro"], - "requirements": ["pyfibaro==0.7.6"] + "requirements": ["pyfibaro==0.7.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0c65976d815..7e2ddc78c5b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1821,7 +1821,7 @@ pyevilgenius==2.0.0 pyezviz==0.2.1.2 # homeassistant.components.fibaro -pyfibaro==0.7.6 +pyfibaro==0.7.7 # homeassistant.components.fido pyfido==2.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1e8cec938f0..3eb48da244e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1414,7 +1414,7 @@ pyevilgenius==2.0.0 pyezviz==0.2.1.2 # homeassistant.components.fibaro -pyfibaro==0.7.6 +pyfibaro==0.7.7 # homeassistant.components.fido pyfido==2.1.2 From 6b4457043d7df73bd5acacbf41cf74ac65a509ab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 7 Apr 2024 14:08:25 -1000 Subject: [PATCH 393/967] Deprecate async_add_hass_job (#115061) --- homeassistant/core.py | 61 +++++++++++++++++-- .../components/homematicip_cloud/test_hap.py | 2 +- tests/test_core.py | 43 +++++++++---- 3 files changed, 88 insertions(+), 18 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 574edf34c9b..113bbf7bf77 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -557,7 +557,7 @@ class HomeAssistant: target = cast(Callable[[*_Ts], Any], target) self.loop.call_soon_threadsafe( functools.partial( - self.async_add_hass_job, HassJob(target), *args, eager_start=True + self._async_add_hass_job, HassJob(target), *args, eager_start=True ) ) @@ -629,7 +629,7 @@ class HomeAssistant: # https://github.com/home-assistant/core/pull/71960 if TYPE_CHECKING: target = cast(Callable[[*_Ts], Coroutine[Any, Any, _R] | _R], target) - return self.async_add_hass_job(HassJob(target), *args, eager_start=eager_start) + return self._async_add_hass_job(HassJob(target), *args, eager_start=eager_start) @overload @callback @@ -664,6 +664,58 @@ class HomeAssistant: If eager_start is True, coroutine functions will be scheduled eagerly. If background is True, the task will created as a background task. + This method must be run in the event loop. + hassjob: HassJob to call. + args: parameters for method to call. + """ + # late import to avoid circular imports + from .helpers import frame # pylint: disable=import-outside-toplevel + + frame.report( + "calls `async_add_hass_job`, which is deprecated and will be removed in Home " + "Assistant 2025.5; Please review " + "https://developers.home-assistant.io/blog/2024/04/07/deprecate_add_hass_job" + " for replacement options", + error_if_core=False, + ) + + return self._async_add_hass_job( + hassjob, *args, eager_start=eager_start, background=background + ) + + @overload + @callback + def _async_add_hass_job( + self, + hassjob: HassJob[..., Coroutine[Any, Any, _R]], + *args: Any, + eager_start: bool = False, + background: bool = False, + ) -> asyncio.Future[_R] | None: ... + + @overload + @callback + def _async_add_hass_job( + self, + hassjob: HassJob[..., Coroutine[Any, Any, _R] | _R], + *args: Any, + eager_start: bool = False, + background: bool = False, + ) -> asyncio.Future[_R] | None: ... + + @callback + def _async_add_hass_job( + self, + hassjob: HassJob[..., Coroutine[Any, Any, _R] | _R], + *args: Any, + eager_start: bool = False, + background: bool = False, + ) -> asyncio.Future[_R] | None: + """Add a HassJob from within the event loop. + + If eager_start is True, coroutine functions will be scheduled eagerly. + If background is True, the task will created as a background task. + This method must be run in the event loop. hassjob: HassJob to call. args: parameters for method to call. @@ -841,7 +893,7 @@ class HomeAssistant: hassjob.target(*args) return None - return self.async_add_hass_job( + return self._async_add_hass_job( hassjob, *args, eager_start=True, background=background ) @@ -1458,7 +1510,8 @@ class EventBus: except Exception: # pylint: disable=broad-except _LOGGER.exception("Error running job: %s", job) else: - self._hass.async_add_hass_job(job, event) + # pylint: disable-next=protected-access + self._hass._async_add_hass_job(job, event) def listen( self, diff --git a/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py index cddade7cec5..3cb8b7d61e9 100644 --- a/tests/components/homematicip_cloud/test_hap.py +++ b/tests/components/homematicip_cloud/test_hap.py @@ -105,7 +105,7 @@ async def test_hap_setup_connection_error() -> None: ): assert not await hap.async_setup() - assert not hass.async_add_hass_job.mock_calls + assert not hass.async_run_hass_job.mock_calls assert not hass.config_entries.flow.async_init.mock_calls diff --git a/tests/test_core.py b/tests/test_core.py index e9d8d39ce18..a3722b0646d 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -93,7 +93,7 @@ async def test_async_add_hass_job_schedule_callback() -> None: hass = MagicMock() job = MagicMock() - ha.HomeAssistant.async_add_hass_job(hass, ha.HassJob(ha.callback(job))) + ha.HomeAssistant._async_add_hass_job(hass, ha.HassJob(ha.callback(job))) assert len(hass.loop.call_soon.mock_calls) == 1 assert len(hass.loop.create_task.mock_calls) == 0 assert len(hass.add_job.mock_calls) == 0 @@ -107,7 +107,7 @@ async def test_async_add_hass_job_eager_start_coro_suspends( async def job_that_suspends(): await asyncio.sleep(0) - task = hass.async_add_hass_job( + task = hass._async_add_hass_job( ha.HassJob(ha.callback(job_that_suspends)), eager_start=True ) assert not task.done() @@ -137,7 +137,7 @@ async def test_async_add_hass_job_background(hass: HomeAssistant) -> None: async def job_that_suspends(): await asyncio.sleep(0) - task = hass.async_add_hass_job( + task = hass._async_add_hass_job( ha.HassJob(ha.callback(job_that_suspends)), background=True ) assert not task.done() @@ -167,7 +167,7 @@ async def test_async_add_hass_job_eager_background(hass: HomeAssistant) -> None: async def job_that_suspends(): await asyncio.sleep(0) - task = hass.async_add_hass_job( + task = hass._async_add_hass_job( ha.HassJob(ha.callback(job_that_suspends)), background=True ) assert not task.done() @@ -232,7 +232,7 @@ async def test_async_add_hass_job_coro_named(hass: HomeAssistant) -> None: job = ha.HassJob(mycoro, "named coro") assert "named coro" in str(job) assert job.name == "named coro" - task = ha.HomeAssistant.async_add_hass_job(hass, job) + task = ha.HomeAssistant._async_add_hass_job(hass, job) assert "named coro" in str(task) @@ -245,7 +245,7 @@ async def test_async_add_hass_job_eager_start(hass: HomeAssistant) -> None: job = ha.HassJob(mycoro, "named coro") assert "named coro" in str(job) assert job.name == "named coro" - task = ha.HomeAssistant.async_add_hass_job(hass, job, eager_start=True) + task = ha.HomeAssistant._async_add_hass_job(hass, job, eager_start=True) assert "named coro" in str(task) @@ -255,7 +255,7 @@ async def test_async_add_hass_job_schedule_partial_callback() -> None: job = MagicMock() partial = functools.partial(ha.callback(job)) - ha.HomeAssistant.async_add_hass_job(hass, ha.HassJob(partial)) + ha.HomeAssistant._async_add_hass_job(hass, ha.HassJob(partial)) assert len(hass.loop.call_soon.mock_calls) == 1 assert len(hass.loop.create_task.mock_calls) == 0 assert len(hass.add_job.mock_calls) == 0 @@ -268,7 +268,7 @@ async def test_async_add_hass_job_schedule_coroutinefunction() -> None: async def job(): pass - ha.HomeAssistant.async_add_hass_job(hass, ha.HassJob(job)) + ha.HomeAssistant._async_add_hass_job(hass, ha.HassJob(job)) assert len(hass.loop.call_soon.mock_calls) == 0 assert len(hass.loop.create_task.mock_calls) == 1 assert len(hass.add_job.mock_calls) == 0 @@ -285,7 +285,7 @@ async def test_async_add_hass_job_schedule_corofunction_eager_start() -> None: "homeassistant.core.create_eager_task", wraps=create_eager_task ) as mock_create_eager_task: hass_job = ha.HassJob(job) - task = ha.HomeAssistant.async_add_hass_job(hass, hass_job, eager_start=True) + task = ha.HomeAssistant._async_add_hass_job(hass, hass_job, eager_start=True) assert len(hass.loop.call_soon.mock_calls) == 0 assert len(hass.add_job.mock_calls) == 0 assert mock_create_eager_task.mock_calls @@ -301,7 +301,7 @@ async def test_async_add_hass_job_schedule_partial_coroutinefunction() -> None: partial = functools.partial(job) - ha.HomeAssistant.async_add_hass_job(hass, ha.HassJob(partial)) + ha.HomeAssistant._async_add_hass_job(hass, ha.HassJob(partial)) assert len(hass.loop.call_soon.mock_calls) == 0 assert len(hass.loop.create_task.mock_calls) == 1 assert len(hass.add_job.mock_calls) == 0 @@ -314,7 +314,7 @@ async def test_async_add_job_add_hass_threaded_job_to_pool() -> None: def job(): pass - ha.HomeAssistant.async_add_hass_job(hass, ha.HassJob(job)) + ha.HomeAssistant._async_add_hass_job(hass, ha.HassJob(job)) assert len(hass.loop.call_soon.mock_calls) == 0 assert len(hass.loop.create_task.mock_calls) == 0 assert len(hass.loop.run_in_executor.mock_calls) == 2 @@ -383,7 +383,7 @@ async def test_async_run_eager_hass_job_calls_coro_function() -> None: pass ha.HomeAssistant.async_run_hass_job(hass, ha.HassJob(job)) - assert len(hass.async_add_hass_job.mock_calls) == 1 + assert len(hass._async_add_hass_job.mock_calls) == 1 async def test_async_run_hass_job_calls_callback() -> None: @@ -409,7 +409,7 @@ async def test_async_run_hass_job_delegates_non_async() -> None: ha.HomeAssistant.async_run_hass_job(hass, ha.HassJob(job)) assert len(calls) == 0 - assert len(hass.async_add_hass_job.mock_calls) == 1 + assert len(hass._async_add_hass_job.mock_calls) == 1 async def test_async_get_hass_can_be_called(hass: HomeAssistant) -> None: @@ -3236,6 +3236,23 @@ async def test_async_add_job_deprecated( ) in caplog.text +async def test_async_add_hass_job_deprecated( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test async_add_hass_job warns about its deprecation.""" + + async def _test(): + pass + + hass.async_add_hass_job(HassJob(_test)) + assert ( + "Detected code that calls `async_add_hass_job`, which is deprecated " + "and will be removed in Home Assistant 2025.5; Please review " + "https://developers.home-assistant.io/blog/2024/04/07/deprecate_add_hass_job" + " for replacement options" + ) in caplog.text + + async def test_eventbus_lazy_object_creation(hass: HomeAssistant) -> None: """Test we don't create unneeded objects when firing events.""" calls = [] From 89a2c89fe229a5cbcc227600085873dd3be8d494 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 7 Apr 2024 15:25:55 -1000 Subject: [PATCH 394/967] Avoid checking for polling if an entity fails to add (#115159) * Avoid checking for polling if an entity fails to add * no need to do protected access * no need to do protected access * no need to do protected access * no need to do protected access * coverage * fix test * fix * broken one must be first --- homeassistant/helpers/entity_platform.py | 11 +++++++- tests/helpers/test_entity_platform.py | 36 +++++++++++++++++++++++- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index f605f8381b0..261512c14af 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -631,7 +631,16 @@ class EntityPlatform: if ( (self.config_entry and self.config_entry.pref_disable_polling) or self._async_unsub_polling is not None - or not any(entity.should_poll for entity in entities) + or not any( + # Entity may have failed to add or called `add_to_platform_abort` + # so we check if the entity is in self.entities before + # checking `entity.should_poll` since `should_poll` may need to + # check `self.hass` which will be `None` if the entity did not add + entity.entity_id + and entity.entity_id in self.entities + and entity.should_poll + for entity in entities + ) ): return diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 31c6f8e6e30..59c4f7357f3 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -5,7 +5,7 @@ from collections.abc import Iterable from datetime import timedelta import logging from typing import Any -from unittest.mock import ANY, Mock, patch +from unittest.mock import ANY, AsyncMock, Mock, patch import pytest @@ -78,6 +78,40 @@ async def test_polling_only_updates_entities_it_should_poll( assert poll_ent.async_update.called +async def test_polling_check_works_if_entity_add_fails( + hass: HomeAssistant, +) -> None: + """Test the polling check works if an entity add fails.""" + component = EntityComponent(_LOGGER, DOMAIN, hass, timedelta(seconds=20)) + await component.async_setup({}) + + class MockEntityNeedsSelfHassInShouldPoll(MockEntity): + """Mock entity that needs self.hass in should_poll.""" + + @property + def should_poll(self) -> bool: + """Return True if entity has to be polled.""" + return self.hass.data is not None + + working_poll_ent = MockEntityNeedsSelfHassInShouldPoll(should_poll=True) + working_poll_ent.async_update = AsyncMock() + broken_poll_ent = MockEntityNeedsSelfHassInShouldPoll(should_poll=True) + broken_poll_ent.async_update = AsyncMock(side_effect=Exception("Broken")) + + await component.async_add_entities( + [broken_poll_ent, working_poll_ent], update_before_add=True + ) + + working_poll_ent.async_update.reset_mock() + broken_poll_ent.async_update.reset_mock() + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=20)) + await hass.async_block_till_done(wait_background_tasks=True) + + assert not broken_poll_ent.async_update.called + assert working_poll_ent.async_update.called + + async def test_polling_disabled_by_config_entry(hass: HomeAssistant) -> None: """Test the polling of only updated entities.""" entity_platform = MockEntityPlatform(hass) From af1023074ed60a5ce0cf6fce02e9fbdafdbb8610 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 7 Apr 2024 15:30:03 -1000 Subject: [PATCH 395/967] Add an event_filter to google_assistant state reporting (#115160) * adjust * fix test since it happens sooner now * remove debug * remove unneeded test change * reduce * reduce --- .../google_assistant/report_state.py | 68 ++++++++++++------- .../google_assistant/test_report_state.py | 9 +-- 2 files changed, 48 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/google_assistant/report_state.py b/homeassistant/components/google_assistant/report_state.py index 26a341bd7b6..1230b9a272e 100644 --- a/homeassistant/components/google_assistant/report_state.py +++ b/homeassistant/components/google_assistant/report_state.py @@ -4,12 +4,19 @@ from __future__ import annotations from collections import deque import logging -from typing import Any +from typing import TYPE_CHECKING, Any from uuid import uuid4 -from homeassistant.const import MATCH_ALL -from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, State, callback -from homeassistant.helpers.event import async_call_later, async_track_state_change +from homeassistant.const import EVENT_STATE_CHANGED +from homeassistant.core import ( + CALLBACK_TYPE, + Event, + EventStateChangedData, + HassJob, + HomeAssistant, + callback, +) +from homeassistant.helpers.event import async_call_later from homeassistant.helpers.significant_change import create_checker from .const import DOMAIN @@ -31,7 +38,9 @@ _LOGGER = logging.getLogger(__name__) @callback -def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig): +def async_enable_report_state( + hass: HomeAssistant, google_config: AbstractConfig +) -> CALLBACK_TYPE: """Enable state and notification reporting.""" checker = None unsub_pending: CALLBACK_TYPE | None = None @@ -60,33 +69,36 @@ def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig report_states_job = HassJob(report_states) - async def async_entity_state_listener( - changed_entity: str, old_state: State | None, new_state: State | None - ) -> None: - nonlocal unsub_pending, checker - - if not hass.is_running: - return - - if not new_state: - return - - if not google_config.should_expose(new_state): - return - - if not ( - entity := async_get_google_entity_if_supported_cached( + @callback + def _async_entity_state_filter(data: EventStateChangedData) -> bool: + return bool( + hass.is_running + and (new_state := data["new_state"]) + and google_config.should_expose(new_state) + and async_get_google_entity_if_supported_cached( hass, google_config, new_state ) - ): - return + ) + + async def _async_entity_state_listener(event: Event[EventStateChangedData]) -> None: + """Handle state changes.""" + nonlocal unsub_pending, checker + data = event.data + new_state = data["new_state"] + if TYPE_CHECKING: + assert new_state is not None # verified in filter + entity = async_get_google_entity_if_supported_cached( + hass, google_config, new_state + ) + if TYPE_CHECKING: + assert entity is not None # verified in filter # We only trigger notifications on changes in the state value, not attributes. # This is mainly designed for our event entity types # We need to synchronize notifications using a `SYNC` response, # together with other state changes. if ( - old_state + (old_state := data["old_state"]) and old_state.state != new_state.state and (notifications := entity.notifications_serialize()) is not None ): @@ -106,6 +118,7 @@ def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig result, ) + changed_entity = data["entity_id"] try: entity_data = entity.query_serialize() except SmartHomeError as err: @@ -173,7 +186,12 @@ def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig await google_config.async_report_state_all({"devices": {"states": entities}}) - unsub = async_track_state_change(hass, MATCH_ALL, async_entity_state_listener) + unsub = hass.bus.async_listen( + EVENT_STATE_CHANGED, + _async_entity_state_listener, + event_filter=_async_entity_state_filter, + run_immediately=True, + ) unsub = async_call_later( hass, INITIAL_REPORT_DELAY, HassJob(initial_report, cancel_on_shutdown=True) diff --git a/tests/components/google_assistant/test_report_state.py b/tests/components/google_assistant/test_report_state.py index 758ebf63db9..9c8a0f951cc 100644 --- a/tests/components/google_assistant/test_report_state.py +++ b/tests/components/google_assistant/test_report_state.py @@ -216,6 +216,10 @@ async def test_report_notifications( hass, datetime.fromisoformat("2023-08-01T01:01:00+00:00") ) await hass.async_block_till_done() + for call in mock_report_state.mock_calls: + if "states" in call[1][0]["devices"]: + states = call[1][0]["devices"]["states"] + assert states["event.doorbell"] == {"online": True} # Test the notification request failed caplog.clear() @@ -233,12 +237,10 @@ async def test_report_notifications( hass, datetime.fromisoformat("2023-08-01T01:03:00+00:00") ) await hass.async_block_till_done() - assert len(mock_report_state.mock_calls) == 2 + assert len(mock_report_state.mock_calls) == 1 for call in mock_report_state.mock_calls: if "notifications" in call[1][0]["devices"]: notifications = call[1][0]["devices"]["notifications"] - elif "states" in call[1][0]["devices"]: - states = call[1][0]["devices"]["states"] assert notifications["event.doorbell"] == { "ObjectDetection": { "objects": {"unclassified": 1}, @@ -246,7 +248,6 @@ async def test_report_notifications( "detectionTimestamp": epoc_event_time * 1000, } } - assert states["event.doorbell"] == {"online": True} assert "Sending event notification for entity event.doorbell" in caplog.text assert ( "Unable to send notification with result code: 500, check log for more info" From 1fd5f64dcc21f4b873b9ac959f86c70906cf1469 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 7 Apr 2024 16:30:54 -1000 Subject: [PATCH 396/967] Migrate matrix to use run_immediately for start listener (#115167) * Migrate matrix to use run_immediately for start listener * Migrate matrix to use run_immediately for start listener --- homeassistant/components/matrix/__init__.py | 2 +- tests/components/matrix/conftest.py | 3 +++ tests/components/matrix/test_rooms.py | 28 ++++++++++++++++++--- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/matrix/__init__.py b/homeassistant/components/matrix/__init__.py index a283ba20dcb..98653ba19ad 100644 --- a/homeassistant/components/matrix/__init__.py +++ b/homeassistant/components/matrix/__init__.py @@ -220,7 +220,7 @@ class MatrixBot: ) # milliseconds. self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, handle_startup, run_immediately=False + EVENT_HOMEASSISTANT_START, handle_startup, run_immediately=True ) def _load_commands(self, commands: list[ConfigCommand]) -> None: diff --git a/tests/components/matrix/conftest.py b/tests/components/matrix/conftest.py index 2c24f4d0e75..18227914df4 100644 --- a/tests/components/matrix/conftest.py +++ b/tests/components/matrix/conftest.py @@ -281,6 +281,9 @@ async def matrix_bot( assert await async_setup_component(hass, MATRIX_DOMAIN, MOCK_CONFIG_DATA) assert await async_setup_component(hass, NOTIFY_DOMAIN, MOCK_CONFIG_DATA) await hass.async_block_till_done() + + # Accessing hass.data in tests is not desirable, but all the tests here + # currently do this. assert isinstance(matrix_bot := hass.data[MATRIX_DOMAIN], MatrixBot) await hass.async_start() diff --git a/tests/components/matrix/test_rooms.py b/tests/components/matrix/test_rooms.py index 29081b80fd5..66d1afbf532 100644 --- a/tests/components/matrix/test_rooms.py +++ b/tests/components/matrix/test_rooms.py @@ -1,14 +1,36 @@ """Test MatrixBot._join.""" +import pytest + from homeassistant.components.matrix import MatrixBot +from homeassistant.components.matrix.const import DOMAIN as MATRIX_DOMAIN +from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN +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 -async def test_join(hass, matrix_bot: MatrixBot, caplog): +async def test_join( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + mock_client, + mock_save_json, + mock_allowed_path, +) -> None: """Test joining configured rooms.""" + assert await async_setup_component(hass, MATRIX_DOMAIN, MOCK_CONFIG_DATA) + assert await async_setup_component(hass, NOTIFY_DOMAIN, MOCK_CONFIG_DATA) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done(wait_background_tasks=True) + + # Accessing hass.data in tests is not desirable, but all the tests here + # currently do this. + matrix_bot = hass.data[MATRIX_DOMAIN] - await hass.async_start() for room_id in TEST_JOINABLE_ROOMS: assert f"Joined or already in room '{room_id}'" in caplog.messages @@ -21,7 +43,7 @@ async def test_join(hass, matrix_bot: MatrixBot, caplog): ) -async def test_resolve_aliases(hass, matrix_bot: MatrixBot): +async def test_resolve_aliases(hass: HomeAssistant, matrix_bot: MatrixBot) -> None: """Test resolving configured room aliases into room ids.""" await hass.async_start() From 9ef28f83eaad50ba763b191e58e8be0050d4aaba Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 7 Apr 2024 16:38:25 -1000 Subject: [PATCH 397/967] Switch async_track_state_change to use run_immediately (#115164) --- homeassistant/helpers/event.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index e3f91320c7b..b622534d571 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -276,7 +276,7 @@ def async_track_state_change( EVENT_STATE_CHANGED, state_change_dispatcher, event_filter=state_change_filter, - run_immediately=False, + run_immediately=True, ) From 3daecc7a31a00108e551397a30128f356b060936 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 7 Apr 2024 16:47:41 -1000 Subject: [PATCH 398/967] Remove remaining run_immediately=False from tests (#115168) --- tests/common.py | 6 +++--- tests/components/automation/test_init.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/common.py b/tests/common.py index 3ed0f9e2008..88450d34564 100644 --- a/tests/common.py +++ b/tests/common.py @@ -353,13 +353,13 @@ async def async_test_home_assistant( hass.set_state(CoreState.running) - @callback - def clear_instance(event): + async def clear_instance(event): """Clear global instance.""" + await asyncio.sleep(0) # Give aiohttp one loop iteration to close INSTANCES.remove(hass) hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_CLOSE, clear_instance, run_immediately=False + EVENT_HOMEASSISTANT_CLOSE, clear_instance, run_immediately=True ) yield hass diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index ba02e61f0a7..439ca76d545 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -2506,6 +2506,7 @@ async def test_recursive_automation_starting_script( async def async_automation_triggered(event): """Listen to automation_triggered event from the automation integration.""" automation_triggered.append(event) + await asyncio.sleep(0) # Yield to allow other tasks to run hass.states.async_set("sensor.test", str(len(automation_triggered))) hass.services.async_register("test", "script_done", async_service_handler) @@ -2513,7 +2514,7 @@ async def test_recursive_automation_starting_script( "test", "automation_started", async_service_handler ) hass.bus.async_listen( - "automation_triggered", async_automation_triggered, run_immediately=False + "automation_triggered", async_automation_triggered, run_immediately=True ) hass.bus.async_fire("trigger_automation") From 111ffdd77e79ec5ec662a10ba3deb069d1a65ac8 Mon Sep 17 00:00:00 2001 From: Federico D'Amico <48856240+FedDam@users.noreply.github.com> Date: Mon, 8 Apr 2024 05:35:46 +0200 Subject: [PATCH 399/967] Improve microBees code quality (#114939) microBees code quality --- homeassistant/components/microbees/cover.py | 14 +++++++++++--- homeassistant/components/microbees/light.py | 16 ++++++++-------- homeassistant/components/microbees/strings.json | 5 ++++- homeassistant/components/microbees/switch.py | 16 ++++++++-------- 4 files changed, 31 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/microbees/cover.py b/homeassistant/components/microbees/cover.py index bdf6e815af1..b6d5d366d89 100644 --- a/homeassistant/components/microbees/cover.py +++ b/homeassistant/components/microbees/cover.py @@ -19,6 +19,8 @@ from .const import DOMAIN from .coordinator import MicroBeesUpdateCoordinator from .entity import MicroBeesEntity +COVER_IDS = {47: "roller_shutter"} + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -32,11 +34,17 @@ async def async_setup_entry( MBCover( coordinator, bee_id, - next(filter(lambda x: x.deviceID == 551, bee.actuators)).id, - next(filter(lambda x: x.deviceID == 552, bee.actuators)).id, + next( + (actuator.id for actuator in bee.actuators if actuator.deviceID == 551), + None, + ), + next( + (actuator.id for actuator in bee.actuators if actuator.deviceID == 552), + None, + ), ) for bee_id, bee in coordinator.data.bees.items() - if bee.productID == 47 + if bee.productID in COVER_IDS ) diff --git a/homeassistant/components/microbees/light.py b/homeassistant/components/microbees/light.py index 411eab22324..654cdc37182 100644 --- a/homeassistant/components/microbees/light.py +++ b/homeassistant/components/microbees/light.py @@ -60,19 +60,19 @@ class MBLight(MicroBeesActuatorEntity, LightEntity): sendCommand = await self.coordinator.microbees.sendCommand( self.actuator_id, 1, color=self._attr_rgbw_color ) - if sendCommand: - self.actuator.value = True - self.async_write_ha_state() - else: + if not sendCommand: raise HomeAssistantError(f"Failed to turn on {self.name}") + self.actuator.value = True + self.async_write_ha_state() + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the light.""" sendCommand = await self.coordinator.microbees.sendCommand( self.actuator_id, 0, color=self._attr_rgbw_color ) - if sendCommand: - self.actuator.value = False - self.async_write_ha_state() - else: + if not sendCommand: raise HomeAssistantError(f"Failed to turn off {self.name}") + + self.actuator.value = False + self.async_write_ha_state() diff --git a/homeassistant/components/microbees/strings.json b/homeassistant/components/microbees/strings.json index 6f17a12834e..49d42af83d3 100644 --- a/homeassistant/components/microbees/strings.json +++ b/homeassistant/components/microbees/strings.json @@ -19,7 +19,10 @@ "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%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]", + "wrong_account": "You can only reauthenticate this entry with the same microBees account." }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" diff --git a/homeassistant/components/microbees/switch.py b/homeassistant/components/microbees/switch.py index 8e3c03e9ba4..1d668d041e1 100644 --- a/homeassistant/components/microbees/switch.py +++ b/homeassistant/components/microbees/switch.py @@ -56,17 +56,17 @@ class MBSwitch(MicroBeesActuatorEntity, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the switch.""" send_command = await self.coordinator.microbees.sendCommand(self.actuator_id, 1) - if send_command: - self.actuator.value = True - self.async_write_ha_state() - else: + if not send_command: raise HomeAssistantError(f"Failed to turn on {self.name}") + self.actuator.value = True + self.async_write_ha_state() + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the switch.""" send_command = await self.coordinator.microbees.sendCommand(self.actuator_id, 0) - if send_command: - self.actuator.value = False - self.async_write_ha_state() - else: + if not send_command: raise HomeAssistantError(f"Failed to turn off {self.name}") + + self.actuator.value = False + self.async_write_ha_state() From 487480dc8874ed1de4562bbdfb9f1dafed5ff7bd Mon Sep 17 00:00:00 2001 From: MarkGodwin <10632972+MarkGodwin@users.noreply.github.com> Date: Mon, 8 Apr 2024 05:26:20 +0100 Subject: [PATCH 400/967] Address late review of TP-Link Omada (#115121) tplink_omada implement feedback from #114138 --- .../components/tplink_omada/switch.py | 88 ++++++++++--------- tests/components/tplink_omada/test_switch.py | 2 +- 2 files changed, 47 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/tplink_omada/switch.py b/homeassistant/components/tplink_omada/switch.py index b8abb4cb773..9f9eeceb866 100644 --- a/homeassistant/components/tplink_omada/switch.py +++ b/homeassistant/components/tplink_omada/switch.py @@ -64,6 +64,7 @@ async def async_setup_entry( ]( coordinator, switch, + port, port.port_id, desc, port_name=_get_switch_port_base_name(port), @@ -79,7 +80,7 @@ async def async_setup_entry( entities.extend( OmadaDevicePortSwitchEntity[ OmadaGatewayCoordinator, OmadaGateway, OmadaGatewayPortStatus - ](gateway_coordinator, gateway, p.port_number, desc) + ](gateway_coordinator, gateway, p, str(p.port_number), desc) for p in gateway.port_status for desc in GATEWAY_PORT_STATUS_SWITCHES if desc.exists_func(gateway, p) @@ -87,7 +88,7 @@ async def async_setup_entry( entities.extend( OmadaDevicePortSwitchEntity[ OmadaGatewayCoordinator, OmadaGateway, OmadaGatewayPortConfig - ](gateway_coordinator, gateway, p.port_number, desc) + ](gateway_coordinator, gateway, p, str(p.port_number), desc) for p in gateway.port_configs for desc in GATEWAY_PORT_CONFIG_SWITCHES if desc.exists_func(gateway, p) @@ -111,12 +112,9 @@ class OmadaDevicePortSwitchEntityDescription( """Entity description for a toggle switch derived from a network port on an Omada device.""" exists_func: Callable[[TDevice, TPort], bool] = lambda _, p: True - coordinator_update_func: Callable[ - [TCoordinator, TDevice, int | str], TPort | None - ] = lambda *_: None - set_func: Callable[[OmadaSiteClient, TDevice, TPort, bool], Awaitable[TPort]] + coordinator_update_func: Callable[[TCoordinator, TDevice, TPort], TPort | None] + set_func: Callable[[OmadaSiteClient, TDevice, TPort, bool], Awaitable[TPort | None]] update_func: Callable[[TPort], bool] - refresh_after_set: bool = False @dataclass(frozen=True, kw_only=True) @@ -128,9 +126,9 @@ class OmadaSwitchPortSwitchEntityDescription( """Entity description for a toggle switch for a feature of a Port on an Omada Switch.""" coordinator_update_func: Callable[ - [OmadaSwitchPortCoordinator, OmadaSwitch, int | str], + [OmadaSwitchPortCoordinator, OmadaSwitch, OmadaSwitchPortDetails], OmadaSwitchPortDetails | None, - ] = lambda coord, _, port_id: coord.data.get(str(port_id)) + ] = lambda coord, _, port: coord.data.get(port.port_id) @dataclass(frozen=True, kw_only=True) @@ -142,10 +140,12 @@ class OmadaGatewayPortConfigSwitchEntityDescription( """Entity description for a toggle switch for a configuration of a Port on an Omada Gateway.""" coordinator_update_func: Callable[ - [OmadaGatewayCoordinator, OmadaGateway, int | str], + [OmadaGatewayCoordinator, OmadaGateway, OmadaGatewayPortConfig], OmadaGatewayPortConfig | None, - ] = lambda coord, device, port_id: next( - p for p in coord.data[device.mac].port_configs if p.port_number == port_id + ] = lambda coord, device, port: next( + p + for p in coord.data[device.mac].port_configs + if p.port_number == port.port_number ) @@ -158,20 +158,24 @@ class OmadaGatewayPortStatusSwitchEntityDescription( """Entity description for a toggle switch for a status of a Port on an Omada Gateway.""" coordinator_update_func: Callable[ - [OmadaGatewayCoordinator, OmadaGateway, int | str], OmadaGatewayPortStatus - ] = lambda coord, device, port_id: next( - p for p in coord.data[device.mac].port_status if p.port_number == port_id + [OmadaGatewayCoordinator, OmadaGateway, OmadaGatewayPortStatus], + OmadaGatewayPortStatus, + ] = lambda coord, device, port: next( + p + for p in coord.data[device.mac].port_status + if p.port_number == port.port_number ) -def _wan_connect_disconnect( +async def _wan_connect_disconnect( client: OmadaSiteClient, device: OmadaDevice, port: OmadaGatewayPortStatus, enable: bool, ipv6: bool, -) -> Awaitable[OmadaGatewayPortStatus]: - return client.set_gateway_wan_port_connect_state( +) -> None: + # The state returned by the API is not valid. By returning None, we force a refresh + await client.set_gateway_wan_port_connect_state( port.port_number, enable, device, ipv6=ipv6 ) @@ -180,10 +184,13 @@ SWITCH_PORT_DETAILS_SWITCHES: list[OmadaSwitchPortSwitchEntityDescription] = [ OmadaSwitchPortSwitchEntityDescription( key="poe", translation_key="poe_control", - exists_func=lambda d, p: d.device_capabilities.supports_poe - and p.type != PortType.SFP, - set_func=lambda client, device, port, enable: client.update_switch_port( - device, port, overrides=SwitchPortOverrides(enable_poe=enable) + exists_func=( + lambda d, p: d.device_capabilities.supports_poe and p.type != PortType.SFP + ), + set_func=( + lambda client, device, port, enable: client.update_switch_port( + device, port, overrides=SwitchPortOverrides(enable_poe=enable) + ) ), update_func=lambda p: p.poe_mode != PoEMode.DISABLED, entity_category=EntityCategory.CONFIG, @@ -197,7 +204,6 @@ GATEWAY_PORT_STATUS_SWITCHES: list[OmadaGatewayPortStatusSwitchEntityDescription exists_func=lambda _, p: p.mode == GatewayPortMode.WAN, set_func=partial(_wan_connect_disconnect, ipv6=False), update_func=lambda p: p.wan_connected, - refresh_after_set=True, ), OmadaGatewayPortStatusSwitchEntityDescription( key="wan_connect_ipv6", @@ -205,7 +211,6 @@ GATEWAY_PORT_STATUS_SWITCHES: list[OmadaGatewayPortStatusSwitchEntityDescription exists_func=lambda _, p: p.mode == GatewayPortMode.WAN and p.wan_ipv6_enabled, set_func=partial(_wan_connect_disconnect, ipv6=True), update_func=lambda p: p.ipv6_wan_connected, - refresh_after_set=True, ), ] @@ -230,7 +235,6 @@ class OmadaDevicePortSwitchEntity( """Generic toggle switch entity for a Netork Port of an Omada Device.""" _attr_has_entity_name = True - _port_details: TPort | None = None entity_description: OmadaDevicePortSwitchEntityDescription[ TCoordinator, TDevice, TPort ] @@ -239,7 +243,8 @@ class OmadaDevicePortSwitchEntity( self, coordinator: TCoordinator, device: TDevice, - port_id: int | str, + port_details: TPort, + port_id: str, entity_description: OmadaDevicePortSwitchEntityDescription[ TCoordinator, TDevice, TPort ], @@ -249,9 +254,9 @@ class OmadaDevicePortSwitchEntity( super().__init__(coordinator, device) self.entity_description = entity_description self._device = device - self._port_id = port_id + self._port_details = port_details self._attr_unique_id = f"{device.mac}_{port_id}_{entity_description.key}" - self._attr_translation_placeholders = {"port_name": port_name or str(port_id)} + self._attr_translation_placeholders = {"port_name": port_name or port_id} async def async_added_to_hass(self) -> None: """When entity is added to hass.""" @@ -259,18 +264,17 @@ class OmadaDevicePortSwitchEntity( self._do_update() async def _async_turn_on_off(self, enable: bool) -> None: - if self._port_details: - self._port_details = await self.entity_description.set_func( - self.coordinator.omada_client, self._device, self._port_details, enable - ) + updated_details = await self.entity_description.set_func( + self.coordinator.omada_client, self._device, self._port_details, enable + ) - if self.entity_description.refresh_after_set: - # Refresh to make sure the requested changes stuck + if updated_details: + self._port_details = updated_details + self._attr_is_on = self.entity_description.update_func(self._port_details) + else: self._attr_is_on = enable await self.coordinator.async_request_refresh() - elif self._port_details: - self._attr_is_on = self.entity_description.update_func(self._port_details) - self.async_write_ha_state() + self.async_write_ha_state() async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" @@ -290,12 +294,12 @@ class OmadaDevicePortSwitchEntity( ) def _do_update(self) -> None: - port = self.entity_description.coordinator_update_func( - self.coordinator, self._device, self._port_id + latest_port_details = self.entity_description.coordinator_update_func( + self.coordinator, self._device, self._port_details ) - if port: - self._port_details = port - self._attr_is_on = self.entity_description.update_func(port) + if latest_port_details: + self._port_details = latest_port_details + self._attr_is_on = self.entity_description.update_func(self._port_details) @callback def _handle_coordinator_update(self) -> None: diff --git a/tests/components/tplink_omada/test_switch.py b/tests/components/tplink_omada/test_switch.py index 78b22a4e829..be2c21d02ab 100644 --- a/tests/components/tplink_omada/test_switch.py +++ b/tests/components/tplink_omada/test_switch.py @@ -160,7 +160,7 @@ async def test_gateway_port_poe_switch( assert entity.state == "on" -async def test_gaateway_wan_port_has_no_poe_switch( +async def test_gateway_wan_port_has_no_poe_switch( hass: HomeAssistant, init_integration: MockConfigEntry, ) -> None: From 16fc935c8798df465957071b639702ed5e7ac135 Mon Sep 17 00:00:00 2001 From: Richard Kroegel <42204099+rikroe@users.noreply.github.com> Date: Mon, 8 Apr 2024 09:26:50 +0200 Subject: [PATCH 401/967] Refactor BMW entity availability (#110294) Co-authored-by: Richard --- .../bmw_connected_drive/binary_sensor.py | 6 +- .../components/bmw_connected_drive/lock.py | 2 +- .../components/bmw_connected_drive/sensor.py | 55 ++-- .../snapshots/test_sensor.ambr | 274 +++++++++--------- 4 files changed, 174 insertions(+), 163 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index 85a0cbf8812..d40d85e4cd4 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -116,6 +116,7 @@ class BMWBinarySensorEntityDescription(BinarySensorEntityDescription): value_fn: Callable[[MyBMWVehicle], bool] attr_fn: Callable[[MyBMWVehicle, UnitSystem], dict[str, Any]] | None = None + is_available: Callable[[MyBMWVehicle], bool] = lambda v: v.is_lsc_enabled SENSOR_TYPES: tuple[BMWBinarySensorEntityDescription, ...] = ( @@ -174,12 +175,14 @@ SENSOR_TYPES: tuple[BMWBinarySensorEntityDescription, ...] = ( device_class=BinarySensorDeviceClass.BATTERY_CHARGING, # device class power: On means power detected, Off means no power value_fn=lambda v: v.fuel_and_battery.charging_status == ChargingState.CHARGING, + is_available=lambda v: v.has_electric_drivetrain, ), BMWBinarySensorEntityDescription( key="connection_status", translation_key="connection_status", device_class=BinarySensorDeviceClass.PLUG, value_fn=lambda v: v.fuel_and_battery.is_charger_connected, + is_available=lambda v: v.has_electric_drivetrain, ), BMWBinarySensorEntityDescription( key="is_pre_entry_climatization_enabled", @@ -187,6 +190,7 @@ SENSOR_TYPES: tuple[BMWBinarySensorEntityDescription, ...] = ( value_fn=lambda v: v.charging_profile.is_pre_entry_climatization_enabled if v.charging_profile else False, + is_available=lambda v: v.has_electric_drivetrain, ), ) @@ -203,7 +207,7 @@ async def async_setup_entry( BMWBinarySensor(coordinator, vehicle, description, hass.config.units) for vehicle in coordinator.account.vehicles for description in SENSOR_TYPES - if description.key in vehicle.available_attributes + if description.is_available(vehicle) ] async_add_entities(entities) diff --git a/homeassistant/components/bmw_connected_drive/lock.py b/homeassistant/components/bmw_connected_drive/lock.py index 9529c135280..bbfadcef9db 100644 --- a/homeassistant/components/bmw_connected_drive/lock.py +++ b/homeassistant/components/bmw_connected_drive/lock.py @@ -51,7 +51,7 @@ class BMWLock(BMWBaseEntity, LockEntity): super().__init__(coordinator, vehicle) self._attr_unique_id = f"{vehicle.vin}-lock" - self.door_lock_state_available = DOOR_LOCK_STATE in vehicle.available_attributes + self.door_lock_state_available = vehicle.is_lsc_enabled async def async_lock(self, **kwargs: Any) -> None: """Lock the car.""" diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 49842305af0..e1ed398cfec 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -36,6 +36,7 @@ class BMWSensorEntityDescription(SensorEntityDescription): 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( @@ -53,57 +54,63 @@ def convert_and_round( return None -SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = { +SENSOR_TYPES: list[BMWSensorEntityDescription] = [ # --- Generic --- - "ac_current_limit": BMWSensorEntityDescription( + BMWSensorEntityDescription( key="ac_current_limit", translation_key="ac_current_limit", key_class="charging_profile", unit_type=UnitOfElectricCurrent.AMPERE, entity_registry_enabled_default=False, + is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain, ), - "charging_start_time": BMWSensorEntityDescription( + BMWSensorEntityDescription( key="charging_start_time", translation_key="charging_start_time", key_class="fuel_and_battery", device_class=SensorDeviceClass.TIMESTAMP, entity_registry_enabled_default=False, + is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain, ), - "charging_end_time": BMWSensorEntityDescription( + BMWSensorEntityDescription( key="charging_end_time", translation_key="charging_end_time", key_class="fuel_and_battery", device_class=SensorDeviceClass.TIMESTAMP, + is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain, ), - "charging_status": BMWSensorEntityDescription( + 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, ), - "charging_target": BMWSensorEntityDescription( + BMWSensorEntityDescription( key="charging_target", translation_key="charging_target", key_class="fuel_and_battery", unit_type=PERCENTAGE, + is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain, ), - "remaining_battery_percent": BMWSensorEntityDescription( + BMWSensorEntityDescription( key="remaining_battery_percent", translation_key="remaining_battery_percent", key_class="fuel_and_battery", unit_type=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, + is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain, ), # --- Specific --- - "mileage": BMWSensorEntityDescription( + BMWSensorEntityDescription( key="mileage", translation_key="mileage", unit_type=LENGTH, value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2), state_class=SensorStateClass.TOTAL_INCREASING, ), - "remaining_range_total": BMWSensorEntityDescription( + BMWSensorEntityDescription( key="remaining_range_total", translation_key="remaining_range_total", key_class="fuel_and_battery", @@ -111,38 +118,42 @@ SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = { value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2), state_class=SensorStateClass.MEASUREMENT, ), - "remaining_range_electric": BMWSensorEntityDescription( + 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), state_class=SensorStateClass.MEASUREMENT, + is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain, ), - "remaining_range_fuel": BMWSensorEntityDescription( + 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), state_class=SensorStateClass.MEASUREMENT, + is_available=lambda v: v.is_lsc_enabled and v.has_combustion_drivetrain, ), - "remaining_fuel": BMWSensorEntityDescription( + 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), state_class=SensorStateClass.MEASUREMENT, + is_available=lambda v: v.is_lsc_enabled and v.has_combustion_drivetrain, ), - "remaining_fuel_percent": BMWSensorEntityDescription( + BMWSensorEntityDescription( key="remaining_fuel_percent", translation_key="remaining_fuel_percent", key_class="fuel_and_battery", unit_type=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, + is_available=lambda v: v.is_lsc_enabled and v.has_combustion_drivetrain, ), -} +] async def async_setup_entry( @@ -153,16 +164,12 @@ async def async_setup_entry( """Set up the MyBMW sensors from config entry.""" coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - entities: list[BMWSensor] = [] - - for vehicle in coordinator.account.vehicles: - entities.extend( - [ - BMWSensor(coordinator, vehicle, description) - for attribute_name in vehicle.available_attributes - if (description := SENSOR_TYPES.get(attribute_name)) - ] - ) + entities = [ + BMWSensor(coordinator, vehicle, description) + for vehicle in coordinator.account.vehicles + for description in SENSOR_TYPES + if description.is_available(vehicle) + ] async_add_entities(entities) diff --git a/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr b/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr index e28b4485af0..c9dd4e3ddb8 100644 --- a/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr +++ b/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr @@ -1,34 +1,6 @@ # serializer version: 1 # name: test_entity_state_attrs list([ - 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 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', @@ -54,6 +26,19 @@ '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', @@ -69,6 +54,34 @@ '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', @@ -83,47 +96,6 @@ 'last_updated': , 'state': '340', }), - 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', - '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 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', @@ -149,6 +121,19 @@ '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', @@ -164,6 +149,34 @@ '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', @@ -181,15 +194,16 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i4 eDrive40 Charging target', - 'unit_of_measurement': '%', + 'friendly_name': 'M340i xDrive Mileage', + 'state_class': , + 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.i4_edrive40_charging_target', + 'entity_id': 'sensor.m340i_xdrive_mileage', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '80', + 'state': '1121', }), StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -208,16 +222,16 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'M340i xDrive Mileage', - 'state_class': , + 'friendly_name': 'M340i xDrive Remaining range fuel', + 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.m340i_xdrive_mileage', + 'entity_id': 'sensor.m340i_xdrive_remaining_range_fuel', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1121', + 'state': '629', }), StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -233,20 +247,6 @@ 'last_updated': , 'state': '40', }), - 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', @@ -261,34 +261,6 @@ 'last_updated': , 'state': '80', }), - 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) 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', @@ -314,6 +286,19 @@ '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', @@ -329,6 +314,34 @@ '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', @@ -346,15 +359,16 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i3 (+ REX) Charging target', - 'unit_of_measurement': '%', + 'friendly_name': 'i3 (+ REX) Remaining range fuel', + 'state_class': , + 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.i3_rex_charging_target', + 'entity_id': 'sensor.i3_rex_remaining_range_fuel', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '100', + 'state': '105', }), StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -370,20 +384,6 @@ 'last_updated': , 'state': '6', }), - 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', From 1cace9a609d20a02887208311a500bc145070cf3 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Mon, 8 Apr 2024 17:44:51 +1000 Subject: [PATCH 402/967] Add reauth to Teslemetry (#114726) * Add reauth * Add tests * PARALLEL_UPDATES * Bump quality to platinum * Fix assertion * Remove quality * Remove async_create_task * Review Feedback * Remove loop inside parametrize * Change config during reauth * Fix missing return --- .../components/teslemetry/__init__.py | 14 ++- .../components/teslemetry/config_flow.py | 75 +++++++++++---- .../components/teslemetry/coordinator.py | 21 +++- .../components/teslemetry/test_config_flow.py | 96 +++++++++++++++++++ tests/components/teslemetry/test_init.py | 61 ++++++------ 5 files changed, 207 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/teslemetry/__init__.py b/homeassistant/components/teslemetry/__init__.py index 1da3533fef1..084d51ff31b 100644 --- a/homeassistant/components/teslemetry/__init__.py +++ b/homeassistant/components/teslemetry/__init__.py @@ -13,10 +13,10 @@ from tesla_fleet_api.exceptions import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ACCESS_TOKEN, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DOMAIN, LOGGER +from .const import DOMAIN from .coordinator import ( TeslemetryEnergyDataCoordinator, TeslemetryVehicleDataCoordinator, @@ -38,12 +38,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) try: products = (await teslemetry.products())["response"] - except InvalidToken: - LOGGER.error("Access token is invalid, unable to connect to Teslemetry") - return False - except SubscriptionRequired: - LOGGER.error("Subscription required, unable to connect to Telemetry") - return False + except InvalidToken as e: + raise ConfigEntryAuthFailed from e + except SubscriptionRequired as e: + raise ConfigEntryAuthFailed from e except TeslaFleetError as e: raise ConfigEntryNotReady from e diff --git a/homeassistant/components/teslemetry/config_flow.py b/homeassistant/components/teslemetry/config_flow.py index 0803688b1ca..5fb6ce56aed 100644 --- a/homeassistant/components/teslemetry/config_flow.py +++ b/homeassistant/components/teslemetry/config_flow.py @@ -14,7 +14,7 @@ from tesla_fleet_api.exceptions import ( ) import voluptuous as vol -from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -31,33 +31,38 @@ class TeslemetryConfigFlow(ConfigFlow, domain=DOMAIN): """Config Teslemetry API connection.""" VERSION = 1 + _entry: ConfigEntry | None = None + + async def async_auth(self, user_input: Mapping[str, Any]) -> dict[str, str]: + """Reusable Auth Helper.""" + teslemetry = Teslemetry( + session=async_get_clientsession(self.hass), + access_token=user_input[CONF_ACCESS_TOKEN], + ) + try: + await teslemetry.test() + except InvalidToken: + return {CONF_ACCESS_TOKEN: "invalid_access_token"} + except SubscriptionRequired: + return {"base": "subscription_required"} + except ClientConnectionError: + return {"base": "cannot_connect"} + except TeslaFleetError as e: + LOGGER.error(e) + return {"base": "unknown"} + return {} async def async_step_user( self, user_input: Mapping[str, Any] | None = None ) -> ConfigFlowResult: """Get configuration from the user.""" errors: dict[str, str] = {} - if user_input: - teslemetry = Teslemetry( - session=async_get_clientsession(self.hass), - access_token=user_input[CONF_ACCESS_TOKEN], + if user_input and not (errors := await self.async_auth(user_input)): + self._abort_if_unique_id_configured() + return self.async_create_entry( + title="Teslemetry", + data=user_input, ) - try: - await teslemetry.test() - except InvalidToken: - errors[CONF_ACCESS_TOKEN] = "invalid_access_token" - except SubscriptionRequired: - errors["base"] = "subscription_required" - except ClientConnectionError: - errors["base"] = "cannot_connect" - except TeslaFleetError as e: - LOGGER.exception(str(e)) - errors["base"] = "unknown" - else: - return self.async_create_entry( - title="Teslemetry", - data=user_input, - ) return self.async_show_form( step_id="user", @@ -65,3 +70,31 @@ class TeslemetryConfigFlow(ConfigFlow, domain=DOMAIN): description_placeholders=DESCRIPTION_PLACEHOLDERS, errors=errors, ) + + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: + """Handle reauth on failure.""" + self._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: + """Handle users reauth credentials.""" + + assert self._entry + errors: dict[str, str] = {} + + if user_input and not (errors := await self.async_auth(user_input)): + return self.async_update_reload_and_abort( + self._entry, + data=user_input, + ) + + return self.async_show_form( + step_id="reauth_confirm", + description_placeholders=DESCRIPTION_PLACEHOLDERS, + data_schema=TESLEMETRY_SCHEMA, + errors=errors, + ) diff --git a/homeassistant/components/teslemetry/coordinator.py b/homeassistant/components/teslemetry/coordinator.py index 27ff45f75a3..75794c7cdec 100644 --- a/homeassistant/components/teslemetry/coordinator.py +++ b/homeassistant/components/teslemetry/coordinator.py @@ -5,10 +5,15 @@ from typing import Any from tesla_fleet_api import EnergySpecific, VehicleSpecific from tesla_fleet_api.const import VehicleDataEndpoint -from tesla_fleet_api.exceptions import TeslaFleetError, VehicleOffline +from tesla_fleet_api.exceptions import ( + InvalidToken, + SubscriptionRequired, + TeslaFleetError, + VehicleOffline, +) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import LOGGER, TeslemetryState @@ -54,6 +59,10 @@ class TeslemetryVehicleDataCoordinator(TeslemetryDataCoordinator): if response["response"]["state"] != TeslemetryState.ONLINE: # The first refresh will fail, so retry later raise ConfigEntryNotReady("Vehicle is not online") + except InvalidToken as e: + raise ConfigEntryAuthFailed from e + except SubscriptionRequired as e: + raise ConfigEntryAuthFailed from e except TeslaFleetError as e: # The first refresh will also fail, so retry later raise ConfigEntryNotReady from e @@ -67,6 +76,10 @@ class TeslemetryVehicleDataCoordinator(TeslemetryDataCoordinator): except VehicleOffline: self.data["state"] = TeslemetryState.OFFLINE return self.data + except InvalidToken as e: + raise ConfigEntryAuthFailed from e + except SubscriptionRequired as e: + raise ConfigEntryAuthFailed from e except TeslaFleetError as e: raise UpdateFailed(e.message) from e @@ -97,6 +110,10 @@ class TeslemetryEnergyDataCoordinator(TeslemetryDataCoordinator): try: data = await self.api.live_status() + except InvalidToken as e: + raise ConfigEntryAuthFailed from e + except SubscriptionRequired as e: + raise ConfigEntryAuthFailed from e except TeslaFleetError as e: raise UpdateFailed(e.message) from e diff --git a/tests/components/teslemetry/test_config_flow.py b/tests/components/teslemetry/test_config_flow.py index f2894c695fa..2f12b202712 100644 --- a/tests/components/teslemetry/test_config_flow.py +++ b/tests/components/teslemetry/test_config_flow.py @@ -18,6 +18,10 @@ from homeassistant.data_entry_flow import FlowResultType from .const import CONFIG +from tests.common import MockConfigEntry + +BAD_CONFIG = {CONF_ACCESS_TOKEN: "bad_access_token"} + @pytest.fixture(autouse=True) def mock_test(): @@ -86,3 +90,95 @@ async def test_form_errors(hass: HomeAssistant, side_effect, error, mock_test) - CONFIG, ) assert result3["type"] is FlowResultType.CREATE_ENTRY + + +async def test_reauth(hass: HomeAssistant, mock_test) -> None: + """Test reauth flow.""" + + mock_entry = MockConfigEntry( + domain=DOMAIN, + data=BAD_CONFIG, + ) + mock_entry.add_to_hass(hass) + + result1 = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": mock_entry.entry_id, + }, + data=BAD_CONFIG, + ) + + assert result1["type"] is FlowResultType.FORM + assert result1["step_id"] == "reauth_confirm" + assert not result1["errors"] + + with patch( + "homeassistant.components.teslemetry.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result1["flow_id"], + CONFIG, + ) + await hass.async_block_till_done() + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_test.mock_calls) == 1 + + assert result2["type"] is FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" + assert mock_entry.data == CONFIG + + +@pytest.mark.parametrize( + ("side_effect", "error"), + [ + (InvalidToken, {CONF_ACCESS_TOKEN: "invalid_access_token"}), + (SubscriptionRequired, {"base": "subscription_required"}), + (ClientConnectionError, {"base": "cannot_connect"}), + (TeslaFleetError, {"base": "unknown"}), + ], +) +async def test_reauth_errors( + hass: HomeAssistant, mock_test, side_effect, error +) -> None: + """Test reauth flows that fail.""" + + # Start the reauth + mock_entry = MockConfigEntry( + domain=DOMAIN, + data=BAD_CONFIG, + ) + 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, + }, + data=BAD_CONFIG, + ) + + mock_test.side_effect = side_effect + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + BAD_CONFIG, + ) + await hass.async_block_till_done() + + assert result2["type"] is FlowResultType.FORM + assert result2["errors"] == error + + # Complete the flow + mock_test.side_effect = None + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + CONFIG, + ) + assert "errors" not in result3 + assert result3["type"] is FlowResultType.ABORT + assert result3["reason"] == "reauth_successful" + assert mock_entry.data == CONFIG diff --git a/tests/components/teslemetry/test_init.py b/tests/components/teslemetry/test_init.py index 9742338f27a..fb405e2ee03 100644 --- a/tests/components/teslemetry/test_init.py +++ b/tests/components/teslemetry/test_init.py @@ -3,6 +3,7 @@ from datetime import timedelta from freezegun.api import FrozenDateTimeFactory +import pytest from tesla_fleet_api.exceptions import ( InvalidToken, SubscriptionRequired, @@ -20,6 +21,12 @@ from .const import WAKE_UP_ASLEEP, WAKE_UP_ONLINE from tests.common import async_fire_time_changed +ERRORS = [ + (InvalidToken, ConfigEntryState.SETUP_ERROR), + (SubscriptionRequired, ConfigEntryState.SETUP_ERROR), + (TeslaFleetError, ConfigEntryState.SETUP_RETRY), +] + async def test_load_unload(hass: HomeAssistant) -> None: """Test load and unload.""" @@ -31,28 +38,15 @@ async def test_load_unload(hass: HomeAssistant) -> None: assert entry.state is ConfigEntryState.NOT_LOADED -async def test_auth_failure(hass: HomeAssistant, mock_products) -> None: - """Test init with an authentication error.""" +@pytest.mark.parametrize(("side_effect", "state"), ERRORS) +async def test_init_error( + hass: HomeAssistant, mock_products, side_effect, state +) -> None: + """Test init with errors.""" - mock_products.side_effect = InvalidToken + mock_products.side_effect = side_effect entry = await setup_platform(hass) - assert entry.state is ConfigEntryState.SETUP_ERROR - - -async def test_subscription_failure(hass: HomeAssistant, mock_products) -> None: - """Test init with an client response error.""" - - mock_products.side_effect = SubscriptionRequired - entry = await setup_platform(hass) - assert entry.state is ConfigEntryState.SETUP_ERROR - - -async def test_other_failure(hass: HomeAssistant, mock_products) -> None: - """Test init with an client response error.""" - - mock_products.side_effect = TeslaFleetError - entry = await setup_platform(hass) - assert entry.state is ConfigEntryState.SETUP_RETRY + assert entry.state is state # Vehicle Coordinator @@ -88,11 +82,14 @@ async def test_vehicle_first_refresh( mock_vehicle_data.assert_called_once() -async def test_vehicle_first_refresh_error(hass: HomeAssistant, mock_wake_up) -> None: +@pytest.mark.parametrize(("side_effect", "state"), ERRORS) +async def test_vehicle_first_refresh_error( + hass: HomeAssistant, mock_wake_up, side_effect, state +) -> None: """Test first coordinator refresh with an error.""" - mock_wake_up.side_effect = TeslaFleetError + mock_wake_up.side_effect = side_effect entry = await setup_platform(hass) - assert entry.state is ConfigEntryState.SETUP_RETRY + assert entry.state is state async def test_vehicle_refresh_offline( @@ -111,18 +108,24 @@ async def test_vehicle_refresh_offline( mock_vehicle_data.assert_called_once() -async def test_vehicle_refresh_error(hass: HomeAssistant, mock_vehicle_data) -> None: +@pytest.mark.parametrize(("side_effect", "state"), ERRORS) +async def test_vehicle_refresh_error( + hass: HomeAssistant, mock_vehicle_data, side_effect, state +) -> None: """Test coordinator refresh with an error.""" - mock_vehicle_data.side_effect = TeslaFleetError + mock_vehicle_data.side_effect = side_effect entry = await setup_platform(hass) - assert entry.state is ConfigEntryState.SETUP_RETRY + assert entry.state is state # Test Energy Coordinator -async def test_energy_refresh_error(hass: HomeAssistant, mock_live_status) -> None: +@pytest.mark.parametrize(("side_effect", "state"), ERRORS) +async def test_energy_refresh_error( + hass: HomeAssistant, mock_live_status, side_effect, state +) -> None: """Test coordinator refresh with an error.""" - mock_live_status.side_effect = TeslaFleetError + mock_live_status.side_effect = side_effect entry = await setup_platform(hass) - assert entry.state is ConfigEntryState.SETUP_RETRY + assert entry.state is state From 8b5177e989abe8b16165bbecbde8df232288dd47 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 8 Apr 2024 09:50:28 +0200 Subject: [PATCH 403/967] Add IMAP fetch service (#115127) * Add IMAP fetch service * Fix docstr --- homeassistant/components/imap/__init__.py | 46 +++++++++++++- homeassistant/components/imap/icons.json | 3 +- homeassistant/components/imap/services.yaml | 13 ++++ homeassistant/components/imap/strings.json | 17 ++++++ tests/components/imap/test_init.py | 67 ++++++++++++++------- 5 files changed, 121 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/imap/__init__.py b/homeassistant/components/imap/__init__.py index 22e32187255..f39a78925c1 100644 --- a/homeassistant/components/imap/__init__.py +++ b/homeassistant/components/imap/__init__.py @@ -11,7 +11,13 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform -from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.core import ( + HomeAssistant, + ServiceCall, + ServiceResponse, + SupportsResponse, + callback, +) from homeassistant.exceptions import ( ConfigEntryAuthFailed, ConfigEntryError, @@ -23,6 +29,7 @@ from homeassistant.helpers.typing import ConfigType from .const import CONF_ENABLE_PUSH, DOMAIN from .coordinator import ( + ImapMessage, ImapPollingDataUpdateCoordinator, ImapPushDataUpdateCoordinator, connect_to_server, @@ -56,6 +63,7 @@ SERVICE_MOVE_SCHEMA = _SERVICE_UID_SCHEMA.extend( } ) SERVICE_DELETE_SCHEMA = _SERVICE_UID_SCHEMA +SERVICE_FETCH_TEXT_SCHEMA = _SERVICE_UID_SCHEMA async def async_get_imap_client(hass: HomeAssistant, entry_id: str) -> IMAP4_SSL: @@ -188,6 +196,42 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.services.async_register(DOMAIN, "delete", async_delete, SERVICE_DELETE_SCHEMA) + async def async_fetch(call: ServiceCall) -> ServiceResponse: + """Process fetch email service and return content.""" + entry_id: str = call.data[CONF_ENTRY] + uid: str = call.data[CONF_UID] + _LOGGER.debug( + "Fetch text for message %s. Entry: %s", + uid, + entry_id, + ) + client = await async_get_imap_client(hass, entry_id) + try: + response = await client.fetch(uid, "BODY.PEEK[]") + except (TimeoutError, AioImapException) as exc: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="imap_server_fail", + translation_placeholders={"error": str(exc)}, + ) from exc + raise_on_error(response, "fetch_failed") + message = ImapMessage(response.lines[1]) + await client.close() + return { + "text": message.text, + "sender": message.sender, + "subject": message.subject, + "uid": uid, + } + + hass.services.async_register( + DOMAIN, + "fetch", + async_fetch, + SERVICE_FETCH_TEXT_SCHEMA, + supports_response=SupportsResponse.ONLY, + ) + return True diff --git a/homeassistant/components/imap/icons.json b/homeassistant/components/imap/icons.json index 2e61cf56573..6672f9a4a7f 100644 --- a/homeassistant/components/imap/icons.json +++ b/homeassistant/components/imap/icons.json @@ -12,6 +12,7 @@ "services": { "seen": "mdi:email-open-outline", "move": "mdi:email-arrow-right-outline", - "delete": "mdi:trash-can-outline" + "delete": "mdi:trash-can-outline", + "fetch": "mdi:email-sync-outline" } } diff --git a/homeassistant/components/imap/services.yaml b/homeassistant/components/imap/services.yaml index f0694bfba70..be56eb148da 100644 --- a/homeassistant/components/imap/services.yaml +++ b/homeassistant/components/imap/services.yaml @@ -43,3 +43,16 @@ delete: required: true selector: text: + +fetch: + fields: + entry: + required: true + selector: + config_entry: + integration: "imap" + uid: + required: true + example: "12" + selector: + text: diff --git a/homeassistant/components/imap/strings.json b/homeassistant/components/imap/strings.json index 3a20fc244c6..a8413922036 100644 --- a/homeassistant/components/imap/strings.json +++ b/homeassistant/components/imap/strings.json @@ -45,6 +45,9 @@ "expunge_failed": { "message": "Expunging the message failed with \"{error}\"." }, + "fetch_failed": { + "message": "Fetching the message text failed with \"{error}\"." + }, "invalid_entry": { "message": "No valid IMAP entry was found." }, @@ -92,6 +95,20 @@ } }, "services": { + "fetch": { + "name": "Fetch message", + "description": "Fetch the email message from the server.", + "fields": { + "entry": { + "name": "Entry", + "description": "The IMAP config entry." + }, + "uid": { + "name": "UID", + "description": "The email identifier (UID)." + } + } + }, "seen": { "name": "Mark message as seen", "description": "Mark an email as seen.", diff --git a/tests/components/imap/test_init.py b/tests/components/imap/test_init.py index b0cfb9051a4..69c1aaabb2e 100644 --- a/tests/components/imap/test_init.py +++ b/tests/components/imap/test_init.py @@ -847,6 +847,17 @@ async def test_services(hass: HomeAssistant, mock_imap_protocol: MagicMock) -> N mock_imap_protocol.store.assert_called_with("1", "+FLAGS (\\Deleted)") mock_imap_protocol.protocol.expunge.assert_called_once() + # Test fetch service + data = {"entry": config_entry.entry_id, "uid": "1"} + response = await hass.services.async_call( + DOMAIN, "fetch", data, blocking=True, return_response=True + ) + mock_imap_protocol.fetch.assert_called_with("1", "BODY.PEEK[]") + assert response["text"] == "Test body\r\n" + assert response["sender"] == "john.doe@example.com" + assert response["subject"] == "Test subject" + assert response["uid"] == "1" + # Test with invalid entry_id data = {"entry": "invalid", "uid": "1"} with pytest.raises(ServiceValidationError) as exc: @@ -877,43 +888,53 @@ async def test_services(hass: HomeAssistant, mock_imap_protocol: MagicMock) -> N ) # Test unexpected errors with storing a flag during a service call - service_calls = { - "seen": {"entry": config_entry.entry_id, "uid": "1"}, - "move": { - "entry": config_entry.entry_id, - "uid": "1", - "seen": False, - "target_folder": "Trash", - }, - "delete": {"entry": config_entry.entry_id, "uid": "1"}, + service_calls_response = { + "seen": ({"entry": config_entry.entry_id, "uid": "1"}, False), + "move": ( + { + "entry": config_entry.entry_id, + "uid": "1", + "seen": False, + "target_folder": "Trash", + }, + False, + ), + "delete": ({"entry": config_entry.entry_id, "uid": "1"}, False), + "fetch": ({"entry": config_entry.entry_id, "uid": "1"}, True), } - store_error_translation_key = { - "seen": "seen_failed", - "move": "copy_failed", - "delete": "delete_failed", + patch_error_translation_key = { + "seen": ("store", "seen_failed"), + "move": ("copy", "copy_failed"), + "delete": ("store", "delete_failed"), + "fetch": ("fetch", "fetch_failed"), } - for service, data in service_calls.items(): + for service, (data, response) in service_calls_response.items(): with ( pytest.raises(ServiceValidationError) as exc, patch.object( - mock_imap_protocol, "store", side_effect=AioImapException("Bla") + mock_imap_protocol, + patch_error_translation_key[service][0], + side_effect=AioImapException("Bla"), ), ): - await hass.services.async_call(DOMAIN, service, data, blocking=True) + await hass.services.async_call( + DOMAIN, service, data, blocking=True, return_response=response + ) assert exc.value.translation_domain == DOMAIN assert exc.value.translation_key == "imap_server_fail" assert exc.value.translation_placeholders == {"error": "Bla"} - # Test with bad responses on store command + # Test with bad responses with ( pytest.raises(ServiceValidationError) as exc, patch.object( - mock_imap_protocol, "store", return_value=Response("BAD", [b"Bla"]) - ), - patch.object( - mock_imap_protocol, "copy", return_value=Response("BAD", [b"Bla"]) + mock_imap_protocol, + patch_error_translation_key[service][0], + return_value=Response("BAD", [b"Bla"]), ), ): - await hass.services.async_call(DOMAIN, service, data, blocking=True) + await hass.services.async_call( + DOMAIN, service, data, blocking=True, return_response=response + ) assert exc.value.translation_domain == DOMAIN - assert exc.value.translation_key == store_error_translation_key[service] + assert exc.value.translation_key == patch_error_translation_key[service][1] assert exc.value.translation_placeholders == {"error": "Bla"} From ecda6b70ffdd1a9ae66680276ccd7df9ab93f5a0 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 8 Apr 2024 10:04:16 +0200 Subject: [PATCH 404/967] Filter out fuzzy translations from Lokalise (#114968) --- script/translations/download.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/script/translations/download.py b/script/translations/download.py index 958a4b35a7b..8f7327c07ec 100755 --- a/script/translations/download.py +++ b/script/translations/download.py @@ -39,6 +39,8 @@ def run_download_docker(): CORE_PROJECT_ID, "--original-filenames=false", "--replace-breaks=false", + "--filter-data", + "nonfuzzy", "--export-empty-as", "skip", "--format", From f06b00c6d875e3ad0150966a31f56317ea0134ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niccol=C3=B2=20Maggioni?= Date: Mon, 8 Apr 2024 10:04:59 +0200 Subject: [PATCH 405/967] Fix hang in SNMP device_tracker implementation (#112815) Co-authored-by: J. Nick Koston --- CODEOWNERS | 2 + .../components/snmp/device_tracker.py | 154 ++++++++++++------ homeassistant/components/snmp/manifest.json | 2 +- 3 files changed, 110 insertions(+), 48 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 946caef629e..40d7c0f502a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1270,6 +1270,8 @@ build.json @home-assistant/supervisor /homeassistant/components/sms/ @ocalvo /homeassistant/components/snapcast/ @luar123 /tests/components/snapcast/ @luar123 +/homeassistant/components/snmp/ @nmaggioni +/tests/components/snmp/ @nmaggioni /homeassistant/components/snooz/ @AustinBrunkhorst /tests/components/snooz/ @AustinBrunkhorst /homeassistant/components/solaredge/ @frenck diff --git a/homeassistant/components/snmp/device_tracker.py b/homeassistant/components/snmp/device_tracker.py index 4b8ab073b9c..a1a91116f0f 100644 --- a/homeassistant/components/snmp/device_tracker.py +++ b/homeassistant/components/snmp/device_tracker.py @@ -5,8 +5,19 @@ from __future__ import annotations import binascii import logging -from pysnmp.entity import config as cfg -from pysnmp.entity.rfc3413.oneliner import cmdgen +from pysnmp.error import PySnmpError +from pysnmp.hlapi.asyncio import ( + CommunityData, + ContextData, + ObjectIdentity, + ObjectType, + SnmpEngine, + Udp6TransportTarget, + UdpTransportTarget, + UsmUserData, + bulkWalkCmd, + isEndOfMib, +) import voluptuous as vol from homeassistant.components.device_tracker import ( @@ -24,7 +35,13 @@ from .const import ( CONF_BASEOID, CONF_COMMUNITY, CONF_PRIV_KEY, + DEFAULT_AUTH_PROTOCOL, DEFAULT_COMMUNITY, + DEFAULT_PORT, + DEFAULT_PRIV_PROTOCOL, + DEFAULT_TIMEOUT, + DEFAULT_VERSION, + SNMP_VERSIONS, ) _LOGGER = logging.getLogger(__name__) @@ -40,9 +57,12 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( ) -def get_scanner(hass: HomeAssistant, config: ConfigType) -> SnmpScanner | None: +async def async_get_scanner( + hass: HomeAssistant, config: ConfigType +) -> SnmpScanner | None: """Validate the configuration and return an SNMP scanner.""" scanner = SnmpScanner(config[DOMAIN]) + await scanner.async_init() return scanner if scanner.success_init else None @@ -51,39 +71,75 @@ class SnmpScanner(DeviceScanner): """Queries any SNMP capable Access Point for connected devices.""" def __init__(self, config): - """Initialize the scanner.""" + """Initialize the scanner and test the target device.""" + host = config[CONF_HOST] + community = config[CONF_COMMUNITY] + baseoid = config[CONF_BASEOID] + authkey = config.get(CONF_AUTH_KEY) + authproto = DEFAULT_AUTH_PROTOCOL + privkey = config.get(CONF_PRIV_KEY) + privproto = DEFAULT_PRIV_PROTOCOL - self.snmp = cmdgen.CommandGenerator() + try: + # Try IPv4 first. + target = UdpTransportTarget((host, DEFAULT_PORT), timeout=DEFAULT_TIMEOUT) + except PySnmpError: + # Then try IPv6. + try: + target = Udp6TransportTarget( + (host, DEFAULT_PORT), timeout=DEFAULT_TIMEOUT + ) + except PySnmpError as err: + _LOGGER.error("Invalid SNMP host: %s", err) + return - self.host = cmdgen.UdpTransportTarget((config[CONF_HOST], 161)) - if CONF_AUTH_KEY not in config or CONF_PRIV_KEY not in config: - self.auth = cmdgen.CommunityData(config[CONF_COMMUNITY]) + if authkey is not None or privkey is not None: + if not authkey: + authproto = "none" + if not privkey: + privproto = "none" + + request_args = [ + SnmpEngine(), + UsmUserData( + community, + authKey=authkey or None, + privKey=privkey or None, + authProtocol=authproto, + privProtocol=privproto, + ), + target, + ContextData(), + ] else: - self.auth = cmdgen.UsmUserData( - config[CONF_COMMUNITY], - config[CONF_AUTH_KEY], - config[CONF_PRIV_KEY], - authProtocol=cfg.usmHMACSHAAuthProtocol, - privProtocol=cfg.usmAesCfb128Protocol, - ) - self.baseoid = cmdgen.MibVariable(config[CONF_BASEOID]) - self.last_results = [] + request_args = [ + SnmpEngine(), + CommunityData(community, mpModel=SNMP_VERSIONS[DEFAULT_VERSION]), + target, + ContextData(), + ] - # Test the router is accessible - data = self.get_snmp_data() + self.request_args = request_args + self.baseoid = baseoid + self.last_results = [] + self.success_init = False + + async def async_init(self): + """Make a one-off read to check if the target device is reachable and readable.""" + data = await self.async_get_snmp_data() self.success_init = data is not None - def scan_devices(self): + async def async_scan_devices(self): """Scan for new devices and return a list with found device IDs.""" - self._update_info() + await self._async_update_info() return [client["mac"] for client in self.last_results if client.get("mac")] - def get_device_name(self, device): + async def async_get_device_name(self, device): """Return the name of the given device or None if we don't know.""" # We have no names return None - def _update_info(self): + async def _async_update_info(self): """Ensure the information from the device is up to date. Return boolean if scanning successful. @@ -91,38 +147,42 @@ class SnmpScanner(DeviceScanner): if not self.success_init: return False - if not (data := self.get_snmp_data()): + if not (data := await self.async_get_snmp_data()): return False self.last_results = data return True - def get_snmp_data(self): + async def async_get_snmp_data(self): """Fetch MAC addresses from access point via SNMP.""" devices = [] - errindication, errstatus, errindex, restable = self.snmp.nextCmd( - self.auth, self.host, self.baseoid + walker = bulkWalkCmd( + *self.request_args, + 0, + 50, + ObjectType(ObjectIdentity(self.baseoid)), + lexicographicMode=False, ) + async for errindication, errstatus, errindex, res in walker: + if errindication: + _LOGGER.error("SNMPLIB error: %s", errindication) + return + if errstatus: + _LOGGER.error( + "SNMP error: %s at %s", + errstatus.prettyPrint(), + errindex and res[int(errindex) - 1][0] or "?", + ) + return - if errindication: - _LOGGER.error("SNMPLIB error: %s", errindication) - return - if errstatus: - _LOGGER.error( - "SNMP error: %s at %s", - errstatus.prettyPrint(), - errindex and restable[int(errindex) - 1][0] or "?", - ) - return - - for resrow in restable: - for _, val in resrow: - try: - mac = binascii.hexlify(val.asOctets()).decode("utf-8") - except AttributeError: - continue - _LOGGER.debug("Found MAC address: %s", mac) - mac = ":".join([mac[i : i + 2] for i in range(0, len(mac), 2)]) - devices.append({"mac": mac}) + for _oid, value in res: + if not isEndOfMib(res): + try: + mac = binascii.hexlify(value.asOctets()).decode("utf-8") + except AttributeError: + continue + _LOGGER.debug("Found MAC address: %s", mac) + mac = ":".join([mac[i : i + 2] for i in range(0, len(mac), 2)]) + devices.append({"mac": mac}) return devices diff --git a/homeassistant/components/snmp/manifest.json b/homeassistant/components/snmp/manifest.json index c4aa82f2a74..d79910c44cd 100644 --- a/homeassistant/components/snmp/manifest.json +++ b/homeassistant/components/snmp/manifest.json @@ -1,7 +1,7 @@ { "domain": "snmp", "name": "SNMP", - "codeowners": [], + "codeowners": ["@nmaggioni"], "documentation": "https://www.home-assistant.io/integrations/snmp", "iot_class": "local_polling", "loggers": ["pyasn1", "pysmi", "pysnmp"], From e403b50ddc6dadea5670a776ade0361070f8a428 Mon Sep 17 00:00:00 2001 From: gibwar Date: Mon, 8 Apr 2024 02:05:46 -0600 Subject: [PATCH 406/967] Only reset requested utility meter with no tariff (#115170) --- .../components/utility_meter/sensor.py | 8 +- tests/components/utility_meter/test_sensor.py | 133 ++++++++++++++++++ 2 files changed, 140 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index ff993ee3696..223e54d7d9f 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -583,7 +583,13 @@ class UtilityMeterSensor(RestoreSensor): async def async_reset_meter(self, entity_id): """Reset meter.""" - if self._tariff is not None and self._tariff_entity != entity_id: + if self._tariff_entity is not None and self._tariff_entity != entity_id: + return + if ( + self._tariff_entity is None + and entity_id is not None + and self.entity_id != entity_id + ): return _LOGGER.debug("Reset utility meter <%s>", self.entity_id) self._last_reset = dt_util.utcnow() diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index e6abd086a78..cd0a8082578 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -983,6 +983,139 @@ async def test_service_reset_no_tariffs( assert state.attributes.get("last_period") == "3" +@pytest.mark.parametrize( + ("yaml_config", "config_entry_configs"), + [ + ( + { + "utility_meter": { + "energy_bill": { + "source": "sensor.energy", + }, + "water_bill": { + "source": "sensor.water", + }, + }, + }, + None, + ), + ( + None, + [ + { + "cycle": "none", + "delta_values": False, + "name": "Energy bill", + "net_consumption": False, + "offset": 0, + "periodically_resetting": True, + "source": "sensor.energy", + "tariffs": [], + }, + { + "cycle": "none", + "delta_values": False, + "name": "Water bill", + "net_consumption": False, + "offset": 0, + "periodically_resetting": True, + "source": "sensor.water", + "tariffs": [], + }, + ], + ), + ], +) +async def test_service_reset_no_tariffs_correct_with_multi( + hass: HomeAssistant, yaml_config, config_entry_configs +) -> None: + """Test complex utility sensor service reset for multiple sensors with no tarrifs. + + See GitHub issue #114864: Service "utility_meter.reset" affects all meters. + """ + + # Home assistant is not runnit yet + hass.state = CoreState.not_running + last_reset = "2023-10-01T00:00:00+00:00" + + mock_restore_cache_with_extra_data( + hass, + [ + ( + State( + "sensor.energy_bill", + "3", + attributes={ + ATTR_LAST_RESET: last_reset, + }, + ), + {}, + ), + ( + State( + "sensor.water_bill", + "6", + attributes={ + ATTR_LAST_RESET: last_reset, + }, + ), + {}, + ), + ], + ) + + if yaml_config: + assert await async_setup_component(hass, DOMAIN, yaml_config) + await hass.async_block_till_done() + else: + for entry in config_entry_configs: + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options=entry, + title=entry["name"], + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("sensor.energy_bill") + assert state + assert state.state == "3" + assert state.attributes.get("last_reset") == last_reset + assert state.attributes.get("last_period") == "0" + + state = hass.states.get("sensor.water_bill") + assert state + assert state.state == "6" + assert state.attributes.get("last_reset") == last_reset + assert state.attributes.get("last_period") == "0" + + now = dt_util.utcnow() + with freeze_time(now): + await hass.services.async_call( + domain=DOMAIN, + service=SERVICE_RESET, + service_data={}, + target={"entity_id": "sensor.energy_bill"}, + blocking=True, + ) + + await hass.async_block_till_done() + + state = hass.states.get("sensor.energy_bill") + assert state + assert state.state == "0" + assert state.attributes.get("last_reset") == now.isoformat() + assert state.attributes.get("last_period") == "3" + + state = hass.states.get("sensor.water_bill") + assert state + assert state.state == "6" + assert state.attributes.get("last_reset") == last_reset + assert state.attributes.get("last_period") == "0" + + @pytest.mark.parametrize( ("yaml_config", "config_entry_config"), [ From c5f05908d7e130ae78ece347cdc461f6c70b58bb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Apr 2024 10:46:25 +0200 Subject: [PATCH 407/967] Bump github/codeql-action from 3.24.9 to 3.24.10 (#115179) --- .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 aa9822e0131..475c0bd352f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,11 +24,11 @@ jobs: uses: actions/checkout@v4.1.2 - name: Initialize CodeQL - uses: github/codeql-action/init@v3.24.9 + uses: github/codeql-action/init@v3.24.10 with: languages: python - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3.24.9 + uses: github/codeql-action/analyze@v3.24.10 with: category: "/language:python" From 53b4fd419a2d45d2eea02538b51706f56292290f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 8 Apr 2024 11:18:20 +0200 Subject: [PATCH 408/967] Update build system dependencies (#115102) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b7aca115f44..eb13aa02f5e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools==68.0.0", "wheel~=0.40.0"] +requires = ["setuptools==69.2.0", "wheel~=0.43.0"] build-backend = "setuptools.build_meta" [project] From d9dada4fb3d6f61cf84e0aaea9d79f759df64961 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 8 Apr 2024 14:33:24 +0200 Subject: [PATCH 409/967] Remove condition from ecobee humidifier attribute test (#115197) Remove conditions from ecobee humidifier attribute test --- tests/components/ecobee/test_humidifier.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/tests/components/ecobee/test_humidifier.py b/tests/components/ecobee/test_humidifier.py index f35a7dc9237..696ca3d6c0d 100644 --- a/tests/components/ecobee/test_humidifier.py +++ b/tests/components/ecobee/test_humidifier.py @@ -44,21 +44,18 @@ async def test_attributes(hass: HomeAssistant) -> None: state = hass.states.get(DEVICE_ID) assert state.state == STATE_ON - if state.attributes.get(ATTR_CURRENT_HUMIDITY): - assert state.attributes.get(ATTR_CURRENT_HUMIDITY) == 15 - assert state.attributes.get(ATTR_MIN_HUMIDITY) == DEFAULT_MIN_HUMIDITY - assert state.attributes.get(ATTR_MAX_HUMIDITY) == DEFAULT_MAX_HUMIDITY - assert state.attributes.get(ATTR_HUMIDITY) == 40 - assert state.attributes.get(ATTR_AVAILABLE_MODES) == [ + assert state.attributes[ATTR_CURRENT_HUMIDITY] == 15 + assert state.attributes[ATTR_MIN_HUMIDITY] == DEFAULT_MIN_HUMIDITY + assert state.attributes[ATTR_MAX_HUMIDITY] == DEFAULT_MAX_HUMIDITY + assert state.attributes[ATTR_HUMIDITY] == 40 + assert state.attributes[ATTR_AVAILABLE_MODES] == [ MODE_OFF, MODE_AUTO, MODE_MANUAL, ] - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "ecobee" - assert state.attributes.get(ATTR_DEVICE_CLASS) == HumidifierDeviceClass.HUMIDIFIER - assert ( - state.attributes.get(ATTR_SUPPORTED_FEATURES) == HumidifierEntityFeature.MODES - ) + assert state.attributes[ATTR_FRIENDLY_NAME] == "ecobee" + assert state.attributes[ATTR_DEVICE_CLASS] == HumidifierDeviceClass.HUMIDIFIER + assert state.attributes[ATTR_SUPPORTED_FEATURES] == HumidifierEntityFeature.MODES async def test_turn_on(hass: HomeAssistant) -> None: From 85b453b86c372a56973741de1bb88d2edf371e41 Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Mon, 8 Apr 2024 15:28:07 +0200 Subject: [PATCH 410/967] Fix Downloader test cases and error title (#114847) * Fix test cases * Return value * isdir change * FIx test cases and error title * Removing patch * Tiny needless fine-tuning --- .../components/downloader/config_flow.py | 2 +- .../components/downloader/strings.json | 2 +- .../components/downloader/test_config_flow.py | 39 +++++++------------ 3 files changed, 17 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/downloader/config_flow.py b/homeassistant/components/downloader/config_flow.py index 27101630599..e7191e055a6 100644 --- a/homeassistant/components/downloader/config_flow.py +++ b/homeassistant/components/downloader/config_flow.py @@ -29,7 +29,7 @@ class DownloaderConfigFlow(ConfigFlow, domain=DOMAIN): try: await self._validate_input(user_input) except DirectoryDoesNotExist: - errors["base"] = "cannot_connect" + errors["base"] = "directory_does_not_exist" else: return self.async_create_entry(title=DEFAULT_NAME, data=user_input) diff --git a/homeassistant/components/downloader/strings.json b/homeassistant/components/downloader/strings.json index 4cadabf96c6..cf962bd9713 100644 --- a/homeassistant/components/downloader/strings.json +++ b/homeassistant/components/downloader/strings.json @@ -6,7 +6,7 @@ } }, "error": { - "cannot_connect": "The directory could not be reached. Please check your settings." + "directory_does_not_exist": "The directory could not be reached. Please check your settings." }, "abort": { "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" diff --git a/tests/components/downloader/test_config_flow.py b/tests/components/downloader/test_config_flow.py index b561fae98e9..132b83dffdf 100644 --- a/tests/components/downloader/test_config_flow.py +++ b/tests/components/downloader/test_config_flow.py @@ -5,7 +5,6 @@ from unittest.mock import patch import pytest from homeassistant import config_entries -from homeassistant.components.downloader.config_flow import DirectoryDoesNotExist from homeassistant.components.downloader.const import CONF_DOWNLOAD_DIR, DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.core import HomeAssistant @@ -21,39 +20,35 @@ async def test_user_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=CONFIG, + ) + assert result["type"] is FlowResultType.FORM - with patch( - "homeassistant.components.downloader.async_setup_entry", - return_value=True, - ): + with patch("os.path.isdir", return_value=False): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONFIG, ) assert result["type"] is FlowResultType.FORM - - with patch( - "homeassistant.components.downloader.config_flow.DownloaderConfigFlow._validate_input", - side_effect=DirectoryDoesNotExist, - ): - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "user" - assert result["errors"] == {"base": "cannot_connect"} + assert result["step_id"] == "user" + assert result["errors"] == {"base": "directory_does_not_exist"} with ( patch( "homeassistant.components.downloader.async_setup_entry", return_value=True ), patch( - "homeassistant.components.downloader.config_flow.DownloaderConfigFlow._validate_input", - return_value=None, + "os.path.isdir", + return_value=True, ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONFIG, ) - + await hass.async_block_till_done() assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Downloader" assert result["data"] == {"download_dir": "download_dir"} @@ -66,7 +61,6 @@ async def test_single_instance_allowed( ) -> None: """Test we abort if already setup.""" mock_config_entry = MockConfigEntry(domain=DOMAIN) - mock_config_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( @@ -84,21 +78,19 @@ async def test_import_flow_success(hass: HomeAssistant) -> None: "homeassistant.components.downloader.async_setup_entry", return_value=True ), patch( - "homeassistant.components.downloader.config_flow.DownloaderConfigFlow._validate_input", - return_value=None, + "os.path.isdir", + return_value=True, ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, - data={}, + data=CONFIG, ) await hass.async_block_till_done() - assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Downloader" - assert result["data"] == {} - assert result["options"] == {} + assert result["data"] == CONFIG async def test_import_flow_directory_not_found(hass: HomeAssistant) -> None: @@ -112,6 +104,5 @@ async def test_import_flow_directory_not_found(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result["type"] is FlowResultType.ABORT assert result["reason"] == "directory_does_not_exist" From f8b6629b2650d78daad45f2482f51288edd869c4 Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Mon, 8 Apr 2024 15:42:22 +0200 Subject: [PATCH 411/967] Enable Ruff PGH rules (#115091) --- pyproject.toml | 2 +- tests/components/knx/conftest.py | 2 +- tests/components/plex/test_update.py | 2 +- tests/components/recorder/db_schema_0.py | 6 ++--- tests/components/recorder/db_schema_16.py | 24 ++++++++--------- tests/components/recorder/db_schema_18.py | 20 +++++++------- tests/components/recorder/db_schema_22.py | 26 +++++++++---------- tests/components/recorder/db_schema_23.py | 26 +++++++++---------- .../db_schema_23_with_newer_columns.py | 26 +++++++++---------- tests/components/ring/test_button.py | 2 +- tests/components/smartthings/test_cover.py | 6 ++--- tests/components/smartthings/test_scene.py | 2 +- tests/util/test_logging.py | 2 +- 13 files changed, 73 insertions(+), 73 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index eb13aa02f5e..f6c7acb0a97 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -634,7 +634,7 @@ select = [ "N805", # First argument of a method should be named self "N815", # Variable {name} in class scope should not be mixedCase "PERF", # Perflint - "PGH004", # Use specific rule codes when using noqa + "PGH", # pygrep-hooks "PIE", # flake8-pie "PL", # pylint "PT", # flake8-pytest-style diff --git a/tests/components/knx/conftest.py b/tests/components/knx/conftest.py index 92a9e3594ee..a580fc9eb2c 100644 --- a/tests/components/knx/conftest.py +++ b/tests/components/knx/conftest.py @@ -162,7 +162,7 @@ class KNXTestKit: if payload is not None: assert ( - telegram.payload.value.value == payload # type: ignore + telegram.payload.value.value == payload # type: ignore[attr-defined] ), f"Payload mismatch in {telegram} - Expected: {payload}" async def assert_read(self, group_address: str) -> None: diff --git a/tests/components/plex/test_update.py b/tests/components/plex/test_update.py index a96e0409bbb..942162665af 100644 --- a/tests/components/plex/test_update.py +++ b/tests/components/plex/test_update.py @@ -97,7 +97,7 @@ async def test_plex_update( }, blocking=True, ) - assert apply_mock.called_once + assert apply_mock.call_count == 1 # Failed upgrade request requests_mock.put("/updater/apply", status_code=500) diff --git a/tests/components/recorder/db_schema_0.py b/tests/components/recorder/db_schema_0.py index 6365ff6a7e7..9062de01b59 100644 --- a/tests/components/recorder/db_schema_0.py +++ b/tests/components/recorder/db_schema_0.py @@ -30,7 +30,7 @@ Base = declarative_base() _LOGGER = logging.getLogger(__name__) -class Events(Base): # type: ignore +class Events(Base): # type: ignore[valid-type,misc] """Event history data.""" __tablename__ = "events" @@ -66,7 +66,7 @@ class Events(Base): # type: ignore return None -class States(Base): # type: ignore +class States(Base): # type: ignore[valid-type,misc] """State change history.""" __tablename__ = "states" @@ -125,7 +125,7 @@ class States(Base): # type: ignore return None -class RecorderRuns(Base): # type: ignore +class RecorderRuns(Base): # type: ignore[valid-type,misc] """Representation of recorder run.""" __tablename__ = "recorder_runs" diff --git a/tests/components/recorder/db_schema_16.py b/tests/components/recorder/db_schema_16.py index 4d48400e370..24786b1ad44 100644 --- a/tests/components/recorder/db_schema_16.py +++ b/tests/components/recorder/db_schema_16.py @@ -66,7 +66,7 @@ DATETIME_TYPE = DateTime(timezone=True).with_variant( ) -class Events(Base): # type: ignore +class Events(Base): # type: ignore[valid-type,misc] """Event history data.""" __table_args__ = { @@ -84,7 +84,7 @@ class Events(Base): # type: ignore context_user_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) context_parent_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) - __table_args__ = ( # noqa: PIE794 + __table_args__ = ( # type: ignore[assignment] # noqa: PIE794 # Used for fetching events at a specific time # see logbook Index("ix_events_event_type_time_fired", "event_type", "time_fired"), @@ -133,7 +133,7 @@ class Events(Base): # type: ignore return None -class States(Base): # type: ignore +class States(Base): # type: ignore[valid-type,misc] """State change history.""" __table_args__ = { @@ -156,7 +156,7 @@ class States(Base): # type: ignore event = relationship("Events", uselist=False) old_state = relationship("States", remote_side=[state_id]) - __table_args__ = ( # noqa: PIE794 + __table_args__ = ( # type: ignore[assignment] # noqa: PIE794 # Used for fetching the state of entities at a specific time # (get_states in history.py) Index("ix_states_entity_id_last_updated", "entity_id", "last_updated"), @@ -217,7 +217,7 @@ class States(Base): # type: ignore return None -class Statistics(Base): # type: ignore +class Statistics(Base): # type: ignore[valid-type,misc] """Statistics.""" __table_args__ = { @@ -237,7 +237,7 @@ class Statistics(Base): # type: ignore state = Column(Float()) sum = Column(Float()) - __table_args__ = ( # noqa: PIE794 + __table_args__ = ( # type: ignore[assignment] # noqa: PIE794 # Used for fetching statistics for a certain entity at a specific time Index("ix_statistics_statistic_id_start", "statistic_id", "start"), ) @@ -253,7 +253,7 @@ class Statistics(Base): # type: ignore ) -class RecorderRuns(Base): # type: ignore +class RecorderRuns(Base): # type: ignore[valid-type,misc] """Representation of recorder run.""" __tablename__ = TABLE_RECORDER_RUNS @@ -304,7 +304,7 @@ class RecorderRuns(Base): # type: ignore return self -class SchemaChanges(Base): # type: ignore +class SchemaChanges(Base): # type: ignore[valid-type,misc] """Representation of schema version changes.""" __tablename__ = TABLE_SCHEMA_CHANGES @@ -366,7 +366,7 @@ class LazyState(State): self._last_updated = None self._context = None - @property # type: ignore + @property def attributes(self): """State attributes.""" if not self._attributes: @@ -383,7 +383,7 @@ class LazyState(State): """Set attributes.""" self._attributes = value - @property # type: ignore + @property def context(self): """State context.""" if not self._context: @@ -395,7 +395,7 @@ class LazyState(State): """Set context.""" self._context = value - @property # type: ignore + @property def last_changed(self): """Last changed datetime.""" if not self._last_changed: @@ -407,7 +407,7 @@ class LazyState(State): """Set last changed datetime.""" self._last_changed = value - @property # type: ignore + @property def last_updated(self): """Last updated datetime.""" if not self._last_updated: diff --git a/tests/components/recorder/db_schema_18.py b/tests/components/recorder/db_schema_18.py index 2ce0dfae5f5..db6fbb78f56 100644 --- a/tests/components/recorder/db_schema_18.py +++ b/tests/components/recorder/db_schema_18.py @@ -68,7 +68,7 @@ DATETIME_TYPE = DateTime(timezone=True).with_variant( ) -class Events(Base): # type: ignore +class Events(Base): # type: ignore[valid-type,misc] """Event history data.""" __table_args__ = ( @@ -131,7 +131,7 @@ class Events(Base): # type: ignore return None -class States(Base): # type: ignore +class States(Base): # type: ignore[valid-type,misc] """State change history.""" __table_args__ = ( @@ -211,7 +211,7 @@ class States(Base): # type: ignore return None -class Statistics(Base): # type: ignore +class Statistics(Base): # type: ignore[valid-type,misc] """Statistics.""" __table_args__ = ( @@ -244,7 +244,7 @@ class Statistics(Base): # type: ignore ) -class StatisticsMeta(Base): # type: ignore +class StatisticsMeta(Base): # type: ignore[valid-type,misc] """Statistics meta data.""" __tablename__ = TABLE_STATISTICS_META @@ -267,7 +267,7 @@ class StatisticsMeta(Base): # type: ignore ) -class RecorderRuns(Base): # type: ignore +class RecorderRuns(Base): # type: ignore[valid-type,misc] """Representation of recorder run.""" __table_args__ = (Index("ix_recorder_runs_start_end", "start", "end"),) @@ -317,7 +317,7 @@ class RecorderRuns(Base): # type: ignore return self -class SchemaChanges(Base): # type: ignore +class SchemaChanges(Base): # type: ignore[valid-type,misc] """Representation of schema version changes.""" __tablename__ = TABLE_SCHEMA_CHANGES @@ -379,7 +379,7 @@ class LazyState(State): self._last_updated = None self._context = None - @property # type: ignore + @property def attributes(self): """State attributes.""" if not self._attributes: @@ -396,7 +396,7 @@ class LazyState(State): """Set attributes.""" self._attributes = value - @property # type: ignore + @property def context(self): """State context.""" if not self._context: @@ -408,7 +408,7 @@ class LazyState(State): """Set context.""" self._context = value - @property # type: ignore + @property def last_changed(self): """Last changed datetime.""" if not self._last_changed: @@ -420,7 +420,7 @@ class LazyState(State): """Set last changed datetime.""" self._last_changed = value - @property # type: ignore + @property def last_updated(self): """Last updated datetime.""" if not self._last_updated: diff --git a/tests/components/recorder/db_schema_22.py b/tests/components/recorder/db_schema_22.py index 0d336c96403..cd0dc52a927 100644 --- a/tests/components/recorder/db_schema_22.py +++ b/tests/components/recorder/db_schema_22.py @@ -84,7 +84,7 @@ DOUBLE_TYPE = ( ) -class Events(Base): # type: ignore +class Events(Base): # type: ignore[valid-type,misc] """Event history data.""" __table_args__ = ( @@ -148,7 +148,7 @@ class Events(Base): # type: ignore return None -class States(Base): # type: ignore +class States(Base): # type: ignore[valid-type,misc] """State change history.""" __table_args__ = ( @@ -283,13 +283,13 @@ class StatisticsBase: @classmethod def from_stats(cls, metadata_id: int, stats: StatisticData): """Create object from a statistics.""" - return cls( # type: ignore + return cls( # type: ignore[call-arg,misc] metadata_id=metadata_id, **stats, ) -class Statistics(Base, StatisticsBase): # type: ignore +class Statistics(Base, StatisticsBase): # type: ignore[valid-type,misc] """Long term statistics.""" duration = timedelta(hours=1) @@ -301,7 +301,7 @@ class Statistics(Base, StatisticsBase): # type: ignore __tablename__ = TABLE_STATISTICS -class StatisticsShortTerm(Base, StatisticsBase): # type: ignore +class StatisticsShortTerm(Base, StatisticsBase): # type: ignore[valid-type,misc] """Short term statistics.""" duration = timedelta(minutes=5) @@ -322,7 +322,7 @@ class StatisticMetaData(TypedDict): has_sum: bool -class StatisticsMeta(Base): # type: ignore +class StatisticsMeta(Base): # type: ignore[valid-type,misc] """Statistics meta data.""" __table_args__ = ( @@ -354,7 +354,7 @@ class StatisticsMeta(Base): # type: ignore ) -class RecorderRuns(Base): # type: ignore +class RecorderRuns(Base): # type: ignore[valid-type,misc] """Representation of recorder run.""" __table_args__ = (Index("ix_recorder_runs_start_end", "start", "end"),) @@ -404,7 +404,7 @@ class RecorderRuns(Base): # type: ignore return self -class SchemaChanges(Base): # type: ignore +class SchemaChanges(Base): # type: ignore[valid-type,misc] """Representation of schema version changes.""" __tablename__ = TABLE_SCHEMA_CHANGES @@ -422,7 +422,7 @@ class SchemaChanges(Base): # type: ignore ) -class StatisticsRuns(Base): # type: ignore +class StatisticsRuns(Base): # type: ignore[valid-type,misc] """Representation of statistics run.""" __tablename__ = TABLE_STATISTICS_RUNS @@ -498,7 +498,7 @@ class LazyState(State): self._last_updated = None self._context = None - @property # type: ignore + @property def attributes(self): """State attributes.""" if not self._attributes: @@ -515,7 +515,7 @@ class LazyState(State): """Set attributes.""" self._attributes = value - @property # type: ignore + @property def context(self): """State context.""" if not self._context: @@ -527,7 +527,7 @@ class LazyState(State): """Set context.""" self._context = value - @property # type: ignore + @property def last_changed(self): """Last changed datetime.""" if not self._last_changed: @@ -539,7 +539,7 @@ class LazyState(State): """Set last changed datetime.""" self._last_changed = value - @property # type: ignore + @property def last_updated(self): """Last updated datetime.""" if not self._last_updated: diff --git a/tests/components/recorder/db_schema_23.py b/tests/components/recorder/db_schema_23.py index d4b6e8b0a73..9187d271216 100644 --- a/tests/components/recorder/db_schema_23.py +++ b/tests/components/recorder/db_schema_23.py @@ -83,7 +83,7 @@ DOUBLE_TYPE = ( ) -class Events(Base): # type: ignore +class Events(Base): # type: ignore[valid-type,misc] """Event history data.""" __table_args__ = ( @@ -147,7 +147,7 @@ class Events(Base): # type: ignore return None -class States(Base): # type: ignore +class States(Base): # type: ignore[valid-type,misc] """State change history.""" __table_args__ = ( @@ -282,13 +282,13 @@ class StatisticsBase: @classmethod def from_stats(cls, metadata_id: int, stats: StatisticData): """Create object from a statistics.""" - return cls( # type: ignore + return cls( # type: ignore[call-arg,misc] metadata_id=metadata_id, **stats, ) -class Statistics(Base, StatisticsBase): # type: ignore +class Statistics(Base, StatisticsBase): # type: ignore[valid-type,misc] """Long term statistics.""" duration = timedelta(hours=1) @@ -300,7 +300,7 @@ class Statistics(Base, StatisticsBase): # type: ignore __tablename__ = TABLE_STATISTICS -class StatisticsShortTerm(Base, StatisticsBase): # type: ignore +class StatisticsShortTerm(Base, StatisticsBase): # type: ignore[valid-type,misc] """Short term statistics.""" duration = timedelta(minutes=5) @@ -323,7 +323,7 @@ class StatisticMetaData(TypedDict): unit_of_measurement: str | None -class StatisticsMeta(Base): # type: ignore +class StatisticsMeta(Base): # type: ignore[valid-type,misc] """Statistics meta data.""" __table_args__ = ( @@ -344,7 +344,7 @@ class StatisticsMeta(Base): # type: ignore return StatisticsMeta(**meta) -class RecorderRuns(Base): # type: ignore +class RecorderRuns(Base): # type: ignore[valid-type,misc] """Representation of recorder run.""" __table_args__ = (Index("ix_recorder_runs_start_end", "start", "end"),) @@ -394,7 +394,7 @@ class RecorderRuns(Base): # type: ignore return self -class SchemaChanges(Base): # type: ignore +class SchemaChanges(Base): # type: ignore[valid-type,misc] """Representation of schema version changes.""" __tablename__ = TABLE_SCHEMA_CHANGES @@ -412,7 +412,7 @@ class SchemaChanges(Base): # type: ignore ) -class StatisticsRuns(Base): # type: ignore +class StatisticsRuns(Base): # type: ignore[valid-type,misc] """Representation of statistics run.""" __tablename__ = TABLE_STATISTICS_RUNS @@ -488,7 +488,7 @@ class LazyState(State): self._last_updated = None self._context = None - @property # type: ignore + @property def attributes(self): """State attributes.""" if not self._attributes: @@ -505,7 +505,7 @@ class LazyState(State): """Set attributes.""" self._attributes = value - @property # type: ignore + @property def context(self): """State context.""" if not self._context: @@ -517,7 +517,7 @@ class LazyState(State): """Set context.""" self._context = value - @property # type: ignore + @property def last_changed(self): """Last changed datetime.""" if not self._last_changed: @@ -529,7 +529,7 @@ class LazyState(State): """Set last changed datetime.""" self._last_changed = value - @property # type: ignore + @property def last_updated(self): """Last updated datetime.""" if not self._last_updated: diff --git a/tests/components/recorder/db_schema_23_with_newer_columns.py b/tests/components/recorder/db_schema_23_with_newer_columns.py index 6893a7257f4..9f902523c64 100644 --- a/tests/components/recorder/db_schema_23_with_newer_columns.py +++ b/tests/components/recorder/db_schema_23_with_newer_columns.py @@ -102,7 +102,7 @@ EVENTS_CONTEXT_ID_BIN_INDEX = "ix_events_context_id_bin" STATES_CONTEXT_ID_BIN_INDEX = "ix_states_context_id_bin" -class Events(Base): # type: ignore +class Events(Base): # type: ignore[valid-type,misc] """Event history data.""" __table_args__ = ( @@ -225,7 +225,7 @@ class EventTypes(Base): # type: ignore[misc,valid-type] event_type = Column(String(MAX_LENGTH_EVENT_EVENT_TYPE)) -class States(Base): # type: ignore +class States(Base): # type: ignore[valid-type,misc] """State change history.""" __table_args__ = ( @@ -406,13 +406,13 @@ class StatisticsBase: @classmethod def from_stats(cls, metadata_id: int, stats: StatisticData): """Create object from a statistics.""" - return cls( # type: ignore + return cls( # type: ignore[call-arg,misc] metadata_id=metadata_id, **stats, ) -class Statistics(Base, StatisticsBase): # type: ignore +class Statistics(Base, StatisticsBase): # type: ignore[valid-type,misc] """Long term statistics.""" duration = timedelta(hours=1) @@ -424,7 +424,7 @@ class Statistics(Base, StatisticsBase): # type: ignore __tablename__ = TABLE_STATISTICS -class StatisticsShortTerm(Base, StatisticsBase): # type: ignore +class StatisticsShortTerm(Base, StatisticsBase): # type: ignore[valid-type,misc] """Short term statistics.""" duration = timedelta(minutes=5) @@ -447,7 +447,7 @@ class StatisticMetaData(TypedDict): unit_of_measurement: str | None -class StatisticsMeta(Base): # type: ignore +class StatisticsMeta(Base): # type: ignore[valid-type,misc] """Statistics meta data.""" __table_args__ = ( @@ -468,7 +468,7 @@ class StatisticsMeta(Base): # type: ignore return StatisticsMeta(**meta) -class RecorderRuns(Base): # type: ignore +class RecorderRuns(Base): # type: ignore[valid-type,misc] """Representation of recorder run.""" __table_args__ = (Index("ix_recorder_runs_start_end", "start", "end"),) @@ -518,7 +518,7 @@ class RecorderRuns(Base): # type: ignore return self -class SchemaChanges(Base): # type: ignore +class SchemaChanges(Base): # type: ignore[valid-type,misc] """Representation of schema version changes.""" __tablename__ = TABLE_SCHEMA_CHANGES @@ -536,7 +536,7 @@ class SchemaChanges(Base): # type: ignore ) -class StatisticsRuns(Base): # type: ignore +class StatisticsRuns(Base): # type: ignore[valid-type,misc] """Representation of statistics run.""" __tablename__ = TABLE_STATISTICS_RUNS @@ -612,7 +612,7 @@ class LazyState(State): self._last_updated = None self._context = None - @property # type: ignore + @property def attributes(self): """State attributes.""" if not self._attributes: @@ -629,7 +629,7 @@ class LazyState(State): """Set attributes.""" self._attributes = value - @property # type: ignore + @property def context(self): """State context.""" if not self._context: @@ -641,7 +641,7 @@ class LazyState(State): """Set context.""" self._context = value - @property # type: ignore + @property def last_changed(self): """Last changed datetime.""" if not self._last_changed: @@ -653,7 +653,7 @@ class LazyState(State): """Set last changed datetime.""" self._last_changed = value - @property # type: ignore + @property def last_updated(self): """Last updated datetime.""" if not self._last_updated: diff --git a/tests/components/ring/test_button.py b/tests/components/ring/test_button.py index 6f0c29b1fcc..6b2200b2bf3 100644 --- a/tests/components/ring/test_button.py +++ b/tests/components/ring/test_button.py @@ -39,4 +39,4 @@ async def test_button_opens_door( ) await hass.async_block_till_done() - assert mock.called_once + assert mock.call_count == 1 diff --git a/tests/components/smartthings/test_cover.py b/tests/components/smartthings/test_cover.py index c4f6c15a3fe..e19ac403e5d 100644 --- a/tests/components/smartthings/test_cover.py +++ b/tests/components/smartthings/test_cover.py @@ -140,7 +140,7 @@ async def test_set_cover_position_switch_level( assert state.attributes[ATTR_CURRENT_POSITION] == 10 # Ensure API called - assert device._api.post_device_command.call_count == 1 # type: ignore + assert device._api.post_device_command.call_count == 1 async def test_set_cover_position(hass: HomeAssistant, device_factory) -> None: @@ -171,7 +171,7 @@ async def test_set_cover_position(hass: HomeAssistant, device_factory) -> None: assert state.attributes[ATTR_CURRENT_POSITION] == 10 # Ensure API called - assert device._api.post_device_command.call_count == 1 # type: ignore + assert device._api.post_device_command.call_count == 1 async def test_set_cover_position_unsupported( @@ -196,7 +196,7 @@ async def test_set_cover_position_unsupported( # Ensure API was not called - assert device._api.post_device_command.call_count == 0 # type: ignore + assert device._api.post_device_command.call_count == 0 async def test_update_to_open_from_signal(hass: HomeAssistant, device_factory) -> None: diff --git a/tests/components/smartthings/test_scene.py b/tests/components/smartthings/test_scene.py index 1eaaad55d0f..d33db0a1dd9 100644 --- a/tests/components/smartthings/test_scene.py +++ b/tests/components/smartthings/test_scene.py @@ -38,7 +38,7 @@ async def test_scene_activate(hass: HomeAssistant, scene) -> None: assert state.attributes["icon"] == scene.icon assert state.attributes["color"] == scene.color assert state.attributes["location_id"] == scene.location_id - assert scene.execute.call_count == 1 # type: ignore + assert scene.execute.call_count == 1 async def test_unload_config_entry(hass: HomeAssistant, scene) -> None: diff --git a/tests/util/test_logging.py b/tests/util/test_logging.py index 53342e8d1bd..8e7106475a2 100644 --- a/tests/util/test_logging.py +++ b/tests/util/test_logging.py @@ -20,7 +20,7 @@ import homeassistant.util.logging as logging_util async def test_logging_with_queue_handler() -> None: """Test logging with HomeAssistantQueueHandler.""" - simple_queue = queue.SimpleQueue() # type: ignore + simple_queue = queue.SimpleQueue() handler = logging_util.HomeAssistantQueueHandler(simple_queue) log_record = logging.makeLogRecord({"msg": "Test Log Record"}) From 376aafc83e75662968ec88283558e08536d40ec1 Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Mon, 8 Apr 2024 15:43:58 +0200 Subject: [PATCH 412/967] Enable Ruff INP001 (#115082) --- homeassistant/components/knx/helpers/__init__.py | 1 + .../components/recorder/auto_repairs/states/__init__.py | 1 + pylint/ruff.toml | 5 +++++ pyproject.toml | 1 + script/scaffold/templates/ruff.toml | 6 ++++++ tests/testing_config/custom_components/ruff.toml | 6 ++++++ 6 files changed, 20 insertions(+) create mode 100644 homeassistant/components/knx/helpers/__init__.py create mode 100644 homeassistant/components/recorder/auto_repairs/states/__init__.py create mode 100644 script/scaffold/templates/ruff.toml create mode 100644 tests/testing_config/custom_components/ruff.toml diff --git a/homeassistant/components/knx/helpers/__init__.py b/homeassistant/components/knx/helpers/__init__.py new file mode 100644 index 00000000000..25d84406d03 --- /dev/null +++ b/homeassistant/components/knx/helpers/__init__.py @@ -0,0 +1 @@ +"""Helpers for KNX.""" diff --git a/homeassistant/components/recorder/auto_repairs/states/__init__.py b/homeassistant/components/recorder/auto_repairs/states/__init__.py new file mode 100644 index 00000000000..0429e0cab12 --- /dev/null +++ b/homeassistant/components/recorder/auto_repairs/states/__init__.py @@ -0,0 +1 @@ +"""States repairs for Recorder.""" diff --git a/pylint/ruff.toml b/pylint/ruff.toml index ebf53daa903..4e1a0388f31 100644 --- a/pylint/ruff.toml +++ b/pylint/ruff.toml @@ -1,6 +1,11 @@ # This extend our general Ruff rules specifically for tests extend = "../pyproject.toml" +[lint] +extend-ignore = [ + "INP001", # File is part of an implicit namespace package. Add an `__init__.py`. +] + [lint.isort] known-third-party = [ "pylint", diff --git a/pyproject.toml b/pyproject.toml index f6c7acb0a97..bcd0320d6f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -627,6 +627,7 @@ select = [ "F", # pyflakes/autoflake "G", # flake8-logging-format "I", # isort + "INP", # flake8-no-pep420 "ISC", # flake8-implicit-str-concat "ICN001", # import concentions; {name} should be imported as {asname} "LOG", # flake8-logging diff --git a/script/scaffold/templates/ruff.toml b/script/scaffold/templates/ruff.toml new file mode 100644 index 00000000000..00a6d7ef849 --- /dev/null +++ b/script/scaffold/templates/ruff.toml @@ -0,0 +1,6 @@ +extend = "../../ruff.toml" + +[lint] +extend-ignore = [ + "INP001", # File is part of an implicit namespace package. Add an `__init__.py`. +] diff --git a/tests/testing_config/custom_components/ruff.toml b/tests/testing_config/custom_components/ruff.toml new file mode 100644 index 00000000000..00a6d7ef849 --- /dev/null +++ b/tests/testing_config/custom_components/ruff.toml @@ -0,0 +1,6 @@ +extend = "../../ruff.toml" + +[lint] +extend-ignore = [ + "INP001", # File is part of an implicit namespace package. Add an `__init__.py`. +] From cbaef096faddb5c924dcdf234fe4beb7e22ba57c Mon Sep 17 00:00:00 2001 From: Illia <146177275+ikalnyi@users.noreply.github.com> Date: Mon, 8 Apr 2024 17:24:32 +0200 Subject: [PATCH 413/967] Add Arve integration (#113156) * add Arve integration * Update homeassistant/components/arve/config_flow.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Various fixes, changed scan interval to one minute * coordinator implementation * Code cleanup * Moved device info to the entity.py, ArveDeviceEntityDescription to sensor.py * delete refresh before adding entities Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Update tests/components/arve/test_config_flow.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Update tests/components/arve/conftest.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Changed value_fn in sensors.py, added typing to description * Code cleanups, platfrom test implementation * New code cleanups, first two working tests * Created platform test, generated snapshots * Reworked integration to get all of the customer devices * new fixes * Added customer id, small cleanups * Logic of setting unique_id to the config flow * Added test of abortion on duplicate config_flow id * Added "available" and "device" properties fro ArveDeviceEntity * small _attr_unique_id fix * Added new test, improved mocking, various fixes * Various cleanups and fixes * microfix * Update homeassistant/components/arve/strings.json * ruff fix --------- Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> Co-authored-by: Joost Lekkerkerker --- CODEOWNERS | 2 + homeassistant/components/arve/__init__.py | 34 + homeassistant/components/arve/config_flow.py | 53 ++ homeassistant/components/arve/const.py | 7 + homeassistant/components/arve/coordinator.py | 63 ++ homeassistant/components/arve/entity.py | 53 ++ homeassistant/components/arve/icons.json | 9 + homeassistant/components/arve/manifest.json | 9 + homeassistant/components/arve/sensor.py | 108 +++ homeassistant/components/arve/strings.json | 26 + homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/arve/__init__.py | 20 + tests/components/arve/conftest.py | 56 ++ .../arve/snapshots/test_sensor.ambr | 773 ++++++++++++++++++ tests/components/arve/test_config_flow.py | 79 ++ tests/components/arve/test_sensor.py | 43 + 19 files changed, 1348 insertions(+) create mode 100644 homeassistant/components/arve/__init__.py create mode 100644 homeassistant/components/arve/config_flow.py create mode 100644 homeassistant/components/arve/const.py create mode 100644 homeassistant/components/arve/coordinator.py create mode 100644 homeassistant/components/arve/entity.py create mode 100644 homeassistant/components/arve/icons.json create mode 100644 homeassistant/components/arve/manifest.json create mode 100644 homeassistant/components/arve/sensor.py create mode 100644 homeassistant/components/arve/strings.json create mode 100644 tests/components/arve/__init__.py create mode 100644 tests/components/arve/conftest.py create mode 100644 tests/components/arve/snapshots/test_sensor.ambr create mode 100644 tests/components/arve/test_config_flow.py create mode 100644 tests/components/arve/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 40d7c0f502a..a4e237e79f1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -130,6 +130,8 @@ build.json @home-assistant/supervisor /homeassistant/components/arcam_fmj/ @elupus /tests/components/arcam_fmj/ @elupus /homeassistant/components/arris_tg2492lg/ @vanbalken +/homeassistant/components/arve/ @ikalnyi +/tests/components/arve/ @ikalnyi /homeassistant/components/aseko_pool_live/ @milanmeu /tests/components/aseko_pool_live/ @milanmeu /homeassistant/components/assist_pipeline/ @balloob @synesthesiam diff --git a/homeassistant/components/arve/__init__.py b/homeassistant/components/arve/__init__.py new file mode 100644 index 00000000000..91e38da4c60 --- /dev/null +++ b/homeassistant/components/arve/__init__.py @@ -0,0 +1,34 @@ +"""The Arve integration.""" + +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .coordinator import ArveCoordinator + +PLATFORMS: list[Platform] = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Arve from a config entry.""" + + coordinator = ArveCoordinator(hass) + + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = 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.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/arve/config_flow.py b/homeassistant/components/arve/config_flow.py new file mode 100644 index 00000000000..23d344d2325 --- /dev/null +++ b/homeassistant/components/arve/config_flow.py @@ -0,0 +1,53 @@ +"""Config flow for Arve integration.""" + +from __future__ import annotations + +import logging +from typing import Any + +from asyncarve import Arve, ArveConnectionError, ArveCustomer +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CLIENT_SECRET + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class ArveConfigFlowHandler(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Arve.""" + + 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: + arve = Arve( + user_input[CONF_ACCESS_TOKEN], + user_input[CONF_CLIENT_SECRET], + ) + try: + customer: ArveCustomer = await arve.get_customer_id() + except ArveConnectionError: + errors["base"] = "cannot_connect" + else: + await self.async_set_unique_id(customer.customerId) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title="Arve", + data=user_input, + ) + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_ACCESS_TOKEN): str, + vol.Required(CONF_CLIENT_SECRET): str, + } + ), + errors=errors, + ) diff --git a/homeassistant/components/arve/const.py b/homeassistant/components/arve/const.py new file mode 100644 index 00000000000..1350640f887 --- /dev/null +++ b/homeassistant/components/arve/const.py @@ -0,0 +1,7 @@ +"""Constants for the Arve integration.""" + +import logging + +DOMAIN = "arve" + +LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/arve/coordinator.py b/homeassistant/components/arve/coordinator.py new file mode 100644 index 00000000000..b053e30336b --- /dev/null +++ b/homeassistant/components/arve/coordinator.py @@ -0,0 +1,63 @@ +"""Coordinator for the Arve integration.""" + +from __future__ import annotations + +from datetime import timedelta + +from asyncarve import ( + Arve, + ArveConnectionError, + ArveDeviceInfo, + ArveDevices, + ArveError, + ArveSensProData, +) + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CLIENT_SECRET +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN, LOGGER + + +class ArveCoordinator(DataUpdateCoordinator[ArveSensProData]): + """Arve coordinator.""" + + config_entry: ConfigEntry + devices: ArveDevices + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize Arve coordinator.""" + super().__init__( + hass, + LOGGER, + name=DOMAIN, + update_interval=timedelta(seconds=60), + ) + + self.arve = Arve( + self.config_entry.data[CONF_ACCESS_TOKEN], + self.config_entry.data[CONF_CLIENT_SECRET], + session=async_get_clientsession(hass), + ) + + async def _async_update_data(self) -> dict[str, ArveDeviceInfo]: + """Fetch data from API endpoint.""" + try: + self.devices = await self.arve.get_devices() + + response_data = { + sn: ArveDeviceInfo( + await self.arve.device_sensor_data(sn), + await self.arve.get_sensor_info(sn), + ) + for sn in self.devices.sn + } + except ArveConnectionError as err: + raise UpdateFailed("Unable to connect to the Arve device") from err + except ArveError as err: + raise UpdateFailed("Unknown error occurred") from err + + return response_data diff --git a/homeassistant/components/arve/entity.py b/homeassistant/components/arve/entity.py new file mode 100644 index 00000000000..46c6bfc75ec --- /dev/null +++ b/homeassistant/components/arve/entity.py @@ -0,0 +1,53 @@ +"""Arve base entity.""" + +from __future__ import annotations + +from asyncarve import ArveDeviceInfo + +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import ArveCoordinator + + +class ArveDeviceEntity(CoordinatorEntity[ArveCoordinator]): + """Defines a base Arve device entity.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: ArveCoordinator, + description: EntityDescription, + serial_number: str, + ) -> None: + """Initialize the Arve device entity.""" + super().__init__(coordinator) + + self.device_serial_number = serial_number + + self.entity_description = description + + self._attr_unique_id = f"{serial_number}_{description.key}" + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, serial_number)}, + manufacturer="Calanda Air AG", + model="Arve Sens Pro", + serial_number=serial_number, + name=self.device.info.name, + ) + + @property + def available(self) -> bool: + """Check if device is available.""" + return super()._attr_available and ( + self.device_serial_number in self.coordinator.data + ) + + @property + def device(self) -> ArveDeviceInfo: + """Returns device instance.""" + return self.coordinator.data[self.device_serial_number] diff --git a/homeassistant/components/arve/icons.json b/homeassistant/components/arve/icons.json new file mode 100644 index 00000000000..887a0694e5d --- /dev/null +++ b/homeassistant/components/arve/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "sensor": { + "tvoc": { + "default": "mdi:flask" + } + } + } +} diff --git a/homeassistant/components/arve/manifest.json b/homeassistant/components/arve/manifest.json new file mode 100644 index 00000000000..fa33b3309ce --- /dev/null +++ b/homeassistant/components/arve/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "arve", + "name": "Arve", + "codeowners": ["@ikalnyi"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/arve", + "iot_class": "cloud_polling", + "requirements": ["asyncarve==0.0.9"] +} diff --git a/homeassistant/components/arve/sensor.py b/homeassistant/components/arve/sensor.py new file mode 100644 index 00000000000..f95b26b0451 --- /dev/null +++ b/homeassistant/components/arve/sensor.py @@ -0,0 +1,108 @@ +"""Sensor platform for Arve devices.""" + +from collections.abc import Callable +from dataclasses import dataclass + +from asyncarve import ArveSensProData + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_MILLION, + PERCENTAGE, + UnitOfTemperature, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import ArveCoordinator +from .entity import ArveDeviceEntity + + +@dataclass(frozen=True, kw_only=True) +class ArveDeviceEntityDescription(SensorEntityDescription): + """Describes Arve device entity.""" + + value_fn: Callable[[ArveSensProData], float | int] + + +SENSORS: tuple[ArveDeviceEntityDescription, ...] = ( + ArveDeviceEntityDescription( + key="CO2", + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + device_class=SensorDeviceClass.CO2, + value_fn=lambda arve_data: arve_data.co2, + state_class=SensorStateClass.MEASUREMENT, + ), + ArveDeviceEntityDescription( + key="AQI", + device_class=SensorDeviceClass.AQI, + value_fn=lambda arve_data: arve_data.aqi, + state_class=SensorStateClass.MEASUREMENT, + ), + ArveDeviceEntityDescription( + key="Humidity", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.HUMIDITY, + value_fn=lambda arve_data: arve_data.humidity, + state_class=SensorStateClass.MEASUREMENT, + ), + ArveDeviceEntityDescription( + key="PM10", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.PM10, + value_fn=lambda arve_data: arve_data.pm10, + state_class=SensorStateClass.MEASUREMENT, + ), + ArveDeviceEntityDescription( + key="PM25", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.PM25, + value_fn=lambda arve_data: arve_data.pm25, + state_class=SensorStateClass.MEASUREMENT, + ), + ArveDeviceEntityDescription( + key="Temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + value_fn=lambda arve_data: arve_data.temperature, + state_class=SensorStateClass.MEASUREMENT, + ), + ArveDeviceEntityDescription( + key="TVOC", + translation_key="tvoc", + value_fn=lambda arve_data: arve_data.tvoc, + state_class=SensorStateClass.MEASUREMENT, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Arve device based on a config entry.""" + coordinator: ArveCoordinator = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + ArveDevice(coordinator, description, sn) + for description in SENSORS + for sn in coordinator.devices.sn + ) + + +class ArveDevice(ArveDeviceEntity, SensorEntity): + """Define an Arve device.""" + + entity_description: ArveDeviceEntityDescription + + @property + def native_value(self) -> int | float: + """State of the sensor.""" + return self.entity_description.value_fn(self.device.sensors) diff --git a/homeassistant/components/arve/strings.json b/homeassistant/components/arve/strings.json new file mode 100644 index 00000000000..cbfe3c6b065 --- /dev/null +++ b/homeassistant/components/arve/strings.json @@ -0,0 +1,26 @@ +{ + "config": { + "step": { + "user": { + "description": "Set up your Arve device", + "data": { + "access_token": "Arve token", + "client_secret": "Arve customer token" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "entity": { + "sensor": { + "tvoc": { + "name": "Total volatile organic compounds" + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index acac5f8df5d..125f02df3b5 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -55,6 +55,7 @@ FLOWS = { "aprilaire", "aranet", "arcam_fmj", + "arve", "aseko_pool_live", "asuswrt", "atag", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index e6c0588979c..f027db93fe0 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -455,6 +455,12 @@ } } }, + "arve": { + "name": "Arve", + "integration_type": "hub", + "config_flow": true, + "iot_class": "cloud_polling" + }, "arwn": { "name": "Ambient Radio Weather Network", "integration_type": "hub", diff --git a/requirements_all.txt b/requirements_all.txt index 7e2ddc78c5b..c0984c8b758 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -489,6 +489,9 @@ asterisk_mbox==0.5.0 # homeassistant.components.yeelight async-upnp-client==0.38.3 +# homeassistant.components.arve +asyncarve==0.0.9 + # homeassistant.components.keyboard_remote asyncinotify==4.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3eb48da244e..526466e04e2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -444,6 +444,9 @@ asterisk_mbox==0.5.0 # homeassistant.components.yeelight async-upnp-client==0.38.3 +# homeassistant.components.arve +asyncarve==0.0.9 + # homeassistant.components.sleepiq asyncsleepiq==1.5.2 diff --git a/tests/components/arve/__init__.py b/tests/components/arve/__init__.py new file mode 100644 index 00000000000..24f970b55b6 --- /dev/null +++ b/tests/components/arve/__init__.py @@ -0,0 +1,20 @@ +"""Tests for the Arve integration.""" + +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CLIENT_SECRET +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +USER_INPUT = { + CONF_ACCESS_TOKEN: "test-access-token", + CONF_CLIENT_SECRET: "test-customer-token", +} + + +async def async_init_integration( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Set up the Arve 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() diff --git a/tests/components/arve/conftest.py b/tests/components/arve/conftest.py new file mode 100644 index 00000000000..f1dfee8ba41 --- /dev/null +++ b/tests/components/arve/conftest.py @@ -0,0 +1,56 @@ +"""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 homeassistant.components.arve.const import DOMAIN +from homeassistant.core import HomeAssistant + +from . import USER_INPUT + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.arve.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +def mock_config_entry(hass: HomeAssistant, mock_arve: MagicMock) -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title="Arve", domain=DOMAIN, data=USER_INPUT, unique_id=mock_arve.customer_id + ) + + +@pytest.fixture +def mock_arve(): + """Return a mocked Arve client.""" + + with ( + patch( + "homeassistant.components.arve.coordinator.Arve", autospec=True + ) as arve_mock, + patch("homeassistant.components.arve.config_flow.Arve", new=arve_mock), + ): + arve = arve_mock.return_value + arve.customer_id = 12345 + + arve.get_customer_id.return_value = ArveCustomer(12345) + + arve.get_devices.return_value = ArveDevices(["test-serial-number"]) + arve.get_sensor_info.return_value = ArveSensPro("Test Sensor", "1.0", "prov1") + + arve.device_sensor_data.return_value = ArveSensProData( + 14, 595.75, 28.71, 0.16, 0.19, 26.02, 7 + ) + + yield arve diff --git a/tests/components/arve/snapshots/test_sensor.ambr b/tests/components/arve/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..5c5c4c84d08 --- /dev/null +++ b/tests/components/arve/snapshots/test_sensor.ambr @@ -0,0 +1,773 @@ +# serializer version: 1 +# name: test_sensors[entry_air_quality_index] + 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_sensor_air_quality_index', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Air quality index', + 'platform': 'arve', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'test-serial-number_AQI', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[entry_carbon_dioxide] + 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_sensor_carbon_dioxide', + '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', + 'platform': 'arve', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'test-serial-number_CO2', + 'unit_of_measurement': 'ppm', + }) +# --- +# name: test_sensors[entry_humidity] + 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_sensor_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': 'arve', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'test-serial-number_Humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[entry_pm10] + 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_sensor_pm10', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'PM10', + 'platform': 'arve', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'test-serial-number_PM10', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensors[entry_pm2_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.test_sensor_pm2_5', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'PM2.5', + 'platform': 'arve', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'test-serial-number_PM25', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensors[entry_temperature] + 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_sensor_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': 'arve', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'test-serial-number_Temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[entry_test-serial-number_air_quality_index] + 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_sensor_air_quality_index', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Air quality index', + 'platform': 'arve', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'test-serial-number_AQI', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[entry_test-serial-number_carbon_dioxide] + 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_sensor_carbon_dioxide', + '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', + 'platform': 'arve', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'test-serial-number_CO2', + 'unit_of_measurement': 'ppm', + }) +# --- +# name: test_sensors[entry_test-serial-number_humidity] + 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_sensor_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': 'arve', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'test-serial-number_Humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[entry_test-serial-number_none] + 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.my_arve_none', + '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': 'TVOC', + 'platform': 'arve', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tvoc', + 'unique_id': 'test-serial-number_tvoc', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[entry_test-serial-number_pm10] + 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_sensor_pm10', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'PM10', + 'platform': 'arve', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'test-serial-number_PM10', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensors[entry_test-serial-number_pm2_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.test_sensor_pm2_5', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'PM2.5', + 'platform': 'arve', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'test-serial-number_PM25', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensors[entry_test-serial-number_temperature] + 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_sensor_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': 'arve', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'test-serial-number_Temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[entry_test-serial-number_total_volatile_organic_compounds] + 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_sensor_total_volatile_organic_compounds', + '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 volatile organic compounds', + 'platform': 'arve', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tvoc', + 'unique_id': 'test-serial-number_TVOC', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[entry_test-serial-number_tvoc] + 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.my_arve_tvoc', + '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': 'TVOC', + 'platform': 'arve', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tvoc', + 'unique_id': 'test-serial-number_tvoc', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[entry_total_volatile_organic_compounds] + 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_sensor_total_volatile_organic_compounds', + '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 volatile organic compounds', + 'platform': 'arve', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tvoc', + 'unique_id': 'test-serial-number_TVOC', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[my_arve_air_quality_index] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'aqi', + 'friendly_name': 'My Arve AQI', + }), + 'context': , + 'entity_id': 'sensor.my_arve_air_quality_index', + 'last_changed': , + 'last_updated': , + 'state': "", + }) +# --- +# name: test_sensors[my_arve_carbon_dioxide] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'carbon_dioxide', + 'friendly_name': 'My Arve CO2', + 'unit_of_measurement': 'ppm', + }), + 'context': , + 'entity_id': 'sensor.my_arve_carbon_dioxide', + 'last_changed': , + 'last_updated': , + 'state': "", + }) +# --- +# name: test_sensors[my_arve_humidity] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'My Arve Humidity', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.my_arve_humidity', + 'last_changed': , + 'last_updated': , + 'state': "", + }) +# --- +# name: test_sensors[my_arve_none] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'My Arve TVOC', + }), + 'context': , + 'entity_id': 'sensor.my_arve_none', + 'last_changed': , + 'last_updated': , + 'state': "", + }) +# --- +# name: test_sensors[my_arve_pm10] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pm10', + 'friendly_name': 'My Arve PM10', + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.my_arve_pm10', + 'last_changed': , + 'last_updated': , + 'state': "", + }) +# --- +# name: test_sensors[my_arve_pm2_5] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pm25', + 'friendly_name': 'My Arve PM25', + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.my_arve_pm2_5', + 'last_changed': , + 'last_updated': , + 'state': "", + }) +# --- +# name: test_sensors[my_arve_temperature] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'My Arve Temperature', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.my_arve_temperature', + 'last_changed': , + 'last_updated': , + 'state': "", + }) +# --- +# name: test_sensors[my_arve_tvoc] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'My Arve TVOC', + }), + 'context': , + 'entity_id': 'sensor.my_arve_tvoc', + 'last_changed': , + 'last_updated': , + 'state': "", + }) +# --- +# name: test_sensors[test_sensor_air_quality_index] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'aqi', + 'friendly_name': 'Test Sensor Air quality index', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.test_sensor_air_quality_index', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '14', + }) +# --- +# name: test_sensors[test_sensor_carbon_dioxide] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'carbon_dioxide', + 'friendly_name': 'Test Sensor Carbon dioxide', + 'state_class': , + 'unit_of_measurement': 'ppm', + }), + 'context': , + 'entity_id': 'sensor.test_sensor_carbon_dioxide', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '595.75', + }) +# --- +# name: test_sensors[test_sensor_humidity] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'Test Sensor Humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.test_sensor_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '28.71', + }) +# --- +# name: test_sensors[test_sensor_pm10] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pm10', + 'friendly_name': 'Test Sensor PM10', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.test_sensor_pm10', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.16', + }) +# --- +# name: test_sensors[test_sensor_pm2_5] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pm25', + 'friendly_name': 'Test Sensor PM2.5', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.test_sensor_pm2_5', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.19', + }) +# --- +# name: test_sensors[test_sensor_temperature] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Test Sensor Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_sensor_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '26.02', + }) +# --- +# name: test_sensors[test_sensor_total_volatile_organic_compounds] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Sensor Total volatile organic compounds', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.test_sensor_total_volatile_organic_compounds', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7', + }) +# --- diff --git a/tests/components/arve/test_config_flow.py b/tests/components/arve/test_config_flow.py new file mode 100644 index 00000000000..efa36e37d44 --- /dev/null +++ b/tests/components/arve/test_config_flow.py @@ -0,0 +1,79 @@ +"""Test the Arve config flow.""" + +from unittest.mock import AsyncMock + +from homeassistant.components.arve.config_flow import ArveConnectionError +from homeassistant.components.arve.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CLIENT_SECRET +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from . import USER_INPUT, async_init_integration + +from tests.common import MockConfigEntry + + +async def test_correct_flow( + hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_arve: AsyncMock +) -> None: + """Test the whole 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" + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], USER_INPUT + ) + await hass.async_block_till_done() + + assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["data"] == USER_INPUT + assert len(mock_setup_entry.mock_calls) == 1 + assert result2["result"].unique_id == 12345 + + +async def test_form_cannot_connect( + hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_arve: AsyncMock +) -> None: + """Test we handle cannot connect error.""" + mock_arve.get_customer_id.side_effect = ArveConnectionError + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT, + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_form_abort_already_configured( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test form aborts if already configured.""" + await async_init_integration(hass, mock_config_entry) + + 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"], + { + CONF_ACCESS_TOKEN: "test-access-token", + CONF_CLIENT_SECRET: "test-customer-token", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] is FlowResultType.ABORT + assert result2["reason"] == "already_configured" diff --git a/tests/components/arve/test_sensor.py b/tests/components/arve/test_sensor.py new file mode 100644 index 00000000000..541820fd7b6 --- /dev/null +++ b/tests/components/arve/test_sensor.py @@ -0,0 +1,43 @@ +"""Test for Arve sensors.""" + +from unittest.mock import MagicMock + +from syrupy import SnapshotAssertion + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import async_init_integration + +from tests.common import MockConfigEntry + +SENSORS = ( + "air_quality_index", + "carbon_dioxide", + "humidity", + "pm10", + "pm2_5", + "temperature", + "total_volatile_organic_compounds", +) + + +async def test_sensors( + hass: HomeAssistant, + mock_arve: MagicMock, + entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test the Arve sensors.""" + await async_init_integration(hass, mock_config_entry) + + for sensor in SENSORS: + state = hass.states.get(f"sensor.test_sensor_{sensor}") + assert state + assert state == snapshot(name=f"test_sensor_{sensor}") + + entry = entity_registry.async_get(state.entity_id) + assert entry + assert entry.device_id + assert entry == snapshot(name=f"entry_{sensor}") From f9a7e6bb9fdb7242686f1a9d23c17004c4be3cfa Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 8 Apr 2024 11:29:55 -0400 Subject: [PATCH 414/967] Add migration logic to assist_pipeline (#115172) --- .../components/assist_pipeline/__init__.py | 4 ++ .../components/assist_pipeline/const.py | 1 + .../components/assist_pipeline/pipeline.py | 51 +++++++++++++++++-- .../components/conversation/__init__.py | 24 ++++++++- .../components/conversation/manifest.json | 1 - homeassistant/const.py | 1 + script/hassfest/dependencies.py | 2 + .../assist_pipeline/test_pipeline.py | 45 ++++++++++++++++ 8 files changed, 122 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/assist_pipeline/__init__.py b/homeassistant/components/assist_pipeline/__init__.py index f15657d5a91..f481411e551 100644 --- a/homeassistant/components/assist_pipeline/__init__.py +++ b/homeassistant/components/assist_pipeline/__init__.py @@ -31,6 +31,8 @@ from .pipeline import ( async_create_default_pipeline, async_get_pipeline, async_get_pipelines, + async_migrate_engine, + async_run_migrations, async_setup_pipeline_store, async_update_pipeline, ) @@ -40,6 +42,7 @@ __all__ = ( "DOMAIN", "async_create_default_pipeline", "async_get_pipelines", + "async_migrate_engine", "async_setup", "async_pipeline_from_audio_stream", "async_update_pipeline", @@ -72,6 +75,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.data[DATA_LAST_WAKE_UP] = {} await async_setup_pipeline_store(hass) + await async_run_migrations(hass) async_register_websocket_api(hass) return True diff --git a/homeassistant/components/assist_pipeline/const.py b/homeassistant/components/assist_pipeline/const.py index 3463d94fb84..36b72dad69c 100644 --- a/homeassistant/components/assist_pipeline/const.py +++ b/homeassistant/components/assist_pipeline/const.py @@ -3,6 +3,7 @@ DOMAIN = "assist_pipeline" DATA_CONFIG = f"{DOMAIN}.config" +DATA_MIGRATIONS = f"{DOMAIN}_migrations" DEFAULT_PIPELINE_TIMEOUT = 60 * 5 # seconds diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py index 0d25950d65b..2251167466c 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -13,7 +13,7 @@ from pathlib import Path from queue import Empty, Queue from threading import Thread import time -from typing import TYPE_CHECKING, Any, Final, cast +from typing import TYPE_CHECKING, Any, Final, Literal, cast import wave import voluptuous as vol @@ -56,6 +56,7 @@ from .const import ( CONF_DEBUG_RECORDING_DIR, DATA_CONFIG, DATA_LAST_WAKE_UP, + DATA_MIGRATIONS, DOMAIN, WAKE_WORD_COOLDOWN, ) @@ -376,10 +377,6 @@ class Pipeline: This function was added in HA Core 2023.10, previous versions will raise if there are unexpected items in the serialized data. """ - # Migrate to new value for conversation agent - if data["conversation_engine"] == conversation.OLD_HOME_ASSISTANT_AGENT: - data["conversation_engine"] = conversation.HOME_ASSISTANT_AGENT - return cls( conversation_engine=data["conversation_engine"], conversation_language=data["conversation_language"], @@ -1818,3 +1815,47 @@ async def async_setup_pipeline_store(hass: HomeAssistant) -> PipelineData: PIPELINE_FIELDS, ).async_setup(hass) return PipelineData(pipeline_store) + + +@callback +def async_migrate_engine( + hass: HomeAssistant, + engine_type: Literal["conversation", "stt", "tts", "wake_word"], + old_value: str, + new_value: str, +) -> None: + """Register a migration of an engine used in pipelines.""" + hass.data.setdefault(DATA_MIGRATIONS, {})[engine_type] = (old_value, new_value) + + # Run migrations when config is already loaded + if DATA_CONFIG in hass.data: + hass.async_create_background_task( + async_run_migrations(hass), "assist_pipeline_migration", eager_start=True + ) + + +async def async_run_migrations(hass: HomeAssistant) -> None: + """Run pipeline migrations.""" + if not (migrations := hass.data.get(DATA_MIGRATIONS)): + return + + engine_attr = { + "conversation": "conversation_engine", + "stt": "stt_engine", + "tts": "tts_engine", + "wake_word": "wake_word_entity", + } + + updates = [] + + for pipeline in async_get_pipelines(hass): + attr_updates = {} + for engine_type, (old_value, new_value) in migrations.items(): + if getattr(pipeline, engine_attr[engine_type]) == old_value: + attr_updates[engine_attr[engine_type]] = new_value + + if attr_updates: + updates.append((pipeline, attr_updates)) + + for pipeline, attr_updates in updates: + await async_update_pipeline(hass, pipeline, **attr_updates) diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index 63e0e9bff59..333fb24498b 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -43,8 +43,9 @@ __all__ = [ "async_converse", "async_get_agent_info", "async_set_agent", - "async_unset_agent", "async_setup", + "async_unset_agent", + "ConversationEntity", "ConversationInput", "ConversationResult", ] @@ -188,6 +189,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass, entity_component, config.get(DOMAIN, {}).get("intents", {}) ) + # Temporary migration. We can remove this in 2024.10 + from homeassistant.components.assist_pipeline import ( # pylint: disable=import-outside-toplevel + async_migrate_engine, + ) + + async_migrate_engine( + hass, "conversation", OLD_HOME_ASSISTANT_AGENT, HOME_ASSISTANT_AGENT + ) + async def handle_process(service: ServiceCall) -> ServiceResponse: """Parse text into commands.""" text = service.data[ATTR_TEXT] @@ -227,3 +237,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async_setup_conversation_http(hass) return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up a config entry.""" + component: EntityComponent[ConversationEntity] = hass.data[DOMAIN] + return await component.async_setup_entry(entry) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + component: EntityComponent[ConversationEntity] = hass.data[DOMAIN] + return await component.async_unload_entry(entry) diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 76c5b5ad666..8ee27986bb8 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -5,7 +5,6 @@ "dependencies": ["http", "intent"], "documentation": "https://www.home-assistant.io/integrations/conversation", "integration_type": "system", - "iot_class": "local_push", "quality_scale": "internal", "requirements": ["hassil==1.6.1", "home-assistant-intents==2024.4.3"] } diff --git a/homeassistant/const.py b/homeassistant/const.py index 0eed33c48d7..a9dbfef5eab 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -44,6 +44,7 @@ class Platform(StrEnum): CALENDAR = "calendar" CAMERA = "camera" CLIMATE = "climate" + CONVERSATION = "conversation" COVER = "cover" DATE = "date" DATETIME = "datetime" diff --git a/script/hassfest/dependencies.py b/script/hassfest/dependencies.py index d9ec114e5bb..6fe7700cb3f 100644 --- a/script/hassfest/dependencies.py +++ b/script/hassfest/dependencies.py @@ -156,6 +156,8 @@ IGNORE_VIOLATIONS = { ("websocket_api", "lovelace"), ("websocket_api", "shopping_list"), "logbook", + # Temporary needed for migration until 2024.10 + ("conversation", "assist_pipeline"), } diff --git a/tests/components/assist_pipeline/test_pipeline.py b/tests/components/assist_pipeline/test_pipeline.py index 3588bba6416..cf3afff0172 100644 --- a/tests/components/assist_pipeline/test_pipeline.py +++ b/tests/components/assist_pipeline/test_pipeline.py @@ -19,6 +19,7 @@ from homeassistant.components.assist_pipeline.pipeline import ( async_create_default_pipeline, async_get_pipeline, async_get_pipelines, + async_migrate_engine, async_update_pipeline, ) from homeassistant.core import HomeAssistant @@ -118,6 +119,12 @@ async def test_loading_pipelines_from_storage( hass: HomeAssistant, hass_storage: dict[str, Any] ) -> None: """Test loading stored pipelines on start.""" + async_migrate_engine( + hass, + "conversation", + conversation.OLD_HOME_ASSISTANT_AGENT, + conversation.HOME_ASSISTANT_AGENT, + ) id_1 = "01GX8ZWBAQYWNB1XV3EXEZ75DY" hass_storage[STORAGE_KEY] = { "version": STORAGE_VERSION, @@ -614,3 +621,41 @@ async def test_update_pipeline( "wake_word_entity": "wake_work.test_1", "wake_word_id": "wake_word_id_1", } + + +async def test_migrate_after_load( + hass: HomeAssistant, init_supporting_components +) -> None: + """Test migrating an engine after done loading.""" + assert await async_setup_component(hass, "assist_pipeline", {}) + + pipeline_data: PipelineData = hass.data[DOMAIN] + store = pipeline_data.pipeline_store + assert len(store.data) == 1 + + assert ( + await async_create_default_pipeline( + hass, + stt_engine_id="bla", + tts_engine_id="bla", + pipeline_name="Bla pipeline", + ) + is None + ) + pipeline = await async_create_default_pipeline( + hass, + stt_engine_id="test", + tts_engine_id="test", + pipeline_name="Test pipeline", + ) + assert pipeline is not None + + async_migrate_engine(hass, "stt", "test", "stt.test") + async_migrate_engine(hass, "tts", "test", "tts.test") + + await hass.async_block_till_done(wait_background_tasks=True) + + pipeline_updated = async_get_pipeline(hass, pipeline.id) + + assert pipeline_updated.stt_engine == "stt.test" + assert pipeline_updated.tts_engine == "tts.test" From f23e48f373402e68baf479d06095150ac3434c77 Mon Sep 17 00:00:00 2001 From: Kim de Vos Date: Mon, 8 Apr 2024 19:29:54 +0200 Subject: [PATCH 415/967] Add sensor for CPU and memory utilization for unifi device (#114986) Add system stats to unifi device sensors --- homeassistant/components/unifi/sensor.py | 41 +++++++++++++++++++- tests/components/unifi/test_sensor.py | 48 +++++++++++++++++++++++- 2 files changed, 86 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index 54ecc2ea763..360f40384c9 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -10,6 +10,7 @@ from collections.abc import Callable from dataclasses import dataclass from datetime import date, datetime, timedelta from decimal import Decimal +from functools import partial from aiounifi.interfaces.api_handlers import ItemEvent from aiounifi.interfaces.clients import Clients @@ -32,7 +33,7 @@ from homeassistant.components.sensor import ( UnitOfTemperature, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EntityCategory, UnitOfDataRate, UnitOfPower +from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfDataRate, UnitOfPower from homeassistant.core import Event as core_Event, HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -140,6 +141,16 @@ def async_device_outlet_supported_fn(hub: UnifiHub, obj_id: str) -> bool: return hub.api.devices[obj_id].outlet_ac_power_budget is not None +def device_system_stats_supported_fn( + stat_index: int, hub: UnifiHub, obj_id: str +) -> bool: + """Determine if a device supports reading item at index in system stats.""" + return ( + "system-stats" in hub.api.devices[obj_id].raw + and hub.api.devices[obj_id].system_stats[stat_index] != "" + ) + + @callback def async_client_is_connected_fn(hub: UnifiHub, obj_id: str) -> bool: """Check if client was last seen recently.""" @@ -352,6 +363,34 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = ( unique_id_fn=lambda hub, obj_id: f"password-{obj_id}", value_fn=lambda hub, obj: obj.x_passphrase, ), + UnifiSensorEntityDescription[Devices, Device]( + key="Device CPU utilization", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + 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: "CPU utilization", + object_fn=lambda api, obj_id: api.devices[obj_id], + supported_fn=partial(device_system_stats_supported_fn, 0), + unique_id_fn=lambda hub, obj_id: f"cpu_utilization-{obj_id}", + value_fn=lambda hub, device: device.system_stats[0], + ), + UnifiSensorEntityDescription[Devices, Device]( + key="Device memory utilization", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + 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: "Memory utilization", + object_fn=lambda api, obj_id: api.devices[obj_id], + supported_fn=partial(device_system_stats_supported_fn, 1), + unique_id_fn=lambda hub, obj_id: f"memory_utilization-{obj_id}", + value_fn=lambda hub, device: device.system_stats[1], + ), ) diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index 239707aa4c9..e8f9f763409 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -835,8 +835,8 @@ async def test_outlet_power_readings( """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()) == 11 - assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 5 + assert len(hass.states.async_all()) == 13 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 7 ent_reg_entry = entity_registry.async_get(f"sensor.{entity_id}") assert ent_reg_entry.unique_id == expected_unique_id @@ -1069,3 +1069,47 @@ async def test_wlan_password( mock_unifi_websocket(message=MessageKey.WLAN_CONF_UPDATED, data=wlan_1) await hass.async_block_till_done() assert hass.states.get(sensor_password).state == password + + +async def test_device_system_stats( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + aioclient_mock: AiohttpClientMocker, + mock_unifi_websocket, +) -> 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 + + assert hass.states.get("sensor.device_cpu_utilization").state == "5.8" + assert hass.states.get("sensor.device_memory_utilization").state == "31.1" + + assert ( + entity_registry.async_get("sensor.device_cpu_utilization").entity_category + is EntityCategory.DIAGNOSTIC + ) + + assert ( + entity_registry.async_get("sensor.device_memory_utilization").entity_category + is EntityCategory.DIAGNOSTIC + ) + + # Verify new event change system-stats + device["system-stats"] = {"cpu": 7.7, "mem": 33.3, "uptime": 7316} + mock_unifi_websocket(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" From fc1ebdaaa37daeb0559315cf9dbac7d73d29f25a Mon Sep 17 00:00:00 2001 From: Luca Angemi Date: Mon, 8 Apr 2024 19:34:50 +0200 Subject: [PATCH 416/967] Add config message items selector to imap option flow (#115108) * Update const.py * Update config_flow.py * Update coordinator.py * Update coordinator.py * Update strings.json * Update config_flow.py * Update const.py * Update coordinator.py * Update config_flow.py * Update config_flow.py * Update test_diagnostics.py * Update const.py * Update test_init.py * Update test_diagnostics.py * Update test_diagnostics.py * Update test_diagnostics.py * Update test_init.py * Update test_diagnostics.py * Update test_init.py * Update test_diagnostics.py * Update test_diagnostics.py * Update test_diagnostics.py * Update test_config_flow.py * Update config_flow.py * Update test_config_flow.py * Update test_init.py * Update const.py * Only make text and headers optional * Add message data tests * Add message data test * Update test_config_flow.py * Update test message data * Fix ruff * Fix ruff * Update test_init.py * Update strings.json --------- Co-authored-by: jbouwh Co-authored-by: Jan Bouwhuis --- homeassistant/components/imap/config_flow.py | 15 +++++++++ homeassistant/components/imap/const.py | 7 ++-- homeassistant/components/imap/coordinator.py | 18 ++++++---- homeassistant/components/imap/strings.json | 9 ++++- tests/components/imap/test_config_flow.py | 34 +++++++++++++++++++ tests/components/imap/test_diagnostics.py | 4 +++ tests/components/imap/test_init.py | 35 ++++++++++++++++++++ 7 files changed, 112 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/imap/config_flow.py b/homeassistant/components/imap/config_flow.py index 414d5830bae..62ed4d42a07 100644 --- a/homeassistant/components/imap/config_flow.py +++ b/homeassistant/components/imap/config_flow.py @@ -33,6 +33,7 @@ from .const import ( CONF_CHARSET, CONF_CUSTOM_EVENT_DATA_TEMPLATE, CONF_ENABLE_PUSH, + CONF_EVENT_MESSAGE_DATA, CONF_FOLDER, CONF_MAX_MESSAGE_SIZE, CONF_SEARCH, @@ -42,6 +43,7 @@ from .const import ( DEFAULT_PORT, DOMAIN, MAX_MESSAGE_SIZE_LIMIT, + MESSAGE_DATA_OPTIONS, ) from .coordinator import connect_to_server from .errors import InvalidAuth, InvalidFolder @@ -55,6 +57,13 @@ CIPHER_SELECTOR = SelectSelector( ) ) TEMPLATE_SELECTOR = TemplateSelector(TemplateSelectorConfig()) +EVENT_MESSAGE_DATA_SELECTOR = SelectSelector( + SelectSelectorConfig( + options=MESSAGE_DATA_OPTIONS, + translation_key=CONF_EVENT_MESSAGE_DATA, + multiple=True, + ) +) CONFIG_SCHEMA = vol.Schema( { @@ -65,6 +74,8 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional(CONF_CHARSET, default="utf-8"): str, vol.Optional(CONF_FOLDER, default="INBOX"): str, vol.Optional(CONF_SEARCH, default="UnSeen UnDeleted"): str, + # The default for new entries is to not include text and headers + vol.Optional(CONF_EVENT_MESSAGE_DATA, default=[]): cv.ensure_list, } ) CONFIG_SCHEMA_ADVANCED = { @@ -78,6 +89,10 @@ OPTIONS_SCHEMA = vol.Schema( { vol.Optional(CONF_FOLDER, default="INBOX"): str, vol.Optional(CONF_SEARCH, default="UnSeen UnDeleted"): str, + # The default for older entries is to include text and headers + vol.Optional( + CONF_EVENT_MESSAGE_DATA, default=MESSAGE_DATA_OPTIONS + ): EVENT_MESSAGE_DATA_SELECTOR, } ) diff --git a/homeassistant/components/imap/const.py b/homeassistant/components/imap/const.py index fd3da28971e..a341a2a55e7 100644 --- a/homeassistant/components/imap/const.py +++ b/homeassistant/components/imap/const.py @@ -8,7 +8,8 @@ CONF_SERVER: Final = "server" CONF_FOLDER: Final = "folder" CONF_SEARCH: Final = "search" CONF_CHARSET: Final = "charset" -CONF_MAX_MESSAGE_SIZE = "max_message_size" +CONF_EVENT_MESSAGE_DATA: Final = "event_message_data" +CONF_MAX_MESSAGE_SIZE: Final = "max_message_size" CONF_CUSTOM_EVENT_DATA_TEMPLATE: Final = "custom_event_data_template" CONF_SSL_CIPHER_LIST: Final = "ssl_cipher_list" CONF_ENABLE_PUSH: Final = "enable_push" @@ -17,4 +18,6 @@ DEFAULT_PORT: Final = 993 DEFAULT_MAX_MESSAGE_SIZE = 2048 -MAX_MESSAGE_SIZE_LIMIT = 30000 +MESSAGE_DATA_OPTIONS: Final = ["text", "headers"] + +MAX_MESSAGE_SIZE_LIMIT: Final = 30000 diff --git a/homeassistant/components/imap/coordinator.py b/homeassistant/components/imap/coordinator.py index 59ceb2b3b3d..94699ae5dd4 100644 --- a/homeassistant/components/imap/coordinator.py +++ b/homeassistant/components/imap/coordinator.py @@ -41,6 +41,7 @@ from homeassistant.util.ssl import ( from .const import ( CONF_CHARSET, CONF_CUSTOM_EVENT_DATA_TEMPLATE, + CONF_EVENT_MESSAGE_DATA, CONF_FOLDER, CONF_MAX_MESSAGE_SIZE, CONF_SEARCH, @@ -48,6 +49,7 @@ from .const import ( CONF_SSL_CIPHER_LIST, DEFAULT_MAX_MESSAGE_SIZE, DOMAIN, + MESSAGE_DATA_OPTIONS, ) from .errors import InvalidAuth, InvalidFolder @@ -225,6 +227,12 @@ class ImapDataUpdateCoordinator(DataUpdateCoordinator[int | None]): self._last_message_id: str | None = None self.custom_event_template = None self._diagnostics_data: dict[str, Any] = {} + self._event_data_keys: list[str] = entry.data.get( + CONF_EVENT_MESSAGE_DATA, MESSAGE_DATA_OPTIONS + ) + self._max_event_size: int = entry.data.get( + CONF_MAX_MESSAGE_SIZE, DEFAULT_MAX_MESSAGE_SIZE + ) _custom_event_template = entry.data.get(CONF_CUSTOM_EVENT_DATA_TEMPLATE) if _custom_event_template is not None: self.custom_event_template = Template(_custom_event_template, hass=hass) @@ -261,12 +269,11 @@ class ImapDataUpdateCoordinator(DataUpdateCoordinator[int | None]): "folder": self.config_entry.data[CONF_FOLDER], "initial": initial, "date": message.date, - "text": message.text, "sender": message.sender, "subject": message.subject, - "headers": message.headers, "uid": last_message_uid, } + data.update({key: getattr(message, key) for key in self._event_data_keys}) if self.custom_event_template is not None: try: data["custom"] = self.custom_event_template.async_render( @@ -289,11 +296,8 @@ class ImapDataUpdateCoordinator(DataUpdateCoordinator[int | None]): last_message_uid, err, ) - data["text"] = message.text[ - : self.config_entry.data.get( - CONF_MAX_MESSAGE_SIZE, DEFAULT_MAX_MESSAGE_SIZE - ) - ] + if "text" in data: + data["text"] = message.text[: self._max_event_size] self._update_diagnostics(data) if (size := len(json_bytes(data))) > MAX_EVENT_DATA_BYTES: _LOGGER.warning( diff --git a/homeassistant/components/imap/strings.json b/homeassistant/components/imap/strings.json index a8413922036..115d46f3d0e 100644 --- a/homeassistant/components/imap/strings.json +++ b/homeassistant/components/imap/strings.json @@ -72,7 +72,8 @@ "search": "[%key:component::imap::config::step::user::data::search%]", "custom_event_data_template": "Template to create custom event data", "max_message_size": "Max message size (2048 < size < 30000)", - "enable_push": "Enable Push-IMAP if the server supports it. Turn off if Push-IMAP updates are unreliable." + "enable_push": "Enable Push-IMAP if the server supports it. Turn off if Push-IMAP updates are unreliable.", + "event_message_data": "Message data to be included in the `imap_content` event data:" } } }, @@ -92,6 +93,12 @@ "modern": "Modern ciphers", "intermediate": "Intermediate ciphers" } + }, + "event_message_data": { + "options": { + "text": "Body text", + "headers": "Message headers" + } } }, "services": { diff --git a/tests/components/imap/test_config_flow.py b/tests/components/imap/test_config_flow.py index 2354c5fc9b9..459cecec4a6 100644 --- a/tests/components/imap/test_config_flow.py +++ b/tests/components/imap/test_config_flow.py @@ -29,11 +29,13 @@ MOCK_CONFIG = { "charset": "utf-8", "folder": "INBOX", "search": "UnSeen UnDeleted", + "event_message_data": ["text", "headers"], } MOCK_OPTIONS = { "folder": "INBOX", "search": "UnSeen UnDeleted", + "event_message_data": ["text", "headers"], } pytestmark = pytest.mark.usefixtures("mock_setup_entry") @@ -504,6 +506,38 @@ async def test_config_flow_with_cipherlist_and_ssl_verify( assert len(mock_setup_entry.mock_calls) == 1 +@pytest.mark.parametrize("event_message_data", [[], ["headers"], ["text", "headers"]]) +async def test_config_flow_with_event_message_data( + hass: HomeAssistant, mock_setup_entry: AsyncMock, event_message_data: list +) -> None: + """Test with different message data.""" + config = MOCK_CONFIG.copy() + config["event_message_data"] = event_message_data + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER, "show_advanced_options": False}, + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.imap.config_flow.connect_to_server" + ) as mock_client: + mock_client.return_value.search.return_value = ( + "OK", + [b""], + ) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], config + ) + await hass.async_block_till_done() + + assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["title"] == "email@email.com" + assert result2["data"] == config + assert len(mock_setup_entry.mock_calls) == 1 + + async def test_config_flow_from_with_advanced_settings( hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: diff --git a/tests/components/imap/test_diagnostics.py b/tests/components/imap/test_diagnostics.py index 79d51b73401..721e09352f2 100644 --- a/tests/components/imap/test_diagnostics.py +++ b/tests/components/imap/test_diagnostics.py @@ -66,6 +66,10 @@ async def test_entry_diagnostics( "port": 993, "charset": "utf-8", "folder": "INBOX", + "event_message_data": [ + "text", + "headers", + ], "search": "UnSeen UnDeleted", "custom_event_data_template": "{{ 4 * 4 }}", } diff --git a/tests/components/imap/test_init.py b/tests/components/imap/test_init.py index 69c1aaabb2e..a8f51142d8d 100644 --- a/tests/components/imap/test_init.py +++ b/tests/components/imap/test_init.py @@ -674,6 +674,41 @@ async def test_message_is_truncated( assert len(event_data["text"]) == 3 +@pytest.mark.parametrize("imap_search", [TEST_SEARCH_RESPONSE]) +@pytest.mark.parametrize( + "imap_fetch", [(TEST_FETCH_RESPONSE_TEXT_PLAIN)], ids=["plain"] +) +@pytest.mark.parametrize("imap_has_capability", [True, False], ids=["push", "poll"]) +@pytest.mark.parametrize("event_message_data", [[], ["text"], ["text", "headers"]]) +async def test_message_data( + hass: HomeAssistant, + mock_imap_protocol: MagicMock, + caplog: pytest.LogCaptureFixture, + event_message_data: list, +) -> None: + """Test with different message data.""" + event_called = async_capture_events(hass, "imap_content") + + config = MOCK_CONFIG.copy() + # Mock different message data + config["event_message_data"] = event_message_data + config_entry = MockConfigEntry(domain=DOMAIN, data=config) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + # Make sure we have had one update (when polling) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) + await hass.async_block_till_done() + state = hass.states.get("sensor.imap_email_email_com") + # We should have received one message + assert state is not None + assert state.state == "1" + assert len(event_called) == 1 + + event_data = event_called[0].data + assert set(event_message_data).issubset(set(event_data)) + + @pytest.mark.parametrize( ("imap_search", "imap_fetch"), [(TEST_SEARCH_RESPONSE, TEST_FETCH_RESPONSE_TEXT_PLAIN)], From 017b2fe685158a537d064230cf5c795576935157 Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Mon, 8 Apr 2024 20:22:09 +0200 Subject: [PATCH 417/967] Change scan interval for Husqvarna Automower (#115225) * Change scan interval for Husqvarna Automower * Also use const --- homeassistant/components/husqvarna_automower/coordinator.py | 3 ++- tests/components/husqvarna_automower/test_binary_sensor.py | 4 ++-- tests/components/husqvarna_automower/test_lawn_mower.py | 4 ++-- tests/components/husqvarna_automower/test_select.py | 4 ++-- tests/components/husqvarna_automower/test_sensor.py | 6 +++--- tests/components/husqvarna_automower/test_switch.py | 4 ++-- 6 files changed, 13 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/coordinator.py b/homeassistant/components/husqvarna_automower/coordinator.py index 2188725ed76..8d9588db5b7 100644 --- a/homeassistant/components/husqvarna_automower/coordinator.py +++ b/homeassistant/components/husqvarna_automower/coordinator.py @@ -16,6 +16,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) MAX_WS_RECONNECT_TIME = 600 +SCAN_INTERVAL = timedelta(minutes=8) class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, MowerAttributes]]): @@ -29,7 +30,7 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, MowerAttrib hass, _LOGGER, name=DOMAIN, - update_interval=timedelta(minutes=5), + update_interval=SCAN_INTERVAL, ) self.api = api diff --git a/tests/components/husqvarna_automower/test_binary_sensor.py b/tests/components/husqvarna_automower/test_binary_sensor.py index 425636ba915..144dc734025 100644 --- a/tests/components/husqvarna_automower/test_binary_sensor.py +++ b/tests/components/husqvarna_automower/test_binary_sensor.py @@ -1,6 +1,5 @@ """Tests for binary sensor platform.""" -from datetime import timedelta from unittest.mock import AsyncMock, patch from aioautomower.model import MowerActivities @@ -9,6 +8,7 @@ from freezegun.api import FrozenDateTimeFactory from syrupy import SnapshotAssertion from homeassistant.components.husqvarna_automower.const import DOMAIN +from homeassistant.components.husqvarna_automower.coordinator import SCAN_INTERVAL from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -51,7 +51,7 @@ async def test_binary_sensor_states( ]: values[TEST_MOWER_ID].mower.activity = activity mock_automower_client.get_status.return_value = values - freezer.tick(timedelta(minutes=5)) + freezer.tick(SCAN_INTERVAL) async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get(f"binary_sensor.{entity}") diff --git a/tests/components/husqvarna_automower/test_lawn_mower.py b/tests/components/husqvarna_automower/test_lawn_mower.py index 6e491fd4a28..c8aea0e7c98 100644 --- a/tests/components/husqvarna_automower/test_lawn_mower.py +++ b/tests/components/husqvarna_automower/test_lawn_mower.py @@ -1,6 +1,5 @@ """Tests for lawn_mower module.""" -from datetime import timedelta from unittest.mock import AsyncMock from aioautomower.exceptions import ApiException @@ -9,6 +8,7 @@ from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components.husqvarna_automower.const import DOMAIN +from homeassistant.components.husqvarna_automower.coordinator import SCAN_INTERVAL from homeassistant.components.lawn_mower import LawnMowerActivity from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -46,7 +46,7 @@ async def test_lawn_mower_states( values[TEST_MOWER_ID].mower.activity = activity values[TEST_MOWER_ID].mower.state = state mock_automower_client.get_status.return_value = values - freezer.tick(timedelta(minutes=5)) + freezer.tick(SCAN_INTERVAL) async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get("lawn_mower.test_mower_1") diff --git a/tests/components/husqvarna_automower/test_select.py b/tests/components/husqvarna_automower/test_select.py index 4283c7d3797..9e255eb410f 100644 --- a/tests/components/husqvarna_automower/test_select.py +++ b/tests/components/husqvarna_automower/test_select.py @@ -1,6 +1,5 @@ """Tests for select platform.""" -from datetime import timedelta from unittest.mock import AsyncMock from aioautomower.exceptions import ApiException @@ -10,6 +9,7 @@ from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components.husqvarna_automower.const import DOMAIN +from homeassistant.components.husqvarna_automower.coordinator import SCAN_INTERVAL from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -48,7 +48,7 @@ async def test_select_states( ]: values[TEST_MOWER_ID].headlight.mode = state mock_automower_client.get_status.return_value = values - freezer.tick(timedelta(minutes=5)) + freezer.tick(SCAN_INTERVAL) async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get("select.test_mower_1_headlight_mode") diff --git a/tests/components/husqvarna_automower/test_sensor.py b/tests/components/husqvarna_automower/test_sensor.py index 6d4e8412ad3..5d304330aca 100644 --- a/tests/components/husqvarna_automower/test_sensor.py +++ b/tests/components/husqvarna_automower/test_sensor.py @@ -1,6 +1,5 @@ """Tests for sensor platform.""" -from datetime import timedelta from unittest.mock import AsyncMock, patch from aioautomower.model import MowerModes @@ -10,6 +9,7 @@ import pytest from syrupy import SnapshotAssertion from homeassistant.components.husqvarna_automower.const import DOMAIN +from homeassistant.components.husqvarna_automower.coordinator import SCAN_INTERVAL from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -41,7 +41,7 @@ async def test_sensor_unknown_states( values[TEST_MOWER_ID].mower.mode = MowerModes.UNKNOWN mock_automower_client.get_status.return_value = values - freezer.tick(timedelta(minutes=5)) + freezer.tick(SCAN_INTERVAL) async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get("sensor.test_mower_1_mode") @@ -112,7 +112,7 @@ async def test_error_sensor( ]: values[TEST_MOWER_ID].mower.error_key = state mock_automower_client.get_status.return_value = values - freezer.tick(timedelta(minutes=5)) + freezer.tick(SCAN_INTERVAL) async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get("sensor.test_mower_1_error") diff --git a/tests/components/husqvarna_automower/test_switch.py b/tests/components/husqvarna_automower/test_switch.py index 22137a35323..8dbb5450db1 100644 --- a/tests/components/husqvarna_automower/test_switch.py +++ b/tests/components/husqvarna_automower/test_switch.py @@ -1,6 +1,5 @@ """Tests for switch platform.""" -from datetime import timedelta from unittest.mock import AsyncMock, patch from aioautomower.exceptions import ApiException @@ -11,6 +10,7 @@ import pytest from syrupy import SnapshotAssertion from homeassistant.components.husqvarna_automower.const import DOMAIN +from homeassistant.components.husqvarna_automower.coordinator import SCAN_INTERVAL from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -45,7 +45,7 @@ async def test_switch_states( values[TEST_MOWER_ID].mower.state = state values[TEST_MOWER_ID].planner.restricted_reason = restricted_reson mock_automower_client.get_status.return_value = values - freezer.tick(timedelta(minutes=5)) + freezer.tick(SCAN_INTERVAL) async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get("switch.test_mower_1_enable_schedule") From a52a80f8062272146f78083fc6d18a95f538e854 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 8 Apr 2024 20:25:22 +0200 Subject: [PATCH 418/967] Always include old_state in EventStateChangedData [tests] (#115098) --- tests/common.py | 12 ++++++------ tests/components/mqtt_eventstream/test_init.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/common.py b/tests/common.py index 88450d34564..38af58642c9 100644 --- a/tests/common.py +++ b/tests/common.py @@ -589,12 +589,12 @@ def json_round_trip(obj: Any) -> Any: def mock_state_change_event( hass: HomeAssistant, new_state: State, old_state: State | None = None ) -> None: - """Mock state change envent.""" - event_data = {"entity_id": new_state.entity_id, "new_state": new_state} - - if old_state: - event_data["old_state"] = old_state - + """Mock state change event.""" + event_data = { + "entity_id": new_state.entity_id, + "new_state": new_state, + "old_state": old_state, + } hass.bus.fire(EVENT_STATE_CHANGED, event_data, context=new_state.context) diff --git a/tests/components/mqtt_eventstream/test_init.py b/tests/components/mqtt_eventstream/test_init.py index 24b4a83c425..90034382fc8 100644 --- a/tests/components/mqtt_eventstream/test_init.py +++ b/tests/components/mqtt_eventstream/test_init.py @@ -111,7 +111,7 @@ async def test_state_changed_event_sends_message( "last_updated": now.isoformat(), "state": "on", } - event["event_data"] = {"new_state": new_state, "entity_id": e_id} + event["event_data"] = {"new_state": new_state, "entity_id": e_id, "old_state": None} # Verify that the message received was that expected result = json.loads(msg) From 6116f11e6c56c356c995602a8c0029e32047f1c8 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 8 Apr 2024 20:25:34 +0200 Subject: [PATCH 419/967] Use EventType for system events (#115190) --- homeassistant/components/api/__init__.py | 3 ++- homeassistant/components/homeassistant/logbook.py | 7 +++++-- homeassistant/const.py | 13 ++++++++----- homeassistant/helpers/start.py | 5 ++++- homeassistant/helpers/typing.py | 3 ++- 5 files changed, 21 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py index 2a2b55429dd..489475e9dd7 100644 --- a/homeassistant/components/api/__init__.py +++ b/homeassistant/components/api/__init__.py @@ -49,6 +49,7 @@ from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.json import json_dumps, json_fragment from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.helpers.typing import ConfigType +from homeassistant.util.event_type import EventType from homeassistant.util.json import json_loads _LOGGER = logging.getLogger(__name__) @@ -134,7 +135,7 @@ class APIEventStream(HomeAssistantView): stop_obj = object() to_write: asyncio.Queue[object | str] = asyncio.Queue() - restrict: list[str] | None = None + restrict: list[EventType[Any] | str] | None = None if restrict_str := request.query.get("restrict"): restrict = [*restrict_str.split(","), EVENT_HOMEASSISTANT_STOP] diff --git a/homeassistant/components/homeassistant/logbook.py b/homeassistant/components/homeassistant/logbook.py index 1c67075b671..92a91dbd5cb 100644 --- a/homeassistant/components/homeassistant/logbook.py +++ b/homeassistant/components/homeassistant/logbook.py @@ -12,6 +12,7 @@ from homeassistant.components.logbook import ( ) from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.helpers.typing import NoEventData from homeassistant.util.event_type import EventType from . import DOMAIN @@ -25,12 +26,14 @@ EVENT_TO_NAME: dict[EventType[Any] | str, str] = { @callback def async_describe_events( hass: HomeAssistant, - async_describe_event: Callable[[str, str, Callable[[Event], dict[str, str]]], None], + async_describe_event: Callable[ + [str, EventType[NoEventData] | str, Callable[[Event], dict[str, str]]], None + ], ) -> None: """Describe logbook events.""" @callback - def async_describe_hass_event(event: Event) -> dict[str, str]: + def async_describe_hass_event(event: Event[NoEventData]) -> dict[str, str]: """Describe homeassistant logbook event.""" return { LOGBOOK_ENTRY_NAME: "Home Assistant", diff --git a/homeassistant/const.py b/homeassistant/const.py index a9dbfef5eab..58a1c92ea72 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -18,6 +18,7 @@ from .util.signal_type import SignalType if TYPE_CHECKING: from .core import EventStateChangedData + from .helpers.typing import NoEventData APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 @@ -302,11 +303,13 @@ CONF_ZONE: Final = "zone" EVENT_CALL_SERVICE: Final = "call_service" EVENT_COMPONENT_LOADED: Final = "component_loaded" EVENT_CORE_CONFIG_UPDATE: Final = "core_config_updated" -EVENT_HOMEASSISTANT_CLOSE: Final = "homeassistant_close" -EVENT_HOMEASSISTANT_START: Final = "homeassistant_start" -EVENT_HOMEASSISTANT_STARTED: Final = "homeassistant_started" -EVENT_HOMEASSISTANT_STOP: Final = "homeassistant_stop" -EVENT_HOMEASSISTANT_FINAL_WRITE: Final = "homeassistant_final_write" +EVENT_HOMEASSISTANT_CLOSE: EventType[NoEventData] = EventType("homeassistant_close") +EVENT_HOMEASSISTANT_START: EventType[NoEventData] = EventType("homeassistant_start") +EVENT_HOMEASSISTANT_STARTED: EventType[NoEventData] = EventType("homeassistant_started") +EVENT_HOMEASSISTANT_STOP: EventType[NoEventData] = EventType("homeassistant_stop") +EVENT_HOMEASSISTANT_FINAL_WRITE: EventType[NoEventData] = EventType( + "homeassistant_final_write" +) EVENT_LOGBOOK_ENTRY: Final = "logbook_entry" EVENT_LOGGING_CHANGED: Final = "logging_changed" EVENT_SERVICE_REGISTERED: Final = "service_registered" diff --git a/homeassistant/helpers/start.py b/homeassistant/helpers/start.py index 4d07ec213bb..839514cbf2d 100644 --- a/homeassistant/helpers/start.py +++ b/homeassistant/helpers/start.py @@ -14,13 +14,16 @@ from homeassistant.core import ( HomeAssistant, callback, ) +from homeassistant.util.event_type import EventType + +from .typing import NoEventData @callback def _async_at_core_state( hass: HomeAssistant, at_start_cb: Callable[[HomeAssistant], Coroutine[Any, Any, None] | None], - event_type: str, + event_type: EventType[NoEventData], check_state: Callable[[HomeAssistant], bool], ) -> CALLBACK_TYPE: """Execute a job at_start_cb when Home Assistant has the wanted state. diff --git a/homeassistant/helpers/typing.py b/homeassistant/helpers/typing.py index 9bc34a09066..cf97e92d6be 100644 --- a/homeassistant/helpers/typing.py +++ b/homeassistant/helpers/typing.py @@ -3,7 +3,7 @@ from collections.abc import Mapping from enum import Enum from functools import partial -from typing import Any +from typing import Any, Never import homeassistant.core @@ -20,6 +20,7 @@ DiscoveryInfoType = dict[str, Any] ServiceDataType = dict[str, Any] StateType = str | int | float | None TemplateVarsType = Mapping[str, Any] | None +NoEventData = Mapping[str, Never] # Custom type for recorder Queries QueryType = Any From 0d18679c8fcc6995d1bc1828d4b17570f2030be5 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 8 Apr 2024 20:25:39 +0200 Subject: [PATCH 420/967] Use EventType for remaining registry events (#115189) --- homeassistant/helpers/area_registry.py | 14 ++++++++++---- homeassistant/helpers/category_registry.py | 5 ++++- homeassistant/helpers/floor_registry.py | 5 ++++- homeassistant/helpers/label_registry.py | 5 ++++- 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py index 69b405c6af0..1384735e2fd 100644 --- a/homeassistant/helpers/area_registry.py +++ b/homeassistant/helpers/area_registry.py @@ -8,6 +8,7 @@ from typing import Any, Literal, TypedDict, cast from homeassistant.core import HomeAssistant, callback from homeassistant.util import slugify +from homeassistant.util.event_type import EventType from . import device_registry as dr, entity_registry as er from .normalized_name_base_registry import ( @@ -20,7 +21,9 @@ from .storage import Store from .typing import UNDEFINED, UndefinedType DATA_REGISTRY = "area_registry" -EVENT_AREA_REGISTRY_UPDATED = "area_registry_updated" +EVENT_AREA_REGISTRY_UPDATED: EventType[EventAreaRegistryUpdatedData] = EventType( + "area_registry_updated" +) STORAGE_KEY = "core.area_registry" STORAGE_VERSION_MAJOR = 1 STORAGE_VERSION_MINOR = 6 @@ -219,7 +222,8 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]): self.areas[area.id] = area self.async_schedule_save() self.hass.bus.async_fire( - EVENT_AREA_REGISTRY_UPDATED, {"action": "create", "area_id": area.id} + EVENT_AREA_REGISTRY_UPDATED, + EventAreaRegistryUpdatedData(action="create", area_id=area.id), ) return area @@ -234,7 +238,8 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]): del self.areas[area_id] self.hass.bus.async_fire( - EVENT_AREA_REGISTRY_UPDATED, {"action": "remove", "area_id": area_id} + EVENT_AREA_REGISTRY_UPDATED, + EventAreaRegistryUpdatedData(action="remove", area_id=area_id), ) self.async_schedule_save() @@ -262,7 +267,8 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]): picture=picture, ) self.hass.bus.async_fire( - EVENT_AREA_REGISTRY_UPDATED, {"action": "update", "area_id": area_id} + EVENT_AREA_REGISTRY_UPDATED, + EventAreaRegistryUpdatedData(action="update", area_id=area_id), ) return updated diff --git a/homeassistant/helpers/category_registry.py b/homeassistant/helpers/category_registry.py index 7d559477f57..4ae920055a2 100644 --- a/homeassistant/helpers/category_registry.py +++ b/homeassistant/helpers/category_registry.py @@ -8,6 +8,7 @@ from dataclasses import dataclass, field from typing import Literal, TypedDict, cast from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.util.event_type import EventType from homeassistant.util.ulid import ulid_now from .registry import BaseRegistry @@ -15,7 +16,9 @@ from .storage import Store from .typing import UNDEFINED, UndefinedType DATA_REGISTRY = "category_registry" -EVENT_CATEGORY_REGISTRY_UPDATED = "category_registry_updated" +EVENT_CATEGORY_REGISTRY_UPDATED: EventType[EventCategoryRegistryUpdatedData] = ( + EventType("category_registry_updated") +) STORAGE_KEY = "core.category_registry" STORAGE_VERSION_MAJOR = 1 diff --git a/homeassistant/helpers/floor_registry.py b/homeassistant/helpers/floor_registry.py index a10d3af6101..4a11d85176a 100644 --- a/homeassistant/helpers/floor_registry.py +++ b/homeassistant/helpers/floor_registry.py @@ -9,6 +9,7 @@ from typing import Literal, TypedDict, cast from homeassistant.core import Event, HomeAssistant, callback from homeassistant.util import slugify +from homeassistant.util.event_type import EventType from .normalized_name_base_registry import ( NormalizedNameBaseRegistryEntry, @@ -20,7 +21,9 @@ from .storage import Store from .typing import UNDEFINED, UndefinedType DATA_REGISTRY = "floor_registry" -EVENT_FLOOR_REGISTRY_UPDATED = "floor_registry_updated" +EVENT_FLOOR_REGISTRY_UPDATED: EventType[EventFloorRegistryUpdatedData] = EventType( + "floor_registry_updated" +) STORAGE_KEY = "core.floor_registry" STORAGE_VERSION_MAJOR = 1 diff --git a/homeassistant/helpers/label_registry.py b/homeassistant/helpers/label_registry.py index e409b41b26f..81901c71745 100644 --- a/homeassistant/helpers/label_registry.py +++ b/homeassistant/helpers/label_registry.py @@ -9,6 +9,7 @@ from typing import Literal, TypedDict, cast from homeassistant.core import Event, HomeAssistant, callback from homeassistant.util import slugify +from homeassistant.util.event_type import EventType from .normalized_name_base_registry import ( NormalizedNameBaseRegistryEntry, @@ -20,7 +21,9 @@ from .storage import Store from .typing import UNDEFINED, UndefinedType DATA_REGISTRY = "label_registry" -EVENT_LABEL_REGISTRY_UPDATED = "label_registry_updated" +EVENT_LABEL_REGISTRY_UPDATED: EventType[EventLabelRegistryUpdatedData] = EventType( + "label_registry_updated" +) STORAGE_KEY = "core.label_registry" STORAGE_VERSION_MAJOR = 1 From 2fc0d8494d567c7d6bfc3de79961270c3c578e55 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 8 Apr 2024 20:25:57 +0200 Subject: [PATCH 421/967] Use EventType for device_registry_updated (#115188) --- homeassistant/helpers/device_registry.py | 21 +++++++++++++-------- homeassistant/helpers/entity_registry.py | 9 +++++++-- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index dc3f4bff434..df2c5b57395 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -16,6 +16,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT from homeassistant.core import Event, HomeAssistant, callback, get_release_channel from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import async_suggest_report_issue +from homeassistant.util.event_type import EventType from homeassistant.util.json import format_unserializable_data import homeassistant.util.uuid as uuid_util @@ -40,7 +41,9 @@ if TYPE_CHECKING: _LOGGER = logging.getLogger(__name__) DATA_REGISTRY = "device_registry" -EVENT_DEVICE_REGISTRY_UPDATED = "device_registry_updated" +EVENT_DEVICE_REGISTRY_UPDATED: EventType[EventDeviceRegistryUpdatedData] = EventType( + "device_registry_updated" +) STORAGE_KEY = "core.device_registry" STORAGE_VERSION_MAJOR = 1 STORAGE_VERSION_MINOR = 5 @@ -908,12 +911,11 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): self.async_schedule_save() - data: dict[str, Any] = { - "action": "create" if old.is_new else "update", - "device_id": new.id, - } - if not old.is_new: - data["changes"] = old_values + data: EventDeviceRegistryUpdatedData + if old.is_new: + data = {"action": "create", "device_id": new.id} + else: + data = {"action": "update", "device_id": new.id, "changes": old_values} self.hass.bus.async_fire(EVENT_DEVICE_REGISTRY_UPDATED, data) @@ -934,7 +936,10 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): if other_device.via_device_id == device_id: self.async_update_device(other_device.id, via_device_id=None) self.hass.bus.async_fire( - EVENT_DEVICE_REGISTRY_UPDATED, {"action": "remove", "device_id": device_id} + EVENT_DEVICE_REGISTRY_UPDATED, + _EventDeviceRegistryUpdatedData_CreateRemove( + action="remove", device_id=device_id + ), ) self.async_schedule_save() diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index b1e92f51c2c..1197e79ad8d 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -51,7 +51,10 @@ from homeassistant.util.json import format_unserializable_data from homeassistant.util.read_only_dict import ReadOnlyDict from . import device_registry as dr, storage -from .device_registry import EVENT_DEVICE_REGISTRY_UPDATED +from .device_registry import ( + EVENT_DEVICE_REGISTRY_UPDATED, + EventDeviceRegistryUpdatedData, +) from .json import JSON_DUMP, find_paths_unserializable_data, json_bytes, json_fragment from .registry import BaseRegistry, BaseRegistryItems from .typing import UNDEFINED, UndefinedType @@ -903,7 +906,9 @@ class EntityRegistry(BaseRegistry): self.async_schedule_save() @callback - def async_device_modified(self, event: Event) -> None: + def async_device_modified( + self, event: Event[EventDeviceRegistryUpdatedData] + ) -> None: """Handle the removal or update of a device. Remove entities from the registry that are associated to a device when From 266e4f0c8bbdd9dbb1252168a21b44a98dec0680 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 8 Apr 2024 08:29:27 -1000 Subject: [PATCH 422/967] Migrate rfxtrx to use run_immediately=True for the device registry listener (#115165) --- homeassistant/components/rfxtrx/__init__.py | 2 +- homeassistant/components/rfxtrx/config_flow.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index b40e8d921ed..78b7daa8347 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -281,7 +281,7 @@ async def async_setup_internal(hass: HomeAssistant, entry: ConfigEntry) -> None: entry.async_on_unload( hass.bus.async_listen( - dr.EVENT_DEVICE_REGISTRY_UPDATED, _updated_device, run_immediately=False + dr.EVENT_DEVICE_REGISTRY_UPDATED, _updated_device, run_immediately=True ) ) diff --git a/homeassistant/components/rfxtrx/config_flow.py b/homeassistant/components/rfxtrx/config_flow.py index 837ca554615..c1b52962f32 100644 --- a/homeassistant/components/rfxtrx/config_flow.py +++ b/homeassistant/components/rfxtrx/config_flow.py @@ -486,7 +486,10 @@ class RfxtrxOptionsFlow(OptionsFlow): if devices: for event_code, options in devices.items(): if options is None: - entry_data[CONF_DEVICES].pop(event_code) + # If the config entry is setup, the device registry + # listener will remove the device from the config + # entry before we get here + entry_data[CONF_DEVICES].pop(event_code, None) else: entry_data[CONF_DEVICES][event_code] = options self.hass.config_entries.async_update_entry(self._config_entry, data=entry_data) From 025c6f5e2e86e34948b9b1a2b0908220cf792d5a Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Mon, 8 Apr 2024 20:44:18 +0200 Subject: [PATCH 423/967] Add `__slots__` to NodeClass classes (#115079) Co-authored-by: J. Nick Koston --- homeassistant/util/yaml/loader.py | 31 +++++++++++++++--------------- homeassistant/util/yaml/objects.py | 15 +++++++++++++++ tests/test_config.py | 1 - 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index 79ee2797ad9..3a779b5e944 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -8,7 +8,7 @@ from io import StringIO, TextIOWrapper import logging import os from pathlib import Path -from typing import Any, TextIO, TypeVar, overload +from typing import Any, TextIO, overload import yaml @@ -33,7 +33,6 @@ from .objects import Input, NodeDictClass, NodeListClass, NodeStrClass # mypy: allow-untyped-calls, no-warn-return-any JSON_TYPE = list | dict | str -_DictT = TypeVar("_DictT", bound=dict) _LOGGER = logging.getLogger(__name__) @@ -286,37 +285,37 @@ def _parse_yaml( @overload def _add_reference( - obj: list | NodeListClass, - loader: LoaderType, - node: yaml.nodes.Node, + obj: list | NodeListClass, loader: LoaderType, node: yaml.nodes.Node ) -> NodeListClass: ... @overload def _add_reference( - obj: str | NodeStrClass, - loader: LoaderType, - node: yaml.nodes.Node, + obj: str | NodeStrClass, loader: LoaderType, node: yaml.nodes.Node ) -> NodeStrClass: ... @overload def _add_reference( - obj: _DictT, loader: LoaderType, node: yaml.nodes.Node -) -> _DictT: ... + obj: dict | NodeDictClass, loader: LoaderType, node: yaml.nodes.Node +) -> NodeDictClass: ... -def _add_reference( # type: ignore[no-untyped-def] - obj, loader: LoaderType, node: yaml.nodes.Node -): +def _add_reference( + obj: dict | list | str | NodeDictClass | NodeListClass | NodeStrClass, + loader: LoaderType, + node: yaml.nodes.Node, +) -> NodeDictClass | NodeListClass | NodeStrClass: """Add file reference information to an object.""" if isinstance(obj, list): obj = NodeListClass(obj) - if isinstance(obj, str): + elif isinstance(obj, str): obj = NodeStrClass(obj) + elif isinstance(obj, dict): + obj = NodeDictClass(obj) try: # suppress is much slower - setattr(obj, "__config_file__", loader.get_name) - setattr(obj, "__line__", node.start_mark.line + 1) + obj.__config_file__ = loader.get_name + obj.__line__ = node.start_mark.line + 1 except AttributeError: pass return obj diff --git a/homeassistant/util/yaml/objects.py b/homeassistant/util/yaml/objects.py index 70c229c1a2f..d35ba11d25e 100644 --- a/homeassistant/util/yaml/objects.py +++ b/homeassistant/util/yaml/objects.py @@ -13,10 +13,20 @@ import yaml class NodeListClass(list): """Wrapper class to be able to add attributes on a list.""" + __slots__ = ("__config_file__", "__line__") + + __config_file__: str + __line__: int | str + class NodeStrClass(str): """Wrapper class to be able to add attributes on a string.""" + __slots__ = ("__config_file__", "__line__") + + __config_file__: str + __line__: int | str + def __voluptuous_compile__(self, schema: vol.Schema) -> Any: """Needed because vol.Schema.compile does not handle str subclasses.""" return _compile_scalar(self) @@ -25,6 +35,11 @@ class NodeStrClass(str): class NodeDictClass(dict): """Wrapper class to be able to add attributes on a dict.""" + __slots__ = ("__config_file__", "__line__") + + __config_file__: str + __line__: int | str + @dataclass(slots=True, frozen=True) class Input: diff --git a/tests/test_config.py b/tests/test_config.py index 89a9b7b7082..defd6a1018b 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -381,7 +381,6 @@ async def mock_custom_validator_integrations_with_docs( class ConfigTestClass(NodeDictClass): """Test class for config with wrapper.""" - __dict__ = {"__config_file__": "configuration.yaml", "__line__": 140} __line__ = 140 __config_file__ = "configuration.yaml" From 4e94f11665a8c22dc15ec428512c704db791777f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 8 Apr 2024 20:44:59 +0200 Subject: [PATCH 424/967] Use EventType for entity_registry_updated (#115187) --- homeassistant/components/person/__init__.py | 15 +++++++++------ homeassistant/config_entries.py | 8 ++++++-- homeassistant/helpers/device_registry.py | 8 ++++++-- homeassistant/helpers/entity_registry.py | 19 ++++++++++++++----- 4 files changed, 35 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index cf4059dcc6b..87b158f80c3 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations -from collections.abc import Callable, Mapping +from collections.abc import Callable import logging from typing import Any, Self @@ -17,7 +17,6 @@ from homeassistant.components.device_tracker import ( ) from homeassistant.const import ( ATTR_EDITABLE, - ATTR_ENTITY_ID, ATTR_GPS_ACCURACY, ATTR_ID, ATTR_LATITUDE, @@ -246,16 +245,20 @@ class PersonStorageCollection(collection.DictStorageCollection): ) @callback - def _entity_registry_filter(self, event_data: Mapping[str, Any]) -> bool: + def _entity_registry_filter( + self, event_data: er.EventEntityRegistryUpdatedData + ) -> bool: """Filter entity registry events.""" return ( event_data["action"] == "remove" - and split_entity_id(event_data[ATTR_ENTITY_ID])[0] == "device_tracker" + and split_entity_id(event_data["entity_id"])[0] == "device_tracker" ) - async def _entity_registry_updated(self, event: Event) -> None: + async def _entity_registry_updated( + self, event: Event[er.EventEntityRegistryUpdatedData] + ) -> None: """Handle entity registry updated.""" - entity_id = event.data[ATTR_ENTITY_ID] + entity_id = event.data["entity_id"] for person in list(self.data.values()): if entity_id not in person[CONF_DEVICE_TRACKERS]: continue diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index adbb99b9d3c..d7ffd697a2f 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -2507,7 +2507,9 @@ class EntityRegistryDisabledHandler: ) @callback - def _handle_entry_updated(self, event: Event) -> None: + def _handle_entry_updated( + self, event: Event[entity_registry.EventEntityRegistryUpdatedData] + ) -> None: """Handle entity registry entry update.""" if self.registry is None: self.registry = entity_registry.async_get(self.hass) @@ -2574,7 +2576,9 @@ class EntityRegistryDisabledHandler: @callback -def _handle_entry_updated_filter(event_data: Mapping[str, Any]) -> bool: +def _handle_entry_updated_filter( + event_data: entity_registry.EventEntityRegistryUpdatedData, +) -> bool: """Handle entity registry entry update filter. Only handle changes to "disabled_by". diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index df2c5b57395..2142cae854a 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -1221,12 +1221,16 @@ def async_setup_cleanup(hass: HomeAssistant, dev_reg: DeviceRegistry) -> None: ) @callback - def _async_entity_registry_changed(event: Event) -> None: + def _async_entity_registry_changed( + event: Event[entity_registry.EventEntityRegistryUpdatedData], + ) -> None: """Handle entity updated or removed dispatch.""" debounced_cleanup.async_schedule_call() @callback - def entity_registry_changed_filter(event_data: Mapping[str, Any]) -> bool: + def entity_registry_changed_filter( + event_data: entity_registry.EventEntityRegistryUpdatedData, + ) -> bool: """Handle entity updated or removed filter.""" if ( event_data["action"] == "update" diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 1197e79ad8d..727dbda9c2d 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -47,6 +47,7 @@ from homeassistant.core import ( from homeassistant.exceptions import MaxLengthExceeded from homeassistant.loader import async_suggest_report_issue from homeassistant.util import slugify, uuid as uuid_util +from homeassistant.util.event_type import EventType from homeassistant.util.json import format_unserializable_data from homeassistant.util.read_only_dict import ReadOnlyDict @@ -65,7 +66,9 @@ if TYPE_CHECKING: T = TypeVar("T") DATA_REGISTRY = "entity_registry" -EVENT_ENTITY_REGISTRY_UPDATED = "entity_registry_updated" +EVENT_ENTITY_REGISTRY_UPDATED: EventType[EventEntityRegistryUpdatedData] = EventType( + "entity_registry_updated" +) _LOGGER = logging.getLogger(__name__) @@ -879,7 +882,10 @@ class EntityRegistry(BaseRegistry): self.async_schedule_save() self.hass.bus.async_fire( - EVENT_ENTITY_REGISTRY_UPDATED, {"action": "create", "entity_id": entity_id} + EVENT_ENTITY_REGISTRY_UPDATED, + _EventEntityRegistryUpdatedData_CreateRemove( + action="create", entity_id=entity_id + ), ) return entry @@ -901,7 +907,10 @@ class EntityRegistry(BaseRegistry): unique_id=entity.unique_id, ) self.hass.bus.async_fire( - EVENT_ENTITY_REGISTRY_UPDATED, {"action": "remove", "entity_id": entity_id} + EVENT_ENTITY_REGISTRY_UPDATED, + _EventEntityRegistryUpdatedData_CreateRemove( + action="remove", entity_id=entity_id + ), ) self.async_schedule_save() @@ -1082,7 +1091,7 @@ class EntityRegistry(BaseRegistry): self.async_schedule_save() - data: dict[str, str | dict[str, Any]] = { + data: _EventEntityRegistryUpdatedData_Update = { "action": "update", "entity_id": entity_id, "changes": old_values, @@ -1531,7 +1540,7 @@ def _async_setup_entity_restore(hass: HomeAssistant, registry: EntityRegistry) - return bool(event_data["action"] == "remove") @callback - def cleanup_restored_states(event: Event) -> None: + def cleanup_restored_states(event: Event[EventEntityRegistryUpdatedData]) -> None: """Clean up restored states.""" state = hass.states.get(event.data["entity_id"]) From f114ebd79d7545b8be4cd3574d168078f8cc3be2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Conde=20G=C3=B3mez?= Date: Mon, 8 Apr 2024 20:46:22 +0200 Subject: [PATCH 425/967] Remove @skgsergio from foscam and qingping codeowners (#115210) --- CODEOWNERS | 8 ++++---- homeassistant/components/foscam/manifest.json | 2 +- homeassistant/components/qingping/manifest.json | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index a4e237e79f1..df1a7370fcd 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -458,8 +458,8 @@ build.json @home-assistant/supervisor /homeassistant/components/forked_daapd/ @uvjustin /tests/components/forked_daapd/ @uvjustin /homeassistant/components/fortios/ @kimfrellsen -/homeassistant/components/foscam/ @skgsergio @krmarien -/tests/components/foscam/ @skgsergio @krmarien +/homeassistant/components/foscam/ @krmarien +/tests/components/foscam/ @krmarien /homeassistant/components/freebox/ @hacf-fr @Quentame /tests/components/freebox/ @hacf-fr @Quentame /homeassistant/components/freedompro/ @stefano055415 @@ -1079,8 +1079,8 @@ build.json @home-assistant/supervisor /tests/components/pvpc_hourly_pricing/ @azogue /homeassistant/components/qbittorrent/ @geoffreylagaisse @finder39 /tests/components/qbittorrent/ @geoffreylagaisse @finder39 -/homeassistant/components/qingping/ @bdraco @skgsergio -/tests/components/qingping/ @bdraco @skgsergio +/homeassistant/components/qingping/ @bdraco +/tests/components/qingping/ @bdraco /homeassistant/components/qld_bushfire/ @exxamalte /tests/components/qld_bushfire/ @exxamalte /homeassistant/components/qnap/ @disforw diff --git a/homeassistant/components/foscam/manifest.json b/homeassistant/components/foscam/manifest.json index 6f256f99854..9ddb7c4b4fc 100644 --- a/homeassistant/components/foscam/manifest.json +++ b/homeassistant/components/foscam/manifest.json @@ -1,7 +1,7 @@ { "domain": "foscam", "name": "Foscam", - "codeowners": ["@skgsergio", "@krmarien"], + "codeowners": ["@krmarien"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/foscam", "iot_class": "local_polling", diff --git a/homeassistant/components/qingping/manifest.json b/homeassistant/components/qingping/manifest.json index c25652ca91e..e0317ab89b5 100644 --- a/homeassistant/components/qingping/manifest.json +++ b/homeassistant/components/qingping/manifest.json @@ -15,7 +15,7 @@ "connectable": false } ], - "codeowners": ["@bdraco", "@skgsergio"], + "codeowners": ["@bdraco"], "config_flow": true, "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/qingping", From cbbadf6256d8c7dfbee90c957901e224522bf4a6 Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Mon, 8 Apr 2024 20:47:03 +0200 Subject: [PATCH 426/967] Enable Ruff PYI036 (#115228) --- homeassistant/util/timeout.py | 36 +++++++++++++++++------------------ pyproject.toml | 1 - 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/homeassistant/util/timeout.py b/homeassistant/util/timeout.py index 94baa57e4d8..72cabffeed6 100644 --- a/homeassistant/util/timeout.py +++ b/homeassistant/util/timeout.py @@ -39,9 +39,9 @@ class _GlobalFreezeContext: async def __aexit__( self, - exc_type: type[BaseException], - exc_val: BaseException, - exc_tb: TracebackType, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, ) -> bool | None: self._exit() return None @@ -52,9 +52,9 @@ class _GlobalFreezeContext: def __exit__( self, - exc_type: type[BaseException], - exc_val: BaseException, - exc_tb: TracebackType, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, ) -> bool | None: self._loop.call_soon_threadsafe(self._exit) return None @@ -107,9 +107,9 @@ class _ZoneFreezeContext: async def __aexit__( self, - exc_type: type[BaseException], - exc_val: BaseException, - exc_tb: TracebackType, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, ) -> bool | None: self._exit() return None @@ -120,9 +120,9 @@ class _ZoneFreezeContext: def __exit__( self, - exc_type: type[BaseException], - exc_val: BaseException, - exc_tb: TracebackType, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, ) -> bool | None: self._loop.call_soon_threadsafe(self._exit) return None @@ -171,9 +171,9 @@ class _GlobalTaskContext: async def __aexit__( self, - exc_type: type[BaseException], - exc_val: BaseException, - exc_tb: TracebackType, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, ) -> bool | None: self._stop_timer() self._manager.global_tasks.remove(self) @@ -286,9 +286,9 @@ class _ZoneTaskContext: async def __aexit__( self, - exc_type: type[BaseException], - exc_val: BaseException, - exc_tb: TracebackType, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, ) -> bool | None: self._zone.exit_task(self) self._stop_timer() diff --git a/pyproject.toml b/pyproject.toml index bcd0320d6f1..0ab3fd23597 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -722,7 +722,6 @@ ignore = [ # temporarily disabled "PT019", "PYI024", # Use typing.NamedTuple instead of collections.namedtuple - "PYI036", "PYI041", "RET503", "RET502", From 9cbed10372383bb1a0bd96d46e7eec29d9f1610e Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Mon, 8 Apr 2024 21:29:05 +0200 Subject: [PATCH 427/967] Enable Ruff PYI041 (#115229) --- homeassistant/auth/jwt_wrapper.py | 2 +- homeassistant/components/alexa/resources.py | 10 ++++----- homeassistant/components/demo/sensor.py | 2 +- .../components/homekit/type_thermostats.py | 4 ++-- homeassistant/components/homekit/util.py | 4 ++-- homeassistant/components/isy994/helpers.py | 2 +- .../components/modbus/base_platform.py | 2 +- .../components/nibe_heatpump/coordinator.py | 4 ++-- .../components/number/significant_change.py | 8 +++---- .../components/sensor/significant_change.py | 8 +++---- homeassistant/components/sonos/diagnostics.py | 2 +- homeassistant/components/tomorrowio/sensor.py | 4 ++-- homeassistant/components/tplink/light.py | 2 +- homeassistant/components/tuya/base.py | 12 +++++----- homeassistant/components/tuya/util.py | 10 ++++----- .../components/weather/significant_change.py | 2 +- homeassistant/helpers/significant_change.py | 22 +++++++++---------- pyproject.toml | 1 - tests/components/coolmaster/conftest.py | 2 +- tests/components/nibe_heatpump/__init__.py | 2 +- tests/components/sensor/test_init.py | 2 +- tests/helpers/test_template.py | 2 +- 22 files changed, 54 insertions(+), 55 deletions(-) diff --git a/homeassistant/auth/jwt_wrapper.py b/homeassistant/auth/jwt_wrapper.py index 58f9260ff8f..3aa3ac63764 100644 --- a/homeassistant/auth/jwt_wrapper.py +++ b/homeassistant/auth/jwt_wrapper.py @@ -78,7 +78,7 @@ class _PyJWTWithVerify(PyJWT): key: str, algorithms: list[str], issuer: str | None = None, - leeway: int | float | timedelta = 0, + leeway: float | timedelta = 0, options: dict[str, Any] | None = None, ) -> dict[str, Any]: """Verify a JWT's signature and claims.""" diff --git a/homeassistant/components/alexa/resources.py b/homeassistant/components/alexa/resources.py index 4bc63f6ccae..7782716798a 100644 --- a/homeassistant/components/alexa/resources.py +++ b/homeassistant/components/alexa/resources.py @@ -291,9 +291,9 @@ class AlexaPresetResource(AlexaCapabilityResource): def __init__( self, labels: list[str], - min_value: int | float, - max_value: int | float, - precision: int | float, + min_value: float, + max_value: float, + precision: float, unit: str | None = None, ) -> None: """Initialize an Alexa presetResource.""" @@ -306,7 +306,7 @@ class AlexaPresetResource(AlexaCapabilityResource): if unit in AlexaGlobalCatalog.__dict__.values(): self._unit_of_measure = unit - def add_preset(self, value: int | float, labels: list[str]) -> None: + def add_preset(self, value: float, labels: list[str]) -> None: """Add preset to configuration presets array.""" self._presets.append({"value": value, "labels": labels}) @@ -405,7 +405,7 @@ class AlexaSemantics: ) def add_states_to_range( - self, states: list[str], min_value: int | float, max_value: int | float + self, states: list[str], min_value: float, max_value: float ) -> None: """Add StatesToRange stateMappings.""" self._add_state_mapping( diff --git a/homeassistant/components/demo/sensor.py b/homeassistant/components/demo/sensor.py index 4281ca9cc59..0c61faae00e 100644 --- a/homeassistant/components/demo/sensor.py +++ b/homeassistant/components/demo/sensor.py @@ -149,7 +149,7 @@ class DemoSensor(SensorEntity): self, unique_id: str, device_name: str | None, - state: float | int | str | None, + state: float | str | None, device_class: SensorDeviceClass, state_class: SensorStateClass | None, unit_of_measurement: str | None, diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index ba0f9790efb..5dc520e8568 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -411,10 +411,10 @@ class Thermostat(HomeAccessory): params = {ATTR_ENTITY_ID: self.entity_id, ATTR_FAN_MODE: mode} self.async_call_service(DOMAIN_CLIMATE, SERVICE_SET_FAN_MODE, params) - def _temperature_to_homekit(self, temp: float | int) -> float: + def _temperature_to_homekit(self, temp: float) -> float: return temperature_to_homekit(temp, self._unit) - def _temperature_to_states(self, temp: float | int) -> float: + def _temperature_to_states(self, temp: float) -> float: return temperature_to_states(temp, self._unit) def _set_chars(self, char_values: dict[str, Any]) -> None: diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index f63ad9f46ae..dec7fe8eba7 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -401,14 +401,14 @@ def cleanup_name_for_homekit(name: str | None) -> str: return name.translate(HOMEKIT_CHAR_TRANSLATIONS)[:MAX_NAME_LENGTH] -def temperature_to_homekit(temperature: float | int, unit: str) -> float: +def temperature_to_homekit(temperature: float, unit: str) -> float: """Convert temperature to Celsius for HomeKit.""" return round( TemperatureConverter.convert(temperature, unit, UnitOfTemperature.CELSIUS), 1 ) -def temperature_to_states(temperature: float | int, unit: str) -> float: +def temperature_to_states(temperature: float, unit: str) -> float: """Convert temperature back from Celsius to Home Assistant unit.""" return ( round( diff --git a/homeassistant/components/isy994/helpers.py b/homeassistant/components/isy994/helpers.py index 8b6a4249931..3686a182fe9 100644 --- a/homeassistant/components/isy994/helpers.py +++ b/homeassistant/components/isy994/helpers.py @@ -431,7 +431,7 @@ def _categorize_programs(isy_data: IsyData, programs: Programs) -> None: def convert_isy_value_to_hass( - value: int | float | None, + value: float | None, uom: str | None, precision: int | str, fallback_precision: int | None = None, diff --git a/homeassistant/components/modbus/base_platform.py b/homeassistant/components/modbus/base_platform.py index 5c8816dd74e..9f0e862f283 100644 --- a/homeassistant/components/modbus/base_platform.py +++ b/homeassistant/components/modbus/base_platform.py @@ -202,7 +202,7 @@ class BaseStructPlatform(BasePlatform, RestoreEntity): registers.reverse() return registers - def __process_raw_value(self, entry: float | int | str | bytes) -> str | None: + def __process_raw_value(self, entry: float | str | bytes) -> str | None: """Process value from sensor with NaN handling, scaling, offset, min/max etc.""" if self._nan_value and entry in (self._nan_value, -self._nan_value): return None diff --git a/homeassistant/components/nibe_heatpump/coordinator.py b/homeassistant/components/nibe_heatpump/coordinator.py index a3d37ce0719..fc212faee71 100644 --- a/homeassistant/components/nibe_heatpump/coordinator.py +++ b/homeassistant/components/nibe_heatpump/coordinator.py @@ -136,7 +136,7 @@ class Coordinator(ContextCoordinator[dict[int, CoilData], int]): return float(value) # type: ignore[arg-type] return None - async def async_write_coil(self, coil: Coil, value: int | float | str) -> None: + async def async_write_coil(self, coil: Coil, value: float | str) -> None: """Write coil and update state.""" data = CoilData(coil, value) await self.connection.write_coil(data) @@ -224,7 +224,7 @@ class CoilEntity(CoordinatorEntity[Coordinator]): def _async_read_coil(self, data: CoilData): """Update state of entity based on coil data.""" - async def _async_write_coil(self, value: int | float | str): + async def _async_write_coil(self, value: float | str): """Write coil and update state.""" await self.coordinator.async_write_coil(self._coil, value) diff --git a/homeassistant/components/number/significant_change.py b/homeassistant/components/number/significant_change.py index ff77f25e527..14cb2246615 100644 --- a/homeassistant/components/number/significant_change.py +++ b/homeassistant/components/number/significant_change.py @@ -21,10 +21,10 @@ from .const import NumberDeviceClass def _absolute_and_relative_change( - old_state: int | float | None, - new_state: int | float | None, - absolute_change: int | float, - percentage_change: int | float, + old_state: float | None, + new_state: float | None, + absolute_change: float, + percentage_change: float, ) -> bool: return check_absolute_change( old_state, new_state, absolute_change diff --git a/homeassistant/components/sensor/significant_change.py b/homeassistant/components/sensor/significant_change.py index f320a7efcdf..00de36fc67c 100644 --- a/homeassistant/components/sensor/significant_change.py +++ b/homeassistant/components/sensor/significant_change.py @@ -20,10 +20,10 @@ from . import SensorDeviceClass def _absolute_and_relative_change( - old_state: int | float | None, - new_state: int | float | None, - absolute_change: int | float, - percentage_change: int | float, + old_state: float | None, + new_state: float | None, + absolute_change: float, + percentage_change: float, ) -> bool: return check_absolute_change( old_state, new_state, absolute_change diff --git a/homeassistant/components/sonos/diagnostics.py b/homeassistant/components/sonos/diagnostics.py index b97b03b9be2..09fe9d9db5f 100644 --- a/homeassistant/components/sonos/diagnostics.py +++ b/homeassistant/components/sonos/diagnostics.py @@ -113,7 +113,7 @@ async def async_generate_speaker_info( payload: dict[str, Any] = {} def get_contents( - item: int | float | str | dict[str, Any], + item: float | str | dict[str, Any], ) -> int | float | str | dict[str, Any]: if isinstance(item, (int, float, str)): return item diff --git a/homeassistant/components/tomorrowio/sensor.py b/homeassistant/components/tomorrowio/sensor.py index 371121a9da3..f3ca5302b2a 100644 --- a/homeassistant/components/tomorrowio/sensor.py +++ b/homeassistant/components/tomorrowio/sensor.py @@ -100,7 +100,7 @@ class TomorrowioSensorEntityDescription(SensorEntityDescription): # From https://cfpub.epa.gov/ncer_abstracts/index.cfm/fuseaction/display.files/fileID/14285 # x ug/m^3 = y ppb * molecular weight / 24.45 -def convert_ppb_to_ugm3(molecular_weight: int | float) -> Callable[[float], float]: +def convert_ppb_to_ugm3(molecular_weight: float) -> Callable[[float], float]: """Return function to convert ppb to ug/m^3.""" return lambda x: (x * molecular_weight) / 24.45 @@ -339,7 +339,7 @@ async def async_setup_entry( def handle_conversion( - value: float | int, conversion: Callable[[float], float] | float + value: float, conversion: Callable[[float], float] | float ) -> float: """Handle conversion of a value based on conversion type.""" if callable(conversion): diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index d007868930a..aa50c3f2ed2 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -228,7 +228,7 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity): await self.device.set_hsv(hue, sat, brightness, transition=transition) async def _async_set_color_temp( - self, color_temp: float | int, brightness: int | None, transition: int | None + self, color_temp: float, brightness: int | None, transition: int | None ) -> None: device = self.device valid_temperature_range = device.valid_temperature_range diff --git a/homeassistant/components/tuya/base.py b/homeassistant/components/tuya/base.py index 8ff7041fd5e..8b161f0538f 100644 --- a/homeassistant/components/tuya/base.py +++ b/homeassistant/components/tuya/base.py @@ -45,19 +45,19 @@ class IntegerTypeData: """Return the step scaled.""" return self.step / (10**self.scale) - def scale_value(self, value: float | int) -> float: + def scale_value(self, value: float) -> float: """Scale a value.""" return value / (10**self.scale) - def scale_value_back(self, value: float | int) -> int: + def scale_value_back(self, value: float) -> int: """Return raw value for scaled.""" return int(value * (10**self.scale)) def remap_value_to( self, value: float, - to_min: float | int = 0, - to_max: float | int = 255, + to_min: float = 0, + to_max: float = 255, reverse: bool = False, ) -> float: """Remap a value from this range to a new range.""" @@ -66,8 +66,8 @@ class IntegerTypeData: def remap_value_from( self, value: float, - from_min: float | int = 0, - from_max: float | int = 255, + from_min: float = 0, + from_max: float = 255, reverse: bool = False, ) -> float: """Remap a value from its current range to this range.""" diff --git a/homeassistant/components/tuya/util.py b/homeassistant/components/tuya/util.py index b6e6f17f49b..c1615b89c2d 100644 --- a/homeassistant/components/tuya/util.py +++ b/homeassistant/components/tuya/util.py @@ -4,11 +4,11 @@ from __future__ import annotations def remap_value( - value: float | int, - from_min: float | int = 0, - from_max: float | int = 255, - to_min: float | int = 0, - to_max: float | int = 255, + value: float, + from_min: float = 0, + from_max: float = 255, + to_min: float = 0, + to_max: float = 255, reverse: bool = False, ) -> float: """Remap a value from its current range, to a new range.""" diff --git a/homeassistant/components/weather/significant_change.py b/homeassistant/components/weather/significant_change.py index d36139904f5..ce7bcd15ede 100644 --- a/homeassistant/components/weather/significant_change.py +++ b/homeassistant/components/weather/significant_change.py @@ -64,7 +64,7 @@ VALID_CARDINAL_DIRECTIONS: list[str] = [ ] -def _cardinal_to_degrees(value: str | int | float | None) -> int | float | None: +def _cardinal_to_degrees(value: str | float | None) -> int | float | None: """Translate a cardinal direction into azimuth angle (degrees).""" if not isinstance(value, str): return value diff --git a/homeassistant/helpers/significant_change.py b/homeassistant/helpers/significant_change.py index 44b103e5c27..1b1f1b5c617 100644 --- a/homeassistant/helpers/significant_change.py +++ b/homeassistant/helpers/significant_change.py @@ -99,9 +99,9 @@ def either_one_none(val1: Any | None, val2: Any | None) -> bool: def _check_numeric_change( - old_state: int | float | None, - new_state: int | float | None, - change: int | float, + old_state: float | None, + new_state: float | None, + change: float, metric: Callable[[int | float, int | float], int | float], ) -> bool: """Check if two numeric values have changed.""" @@ -121,9 +121,9 @@ def _check_numeric_change( def check_absolute_change( - val1: int | float | None, - val2: int | float | None, - change: int | float, + val1: float | None, + val2: float | None, + change: float, ) -> bool: """Check if two numeric values have changed.""" return _check_numeric_change( @@ -132,13 +132,13 @@ def check_absolute_change( def check_percentage_change( - old_state: int | float | None, - new_state: int | float | None, - change: int | float, + old_state: float | None, + new_state: float | None, + change: float, ) -> bool: """Check if two numeric values have changed.""" - def percentage_change(old_state: int | float, new_state: int | float) -> float: + def percentage_change(old_state: float, new_state: float) -> float: if old_state == new_state: return 0 try: @@ -149,7 +149,7 @@ def check_percentage_change( return _check_numeric_change(old_state, new_state, change, percentage_change) -def check_valid_float(value: str | int | float) -> bool: +def check_valid_float(value: str | float) -> bool: """Check if given value is a valid float.""" try: float(value) diff --git a/pyproject.toml b/pyproject.toml index 0ab3fd23597..bda0ee4f85f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -722,7 +722,6 @@ ignore = [ # temporarily disabled "PT019", "PYI024", # Use typing.NamedTuple instead of collections.namedtuple - "PYI041", "RET503", "RET502", "RET501", diff --git a/tests/components/coolmaster/conftest.py b/tests/components/coolmaster/conftest.py index 7ddf1fd5942..15670af4bc8 100644 --- a/tests/components/coolmaster/conftest.py +++ b/tests/components/coolmaster/conftest.py @@ -64,7 +64,7 @@ class CoolMasterNetUnitMock: self._attributes["mode"] = value return CoolMasterNetUnitMock(self.unit_id, self._attributes) - async def set_thermostat(self, value: int | float) -> CoolMasterNetUnitMock: + async def set_thermostat(self, value: float) -> CoolMasterNetUnitMock: """Set the target temperature.""" self._attributes["thermostat"] = value return CoolMasterNetUnitMock(self.unit_id, self._attributes) diff --git a/tests/components/nibe_heatpump/__init__.py b/tests/components/nibe_heatpump/__init__.py index 1dbef2fe4f2..15cd9859d6e 100644 --- a/tests/components/nibe_heatpump/__init__.py +++ b/tests/components/nibe_heatpump/__init__.py @@ -50,7 +50,7 @@ class MockConnection(Connection): async def verify_connectivity(self): """Verify that we have functioning communication.""" - def mock_coil_update(self, coil_id: int, value: int | float | str | None): + def mock_coil_update(self, coil_id: int, value: float | str | None): """Trigger an out of band coil update.""" coil = self.heatpump.get_coil_by_address(coil_id) self.coils[coil_id] = value diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 0ecb4b9c60f..9e8e401ea46 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -2630,7 +2630,7 @@ async def test_suggested_unit_guard_valid_unit( native_unit: str, native_value: int, suggested_unit: str, - expect_value: float | int, + expect_value: float, ) -> None: """Test suggested_unit_of_measurement guard. diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 54fdf0368eb..87279ef707b 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -1176,7 +1176,7 @@ def test_as_datetime(hass: HomeAssistant, input) -> None: ) def test_as_datetime_from_timestamp( hass: HomeAssistant, - input: int | float, + input: float, output: str, ) -> None: """Test converting a UNIX timestamp to a date object.""" From 5ef42078a3462b718968842bbbb73a551c171b08 Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Mon, 8 Apr 2024 15:40:59 -0400 Subject: [PATCH 428/967] Add a service to get maps for Roborock (#111478) * add service to get rooms * fix linting * add snapshot * change map id --- homeassistant/components/roborock/__init__.py | 15 ++++++++--- homeassistant/components/roborock/const.py | 2 ++ .../components/roborock/coordinator.py | 27 ++++++++++++++++--- homeassistant/components/roborock/icons.json | 3 +++ homeassistant/components/roborock/image.py | 13 ++++++--- homeassistant/components/roborock/models.py | 9 +++++++ .../components/roborock/services.yaml | 4 +++ .../components/roborock/strings.json | 6 +++++ homeassistant/components/roborock/vacuum.py | 19 +++++++++++-- tests/components/roborock/conftest.py | 17 ++++++++++++ .../roborock/snapshots/test_vacuum.ambr | 27 +++++++++++++++++++ tests/components/roborock/test_vacuum.py | 19 +++++++++++++ 12 files changed, 147 insertions(+), 14 deletions(-) create mode 100644 homeassistant/components/roborock/services.yaml create mode 100644 tests/components/roborock/snapshots/test_vacuum.ambr diff --git a/homeassistant/components/roborock/__init__.py b/homeassistant/components/roborock/__init__.py index e64b83be1dd..141770e733d 100644 --- a/homeassistant/components/roborock/__init__.py +++ b/homeassistant/components/roborock/__init__.py @@ -8,7 +8,7 @@ from datetime import timedelta import logging from typing import Any -from roborock import RoborockException, RoborockInvalidCredentials +from roborock import HomeDataRoom, RoborockException, RoborockInvalidCredentials from roborock.cloud_api import RoborockMqttClient from roborock.containers import DeviceData, HomeDataDevice, HomeDataProduct, UserData from roborock.web_api import RoborockApiClient @@ -28,6 +28,7 @@ _LOGGER = logging.getLogger(__name__) 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()) user_data = UserData.from_dict(entry.data[CONF_USER_DATA]) @@ -56,7 +57,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: } # Get a Coordinator if the device is available or if we have connected to the device before coordinators = await asyncio.gather( - *build_setup_functions(hass, device_map, user_data, product_info), + *build_setup_functions( + hass, device_map, user_data, product_info, home_data.rooms + ), return_exceptions=True, ) # Valid coordinators are those where we had networking cached or we could get networking @@ -85,10 +88,13 @@ def build_setup_functions( device_map: dict[str, HomeDataDevice], user_data: UserData, product_info: dict[str, HomeDataProduct], + home_data_rooms: list[HomeDataRoom], ) -> list[Coroutine[Any, Any, RoborockDataUpdateCoordinator | 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]) + setup_device( + hass, user_data, device, product_info[device.product_id], home_data_rooms + ) for device in device_map.values() ] @@ -98,6 +104,7 @@ async def setup_device( user_data: UserData, device: HomeDataDevice, product_info: HomeDataProduct, + home_data_rooms: list[HomeDataRoom], ) -> RoborockDataUpdateCoordinator | None: """Set up a device Coordinator.""" mqtt_client = RoborockMqttClient(user_data, DeviceData(device, product_info.name)) @@ -117,7 +124,7 @@ async def setup_device( await mqtt_client.async_release() raise coordinator = RoborockDataUpdateCoordinator( - hass, device, networking, product_info, mqtt_client + hass, device, networking, product_info, mqtt_client, home_data_rooms ) # Verify we can communicate locally - if we can't, switch to cloud api await coordinator.verify_api() diff --git a/homeassistant/components/roborock/const.py b/homeassistant/components/roborock/const.py index 77f0be3363e..6b1ed975fca 100644 --- a/homeassistant/components/roborock/const.py +++ b/homeassistant/components/roborock/const.py @@ -30,3 +30,5 @@ IMAGE_DRAWABLES: list[Drawable] = [ IMAGE_CACHE_INTERVAL = 90 MAP_SLEEP = 3 + +GET_MAPS_SERVICE_NAME = "get_maps" diff --git a/homeassistant/components/roborock/coordinator.py b/homeassistant/components/roborock/coordinator.py index e682b119069..c5fd0c09c46 100644 --- a/homeassistant/components/roborock/coordinator.py +++ b/homeassistant/components/roborock/coordinator.py @@ -2,9 +2,11 @@ from __future__ import annotations +import asyncio from datetime import timedelta import logging +from roborock import HomeDataRoom from roborock.cloud_api import RoborockMqttClient from roborock.containers import DeviceData, HomeDataDevice, HomeDataProduct, NetworkInfo from roborock.exceptions import RoborockException @@ -18,7 +20,7 @@ from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN -from .models import RoborockHassDeviceInfo +from .models import RoborockHassDeviceInfo, RoborockMapInfo SCAN_INTERVAL = timedelta(seconds=30) @@ -35,6 +37,7 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]): device_networking: NetworkInfo, product_info: HomeDataProduct, cloud_api: RoborockMqttClient, + home_data_rooms: list[HomeDataRoom], ) -> None: """Initialize.""" super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL) @@ -61,7 +64,8 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]): if mac := self.roborock_device_info.network_info.mac: self.device_info[ATTR_CONNECTIONS] = {(dr.CONNECTION_NETWORK_MAC, mac)} # Maps from map flag to map name - self.maps: dict[int, str] = {} + self.maps: dict[int, RoborockMapInfo] = {} + self._home_data_rooms = {str(room.id): room.name for room in home_data_rooms} async def verify_api(self) -> None: """Verify that the api is reachable. If it is not, switch clients.""" @@ -95,7 +99,7 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]): async def _async_update_data(self) -> DeviceProp: """Update data via library.""" try: - await self._update_device_prop() + await asyncio.gather(*(self._update_device_prop(), self.get_rooms())) self._set_current_map() except RoborockException as ex: raise UpdateFailed(ex) from ex @@ -117,4 +121,19 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]): maps = await self.api.get_multi_maps_list() if maps and maps.map_info: for roborock_map in maps.map_info: - self.maps[roborock_map.mapFlag] = roborock_map.name + self.maps[roborock_map.mapFlag] = RoborockMapInfo( + flag=roborock_map.mapFlag, name=roborock_map.name, rooms={} + ) + + async def get_rooms(self) -> None: + """Get all of the rooms for the current map.""" + # The api is only able to access rooms for the currently selected map + # So it is important this is only called when you have the map you care + # about selected. + if self.current_map in self.maps: + iot_rooms = await self.api.get_room_mapping() + if iot_rooms is not None: + for room in iot_rooms: + self.maps[self.current_map].rooms[room.segment_id] = ( + self._home_data_rooms.get(room.iot_id, "Unknown") + ) diff --git a/homeassistant/components/roborock/icons.json b/homeassistant/components/roborock/icons.json index 43e7f185433..babde739775 100644 --- a/homeassistant/components/roborock/icons.json +++ b/homeassistant/components/roborock/icons.json @@ -105,5 +105,8 @@ "default": "mdi:power-plug-off" } } + }, + "services": { + "get_maps": "mdi:floor-plan" } } diff --git a/homeassistant/components/roborock/image.py b/homeassistant/components/roborock/image.py index 3367f1b3017..775ab98fd59 100644 --- a/homeassistant/components/roborock/image.py +++ b/homeassistant/components/roborock/image.py @@ -130,23 +130,27 @@ async def create_coordinator_maps( maps_info = sorted( coord.maps.items(), key=lambda data: data[0] == cur_map, reverse=True ) - for map_flag, map_name in maps_info: + for map_flag, map_info in maps_info: # Load the map - so we can access it with get_map_v1 if map_flag != cur_map: # Only change the map and sleep if we have multiple maps. await coord.api.send_command(RoborockCommand.LOAD_MULTI_MAP, [map_flag]) + coord.current_map = map_flag # We cannot get the map until the roborock servers fully process the # map change. await asyncio.sleep(MAP_SLEEP) # Get the map data - api_data: bytes = await coord.cloud_api.get_map_v1() + map_update = await asyncio.gather( + *[coord.cloud_api.get_map_v1(), coord.get_rooms()] + ) + api_data: bytes = map_update[0] entities.append( RoborockMap( - f"{slugify(coord.roborock_device_info.device.duid)}_map_{map_name}", + f"{slugify(coord.roborock_device_info.device.duid)}_map_{map_info.name}", coord, map_flag, api_data, - map_name, + map_info.name, ) ) if len(coord.maps) != 1: @@ -154,4 +158,5 @@ async def create_coordinator_maps( # does not change the end user's app. # Only needs to happen when we changed maps above. await coord.cloud_api.send_command(RoborockCommand.LOAD_MULTI_MAP, [cur_map]) + coord.current_map = cur_map return entities diff --git a/homeassistant/components/roborock/models.py b/homeassistant/components/roborock/models.py index 45b98fddbc5..b516c0ee05c 100644 --- a/homeassistant/components/roborock/models.py +++ b/homeassistant/components/roborock/models.py @@ -24,3 +24,12 @@ class RoborockHassDeviceInfo: "product": self.product.as_dict(), "props": self.props.as_dict(), } + + +@dataclass +class RoborockMapInfo: + """A model to describe all information about a map we may want.""" + + flag: int + name: str + rooms: dict[int, str] diff --git a/homeassistant/components/roborock/services.yaml b/homeassistant/components/roborock/services.yaml new file mode 100644 index 00000000000..18de5c98c7b --- /dev/null +++ b/homeassistant/components/roborock/services.yaml @@ -0,0 +1,4 @@ +get_maps: + target: + entity: + domain: vacuum diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index 7c457a1935b..30aa64f626a 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -293,5 +293,11 @@ "no_coordinators": { "message": "No devices were able to successfully setup" } + }, + "services": { + "get_maps": { + "name": "Get maps", + "description": "Get the map and room information of your device." + } } } diff --git a/homeassistant/components/roborock/vacuum.py b/homeassistant/components/roborock/vacuum.py index 22d9353e2a2..d8108abf78c 100644 --- a/homeassistant/components/roborock/vacuum.py +++ b/homeassistant/components/roborock/vacuum.py @@ -1,5 +1,6 @@ """Support for Roborock vacuum class.""" +from dataclasses import asdict from typing import Any from roborock.code_mappings import RoborockStateCode @@ -17,11 +18,12 @@ from homeassistant.components.vacuum import ( VacuumEntityFeature, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, ServiceResponse, SupportsResponse +from homeassistant.helpers import entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify -from .const import DOMAIN +from .const import DOMAIN, GET_MAPS_SERVICE_NAME from .coordinator import RoborockDataUpdateCoordinator from .device import RoborockCoordinatedEntity @@ -66,6 +68,15 @@ async def async_setup_entry( for device_id, coordinator in coordinators.items() ) + platform = entity_platform.async_get_current_platform() + + platform.async_register_entity_service( + GET_MAPS_SERVICE_NAME, + {}, + RoborockVacuum.get_maps.__name__, + supports_response=SupportsResponse.ONLY, + ) + class RoborockVacuum(RoborockCoordinatedEntity, StateVacuumEntity): """General Representation of a Roborock vacuum.""" @@ -164,3 +175,7 @@ class RoborockVacuum(RoborockCoordinatedEntity, StateVacuumEntity): ) -> None: """Send a command to a vacuum cleaner.""" await self.send(command, params) + + async def get_maps(self) -> ServiceResponse: + """Get map information such as map id and room ids.""" + return {"maps": [asdict(map) for map in self.coordinator.maps.values()]} diff --git a/tests/components/roborock/conftest.py b/tests/components/roborock/conftest.py index 91331a1486a..2910fa38995 100644 --- a/tests/components/roborock/conftest.py +++ b/tests/components/roborock/conftest.py @@ -3,6 +3,7 @@ from unittest.mock import patch import pytest +from roborock import RoomMapping from homeassistant.components.roborock.const import ( CONF_BASE_URL, @@ -74,6 +75,22 @@ def bypass_api_fixture() -> None: "homeassistant.components.roborock.image.MAP_SLEEP", 0, ), + patch( + "homeassistant.components.roborock.coordinator.RoborockLocalClient.get_room_mapping", + return_value=[ + RoomMapping(16, "2362048"), + RoomMapping(17, "2362044"), + RoomMapping(18, "2362041"), + ], + ), + patch( + "homeassistant.components.roborock.coordinator.RoborockMqttClient.get_room_mapping", + return_value=[ + RoomMapping(16, "2362048"), + RoomMapping(17, "2362044"), + RoomMapping(18, "2362041"), + ], + ), ): yield diff --git a/tests/components/roborock/snapshots/test_vacuum.ambr b/tests/components/roborock/snapshots/test_vacuum.ambr new file mode 100644 index 00000000000..d03bec28125 --- /dev/null +++ b/tests/components/roborock/snapshots/test_vacuum.ambr @@ -0,0 +1,27 @@ +# serializer version: 1 +# name: test_get_maps + dict({ + 'vacuum.roborock_s7_maxv': dict({ + 'maps': list([ + dict({ + 'flag': 0, + 'name': 'Upstairs', + 'rooms': dict({ + 16: 'Example room 1', + 17: 'Example room 2', + 18: 'Example room 3', + }), + }), + dict({ + 'flag': 1, + 'name': 'Downstairs', + 'rooms': dict({ + 16: 'Example room 1', + 17: 'Example room 2', + 18: 'Example room 3', + }), + }), + ]), + }), + }) +# --- diff --git a/tests/components/roborock/test_vacuum.py b/tests/components/roborock/test_vacuum.py index a3d5854edd1..cc01acc29fd 100644 --- a/tests/components/roborock/test_vacuum.py +++ b/tests/components/roborock/test_vacuum.py @@ -7,8 +7,10 @@ from unittest.mock import patch import pytest from roborock import RoborockException from roborock.roborock_typing import RoborockCommand +from syrupy.assertion import SnapshotAssertion from homeassistant.components.roborock import DOMAIN +from homeassistant.components.roborock.const import GET_MAPS_SERVICE_NAME from homeassistant.components.vacuum import ( SERVICE_CLEAN_SPOT, SERVICE_LOCATE, @@ -154,3 +156,20 @@ async def test_failed_user_command( data, blocking=True, ) + + +async def test_get_maps( + hass: HomeAssistant, + bypass_api_fixture, + setup_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test that the service for maps correctly outputs rooms with the right name.""" + response = await hass.services.async_call( + DOMAIN, + GET_MAPS_SERVICE_NAME, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + return_response=True, + ) + assert response == snapshot From ca5ed274cb7c3cf72719266e68ac0b27b268a65c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 8 Apr 2024 10:07:54 -1000 Subject: [PATCH 429/967] Deprecate calling async_listen and async_listen_once with run_immediately (#115169) --- .../components/alexa/state_report.py | 1 - homeassistant/components/api/__init__.py | 1 - homeassistant/components/apple_tv/__init__.py | 4 +- homeassistant/components/august/subscriber.py | 1 - .../components/automation/__init__.py | 1 - .../components/azure_event_hub/__init__.py | 2 +- .../components/bluetooth/__init__.py | 5 +- homeassistant/components/bluetooth/manager.py | 6 +-- .../bluetooth/passive_update_processor.py | 1 - homeassistant/components/bond/__init__.py | 4 +- homeassistant/components/camera/__init__.py | 8 +-- homeassistant/components/cloud/__init__.py | 4 +- .../components/conversation/default_agent.py | 4 -- .../components/device_tracker/config_entry.py | 4 +- .../components/device_tracker/legacy.py | 4 +- homeassistant/components/dhcp/__init__.py | 8 +-- homeassistant/components/esphome/dashboard.py | 2 +- homeassistant/components/esphome/manager.py | 5 +- .../google_assistant/report_state.py | 1 - homeassistant/components/hassio/__init__.py | 2 +- .../homeassistant/triggers/event.py | 4 +- .../homeassistant_alerts/__init__.py | 4 +- homeassistant/components/homekit/__init__.py | 4 +- .../components/homekit_controller/__init__.py | 4 +- .../homekit_controller/connection.py | 1 - .../components/homekit_controller/utils.py | 4 +- homeassistant/components/isy994/__init__.py | 4 +- homeassistant/components/logbook/helpers.py | 4 +- homeassistant/components/matrix/__init__.py | 4 +- homeassistant/components/person/__init__.py | 1 - homeassistant/components/recorder/core.py | 9 +--- .../components/recorder/entity_registry.py | 1 - homeassistant/components/rfxtrx/__init__.py | 4 +- .../components/samsungtv/__init__.py | 4 +- .../components/shelly/coordinator.py | 8 +-- homeassistant/components/sonos/__init__.py | 2 - homeassistant/components/ssdp/__init__.py | 9 +--- homeassistant/components/stream/__init__.py | 4 +- .../components/template/coordinator.py | 2 +- homeassistant/components/trace/__init__.py | 4 +- .../components/unifiprotect/__init__.py | 4 +- homeassistant/components/usb/__init__.py | 12 ++--- .../components/websocket_api/commands.py | 3 +- .../components/websocket_api/http.py | 2 +- homeassistant/components/wemo/__init__.py | 4 +- .../components/yalexs_ble/__init__.py | 4 +- homeassistant/components/zeroconf/__init__.py | 8 +-- homeassistant/components/zone/__init__.py | 4 +- homeassistant/config_entries.py | 5 +- homeassistant/core.py | 50 +++++++++++-------- homeassistant/helpers/area_registry.py | 2 - homeassistant/helpers/device_registry.py | 11 +--- homeassistant/helpers/discovery_flow.py | 4 +- homeassistant/helpers/entity_component.py | 4 +- homeassistant/helpers/entity_registry.py | 12 +---- homeassistant/helpers/event.py | 2 - homeassistant/helpers/integration_platform.py | 1 - homeassistant/helpers/restore_state.py | 2 +- homeassistant/helpers/start.py | 2 +- homeassistant/helpers/storage.py | 3 -- homeassistant/helpers/template.py | 8 +-- homeassistant/helpers/update_coordinator.py | 2 +- homeassistant/setup.py | 5 +- tests/common.py | 11 ++-- tests/components/automation/test_init.py | 4 +- tests/test_core.py | 49 ++++++++++++------ tests/test_data_entry_flow.py | 2 - 67 files changed, 126 insertions(+), 243 deletions(-) diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index 978879b5d88..24d750e7cb7 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -342,7 +342,6 @@ async def async_enable_proactive_mode( EVENT_STATE_CHANGED, _async_entity_state_listener, event_filter=_async_entity_state_filter, - run_immediately=True, ) diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py index 489475e9dd7..496b6fa5fb1 100644 --- a/homeassistant/components/api/__init__.py +++ b/homeassistant/components/api/__init__.py @@ -398,7 +398,6 @@ class APIDomainServicesView(HomeAssistantView): cancel_listen = hass.bus.async_listen( EVENT_STATE_CHANGED, _async_save_changed_entities, - run_immediately=True, ) try: diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index 9a72a89c876..cd1a1c59127 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -102,9 +102,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await manager.disconnect() entry.async_on_unload( - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, on_hass_stop, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/august/subscriber.py b/homeassistant/components/august/subscriber.py index 9332080d9ad..7294f8bc90f 100644 --- a/homeassistant/components/august/subscriber.py +++ b/homeassistant/components/august/subscriber.py @@ -71,7 +71,6 @@ class AugustSubscriberMixin: self._stop_interval = self._hass.bus.async_listen( EVENT_HOMEASSISTANT_STOP, self._async_cancel_update_interval, - run_immediately=True, ) @callback diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index ff0a5fc5193..fa05791b9c9 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -782,7 +782,6 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity): self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STARTED, self._async_enable_automation, - run_immediately=True, ) self.async_write_ha_state() diff --git a/homeassistant/components/azure_event_hub/__init__.py b/homeassistant/components/azure_event_hub/__init__.py index 0a84ca44141..668444f9990 100644 --- a/homeassistant/components/azure_event_hub/__init__.py +++ b/homeassistant/components/azure_event_hub/__init__.py @@ -158,7 +158,7 @@ class AzureEventHub: """ logging.getLogger("azure.eventhub").setLevel(logging.WARNING) self._listener_remover = self.hass.bus.async_listen( - MATCH_ALL, self.async_listen, run_immediately=True + MATCH_ALL, self.async_listen ) self._schedule_next_send() diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 1e091ec32cc..3273080d88b 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -166,9 +166,7 @@ async def _async_start_adapter_discovery( """Shutdown debouncer.""" discovery_debouncer.async_shutdown() - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, _async_shutdown_debouncer, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_shutdown_debouncer) async def _async_call_debouncer(now: datetime.datetime) -> None: """Call the debouncer at a later time.""" @@ -201,7 +199,6 @@ async def _async_start_adapter_discovery( hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, hass_callback(lambda event: cancel()), - run_immediately=True, ) diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index 3a240e9f01e..2eb07c5133f 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -135,11 +135,9 @@ class HomeAssistantBluetoothManager(BluetoothManager): self._bluetooth_adapters, self.storage ) self._cancel_logging_listener = self.hass.bus.async_listen( - EVENT_LOGGING_CHANGED, self._async_logging_changed, run_immediately=True - ) - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, self.async_stop, run_immediately=True + EVENT_LOGGING_CHANGED, self._async_logging_changed ) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop) seen: set[str] = set() for address, service_info in itertools.chain( self._connectable_history.items(), self._all_history.items() diff --git a/homeassistant/components/bluetooth/passive_update_processor.py b/homeassistant/components/bluetooth/passive_update_processor.py index b3bf3adf93c..1d1078633fe 100644 --- a/homeassistant/components/bluetooth/passive_update_processor.py +++ b/homeassistant/components/bluetooth/passive_update_processor.py @@ -274,7 +274,6 @@ async def async_setup(hass: HomeAssistant) -> None: hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, _async_save_processor_data_at_stop, - run_immediately=True, ) diff --git a/homeassistant/components/bond/__init__.py b/homeassistant/components/bond/__init__.py index 216a4b501f2..9ecfedee570 100644 --- a/homeassistant/components/bond/__init__.py +++ b/homeassistant/components/bond/__init__.py @@ -68,9 +68,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.async_on_unload(_async_stop_event) entry.async_on_unload( - hass.bus.async_listen( - EVENT_HOMEASSISTANT_STOP, _async_stop_event, run_immediately=True - ) + hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, _async_stop_event) ) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = BondData(hub, bpup_subs) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index a81711f2793..2430ccebb4f 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -412,9 +412,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: stream.add_provider("hls") await stream.start() - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STARTED, preload_stream, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, preload_stream) @callback def update_tokens(t: datetime) -> None: @@ -432,9 +430,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Unsubscribe track time interval timer.""" unsub() - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, unsub_track_time_interval, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, unsub_track_time_interval) component.async_register_entity_service( SERVICE_ENABLE_MOTION, {}, "async_enable_motion_detection" diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index d85415cf9eb..80f9d9f9368 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -262,9 +262,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Shutdown event.""" await cloud.stop() - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, _shutdown, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown) _remote_handle_prefs_updated(cloud) diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 57ac5e2cc58..121702115b9 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -197,24 +197,20 @@ class DefaultAgent(ConversationEntity): self.hass.bus.async_listen( ar.EVENT_AREA_REGISTRY_UPDATED, self._async_clear_slot_list, - run_immediately=True, ), self.hass.bus.async_listen( fr.EVENT_FLOOR_REGISTRY_UPDATED, self._async_clear_slot_list, - run_immediately=True, ), self.hass.bus.async_listen( er.EVENT_ENTITY_REGISTRY_UPDATED, self._async_clear_slot_list, event_filter=self._filter_entity_registry_changes, - run_immediately=True, ), self.hass.bus.async_listen( EVENT_STATE_CHANGED, self._async_clear_slot_list, event_filter=self._filter_state_changes, - run_immediately=True, ), async_listen_entity_updates(self.hass, DOMAIN, self._async_clear_slot_list), ] diff --git a/homeassistant/components/device_tracker/config_entry.py b/homeassistant/components/device_tracker/config_entry.py index 33c753c41e1..0372dff3a86 100644 --- a/homeassistant/components/device_tracker/config_entry.py +++ b/homeassistant/components/device_tracker/config_entry.py @@ -159,9 +159,7 @@ def _async_register_mac( # Enable entity ent_reg.async_update_entity(entity_id, disabled_by=None) - hass.bus.async_listen( - dr.EVENT_DEVICE_REGISTRY_UPDATED, handle_device_event, run_immediately=True - ) + hass.bus.async_listen(dr.EVENT_DEVICE_REGISTRY_UPDATED, handle_device_event) class BaseTrackerEntity(Entity): diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index e292e97a8ec..dfeed98f320 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -281,9 +281,7 @@ async def _async_setup_integration( """ cancel_update_stale() - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, _on_hass_stop, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _on_hass_stop) @attr.s diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index 40cc0c02c84..b4d06b6e276 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -162,13 +162,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: for watcher in watchers: watcher.async_stop() - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, _async_stop, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop) - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STARTED, _async_initialize, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _async_initialize) return True diff --git a/homeassistant/components/esphome/dashboard.py b/homeassistant/components/esphome/dashboard.py index b8a72ac4398..54a593fe0cc 100644 --- a/homeassistant/components/esphome/dashboard.py +++ b/homeassistant/components/esphome/dashboard.py @@ -103,7 +103,7 @@ class ESPHomeDashboardManager: await dashboard.async_shutdown() self._cancel_shutdown = hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, on_hass_stop, run_immediately=True + EVENT_HOMEASSISTANT_STOP, on_hass_stop ) new_data = {"info": {"addon_slug": addon_slug, "host": host, "port": port}} diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index ad24a68103d..3813d22ce97 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -550,15 +550,12 @@ class ESPHomeManager: # 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, run_immediately=True - ) + 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, - run_immediately=True, ) ) diff --git a/homeassistant/components/google_assistant/report_state.py b/homeassistant/components/google_assistant/report_state.py index 1230b9a272e..7fbe4bab5a9 100644 --- a/homeassistant/components/google_assistant/report_state.py +++ b/homeassistant/components/google_assistant/report_state.py @@ -190,7 +190,6 @@ def async_enable_report_state( EVENT_STATE_CHANGED, _async_entity_state_listener, event_filter=_async_entity_state_filter, - run_immediately=True, ) unsub = async_call_later( diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 8f648bd006b..ce3b5b05ffe 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -386,7 +386,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: last_timezone = new_timezone await hassio.update_hass_timezone(new_timezone) - hass.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, push_config, run_immediately=True) + hass.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, push_config) push_config_task = hass.async_create_task(push_config(None), eager_start=True) # Start listening for problems with supervisor and making issues diff --git a/homeassistant/components/homeassistant/triggers/event.py b/homeassistant/components/homeassistant/triggers/event.py index 85bd2708d5e..d29baf342ab 100644 --- a/homeassistant/components/homeassistant/triggers/event.py +++ b/homeassistant/components/homeassistant/triggers/event.py @@ -171,9 +171,7 @@ async def async_attach_trigger( event_filter = filter_event if event_data_items or event_data_schema else None removes = [ - hass.bus.async_listen( - event_type, handle_event, event_filter=event_filter, run_immediately=True - ) + hass.bus.async_listen(event_type, handle_event, event_filter=event_filter) for event_type in event_types ] diff --git a/homeassistant/components/homeassistant_alerts/__init__.py b/homeassistant/components/homeassistant_alerts/__init__.py index b0eefad053e..7dcd9f8db97 100644 --- a/homeassistant/components/homeassistant_alerts/__init__.py +++ b/homeassistant/components/homeassistant_alerts/__init__.py @@ -104,9 +104,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: refresh_debouncer.async_schedule_call() await coordinator.async_refresh() - hass.bus.async_listen( - EVENT_COMPONENT_LOADED, _component_loaded, run_immediately=True - ) + hass.bus.async_listen(EVENT_COMPONENT_LOADED, _component_loaded) async_at_start(hass, initial_refresh) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 1b9ccaca8bf..f9f91ec162b 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -351,9 +351,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.async_on_unload(entry.add_update_listener(_async_update_listener)) entry.async_on_unload( - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, homekit.async_stop, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, homekit.async_stop) ) entry_data = HomeKitEntryData( diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 218094ddaf5..639cec6dcb5 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -86,9 +86,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) ) - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, _async_stop_homekit_controller, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop_homekit_controller) return True diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 7b3f8e45845..78beb7bfffa 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -286,7 +286,6 @@ class HKDevice: self.hass.bus.async_listen( EVENT_HOMEASSISTANT_STARTED, self._async_populate_ble_accessory_state, - run_immediately=True, ) ) else: diff --git a/homeassistant/components/homekit_controller/utils.py b/homeassistant/components/homekit_controller/utils.py index 40879a533f4..2f94f5bac92 100644 --- a/homeassistant/components/homekit_controller/utils.py +++ b/homeassistant/components/homekit_controller/utils.py @@ -77,9 +77,7 @@ async def async_get_controller(hass: HomeAssistant) -> Controller: # Right now _async_stop_homekit_controller is only called on HA exiting # So we don't have to worry about leaking a callback here. - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, _async_stop_homekit_controller, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop_homekit_controller) await controller.async_start() diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index db72cc45a30..0c238182849 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -166,9 +166,7 @@ async def async_setup_entry( entry.async_on_unload(entry.add_update_listener(_async_update_listener)) entry.async_on_unload( - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, _async_stop_auto_update, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop_auto_update) ) # Register Integration-wide Services: diff --git a/homeassistant/components/logbook/helpers.py b/homeassistant/components/logbook/helpers.py index 8ec953a0afd..4fa0da9033a 100644 --- a/homeassistant/components/logbook/helpers.py +++ b/homeassistant/components/logbook/helpers.py @@ -176,8 +176,7 @@ def async_subscribe_events( target, entities_filter, entity_ids, device_ids ) subscriptions.extend( - hass.bus.async_listen(event_type, event_forwarder, run_immediately=True) - for event_type in event_types + hass.bus.async_listen(event_type, event_forwarder) for event_type in event_types ) if device_ids and not entity_ids: @@ -211,7 +210,6 @@ def async_subscribe_events( hass.bus.async_listen( EVENT_STATE_CHANGED, _forward_state_events_filtered, - run_immediately=True, ) ) diff --git a/homeassistant/components/matrix/__init__.py b/homeassistant/components/matrix/__init__.py index 98653ba19ad..b8f1ec08fe0 100644 --- a/homeassistant/components/matrix/__init__.py +++ b/homeassistant/components/matrix/__init__.py @@ -219,9 +219,7 @@ class MatrixBot: loop_sleep_time=1_000, ) # milliseconds. - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, handle_startup, run_immediately=True - ) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, handle_startup) def _load_commands(self, commands: list[ConfigCommand]) -> None: for command in commands: diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index 87b158f80c3..4f86654a7d3 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -241,7 +241,6 @@ class PersonStorageCollection(collection.DictStorageCollection): er.EVENT_ENTITY_REGISTRY_UPDATED, self._entity_registry_updated, event_filter=self._entity_registry_filter, - run_immediately=True, ) @callback diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 1780436168d..cc412c88612 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -333,7 +333,6 @@ class Recorder(threading.Thread): self._event_listener = self.hass.bus.async_listen( MATCH_ALL, _event_listener, - run_immediately=True, ) self._queue_watcher = async_track_time_interval( self.hass, @@ -478,12 +477,8 @@ class Recorder(threading.Thread): def async_register(self) -> None: """Post connection initialize.""" bus = self.hass.bus - bus.async_listen_once( - EVENT_HOMEASSISTANT_CLOSE, self._async_close, run_immediately=True - ) - bus.async_listen_once( - EVENT_HOMEASSISTANT_FINAL_WRITE, self._async_shutdown, run_immediately=True - ) + bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, self._async_close) + bus.async_listen_once(EVENT_HOMEASSISTANT_FINAL_WRITE, self._async_shutdown) async_at_started(self.hass, self._async_hass_started) @callback diff --git a/homeassistant/components/recorder/entity_registry.py b/homeassistant/components/recorder/entity_registry.py index 1c0299fc8da..07f8f2f88de 100644 --- a/homeassistant/components/recorder/entity_registry.py +++ b/homeassistant/components/recorder/entity_registry.py @@ -47,7 +47,6 @@ def async_setup(hass: HomeAssistant) -> None: er.EVENT_ENTITY_REGISTRY_UPDATED, _async_entity_id_changed, event_filter=entity_registry_changed_filter, - run_immediately=True, ) async_at_start(hass, _setup_entity_registry_event_handler) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 78b7daa8347..6f0e5932adc 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -280,9 +280,7 @@ async def async_setup_internal(hass: HomeAssistant, entry: ConfigEntry) -> None: hass.data[DOMAIN][DATA_RFXOBJECT] = rfx_object entry.async_on_unload( - hass.bus.async_listen( - dr.EVENT_DEVICE_REGISTRY_UPDATED, _updated_device, run_immediately=True - ) + hass.bus.async_listen(dr.EVENT_DEVICE_REGISTRY_UPDATED, _updated_device) ) def _shutdown_rfxtrx(event: Event) -> None: diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index 68a58710c19..9dcb2f9f57e 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -149,9 +149,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await bridge.async_close_remote() entry.async_on_unload( - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, stop_bridge, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_bridge) ) await _async_update_ssdp_locations(hass, entry) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index c52585c3363..18f96dd9c2e 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -189,9 +189,7 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): self.async_add_listener(self._async_device_updates_handler) ) entry.async_on_unload( - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop) ) @callback @@ -420,9 +418,7 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): self._input_event_listeners: list[Callable[[dict[str, Any]], None]] = [] entry.async_on_unload( - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop) ) entry.async_on_unload(entry.add_update_listener(self._async_update_listener)) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 0eab8dcc779..2049cb4c8c7 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -577,7 +577,6 @@ class SonosDiscoveryManager: self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, self._async_stop_event_listener, - run_immediately=True, ) ) _LOGGER.debug("Adding discovery job") @@ -586,7 +585,6 @@ class SonosDiscoveryManager: self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, self._stop_manual_heartbeat, - run_immediately=True, ) ) await self.async_poll_manual_hosts() diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index 08d1bbb858e..1678daf4059 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -392,9 +392,7 @@ class Scanner: await self._async_start_ssdp_listeners() - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, self.async_stop, run_immediately=True - ) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop) self._cancel_scan = async_track_time_interval( self.hass, self.async_scan, SCAN_INTERVAL, name="SSDP scanner" ) @@ -755,13 +753,10 @@ class Server: async def async_start(self) -> None: """Start the server.""" bus = self.hass.bus - bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, self.async_stop, run_immediately=True - ) + bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop) bus.async_listen_once( EVENT_HOMEASSISTANT_STARTED, self._async_start_upnp_servers, - run_immediately=True, ) async def _async_get_instance_udn(self) -> str: diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 44cf9177993..64c520150c2 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -214,7 +214,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # Only pass through PyAV log messages if stream logging is above DEBUG cancel_logging_listener = hass.bus.async_listen( - EVENT_LOGGING_CHANGED, update_pyav_logging, run_immediately=True + EVENT_LOGGING_CHANGED, update_pyav_logging ) # libav.mp4 and libav.swscaler have a few unimportant messages that are logged # at logging.WARNING. Set those Logger levels to logging.ERROR @@ -266,7 +266,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: _LOGGER.debug("Stopped stream workers") cancel_logging_listener() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown, run_immediately=True) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown) return True diff --git a/homeassistant/components/template/coordinator.py b/homeassistant/components/template/coordinator.py index 47de31d07c2..d2ce44a0ad1 100644 --- a/homeassistant/components/template/coordinator.py +++ b/homeassistant/components/template/coordinator.py @@ -47,7 +47,7 @@ class TriggerUpdateCoordinator(DataUpdateCoordinator): await self._attach_triggers() else: self._unsub_start = self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, self._attach_triggers, run_immediately=True + EVENT_HOMEASSISTANT_START, self._attach_triggers ) for platform_domain in PLATFORMS: diff --git a/homeassistant/components/trace/__init__.py b/homeassistant/components/trace/__init__.py index 17fdf20368a..03b1845d6a8 100644 --- a/homeassistant/components/trace/__init__.py +++ b/homeassistant/components/trace/__init__.py @@ -68,9 +68,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: _LOGGER.error("Error storing traces", exc_info=exc) # Store traces when stopping hass - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, _async_store_traces_at_stop, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_store_traces_at_stop) return True diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index 5943cc4f85e..d85f91be860 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -110,9 +110,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = 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, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, data_service.async_stop) ) if not entry.options.get(CONF_ALLOW_EA, False) and ( diff --git a/homeassistant/components/usb/__init__.py b/homeassistant/components/usb/__init__.py index 48697c98ae7..959a8f5894c 100644 --- a/homeassistant/components/usb/__init__.py +++ b/homeassistant/components/usb/__init__.py @@ -207,12 +207,8 @@ class USBDiscovery: async def async_setup(self) -> None: """Set up USB Discovery.""" await self._async_start_monitor() - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STARTED, self.async_start, run_immediately=True - ) - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, self.async_stop, run_immediately=True - ) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, self.async_start) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop) async def async_start(self, event: Event) -> None: """Start USB Discovery and run a manual scan.""" @@ -242,9 +238,7 @@ class USBDiscovery: def _stop_observer(event: Event) -> None: observer.stop() - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, _stop_observer, run_immediately=True - ) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_observer) self.observer_active = True def _get_monitor_observer(self) -> MonitorObserver | None: diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 7f30c08c076..54539158148 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -165,7 +165,7 @@ def handle_subscribe_events( ) connection.subscriptions[msg["id"]] = hass.bus.async_listen( - event_type, forward_events, run_immediately=True + event_type, forward_events ) connection.send_result(msg["id"]) @@ -410,7 +410,6 @@ def handle_subscribe_entities( connection.user, msg["id"], ), - run_immediately=True, ) connection.send_result(msg["id"]) diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index 83d68ee21ea..fc75b46ddbd 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -292,7 +292,7 @@ class WebSocketHandler: self._handle_task = asyncio.current_task() unsub_stop = hass.bus.async_listen( - EVENT_HOMEASSISTANT_STOP, self._async_handle_hass_stop, run_immediately=True + EVENT_HOMEASSISTANT_STOP, self._async_handle_hass_stop ) writer = wsock._writer # pylint: disable=protected-access diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index 4eb33a09553..7d068cbd5bf 100644 --- a/homeassistant/components/wemo/__init__.py +++ b/homeassistant/components/wemo/__init__.py @@ -96,9 +96,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: discovery_responder.stop() registry.stop() - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, _on_hass_stop, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _on_hass_stop) yaml_config = config.get(DOMAIN, {}) hass.data[DOMAIN] = WemoData( diff --git a/homeassistant/components/yalexs_ble/__init__.py b/homeassistant/components/yalexs_ble/__init__.py index f608da6cd60..8c9c5176003 100644 --- a/homeassistant/components/yalexs_ble/__init__.py +++ b/homeassistant/components/yalexs_ble/__init__.py @@ -127,9 +127,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(_async_update_listener)) entry.async_on_unload( - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, _async_shutdown, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_shutdown) ) return True diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 5ebe1bb769e..7b4c06ffb62 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -169,9 +169,7 @@ async def _async_get_instance(hass: HomeAssistant, **zcargs: Any) -> HaAsyncZero # Wait to the close event to shutdown zeroconf to give # integrations time to send a good bye message - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_CLOSE, _async_stop_zeroconf, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, _async_stop_zeroconf) hass.data[DOMAIN] = aio_zc return aio_zc @@ -248,9 +246,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def _async_zeroconf_hass_stop(_event: Event) -> None: await discovery.async_stop() - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, _async_zeroconf_hass_stop, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_zeroconf_hass_stop) async_when_setup_or_start(hass, "frontend", _async_zeroconf_hass_start) return True diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index eaee24376c7..2473200102d 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -288,9 +288,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Handle core config updated.""" await home_zone.async_update_config(_home_conf(hass)) - hass.bus.async_listen( - EVENT_CORE_CONFIG_UPDATE, core_config_updated, run_immediately=True - ) + hass.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, core_config_updated) hass.data[DOMAIN] = storage_collection diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index d7ffd697a2f..dd48c53160e 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -636,7 +636,6 @@ class ConfigEntry: self._async_cancel_retry_setup = hass.bus.async_listen( EVENT_HOMEASSISTANT_STARTED, functools.partial(self._async_setup_again, hass), - run_immediately=True, ) await self._async_process_on_unload(hass) @@ -1646,9 +1645,7 @@ class ConfigEntries: old_conf_migrate_func=_old_conf_migrator, ) - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, self._async_shutdown, run_immediately=True - ) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._async_shutdown) if config is None: self._entries = ConfigEntryItems(self.hass) diff --git a/homeassistant/core.py b/homeassistant/core.py index 113bbf7bf77..0cd7c29cf52 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -136,6 +136,7 @@ _P = ParamSpec("_P") _Ts = TypeVarTuple("_Ts") # Internal; not helpers.typing.UNDEFINED due to circular dependency _UNDEF: dict[Any, Any] = {} +_SENTINEL = object() _CallableT = TypeVar("_CallableT", bound=Callable[..., Any]) _DataT = TypeVar("_DataT", bound=Mapping[str, Any], default=Mapping[str, Any]) CALLBACK_TYPE = Callable[[], None] @@ -1355,7 +1356,6 @@ def _event_repr( _FilterableJobType = tuple[ HassJob[[Event[_DataT]], Coroutine[Any, Any, None] | None], # job Callable[[_DataT], bool] | None, # event_filter - bool, # run_immediately ] @@ -1399,9 +1399,7 @@ class EventBus: self._listeners[MATCH_ALL] = self._match_all_listeners self._hass = hass self._async_logging_changed() - self.async_listen( - EVENT_LOGGING_CHANGED, self._async_logging_changed, run_immediately=True - ) + self.async_listen(EVENT_LOGGING_CHANGED, self._async_logging_changed) @callback def _async_logging_changed(self, event: Event | None = None) -> None: @@ -1486,7 +1484,7 @@ class EventBus: event: Event[_DataT] | None = None - for job, event_filter, run_immediately in listeners: + for job, event_filter in listeners: if event_filter is not None: try: if event_data is None or not event_filter(event_data): @@ -1504,14 +1502,10 @@ class EventBus: context, ) - if run_immediately: - try: - self._hass.async_run_hass_job(job, event) - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Error running job: %s", job) - else: - # pylint: disable-next=protected-access - self._hass._async_add_hass_job(job, event) + try: + self._hass.async_run_hass_job(job, event) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error running job: %s", job) def listen( self, @@ -1539,7 +1533,7 @@ class EventBus: event_type: EventType[_DataT] | str, listener: Callable[[Event[_DataT]], Coroutine[Any, Any, None] | None], event_filter: Callable[[_DataT], bool] | None = None, - run_immediately: bool = True, + run_immediately: bool | object = _SENTINEL, ) -> CALLBACK_TYPE: """Listen for all events or events of a specific type. @@ -1556,6 +1550,16 @@ class EventBus: This method must be run in the event loop. """ + if run_immediately in (True, False): + # late import to avoid circular imports + from .helpers import frame # pylint: disable=import-outside-toplevel + + frame.report( + "calls `async_listen` with run_immediately, which is" + " deprecated and will be removed in Assistant 2025.5", + error_if_core=False, + ) + if event_filter is not None and not is_callback_check_partial(event_filter): raise HomeAssistantError(f"Event filter {event_filter} is not a callback") if event_type == EVENT_STATE_REPORTED: @@ -1563,16 +1567,11 @@ class EventBus: raise HomeAssistantError( f"Event filter is required for event {event_type}" ) - if not run_immediately: - raise HomeAssistantError( - f"Run immediately must be set to True for event {event_type}" - ) return self._async_listen_filterable_job( event_type, ( HassJob(listener, f"listen {event_type}"), event_filter, - run_immediately, ), ) @@ -1614,7 +1613,7 @@ class EventBus: self, event_type: EventType[_DataT] | str, listener: Callable[[Event[_DataT]], Coroutine[Any, Any, None] | None], - run_immediately: bool = True, + run_immediately: bool | object = _SENTINEL, ) -> CALLBACK_TYPE: """Listen once for event of a specific type. @@ -1625,6 +1624,16 @@ class EventBus: This method must be run in the event loop. """ + if run_immediately in (True, False): + # late import to avoid circular imports + from .helpers import frame # pylint: disable=import-outside-toplevel + + frame.report( + "calls `async_listen_once` with run_immediately, which is " + "deprecated and will be removed in Assistant 2025.5", + error_if_core=False, + ) + one_time_listener: _OneTimeListener[_DataT] = _OneTimeListener( self._hass, HassJob(listener) ) @@ -1637,7 +1646,6 @@ class EventBus: job_type=HassJobType.Callback, ), None, - run_immediately, ), ) one_time_listener.remove = remove diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py index 1384735e2fd..2734ab5e2e5 100644 --- a/homeassistant/helpers/area_registry.py +++ b/homeassistant/helpers/area_registry.py @@ -393,7 +393,6 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]): event_type=fr.EVENT_FLOOR_REGISTRY_UPDATED, event_filter=_removed_from_registry_filter, listener=_handle_floor_registry_update, - run_immediately=True, ) @callback @@ -410,7 +409,6 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]): event_type=lr.EVENT_LABEL_REGISTRY_UPDATED, event_filter=_removed_from_registry_filter, listener=_handle_label_registry_update, - run_immediately=True, ) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 2142cae854a..0270c8dc456 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -1207,7 +1207,6 @@ def async_setup_cleanup(hass: HomeAssistant, dev_reg: DeviceRegistry) -> None: event_type=lr.EVENT_LABEL_REGISTRY_UPDATED, event_filter=_label_removed_from_registry_filter, listener=_handle_label_registry_update, - run_immediately=True, ) @callback @@ -1245,7 +1244,6 @@ def async_setup_cleanup(hass: HomeAssistant, dev_reg: DeviceRegistry) -> None: entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, _async_entity_registry_changed, event_filter=entity_registry_changed_filter, - run_immediately=True, ) return @@ -1255,22 +1253,17 @@ def async_setup_cleanup(hass: HomeAssistant, dev_reg: DeviceRegistry) -> None: entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, _async_entity_registry_changed, event_filter=entity_registry_changed_filter, - run_immediately=True, ) await debounced_cleanup.async_call() - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STARTED, startup_clean, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, startup_clean) @callback def _on_homeassistant_stop(event: Event) -> None: """Cancel debounced cleanup.""" debounced_cleanup.async_cancel() - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, _on_homeassistant_stop, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _on_homeassistant_stop) def _normalize_connections(connections: set[tuple[str, str]]) -> set[tuple[str, str]]: diff --git a/homeassistant/helpers/discovery_flow.py b/homeassistant/helpers/discovery_flow.py index e24b405c685..b6633a3f718 100644 --- a/homeassistant/helpers/discovery_flow.py +++ b/homeassistant/helpers/discovery_flow.py @@ -82,9 +82,7 @@ class FlowDispatcher: @callback def async_setup(self) -> None: """Set up the flow disptcher.""" - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STARTED, self._async_start, run_immediately=True - ) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, self._async_start) async def _async_start(self, event: Event) -> None: """Start processing pending flows.""" diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index b764a29a686..f467b5683a9 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -120,9 +120,7 @@ class EntityComponent(Generic[_EntityT]): Note: this is only required if the integration never calls `setup` or `async_setup`. """ - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, self._async_shutdown, run_immediately=True - ) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._async_shutdown) def setup(self, config: ConfigType) -> None: """Set up a full entity component. diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 727dbda9c2d..d6e7395a2cb 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -683,7 +683,6 @@ class EntityRegistry(BaseRegistry): self.hass.bus.async_listen( EVENT_DEVICE_REGISTRY_UPDATED, self.async_device_modified, - run_immediately=True, ) @callback @@ -1492,7 +1491,6 @@ def _async_setup_cleanup(hass: HomeAssistant, registry: EntityRegistry) -> None: event_type=lr.EVENT_LABEL_REGISTRY_UPDATED, event_filter=_removed_from_registry_filter, listener=_handle_label_registry_update, - run_immediately=True, ) @callback @@ -1506,7 +1504,6 @@ def _async_setup_cleanup(hass: HomeAssistant, registry: EntityRegistry) -> None: event_type=cr.EVENT_CATEGORY_REGISTRY_UPDATED, event_filter=_removed_from_registry_filter, listener=_handle_category_registry_update, - run_immediately=True, ) @callback @@ -1525,9 +1522,7 @@ def _async_setup_cleanup(hass: HomeAssistant, registry: EntityRegistry) -> None: """Cancel cleanup.""" cancel() - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, _on_homeassistant_stop, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _on_homeassistant_stop) @callback @@ -1553,7 +1548,6 @@ def _async_setup_entity_restore(hass: HomeAssistant, registry: EntityRegistry) - EVENT_ENTITY_REGISTRY_UPDATED, cleanup_restored_states, event_filter=cleanup_restored_states_filter, - run_immediately=True, ) if hass.is_running: @@ -1570,9 +1564,7 @@ def _async_setup_entity_restore(hass: HomeAssistant, registry: EntityRegistry) - entry.write_unavailable_state(hass) - hass.bus.async_listen( - EVENT_HOMEASSISTANT_START, _write_unavailable_states, run_immediately=True - ) + hass.bus.async_listen(EVENT_HOMEASSISTANT_START, _write_unavailable_states) async def async_migrate_entries( diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index b622534d571..3d51610010c 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -276,7 +276,6 @@ def async_track_state_change( EVENT_STATE_CHANGED, state_change_dispatcher, event_filter=state_change_filter, - run_immediately=True, ) @@ -419,7 +418,6 @@ def _async_track_event( tracker.event_type, ft.partial(tracker.dispatcher_callable, hass, callbacks), event_filter=ft.partial(tracker.filter_callable, hass, callbacks), - run_immediately=True, ) job = HassJob(action, f"track {tracker.event_type} event {keys}", job_type=job_type) diff --git a/homeassistant/helpers/integration_platform.py b/homeassistant/helpers/integration_platform.py index 6d474557748..70846156702 100644 --- a/homeassistant/helpers/integration_platform.py +++ b/homeassistant/helpers/integration_platform.py @@ -169,7 +169,6 @@ async def async_process_integration_platforms( hass, integration_platforms, ), - run_immediately=True, ) else: integration_platforms = hass.data[DATA_INTEGRATION_PLATFORMS] diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index 4970b89db1f..40c898fe1d2 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -252,7 +252,7 @@ class RestoreStateData: # Dump states when stopping hass self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, _async_dump_states_at_stop, run_immediately=True + EVENT_HOMEASSISTANT_STOP, _async_dump_states_at_stop ) @callback diff --git a/homeassistant/helpers/start.py b/homeassistant/helpers/start.py index 839514cbf2d..70664430582 100644 --- a/homeassistant/helpers/start.py +++ b/homeassistant/helpers/start.py @@ -50,7 +50,7 @@ def _async_at_core_state( if unsub: unsub() - unsub = hass.bus.async_listen_once(event_type, _matched_event, run_immediately=True) + unsub = hass.bus.async_listen_once(event_type, _matched_event) return cancel diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 60e464d3985..20054274275 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -125,7 +125,6 @@ class _StoreManager: hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STARTED, self._async_schedule_cleanup, - run_immediately=True, ) @callback @@ -185,7 +184,6 @@ class _StoreManager: self._hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, self._async_cancel_and_cleanup, - run_immediately=True, ) @callback @@ -481,7 +479,6 @@ class Store(Generic[_T]): self._unsub_final_write_listener = self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_FINAL_WRITE, self._async_callback_final_write, - run_immediately=True, ) @callback diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 63a700e031b..501dd21d416 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -212,12 +212,8 @@ def async_setup(hass: HomeAssistant) -> bool: cancel = async_track_time_interval( hass, _async_adjust_lru_sizes, timedelta(minutes=10) ) - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, _async_adjust_lru_sizes, run_immediately=True - ) - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, callback(lambda _: cancel()), run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _async_adjust_lru_sizes) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, callback(lambda _: cancel())) return True diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index 0d7365c25bd..76472327d97 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -136,7 +136,7 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]): await self.async_shutdown() self._unsub_shutdown = self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, _on_hass_stop, run_immediately=True + EVENT_HOMEASSISTANT_STOP, _on_hass_stop ) @callback diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 0516f78b198..9979ebeafd5 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -615,14 +615,11 @@ def _async_when_setup( EVENT_COMPONENT_LOADED, _matched_event, event_filter=_async_is_component_filter, - run_immediately=True, ) ) if start_event: listeners.append( - hass.bus.async_listen( - EVENT_HOMEASSISTANT_START, _matched_event, run_immediately=True - ) + hass.bus.async_listen(EVENT_HOMEASSISTANT_START, _matched_event) ) diff --git a/tests/common.py b/tests/common.py index 38af58642c9..ba106d70e00 100644 --- a/tests/common.py +++ b/tests/common.py @@ -299,7 +299,6 @@ async def async_test_home_assistant( hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, hass.config_entries._async_shutdown, - run_immediately=True, ) # Load the registries @@ -358,9 +357,7 @@ async def async_test_home_assistant( await asyncio.sleep(0) # Give aiohttp one loop iteration to close INSTANCES.remove(hass) - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_CLOSE, clear_instance, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, clear_instance) yield hass @@ -918,9 +915,7 @@ class MockEntityPlatform(entity_platform.EntityPlatform): def _async_on_stop(_: Event) -> None: self.async_shutdown() - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, _async_on_stop, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_on_stop) class MockToggleEntity(entity.ToggleEntity): @@ -1493,7 +1488,7 @@ def async_capture_events(hass: HomeAssistant, event_name: str) -> list[Event]: def capture_events(event: Event) -> None: events.append(event) - hass.bus.async_listen(event_name, capture_events, run_immediately=True) + hass.bus.async_listen(event_name, capture_events) return events diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 439ca76d545..7805f3ea151 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -2513,9 +2513,7 @@ async def test_recursive_automation_starting_script( hass.services.async_register( "test", "automation_started", async_service_handler ) - hass.bus.async_listen( - "automation_triggered", async_automation_triggered, run_immediately=True - ) + hass.bus.async_listen("automation_triggered", async_automation_triggered) hass.bus.async_fire("trigger_automation") await asyncio.wait_for(script_done_event.wait(), 10) diff --git a/tests/test_core.py b/tests/test_core.py index a3722b0646d..c78a455f089 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1174,7 +1174,7 @@ async def test_eventbus_run_immediately_callback(hass: HomeAssistant) -> None: """Mock listener.""" calls.append(event) - unsub = hass.bus.async_listen("test", listener, run_immediately=True) + unsub = hass.bus.async_listen("test", listener) hass.bus.async_fire("test", {"event": True}) # No async_block_till_done here @@ -1191,7 +1191,7 @@ async def test_eventbus_run_immediately_coro(hass: HomeAssistant) -> None: """Mock listener.""" calls.append(event) - unsub = hass.bus.async_listen("test", listener, run_immediately=True) + unsub = hass.bus.async_listen("test", listener) hass.bus.async_fire("test", {"event": True}) # No async_block_till_done here @@ -1208,7 +1208,7 @@ async def test_eventbus_listen_once_run_immediately_coro(hass: HomeAssistant) -> """Mock listener.""" calls.append(event) - hass.bus.async_listen_once("test", listener, run_immediately=True) + hass.bus.async_listen_once("test", listener) hass.bus.async_fire("test", {"event": True}) # No async_block_till_done here @@ -3343,9 +3343,7 @@ async def test_statemachine_report_state(hass: HomeAssistant) -> None: hass.states.async_set("light.bowl", "on", {}) state_changed_events = async_capture_events(hass, EVENT_STATE_CHANGED) state_reported_events = [] - hass.bus.async_listen( - EVENT_STATE_REPORTED, listener, event_filter=filter, run_immediately=True - ) + hass.bus.async_listen(EVENT_STATE_REPORTED, listener, event_filter=filter) hass.states.async_set("light.bowl", "on") await hass.async_block_till_done() @@ -3380,17 +3378,36 @@ async def test_report_state_listener_restrictions(hass: HomeAssistant) -> None: """Mock filter.""" return False - # run_immediately set to False - with pytest.raises(HomeAssistantError): - hass.bus.async_listen( - EVENT_STATE_REPORTED, listener, event_filter=filter, run_immediately=False - ) - # no filter with pytest.raises(HomeAssistantError): - hass.bus.async_listen(EVENT_STATE_REPORTED, listener, run_immediately=True) + hass.bus.async_listen(EVENT_STATE_REPORTED, listener) # Both filter and run_immediately - hass.bus.async_listen( - EVENT_STATE_REPORTED, listener, event_filter=filter, run_immediately=True - ) + hass.bus.async_listen(EVENT_STATE_REPORTED, listener, event_filter=filter) + + +@pytest.mark.parametrize( + "run_immediately", + [True, False], +) +@pytest.mark.parametrize( + "method", + ["async_listen", "async_listen_once"], +) +async def test_async_listen_with_run_immediately_deprecated( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + run_immediately: bool, + method: str, +) -> None: + """Test async_add_job warns about its deprecation.""" + + async def _test(event: ha.Event): + pass + + func = getattr(hass.bus, method) + func(EVENT_HOMEASSISTANT_START, _test, run_immediately=run_immediately) + assert ( + f"Detected code that calls `{method}` with run_immediately, which is " + "deprecated and will be removed in Assistant 2025.5." + ) in caplog.text diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index edba232eb69..ab82ef65269 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -399,7 +399,6 @@ async def test_show_progress(hass: HomeAssistant, manager) -> None: hass.bus.async_listen( data_entry_flow.EVENT_DATA_ENTRY_FLOW_PROGRESSED, capture_events, - run_immediately=True, ) result = await manager.async_init("test") @@ -479,7 +478,6 @@ async def test_show_progress_error(hass: HomeAssistant, manager) -> None: hass.bus.async_listen( data_entry_flow.EVENT_DATA_ENTRY_FLOW_PROGRESSED, capture_events, - run_immediately=True, ) result = await manager.async_init("test") From 44f8dbf86bb98c30ec2726db9b21c45f6b7920c1 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Mon, 8 Apr 2024 22:11:14 +0200 Subject: [PATCH 430/967] Fix failing escea test in connection with greeneye_monitor (#115237) * Fix failing escea test in connection with greeneye_monitor * typing --- tests/components/greeneye_monitor/conftest.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/components/greeneye_monitor/conftest.py b/tests/components/greeneye_monitor/conftest.py index d09d31d1db8..8d25a671806 100644 --- a/tests/components/greeneye_monitor/conftest.py +++ b/tests/components/greeneye_monitor/conftest.py @@ -1,5 +1,6 @@ """Common fixtures for testing greeneye_monitor.""" +from collections.abc import Generator from typing import Any from unittest.mock import AsyncMock, MagicMock, patch @@ -98,17 +99,18 @@ def assert_sensor_registered( @pytest.fixture -def monitors() -> AsyncMock: +def monitors() -> Generator[AsyncMock, None, None]: """Provide a mock greeneye.Monitors object that has listeners and can add new monitors.""" - with patch("greeneye.Monitors", new=AsyncMock) as mock_monitors: - add_listeners(mock_monitors) - mock_monitors.monitors = {} + with patch("greeneye.Monitors", autospec=True) as mock_monitors: + mock = mock_monitors.return_value + add_listeners(mock) + mock.monitors = {} def add_monitor(monitor: MagicMock) -> None: """Add the given mock monitor as a monitor with the given serial number, notifying any listeners on the Monitors object.""" serial_number = monitor.serial_number - mock_monitors.monitors[serial_number] = monitor - mock_monitors.notify_all_listeners(monitor) + mock.monitors[serial_number] = monitor + mock.notify_all_listeners(monitor) - mock_monitors.add_monitor = add_monitor - yield mock_monitors + mock.add_monitor = add_monitor + yield mock From 95958ac0efbfcb8153ab6292eab714f7e1a66a09 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 8 Apr 2024 11:05:40 -1000 Subject: [PATCH 431/967] Increase discovery flow init concurrency limit to 20 (#115230) --- homeassistant/helpers/discovery_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/helpers/discovery_flow.py b/homeassistant/helpers/discovery_flow.py index b6633a3f718..314777733c3 100644 --- a/homeassistant/helpers/discovery_flow.py +++ b/homeassistant/helpers/discovery_flow.py @@ -11,7 +11,7 @@ from homeassistant.core import CoreState, Event, HomeAssistant, callback from homeassistant.loader import bind_hass from homeassistant.util.async_ import gather_with_limited_concurrency -FLOW_INIT_LIMIT = 2 +FLOW_INIT_LIMIT = 20 DISCOVERY_FLOW_DISPATCHER = "discovery_flow_dispatcher" From aa85e59c6f58beb57646cfa788a1db6be83c8eca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 8 Apr 2024 11:05:56 -1000 Subject: [PATCH 432/967] Migrate group to use shorthand attributes for name and icon (#115244) --- homeassistant/components/group/__init__.py | 4 ++-- homeassistant/components/group/entity.py | 24 ++++++---------------- 2 files changed, 8 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 7657201da4d..a0f8d2b9a39 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -277,11 +277,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await group.async_update_tracked_entity_ids(entity_ids) if ATTR_NAME in service.data: - group.name = service.data[ATTR_NAME] + group.set_name(service.data[ATTR_NAME]) need_update = True if ATTR_ICON in service.data: - group.icon = service.data[ATTR_ICON] + group.set_icon(service.data[ATTR_ICON]) need_update = True if ATTR_ALL in service.data: diff --git a/homeassistant/components/group/entity.py b/homeassistant/components/group/entity.py index dcb16fd6af3..a8fd9027984 100644 --- a/homeassistant/components/group/entity.py +++ b/homeassistant/components/group/entity.py @@ -150,9 +150,9 @@ class Group(Entity): This Object has factory function for creation. """ self.hass = hass - self._name = name + self._attr_name = name self._state: str | None = None - self._icon = icon + self._attr_icon = icon self._set_tracked(entity_ids) self._on_off: dict[str, bool] = {} self._assumed: dict[str, bool] = {} @@ -234,30 +234,18 @@ class Group(Entity): await async_get_component(hass).async_add_entities([group]) return group - @property - def name(self) -> str: - """Return the name of the group.""" - return self._name - - @name.setter - def name(self, value: str) -> None: + def set_name(self, value: str) -> None: """Set Group name.""" - self._name = value + self._attr_name = value @property def state(self) -> str | None: """Return the state of the group.""" return self._state - @property - def icon(self) -> str | None: - """Return the icon of the group.""" - return self._icon - - @icon.setter - def icon(self, value: str | None) -> None: + def set_icon(self, value: str | None) -> None: """Set Icon for group.""" - self._icon = value + self._attr_icon = value @property def extra_state_attributes(self) -> dict[str, Any]: From 4e983d710fd691c222806b1ec6e8c78445e29750 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 8 Apr 2024 11:09:18 -1000 Subject: [PATCH 433/967] Fix misssing timeout in caldav (#115247) --- homeassistant/components/caldav/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/caldav/__init__.py b/homeassistant/components/caldav/__init__.py index eed06a3a005..3111460e968 100644 --- a/homeassistant/components/caldav/__init__.py +++ b/homeassistant/components/caldav/__init__.py @@ -34,6 +34,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: username=entry.data[CONF_USERNAME], password=entry.data[CONF_PASSWORD], ssl_verify_cert=entry.data[CONF_VERIFY_SSL], + timeout=10, ) try: await hass.async_add_executor_job(client.principal) From d4500cf945b14c14b10fd63c47ebe3528e344591 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 9 Apr 2024 02:56:18 +0200 Subject: [PATCH 434/967] Improve recorder event typing (#115253) --- homeassistant/components/recorder/core.py | 18 +++++++++++++----- homeassistant/components/recorder/db_schema.py | 11 +++++------ .../table_managers/state_attributes.py | 10 ++++++---- .../recorder/table_managers/states_meta.py | 10 ++++++---- 4 files changed, 30 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index cc412c88612..92d9baed771 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -30,7 +30,13 @@ from homeassistant.const import ( EVENT_STATE_CHANGED, MATCH_ALL, ) -from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback +from homeassistant.core import ( + CALLBACK_TYPE, + Event, + EventStateChangedData, + HomeAssistant, + callback, +) from homeassistant.helpers.event import ( async_track_time_change, async_track_time_interval, @@ -862,12 +868,12 @@ class Recorder(threading.Thread): self._guarded_process_one_task_or_event_or_recover(queue_.get()) def _pre_process_startup_events( - self, startup_task_or_events: list[RecorderTask | Event] + self, startup_task_or_events: list[RecorderTask | Event[Any]] ) -> None: """Pre process startup events.""" # Prime all the state_attributes and event_data caches # before we start processing events - state_change_events: list[Event] = [] + state_change_events: list[Event[EventStateChangedData]] = [] non_state_change_events: list[Event] = [] for task_or_event in startup_task_or_events: @@ -1019,7 +1025,7 @@ class Recorder(threading.Thread): self.backlog, ) - def _process_one_event(self, event: Event) -> None: + def _process_one_event(self, event: Event[Any]) -> None: if not self.enabled: return if event.event_type == EVENT_STATE_CHANGED: @@ -1076,7 +1082,9 @@ class Recorder(threading.Thread): self._add_to_session(session, dbevent) - def _process_state_changed_event_into_session(self, event: Event) -> None: + def _process_state_changed_event_into_session( + self, event: Event[EventStateChangedData] + ) -> None: """Process a state_changed event into the session.""" state_attributes_manager = self.state_attributes_manager states_meta_manager = self.states_meta_manager diff --git a/homeassistant/components/recorder/db_schema.py b/homeassistant/components/recorder/db_schema.py index eac743c3d75..186b873047b 100644 --- a/homeassistant/components/recorder/db_schema.py +++ b/homeassistant/components/recorder/db_schema.py @@ -40,7 +40,7 @@ from homeassistant.const import ( MAX_LENGTH_STATE_ENTITY_ID, MAX_LENGTH_STATE_STATE, ) -from homeassistant.core import Context, Event, EventOrigin, State +from homeassistant.core import Context, Event, EventOrigin, EventStateChangedData, State from homeassistant.helpers.json import JSON_DUMP, json_bytes, json_bytes_strip_null import homeassistant.util.dt as dt_util from homeassistant.util.json import ( @@ -478,10 +478,10 @@ class States(Base): return date_time.isoformat(sep=" ", timespec="seconds") @staticmethod - def from_event(event: Event) -> States: + def from_event(event: Event[EventStateChangedData]) -> States: """Create object from a state_changed event.""" entity_id = event.data["entity_id"] - state: State | None = event.data.get("new_state") + state = event.data["new_state"] dbstate = States( entity_id=entity_id, attributes=None, @@ -576,13 +576,12 @@ class StateAttributes(Base): @staticmethod def shared_attrs_bytes_from_event( - event: Event, + event: Event[EventStateChangedData], dialect: SupportedDialect | None, ) -> bytes: """Create shared_attrs from a state_changed event.""" - state: State | None = event.data.get("new_state") # None state means the state was removed from the state machine - if state is None: + if (state := event.data["new_state"]) is None: return b"{}" if state_info := state.state_info: exclude_attrs = { diff --git a/homeassistant/components/recorder/table_managers/state_attributes.py b/homeassistant/components/recorder/table_managers/state_attributes.py index e2fb9153be8..ec975d310e9 100644 --- a/homeassistant/components/recorder/table_managers/state_attributes.py +++ b/homeassistant/components/recorder/table_managers/state_attributes.py @@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, cast from sqlalchemy.orm.session import Session -from homeassistant.core import Event +from homeassistant.core import Event, EventStateChangedData from homeassistant.util.json import JSON_ENCODE_EXCEPTIONS from ..db_schema import StateAttributes @@ -38,7 +38,7 @@ class StateAttributesManager(BaseLRUTableManager[StateAttributes]): super().__init__(recorder, CACHE_SIZE) self.active = True # always active - def serialize_from_event(self, event: Event) -> bytes | None: + def serialize_from_event(self, event: Event[EventStateChangedData]) -> bytes | None: """Serialize event data.""" try: return StateAttributes.shared_attrs_bytes_from_event( @@ -47,12 +47,14 @@ class StateAttributesManager(BaseLRUTableManager[StateAttributes]): except JSON_ENCODE_EXCEPTIONS as ex: _LOGGER.warning( "State is not JSON serializable: %s: %s", - event.data.get("new_state"), + event.data["new_state"], ex, ) return None - def load(self, events: list[Event], session: Session) -> None: + def load( + self, events: list[Event[EventStateChangedData]], session: Session + ) -> None: """Load the shared_attrs to attributes_ids mapping into memory from events. This call is not thread-safe and must be called from the diff --git a/homeassistant/components/recorder/table_managers/states_meta.py b/homeassistant/components/recorder/table_managers/states_meta.py index ebc1dab45f3..2c73dcf3a54 100644 --- a/homeassistant/components/recorder/table_managers/states_meta.py +++ b/homeassistant/components/recorder/table_managers/states_meta.py @@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, cast from sqlalchemy.orm.session import Session -from homeassistant.core import Event +from homeassistant.core import Event, EventStateChangedData from ..db_schema import StatesMeta from ..queries import find_all_states_metadata_ids, find_states_metadata_ids @@ -28,7 +28,9 @@ class StatesMetaManager(BaseLRUTableManager[StatesMeta]): self._did_first_load = False super().__init__(recorder, CACHE_SIZE) - def load(self, events: list[Event], session: Session) -> None: + def load( + self, events: list[Event[EventStateChangedData]], session: Session + ) -> None: """Load the entity_id to metadata_id mapping into memory. This call is not thread-safe and must be called from the @@ -37,9 +39,9 @@ class StatesMetaManager(BaseLRUTableManager[StatesMeta]): self._did_first_load = True self.get_many( { - event.data["new_state"].entity_id + new_state.entity_id for event in events - if event.data.get("new_state") is not None + if (new_state := event.data["new_state"]) is not None }, session, True, From 4a7a22641ee2bbf09915c3922d86712156212de1 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 8 Apr 2024 22:39:31 -0700 Subject: [PATCH 435/967] Fix Google Tasks parsing of remove responses (#115258) --- homeassistant/components/google_tasks/api.py | 5 +++-- tests/components/google_tasks/test_todo.py | 10 +++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/google_tasks/api.py b/homeassistant/components/google_tasks/api.py index 2658fdedc59..ed70f2f6f44 100644 --- a/homeassistant/components/google_tasks/api.py +++ b/homeassistant/components/google_tasks/api.py @@ -112,8 +112,9 @@ class AsyncConfigEntryAuth: raise GoogleTasksApiError( f"Google Tasks API responded with error ({exception.status_code})" ) from exception - data = json.loads(response) - _raise_if_error(data) + if response: + data = json.loads(response) + _raise_if_error(data) for task_id in task_ids: batch.add( diff --git a/tests/components/google_tasks/test_todo.py b/tests/components/google_tasks/test_todo.py index 83d419439d7..afbaabe5cd0 100644 --- a/tests/components/google_tasks/test_todo.py +++ b/tests/components/google_tasks/test_todo.py @@ -156,7 +156,7 @@ def create_response_object(api_response: dict | list) -> tuple[Response, bytes]: def create_batch_response_object( - content_ids: list[str], api_responses: list[dict | list | Response] + content_ids: list[str], api_responses: list[dict | list | Response | None] ) -> tuple[Response, bytes]: """Create a batch response in the multipart/mixed format.""" assert len(api_responses) == len(content_ids) @@ -166,7 +166,7 @@ def create_batch_response_object( body = "" if isinstance(api_response, Response): status = api_response.status - else: + elif api_response is not None: body = json.dumps(api_response) content.extend( [ @@ -194,7 +194,7 @@ def create_batch_response_object( def create_batch_response_handler( - api_responses: list[dict | list | Response], + api_responses: list[dict | list | Response | None], ) -> Callable[[Any], tuple[Response, bytes]]: """Create a fake http2lib response handler that supports generating batch responses. @@ -598,11 +598,11 @@ async def test_partial_update_status( [ LIST_TASK_LIST_RESPONSE, LIST_TASKS_RESPONSE_MULTIPLE, - [EMPTY_RESPONSE, EMPTY_RESPONSE, EMPTY_RESPONSE], # Delete batch + [None, None, None], # Delete batch empty responses LIST_TASKS_RESPONSE, # refresh after delete ] ) - ) + ), ], ) async def test_delete_todo_list_item( From af7d0187cb2596c0562518150ba5a5d964255234 Mon Sep 17 00:00:00 2001 From: Robert Contreras Date: Mon, 8 Apr 2024 23:52:39 -0700 Subject: [PATCH 436/967] Add tests to Home Connect integration (#114214) * Add tests to Home Connect integration * Fix misspelling Co-authored-by: Martin Hjelmare * Changes to tests with properly setup fixtures. * Consolidated api tests, patched library instead of code * Consolidate sensor edge cases, switch mock assertion to call_count * Adjust assertion --------- Co-authored-by: Martin Hjelmare --- .coveragerc | 3 - tests/components/home_connect/conftest.py | 235 ++++++++++++++ .../home_connect/fixtures/appliances.json | 123 +++++++ .../fixtures/programs-active.json | 28 ++ .../fixtures/programs-available.json | 185 +++++++++++ .../home_connect/fixtures/settings.json | 99 ++++++ .../home_connect/fixtures/status.json | 16 + tests/components/home_connect/test_init.py | 301 ++++++++++++++++++ tests/components/home_connect/test_sensor.py | 205 ++++++++++++ 9 files changed, 1192 insertions(+), 3 deletions(-) create mode 100644 tests/components/home_connect/conftest.py create mode 100644 tests/components/home_connect/fixtures/appliances.json create mode 100644 tests/components/home_connect/fixtures/programs-active.json create mode 100644 tests/components/home_connect/fixtures/programs-available.json create mode 100644 tests/components/home_connect/fixtures/settings.json create mode 100644 tests/components/home_connect/fixtures/status.json create mode 100644 tests/components/home_connect/test_init.py create mode 100644 tests/components/home_connect/test_sensor.py diff --git a/.coveragerc b/.coveragerc index ed658f3ca55..b2b9f096d69 100644 --- a/.coveragerc +++ b/.coveragerc @@ -542,12 +542,9 @@ omit = homeassistant/components/hko/weather.py homeassistant/components/hlk_sw16/__init__.py homeassistant/components/hlk_sw16/switch.py - homeassistant/components/home_connect/__init__.py - homeassistant/components/home_connect/api.py homeassistant/components/home_connect/binary_sensor.py homeassistant/components/home_connect/entity.py homeassistant/components/home_connect/light.py - homeassistant/components/home_connect/sensor.py homeassistant/components/home_connect/switch.py homeassistant/components/homematic/__init__.py homeassistant/components/homematic/binary_sensor.py diff --git a/tests/components/home_connect/conftest.py b/tests/components/home_connect/conftest.py new file mode 100644 index 00000000000..5107fb44d69 --- /dev/null +++ b/tests/components/home_connect/conftest.py @@ -0,0 +1,235 @@ +"""Test fixtures for home_connect.""" + +from collections.abc import Awaitable, Callable, Generator +import time +from typing import Any +from unittest.mock import MagicMock, Mock, PropertyMock, patch + +from homeconnect.api import HomeConnectAppliance, HomeConnectError +import pytest + +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) +from homeassistant.components.home_connect import update_all_devices +from homeassistant.components.home_connect.const import DOMAIN +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry, load_json_object_fixture + +MOCK_APPLIANCES_PROPERTIES = { + x["name"]: x + for x in load_json_object_fixture("home_connect/appliances.json")["data"][ + "homeappliances" + ] +} + +CLIENT_ID = "1234" +CLIENT_SECRET = "5678" +FAKE_ACCESS_TOKEN = "some-access-token" +FAKE_REFRESH_TOKEN = "some-refresh-token" +FAKE_AUTH_IMPL = "conftest-imported-cred" + +SERVER_ACCESS_TOKEN = { + "refresh_token": "server-refresh-token", + "access_token": "server-access-token", + "type": "Bearer", + "expires_in": 60, +} + + +@pytest.fixture(name="token_expiration_time") +def mock_token_expiration_time() -> float: + """Fixture for expiration time of the config entry auth token.""" + return time.time() + 86400 + + +@pytest.fixture(name="token_entry") +def mock_token_entry(token_expiration_time: float) -> dict[str, Any]: + """Fixture for OAuth 'token' data for a ConfigEntry.""" + return { + "refresh_token": FAKE_REFRESH_TOKEN, + "access_token": FAKE_ACCESS_TOKEN, + "type": "Bearer", + "expires_at": token_expiration_time, + } + + +@pytest.fixture(name="config_entry") +def mock_config_entry(token_entry: dict[str, Any]) -> MockConfigEntry: + """Fixture for a config entry.""" + return MockConfigEntry( + domain=DOMAIN, + data={ + "auth_implementation": FAKE_AUTH_IMPL, + "token": token_entry, + }, + ) + + +@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), + FAKE_AUTH_IMPL, + ) + + +@pytest.fixture +def platforms() -> list[Platform]: + """Fixture to specify platforms to test.""" + return [] + + +async def bypass_throttle(hass: HomeAssistant, config_entry: MockConfigEntry): + """Add kwarg to disable throttle.""" + await update_all_devices(hass, config_entry, no_throttle=True) + + +@pytest.fixture(name="bypass_throttle") +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), + ): + yield + + +@pytest.fixture(name="integration_setup") +async def mock_integration_setup( + hass: HomeAssistant, + platforms: list[Platform], + config_entry: MockConfigEntry, +) -> Callable[[], Awaitable[bool]]: + """Fixture to set up the integration.""" + config_entry.add_to_hass(hass) + + async def run() -> bool: + with patch("homeassistant.components.home_connect.PLATFORMS", platforms): + result = await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + return result + + return run + + +@pytest.fixture(name="get_appliances") +def mock_get_appliances() -> Generator[None, Any, None]: + """Mock ConfigEntryAuth parent (HomeAssistantAPI) method.""" + with patch( + "homeassistant.components.home_connect.api.ConfigEntryAuth.get_appliances", + ) as mock: + yield mock + + +@pytest.fixture(name="appliance") +def mock_appliance(request) -> Mock: + """Fixture to mock Appliance.""" + app = "Washer" + if hasattr(request, "param") and request.param: + app = request.param + + mock = MagicMock( + autospec=HomeConnectAppliance, + **MOCK_APPLIANCES_PROPERTIES.get(app), + ) + mock.name = app + type(mock).status = PropertyMock(return_value={}) + mock.get.return_value = {} + mock.get_programs_available.return_value = [] + mock.get_status.return_value = {} + mock.get_settings.return_value = {} + + return mock + + +@pytest.fixture(name="problematic_appliance") +def mock_problematic_appliance() -> Mock: + """Fixture to mock a problematic Appliance.""" + app = "Washer" + mock = Mock( + spec=HomeConnectAppliance, + **MOCK_APPLIANCES_PROPERTIES.get(app), + ) + mock.name = app + setattr(mock, "status", {}) + mock.get_programs_active.side_effect = HomeConnectError + mock.get_programs_available.side_effect = HomeConnectError + mock.start_program.side_effect = HomeConnectError + mock.stop_program.side_effect = HomeConnectError + mock.get_status.side_effect = HomeConnectError + mock.get_settings.side_effect = HomeConnectError + mock.set_setting.side_effect = HomeConnectError + + return mock + + +def get_all_appliances(): + """Return a list of `HomeConnectAppliance` instances for all appliances.""" + + appliances = {} + + data = load_json_object_fixture("home_connect/appliances.json").get("data") + programs_active = load_json_object_fixture("home_connect/programs-active.json") + programs_available = load_json_object_fixture( + "home_connect/programs-available.json" + ) + + def listen_callback(mock, callback): + callback["callback"](mock) + + for home_appliance in data["homeappliances"]: + api_status = load_json_object_fixture("home_connect/status.json") + api_settings = load_json_object_fixture("home_connect/settings.json") + + ha_id = home_appliance["haId"] + ha_type = home_appliance["type"] + + appliance = MagicMock(spec=HomeConnectAppliance, **home_appliance) + appliance.name = home_appliance["name"] + appliance.listen_events.side_effect = ( + lambda app=appliance, **x: listen_callback(app, x) + ) + appliance.get_programs_active.return_value = programs_active.get( + ha_type, {} + ).get("data", {}) + appliance.get_programs_available.return_value = [ + program["key"] + for program in programs_available.get(ha_type, {}) + .get("data", {}) + .get("programs", []) + ] + appliance.get_status.return_value = HomeConnectAppliance.json2dict( + api_status.get("data", {}).get("status", []) + ) + appliance.get_settings.return_value = HomeConnectAppliance.json2dict( + api_settings.get(ha_type, {}).get("data", {}).get("settings", []) + ) + setattr(appliance, "status", {}) + appliance.status.update(appliance.get_status.return_value) + appliance.status.update(appliance.get_settings.return_value) + appliance.set_setting.side_effect = ( + lambda x, y, appliance=appliance: appliance.status.update({x: {"value": y}}) + ) + appliance.start_program.side_effect = ( + lambda x, appliance=appliance: appliance.status.update( + {"BSH.Common.Root.ActiveProgram": {"value": x}} + ) + ) + appliance.stop_program.side_effect = ( + lambda appliance=appliance: appliance.status.update( + {"BSH.Common.Root.ActiveProgram": {}} + ) + ) + + appliances[ha_id] = appliance + + return list(appliances.values()) diff --git a/tests/components/home_connect/fixtures/appliances.json b/tests/components/home_connect/fixtures/appliances.json new file mode 100644 index 00000000000..ada18b3482c --- /dev/null +++ b/tests/components/home_connect/fixtures/appliances.json @@ -0,0 +1,123 @@ +{ + "data": { + "homeappliances": [ + { + "name": "FridgeFreezer", + "brand": "SIEMENS", + "vib": "HCS05FRF1", + "connected": true, + "type": "FridgeFreezer", + "enumber": "HCS05FRF1/03", + "haId": "SIEMENS-HCS05FRF1-304F4F9E541D" + }, + { + "name": "Dishwasher", + "brand": "SIEMENS", + "vib": "HCS02DWH1", + "connected": true, + "type": "Dishwasher", + "enumber": "HCS02DWH1/03", + "haId": "SIEMENS-HCS02DWH1-6BE58C26DCC1" + }, + { + "name": "Oven", + "brand": "BOSCH", + "vib": "HCS01OVN1", + "connected": true, + "type": "Oven", + "enumber": "HCS01OVN1/03", + "haId": "BOSCH-HCS01OVN1-43E0065FE245" + }, + { + "name": "Washer", + "brand": "SIEMENS", + "vib": "HCS03WCH1", + "connected": true, + "type": "Washer", + "enumber": "HCS03WCH1/03", + "haId": "SIEMENS-HCS03WCH1-7BC6383CF794" + }, + { + "name": "Dryer", + "brand": "BOSCH", + "vib": "HCS04DYR1", + "connected": true, + "type": "Dryer", + "enumber": "HCS04DYR1/03", + "haId": "BOSCH-HCS04DYR1-831694AE3C5A" + }, + { + "name": "CoffeeMaker", + "brand": "BOSCH", + "vib": "HCS06COM1", + "connected": true, + "type": "CoffeeMaker", + "enumber": "HCS06COM1/03", + "haId": "BOSCH-HCS06COM1-D70390681C2C" + }, + { + "name": "WasherDryer", + "brand": "BOSCH", + "vib": "HCS000001", + "connected": true, + "type": "WasherDryer", + "enumber": "HCS000000/01", + "haId": "BOSCH-HCS000000-D00000000001" + }, + { + "name": "Refrigerator", + "brand": "BOSCH", + "vib": "HCS000002", + "connected": true, + "type": "Refrigerator", + "enumber": "HCS000000/02", + "haId": "BOSCH-HCS000000-D00000000002" + }, + { + "name": "Freezer", + "brand": "BOSCH", + "vib": "HCS000003", + "connected": true, + "type": "Freezer", + "enumber": "HCS000000/03", + "haId": "BOSCH-HCS000000-D00000000003" + }, + { + "name": "Hood", + "brand": "BOSCH", + "vib": "HCS000004", + "connected": true, + "type": "Hood", + "enumber": "HCS000000/04", + "haId": "BOSCH-HCS000000-D00000000004" + }, + { + "name": "Hob", + "brand": "BOSCH", + "vib": "HCS000005", + "connected": true, + "type": "Hob", + "enumber": "HCS000000/05", + "haId": "BOSCH-HCS000000-D00000000005" + }, + { + "name": "CookProcessor", + "brand": "BOSCH", + "vib": "HCS000006", + "connected": true, + "type": "CookProcessor", + "enumber": "HCS000000/06", + "haId": "BOSCH-HCS000000-D00000000006" + }, + { + "name": "DNE", + "brand": "BOSCH", + "vib": "HCS000000", + "connected": true, + "type": "DNE", + "enumber": "HCS000000/00", + "haId": "BOSCH-000000000-000000000000" + } + ] + } +} diff --git a/tests/components/home_connect/fixtures/programs-active.json b/tests/components/home_connect/fixtures/programs-active.json new file mode 100644 index 00000000000..32356a81275 --- /dev/null +++ b/tests/components/home_connect/fixtures/programs-active.json @@ -0,0 +1,28 @@ +{ + "Oven": { + "data": { + "key": "Cooking.Oven.Program.HeatingMode.HotAir", + "name": "Hot air", + "options": [ + { + "key": "Cooking.Oven.Option.SetpointTemperature", + "name": "Target temperature for the cavity", + "value": 230, + "unit": "°C" + }, + { + "key": "BSH.Common.Option.Duration", + "name": "Adjust the duration", + "value": 1200, + "unit": "seconds" + } + ] + } + }, + "Washer": { + "data": { + "key": "BSH.Common.Root.ActiveProgram", + "value": "LaundryCare.Dryer.Program.Mix" + } + } +} diff --git a/tests/components/home_connect/fixtures/programs-available.json b/tests/components/home_connect/fixtures/programs-available.json new file mode 100644 index 00000000000..b99ee5c6add --- /dev/null +++ b/tests/components/home_connect/fixtures/programs-available.json @@ -0,0 +1,185 @@ +{ + "Oven": { + "data": { + "programs": [ + { + "key": "Cooking.Oven.Program.HeatingMode.HotAir", + "name": "Hot air", + "contraints": { + "execution": "selectandstart" + } + }, + { + "key": "Cooking.Oven.Program.HeatingMode.TopBottomHeating", + "name": "Top/bottom heating", + "contraints": { + "execution": "none" + } + }, + { + "key": "Cooking.Oven.Program.HeatingMode.PizzaSetting", + "name": "Pizza setting", + "contraints": { + "execution": "startonly" + } + } + ] + } + }, + "DishWasher": { + "data": { + "programs": [ + { + "key": "Dishcare.Dishwasher.Program.Auto1", + "constraints": { + "execution": "selectandstart" + } + }, + { + "key": "Dishcare.Dishwasher.Program.Auto2", + "constraints": { + "execution": "selectandstart" + } + }, + { + "key": "Dishcare.Dishwasher.Program.Auto3", + "constraints": { + "execution": "selectandstart" + } + }, + { + "key": "Dishcare.Dishwasher.Program.Eco50", + "constraints": { + "execution": "selectandstart" + } + }, + { + "key": "Dishcare.Dishwasher.Program.Quick45", + "constraints": { + "execution": "selectandstart" + } + } + ] + } + }, + "Washer": { + "data": { + "programs": [ + { + "key": "LaundryCare.Washer.Program.Cotton", + "constraints": { + "execution": "selectandstart" + } + }, + { + "key": "LaundryCare.Washer.Program.EasyCare", + "constraints": { + "execution": "selectandstart" + } + }, + { + "key": "LaundryCare.Washer.Program.Mix", + "constraints": { + "execution": "selectandstart" + } + }, + { + "key": "LaundryCare.Washer.Program.DelicatesSilk", + "constraints": { + "execution": "selectandstart" + } + }, + { + "key": "LaundryCare.Washer.Program.Wool", + "constraints": { + "execution": "selectandstart" + } + } + ] + } + }, + "Dryer": { + "data": { + "programs": [ + { + "key": "LaundryCare.Dryer.Program.Cotton", + "constraints": { + "execution": "selectandstart" + } + }, + { + "key": "LaundryCare.Dryer.Program.Synthetic", + "constraints": { + "execution": "selectandstart" + } + }, + { + "key": "LaundryCare.Dryer.Program.Mix", + "constraints": { + "execution": "selectandstart" + } + } + ] + } + }, + "CoffeeMaker": { + "data": { + "programs": [ + { + "key": "ConsumerProducts.CoffeeMaker.Program.Beverage.Espresso", + "constraints": { + "execution": "selectandstart" + } + }, + { + "key": "ConsumerProducts.CoffeeMaker.Program.Beverage.EspressoMacchiato", + "constraints": { + "execution": "selectandstart" + } + }, + { + "key": "ConsumerProducts.CoffeeMaker.Program.Beverage.Coffee", + "constraints": { + "execution": "selectandstart" + } + }, + { + "key": "ConsumerProducts.CoffeeMaker.Program.Beverage.Cappuccino", + "constraints": { + "execution": "selectandstart" + } + }, + { + "key": "ConsumerProducts.CoffeeMaker.Program.Beverage.LatteMacchiato", + "constraints": { + "execution": "selectandstart" + } + }, + { + "key": "ConsumerProducts.CoffeeMaker.Program.Beverage.CaffeLatte", + "constraints": { + "execution": "selectandstart" + } + } + ] + } + }, + "WasherDryer": { + "data": { + "programs": [ + { + "key": "LaundryCare.WasherDryer.Program.Mix", + "constraints": { + "execution": "selectandstart" + } + }, + { + "key": "LaundryCare.Washer.Option.Temperature", + "constraints": { + "execution": "selectandstart" + } + } + ] + } + } +} diff --git a/tests/components/home_connect/fixtures/settings.json b/tests/components/home_connect/fixtures/settings.json new file mode 100644 index 00000000000..5dc0f0e0599 --- /dev/null +++ b/tests/components/home_connect/fixtures/settings.json @@ -0,0 +1,99 @@ +{ + "Dishwasher": { + "data": { + "settings": [ + { + "key": "BSH.Common.Setting.AmbientLightEnabled", + "value": true, + "type": "Boolean" + }, + { + "key": "BSH.Common.Setting.AmbientLightBrightness", + "value": 70, + "unit": "%", + "type": "Double" + }, + { + "key": "BSH.Common.Setting.AmbientLightColor", + "value": "BSH.Common.EnumType.AmbientLightColor.Color43", + "type": "BSH.Common.EnumType.AmbientLightColor" + }, + { + "key": "BSH.Common.Setting.AmbientLightCustomColor", + "value": "#4a88f8", + "type": "String" + }, + { + "key": "BSH.Common.Setting.PowerState", + "value": "BSH.Common.EnumType.PowerState.On", + "type": "BSH.Common.EnumType.PowerState" + }, + { + "key": "BSH.Common.Setting.ChildLock", + "value": false, + "type": "Boolean" + } + ] + } + }, + "Hood": { + "data": { + "settings": [ + { + "key": "Cooking.Common.Setting.Lighting", + "value": true, + "type": "Boolean" + }, + { + "key": "Cooking.Common.Setting.LightingBrightness", + "value": 70, + "unit": "%", + "type": "Double" + }, + { + "key": "Cooking.Hood.Setting.ColorTemperaturePercent", + "value": 70, + "unit": "%", + "type": "Double" + }, + { + "key": "BSH.Common.Setting.ColorTemperature", + "value": "Cooking.Hood.EnumType.ColorTemperature.warmToNeutral", + "type": "BSH.Common.EnumType.ColorTemperature" + }, + { + "key": "BSH.Common.Setting.AmbientLightEnabled", + "value": true, + "type": "Boolean" + }, + { + "key": "BSH.Common.Setting.AmbientLightBrightness", + "value": 70, + "unit": "%", + "type": "Double" + }, + { + "key": "BSH.Common.Setting.AmbientLightColor", + "value": "BSH.Common.EnumType.AmbientLightColor.Color43", + "type": "BSH.Common.EnumType.AmbientLightColor" + }, + { + "key": "BSH.Common.Setting.AmbientLightCustomColor", + "value": "#4a88f8", + "type": "String" + } + ] + } + }, + "Oven": { + "data": { + "settings": [ + { + "key": "BSH.Common.Setting.PowerState", + "value": "BSH.Common.EnumType.PowerState.On", + "type": "BSH.Common.EnumType.PowerState" + } + ] + } + } +} diff --git a/tests/components/home_connect/fixtures/status.json b/tests/components/home_connect/fixtures/status.json new file mode 100644 index 00000000000..8eac586a308 --- /dev/null +++ b/tests/components/home_connect/fixtures/status.json @@ -0,0 +1,16 @@ +{ + "data": { + "status": [ + { "key": "BSH.Common.Status.RemoteControlActive", "value": true }, + { "key": "BSH.Common.Status.RemoteControlStartAllowed", "value": true }, + { + "key": "BSH.Common.Status.OperationState", + "value": "BSH.Common.EnumType.OperationState.Ready" + }, + { + "key": "BSH.Common.Status.DoorState", + "value": "BSH.Common.EnumType.DoorState.Closed" + } + ] + } +} diff --git a/tests/components/home_connect/test_init.py b/tests/components/home_connect/test_init.py new file mode 100644 index 00000000000..e304e2947d5 --- /dev/null +++ b/tests/components/home_connect/test_init.py @@ -0,0 +1,301 @@ +"""Test the integration init functionality.""" + +from collections.abc import Awaitable, Callable, Generator +from typing import Any +from unittest.mock import MagicMock, Mock + +import pytest +from requests import HTTPError +import requests_mock + +from homeassistant.components.home_connect.const import DOMAIN, OAUTH2_TOKEN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from .conftest import ( + CLIENT_ID, + CLIENT_SECRET, + FAKE_ACCESS_TOKEN, + FAKE_REFRESH_TOKEN, + SERVER_ACCESS_TOKEN, + get_all_appliances, +) + +from tests.common import MockConfigEntry +from tests.test_util.aiohttp import AiohttpClientMocker + +SERVICE_KV_CALL_PARAMS = [ + { + "domain": DOMAIN, + "service": "set_option_active", + "service_data": { + "device_id": "DEVICE_ID", + "key": "", + "value": "", + "unit": "", + }, + "blocking": True, + }, + { + "domain": DOMAIN, + "service": "set_option_selected", + "service_data": { + "device_id": "DEVICE_ID", + "key": "", + "value": "", + }, + "blocking": True, + }, + { + "domain": DOMAIN, + "service": "change_setting", + "service_data": { + "device_id": "DEVICE_ID", + "key": "", + "value": "", + }, + "blocking": True, + }, +] + +SERVICE_COMMAND_CALL_PARAMS = [ + { + "domain": DOMAIN, + "service": "pause_program", + "service_data": { + "device_id": "DEVICE_ID", + }, + "blocking": True, + }, + { + "domain": DOMAIN, + "service": "resume_program", + "service_data": { + "device_id": "DEVICE_ID", + }, + "blocking": True, + }, +] + + +SERVICE_PROGRAM_CALL_PARAMS = [ + { + "domain": DOMAIN, + "service": "select_program", + "service_data": { + "device_id": "DEVICE_ID", + "program": "", + "key": "", + "value": "", + }, + "blocking": True, + }, + { + "domain": DOMAIN, + "service": "start_program", + "service_data": { + "device_id": "DEVICE_ID", + "program": "", + "key": "", + "value": "", + "unit": "C", + }, + "blocking": True, + }, +] + +SERVICE_APPLIANCE_METHOD_MAPPING = { + "set_option_active": "set_options_active_program", + "set_option_selected": "set_options_selected_program", + "change_setting": "set_setting", + "pause_program": "execute_command", + "resume_program": "execute_command", + "select_program": "select_program", + "start_program": "start_program", +} + + +async def test_api_setup( + hass: HomeAssistant, + config_entry: MockConfigEntry, + integration_setup: Callable[[], Awaitable[bool]], + setup_credentials: None, + get_appliances: MagicMock, +) -> None: + """Test setup and unload.""" + get_appliances.side_effect = get_all_appliances + assert config_entry.state == ConfigEntryState.NOT_LOADED + assert await integration_setup() + assert config_entry.state == ConfigEntryState.LOADED + + assert await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ConfigEntryState.NOT_LOADED + + +async def test_exception_handling( + bypass_throttle: Generator[None, Any, None], + hass: HomeAssistant, + integration_setup: Callable[[], Awaitable[bool]], + config_entry: MockConfigEntry, + setup_credentials: None, + get_appliances: MagicMock, + problematic_appliance: Mock, +) -> None: + """Test exception handling.""" + get_appliances.return_value = [problematic_appliance] + assert config_entry.state == ConfigEntryState.NOT_LOADED + assert await integration_setup() + assert config_entry.state == ConfigEntryState.LOADED + + +@pytest.mark.parametrize("token_expiration_time", [12345]) +async def test_token_refresh_success( + bypass_throttle: Generator[None, Any, None], + integration_setup: Callable[[], Awaitable[bool]], + config_entry: MockConfigEntry, + aioclient_mock: AiohttpClientMocker, + requests_mock: requests_mock.Mocker, + setup_credentials: None, +) -> None: + """Test where token is expired and the refresh attempt succeeds.""" + + assert config_entry.data["token"]["access_token"] == FAKE_ACCESS_TOKEN + + requests_mock.post(OAUTH2_TOKEN, json=SERVER_ACCESS_TOKEN) + requests_mock.get("/api/homeappliances", json={"data": {"homeappliances": []}}) + + aioclient_mock.post( + OAUTH2_TOKEN, + json=SERVER_ACCESS_TOKEN, + ) + assert await integration_setup() + assert config_entry.state == ConfigEntryState.LOADED + + # Verify token request + assert aioclient_mock.call_count == 1 + assert aioclient_mock.mock_calls[0][2] == { + "client_id": CLIENT_ID, + "client_secret": CLIENT_SECRET, + "grant_type": "refresh_token", + "refresh_token": FAKE_REFRESH_TOKEN, + } + + # Verify updated token + assert ( + config_entry.data["token"]["access_token"] + == SERVER_ACCESS_TOKEN["access_token"] + ) + + +async def test_setup( + hass: HomeAssistant, + integration_setup: Callable[[], Awaitable[bool]], + config_entry: MockConfigEntry, + setup_credentials: None, +) -> None: + """Test setting up the integration.""" + assert config_entry.state == ConfigEntryState.NOT_LOADED + + assert await integration_setup() + assert config_entry.state == ConfigEntryState.LOADED + + assert await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ConfigEntryState.NOT_LOADED + + +async def test_update_throttle( + appliance: Mock, + hass: HomeAssistant, + config_entry: MockConfigEntry, + integration_setup: Callable[[], Awaitable[bool]], + setup_credentials: None, + platforms: list[Platform], + get_appliances: MagicMock, +) -> None: + """Test to check Throttle functionality.""" + assert config_entry.state == ConfigEntryState.NOT_LOADED + + assert await integration_setup() + assert config_entry.state == ConfigEntryState.LOADED + assert get_appliances.call_count == 0 + + +async def test_http_error( + bypass_throttle: Generator[None, Any, None], + hass: HomeAssistant, + config_entry: MockConfigEntry, + integration_setup: Callable[[], Awaitable[bool]], + setup_credentials: None, + get_appliances: MagicMock, +) -> None: + """Test HTTP errors during setup integration.""" + get_appliances.side_effect = HTTPError(response=MagicMock()) + assert config_entry.state == ConfigEntryState.NOT_LOADED + assert await integration_setup() + assert config_entry.state == ConfigEntryState.LOADED + assert get_appliances.call_count == 1 + + +@pytest.mark.parametrize( + "service_call", + SERVICE_KV_CALL_PARAMS + SERVICE_COMMAND_CALL_PARAMS + SERVICE_PROGRAM_CALL_PARAMS, +) +async def test_services( + service_call: list[dict[str, Any]], + 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: + """Create and test services.""" + get_appliances.return_value = [appliance] + assert config_entry.state == ConfigEntryState.NOT_LOADED + assert await integration_setup() + assert config_entry.state == ConfigEntryState.LOADED + + device_registry = dr.async_get(hass) + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={(DOMAIN, appliance.haId)}, + ) + + service_name = service_call["service"] + service_call["service_data"]["device_id"] = device_entry.id + await hass.services.async_call(**service_call) + await hass.async_block_till_done() + assert ( + getattr(appliance, SERVICE_APPLIANCE_METHOD_MAPPING[service_name]).call_count + == 1 + ) + + +async def test_services_exception( + 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: + """Raise a ValueError when device id does not match.""" + get_appliances.return_value = [appliance] + assert config_entry.state == ConfigEntryState.NOT_LOADED + assert await integration_setup() + assert config_entry.state == ConfigEntryState.LOADED + + service_call = SERVICE_KV_CALL_PARAMS[0] + + 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/home_connect/test_sensor.py b/tests/components/home_connect/test_sensor.py new file mode 100644 index 00000000000..77dec8c615b --- /dev/null +++ b/tests/components/home_connect/test_sensor.py @@ -0,0 +1,205 @@ +"""Tests for home_connect sensor entities.""" + +from collections.abc import Awaitable, Callable, Generator +from typing import Any +from unittest.mock import MagicMock, Mock + +from freezegun.api import FrozenDateTimeFactory +import pytest + +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 + +TEST_HC_APP = "Dishwasher" + + +EVENT_PROG_DELAYED_START = { + "BSH.Common.Status.OperationState": { + "value": "BSH.Common.EnumType.OperationState.Delayed" + }, +} + +EVENT_PROG_REMAIN_NO_VALUE = { + "BSH.Common.Option.RemainingProgramTime": {}, + "BSH.Common.Status.OperationState": { + "value": "BSH.Common.EnumType.OperationState.Delayed" + }, +} + + +EVENT_PROG_RUN = { + "BSH.Common.Option.RemainingProgramTime": {"value": "0"}, + "BSH.Common.Option.ProgramProgress": {"value": "60"}, + "BSH.Common.Status.OperationState": { + "value": "BSH.Common.EnumType.OperationState.Run" + }, +} + + +EVENT_PROG_UPDATE_1 = { + "BSH.Common.Option.RemainingProgramTime": {"value": "0"}, + "BSH.Common.Option.ProgramProgress": {"value": "80"}, + "BSH.Common.Status.OperationState": { + "value": "BSH.Common.EnumType.OperationState.Run" + }, +} + +EVENT_PROG_UPDATE_2 = { + "BSH.Common.Option.RemainingProgramTime": {"value": "20"}, + "BSH.Common.Option.ProgramProgress": {"value": "99"}, + "BSH.Common.Status.OperationState": { + "value": "BSH.Common.EnumType.OperationState.Run" + }, +} + +EVENT_PROG_END = { + "BSH.Common.Status.OperationState": { + "value": "BSH.Common.EnumType.OperationState.Ready" + }, +} + + +@pytest.fixture +def platforms() -> list[str]: + """Fixture to specify platforms to test.""" + return [Platform.SENSOR] + + +async def test_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 sensor entities.""" + get_appliances.return_value = [appliance] + assert config_entry.state == ConfigEntryState.NOT_LOADED + assert await integration_setup() + assert config_entry.state == ConfigEntryState.LOADED + + +# Appliance program sequence with a delayed start. +PROGRAM_SEQUENCE_EVENTS = ( + EVENT_PROG_DELAYED_START, + EVENT_PROG_RUN, + EVENT_PROG_UPDATE_1, + EVENT_PROG_UPDATE_2, + EVENT_PROG_END, +) + +# Entity mapping to expected state at each program sequence. +ENTITY_ID_STATES = { + "sensor.dishwasher_operation_state": ( + "Delayed", + "Run", + "Run", + "Run", + "Ready", + ), + "sensor.dishwasher_remaining_program_time": ( + "unavailable", + "2021-01-09T12:00:00+00:00", + "2021-01-09T12:00:00+00:00", + "2021-01-09T12:00:20+00:00", + "unavailable", + ), + "sensor.dishwasher_program_progress": ( + "unavailable", + "60", + "80", + "99", + "unavailable", + ), +} + + +@pytest.mark.parametrize("appliance", [TEST_HC_APP], indirect=True) +@pytest.mark.parametrize( + ("states", "event_run"), + list(zip(list(zip(*ENTITY_ID_STATES.values())), PROGRAM_SEQUENCE_EVENTS)), +) +async def test_event_sensors( + appliance: Mock, + states: tuple, + event_run: dict, + freezer: FrozenDateTimeFactory, + bypass_throttle: Generator[None, Any, None], + hass: HomeAssistant, + config_entry: MockConfigEntry, + integration_setup: Callable[[], Awaitable[bool]], + setup_credentials: None, + get_appliances: MagicMock, +) -> None: + """Test sequence for sensors that are only available after an event happens.""" + entity_ids = ENTITY_ID_STATES.keys() + + time_to_freeze = "2021-01-09 12:00:00+00:00" + freezer.move_to(time_to_freeze) + + 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(event_run) + for entity_id, state in zip(entity_ids, states): + await async_update_entity(hass, entity_id) + await hass.async_block_till_done() + assert hass.states.is_state(entity_id, state) + + +# Program sequence for SensorDeviceClass.TIMESTAMP edge cases. +PROGRAM_SEQUENCE_EDGE_CASE = [ + EVENT_PROG_REMAIN_NO_VALUE, + EVENT_PROG_RUN, + EVENT_PROG_END, + EVENT_PROG_END, +] + +# Expected state at each sequence. +ENTITY_ID_EDGE_CASE_STATES = [ + "unavailable", + "2021-01-09T12:00:01+00:00", + "unavailable", + "unavailable", +] + + +@pytest.mark.parametrize("appliance", [TEST_HC_APP], indirect=True) +async def test_remaining_prog_time_edge_cases( + appliance: Mock, + freezer: FrozenDateTimeFactory, + bypass_throttle: Generator[None, Any, None], + hass: HomeAssistant, + config_entry: MockConfigEntry, + integration_setup: Callable[[], Awaitable[bool]], + setup_credentials: None, + get_appliances: MagicMock, +) -> None: + """Run program sequence to test edge cases for the remaining_prog_time entity.""" + get_appliances.return_value = [appliance] + entity_id = "sensor.dishwasher_remaining_program_time" + time_to_freeze = "2021-01-09 12:00:00+00:00" + freezer.move_to(time_to_freeze) + + assert config_entry.state == ConfigEntryState.NOT_LOADED + assert await integration_setup() + assert config_entry.state == ConfigEntryState.LOADED + + for ( + event, + expected_state, + ) in zip(PROGRAM_SEQUENCE_EDGE_CASE, ENTITY_ID_EDGE_CASE_STATES): + appliance.status.update(event) + await async_update_entity(hass, entity_id) + await hass.async_block_till_done() + freezer.tick() + assert hass.states.is_state(entity_id, expected_state) From a852a38d24fd251b961720d02d54d45d28fa46da Mon Sep 17 00:00:00 2001 From: On Freund Date: Tue, 9 Apr 2024 10:59:27 +0300 Subject: [PATCH 437/967] Configurable maximum concurrency in Risco local (#115226) * Configurable maximum concurrency in Risco local * Show advanced Risco options in advanced mode --- homeassistant/components/risco/__init__.py | 7 ++- homeassistant/components/risco/config_flow.py | 20 +++++-- homeassistant/components/risco/const.py | 8 ++- homeassistant/components/risco/strings.json | 3 +- tests/components/risco/test_config_flow.py | 53 ++++++++++++++++++- 5 files changed, 83 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/risco/__init__.py b/homeassistant/components/risco/__init__.py index 531cd982a1e..7ca18ea77c5 100644 --- a/homeassistant/components/risco/__init__.py +++ b/homeassistant/components/risco/__init__.py @@ -38,7 +38,9 @@ from homeassistant.helpers.storage import Store from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( + CONF_CONCURRENCY, DATA_COORDINATOR, + DEFAULT_CONCURRENCY, DEFAULT_SCAN_INTERVAL, DOMAIN, EVENTS_COORDINATOR, @@ -85,7 +87,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def _async_setup_local_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: data = entry.data - risco = RiscoLocal(data[CONF_HOST], data[CONF_PORT], data[CONF_PIN]) + concurrency = entry.options.get(CONF_CONCURRENCY, DEFAULT_CONCURRENCY) + risco = RiscoLocal( + data[CONF_HOST], data[CONF_PORT], data[CONF_PIN], concurrency=concurrency + ) try: await risco.connect() diff --git a/homeassistant/components/risco/config_flow.py b/homeassistant/components/risco/config_flow.py index ab372be3a14..21761e23d09 100644 --- a/homeassistant/components/risco/config_flow.py +++ b/homeassistant/components/risco/config_flow.py @@ -35,8 +35,10 @@ from .const import ( CONF_CODE_ARM_REQUIRED, CONF_CODE_DISARM_REQUIRED, CONF_COMMUNICATION_DELAY, + CONF_CONCURRENCY, CONF_HA_STATES_TO_RISCO, CONF_RISCO_STATES_TO_HA, + DEFAULT_ADVANCED_OPTIONS, DEFAULT_OPTIONS, DOMAIN, MAX_COMMUNICATION_DELAY, @@ -225,11 +227,8 @@ class RiscoOptionsFlowHandler(OptionsFlow): self._data = {**DEFAULT_OPTIONS, **config_entry.options} def _options_schema(self) -> vol.Schema: - return vol.Schema( + schema = vol.Schema( { - vol.Required( - CONF_SCAN_INTERVAL, default=self._data[CONF_SCAN_INTERVAL] - ): int, vol.Required( CONF_CODE_ARM_REQUIRED, default=self._data[CONF_CODE_ARM_REQUIRED] ): bool, @@ -239,6 +238,19 @@ class RiscoOptionsFlowHandler(OptionsFlow): ): bool, } ) + if self.show_advanced_options: + self._data = {**DEFAULT_ADVANCED_OPTIONS, **self._data} + schema = schema.extend( + { + vol.Required( + CONF_SCAN_INTERVAL, default=self._data[CONF_SCAN_INTERVAL] + ): int, + vol.Required( + CONF_CONCURRENCY, default=self._data[CONF_CONCURRENCY] + ): int, + } + ) + return schema async def async_step_init( self, user_input: dict[str, Any] | None = None diff --git a/homeassistant/components/risco/const.py b/homeassistant/components/risco/const.py index a27aeae4bf0..f1240a704de 100644 --- a/homeassistant/components/risco/const.py +++ b/homeassistant/components/risco/const.py @@ -14,6 +14,7 @@ DATA_COORDINATOR = "risco" EVENTS_COORDINATOR = "risco_events" DEFAULT_SCAN_INTERVAL = 30 +DEFAULT_CONCURRENCY = 4 TYPE_LOCAL = "local" @@ -25,6 +26,7 @@ CONF_CODE_DISARM_REQUIRED = "code_disarm_required" CONF_RISCO_STATES_TO_HA = "risco_states_to_ha" CONF_HA_STATES_TO_RISCO = "ha_states_to_risco" CONF_COMMUNICATION_DELAY = "communication_delay" +CONF_CONCURRENCY = "concurrency" RISCO_GROUPS = ["A", "B", "C", "D"] RISCO_ARM = "arm" @@ -44,9 +46,13 @@ DEFAULT_HA_STATES_TO_RISCO = { } DEFAULT_OPTIONS = { - CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL, CONF_CODE_ARM_REQUIRED: False, CONF_CODE_DISARM_REQUIRED: False, CONF_RISCO_STATES_TO_HA: DEFAULT_RISCO_STATES_TO_HA, CONF_HA_STATES_TO_RISCO: DEFAULT_HA_STATES_TO_RISCO, } + +DEFAULT_ADVANCED_OPTIONS = { + CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL, + CONF_CONCURRENCY: DEFAULT_CONCURRENCY, +} diff --git a/homeassistant/components/risco/strings.json b/homeassistant/components/risco/strings.json index 69d7e571f43..e35b13394cb 100644 --- a/homeassistant/components/risco/strings.json +++ b/homeassistant/components/risco/strings.json @@ -36,7 +36,8 @@ "init": { "title": "Configure options", "data": { - "scan_interval": "How often to poll Risco (in seconds)", + "scan_interval": "How often to poll Risco Cloud (in seconds)", + "concurrency": "Maximum concurrent requests in Risco local", "code_arm_required": "Require PIN to arm", "code_disarm_required": "Require PIN to disarm" } diff --git a/tests/components/risco/test_config_flow.py b/tests/components/risco/test_config_flow.py index 7589bc0ae14..9fade18ea96 100644 --- a/tests/components/risco/test_config_flow.py +++ b/tests/components/risco/test_config_flow.py @@ -46,11 +46,15 @@ TEST_HA_TO_RISCO = { } TEST_OPTIONS = { - "scan_interval": 10, "code_arm_required": True, "code_disarm_required": True, } +TEST_ADVANCED_OPTIONS = { + "scan_interval": 10, + "concurrency": 3, +} + async def test_cloud_form(hass: HomeAssistant) -> None: """Test we get the cloud form.""" @@ -387,6 +391,53 @@ async def test_options_flow(hass: HomeAssistant) -> None: } +async def test_advanced_options_flow(hass: HomeAssistant) -> None: + """Test options flow.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=TEST_CLOUD_DATA["username"], + data=TEST_CLOUD_DATA, + ) + + entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init( + entry.entry_id, context={"show_advanced_options": True} + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "init" + assert "concurrency" in result["data_schema"].schema + assert "scan_interval" in result["data_schema"].schema + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={**TEST_OPTIONS, **TEST_ADVANCED_OPTIONS} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "risco_to_ha" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input=TEST_RISCO_TO_HA, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "ha_to_risco" + + with patch("homeassistant.components.risco.async_setup_entry", return_value=True): + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input=TEST_HA_TO_RISCO, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert entry.options == { + **TEST_OPTIONS, + **TEST_ADVANCED_OPTIONS, + "risco_states_to_ha": TEST_RISCO_TO_HA, + "ha_states_to_risco": TEST_HA_TO_RISCO, + } + + async def test_ha_to_risco_schema(hass: HomeAssistant) -> None: """Test that the schema for the ha-to-risco mapping step is generated properly.""" entry = MockConfigEntry( From 0a57641f3f31554266134d21acb659982a9f246e Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Tue, 9 Apr 2024 09:12:50 +0100 Subject: [PATCH 438/967] Bump ring_doorbell library to 0.8.11 (#115263) --- homeassistant/components/ring/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index 1abc9a99e63..63417f90b42 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -14,5 +14,5 @@ "iot_class": "cloud_polling", "loggers": ["ring_doorbell"], "quality_scale": "silver", - "requirements": ["ring-doorbell[listen]==0.8.10"] + "requirements": ["ring-doorbell[listen]==0.8.11"] } diff --git a/requirements_all.txt b/requirements_all.txt index c0984c8b758..bc624ce85be 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2454,7 +2454,7 @@ rfk101py==0.0.1 rflink==0.0.66 # homeassistant.components.ring -ring-doorbell[listen]==0.8.10 +ring-doorbell[listen]==0.8.11 # homeassistant.components.fleetgo ritassist==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 526466e04e2..29292ae897c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1897,7 +1897,7 @@ reolink-aio==0.8.9 rflink==0.0.66 # homeassistant.components.ring -ring-doorbell[listen]==0.8.10 +ring-doorbell[listen]==0.8.11 # homeassistant.components.roku rokuecp==0.19.2 From bf3eb463ae211115c8e1e2c7b49e8b761ccffefc Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Tue, 9 Apr 2024 09:35:25 +0100 Subject: [PATCH 439/967] Wrap tplink exceptions caused by user actions inside HomeAssistantError (#114919) --- .../components/tplink/coordinator.py | 3 + homeassistant/components/tplink/entity.py | 41 +++++++++++- homeassistant/components/tplink/strings.json | 11 +++ tests/components/tplink/test_light.py | 66 ++++++++++++++++++ tests/components/tplink/test_switch.py | 67 ++++++++++++++++++- 5 files changed, 184 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tplink/coordinator.py b/homeassistant/components/tplink/coordinator.py index 94ad94de0ae..7595cdd8f90 100644 --- a/homeassistant/components/tplink/coordinator.py +++ b/homeassistant/components/tplink/coordinator.py @@ -7,6 +7,7 @@ import logging from kasa import AuthenticationException, SmartDevice, SmartDeviceException +from homeassistant import config_entries from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.debounce import Debouncer @@ -20,6 +21,8 @@ REQUEST_REFRESH_DELAY = 0.35 class TPLinkDataUpdateCoordinator(DataUpdateCoordinator[None]): """DataUpdateCoordinator to gather data for a specific TPLink device.""" + config_entry: config_entries.ConfigEntry + def __init__( self, hass: HomeAssistant, diff --git a/homeassistant/components/tplink/entity.py b/homeassistant/components/tplink/entity.py index 4720fae1259..23766e69257 100644 --- a/homeassistant/components/tplink/entity.py +++ b/homeassistant/components/tplink/entity.py @@ -5,8 +5,14 @@ from __future__ import annotations from collections.abc import Awaitable, Callable, Coroutine from typing import Any, Concatenate, ParamSpec, TypeVar -from kasa import SmartDevice +from kasa import ( + AuthenticationException, + SmartDevice, + SmartDeviceException, + TimeoutException, +) +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -21,10 +27,39 @@ _P = ParamSpec("_P") def async_refresh_after( func: Callable[Concatenate[_T, _P], Awaitable[None]], ) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, None]]: - """Define a wrapper to refresh after.""" + """Define a wrapper to raise HA errors and refresh after.""" async def _async_wrap(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None: - await func(self, *args, **kwargs) + try: + await func(self, *args, **kwargs) + except AuthenticationException as ex: + self.coordinator.config_entry.async_start_reauth(self.hass) + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="device_authentication", + translation_placeholders={ + "func": func.__name__, + "exc": str(ex), + }, + ) from ex + except TimeoutException as ex: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="device_timeout", + translation_placeholders={ + "func": func.__name__, + "exc": str(ex), + }, + ) from ex + except SmartDeviceException as ex: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="device_error", + translation_placeholders={ + "func": func.__name__, + "exc": str(ex), + }, + ) from ex await self.coordinator.async_request_refresh() return _async_wrap diff --git a/homeassistant/components/tplink/strings.json b/homeassistant/components/tplink/strings.json index 19aa35f3604..c863df7c81c 100644 --- a/homeassistant/components/tplink/strings.json +++ b/homeassistant/components/tplink/strings.json @@ -184,5 +184,16 @@ } } } + }, + "exceptions": { + "device_timeout": { + "message": "Timeout communicating with the device {func}: {exc}" + }, + "device_error": { + "message": "Unable to communicate with the device {func}: {exc}" + }, + "device_authentication": { + "message": "Device authentication error {func}: {exc}" + } } } diff --git a/tests/components/tplink/test_light.py b/tests/components/tplink/test_light.py index 767ff4a122c..1217a4d4cca 100644 --- a/tests/components/tplink/test_light.py +++ b/tests/components/tplink/test_light.py @@ -5,6 +5,7 @@ from __future__ import annotations from datetime import timedelta from unittest.mock import MagicMock, PropertyMock +from kasa import AuthenticationException, SmartDeviceException, TimeoutException import pytest from homeassistant.components import tplink @@ -24,8 +25,10 @@ from homeassistant.components.light import ( DOMAIN as LIGHT_DOMAIN, ) from homeassistant.components.tplink.const import DOMAIN +from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -730,3 +733,66 @@ async def test_smart_strip_custom_sequence_effect(hass: HomeAssistant) -> None: } ) strip.set_custom_effect.reset_mock() + + +@pytest.mark.parametrize( + ("exception_type", "msg", "reauth_expected"), + [ + ( + AuthenticationException, + "Device authentication error async_turn_on: test error", + True, + ), + ( + TimeoutException, + "Timeout communicating with the device async_turn_on: test error", + False, + ), + ( + SmartDeviceException, + "Unable to communicate with the device async_turn_on: test error", + False, + ), + ], + ids=["Authentication", "Timeout", "Other"], +) +async def test_light_errors_when_turned_on( + hass: HomeAssistant, + exception_type, + msg, + reauth_expected, +) -> None: + """Tests the light wraps errors correctly.""" + 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.turn_on.side_effect = exception_type(msg) + + with _patch_discovery(device=bulb), _patch_connect(device=bulb): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + + assert not any( + already_migrated_config_entry.async_get_active_flows(hass, {SOURCE_REAUTH}) + ) + + with pytest.raises(HomeAssistantError, match=msg): + await hass.services.async_call( + 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 ( + any( + flow + for flow in already_migrated_config_entry.async_get_active_flows( + hass, {SOURCE_REAUTH} + ) + if flow["handler"] == tplink.DOMAIN + ) + == reauth_expected + ) diff --git a/tests/components/tplink/test_switch.py b/tests/components/tplink/test_switch.py index 6326e9bb671..6fb841346a1 100644 --- a/tests/components/tplink/test_switch.py +++ b/tests/components/tplink/test_switch.py @@ -3,12 +3,13 @@ from datetime import timedelta from unittest.mock import AsyncMock -from kasa import SmartDeviceException +from kasa import AuthenticationException, SmartDeviceException, TimeoutException import pytest from homeassistant.components import tplink from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.tplink.const import DOMAIN +from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, @@ -17,6 +18,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util, slugify @@ -202,3 +204,66 @@ async def test_strip_unique_ids(hass: HomeAssistant) -> None: assert ( entity_registry.async_get(entity_id).unique_id == f"PLUG{plug_id}DEVICEID" ) + + +@pytest.mark.parametrize( + ("exception_type", "msg", "reauth_expected"), + [ + ( + AuthenticationException, + "Device authentication error async_turn_on: test error", + True, + ), + ( + TimeoutException, + "Timeout communicating with the device async_turn_on: test error", + False, + ), + ( + SmartDeviceException, + "Unable to communicate with the device async_turn_on: test error", + False, + ), + ], + ids=["Authentication", "Timeout", "Other"], +) +async def test_plug_errors_when_turned_on( + hass: HomeAssistant, + exception_type, + msg, + reauth_expected, +) -> None: + """Tests the plug wraps errors correctly.""" + 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.turn_on.side_effect = exception_type("test error") + + 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 not any( + already_migrated_config_entry.async_get_active_flows(hass, {SOURCE_REAUTH}) + ) + + with pytest.raises(HomeAssistantError, match=msg): + await hass.services.async_call( + 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 ( + any( + flow + for flow in already_migrated_config_entry.async_get_active_flows( + hass, {SOURCE_REAUTH} + ) + if flow["handler"] == tplink.DOMAIN + ) + == reauth_expected + ) From 80450adb1ab9f2747823f1b1d45820f058ab557e Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 9 Apr 2024 10:38:17 +0200 Subject: [PATCH 440/967] Remove Epson Workforce integration (#115201) * Remove Epson Workforce integration * Remove Epson Workforce integration --- .coveragerc | 1 - CODEOWNERS | 1 - homeassistant/brands/epson.json | 5 - .../components/epsonworkforce/__init__.py | 1 - .../components/epsonworkforce/manifest.json | 9 -- .../components/epsonworkforce/sensor.py | 117 ------------------ homeassistant/generated/integrations.json | 17 +-- requirements_all.txt | 3 - 8 files changed, 3 insertions(+), 151 deletions(-) delete mode 100644 homeassistant/brands/epson.json delete mode 100644 homeassistant/components/epsonworkforce/__init__.py delete mode 100644 homeassistant/components/epsonworkforce/manifest.json delete mode 100644 homeassistant/components/epsonworkforce/sensor.py diff --git a/.coveragerc b/.coveragerc index b2b9f096d69..c02a6fefe75 100644 --- a/.coveragerc +++ b/.coveragerc @@ -365,7 +365,6 @@ omit = homeassistant/components/epion/sensor.py homeassistant/components/epson/__init__.py homeassistant/components/epson/media_player.py - homeassistant/components/epsonworkforce/sensor.py homeassistant/components/eq3btsmart/__init__.py homeassistant/components/eq3btsmart/climate.py homeassistant/components/eq3btsmart/const.py diff --git a/CODEOWNERS b/CODEOWNERS index df1a7370fcd..6e7b7e6f8f4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -399,7 +399,6 @@ build.json @home-assistant/supervisor /tests/components/epion/ @lhgravendeel /homeassistant/components/epson/ @pszafer /tests/components/epson/ @pszafer -/homeassistant/components/epsonworkforce/ @ThaStealth /homeassistant/components/eq3btsmart/ @eulemitkeule @dbuezas /tests/components/eq3btsmart/ @eulemitkeule @dbuezas /homeassistant/components/escea/ @lazdavila diff --git a/homeassistant/brands/epson.json b/homeassistant/brands/epson.json deleted file mode 100644 index 80d5db942a2..00000000000 --- a/homeassistant/brands/epson.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "domain": "epson", - "name": "Epson", - "integrations": ["epson", "epsonworkforce"] -} diff --git a/homeassistant/components/epsonworkforce/__init__.py b/homeassistant/components/epsonworkforce/__init__.py deleted file mode 100644 index 5efd217b1dd..00000000000 --- a/homeassistant/components/epsonworkforce/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The epsonworkforce component.""" diff --git a/homeassistant/components/epsonworkforce/manifest.json b/homeassistant/components/epsonworkforce/manifest.json deleted file mode 100644 index 855689bab7d..00000000000 --- a/homeassistant/components/epsonworkforce/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "domain": "epsonworkforce", - "name": "Epson Workforce", - "codeowners": ["@ThaStealth"], - "documentation": "https://www.home-assistant.io/integrations/epsonworkforce", - "iot_class": "local_polling", - "loggers": ["epsonprinter_pkg"], - "requirements": ["epsonprinter==0.0.9"] -} diff --git a/homeassistant/components/epsonworkforce/sensor.py b/homeassistant/components/epsonworkforce/sensor.py deleted file mode 100644 index dea611a3c3a..00000000000 --- a/homeassistant/components/epsonworkforce/sensor.py +++ /dev/null @@ -1,117 +0,0 @@ -"""Support for Epson Workforce Printer.""" - -from __future__ import annotations - -from datetime import timedelta - -from epsonprinter_pkg.epsonprinterapi import EpsonPrinterAPI -import voluptuous as vol - -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, - SensorEntity, - SensorEntityDescription, -) -from homeassistant.const import CONF_HOST, CONF_MONITORED_CONDITIONS, PERCENTAGE -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( - key="black", - name="Ink level Black", - icon="mdi:water", - native_unit_of_measurement=PERCENTAGE, - ), - SensorEntityDescription( - key="photoblack", - name="Ink level Photoblack", - icon="mdi:water", - native_unit_of_measurement=PERCENTAGE, - ), - SensorEntityDescription( - key="magenta", - name="Ink level Magenta", - icon="mdi:water", - native_unit_of_measurement=PERCENTAGE, - ), - SensorEntityDescription( - key="cyan", - name="Ink level Cyan", - icon="mdi:water", - native_unit_of_measurement=PERCENTAGE, - ), - SensorEntityDescription( - key="yellow", - name="Ink level Yellow", - icon="mdi:water", - native_unit_of_measurement=PERCENTAGE, - ), - SensorEntityDescription( - key="clean", - name="Cleaning level", - icon="mdi:water", - native_unit_of_measurement=PERCENTAGE, - ), -) -MONITORED_CONDITIONS: list[str] = [desc.key for desc in SENSOR_TYPES] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_MONITORED_CONDITIONS): vol.All( - cv.ensure_list, [vol.In(MONITORED_CONDITIONS)] - ), - } -) -SCAN_INTERVAL = timedelta(minutes=60) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_devices: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the cartridge sensor.""" - host = config.get(CONF_HOST) - - api = EpsonPrinterAPI(host) - if not api.available: - raise PlatformNotReady - - sensors = [ - EpsonPrinterCartridge(api, description) - for description in SENSOR_TYPES - if description.key in config[CONF_MONITORED_CONDITIONS] - ] - - add_devices(sensors, True) - - -class EpsonPrinterCartridge(SensorEntity): - """Representation of a cartridge sensor.""" - - def __init__( - self, api: EpsonPrinterAPI, description: SensorEntityDescription - ) -> None: - """Initialize a cartridge sensor.""" - self._api = api - self.entity_description = description - - @property - def native_value(self): - """Return the state of the device.""" - return self._api.getSensorValue(self.entity_description.key) - - @property - def available(self) -> bool: - """Could the device be accessed during the last update call.""" - return self._api.available - - def update(self) -> None: - """Get the latest data from the Epson printer.""" - self._api.update() diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index f027db93fe0..667639226a1 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1650,20 +1650,9 @@ }, "epson": { "name": "Epson", - "integrations": { - "epson": { - "integration_type": "hub", - "config_flow": true, - "iot_class": "local_polling", - "name": "Epson" - }, - "epsonworkforce": { - "integration_type": "hub", - "config_flow": false, - "iot_class": "local_polling", - "name": "Epson Workforce" - } - } + "integration_type": "hub", + "config_flow": true, + "iot_class": "local_polling" }, "eq3": { "name": "eQ-3", diff --git a/requirements_all.txt b/requirements_all.txt index bc624ce85be..446d69da244 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -821,9 +821,6 @@ epion==0.0.3 # homeassistant.components.epson epson-projector==0.5.1 -# homeassistant.components.epsonworkforce -epsonprinter==0.0.9 - # homeassistant.components.eq3btsmart eq3btsmart==1.1.6 From 4cd2351bcc0bb9af7383685650d84bbfadaa2106 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Tue, 9 Apr 2024 10:08:46 +0100 Subject: [PATCH 441/967] Update and migrate ring non string unique ids (#115265) Co-authored-by: J. Nick Koston --- homeassistant/components/ring/__init__.py | 31 ++++- homeassistant/components/ring/camera.py | 2 +- homeassistant/components/ring/light.py | 2 +- tests/components/ring/test_camera.py | 4 +- tests/components/ring/test_init.py | 138 +++++++++++++++++++++- tests/components/ring/test_light.py | 4 +- 6 files changed, 172 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py index e3697d4fccc..ffa99704526 100644 --- a/homeassistant/components/ring/__init__.py +++ b/homeassistant/components/ring/__init__.py @@ -9,8 +9,8 @@ from ring_doorbell import Auth, Ring from homeassistant.config_entries import ConfigEntry from homeassistant.const import APPLICATION_NAME, CONF_TOKEN, __version__ -from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.helpers import device_registry as dr +from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from .const import ( @@ -44,6 +44,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) ring = Ring(auth) + await _migrate_old_unique_ids(hass, entry.entry_id) + devices_coordinator = RingDataCoordinator(hass, ring) notifications_coordinator = RingNotificationsCoordinator(hass, ring) await devices_coordinator.async_config_entry_first_refresh() @@ -111,3 +113,28 @@ async def async_remove_config_entry_device( ) -> bool: """Remove a config entry from a device.""" return True + + +async def _migrate_old_unique_ids(hass: HomeAssistant, entry_id: str) -> None: + entity_registry = er.async_get(hass) + + @callback + def _async_migrator(entity_entry: er.RegistryEntry) -> dict[str, str] | None: + # Old format for camera and light was int + if isinstance(entity_entry.unique_id, int): + new_unique_id = str(entity_entry.unique_id) + if existing_entity_id := entity_registry.async_get_entity_id( + entity_entry.domain, entity_entry.platform, new_unique_id + ): + _LOGGER.error( + "Cannot migrate to unique_id '%s', already exists for '%s', " + "You may have to delete unavailable ring entities", + new_unique_id, + existing_entity_id, + ) + return None + _LOGGER.info("Fixing non string unique id %s", entity_entry.unique_id) + return {"new_unique_id": new_unique_id} + return None + + await er.async_migrate_entries(hass, entry_id, _async_migrator) diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index ec0f4ca3fab..b9d73afe6de 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -67,7 +67,7 @@ class RingCam(RingEntity, Camera): self._video_url = None self._image = None self._expires_at = dt_util.utcnow() - FORCE_REFRESH_INTERVAL - self._attr_unique_id = device.id + self._attr_unique_id = str(device.id) if device.has_capability(MOTION_DETECTION_CAPABILITY): self._attr_motion_detection_enabled = device.motion_detection diff --git a/homeassistant/components/ring/light.py b/homeassistant/components/ring/light.py index 10d13e59810..b9e1c8c38b4 100644 --- a/homeassistant/components/ring/light.py +++ b/homeassistant/components/ring/light.py @@ -58,7 +58,7 @@ class RingLight(RingEntity, LightEntity): def __init__(self, device, coordinator): """Initialize the light.""" super().__init__(device, coordinator) - self._attr_unique_id = device.id + self._attr_unique_id = str(device.id) self._attr_is_on = device.lights == ON_STATE self._no_updates_until = dt_util.utcnow() diff --git a/tests/components/ring/test_camera.py b/tests/components/ring/test_camera.py index de61d7a1452..dde1252d5b8 100644 --- a/tests/components/ring/test_camera.py +++ b/tests/components/ring/test_camera.py @@ -25,10 +25,10 @@ async def test_entity_registry( entity_registry = er.async_get(hass) entry = entity_registry.async_get("camera.front") - assert entry.unique_id == 765432 + assert entry.unique_id == "765432" entry = entity_registry.async_get("camera.internal") - assert entry.unique_id == 345678 + assert entry.unique_id == "345678" @pytest.mark.parametrize( diff --git a/tests/components/ring/test_init.py b/tests/components/ring/test_init.py index 3ca686c37a4..664f8ff1973 100644 --- a/tests/components/ring/test_init.py +++ b/tests/components/ring/test_init.py @@ -8,9 +8,13 @@ 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.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 from homeassistant.helpers.issue_registry import IssueRegistry from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -253,7 +257,7 @@ async def test_issue_deprecated_service_ring_update( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - _ = await hass.services.async_call(DOMAIN, "update", {}, blocking=True) + await hass.services.async_call(DOMAIN, "update", {}, blocking=True) issue = issue_registry.async_get_issue("ring", "deprecated_service_ring_update") assert issue @@ -266,3 +270,135 @@ async def test_issue_deprecated_service_ring_update( "This is deprecated and will stop working in Home Assistant 2024.10. " "Use 'homeassistant.update_entity' instead which updates all ring entities" ) in caplog.text + + +@pytest.mark.parametrize( + ("domain", "old_unique_id"), + [ + ( + LIGHT_DOMAIN, + 123456, + ), + ( + CAMERA_DOMAIN, + 654321, + ), + ], +) +async def test_update_unique_id( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + caplog: pytest.LogCaptureFixture, + requests_mock: requests_mock.Mocker, + domain: str, + old_unique_id: int | str, +) -> None: + """Test unique_id update of integration.""" + entry = MockConfigEntry( + title="Ring", + domain=DOMAIN, + data={ + CONF_USERNAME: "foo@bar.com", + "token": {"access_token": "mock-token"}, + }, + unique_id="foo@bar.com", + ) + entry.add_to_hass(hass) + + entity = entity_registry.async_get_or_create( + domain=domain, + platform=DOMAIN, + unique_id=old_unique_id, + config_entry=entry, + ) + assert entity.unique_id == old_unique_id + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entity_migrated = entity_registry.async_get(entity.entity_id) + assert entity_migrated + assert entity_migrated.unique_id == str(old_unique_id) + assert (f"Fixing non string unique id {old_unique_id}") in caplog.text + + +async def test_update_unique_id_existing( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + caplog: pytest.LogCaptureFixture, + requests_mock: requests_mock.Mocker, +) -> None: + """Test unique_id update of integration.""" + old_unique_id = 123456 + entry = MockConfigEntry( + title="Ring", + domain=DOMAIN, + data={ + CONF_USERNAME: "foo@bar.com", + "token": {"access_token": "mock-token"}, + }, + unique_id="foo@bar.com", + ) + entry.add_to_hass(hass) + + entity = entity_registry.async_get_or_create( + domain=CAMERA_DOMAIN, + platform=DOMAIN, + unique_id=old_unique_id, + config_entry=entry, + ) + entity_existing = entity_registry.async_get_or_create( + domain=CAMERA_DOMAIN, + platform=DOMAIN, + unique_id=str(old_unique_id), + config_entry=entry, + ) + assert entity.unique_id == old_unique_id + assert entity_existing.unique_id == str(old_unique_id) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entity_not_migrated = entity_registry.async_get(entity.entity_id) + entity_existing = entity_registry.async_get(entity_existing.entity_id) + assert entity_not_migrated + assert entity_existing + assert entity_not_migrated.unique_id == old_unique_id + assert ( + f"Cannot migrate to unique_id '{old_unique_id}', " + f"already exists for '{entity_existing.entity_id}', " + "You may have to delete unavailable ring entities" + ) in caplog.text + + +async def test_update_unique_id_no_update( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + caplog: pytest.LogCaptureFixture, + requests_mock: requests_mock.Mocker, +) -> None: + """Test unique_id update of integration.""" + correct_unique_id = "123456" + entry = MockConfigEntry( + title="Ring", + domain=DOMAIN, + data={ + CONF_USERNAME: "foo@bar.com", + "token": {"access_token": "mock-token"}, + }, + unique_id="foo@bar.com", + ) + entry.add_to_hass(hass) + + entity = entity_registry.async_get_or_create( + domain=CAMERA_DOMAIN, + platform=DOMAIN, + unique_id="123456", + config_entry=entry, + ) + assert entity.unique_id == correct_unique_id + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entity_migrated = entity_registry.async_get(entity.entity_id) + assert entity_migrated + assert entity_migrated.unique_id == correct_unique_id + assert "Fixing non string unique id" not in caplog.text diff --git a/tests/components/ring/test_light.py b/tests/components/ring/test_light.py index 621c0b8f1d0..ac0f3b70d27 100644 --- a/tests/components/ring/test_light.py +++ b/tests/components/ring/test_light.py @@ -25,10 +25,10 @@ async def test_entity_registry( entity_registry = er.async_get(hass) entry = entity_registry.async_get("light.front_light") - assert entry.unique_id == 765432 + assert entry.unique_id == "765432" entry = entity_registry.async_get("light.internal_light") - assert entry.unique_id == 345678 + assert entry.unique_id == "345678" async def test_light_off_reports_correctly( From 7bf2baa236d4949a54ec31b5f561769bb551ac31 Mon Sep 17 00:00:00 2001 From: pleum Date: Tue, 9 Apr 2024 16:18:37 +0700 Subject: [PATCH 442/967] Add additional Vital 100S model to vesync (#113838) --- homeassistant/components/vesync/const.py | 3 ++- homeassistant/components/vesync/fan.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/vesync/const.py b/homeassistant/components/vesync/const.py index b2fd090e781..08badae8cd0 100644 --- a/homeassistant/components/vesync/const.py +++ b/homeassistant/components/vesync/const.py @@ -55,5 +55,6 @@ SKU_TO_BASE_DEVICE = { "LAP-V201S-WUS": "Vital200S", # Alt ID Model Vital200S "LAP-V201-AUSR": "Vital200S", # Alt ID Model Vital200S "Vital100S": "Vital100S", - "LAP-V102S-WUS": "Vital100S", # Alt ID Model Vital100S, + "LAP-V102S-WUS": "Vital100S", # Alt ID Model Vital100S + "LAP-V102S-AASR": "Vital100S", # Alt ID Model Vital100S } diff --git a/homeassistant/components/vesync/fan.py b/homeassistant/components/vesync/fan.py index 1d8ea6463bf..6272c033b4f 100644 --- a/homeassistant/components/vesync/fan.py +++ b/homeassistant/components/vesync/fan.py @@ -184,6 +184,8 @@ class VeSyncFanHA(VeSyncDevice, FanEntity): self.smartfan.auto_mode() elif preset_mode == FAN_MODE_SLEEP: self.smartfan.sleep_mode() + elif preset_mode == FAN_MODE_PET: + self.smartfan.pet_mode() self.schedule_update_ha_state() From cad4c3c0c2062b3fb26ae9e3f88245ad6a600e44 Mon Sep 17 00:00:00 2001 From: konsti Date: Tue, 9 Apr 2024 15:08:55 +0200 Subject: [PATCH 443/967] Remove pip jemalloc config from dockerfile (#115206) --- Dockerfile | 4 ---- script/hassfest/docker.py | 4 ---- 2 files changed, 8 deletions(-) diff --git a/Dockerfile b/Dockerfile index 1f2f011b288..28b65d6383d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,14 +30,10 @@ RUN \ uv pip install homeassistant/home_assistant_*.whl; \ fi \ && if [ "${BUILD_ARCH}" = "i386" ]; then \ - LD_PRELOAD="/usr/local/lib/libjemalloc.so.2" \ - MALLOC_CONF="background_thread:true,metadata_thp:auto,dirty_decay_ms:20000,muzzy_decay_ms:20000" \ linux32 uv pip install \ --no-build \ -r homeassistant/requirements_all.txt; \ else \ - LD_PRELOAD="/usr/local/lib/libjemalloc.so.2" \ - MALLOC_CONF="background_thread:true,metadata_thp:auto,dirty_decay_ms:20000,muzzy_decay_ms:20000" \ uv pip install \ --no-build \ -r homeassistant/requirements_all.txt; \ diff --git a/script/hassfest/docker.py b/script/hassfest/docker.py index 4e348d4ae6c..e38a238be7d 100644 --- a/script/hassfest/docker.py +++ b/script/hassfest/docker.py @@ -38,14 +38,10 @@ RUN \ uv pip install homeassistant/home_assistant_*.whl; \ fi \ && if [ "${{BUILD_ARCH}}" = "i386" ]; then \ - LD_PRELOAD="/usr/local/lib/libjemalloc.so.2" \ - MALLOC_CONF="background_thread:true,metadata_thp:auto,dirty_decay_ms:20000,muzzy_decay_ms:20000" \ linux32 uv pip install \ --no-build \ -r homeassistant/requirements_all.txt; \ else \ - LD_PRELOAD="/usr/local/lib/libjemalloc.so.2" \ - MALLOC_CONF="background_thread:true,metadata_thp:auto,dirty_decay_ms:20000,muzzy_decay_ms:20000" \ uv pip install \ --no-build \ -r homeassistant/requirements_all.txt; \ From 68384bba67c5ae6b460eb0f4a312c305dfa16ea5 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 10 Apr 2024 02:55:59 +1200 Subject: [PATCH 444/967] Send/receive Voice Assistant audio via ESPHome native API (#114800) * Protobuf audio test * Remove extraneous code * Rework voice assistant pipeline * Move variables * Fix reading flags * Dont directly put to queue * Bump aioesphomeapi to 24.0.0 * Update tests - Add more tests for API pipeline - Convert some udp tests to use api pipeline - Update fixtures for new device info flags * Fix bad merge --------- Co-authored-by: Michael Hansen --- .../components/esphome/binary_sensor.py | 4 +- .../components/esphome/entry_data.py | 4 +- homeassistant/components/esphome/manager.py | 90 +++-- .../components/esphome/manifest.json | 2 +- homeassistant/components/esphome/select.py | 4 +- .../components/esphome/voice_assistant.py | 217 +++++++---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/esphome/conftest.py | 30 +- tests/components/esphome/test_diagnostics.py | 3 +- .../esphome/test_voice_assistant.py | 367 ++++++++++++------ 11 files changed, 495 insertions(+), 230 deletions(-) diff --git a/homeassistant/components/esphome/binary_sensor.py b/homeassistant/components/esphome/binary_sensor.py index ac0676d8d1e..05ddfc2c43f 100644 --- a/homeassistant/components/esphome/binary_sensor.py +++ b/homeassistant/components/esphome/binary_sensor.py @@ -33,7 +33,9 @@ async def async_setup_entry( entry_data = DomainData.get(hass).get_entry_data(entry) assert entry_data.device_info is not None - if entry_data.device_info.voice_assistant_version: + if entry_data.device_info.voice_assistant_feature_flags_compat( + entry_data.api_version + ): async_add_entities([EsphomeAssistInProgressBinarySensor(entry_data)]) diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 877c099deee..005963db872 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -257,7 +257,9 @@ class RuntimeEntryData: if async_get_dashboard(hass): needed_platforms.add(Platform.UPDATE) - if self.device_info and self.device_info.voice_assistant_version: + if self.device_info and self.device_info.voice_assistant_feature_flags_compat( + self.api_version + ): needed_platforms.add(Platform.BINARY_SENSOR) needed_platforms.add(Platform.SELECT) diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index 3813d22ce97..ef56f3a2164 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -21,6 +21,7 @@ from aioesphomeapi import ( UserService, UserServiceArgType, VoiceAssistantAudioSettings, + VoiceAssistantFeature, ) from awesomeversion import AwesomeVersion import voluptuous as vol @@ -72,7 +73,11 @@ from .domain_data import DomainData # Import config flow so that it's added to the registry from .entry_data import RuntimeEntryData -from .voice_assistant import VoiceAssistantUDPServer +from .voice_assistant import ( + VoiceAssistantAPIPipeline, + VoiceAssistantPipeline, + VoiceAssistantUDPPipeline, +) _LOGGER = logging.getLogger(__name__) @@ -143,7 +148,7 @@ class ESPHomeManager: "cli", "device_id", "domain_data", - "voice_assistant_udp_server", + "voice_assistant_pipeline", "reconnect_logic", "zeroconf_instance", "entry_data", @@ -168,7 +173,7 @@ class ESPHomeManager: self.cli = cli self.device_id: str | None = None self.domain_data = domain_data - self.voice_assistant_udp_server: VoiceAssistantUDPServer | None = None + self.voice_assistant_pipeline: VoiceAssistantPipeline | None = None self.reconnect_logic: ReconnectLogic | None = None self.zeroconf_instance = zeroconf_instance self.entry_data = entry_data @@ -327,9 +332,10 @@ class ESPHomeManager: def _handle_pipeline_finished(self) -> None: self.entry_data.async_set_assist_pipeline_state(False) - if self.voice_assistant_udp_server is not None: - self.voice_assistant_udp_server.close() - self.voice_assistant_udp_server = None + if self.voice_assistant_pipeline is not None: + if isinstance(self.voice_assistant_pipeline, VoiceAssistantUDPPipeline): + self.voice_assistant_pipeline.close() + self.voice_assistant_pipeline = None async def _handle_pipeline_start( self, @@ -339,38 +345,60 @@ class ESPHomeManager: wake_word_phrase: str | None, ) -> int | None: """Start a voice assistant pipeline.""" - if self.voice_assistant_udp_server is not None: + if self.voice_assistant_pipeline is not None: _LOGGER.warning("Voice assistant UDP server was not stopped") - self.voice_assistant_udp_server.stop() - self.voice_assistant_udp_server = None + self.voice_assistant_pipeline.stop() + self.voice_assistant_pipeline = None hass = self.hass - self.voice_assistant_udp_server = VoiceAssistantUDPServer( - hass, - self.entry_data, - self.cli.send_voice_assistant_event, - self._handle_pipeline_finished, - ) - port = await self.voice_assistant_udp_server.start_server() + assert self.entry_data.device_info is not None + if ( + self.entry_data.device_info.voice_assistant_feature_flags_compat( + self.entry_data.api_version + ) + & VoiceAssistantFeature.API_AUDIO + ): + self.voice_assistant_pipeline = VoiceAssistantAPIPipeline( + hass, + self.entry_data, + self.cli.send_voice_assistant_event, + self._handle_pipeline_finished, + self.cli, + ) + port = 0 + else: + self.voice_assistant_pipeline = VoiceAssistantUDPPipeline( + hass, + self.entry_data, + self.cli.send_voice_assistant_event, + self._handle_pipeline_finished, + ) + port = await self.voice_assistant_pipeline.start_server() assert self.device_id is not None, "Device ID must be set" hass.async_create_background_task( - self.voice_assistant_udp_server.run_pipeline( + self.voice_assistant_pipeline.run_pipeline( device_id=self.device_id, conversation_id=conversation_id or None, flags=flags, audio_settings=audio_settings, wake_word_phrase=wake_word_phrase, ), - "esphome.voice_assistant_udp_server.run_pipeline", + "esphome.voice_assistant_pipeline.run_pipeline", ) return port async def _handle_pipeline_stop(self) -> None: """Stop a voice assistant pipeline.""" - if self.voice_assistant_udp_server is not None: - self.voice_assistant_udp_server.stop() + if self.voice_assistant_pipeline is not None: + self.voice_assistant_pipeline.stop() + + async def _handle_audio(self, data: bytes) -> None: + if self.voice_assistant_pipeline is None: + return + assert isinstance(self.voice_assistant_pipeline, VoiceAssistantAPIPipeline) + self.voice_assistant_pipeline.receive_audio_bytes(data) async def on_connect(self) -> None: """Subscribe to states and list entities on successful API login.""" @@ -472,13 +500,23 @@ class ESPHomeManager: ) ) - if device_info.voice_assistant_version: - entry_data.disconnect_callbacks.add( - cli.subscribe_voice_assistant( - self._handle_pipeline_start, - self._handle_pipeline_stop, + flags = device_info.voice_assistant_feature_flags_compat(api_version) + if flags: + if flags & VoiceAssistantFeature.API_AUDIO: + entry_data.disconnect_callbacks.add( + cli.subscribe_voice_assistant( + handle_start=self._handle_pipeline_start, + handle_stop=self._handle_pipeline_stop, + handle_audio=self._handle_audio, + ) + ) + else: + entry_data.disconnect_callbacks.add( + cli.subscribe_voice_assistant( + handle_start=self._handle_pipeline_start, + handle_stop=self._handle_pipeline_stop, + ) ) - ) cli.subscribe_states(entry_data.async_update_state) cli.subscribe_service_calls(self.async_on_service_call) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index f1a5333c403..4d5636a6f26 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -15,7 +15,7 @@ "iot_class": "local_push", "loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"], "requirements": [ - "aioesphomeapi==23.2.0", + "aioesphomeapi==24.0.0", "esphome-dashboard-api==1.2.3", "bleak-esphome==1.0.0" ], diff --git a/homeassistant/components/esphome/select.py b/homeassistant/components/esphome/select.py index 07a9d70e558..612ffc4bcc6 100644 --- a/homeassistant/components/esphome/select.py +++ b/homeassistant/components/esphome/select.py @@ -42,7 +42,9 @@ async def async_setup_entry( entry_data = DomainData.get(hass).get_entry_data(entry) assert entry_data.device_info is not None - if entry_data.device_info.voice_assistant_version: + if entry_data.device_info.voice_assistant_feature_flags_compat( + entry_data.api_version + ): async_add_entities( [ EsphomeAssistPipelineSelect(hass, entry_data), diff --git a/homeassistant/components/esphome/voice_assistant.py b/homeassistant/components/esphome/voice_assistant.py index f856cc27179..f9f753389ed 100644 --- a/homeassistant/components/esphome/voice_assistant.py +++ b/homeassistant/components/esphome/voice_assistant.py @@ -11,9 +11,11 @@ from typing import cast import wave from aioesphomeapi import ( + APIClient, VoiceAssistantAudioSettings, VoiceAssistantCommandFlag, VoiceAssistantEventType, + VoiceAssistantFeature, ) from homeassistant.components import stt, tts @@ -64,13 +66,11 @@ _VOICE_ASSISTANT_EVENT_TYPES: EsphomeEnumMapper[ ) -class VoiceAssistantUDPServer(asyncio.DatagramProtocol): - """Receive UDP packets and forward them to the voice assistant.""" +class VoiceAssistantPipeline: + """Base abstract pipeline class.""" started = False stop_requested = False - transport: asyncio.DatagramTransport | None = None - remote_addr: tuple[str, int] | None = None def __init__( self, @@ -79,12 +79,11 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): handle_event: Callable[[VoiceAssistantEventType, dict[str, str] | None], None], handle_finished: Callable[[], None], ) -> None: - """Initialize UDP receiver.""" + """Initialize the pipeline.""" self.context = Context() self.hass = hass - - assert entry_data.device_info is not None self.entry_data = entry_data + assert entry_data.device_info is not None self.device_info = entry_data.device_info self.queue: asyncio.Queue[bytes] = asyncio.Queue() @@ -95,69 +94,9 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): @property def is_running(self) -> bool: - """True if the UDP server is started and hasn't been asked to stop.""" + """True if the pipeline is started and hasn't been asked to stop.""" return self.started and (not self.stop_requested) - async def start_server(self) -> int: - """Start accepting connections.""" - - def accept_connection() -> VoiceAssistantUDPServer: - """Accept connection.""" - if self.started: - raise RuntimeError("Can only start once") - if self.stop_requested: - raise RuntimeError("No longer accepting connections") - - self.started = True - return self - - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - sock.setblocking(False) - - sock.bind(("", UDP_PORT)) - - await asyncio.get_running_loop().create_datagram_endpoint( - accept_connection, sock=sock - ) - - return cast(int, sock.getsockname()[1]) - - @callback - def connection_made(self, transport: asyncio.BaseTransport) -> None: - """Store transport for later use.""" - self.transport = cast(asyncio.DatagramTransport, transport) - - @callback - def datagram_received(self, data: bytes, addr: tuple[str, int]) -> None: - """Handle incoming UDP packet.""" - if not self.is_running: - return - if self.remote_addr is None: - self.remote_addr = addr - self.queue.put_nowait(data) - - def error_received(self, exc: Exception) -> None: - """Handle when a send or receive operation raises an OSError. - - (Other than BlockingIOError or InterruptedError.) - """ - _LOGGER.error("ESPHome Voice Assistant UDP server error received: %s", exc) - self.handle_finished() - - @callback - def stop(self) -> None: - """Stop the receiver.""" - self.queue.put_nowait(b"") - self.close() - - def close(self) -> None: - """Close the receiver.""" - self.started = False - self.stop_requested = True - - if self.transport is not None: - self.transport.close() - async def _iterate_packets(self) -> AsyncIterable[bytes]: """Iterate over incoming packets.""" while data := await self.queue.get(): @@ -198,7 +137,12 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): url = async_process_play_media_url(self.hass, path) data_to_send = {"url": url} - if self.device_info.voice_assistant_version >= 2: + if ( + self.device_info.voice_assistant_feature_flags_compat( + self.entry_data.api_version + ) + & VoiceAssistantFeature.SPEAKER + ): media_id = tts_output["media_id"] self._tts_task = self.hass.async_create_background_task( self._send_tts(media_id), "esphome_voice_assistant_tts" @@ -243,9 +187,15 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): if audio_settings is None or audio_settings.volume_multiplier == 0: audio_settings = VoiceAssistantAudioSettings() - tts_audio_output = ( - "wav" if self.device_info.voice_assistant_version >= 2 else "mp3" - ) + if ( + self.device_info.voice_assistant_feature_flags_compat( + self.entry_data.api_version + ) + & VoiceAssistantFeature.SPEAKER + ): + tts_audio_output = "wav" + else: + tts_audio_output = "mp3" _LOGGER.debug("Starting pipeline") if flags & VoiceAssistantCommandFlag.USE_WAKE_WORD: @@ -315,7 +265,7 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): self.handle_event(VoiceAssistantEventType.VOICE_ASSISTANT_TTS_STREAM_START, {}) try: - if (not self.is_running) or (self.transport is None): + if not self.is_running: return extension, data = await tts.async_get_media_source_audio( @@ -358,16 +308,133 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): samples_in_chunk = len(chunk) // bytes_per_sample samples_left -= samples_in_chunk - self.transport.sendto(chunk, self.remote_addr) + self.send_audio_bytes(chunk) await asyncio.sleep( samples_in_chunk / stt.AudioSampleRates.SAMPLERATE_16000 * 0.9 ) sample_offset += samples_in_chunk - finally: self.handle_event( VoiceAssistantEventType.VOICE_ASSISTANT_TTS_STREAM_END, {} ) self._tts_task = None self._tts_done.set() + + def send_audio_bytes(self, data: bytes) -> None: + """Send bytes to the device.""" + raise NotImplementedError + + def stop(self) -> None: + """Stop the pipeline.""" + self.queue.put_nowait(b"") + + +class VoiceAssistantUDPPipeline(asyncio.DatagramProtocol, VoiceAssistantPipeline): + """Receive UDP packets and forward them to the voice assistant.""" + + transport: asyncio.DatagramTransport | None = None + remote_addr: tuple[str, int] | None = None + + async def start_server(self) -> int: + """Start accepting connections.""" + + def accept_connection() -> VoiceAssistantUDPPipeline: + """Accept connection.""" + if self.started: + raise RuntimeError("Can only start once") + if self.stop_requested: + raise RuntimeError("No longer accepting connections") + + self.started = True + return self + + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setblocking(False) + + sock.bind(("", UDP_PORT)) + + await asyncio.get_running_loop().create_datagram_endpoint( + accept_connection, sock=sock + ) + + return cast(int, sock.getsockname()[1]) + + @callback + def connection_made(self, transport: asyncio.BaseTransport) -> None: + """Store transport for later use.""" + self.transport = cast(asyncio.DatagramTransport, transport) + + @callback + def datagram_received(self, data: bytes, addr: tuple[str, int]) -> None: + """Handle incoming UDP packet.""" + if not self.is_running: + return + if self.remote_addr is None: + self.remote_addr = addr + self.queue.put_nowait(data) + + def error_received(self, exc: Exception) -> None: + """Handle when a send or receive operation raises an OSError. + + (Other than BlockingIOError or InterruptedError.) + """ + _LOGGER.error("ESPHome Voice Assistant UDP server error received: %s", exc) + self.handle_finished() + + @callback + def stop(self) -> None: + """Stop the receiver.""" + super().stop() + self.close() + + def close(self) -> None: + """Close the receiver.""" + self.started = False + self.stop_requested = True + + if self.transport is not None: + self.transport.close() + + def send_audio_bytes(self, data: bytes) -> None: + """Send bytes to the device via UDP.""" + if self.transport is None: + _LOGGER.error("No transport to send audio to") + return + self.transport.sendto(data, self.remote_addr) + + +class VoiceAssistantAPIPipeline(VoiceAssistantPipeline): + """Send audio to the voice assistant via the API.""" + + def __init__( + self, + hass: HomeAssistant, + entry_data: RuntimeEntryData, + handle_event: Callable[[VoiceAssistantEventType, dict[str, str] | None], None], + handle_finished: Callable[[], None], + api_client: APIClient, + ) -> None: + """Initialize the pipeline.""" + super().__init__(hass, entry_data, handle_event, handle_finished) + self.api_client = api_client + self.started = True + + def send_audio_bytes(self, data: bytes) -> None: + """Send bytes to the device via the API.""" + self.api_client.send_voice_assistant_audio(data) + + @callback + def receive_audio_bytes(self, data: bytes) -> None: + """Receive audio bytes from the device.""" + if not self.is_running: + return + self.queue.put_nowait(data) + + @callback + def stop(self) -> None: + """Stop the pipeline.""" + super().stop() + + self.started = False + self.stop_requested = True diff --git a/requirements_all.txt b/requirements_all.txt index 446d69da244..6c705cb9a18 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -242,7 +242,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==23.2.0 +aioesphomeapi==24.0.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 29292ae897c..e7b27bfa01a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -221,7 +221,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==23.2.0 +aioesphomeapi==24.0.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/tests/components/esphome/conftest.py b/tests/components/esphome/conftest.py index cb6655f710c..e23f020991d 100644 --- a/tests/components/esphome/conftest.py +++ b/tests/components/esphome/conftest.py @@ -18,6 +18,7 @@ from aioesphomeapi import ( HomeassistantServiceCall, ReconnectLogic, UserService, + VoiceAssistantFeature, ) import pytest from zeroconf import Zeroconf @@ -354,10 +355,16 @@ async def mock_voice_assistant_entry( ): """Set up an ESPHome entry with voice assistant.""" - async def _mock_voice_assistant_entry(version: int) -> MockConfigEntry: + async def _mock_voice_assistant_entry( + voice_assistant_feature_flags: VoiceAssistantFeature, + ) -> MockConfigEntry: return ( await _mock_generic_device_entry( - hass, mock_client, {"voice_assistant_version": version}, ([], []), [] + hass, + mock_client, + {"voice_assistant_feature_flags": voice_assistant_feature_flags}, + ([], []), + [], ) ).entry @@ -367,13 +374,28 @@ async def mock_voice_assistant_entry( @pytest.fixture async def mock_voice_assistant_v1_entry(mock_voice_assistant_entry) -> MockConfigEntry: """Set up an ESPHome entry with voice assistant.""" - return await mock_voice_assistant_entry(version=1) + return await mock_voice_assistant_entry( + voice_assistant_feature_flags=VoiceAssistantFeature.VOICE_ASSISTANT + ) @pytest.fixture async def mock_voice_assistant_v2_entry(mock_voice_assistant_entry) -> MockConfigEntry: """Set up an ESPHome entry with voice assistant.""" - return await mock_voice_assistant_entry(version=2) + return await mock_voice_assistant_entry( + voice_assistant_feature_flags=VoiceAssistantFeature.VOICE_ASSISTANT + | VoiceAssistantFeature.SPEAKER + ) + + +@pytest.fixture +async def mock_voice_assistant_api_entry(mock_voice_assistant_entry) -> MockConfigEntry: + """Set up an ESPHome entry with voice assistant.""" + return await mock_voice_assistant_entry( + voice_assistant_feature_flags=VoiceAssistantFeature.VOICE_ASSISTANT + | VoiceAssistantFeature.SPEAKER + | VoiceAssistantFeature.API_AUDIO + ) @pytest.fixture diff --git a/tests/components/esphome/test_diagnostics.py b/tests/components/esphome/test_diagnostics.py index 0f2b18218ff..1cf4f77875f 100644 --- a/tests/components/esphome/test_diagnostics.py +++ b/tests/components/esphome/test_diagnostics.py @@ -94,7 +94,8 @@ async def test_diagnostics_with_bluetooth( "project_version": "", "suggested_area": "", "uses_password": False, - "voice_assistant_version": 0, + "legacy_voice_assistant_version": 0, + "voice_assistant_feature_flags": 0, "webserver_port": 0, }, "services": [], diff --git a/tests/components/esphome/test_voice_assistant.py b/tests/components/esphome/test_voice_assistant.py index 9882419ed5a..e67d833656e 100644 --- a/tests/components/esphome/test_voice_assistant.py +++ b/tests/components/esphome/test_voice_assistant.py @@ -6,7 +6,7 @@ import socket from unittest.mock import Mock, patch import wave -from aioesphomeapi import VoiceAssistantEventType +from aioesphomeapi import APIClient, VoiceAssistantEventType import pytest from homeassistant.components.assist_pipeline import ( @@ -19,7 +19,10 @@ from homeassistant.components.assist_pipeline.error import ( WakeWordDetectionError, ) from homeassistant.components.esphome import DomainData -from homeassistant.components.esphome.voice_assistant import VoiceAssistantUDPServer +from homeassistant.components.esphome.voice_assistant import ( + VoiceAssistantAPIPipeline, + VoiceAssistantUDPPipeline, +) from homeassistant.core import HomeAssistant _TEST_INPUT_TEXT = "This is an input test" @@ -31,43 +34,54 @@ _ONE_SECOND = 16000 * 2 # 16Khz 16-bit @pytest.fixture -def voice_assistant_udp_server( +def voice_assistant_udp_pipeline( hass: HomeAssistant, -) -> VoiceAssistantUDPServer: - """Return the UDP server factory.""" +) -> VoiceAssistantUDPPipeline: + """Return the UDP pipeline factory.""" def _voice_assistant_udp_server(entry): entry_data = DomainData.get(hass).get_entry_data(entry) - server: VoiceAssistantUDPServer = None + server: VoiceAssistantUDPPipeline = None def handle_finished(): nonlocal server assert server is not None server.close() - server = VoiceAssistantUDPServer(hass, entry_data, Mock(), handle_finished) + server = VoiceAssistantUDPPipeline(hass, entry_data, Mock(), handle_finished) return server # noqa: RET504 return _voice_assistant_udp_server @pytest.fixture -def voice_assistant_udp_server_v1( - voice_assistant_udp_server, - mock_voice_assistant_v1_entry, -) -> VoiceAssistantUDPServer: - """Return the UDP server.""" - return voice_assistant_udp_server(entry=mock_voice_assistant_v1_entry) +def voice_assistant_api_pipeline( + hass: HomeAssistant, + mock_client, + mock_voice_assistant_api_entry, +) -> VoiceAssistantAPIPipeline: + """Return the API Pipeline factory.""" + entry_data = DomainData.get(hass).get_entry_data(mock_voice_assistant_api_entry) + return VoiceAssistantAPIPipeline(hass, entry_data, Mock(), Mock(), mock_client) @pytest.fixture -def voice_assistant_udp_server_v2( - voice_assistant_udp_server, +def voice_assistant_udp_pipeline_v1( + voice_assistant_udp_pipeline, + mock_voice_assistant_v1_entry, +) -> VoiceAssistantUDPPipeline: + """Return the UDP pipeline.""" + return voice_assistant_udp_pipeline(entry=mock_voice_assistant_v1_entry) + + +@pytest.fixture +def voice_assistant_udp_pipeline_v2( + voice_assistant_udp_pipeline, mock_voice_assistant_v2_entry, -) -> VoiceAssistantUDPServer: - """Return the UDP server.""" - return voice_assistant_udp_server(entry=mock_voice_assistant_v2_entry) +) -> VoiceAssistantUDPPipeline: + """Return the UDP pipeline.""" + return voice_assistant_udp_pipeline(entry=mock_voice_assistant_v2_entry) @pytest.fixture @@ -85,7 +99,7 @@ def test_wav() -> bytes: async def test_pipeline_events( hass: HomeAssistant, - voice_assistant_udp_server_v1: VoiceAssistantUDPServer, + voice_assistant_udp_pipeline_v1: VoiceAssistantUDPPipeline, ) -> None: """Test that the pipeline function is called.""" @@ -145,15 +159,15 @@ async def test_pipeline_events( elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_WAKE_WORD_END: assert data is None - voice_assistant_udp_server_v1.handle_event = handle_event + voice_assistant_udp_pipeline_v1.handle_event = handle_event with patch( "homeassistant.components.esphome.voice_assistant.async_pipeline_from_audio_stream", new=async_pipeline_from_audio_stream, ): - voice_assistant_udp_server_v1.transport = Mock() + voice_assistant_udp_pipeline_v1.transport = Mock() - await voice_assistant_udp_server_v1.run_pipeline( + await voice_assistant_udp_pipeline_v1.run_pipeline( device_id="mock-device-id", conversation_id=None ) @@ -162,7 +176,7 @@ async def test_udp_server( hass: HomeAssistant, socket_enabled, unused_udp_port_factory, - voice_assistant_udp_server_v1: VoiceAssistantUDPServer, + voice_assistant_udp_pipeline_v1: VoiceAssistantUDPPipeline, ) -> None: """Test the UDP server runs and queues incoming data.""" port_to_use = unused_udp_port_factory() @@ -170,93 +184,133 @@ async def test_udp_server( with patch( "homeassistant.components.esphome.voice_assistant.UDP_PORT", new=port_to_use ): - port = await voice_assistant_udp_server_v1.start_server() + port = await voice_assistant_udp_pipeline_v1.start_server() assert port == port_to_use sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - assert voice_assistant_udp_server_v1.queue.qsize() == 0 + assert voice_assistant_udp_pipeline_v1.queue.qsize() == 0 sock.sendto(b"test", ("127.0.0.1", port)) # Give the socket some time to send/receive the data async with asyncio.timeout(1): - while voice_assistant_udp_server_v1.queue.qsize() == 0: + while voice_assistant_udp_pipeline_v1.queue.qsize() == 0: await asyncio.sleep(0.1) - assert voice_assistant_udp_server_v1.queue.qsize() == 1 + assert voice_assistant_udp_pipeline_v1.queue.qsize() == 1 - voice_assistant_udp_server_v1.stop() - voice_assistant_udp_server_v1.close() + voice_assistant_udp_pipeline_v1.stop() + voice_assistant_udp_pipeline_v1.close() - assert voice_assistant_udp_server_v1.transport.is_closing() + assert voice_assistant_udp_pipeline_v1.transport.is_closing() async def test_udp_server_queue( hass: HomeAssistant, - voice_assistant_udp_server_v1: VoiceAssistantUDPServer, + voice_assistant_udp_pipeline_v1: VoiceAssistantUDPPipeline, ) -> None: """Test the UDP server queues incoming data.""" - voice_assistant_udp_server_v1.started = True + voice_assistant_udp_pipeline_v1.started = True - assert voice_assistant_udp_server_v1.queue.qsize() == 0 + assert voice_assistant_udp_pipeline_v1.queue.qsize() == 0 - voice_assistant_udp_server_v1.datagram_received(bytes(1024), ("localhost", 0)) - assert voice_assistant_udp_server_v1.queue.qsize() == 1 + voice_assistant_udp_pipeline_v1.datagram_received(bytes(1024), ("localhost", 0)) + assert voice_assistant_udp_pipeline_v1.queue.qsize() == 1 - voice_assistant_udp_server_v1.datagram_received(bytes(1024), ("localhost", 0)) - assert voice_assistant_udp_server_v1.queue.qsize() == 2 + voice_assistant_udp_pipeline_v1.datagram_received(bytes(1024), ("localhost", 0)) + assert voice_assistant_udp_pipeline_v1.queue.qsize() == 2 - async for data in voice_assistant_udp_server_v1._iterate_packets(): + async for data in voice_assistant_udp_pipeline_v1._iterate_packets(): assert data == bytes(1024) break - assert voice_assistant_udp_server_v1.queue.qsize() == 1 # One message removed + assert voice_assistant_udp_pipeline_v1.queue.qsize() == 1 # One message removed - voice_assistant_udp_server_v1.stop() + voice_assistant_udp_pipeline_v1.stop() assert ( - voice_assistant_udp_server_v1.queue.qsize() == 2 + voice_assistant_udp_pipeline_v1.queue.qsize() == 2 ) # An empty message added by stop - voice_assistant_udp_server_v1.datagram_received(bytes(1024), ("localhost", 0)) + voice_assistant_udp_pipeline_v1.datagram_received(bytes(1024), ("localhost", 0)) assert ( - voice_assistant_udp_server_v1.queue.qsize() == 2 + voice_assistant_udp_pipeline_v1.queue.qsize() == 2 ) # No new messages added after stop - voice_assistant_udp_server_v1.close() + voice_assistant_udp_pipeline_v1.close() # Stopping the UDP server should cause _iterate_packets to break out # immediately without yielding any data. has_data = False - async for _data in voice_assistant_udp_server_v1._iterate_packets(): + async for _data in voice_assistant_udp_pipeline_v1._iterate_packets(): has_data = True assert not has_data, "Server was stopped" +async def test_api_pipeline_queue( + hass: HomeAssistant, + voice_assistant_api_pipeline: VoiceAssistantAPIPipeline, +) -> None: + """Test the API pipeline queues incoming data.""" + + voice_assistant_api_pipeline.started = True + + assert voice_assistant_api_pipeline.queue.qsize() == 0 + + voice_assistant_api_pipeline.receive_audio_bytes(bytes(1024)) + assert voice_assistant_api_pipeline.queue.qsize() == 1 + + voice_assistant_api_pipeline.receive_audio_bytes(bytes(1024)) + assert voice_assistant_api_pipeline.queue.qsize() == 2 + + async for data in voice_assistant_api_pipeline._iterate_packets(): + assert data == bytes(1024) + break + assert voice_assistant_api_pipeline.queue.qsize() == 1 # One message removed + + voice_assistant_api_pipeline.stop() + assert ( + voice_assistant_api_pipeline.queue.qsize() == 2 + ) # An empty message added by stop + + voice_assistant_api_pipeline.receive_audio_bytes(bytes(1024)) + assert ( + voice_assistant_api_pipeline.queue.qsize() == 2 + ) # No new messages added after stop + + # Stopping the API Pipeline should cause _iterate_packets to break out + # immediately without yielding any data. + has_data = False + async for _data in voice_assistant_api_pipeline._iterate_packets(): + has_data = True + + assert not has_data, "Pipeline was stopped" + + async def test_error_calls_handle_finished( hass: HomeAssistant, - voice_assistant_udp_server_v1: VoiceAssistantUDPServer, + voice_assistant_udp_pipeline_v1: VoiceAssistantUDPPipeline, ) -> None: """Test that the handle_finished callback is called when an error occurs.""" - voice_assistant_udp_server_v1.handle_finished = Mock() + voice_assistant_udp_pipeline_v1.handle_finished = Mock() - voice_assistant_udp_server_v1.error_received(Exception()) + voice_assistant_udp_pipeline_v1.error_received(Exception()) - voice_assistant_udp_server_v1.handle_finished.assert_called() + voice_assistant_udp_pipeline_v1.handle_finished.assert_called() async def test_udp_server_multiple( hass: HomeAssistant, socket_enabled, unused_udp_port_factory, - voice_assistant_udp_server_v1: VoiceAssistantUDPServer, + voice_assistant_udp_pipeline_v1: VoiceAssistantUDPPipeline, ) -> None: """Test that the UDP server raises an error if started twice.""" with patch( "homeassistant.components.esphome.voice_assistant.UDP_PORT", new=unused_udp_port_factory(), ): - await voice_assistant_udp_server_v1.start_server() + await voice_assistant_udp_pipeline_v1.start_server() with ( patch( @@ -265,17 +319,17 @@ async def test_udp_server_multiple( ), pytest.raises(RuntimeError), ): - await voice_assistant_udp_server_v1.start_server() + await voice_assistant_udp_pipeline_v1.start_server() async def test_udp_server_after_stopped( hass: HomeAssistant, socket_enabled, unused_udp_port_factory, - voice_assistant_udp_server_v1: VoiceAssistantUDPServer, + voice_assistant_udp_pipeline_v1: VoiceAssistantUDPPipeline, ) -> None: """Test that the UDP server raises an error if started after stopped.""" - voice_assistant_udp_server_v1.close() + voice_assistant_udp_pipeline_v1.close() with ( patch( "homeassistant.components.esphome.voice_assistant.UDP_PORT", @@ -283,37 +337,37 @@ async def test_udp_server_after_stopped( ), pytest.raises(RuntimeError), ): - await voice_assistant_udp_server_v1.start_server() + await voice_assistant_udp_pipeline_v1.start_server() async def test_unknown_event_type( hass: HomeAssistant, - voice_assistant_udp_server_v1: VoiceAssistantUDPServer, + voice_assistant_api_pipeline: VoiceAssistantAPIPipeline, ) -> None: - """Test the UDP server does not call handle_event for unknown events.""" - voice_assistant_udp_server_v1._event_callback( + """Test the API pipeline does not call handle_event for unknown events.""" + voice_assistant_api_pipeline._event_callback( PipelineEvent( type="unknown-event", data={}, ) ) - assert not voice_assistant_udp_server_v1.handle_event.called + assert not voice_assistant_api_pipeline.handle_event.called async def test_error_event_type( hass: HomeAssistant, - voice_assistant_udp_server_v1: VoiceAssistantUDPServer, + voice_assistant_api_pipeline: VoiceAssistantAPIPipeline, ) -> None: - """Test the UDP server calls event handler with error.""" - voice_assistant_udp_server_v1._event_callback( + """Test the API pipeline calls event handler with error.""" + voice_assistant_api_pipeline._event_callback( PipelineEvent( type=PipelineEventType.ERROR, data={"code": "code", "message": "message"}, ) ) - voice_assistant_udp_server_v1.handle_event.assert_called_with( + voice_assistant_api_pipeline.handle_event.assert_called_with( VoiceAssistantEventType.VOICE_ASSISTANT_ERROR, {"code": "code", "message": "message"}, ) @@ -321,13 +375,13 @@ async def test_error_event_type( async def test_send_tts_not_called( hass: HomeAssistant, - voice_assistant_udp_server_v1: VoiceAssistantUDPServer, + voice_assistant_udp_pipeline_v1: VoiceAssistantUDPPipeline, ) -> None: """Test the UDP server with a v1 device does not call _send_tts.""" with patch( - "homeassistant.components.esphome.voice_assistant.VoiceAssistantUDPServer._send_tts" + "homeassistant.components.esphome.voice_assistant.VoiceAssistantPipeline._send_tts" ) as mock_send_tts: - voice_assistant_udp_server_v1._event_callback( + voice_assistant_udp_pipeline_v1._event_callback( PipelineEvent( type=PipelineEventType.TTS_END, data={ @@ -339,15 +393,35 @@ async def test_send_tts_not_called( mock_send_tts.assert_not_called() -async def test_send_tts_called( +async def test_send_tts_called_udp( hass: HomeAssistant, - voice_assistant_udp_server_v2: VoiceAssistantUDPServer, + voice_assistant_udp_pipeline_v2: VoiceAssistantUDPPipeline, ) -> None: """Test the UDP server with a v2 device calls _send_tts.""" with patch( - "homeassistant.components.esphome.voice_assistant.VoiceAssistantUDPServer._send_tts" + "homeassistant.components.esphome.voice_assistant.VoiceAssistantPipeline._send_tts" ) as mock_send_tts: - voice_assistant_udp_server_v2._event_callback( + voice_assistant_udp_pipeline_v2._event_callback( + PipelineEvent( + type=PipelineEventType.TTS_END, + data={ + "tts_output": {"media_id": _TEST_MEDIA_ID, "url": _TEST_OUTPUT_URL} + }, + ) + ) + + mock_send_tts.assert_called_with(_TEST_MEDIA_ID) + + +async def test_send_tts_called_api( + hass: HomeAssistant, + voice_assistant_api_pipeline: VoiceAssistantAPIPipeline, +) -> None: + """Test the API pipeline calls _send_tts.""" + with patch( + "homeassistant.components.esphome.voice_assistant.VoiceAssistantPipeline._send_tts" + ) as mock_send_tts: + voice_assistant_api_pipeline._event_callback( PipelineEvent( type=PipelineEventType.TTS_END, data={ @@ -361,29 +435,36 @@ async def test_send_tts_called( async def test_send_tts_not_called_when_empty( hass: HomeAssistant, - voice_assistant_udp_server_v1: VoiceAssistantUDPServer, - voice_assistant_udp_server_v2: VoiceAssistantUDPServer, + voice_assistant_udp_pipeline_v1: VoiceAssistantUDPPipeline, + voice_assistant_udp_pipeline_v2: VoiceAssistantUDPPipeline, + voice_assistant_api_pipeline: VoiceAssistantAPIPipeline, ) -> None: - """Test the UDP server with a v1/v2 device doesn't call _send_tts when the output is empty.""" + """Test the pipelines do not call _send_tts when the output is empty.""" with patch( - "homeassistant.components.esphome.voice_assistant.VoiceAssistantUDPServer._send_tts" + "homeassistant.components.esphome.voice_assistant.VoiceAssistantPipeline._send_tts" ) as mock_send_tts: - voice_assistant_udp_server_v1._event_callback( + voice_assistant_udp_pipeline_v1._event_callback( PipelineEvent(type=PipelineEventType.TTS_END, data={"tts_output": {}}) ) mock_send_tts.assert_not_called() - voice_assistant_udp_server_v2._event_callback( + voice_assistant_udp_pipeline_v2._event_callback( + PipelineEvent(type=PipelineEventType.TTS_END, data={"tts_output": {}}) + ) + + mock_send_tts.assert_not_called() + + voice_assistant_api_pipeline._event_callback( PipelineEvent(type=PipelineEventType.TTS_END, data={"tts_output": {}}) ) mock_send_tts.assert_not_called() -async def test_send_tts( +async def test_send_tts_udp( hass: HomeAssistant, - voice_assistant_udp_server_v2: VoiceAssistantUDPServer, + voice_assistant_udp_pipeline_v2: VoiceAssistantUDPPipeline, test_wav, ) -> None: """Test the UDP server calls sendto to transmit audio data to device.""" @@ -391,12 +472,12 @@ async def test_send_tts( "homeassistant.components.esphome.voice_assistant.tts.async_get_media_source_audio", return_value=("wav", test_wav), ): - voice_assistant_udp_server_v2.started = True - voice_assistant_udp_server_v2.transport = Mock(spec=asyncio.DatagramTransport) + voice_assistant_udp_pipeline_v2.started = True + voice_assistant_udp_pipeline_v2.transport = Mock(spec=asyncio.DatagramTransport) with patch.object( - voice_assistant_udp_server_v2.transport, "is_closing", return_value=False + voice_assistant_udp_pipeline_v2.transport, "is_closing", return_value=False ): - voice_assistant_udp_server_v2._event_callback( + voice_assistant_udp_pipeline_v2._event_callback( PipelineEvent( type=PipelineEventType.TTS_END, data={ @@ -408,16 +489,46 @@ async def test_send_tts( ) ) - await voice_assistant_udp_server_v2._tts_done.wait() + await voice_assistant_udp_pipeline_v2._tts_done.wait() - voice_assistant_udp_server_v2.transport.sendto.assert_called() + voice_assistant_udp_pipeline_v2.transport.sendto.assert_called() + + +async def test_send_tts_api( + hass: HomeAssistant, + mock_client: APIClient, + voice_assistant_api_pipeline: VoiceAssistantAPIPipeline, + test_wav, +) -> 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), + ): + voice_assistant_api_pipeline.started = True + + voice_assistant_api_pipeline._event_callback( + PipelineEvent( + type=PipelineEventType.TTS_END, + data={ + "tts_output": { + "media_id": _TEST_MEDIA_ID, + "url": _TEST_OUTPUT_URL, + } + }, + ) + ) + + await voice_assistant_api_pipeline._tts_done.wait() + + mock_client.send_voice_assistant_audio.assert_called() async def test_send_tts_wrong_sample_rate( hass: HomeAssistant, - voice_assistant_udp_server_v2: VoiceAssistantUDPServer, + voice_assistant_api_pipeline: VoiceAssistantAPIPipeline, ) -> None: - """Test the UDP server calls sendto to transmit audio data to device.""" + """Test that only 16000Hz audio will be streamed.""" with io.BytesIO() as wav_io: with wave.open(wav_io, "wb") as wav_file: wav_file.setframerate(22050) @@ -433,10 +544,10 @@ async def test_send_tts_wrong_sample_rate( ), pytest.raises(ValueError), ): - voice_assistant_udp_server_v2.started = True - voice_assistant_udp_server_v2.transport = Mock(spec=asyncio.DatagramTransport) + voice_assistant_api_pipeline.started = True + voice_assistant_api_pipeline.transport = Mock(spec=asyncio.DatagramTransport) - voice_assistant_udp_server_v2._event_callback( + voice_assistant_api_pipeline._event_callback( PipelineEvent( type=PipelineEventType.TTS_END, data={ @@ -445,13 +556,13 @@ async def test_send_tts_wrong_sample_rate( ) ) - assert voice_assistant_udp_server_v2._tts_task is not None - await voice_assistant_udp_server_v2._tts_task # raises ValueError + assert voice_assistant_api_pipeline._tts_task is not None + await voice_assistant_api_pipeline._tts_task # raises ValueError async def test_send_tts_wrong_format( hass: HomeAssistant, - voice_assistant_udp_server_v2: VoiceAssistantUDPServer, + voice_assistant_api_pipeline: VoiceAssistantAPIPipeline, ) -> None: """Test that only WAV audio will be streamed.""" with ( @@ -461,10 +572,10 @@ async def test_send_tts_wrong_format( ), pytest.raises(ValueError), ): - voice_assistant_udp_server_v2.started = True - voice_assistant_udp_server_v2.transport = Mock(spec=asyncio.DatagramTransport) + voice_assistant_api_pipeline.started = True + voice_assistant_api_pipeline.transport = Mock(spec=asyncio.DatagramTransport) - voice_assistant_udp_server_v2._event_callback( + voice_assistant_api_pipeline._event_callback( PipelineEvent( type=PipelineEventType.TTS_END, data={ @@ -473,13 +584,13 @@ async def test_send_tts_wrong_format( ) ) - assert voice_assistant_udp_server_v2._tts_task is not None - await voice_assistant_udp_server_v2._tts_task # raises ValueError + assert voice_assistant_api_pipeline._tts_task is not None + await voice_assistant_api_pipeline._tts_task # raises ValueError async def test_send_tts_not_started( hass: HomeAssistant, - voice_assistant_udp_server_v2: VoiceAssistantUDPServer, + voice_assistant_udp_pipeline_v2: VoiceAssistantUDPPipeline, test_wav, ) -> None: """Test the UDP server does not call sendto when not started.""" @@ -487,10 +598,10 @@ async def test_send_tts_not_started( "homeassistant.components.esphome.voice_assistant.tts.async_get_media_source_audio", return_value=("wav", test_wav), ): - voice_assistant_udp_server_v2.started = False - voice_assistant_udp_server_v2.transport = Mock(spec=asyncio.DatagramTransport) + voice_assistant_udp_pipeline_v2.started = False + voice_assistant_udp_pipeline_v2.transport = Mock(spec=asyncio.DatagramTransport) - voice_assistant_udp_server_v2._event_callback( + voice_assistant_udp_pipeline_v2._event_callback( PipelineEvent( type=PipelineEventType.TTS_END, data={ @@ -499,14 +610,41 @@ async def test_send_tts_not_started( ) ) - await voice_assistant_udp_server_v2._tts_done.wait() + await voice_assistant_udp_pipeline_v2._tts_done.wait() - voice_assistant_udp_server_v2.transport.sendto.assert_not_called() + voice_assistant_udp_pipeline_v2.transport.sendto.assert_not_called() + + +async def test_send_tts_transport_none( + hass: HomeAssistant, + voice_assistant_udp_pipeline_v2: VoiceAssistantUDPPipeline, + test_wav, + 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), + ): + voice_assistant_udp_pipeline_v2.started = True + voice_assistant_udp_pipeline_v2.transport = None + + voice_assistant_udp_pipeline_v2._event_callback( + PipelineEvent( + type=PipelineEventType.TTS_END, + data={ + "tts_output": {"media_id": _TEST_MEDIA_ID, "url": _TEST_OUTPUT_URL} + }, + ) + ) + await voice_assistant_udp_pipeline_v2._tts_done.wait() + + assert "No transport to send audio to" in caplog.text async def test_wake_word( hass: HomeAssistant, - voice_assistant_udp_server_v2: VoiceAssistantUDPServer, + voice_assistant_api_pipeline: VoiceAssistantAPIPipeline, ) -> None: """Test that the pipeline is set to start with Wake word.""" @@ -520,9 +658,7 @@ async def test_wake_word( ), patch("asyncio.Event.wait"), # TTS wait event ): - voice_assistant_udp_server_v2.transport = Mock() - - await voice_assistant_udp_server_v2.run_pipeline( + await voice_assistant_api_pipeline.run_pipeline( device_id="mock-device-id", conversation_id=None, flags=2, @@ -531,7 +667,7 @@ async def test_wake_word( async def test_wake_word_exception( hass: HomeAssistant, - voice_assistant_udp_server_v2: VoiceAssistantUDPServer, + voice_assistant_api_pipeline: VoiceAssistantAPIPipeline, ) -> None: """Test that the pipeline is set to start with Wake word.""" @@ -542,7 +678,6 @@ async def test_wake_word_exception( "homeassistant.components.esphome.voice_assistant.async_pipeline_from_audio_stream", new=async_pipeline_from_audio_stream, ): - voice_assistant_udp_server_v2.transport = Mock() def handle_event( event_type: VoiceAssistantEventType, data: dict[str, str] | None @@ -552,9 +687,9 @@ async def test_wake_word_exception( assert data["code"] == "pipeline-not-found" assert data["message"] == "Pipeline not found" - voice_assistant_udp_server_v2.handle_event = handle_event + voice_assistant_api_pipeline.handle_event = handle_event - await voice_assistant_udp_server_v2.run_pipeline( + await voice_assistant_api_pipeline.run_pipeline( device_id="mock-device-id", conversation_id=None, flags=2, @@ -563,7 +698,7 @@ async def test_wake_word_exception( async def test_wake_word_abort_exception( hass: HomeAssistant, - voice_assistant_udp_server_v2: VoiceAssistantUDPServer, + voice_assistant_api_pipeline: VoiceAssistantAPIPipeline, ) -> None: """Test that the pipeline is set to start with Wake word.""" @@ -575,13 +710,9 @@ async def test_wake_word_abort_exception( "homeassistant.components.esphome.voice_assistant.async_pipeline_from_audio_stream", new=async_pipeline_from_audio_stream, ), - patch.object( - voice_assistant_udp_server_v2, "handle_event" - ) as mock_handle_event, + patch.object(voice_assistant_api_pipeline, "handle_event") as mock_handle_event, ): - voice_assistant_udp_server_v2.transport = Mock() - - await voice_assistant_udp_server_v2.run_pipeline( + await voice_assistant_api_pipeline.run_pipeline( device_id="mock-device-id", conversation_id=None, flags=2, From 51d5d5124876e9f50247437664dbe179f7ad3bb7 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 9 Apr 2024 17:09:55 +0200 Subject: [PATCH 445/967] Bump pymodbus v3.6.7 (#115279) Bump pymodbus v3.6.7. --- homeassistant/components/modbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/modbus/manifest.json b/homeassistant/components/modbus/manifest.json index 956961c7e67..0fe8c7bc42d 100644 --- a/homeassistant/components/modbus/manifest.json +++ b/homeassistant/components/modbus/manifest.json @@ -6,5 +6,5 @@ "iot_class": "local_polling", "loggers": ["pymodbus"], "quality_scale": "platinum", - "requirements": ["pymodbus==3.6.6"] + "requirements": ["pymodbus==3.6.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6c705cb9a18..9003e2da53a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1980,7 +1980,7 @@ pymitv==1.4.3 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==3.6.6 +pymodbus==3.6.7 # homeassistant.components.monoprice pymonoprice==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e7b27bfa01a..03dfeb46e51 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1543,7 +1543,7 @@ pymeteoclimatic==0.1.0 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==3.6.6 +pymodbus==3.6.7 # homeassistant.components.monoprice pymonoprice==0.4 From 2df6f1849fbc90157808f5e9cbfec1d33b4d677d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 9 Apr 2024 11:10:03 -0400 Subject: [PATCH 446/967] Add OpenAI conversation entity (#114942) * Add OpenAI conversation entity * Add migration --- .../openai_conversation/__init__.py | 128 +----------- .../components/openai_conversation/const.py | 3 + .../openai_conversation/conversation.py | 145 +++++++++++++ .../openai_conversation/manifest.json | 1 + .../openai_conversation/conftest.py | 1 + .../snapshots/test_conversation.ambr | 67 ++++++ .../snapshots/test_init.ambr | 34 --- .../openai_conversation/test_conversation.py | 196 ++++++++++++++++++ .../openai_conversation/test_init.py | 184 +--------------- 9 files changed, 425 insertions(+), 334 deletions(-) create mode 100644 homeassistant/components/openai_conversation/conversation.py create mode 100644 tests/components/openai_conversation/snapshots/test_conversation.ambr delete mode 100644 tests/components/openai_conversation/snapshots/test_init.ambr create mode 100644 tests/components/openai_conversation/test_conversation.py diff --git a/homeassistant/components/openai_conversation/__init__.py b/homeassistant/components/openai_conversation/__init__.py index 07e872a0f5d..ffbfc1799c5 100644 --- a/homeassistant/components/openai_conversation/__init__.py +++ b/homeassistant/components/openai_conversation/__init__.py @@ -2,53 +2,30 @@ from __future__ import annotations -import logging -from typing import Literal - import openai import voluptuous as vol from homeassistant.components import conversation from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, MATCH_ALL +from homeassistant.const import CONF_API_KEY, Platform from homeassistant.core import ( HomeAssistant, ServiceCall, ServiceResponse, SupportsResponse, ) -from homeassistant.exceptions import ( - ConfigEntryNotReady, - HomeAssistantError, - TemplateError, -) +from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.helpers import ( config_validation as cv, - intent, issue_registry as ir, selector, - template, ) from homeassistant.helpers.typing import ConfigType -from homeassistant.util import ulid -from .const import ( - CONF_CHAT_MODEL, - CONF_MAX_TOKENS, - CONF_PROMPT, - CONF_TEMPERATURE, - CONF_TOP_P, - DEFAULT_CHAT_MODEL, - DEFAULT_MAX_TOKENS, - DEFAULT_PROMPT, - DEFAULT_TEMPERATURE, - DEFAULT_TOP_P, - DOMAIN, -) +from .const import DOMAIN, LOGGER -_LOGGER = logging.getLogger(__name__) SERVICE_GENERATE_IMAGE = "generate_image" - +PLATFORMS = (Platform.CONVERSATION,) CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) @@ -120,108 +97,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await hass.async_add_executor_job(client.with_options(timeout=10.0).models.list) except openai.AuthenticationError as err: - _LOGGER.error("Invalid API key: %s", err) + LOGGER.error("Invalid API key: %s", err) return False except openai.OpenAIError as err: raise ConfigEntryNotReady(err) from err hass.data.setdefault(DOMAIN, {})[entry.entry_id] = client - conversation.async_set_agent(hass, entry, OpenAIAgent(hass, entry)) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True 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) conversation.async_unset_agent(hass, entry) return True - - -class OpenAIAgent(conversation.AbstractConversationAgent): - """OpenAI conversation agent.""" - - def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: - """Initialize the agent.""" - self.hass = hass - self.entry = entry - self.history: dict[str, list[dict]] = {} - - @property - def supported_languages(self) -> list[str] | Literal["*"]: - """Return a list of supported languages.""" - return MATCH_ALL - - async def async_process( - self, user_input: conversation.ConversationInput - ) -> conversation.ConversationResult: - """Process a sentence.""" - raw_prompt = self.entry.options.get(CONF_PROMPT, DEFAULT_PROMPT) - model = self.entry.options.get(CONF_CHAT_MODEL, DEFAULT_CHAT_MODEL) - max_tokens = self.entry.options.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS) - top_p = self.entry.options.get(CONF_TOP_P, DEFAULT_TOP_P) - temperature = self.entry.options.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE) - - 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() - try: - prompt = self._async_generate_prompt(raw_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 = [{"role": "system", "content": prompt}] - - messages.append({"role": "user", "content": user_input.text}) - - _LOGGER.debug("Prompt for %s: %s", model, messages) - - client = self.hass.data[DOMAIN][self.entry.entry_id] - - try: - result = await client.chat.completions.create( - model=model, - messages=messages, - max_tokens=max_tokens, - top_p=top_p, - temperature=temperature, - user=conversation_id, - ) - except openai.OpenAIError as err: - intent_response = intent.IntentResponse(language=user_input.language) - intent_response.async_set_error( - intent.IntentResponseErrorCode.UNKNOWN, - f"Sorry, I had a problem talking to OpenAI: {err}", - ) - return conversation.ConversationResult( - response=intent_response, conversation_id=conversation_id - ) - - _LOGGER.debug("Response %s", result) - response = result.choices[0].message.model_dump(include={"role", "content"}) - messages.append(response) - self.history[conversation_id] = messages - - intent_response = intent.IntentResponse(language=user_input.language) - intent_response.async_set_speech(response["content"]) - return conversation.ConversationResult( - response=intent_response, conversation_id=conversation_id - ) - - def _async_generate_prompt(self, raw_prompt: str) -> str: - """Generate a prompt for the user.""" - return template.Template(raw_prompt, self.hass).async_render( - { - "ha_name": self.hass.config.location_name, - }, - parse_result=False, - ) diff --git a/homeassistant/components/openai_conversation/const.py b/homeassistant/components/openai_conversation/const.py index 46f8603c5f1..ee4a107c241 100644 --- a/homeassistant/components/openai_conversation/const.py +++ b/homeassistant/components/openai_conversation/const.py @@ -1,6 +1,9 @@ """Constants for the OpenAI Conversation integration.""" +import logging + DOMAIN = "openai_conversation" +LOGGER = logging.getLogger(__name__) CONF_PROMPT = "prompt" DEFAULT_PROMPT = """This smart home is controlled by Home Assistant. diff --git a/homeassistant/components/openai_conversation/conversation.py b/homeassistant/components/openai_conversation/conversation.py new file mode 100644 index 00000000000..158b155c75d --- /dev/null +++ b/homeassistant/components/openai_conversation/conversation.py @@ -0,0 +1,145 @@ +"""Conversation support for OpenAI.""" + +from typing import Literal + +import openai + +from homeassistant.components import assist_pipeline, conversation +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import MATCH_ALL +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import TemplateError +from homeassistant.helpers import intent, template +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util import ulid + +from .const import ( + CONF_CHAT_MODEL, + CONF_MAX_TOKENS, + CONF_PROMPT, + CONF_TEMPERATURE, + CONF_TOP_P, + DEFAULT_CHAT_MODEL, + DEFAULT_MAX_TOKENS, + DEFAULT_PROMPT, + DEFAULT_TEMPERATURE, + DEFAULT_TOP_P, + DOMAIN, + LOGGER, +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up conversation entities.""" + agent = OpenAIConversationEntity(hass, config_entry) + async_add_entities([agent]) + + +class OpenAIConversationEntity( + conversation.ConversationEntity, conversation.AbstractConversationAgent +): + """OpenAI conversation agent.""" + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Initialize the agent.""" + self.hass = hass + self.entry = entry + self.history: dict[str, list[dict]] = {} + self._attr_name = entry.title + self._attr_unique_id = entry.entry_id + + @property + def supported_languages(self) -> list[str] | Literal["*"]: + """Return a list of supported languages.""" + return MATCH_ALL + + async def async_added_to_hass(self) -> None: + """When entity is added to Home Assistant.""" + await super().async_added_to_hass() + assist_pipeline.async_migrate_engine( + self.hass, "conversation", self.entry.entry_id, self.entity_id + ) + conversation.async_set_agent(self.hass, self.entry, self) + + async def async_will_remove_from_hass(self) -> None: + """When entity will be removed from Home Assistant.""" + conversation.async_unset_agent(self.hass, self.entry) + await super().async_will_remove_from_hass() + + async def async_process( + self, user_input: conversation.ConversationInput + ) -> conversation.ConversationResult: + """Process a sentence.""" + raw_prompt = self.entry.options.get(CONF_PROMPT, DEFAULT_PROMPT) + model = self.entry.options.get(CONF_CHAT_MODEL, DEFAULT_CHAT_MODEL) + max_tokens = self.entry.options.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS) + top_p = self.entry.options.get(CONF_TOP_P, DEFAULT_TOP_P) + temperature = self.entry.options.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE) + + 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() + try: + prompt = self._async_generate_prompt(raw_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 = [{"role": "system", "content": prompt}] + + messages.append({"role": "user", "content": user_input.text}) + + LOGGER.debug("Prompt for %s: %s", model, messages) + + client = self.hass.data[DOMAIN][self.entry.entry_id] + + try: + result = await client.chat.completions.create( + model=model, + messages=messages, + max_tokens=max_tokens, + top_p=top_p, + temperature=temperature, + user=conversation_id, + ) + except openai.OpenAIError as err: + intent_response = intent.IntentResponse(language=user_input.language) + intent_response.async_set_error( + intent.IntentResponseErrorCode.UNKNOWN, + f"Sorry, I had a problem talking to OpenAI: {err}", + ) + return conversation.ConversationResult( + response=intent_response, conversation_id=conversation_id + ) + + LOGGER.debug("Response %s", result) + response = result.choices[0].message.model_dump(include={"role", "content"}) + messages.append(response) + self.history[conversation_id] = messages + + intent_response = intent.IntentResponse(language=user_input.language) + intent_response.async_set_speech(response["content"]) + return conversation.ConversationResult( + response=intent_response, conversation_id=conversation_id + ) + + def _async_generate_prompt(self, raw_prompt: str) -> str: + """Generate a prompt for the user.""" + return template.Template(raw_prompt, self.hass).async_render( + { + "ha_name": self.hass.config.location_name, + }, + parse_result=False, + ) diff --git a/homeassistant/components/openai_conversation/manifest.json b/homeassistant/components/openai_conversation/manifest.json index 5138be96b55..b71c84e2081 100644 --- a/homeassistant/components/openai_conversation/manifest.json +++ b/homeassistant/components/openai_conversation/manifest.json @@ -1,6 +1,7 @@ { "domain": "openai_conversation", "name": "OpenAI Conversation", + "after_dependencies": ["assist_pipeline"], "codeowners": ["@balloob"], "config_flow": true, "dependencies": ["conversation"], diff --git a/tests/components/openai_conversation/conftest.py b/tests/components/openai_conversation/conftest.py index 1597fa79d0a..272c23a9510 100644 --- a/tests/components/openai_conversation/conftest.py +++ b/tests/components/openai_conversation/conftest.py @@ -14,6 +14,7 @@ from tests.common import MockConfigEntry def mock_config_entry(hass): """Mock a config entry.""" entry = MockConfigEntry( + title="OpenAI", domain="openai_conversation", data={ "api_key": "bla", diff --git a/tests/components/openai_conversation/snapshots/test_conversation.ambr b/tests/components/openai_conversation/snapshots/test_conversation.ambr new file mode 100644 index 00000000000..1a488bb948c --- /dev/null +++ b/tests/components/openai_conversation/snapshots/test_conversation.ambr @@ -0,0 +1,67 @@ +# serializer version: 1 +# name: test_default_prompt[None] + list([ + dict({ + 'content': ''' + This smart home is controlled by Home Assistant. + + An overview of the areas and the devices in this smart home: + + Test Area: + - Test Device (Test Model) + + Test Area 2: + - Test Device 2 + - Test Device 3 (Test Model 3A) + - Test Device 4 + - 1 (3) + + Answer the user's questions about the world truthfully. + + If the user wants to control a device, reject the request and suggest using the Home Assistant app. + ''', + 'role': 'system', + }), + dict({ + 'content': 'hello', + 'role': 'user', + }), + dict({ + 'content': 'Hello, how can I help you?', + 'role': 'assistant', + }), + ]) +# --- +# name: test_default_prompt[conversation.openai] + list([ + dict({ + 'content': ''' + This smart home is controlled by Home Assistant. + + An overview of the areas and the devices in this smart home: + + Test Area: + - Test Device (Test Model) + + Test Area 2: + - Test Device 2 + - Test Device 3 (Test Model 3A) + - Test Device 4 + - 1 (3) + + Answer the user's questions about the world truthfully. + + If the user wants to control a device, reject the request and suggest using the Home Assistant app. + ''', + 'role': 'system', + }), + dict({ + 'content': 'hello', + 'role': 'user', + }), + dict({ + 'content': 'Hello, how can I help you?', + 'role': 'assistant', + }), + ]) +# --- diff --git a/tests/components/openai_conversation/snapshots/test_init.ambr b/tests/components/openai_conversation/snapshots/test_init.ambr deleted file mode 100644 index bc06f51f416..00000000000 --- a/tests/components/openai_conversation/snapshots/test_init.ambr +++ /dev/null @@ -1,34 +0,0 @@ -# serializer version: 1 -# name: test_default_prompt - list([ - dict({ - 'content': ''' - This smart home is controlled by Home Assistant. - - An overview of the areas and the devices in this smart home: - - Test Area: - - Test Device (Test Model) - - Test Area 2: - - Test Device 2 - - Test Device 3 (Test Model 3A) - - Test Device 4 - - 1 (3) - - Answer the user's questions about the world truthfully. - - If the user wants to control a device, reject the request and suggest using the Home Assistant app. - ''', - 'role': 'system', - }), - dict({ - 'content': 'hello', - 'role': 'user', - }), - dict({ - 'content': 'Hello, how can I help you?', - 'role': 'assistant', - }), - ]) -# --- diff --git a/tests/components/openai_conversation/test_conversation.py b/tests/components/openai_conversation/test_conversation.py new file mode 100644 index 00000000000..9e50204cdde --- /dev/null +++ b/tests/components/openai_conversation/test_conversation.py @@ -0,0 +1,196 @@ +"""Tests for the OpenAI integration.""" + +from unittest.mock import AsyncMock, patch + +from httpx import Response +from openai import RateLimitError +from openai.types.chat.chat_completion import ChatCompletion, Choice +from openai.types.chat.chat_completion_message import ChatCompletionMessage +from openai.types.completion_usage import CompletionUsage +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components import conversation +from homeassistant.core import Context, HomeAssistant +from homeassistant.helpers import area_registry as ar, device_registry as dr, intent + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize("agent_id", [None, "conversation.openai"]) +async def test_default_prompt( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_init_component, + area_registry: ar.AreaRegistry, + device_registry: dr.DeviceRegistry, + snapshot: SnapshotAssertion, + agent_id: str, +) -> None: + """Test that the default prompt works.""" + entry = MockConfigEntry(title=None) + entry.add_to_hass(hass) + for i in range(3): + area_registry.async_create(f"{i}Empty Area") + + if agent_id is None: + agent_id = mock_config_entry.entry_id + + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + connections={("test", "1234")}, + name="Test Device", + manufacturer="Test Manufacturer", + model="Test Model", + suggested_area="Test Area", + ) + for i in range(3): + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + connections={("test", f"{i}abcd")}, + name="Test Service", + manufacturer="Test Manufacturer", + model="Test Model", + suggested_area="Test Area", + entry_type=dr.DeviceEntryType.SERVICE, + ) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + connections={("test", "5678")}, + name="Test Device 2", + manufacturer="Test Manufacturer 2", + model="Device 2", + suggested_area="Test Area 2", + ) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + connections={("test", "9876")}, + name="Test Device 3", + manufacturer="Test Manufacturer 3", + model="Test Model 3A", + suggested_area="Test Area 2", + ) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + connections={("test", "qwer")}, + name="Test Device 4", + suggested_area="Test Area 2", + ) + device = device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + connections={("test", "9876-disabled")}, + name="Test Device 3", + manufacturer="Test Manufacturer 3", + model="Test Model 3A", + suggested_area="Test Area 2", + ) + device_registry.async_update_device( + device.id, disabled_by=dr.DeviceEntryDisabler.USER + ) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + connections={("test", "9876-no-name")}, + manufacturer="Test Manufacturer NoName", + model="Test Model NoName", + suggested_area="Test Area 2", + ) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + connections={("test", "9876-integer-values")}, + name=1, + manufacturer=2, + model=3, + suggested_area="Test Area 2", + ) + with patch( + "openai.resources.chat.completions.AsyncCompletions.create", + new_callable=AsyncMock, + return_value=ChatCompletion( + id="chatcmpl-1234567890ABCDEFGHIJKLMNOPQRS", + choices=[ + Choice( + finish_reason="stop", + index=0, + message=ChatCompletionMessage( + content="Hello, how can I help you?", + role="assistant", + function_call=None, + tool_calls=None, + ), + ) + ], + created=1700000000, + model="gpt-3.5-turbo-0613", + object="chat.completion", + system_fingerprint=None, + usage=CompletionUsage( + completion_tokens=9, prompt_tokens=8, total_tokens=17 + ), + ), + ) as mock_create: + result = await conversation.async_converse( + hass, "hello", None, Context(), agent_id=agent_id + ) + + assert result.response.response_type == intent.IntentResponseType.ACTION_DONE + assert mock_create.mock_calls[0][2]["messages"] == snapshot + + +async def test_error_handling( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_init_component +) -> None: + """Test that the default prompt works.""" + with patch( + "openai.resources.chat.completions.AsyncCompletions.create", + new_callable=AsyncMock, + side_effect=RateLimitError( + response=Response(status_code=None, request=""), body=None, message=None + ), + ): + result = await conversation.async_converse( + hass, "hello", None, Context(), agent_id=mock_config_entry.entry_id + ) + + assert result.response.response_type == intent.IntentResponseType.ERROR, result + assert result.response.error_code == "unknown", result + + +async def test_template_error( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test that template error handling works.""" + hass.config_entries.async_update_entry( + mock_config_entry, + options={ + "prompt": "talk like a {% if True %}smarthome{% else %}pirate please.", + }, + ) + with ( + patch( + "openai.resources.models.AsyncModels.list", + ), + patch( + "openai.resources.chat.completions.AsyncCompletions.create", + new_callable=AsyncMock, + ), + ): + 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.ERROR, result + assert result.response.error_code == "unknown", result + + +async def test_conversation_agent( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_init_component, +) -> None: + """Test OpenAIAgent.""" + agent = conversation.get_agent_manager(hass).async_get_agent( + mock_config_entry.entry_id + ) + assert agent.supported_languages == "*" diff --git a/tests/components/openai_conversation/test_init.py b/tests/components/openai_conversation/test_init.py index 2702b749a64..773ba3bca06 100644 --- a/tests/components/openai_conversation/test_init.py +++ b/tests/components/openai_conversation/test_init.py @@ -1,6 +1,6 @@ """Tests for the OpenAI integration.""" -from unittest.mock import AsyncMock, patch +from unittest.mock import patch from httpx import Response from openai import ( @@ -9,197 +9,17 @@ from openai import ( BadRequestError, RateLimitError, ) -from openai.types.chat.chat_completion import ChatCompletion, Choice -from openai.types.chat.chat_completion_message import ChatCompletionMessage -from openai.types.completion_usage import CompletionUsage from openai.types.image import Image from openai.types.images_response import ImagesResponse import pytest -from syrupy.assertion import SnapshotAssertion -from homeassistant.components import conversation -from homeassistant.core import Context, HomeAssistant +from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import area_registry as ar, device_registry as dr, intent from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry -async def test_default_prompt( - hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_init_component, - area_registry: ar.AreaRegistry, - device_registry: dr.DeviceRegistry, - snapshot: SnapshotAssertion, -) -> None: - """Test that the default prompt works.""" - entry = MockConfigEntry(title=None) - entry.add_to_hass(hass) - for i in range(3): - area_registry.async_create(f"{i}Empty Area") - - device_registry.async_get_or_create( - config_entry_id=entry.entry_id, - connections={("test", "1234")}, - name="Test Device", - manufacturer="Test Manufacturer", - model="Test Model", - suggested_area="Test Area", - ) - for i in range(3): - device_registry.async_get_or_create( - config_entry_id=entry.entry_id, - connections={("test", f"{i}abcd")}, - name="Test Service", - manufacturer="Test Manufacturer", - model="Test Model", - suggested_area="Test Area", - entry_type=dr.DeviceEntryType.SERVICE, - ) - device_registry.async_get_or_create( - config_entry_id=entry.entry_id, - connections={("test", "5678")}, - name="Test Device 2", - manufacturer="Test Manufacturer 2", - model="Device 2", - suggested_area="Test Area 2", - ) - device_registry.async_get_or_create( - config_entry_id=entry.entry_id, - connections={("test", "9876")}, - name="Test Device 3", - manufacturer="Test Manufacturer 3", - model="Test Model 3A", - suggested_area="Test Area 2", - ) - device_registry.async_get_or_create( - config_entry_id=entry.entry_id, - connections={("test", "qwer")}, - name="Test Device 4", - suggested_area="Test Area 2", - ) - device = device_registry.async_get_or_create( - config_entry_id=entry.entry_id, - connections={("test", "9876-disabled")}, - name="Test Device 3", - manufacturer="Test Manufacturer 3", - model="Test Model 3A", - suggested_area="Test Area 2", - ) - device_registry.async_update_device( - device.id, disabled_by=dr.DeviceEntryDisabler.USER - ) - device_registry.async_get_or_create( - config_entry_id=entry.entry_id, - connections={("test", "9876-no-name")}, - manufacturer="Test Manufacturer NoName", - model="Test Model NoName", - suggested_area="Test Area 2", - ) - device_registry.async_get_or_create( - config_entry_id=entry.entry_id, - connections={("test", "9876-integer-values")}, - name=1, - manufacturer=2, - model=3, - suggested_area="Test Area 2", - ) - with patch( - "openai.resources.chat.completions.AsyncCompletions.create", - new_callable=AsyncMock, - return_value=ChatCompletion( - id="chatcmpl-1234567890ABCDEFGHIJKLMNOPQRS", - choices=[ - Choice( - finish_reason="stop", - index=0, - message=ChatCompletionMessage( - content="Hello, how can I help you?", - role="assistant", - function_call=None, - tool_calls=None, - ), - ) - ], - created=1700000000, - model="gpt-3.5-turbo-0613", - object="chat.completion", - system_fingerprint=None, - usage=CompletionUsage( - completion_tokens=9, prompt_tokens=8, total_tokens=17 - ), - ), - ) as mock_create: - 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 - assert mock_create.mock_calls[0][2]["messages"] == snapshot - - -async def test_error_handling( - hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_init_component -) -> None: - """Test that the default prompt works.""" - with patch( - "openai.resources.chat.completions.AsyncCompletions.create", - new_callable=AsyncMock, - side_effect=RateLimitError( - response=Response(status_code=None, request=""), body=None, message=None - ), - ): - result = await conversation.async_converse( - hass, "hello", None, Context(), agent_id=mock_config_entry.entry_id - ) - - assert result.response.response_type == intent.IntentResponseType.ERROR, result - assert result.response.error_code == "unknown", result - - -async def test_template_error( - hass: HomeAssistant, mock_config_entry: MockConfigEntry -) -> None: - """Test that template error handling works.""" - hass.config_entries.async_update_entry( - mock_config_entry, - options={ - "prompt": "talk like a {% if True %}smarthome{% else %}pirate please.", - }, - ) - with ( - patch( - "openai.resources.models.AsyncModels.list", - ), - patch( - "openai.resources.chat.completions.AsyncCompletions.create", - new_callable=AsyncMock, - ), - ): - 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.ERROR, result - assert result.response.error_code == "unknown", result - - -async def test_conversation_agent( - hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_init_component, -) -> None: - """Test OpenAIAgent.""" - agent = conversation.get_agent_manager(hass).async_get_agent( - mock_config_entry.entry_id - ) - assert agent.supported_languages == "*" - - @pytest.mark.parametrize( ("service_data", "expected_args"), [ From d2dcf04946cef6debf667eebf76b3924b7ef9ae3 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Tue, 9 Apr 2024 18:34:04 +0200 Subject: [PATCH 447/967] Bump forecast-solar lib to v3.1.0 (#115272) --- homeassistant/components/forecast_solar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/forecast_solar/manifest.json b/homeassistant/components/forecast_solar/manifest.json index 94b603e108c..f5dd79281e6 100644 --- a/homeassistant/components/forecast_solar/manifest.json +++ b/homeassistant/components/forecast_solar/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "quality_scale": "platinum", - "requirements": ["forecast-solar==3.0.0"] + "requirements": ["forecast-solar==3.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9003e2da53a..4df01955f11 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -887,7 +887,7 @@ fnv-hash-fast==0.5.0 foobot_async==1.0.0 # homeassistant.components.forecast_solar -forecast-solar==3.0.0 +forecast-solar==3.1.0 # homeassistant.components.fortios fortiosapi==1.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 03dfeb46e51..33129c58871 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -728,7 +728,7 @@ fnv-hash-fast==0.5.0 foobot_async==1.0.0 # homeassistant.components.forecast_solar -forecast-solar==3.0.0 +forecast-solar==3.1.0 # homeassistant.components.freebox freebox-api==1.1.0 From e7c247f1f0d103d099f36200fa8b6cf2bfd77feb Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 9 Apr 2024 18:34:29 +0200 Subject: [PATCH 448/967] Remove Twitch YAML import (#115278) --- .../components/twitch/config_flow.py | 82 +--------------- homeassistant/components/twitch/sensor.py | 57 +---------- homeassistant/components/twitch/strings.json | 14 --- tests/components/twitch/test_config_flow.py | 97 +------------------ tests/components/twitch/test_sensor.py | 57 ----------- 5 files changed, 7 insertions(+), 300 deletions(-) diff --git a/homeassistant/components/twitch/config_flow.py b/homeassistant/components/twitch/config_flow.py index 186d097a22b..146d2f39088 100644 --- a/homeassistant/components/twitch/config_flow.py +++ b/homeassistant/components/twitch/config_flow.py @@ -8,17 +8,13 @@ from typing import Any, cast from twitchAPI.helper import first from twitchAPI.twitch import Twitch -from twitchAPI.type import AuthScope, InvalidTokenException from homeassistant.config_entries import ConfigEntry, ConfigFlowResult -from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CLIENT_ID, CONF_TOKEN -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN -from homeassistant.data_entry_flow import AbortFlow +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers.config_entry_oauth2_flow import LocalOAuth2Implementation -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from .const import CONF_CHANNELS, CONF_REFRESH_TOKEN, DOMAIN, LOGGER, OAUTH_SCOPES +from .const import CONF_CHANNELS, DOMAIN, LOGGER, OAUTH_SCOPES class OAuth2FlowHandler( @@ -121,77 +117,3 @@ class OAuth2FlowHandler( if user_input is None: return self.async_show_form(step_id="reauth_confirm") return await self.async_step_user() - - async def async_step_import(self, config: dict[str, Any]) -> ConfigFlowResult: - """Import from yaml.""" - client = await Twitch( - app_id=config[CONF_CLIENT_ID], - authenticate_app=False, - ) - client.auto_refresh_auth = False - token = config[CONF_TOKEN] - try: - await client.set_user_authentication( - token, validate=True, scope=[AuthScope.USER_READ_SUBSCRIPTIONS] - ) - except InvalidTokenException: - async_create_issue( - self.hass, - DOMAIN, - "deprecated_yaml_invalid_token", - breaks_in_ha_version="2024.4.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml_invalid_token", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "Twitch", - }, - ) - return self.async_abort(reason="invalid_token") - user = await first(client.get_users()) - assert user - await self.async_set_unique_id(user.id) - try: - self._abort_if_unique_id_configured() - except AbortFlow: - async_create_issue( - self.hass, - DOMAIN, - "deprecated_yaml_already_imported", - breaks_in_ha_version="2024.4.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml_already_imported", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "Twitch", - }, - ) - raise - async_create_issue( - self.hass, - HOMEASSISTANT_DOMAIN, - "deprecated_yaml", - breaks_in_ha_version="2024.4.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "Twitch", - }, - ) - return self.async_create_entry( - title=user.display_name, - data={ - "auth_implementation": DOMAIN, - CONF_TOKEN: { - CONF_ACCESS_TOKEN: token, - CONF_REFRESH_TOKEN: "", - "expires_at": 0, - }, - "imported": True, - }, - options={CONF_CHANNELS: config[CONF_CHANNELS]}, - ) diff --git a/homeassistant/components/twitch/sensor.py b/homeassistant/components/twitch/sensor.py index 1107513080a..bcd9e95a1ae 100644 --- a/homeassistant/components/twitch/sensor.py +++ b/homeassistant/components/twitch/sensor.py @@ -10,34 +10,15 @@ from twitchAPI.twitch import ( TwitchResourceNotFound, TwitchUser, ) -import voluptuous as vol -from homeassistant.components.application_credentials import ( - ClientCredential, - async_import_client_credential, -) -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_TOKEN +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session -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 .const import CLIENT, CONF_CHANNELS, DOMAIN, LOGGER, OAUTH_SCOPES, SESSION -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_CLIENT_ID): cv.string, - vol.Required(CONF_CLIENT_SECRET): cv.string, - vol.Required(CONF_CHANNELS): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_TOKEN): cv.string, - } -) - - ATTR_GAME = "game" ATTR_TITLE = "title" ATTR_SUBSCRIPTION = "subscribed" @@ -59,40 +40,6 @@ def chunk_list(lst: list, chunk_size: int) -> list[list]: return [lst[i : i + chunk_size] for i in range(0, len(lst), chunk_size)] -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the Twitch platform.""" - await async_import_client_credential( - hass, - DOMAIN, - ClientCredential(config[CONF_CLIENT_ID], config[CONF_CLIENT_SECRET]), - ) - if CONF_TOKEN in config: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config - ) - ) - else: - async_create_issue( - hass, - DOMAIN, - "deprecated_yaml_credentials_imported", - breaks_in_ha_version="2024.4.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml_credentials_imported", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "Twitch", - }, - ) - - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, diff --git a/homeassistant/components/twitch/strings.json b/homeassistant/components/twitch/strings.json index f4128a15adc..bbe46526c36 100644 --- a/homeassistant/components/twitch/strings.json +++ b/homeassistant/components/twitch/strings.json @@ -16,19 +16,5 @@ "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]", "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]" } - }, - "issues": { - "deprecated_yaml_invalid_token": { - "title": "The {integration_title} YAML configuration is being removed", - "description": "Configuring {integration_title} using YAML is being removed.\n\nYour configuration couldn't be imported because the token in the configuration.yaml was invalid.\n\nPlease add Twitch again via the UI.\n\nRemove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue." - }, - "deprecated_yaml_credentials_imported": { - "title": "The {integration_title} YAML configuration is being removed", - "description": "Configuring {integration_title} using YAML is being removed.\n\nYour application credentials are imported, but a config entry could not be created because there was no access token.\n\nPlease add Twitch again via the UI.\n\nRemove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue." - }, - "deprecated_yaml_already_imported": { - "title": "The {integration_title} YAML configuration is being removed", - "description": "Configuring {integration_title} using YAML is being removed.\n\nYour application credentials are already imported.\n\nRemove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue." - } } } diff --git a/tests/components/twitch/test_config_flow.py b/tests/components/twitch/test_config_flow.py index f9d8be4a5d2..94fa2ce0427 100644 --- a/tests/components/twitch/test_config_flow.py +++ b/tests/components/twitch/test_config_flow.py @@ -2,23 +2,20 @@ from unittest.mock import patch -import pytest - from homeassistant.components.twitch.const import ( CONF_CHANNELS, DOMAIN, OAUTH2_AUTHORIZE, ) -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, SOURCE_USER -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_TOKEN +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult, FlowResultType -from homeassistant.helpers import config_entry_oauth2_flow, issue_registry as ir +from homeassistant.helpers import config_entry_oauth2_flow from . import setup_integration from tests.common import MockConfigEntry -from tests.components.twitch import TwitchInvalidTokenMock, TwitchMock +from tests.components.twitch import TwitchMock from tests.components.twitch.conftest import CLIENT_ID, TITLE from tests.typing import ClientSessionGenerator @@ -206,91 +203,3 @@ async def test_reauth_wrong_account( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "wrong_account" - - -async def test_import( - hass: HomeAssistant, - hass_client_no_auth: ClientSessionGenerator, - current_request_with_host: None, - mock_setup_entry, - twitch: TwitchMock, -) -> None: - """Test import flow.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": SOURCE_IMPORT, - }, - data={ - "platform": "twitch", - CONF_CLIENT_ID: "1234", - CONF_CLIENT_SECRET: "abcd", - CONF_TOKEN: "efgh", - "channels": ["channel123"], - }, - ) - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "channel123" - assert "result" in result - assert "token" in result["result"].data - assert result["result"].data["token"]["access_token"] == "efgh" - assert result["result"].data["token"]["refresh_token"] == "" - assert result["result"].unique_id == "123" - assert result["options"] == {CONF_CHANNELS: ["channel123"]} - - -@pytest.mark.parametrize("twitch_mock", [TwitchInvalidTokenMock()]) -async def test_import_invalid_token( - hass: HomeAssistant, - hass_client_no_auth: ClientSessionGenerator, - current_request_with_host: None, - mock_setup_entry, - twitch: TwitchMock, -) -> None: - """Test import flow.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": SOURCE_IMPORT, - }, - data={ - "platform": "twitch", - CONF_CLIENT_ID: "1234", - CONF_CLIENT_SECRET: "abcd", - CONF_TOKEN: "efgh", - "channels": ["channel123"], - }, - ) - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "invalid_token" - issue_registry = ir.async_get(hass) - assert len(issue_registry.issues) == 1 - - -async def test_import_already_imported( - hass: HomeAssistant, - hass_client_no_auth: ClientSessionGenerator, - current_request_with_host: None, - config_entry: MockConfigEntry, - mock_setup_entry, - twitch: TwitchMock, -) -> None: - """Test import flow where the config is already imported.""" - await setup_integration(hass, config_entry) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": SOURCE_IMPORT, - }, - data={ - "platform": "twitch", - CONF_CLIENT_ID: "1234", - CONF_CLIENT_SECRET: "abcd", - CONF_TOKEN: "efgh", - "channels": ["channel123"], - }, - ) - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "already_configured" - issue_registry = ir.async_get(hass) - assert len(issue_registry.issues) == 1 diff --git a/tests/components/twitch/test_sensor.py b/tests/components/twitch/test_sensor.py index 3385cb228fd..bb6624f7847 100644 --- a/tests/components/twitch/test_sensor.py +++ b/tests/components/twitch/test_sensor.py @@ -4,12 +4,7 @@ from datetime import datetime import pytest -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.components.twitch.const import CONF_CHANNELS, DOMAIN -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_TOKEN, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import issue_registry as ir -from homeassistant.setup import async_setup_component from ...common import MockConfigEntry from . import ( @@ -23,58 +18,6 @@ from . import ( ) ENTITY_ID = "sensor.channel123" -CONFIG = { - "auth_implementation": "cred", - CONF_CLIENT_ID: "1234", - CONF_CLIENT_SECRET: "abcd", -} - -LEGACY_CONFIG_WITHOUT_TOKEN = { - SENSOR_DOMAIN: { - "platform": "twitch", - CONF_CLIENT_ID: "1234", - CONF_CLIENT_SECRET: "abcd", - "channels": ["channel123"], - } -} - -LEGACY_CONFIG = { - SENSOR_DOMAIN: { - "platform": "twitch", - CONF_CLIENT_ID: "1234", - CONF_CLIENT_SECRET: "abcd", - CONF_TOKEN: "efgh", - "channels": ["channel123"], - } -} - -OPTIONS = {CONF_CHANNELS: ["channel123"]} - - -async def test_legacy_migration( - hass: HomeAssistant, twitch: TwitchMock, mock_setup_entry -) -> None: - """Test importing legacy yaml.""" - assert await async_setup_component(hass, Platform.SENSOR, LEGACY_CONFIG) - await hass.async_block_till_done() - entries = hass.config_entries.async_entries(DOMAIN) - assert len(entries) == 1 - issue_registry = ir.async_get(hass) - assert len(issue_registry.issues) == 1 - - -async def test_legacy_migration_without_token( - hass: HomeAssistant, twitch: TwitchMock -) -> None: - """Test importing legacy yaml.""" - assert await async_setup_component( - hass, Platform.SENSOR, LEGACY_CONFIG_WITHOUT_TOKEN - ) - await hass.async_block_till_done() - entries = hass.config_entries.async_entries(DOMAIN) - assert len(entries) == 0 - issue_registry = ir.async_get(hass) - assert len(issue_registry.issues) == 1 async def test_offline( From 763df83cdb5c9e5fa0eb0214d94f980e6c721a2f Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 9 Apr 2024 18:34:35 +0200 Subject: [PATCH 449/967] Remove Nextbus YAML import (#115277) --- .../components/nextbus/config_flow.py | 37 +-------- homeassistant/components/nextbus/sensor.py | 52 +------------ tests/components/nextbus/test_config_flow.py | 77 +------------------ tests/components/nextbus/test_sensor.py | 58 +------------- 4 files changed, 7 insertions(+), 217 deletions(-) diff --git a/homeassistant/components/nextbus/config_flow.py b/homeassistant/components/nextbus/config_flow.py index 1d2b7f0b00f..c7e5ed3f36f 100644 --- a/homeassistant/components/nextbus/config_flow.py +++ b/homeassistant/components/nextbus/config_flow.py @@ -7,7 +7,7 @@ from py_nextbus import NextBusClient import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_NAME, CONF_STOP +from homeassistant.const import CONF_STOP from homeassistant.helpers.selector import ( SelectOptionDict, SelectSelector, @@ -102,41 +102,6 @@ class NextBusFlowHandler(ConfigFlow, domain=DOMAIN): """Initialize NextBus config flow.""" self.data: dict[str, str] = {} self._client = NextBusClient(output_format="json") - _LOGGER.info("Init new config flow") - - async def async_step_import(self, config_input: dict[str, str]) -> ConfigFlowResult: - """Handle import of config.""" - agency_tag = config_input[CONF_AGENCY] - route_tag = config_input[CONF_ROUTE] - stop_tag = config_input[CONF_STOP] - - validation_result = await self.hass.async_add_executor_job( - _validate_import, - self._client, - agency_tag, - route_tag, - stop_tag, - ) - if isinstance(validation_result, str): - return self.async_abort(reason=validation_result) - - data = { - CONF_AGENCY: agency_tag, - CONF_ROUTE: route_tag, - CONF_STOP: stop_tag, - CONF_NAME: config_input.get( - CONF_NAME, - f"{config_input[CONF_AGENCY]} {config_input[CONF_ROUTE]}", - ), - } - - await self.async_set_unique_id(_unique_id_from_data(data)) - self._abort_if_unique_id_configured() - - return self.async_create_entry( - title=" ".join(validation_result), - data=data, - ) async def async_step_user( self, diff --git a/homeassistant/components/nextbus/sensor.py b/homeassistant/components/nextbus/sensor.py index 68d10726609..5f89d0d79db 100644 --- a/homeassistant/components/nextbus/sensor.py +++ b/homeassistant/components/nextbus/sensor.py @@ -6,20 +6,11 @@ from itertools import chain import logging from typing import cast -import voluptuous as vol - -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, - SensorDeviceClass, - SensorEntity, -) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, CONF_STOP -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback -import homeassistant.helpers.config_validation as cv +from homeassistant.core import HomeAssistant, callback 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.dt import utc_from_timestamp @@ -29,43 +20,6 @@ from .util import listify, maybe_first _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_AGENCY): cv.string, - vol.Required(CONF_ROUTE): cv.string, - vol.Required(CONF_STOP): cv.string, - vol.Optional(CONF_NAME): cv.string, - } -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Initialize nextbus import from config.""" - async_create_issue( - hass, - HOMEASSISTANT_DOMAIN, - f"deprecated_yaml_{DOMAIN}", - is_fixable=False, - breaks_in_ha_version="2024.4.0", - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "NextBus", - }, - ) - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config - ) - ) - async def async_setup_entry( hass: HomeAssistant, diff --git a/tests/components/nextbus/test_config_flow.py b/tests/components/nextbus/test_config_flow.py index dd16c65e802..1af2cff0897 100644 --- a/tests/components/nextbus/test_config_flow.py +++ b/tests/components/nextbus/test_config_flow.py @@ -7,7 +7,7 @@ import pytest from homeassistant import config_entries, setup from homeassistant.components.nextbus.const import CONF_AGENCY, CONF_ROUTE, DOMAIN -from homeassistant.const import CONF_NAME, CONF_STOP +from homeassistant.const import CONF_STOP from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -29,81 +29,6 @@ def mock_nextbus() -> Generator[MagicMock, None, None]: yield client -async def test_import_config( - hass: HomeAssistant, mock_setup_entry: MagicMock, mock_nextbus_lists: MagicMock -) -> None: - """Test config is imported and component set up.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - data = { - CONF_AGENCY: "sf-muni", - CONF_ROUTE: "F", - CONF_STOP: "5650", - } - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=data, - ) - await hass.async_block_till_done() - - assert result.get("type") is FlowResultType.CREATE_ENTRY - assert ( - result.get("title") - == "San Francisco Muni F - Market & Wharves Market St & 7th St (Outbound)" - ) - assert result.get("data") == {CONF_NAME: "sf-muni F", **data} - - assert len(mock_setup_entry.mock_calls) == 1 - - # Check duplicate entries are aborted - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=data, - ) - await hass.async_block_till_done() - - assert result.get("type") is FlowResultType.ABORT - assert result.get("reason") == "already_configured" - - -@pytest.mark.parametrize( - ("override", "expected_reason"), - [ - ({CONF_AGENCY: "not muni"}, "invalid_agency"), - ({CONF_ROUTE: "not F"}, "invalid_route"), - ({CONF_STOP: "not 5650"}, "invalid_stop"), - ], -) -async def test_import_config_invalid( - hass: HomeAssistant, - mock_setup_entry: MagicMock, - mock_nextbus_lists: MagicMock, - override: dict[str, str], - expected_reason: str, -) -> None: - """Test user is redirected to user setup flow because they have invalid config.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - - data = { - CONF_AGENCY: "sf-muni", - CONF_ROUTE: "F", - CONF_STOP: "5650", - **override, - } - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=data, - ) - await hass.async_block_till_done() - - assert result.get("type") is FlowResultType.ABORT - assert result.get("reason") == expected_reason - - async def test_user_config( hass: HomeAssistant, mock_setup_entry: MagicMock, mock_nextbus_lists: MagicMock ) -> None: diff --git a/tests/components/nextbus/test_sensor.py b/tests/components/nextbus/test_sensor.py index ece40b36fb1..5e4f322e1eb 100644 --- a/tests/components/nextbus/test_sensor.py +++ b/tests/components/nextbus/test_sensor.py @@ -5,7 +5,7 @@ from copy import deepcopy from unittest.mock import MagicMock, patch from urllib.error import HTTPError -from py_nextbus.client import NextBusFormatError, NextBusHTTPError, RouteStop +from py_nextbus.client import NextBusFormatError, NextBusHTTPError import pytest from homeassistant.components import sensor @@ -13,10 +13,8 @@ from homeassistant.components.nextbus.const import CONF_AGENCY, CONF_ROUTE, DOMA from homeassistant.components.nextbus.coordinator import NextBusDataUpdateCoordinator from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_NAME, CONF_STOP -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.update_coordinator import UpdateFailed -from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -107,58 +105,6 @@ async def assert_setup_sensor( return config_entry -async def test_legacy_yaml_setup( - hass: HomeAssistant, - issue_registry: ir.IssueRegistry, -) -> None: - """Test config setup and yaml deprecation.""" - with patch( - "homeassistant.components.nextbus.config_flow.NextBusClient", - ) as NextBusClient: - NextBusClient.return_value.get_predictions_for_multi_stops.return_value = ( - BASIC_RESULTS - ) - await async_setup_component(hass, sensor.DOMAIN, PLATFORM_CONFIG) - await hass.async_block_till_done() - - issue = issue_registry.async_get_issue( - HOMEASSISTANT_DOMAIN, f"deprecated_yaml_{DOMAIN}" - ) - assert issue - - -async def test_valid_config( - hass: HomeAssistant, mock_nextbus: MagicMock, mock_nextbus_lists: MagicMock -) -> None: - """Test that sensor is set up properly with valid config.""" - await assert_setup_sensor(hass, CONFIG_BASIC) - - -async def test_verify_valid_state( - hass: HomeAssistant, - mock_nextbus: MagicMock, - mock_nextbus_lists: MagicMock, - mock_nextbus_predictions: MagicMock, -) -> None: - """Verify all attributes are set from a valid response.""" - await assert_setup_sensor(hass, CONFIG_BASIC) - entity = er.async_get(hass).async_get(SENSOR_ID) - assert entity - - mock_nextbus_predictions.assert_called_once_with( - {RouteStop(VALID_ROUTE, VALID_STOP)} - ) - - state = hass.states.get(SENSOR_ID) - assert state is not None - assert state.state == "2019-03-28T21:09:31+00:00" - assert state.attributes["agency"] == VALID_AGENCY_TITLE - assert state.attributes["route"] == VALID_ROUTE_TITLE - assert state.attributes["stop"] == VALID_STOP_TITLE - assert state.attributes["direction"] == "Outbound" - assert state.attributes["upcoming"] == "1, 2, 3, 10" - - async def test_message_dict( hass: HomeAssistant, mock_nextbus: MagicMock, From f8aec03c035627fcae17aa684d782ccd6395ffc0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 9 Apr 2024 06:37:01 -1000 Subject: [PATCH 450/967] Migrate hyperion to use async_update_reload_and_abort (#115238) --- homeassistant/components/hyperion/config_flow.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/homeassistant/components/hyperion/config_flow.py b/homeassistant/components/hyperion/config_flow.py index e29caa27ef7..d9c808b83a4 100644 --- a/homeassistant/components/hyperion/config_flow.py +++ b/homeassistant/components/hyperion/config_flow.py @@ -412,12 +412,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): entry = await self.async_set_unique_id(hyperion_id, raise_on_progress=False) if self.context.get(CONF_SOURCE) == SOURCE_REAUTH and entry is not None: - self.hass.config_entries.async_update_entry(entry, data=self._data) - # Need to manually reload, as the listener won't have been installed because - # the initial load did not succeed (the reauth flow will not be initiated if - # the load succeeds) - await self.hass.config_entries.async_reload(entry.entry_id) - return self.async_abort(reason="reauth_successful") + return self.async_update_reload_and_abort(entry, data=self._data) self._abort_if_unique_id_configured() From 59d92f16bdc6b283f312ecf8bded03719e35a62d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 9 Apr 2024 07:04:12 -1000 Subject: [PATCH 451/967] Use shorthand attributes in automation for name (#115246) --- homeassistant/components/automation/__init__.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index fa05791b9c9..785d5849d74 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -420,15 +420,10 @@ class UnavailableAutomationEntity(BaseAutomationEntity): raw_config: ConfigType | None, ) -> None: """Initialize an automation entity.""" - self._name = name + self._attr_name = name self._attr_unique_id = automation_id self.raw_config = raw_config - @property - def name(self) -> str: - """Return the name of the entity.""" - return self._name - @cached_property def referenced_labels(self) -> set[str]: """Return a set of referenced labels.""" @@ -488,7 +483,7 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity): trace_config: ConfigType, ) -> None: """Initialize an automation entity.""" - self._name = name + self._attr_name = name self._trigger_config = trigger_config self._async_detach_triggers: CALLBACK_TYPE | None = None self._cond_func = cond_func @@ -504,11 +499,6 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity): self._trace_config = trace_config self._attr_unique_id = automation_id - @property - def name(self) -> str: - """Return the name of the entity.""" - return self._name - @property def extra_state_attributes(self) -> dict[str, Any]: """Return the entity state attributes.""" @@ -732,7 +722,7 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity): translation_placeholders={ "service": f"{err.domain}.{err.service}", "entity_id": self.entity_id, - "name": self.name or self.entity_id, + "name": self._attr_name or self.entity_id, "edit": f"/config/automation/edit/{self.unique_id}", }, ) From 1de1e413a9fae3afe00d523bbe2f16310463449c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 9 Apr 2024 07:04:32 -1000 Subject: [PATCH 452/967] Migrate script entities to use more shorthand attrs (#115245) --- homeassistant/components/script/__init__.py | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index 6aeb0a9965e..6f7974dcb04 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -459,15 +459,10 @@ class UnavailableScriptEntity(BaseScriptEntity): raw_config: ConfigType | None, ) -> None: """Initialize a script entity.""" - self._name = raw_config.get(CONF_ALIAS, key) if raw_config else key + self._attr_name = raw_config.get(CONF_ALIAS, key) if raw_config else key self._attr_unique_id = key self.raw_config = raw_config - @property - def name(self) -> str: - """Return the name of the entity.""" - return self._name - @cached_property def referenced_labels(self) -> set[str]: """Return a set of referenced labels.""" @@ -503,6 +498,7 @@ class ScriptEntity(BaseScriptEntity, RestoreEntity): """Representation of a script entity.""" icon = None + _attr_should_poll = False def __init__(self, hass, key, cfg, raw_config, blueprint_inputs): """Initialize the script.""" @@ -531,16 +527,7 @@ class ScriptEntity(BaseScriptEntity, RestoreEntity): self.raw_config = raw_config self._trace_config = cfg[CONF_TRACE] self._blueprint_inputs = blueprint_inputs - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def name(self): - """Return the name of the entity.""" - return self.script.name + self._attr_name = self.script.name @property def extra_state_attributes(self): From 11af7d91ffebef0f0190867382c7d26517345459 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 9 Apr 2024 07:04:50 -1000 Subject: [PATCH 453/967] Optimize _async_track_event for the single key common case (#115242) --- homeassistant/helpers/event.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 3d51610010c..f689f15725d 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -401,9 +401,6 @@ def _async_track_event( if not keys: return _remove_empty_listener - if isinstance(keys, str): - keys = [keys] - hass_data = hass.data callbacks_key = tracker.callbacks_key @@ -422,11 +419,23 @@ def _async_track_event( job = HassJob(action, f"track {tracker.event_type} event {keys}", job_type=job_type) - for key in keys: - if callback_list := callbacks.get(key): + if isinstance(keys, str): + # Almost all calls to this function use a single key + # so we optimize for that case. We don't use setdefault + # here because this function gets called ~20000 times + # during startup, and we want to avoid the overhead of + # creating empty lists and throwing them away. + if callback_list := callbacks.get(keys): callback_list.append(job) else: - callbacks[key] = [job] + callbacks[keys] = [job] + keys = [keys] + else: + for key in keys: + if callback_list := callbacks.get(key): + callback_list.append(job) + else: + callbacks[key] = [job] return ft.partial(_remove_listener, hass, listeners_key, keys, job, callbacks) From d2a347345467c4c5bd1d6ea4bb3517e2911b3ce4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 9 Apr 2024 07:05:09 -1000 Subject: [PATCH 454/967] Migrate elkm1 to use async_schedule_reload (#115240) --- homeassistant/components/elkm1/config_flow.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/elkm1/config_flow.py b/homeassistant/components/elkm1/config_flow.py index 5991c502ef6..9a71c86478b 100644 --- a/homeassistant/components/elkm1/config_flow.py +++ b/homeassistant/components/elkm1/config_flow.py @@ -174,9 +174,7 @@ class Elkm1ConfigFlow(ConfigFlow, domain=DOMAIN): or hostname_from_url(entry.data[CONF_HOST]) == host ): if async_update_entry_from_discovery(self.hass, entry, device): - self.hass.async_create_task( - self.hass.config_entries.async_reload(entry.entry_id) - ) + self.hass.config_entries.async_schedule_reload(entry.entry_id) return self.async_abort(reason="already_configured") self.context[CONF_HOST] = host for progress in self._async_in_progress(): From 469b01bd64eae4f3ed0d642aaa8064fe771b9625 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 9 Apr 2024 07:05:23 -1000 Subject: [PATCH 455/967] Migrate apple_tv to use async_schedule_reload (#115241) --- homeassistant/components/apple_tv/config_flow.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/apple_tv/config_flow.py b/homeassistant/components/apple_tv/config_flow.py index f9be827741b..1f2aa3b3b3a 100644 --- a/homeassistant/components/apple_tv/config_flow.py +++ b/homeassistant/components/apple_tv/config_flow.py @@ -380,9 +380,7 @@ class AppleTVConfigFlow(ConfigFlow, domain=DOMAIN): }, ) if entry.source != SOURCE_IGNORE: - self.hass.async_create_task( - self.hass.config_entries.async_reload(entry.entry_id) - ) + self.hass.config_entries.async_schedule_reload(entry.entry_id) if not allow_exist: raise DeviceAlreadyConfigured From 076e6ce6e6ecedc56b07ccaaa46f5922066daaa8 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 9 Apr 2024 21:10:22 +0200 Subject: [PATCH 456/967] Bump yt-dlp to 2024.04.09 (#115295) --- 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 c86099a9ea4..940d1d7bb18 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -7,5 +7,5 @@ "iot_class": "calculated", "loggers": ["yt_dlp"], "quality_scale": "internal", - "requirements": ["yt-dlp==2024.03.10"] + "requirements": ["yt-dlp==2024.04.09"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4df01955f11..404fab87547 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2926,7 +2926,7 @@ youless-api==1.0.1 youtubeaio==1.1.5 # homeassistant.components.media_extractor -yt-dlp==2024.03.10 +yt-dlp==2024.04.09 # homeassistant.components.zamg zamg==0.3.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 33129c58871..1e138b73c94 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2267,7 +2267,7 @@ youless-api==1.0.1 youtubeaio==1.1.5 # homeassistant.components.media_extractor -yt-dlp==2024.03.10 +yt-dlp==2024.04.09 # homeassistant.components.zamg zamg==0.3.6 From 6ed2190c2910411e26574dc4d3724f0ba17d873a Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Tue, 9 Apr 2024 21:22:46 +0200 Subject: [PATCH 457/967] Bump zha-quirks to 0.0.114 (#115299) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index e9d75584064..7741673557d 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -24,7 +24,7 @@ "bellows==0.38.1", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.113", + "zha-quirks==0.0.114", "zigpy-deconz==0.23.1", "zigpy==0.63.5", "zigpy-xbee==0.20.1", diff --git a/requirements_all.txt b/requirements_all.txt index 404fab87547..9143631cffc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2941,7 +2941,7 @@ zeroconf==0.132.0 zeversolar==0.3.1 # homeassistant.components.zha -zha-quirks==0.0.113 +zha-quirks==0.0.114 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1e138b73c94..002bc5132c6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2279,7 +2279,7 @@ zeroconf==0.132.0 zeversolar==0.3.1 # homeassistant.components.zha -zha-quirks==0.0.113 +zha-quirks==0.0.114 # homeassistant.components.zha zigpy-deconz==0.23.1 From f527fd0947aa0b8d30374adbe8ec7b6f17db92af Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 9 Apr 2024 11:11:22 -1000 Subject: [PATCH 458/967] Improve error reporting when an integration tries to create a task in a thread (#115307) --- homeassistant/util/async_.py | 20 +++++++--- tests/util/test_async.py | 72 ++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 6 deletions(-) diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py index 5ca19296b41..0cf9fc992c5 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -24,12 +24,20 @@ def create_eager_task( loop: AbstractEventLoop | None = None, ) -> Task[_T]: """Create a task from a coroutine and schedule it to run immediately.""" - return Task( - coro, - loop=loop or get_running_loop(), - name=name, - eager_start=True, - ) + if not loop: + try: + loop = get_running_loop() + except RuntimeError: + # If there is no running loop, create_eager_task is being called from + # the wrong thread. + # Late import to avoid circular dependencies + # pylint: disable-next=import-outside-toplevel + from homeassistant.helpers import frame + + frame.report("attempted to create an asyncio task from a thread") + raise + + return Task(coro, loop=loop, name=name, eager_start=True) def cancelling(task: Future[Any]) -> bool: diff --git a/tests/util/test_async.py b/tests/util/test_async.py index d0131df88ee..157becc4b01 100644 --- a/tests/util/test_async.py +++ b/tests/util/test_async.py @@ -9,6 +9,8 @@ import pytest from homeassistant.core import HomeAssistant from homeassistant.util import async_ as hasync +from tests.common import extract_stack_to_frame + @patch("concurrent.futures.Future") @patch("threading.get_ident") @@ -123,3 +125,73 @@ async def test_create_eager_task_312(hass: HomeAssistant) -> None: assert events == ["eager", "normal"] await task1 await task2 + + +async def test_create_eager_task_from_thread(hass: HomeAssistant) -> None: + """Test we report trying to create an eager task from a thread.""" + + def create_task(): + hasync.create_eager_task(asyncio.sleep(0)) + + with pytest.raises( + RuntimeError, + match=( + "Detected code that attempted to create an asyncio task from a thread. Please report this issue." + ), + ): + await hass.async_add_executor_job(create_task) + + +async def test_create_eager_task_from_thread_in_integration( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test we report trying to create an eager task from a thread.""" + + def create_task(): + hasync.create_eager_task(asyncio.sleep(0)) + + 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 ( + pytest.raises(RuntimeError, match="no running event loop"), + patch( + "homeassistant.helpers.frame.linecache.getline", + return_value="self.light.is_on", + ), + patch( + "homeassistant.util.loop._get_line_from_cache", + return_value="mock_line", + ), + patch( + "homeassistant.util.loop.get_current_frame", + return_value=frames, + ), + patch( + "homeassistant.helpers.frame.get_current_frame", + return_value=frames, + ), + ): + await hass.async_add_executor_job(create_task) + + assert ( + "Detected that integration 'hue' attempted to create an asyncio task " + "from a thread at homeassistant/components/hue/light.py, line 23: " + "self.light.is_on" + ) in caplog.text From fa3cba5b87007c609104bc2c3caab4c756e0a79d Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Wed, 10 Apr 2024 00:15:30 +0200 Subject: [PATCH 459/967] Bump codecov/codecov-action to v4.3.0 (#115317) --- .github/workflows/ci.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b0efe812062..e1281a14b5c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1090,7 +1090,7 @@ jobs: if: needs.info.outputs.test_full_suite == 'true' uses: Wandalen/wretry.action@v3.1.0 with: - action: codecov/codecov-action@v3.1.3 + action: codecov/codecov-action@v4.3.0 with: | fail_ci_if_error: true flags: full-suite @@ -1101,7 +1101,7 @@ jobs: if: needs.info.outputs.test_full_suite == 'false' uses: Wandalen/wretry.action@v3.1.0 with: - action: codecov/codecov-action@v3.1.3 + action: codecov/codecov-action@v4.3.0 with: | fail_ci_if_error: true token: ${{ env.CODECOV_TOKEN }} @@ -1236,7 +1236,7 @@ jobs: if: needs.info.outputs.test_full_suite == 'true' uses: Wandalen/wretry.action@v3.1.0 with: - action: codecov/codecov-action@v3.1.3 + action: codecov/codecov-action@v4.3.0 with: | fail_ci_if_error: true flags: full-suite @@ -1247,7 +1247,7 @@ jobs: if: needs.info.outputs.test_full_suite == 'false' uses: Wandalen/wretry.action@v3.1.0 with: - action: codecov/codecov-action@v3.1.3 + action: codecov/codecov-action@v4.3.0 with: | fail_ci_if_error: true token: ${{ env.CODECOV_TOKEN }} From 968de08e4b90bc31059a7fb8a0829c8100cb87fe Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Tue, 9 Apr 2024 20:50:46 -0400 Subject: [PATCH 460/967] Bump python-roborock to 1.0.0 (#115324) * refactor base code * refactor tests code --- homeassistant/components/roborock/__init__.py | 8 +++--- .../components/roborock/coordinator.py | 10 +++---- homeassistant/components/roborock/device.py | 12 ++++----- .../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 +- tests/components/roborock/conftest.py | 26 +++++++++---------- tests/components/roborock/test_button.py | 2 +- tests/components/roborock/test_image.py | 4 +-- tests/components/roborock/test_init.py | 18 ++++++------- tests/components/roborock/test_number.py | 2 +- tests/components/roborock/test_select.py | 4 +-- tests/components/roborock/test_sensor.py | 8 +++--- tests/components/roborock/test_switch.py | 4 +-- tests/components/roborock/test_time.py | 2 +- tests/components/roborock/test_vacuum.py | 8 +++--- 19 files changed, 60 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/roborock/__init__.py b/homeassistant/components/roborock/__init__.py index 141770e733d..b72fec5a8e1 100644 --- a/homeassistant/components/roborock/__init__.py +++ b/homeassistant/components/roborock/__init__.py @@ -9,8 +9,8 @@ import logging from typing import Any from roborock import HomeDataRoom, RoborockException, RoborockInvalidCredentials -from roborock.cloud_api import RoborockMqttClient from roborock.containers import DeviceData, HomeDataDevice, HomeDataProduct, UserData +from roborock.version_1_apis.roborock_mqtt_client_v1 import RoborockMqttClientV1 from roborock.web_api import RoborockApiClient from homeassistant.config_entries import ConfigEntry @@ -75,7 +75,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: translation_key="no_coordinators", ) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { - coordinator.roborock_device_info.device.duid: coordinator + coordinator.api.device_info.device.duid: coordinator for coordinator in valid_coordinators } await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -107,7 +107,7 @@ async def setup_device( home_data_rooms: list[HomeDataRoom], ) -> RoborockDataUpdateCoordinator | None: """Set up a device Coordinator.""" - mqtt_client = RoborockMqttClient(user_data, DeviceData(device, product_info.name)) + mqtt_client = RoborockMqttClientV1(user_data, DeviceData(device, product_info.name)) try: networking = await mqtt_client.get_networking() if networking is None: @@ -138,7 +138,7 @@ async def setup_device( await coordinator.async_config_entry_first_refresh() except ConfigEntryNotReady as ex: await coordinator.release() - if isinstance(coordinator.api, RoborockMqttClient): + if isinstance(coordinator.api, RoborockMqttClientV1): _LOGGER.warning( "Not setting up %s because the we failed to get data for the first time using the online client. " "Please ensure your Home Assistant instance can communicate with this device. " diff --git a/homeassistant/components/roborock/coordinator.py b/homeassistant/components/roborock/coordinator.py index c5fd0c09c46..293415360bd 100644 --- a/homeassistant/components/roborock/coordinator.py +++ b/homeassistant/components/roborock/coordinator.py @@ -7,11 +7,11 @@ from datetime import timedelta import logging from roborock import HomeDataRoom -from roborock.cloud_api import RoborockMqttClient from roborock.containers import DeviceData, HomeDataDevice, HomeDataProduct, NetworkInfo from roborock.exceptions import RoborockException -from roborock.local_api import RoborockLocalClient 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 homeassistant.const import ATTR_CONNECTIONS from homeassistant.core import HomeAssistant @@ -36,7 +36,7 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]): device: HomeDataDevice, device_networking: NetworkInfo, product_info: HomeDataProduct, - cloud_api: RoborockMqttClient, + cloud_api: RoborockMqttClientV1, home_data_rooms: list[HomeDataRoom], ) -> None: """Initialize.""" @@ -48,7 +48,7 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]): DeviceProp(), ) device_data = DeviceData(device, product_info.model, device_networking.ip) - self.api: RoborockLocalClient | RoborockMqttClient = RoborockLocalClient( + self.api: RoborockLocalClientV1 | RoborockMqttClientV1 = RoborockLocalClientV1( device_data ) self.cloud_api = cloud_api @@ -69,7 +69,7 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]): async def verify_api(self) -> None: """Verify that the api is reachable. If it is not, switch clients.""" - if isinstance(self.api, RoborockLocalClient): + if isinstance(self.api, RoborockLocalClientV1): try: await self.api.ping() except RoborockException: diff --git a/homeassistant/components/roborock/device.py b/homeassistant/components/roborock/device.py index 7affaa396e6..69384d6e23a 100644 --- a/homeassistant/components/roborock/device.py +++ b/homeassistant/components/roborock/device.py @@ -2,21 +2,21 @@ from typing import Any -from roborock.api import AttributeCache, RoborockClient -from roborock.cloud_api import RoborockMqttClient from roborock.command_cache import CacheableAttribute from roborock.containers import Consumable, Status from roborock.exceptions import RoborockException 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 homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import Entity from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import RoborockDataUpdateCoordinator from .const import DOMAIN +from .coordinator import RoborockDataUpdateCoordinator class RoborockEntity(Entity): @@ -28,7 +28,7 @@ class RoborockEntity(Entity): self, unique_id: str, device_info: DeviceInfo, - api: RoborockClient, + api: RoborockClientV1, ) -> None: """Initialize the coordinated Roborock Device.""" self._attr_unique_id = unique_id @@ -36,7 +36,7 @@ class RoborockEntity(Entity): self._api = api @property - def api(self) -> RoborockClient: + def api(self) -> RoborockClientV1: """Returns the api.""" return self._api @@ -116,7 +116,7 @@ class RoborockCoordinatedEntity( return data.status @property - def cloud_api(self) -> RoborockMqttClient: + def cloud_api(self) -> RoborockMqttClientV1: """Return the cloud api.""" return self.coordinator.cloud_api diff --git a/homeassistant/components/roborock/manifest.json b/homeassistant/components/roborock/manifest.json index a7a7fe01d23..711da78de31 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==0.40.0", + "python-roborock==1.0.0", "vacuum-map-parser-roborock==0.1.1" ] } diff --git a/homeassistant/components/roborock/number.py b/homeassistant/components/roborock/number.py index 09030ef8500..0a7abf5a090 100644 --- a/homeassistant/components/roborock/number.py +++ b/homeassistant/components/roborock/number.py @@ -6,9 +6,9 @@ from dataclasses import dataclass import logging from typing import Any -from roborock.api import AttributeCache from roborock.command_cache import CacheableAttribute from roborock.exceptions import RoborockException +from roborock.version_1_apis.roborock_client_v1 import AttributeCache from homeassistant.components.number import NumberEntity, NumberEntityDescription from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/roborock/switch.py b/homeassistant/components/roborock/switch.py index 9c7ca3cdcae..090c3219fd3 100644 --- a/homeassistant/components/roborock/switch.py +++ b/homeassistant/components/roborock/switch.py @@ -8,8 +8,8 @@ from dataclasses import dataclass import logging from typing import Any -from roborock.api import AttributeCache from roborock.command_cache import CacheableAttribute +from roborock.version_1_apis.roborock_client_v1 import AttributeCache from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/roborock/time.py b/homeassistant/components/roborock/time.py index 9a3cac86425..c90fc7fa438 100644 --- a/homeassistant/components/roborock/time.py +++ b/homeassistant/components/roborock/time.py @@ -8,9 +8,9 @@ from datetime import time import logging from typing import Any -from roborock.api import AttributeCache from roborock.command_cache import CacheableAttribute from roborock.exceptions import RoborockException +from roborock.version_1_apis.roborock_client_v1 import AttributeCache from homeassistant.components.time import TimeEntity, TimeEntityDescription from homeassistant.config_entries import ConfigEntry diff --git a/requirements_all.txt b/requirements_all.txt index 9143631cffc..2bcc0be7f31 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2294,7 +2294,7 @@ python-rabbitair==0.0.8 python-ripple-api==0.0.3 # homeassistant.components.roborock -python-roborock==0.40.0 +python-roborock==1.0.0 # homeassistant.components.smarttub python-smarttub==0.0.36 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 002bc5132c6..80bce7f87f7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1773,7 +1773,7 @@ python-qbittorrent==0.4.3 python-rabbitair==0.0.8 # homeassistant.components.roborock -python-roborock==0.40.0 +python-roborock==1.0.0 # homeassistant.components.smarttub python-smarttub==0.0.36 diff --git a/tests/components/roborock/conftest.py b/tests/components/roborock/conftest.py index 2910fa38995..0f3689da161 100644 --- a/tests/components/roborock/conftest.py +++ b/tests/components/roborock/conftest.py @@ -32,26 +32,26 @@ from tests.common import MockConfigEntry def bypass_api_fixture() -> None: """Skip calls to the API.""" with ( - patch("homeassistant.components.roborock.RoborockMqttClient.async_connect"), - patch("homeassistant.components.roborock.RoborockMqttClient._send_command"), + patch("homeassistant.components.roborock.RoborockMqttClientV1.async_connect"), + patch("homeassistant.components.roborock.RoborockMqttClientV1._send_command"), patch( "homeassistant.components.roborock.RoborockApiClient.get_home_data", return_value=HOME_DATA, ), patch( - "homeassistant.components.roborock.RoborockMqttClient.get_networking", + "homeassistant.components.roborock.RoborockMqttClientV1.get_networking", return_value=NETWORK_INFO, ), patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.get_prop", + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.get_prop", return_value=PROP, ), patch( - "homeassistant.components.roborock.coordinator.RoborockMqttClient.get_multi_maps_list", + "homeassistant.components.roborock.coordinator.RoborockMqttClientV1.get_multi_maps_list", return_value=MULTI_MAP_LIST, ), patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.get_multi_maps_list", + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.get_multi_maps_list", return_value=MULTI_MAP_LIST, ), patch( @@ -59,24 +59,24 @@ def bypass_api_fixture() -> None: return_value=MAP_DATA, ), patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.send_message" + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.send_message" ), - patch("homeassistant.components.roborock.RoborockMqttClient._wait_response"), + patch("homeassistant.components.roborock.RoborockMqttClientV1._wait_response"), patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient._wait_response" + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1._wait_response" ), patch( - "roborock.api.AttributeCache.async_value", + "roborock.version_1_apis.AttributeCache.async_value", ), patch( - "roborock.api.AttributeCache.value", + "roborock.version_1_apis.AttributeCache.value", ), patch( "homeassistant.components.roborock.image.MAP_SLEEP", 0, ), patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.get_room_mapping", + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.get_room_mapping", return_value=[ RoomMapping(16, "2362048"), RoomMapping(17, "2362044"), @@ -84,7 +84,7 @@ def bypass_api_fixture() -> None: ], ), patch( - "homeassistant.components.roborock.coordinator.RoborockMqttClient.get_room_mapping", + "homeassistant.components.roborock.coordinator.RoborockMqttClientV1.get_room_mapping", return_value=[ RoomMapping(16, "2362048"), RoomMapping(17, "2362044"), diff --git a/tests/components/roborock/test_button.py b/tests/components/roborock/test_button.py index 5654dac9218..88cf5beab15 100644 --- a/tests/components/roborock/test_button.py +++ b/tests/components/roborock/test_button.py @@ -31,7 +31,7 @@ async def test_update_success( # Ensure that the entity exist, as these test can pass even if there is no entity. assert hass.states.get(entity_id).state == "unknown" with patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.send_message" + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.send_message" ) as mock_send_message: await hass.services.async_call( "button", diff --git a/tests/components/roborock/test_image.py b/tests/components/roborock/test_image.py index 77829e5aaa6..445f90f4a05 100644 --- a/tests/components/roborock/test_image.py +++ b/tests/components/roborock/test_image.py @@ -37,7 +37,7 @@ async def test_floorplan_image( prop.status.in_cleaning = 1 with ( patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.get_prop", + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.get_prop", return_value=prop, ), patch( @@ -72,7 +72,7 @@ async def test_floorplan_image_failed_parse( return_value=map_data, ), patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.get_prop", + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.get_prop", return_value=prop, ), patch( diff --git a/tests/components/roborock/test_init.py b/tests/components/roborock/test_init.py index 08a3afe6c5e..de858ef7cb2 100644 --- a/tests/components/roborock/test_init.py +++ b/tests/components/roborock/test_init.py @@ -19,7 +19,7 @@ async def test_unload_entry( assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert setup_entry.state is ConfigEntryState.LOADED with patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.async_release" + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.async_release" ) as mock_disconnect: assert await hass.config_entries.async_unload(setup_entry.entry_id) await hass.async_block_till_done() @@ -37,7 +37,7 @@ async def test_config_entry_not_ready( "homeassistant.components.roborock.RoborockApiClient.get_home_data", ), patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.get_prop", + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.get_prop", side_effect=RoborockException(), ), ): @@ -55,7 +55,7 @@ async def test_config_entry_not_ready_home_data( side_effect=RoborockException(), ), patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.get_prop", + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.get_prop", side_effect=RoborockException(), ), ): @@ -68,7 +68,7 @@ async def test_get_networking_fails( ) -> None: """Test that when networking fails, we attempt to retry.""" with patch( - "homeassistant.components.roborock.RoborockMqttClient.get_networking", + "homeassistant.components.roborock.RoborockMqttClientV1.get_networking", side_effect=RoborockException(), ): await async_setup_component(hass, DOMAIN, {}) @@ -80,7 +80,7 @@ async def test_get_networking_fails_none( ) -> None: """Test that when networking returns None, we attempt to retry.""" with patch( - "homeassistant.components.roborock.RoborockMqttClient.get_networking", + "homeassistant.components.roborock.RoborockMqttClientV1.get_networking", return_value=None, ): await async_setup_component(hass, DOMAIN, {}) @@ -93,11 +93,11 @@ async def test_cloud_client_fails_props( """Test that if networking succeeds, but we can't communicate with the vacuum, we can't get props, fail.""" with ( patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.ping", + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.ping", side_effect=RoborockException(), ), patch( - "homeassistant.components.roborock.coordinator.RoborockMqttClient.get_prop", + "homeassistant.components.roborock.coordinator.RoborockMqttClientV1.get_prop", side_effect=RoborockException(), ), ): @@ -110,7 +110,7 @@ async def test_local_client_fails_props( ) -> None: """Test that if networking succeeds, but we can't communicate locally with the vacuum, we can't get props, fail.""" with patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.get_prop", + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.get_prop", side_effect=RoborockException(), ): await async_setup_component(hass, DOMAIN, {}) @@ -122,7 +122,7 @@ async def test_fails_maps_continue( ) -> None: """Test that if we fail to get the maps, we still setup.""" with patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.get_multi_maps_list", + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.get_multi_maps_list", side_effect=RoborockException(), ): await async_setup_component(hass, DOMAIN, {}) diff --git a/tests/components/roborock/test_number.py b/tests/components/roborock/test_number.py index 1c20a93cace..3291dd2a7dc 100644 --- a/tests/components/roborock/test_number.py +++ b/tests/components/roborock/test_number.py @@ -27,7 +27,7 @@ async def test_update_success( # Ensure that the entity exist, as these test can pass even if there is no entity. assert hass.states.get(entity_id) is not None with patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.send_message" + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.send_message" ) as mock_send_message: await hass.services.async_call( "number", diff --git a/tests/components/roborock/test_select.py b/tests/components/roborock/test_select.py index 9310d4e2e9a..c8626818749 100644 --- a/tests/components/roborock/test_select.py +++ b/tests/components/roborock/test_select.py @@ -30,7 +30,7 @@ async def test_update_success( # Ensure that the entity exist, as these test can pass even if there is no entity. assert hass.states.get(entity_id) is not None with patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.send_message" + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.send_message" ) as mock_send_message: await hass.services.async_call( "select", @@ -50,7 +50,7 @@ async def test_update_failure( """Test that changing a value will raise a homeassistanterror when it fails.""" with ( patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.send_message", + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.send_message", side_effect=RoborockException(), ), pytest.raises(HomeAssistantError), diff --git a/tests/components/roborock/test_sensor.py b/tests/components/roborock/test_sensor.py index a5f4164eee1..23d16f643b2 100644 --- a/tests/components/roborock/test_sensor.py +++ b/tests/components/roborock/test_sensor.py @@ -3,7 +3,6 @@ from unittest.mock import patch from roborock import DeviceData, HomeDataDevice -from roborock.cloud_api import RoborockMqttClient from roborock.const import ( FILTER_REPLACE_TIME, MAIN_BRUSH_REPLACE_TIME, @@ -11,6 +10,7 @@ from roborock.const import ( SIDE_BRUSH_REPLACE_TIME, ) from roborock.roborock_message import RoborockMessage, RoborockMessageProtocol +from roborock.version_1_apis import RoborockMqttClientV1 from homeassistant.core import HomeAssistant @@ -62,11 +62,11 @@ async def test_listener_update( """Test that when we receive a mqtt topic, we successfully update the entity.""" assert hass.states.get("sensor.roborock_s7_maxv_status").state == "charging" # Listeners are global based on uuid - so this is okay - client = RoborockMqttClient( + client = RoborockMqttClientV1( USER_DATA, DeviceData(device=HomeDataDevice("abc123", "", "", "", ""), model="") ) # Test Status - with patch("roborock.api.AttributeCache.value", STATUS.as_dict()): + with patch("roborock.version_1_apis.AttributeCache.value", STATUS.as_dict()): # Symbolizes a mqtt message coming in client.on_message_received( [ @@ -80,7 +80,7 @@ async def test_listener_update( assert hass.states.get("sensor.roborock_s7_maxv_filter_time_left").state == str( FILTER_REPLACE_TIME - 74382 ) - with patch("roborock.api.AttributeCache.value", CONSUMABLE.as_dict()): + with patch("roborock.version_1_apis.AttributeCache.value", CONSUMABLE.as_dict()): client.on_message_received( [ RoborockMessage( diff --git a/tests/components/roborock/test_switch.py b/tests/components/roborock/test_switch.py index 42a5e92f32a..3afa72b319d 100644 --- a/tests/components/roborock/test_switch.py +++ b/tests/components/roborock/test_switch.py @@ -28,7 +28,7 @@ async def test_update_success( # Ensure that the entity exist, as these test can pass even if there is no entity. assert hass.states.get(entity_id) is not None with patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient._send_command" + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1._send_command" ) as mock_send_message: await hass.services.async_call( "switch", @@ -39,7 +39,7 @@ async def test_update_success( ) assert mock_send_message.assert_called_once with patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.send_message" + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.send_message" ) as mock_send_message: await hass.services.async_call( "switch", diff --git a/tests/components/roborock/test_time.py b/tests/components/roborock/test_time.py index 378c642b2f4..ca6507f887b 100644 --- a/tests/components/roborock/test_time.py +++ b/tests/components/roborock/test_time.py @@ -28,7 +28,7 @@ async def test_update_success( # Ensure that the entity exist, as these test can pass even if there is no entity. assert hass.states.get(entity_id) is not None with patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient._send_command" + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1._send_command" ) as mock_send_message: await hass.services.async_call( "time", diff --git a/tests/components/roborock/test_vacuum.py b/tests/components/roborock/test_vacuum.py index cc01acc29fd..437c9847e21 100644 --- a/tests/components/roborock/test_vacuum.py +++ b/tests/components/roborock/test_vacuum.py @@ -82,7 +82,7 @@ async def test_commands( data = {ATTR_ENTITY_ID: ENTITY_ID, **(service_params or {})} with patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.send_command" + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.send_command" ) as mock_send_command: await hass.services.async_call( Platform.VACUUM, @@ -115,7 +115,7 @@ async def test_resume_cleaning( prop = copy.deepcopy(PROP) prop.status.in_cleaning = in_cleaning_int with patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.get_prop", + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.get_prop", return_value=prop, ): await async_setup_component(hass, DOMAIN, {}) @@ -124,7 +124,7 @@ async def test_resume_cleaning( data = {ATTR_ENTITY_ID: ENTITY_ID} with patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.send_command" + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.send_command" ) as mock_send_command: await hass.services.async_call( Platform.VACUUM, @@ -145,7 +145,7 @@ async def test_failed_user_command( data = {ATTR_ENTITY_ID: ENTITY_ID, "command": "fake_command"} with ( patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.send_command", + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.send_command", side_effect=RoborockException(), ), pytest.raises(HomeAssistantError, match="Error while calling fake_command"), From d8c8d1a297d7e8d7700f5fd7c8c46a913122d87c Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 10 Apr 2024 03:06:11 +0200 Subject: [PATCH 461/967] Use dict instead of MutableMapping [extra_state_attributes] (#115319) --- homeassistant/components/minecraft_server/sensor.py | 4 ++-- homeassistant/helpers/entity.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/minecraft_server/sensor.py b/homeassistant/components/minecraft_server/sensor.py index 4b862f54715..fae004a015e 100644 --- a/homeassistant/components/minecraft_server/sensor.py +++ b/homeassistant/components/minecraft_server/sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations -from collections.abc import Callable, MutableMapping +from collections.abc import Callable from dataclasses import dataclass from typing import Any @@ -37,7 +37,7 @@ class MinecraftServerSensorEntityDescription(SensorEntityDescription): """Class describing Minecraft Server sensor entities.""" value_fn: Callable[[MinecraftServerData], StateType] - attributes_fn: Callable[[MinecraftServerData], MutableMapping[str, Any]] | None + attributes_fn: Callable[[MinecraftServerData], dict[str, Any]] | None supported_server_types: set[MinecraftServerType] diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index c0b301b6fb6..fb071d438b1 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -5,7 +5,7 @@ from __future__ import annotations from abc import ABCMeta import asyncio from collections import deque -from collections.abc import Callable, Coroutine, Iterable, Mapping, MutableMapping +from collections.abc import Callable, Coroutine, Iterable, Mapping import dataclasses from enum import Enum, IntFlag, auto import functools as ft @@ -537,7 +537,7 @@ class Entity( _attr_entity_picture: str | None = None _attr_entity_registry_enabled_default: bool _attr_entity_registry_visible_default: bool - _attr_extra_state_attributes: MutableMapping[str, Any] + _attr_extra_state_attributes: dict[str, Any] _attr_force_update: bool _attr_icon: str | None _attr_name: str | None From 2decf6c02337ed789492ea214b2159e09a00c017 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 10 Apr 2024 03:08:31 +0200 Subject: [PATCH 462/967] Use dict instead of MutableMapping [recorder] (#115318) --- .../components/history/websocket_api.py | 10 +++++----- .../components/recorder/history/__init__.py | 11 +++++----- .../components/recorder/history/legacy.py | 20 +++++++++---------- .../components/recorder/history/modern.py | 20 +++++++++---------- homeassistant/components/sensor/recorder.py | 4 ++-- 5 files changed, 32 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/history/websocket_api.py b/homeassistant/components/history/websocket_api.py index 03bb7efc561..465416607a2 100644 --- a/homeassistant/components/history/websocket_api.py +++ b/homeassistant/components/history/websocket_api.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio -from collections.abc import Callable, Iterable, MutableMapping +from collections.abc import Callable, Iterable from dataclasses import dataclass from datetime import datetime as dt, timedelta import logging @@ -173,7 +173,7 @@ async def ws_get_history_during_period( def _generate_stream_message( - states: MutableMapping[str, list[dict[str, Any]]], + states: dict[str, list[dict[str, Any]]], start_day: dt, end_day: dt, ) -> dict[str, Any]: @@ -201,7 +201,7 @@ def _generate_websocket_response( msg_id: int, start_time: dt, end_time: dt, - states: MutableMapping[str, list[dict[str, Any]]], + states: dict[str, list[dict[str, Any]]], ) -> bytes: """Generate a websocket response.""" return json_bytes( @@ -225,7 +225,7 @@ def _generate_historical_response( ) -> tuple[float, dt | None, bytes | None]: """Generate a historical response.""" states = cast( - MutableMapping[str, list[dict[str, Any]]], + dict[str, list[dict[str, Any]]], history.get_significant_states( hass, start_time, @@ -311,7 +311,7 @@ def _history_compressed_state(state: State, no_attributes: bool) -> dict[str, An def _events_to_compressed_states( events: Iterable[Event], no_attributes: bool -) -> MutableMapping[str, list[dict[str, Any]]]: +) -> dict[str, list[dict[str, Any]]]: """Convert events to a compressed states.""" states_by_entity_ids: dict[str, list[dict[str, Any]]] = {} for event in events: diff --git a/homeassistant/components/recorder/history/__init__.py b/homeassistant/components/recorder/history/__init__.py index 05452fdac47..de7002eb6a4 100644 --- a/homeassistant/components/recorder/history/__init__.py +++ b/homeassistant/components/recorder/history/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations -from collections.abc import MutableMapping from datetime import datetime from typing import Any @@ -43,7 +42,7 @@ def get_full_significant_states_with_session( include_start_time_state: bool = True, significant_changes_only: bool = True, no_attributes: bool = False, -) -> MutableMapping[str, list[State]]: +) -> dict[str, list[State]]: """Return a dict of significant states during a time period.""" if not recorder.get_instance(hass).states_meta_manager.active: from .legacy import ( # pylint: disable=import-outside-toplevel @@ -68,7 +67,7 @@ def get_full_significant_states_with_session( def get_last_state_changes( hass: HomeAssistant, number_of_states: int, entity_id: str -) -> MutableMapping[str, list[State]]: +) -> dict[str, list[State]]: """Return the last number_of_states.""" if not recorder.get_instance(hass).states_meta_manager.active: from .legacy import ( # pylint: disable=import-outside-toplevel @@ -92,7 +91,7 @@ def get_significant_states( minimal_response: bool = False, no_attributes: bool = False, compressed_state_format: bool = False, -) -> MutableMapping[str, list[State | dict[str, Any]]]: +) -> dict[str, list[State | dict[str, Any]]]: """Return a dict of significant states during a time period.""" if not recorder.get_instance(hass).states_meta_manager.active: from .legacy import ( # pylint: disable=import-outside-toplevel @@ -128,7 +127,7 @@ def get_significant_states_with_session( minimal_response: bool = False, no_attributes: bool = False, compressed_state_format: bool = False, -) -> MutableMapping[str, list[State | dict[str, Any]]]: +) -> dict[str, list[State | dict[str, Any]]]: """Return a dict of significant states during a time period.""" if not recorder.get_instance(hass).states_meta_manager.active: from .legacy import ( # pylint: disable=import-outside-toplevel @@ -162,7 +161,7 @@ def state_changes_during_period( descending: bool = False, limit: int | None = None, include_start_time_state: bool = True, -) -> MutableMapping[str, list[State]]: +) -> dict[str, list[State]]: """Return a list of states that changed during a time period.""" if not recorder.get_instance(hass).states_meta_manager.active: from .legacy import ( # pylint: disable=import-outside-toplevel diff --git a/homeassistant/components/recorder/history/legacy.py b/homeassistant/components/recorder/history/legacy.py index ad9505e1af2..8ee3cd30316 100644 --- a/homeassistant/components/recorder/history/legacy.py +++ b/homeassistant/components/recorder/history/legacy.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections import defaultdict -from collections.abc import Callable, Iterable, Iterator, MutableMapping +from collections.abc import Callable, Iterable, Iterator from datetime import datetime from itertools import groupby from operator import attrgetter @@ -209,7 +209,7 @@ def get_significant_states( minimal_response: bool = False, no_attributes: bool = False, compressed_state_format: bool = False, -) -> MutableMapping[str, list[State | dict[str, Any]]]: +) -> dict[str, list[State | dict[str, Any]]]: """Wrap get_significant_states_with_session with an sql session.""" with session_scope(hass=hass, read_only=True) as session: return get_significant_states_with_session( @@ -317,7 +317,7 @@ def get_significant_states_with_session( minimal_response: bool = False, no_attributes: bool = False, compressed_state_format: bool = False, -) -> MutableMapping[str, list[State | dict[str, Any]]]: +) -> dict[str, list[State | dict[str, Any]]]: """Return states changes during UTC period start_time - end_time. entity_ids is an optional iterable of entities to include in the results. @@ -365,14 +365,14 @@ def get_full_significant_states_with_session( include_start_time_state: bool = True, significant_changes_only: bool = True, no_attributes: bool = False, -) -> MutableMapping[str, list[State]]: +) -> dict[str, list[State]]: """Variant of get_significant_states_with_session. Difference with get_significant_states_with_session is that it does not return minimal responses. """ return cast( - MutableMapping[str, list[State]], + dict[str, list[State]], get_significant_states_with_session( hass=hass, session=session, @@ -454,7 +454,7 @@ def state_changes_during_period( descending: bool = False, limit: int | None = None, include_start_time_state: bool = True, -) -> MutableMapping[str, list[State]]: +) -> dict[str, list[State]]: """Return states changes during UTC period start_time - end_time.""" if not entity_id: raise ValueError("entity_id must be provided") @@ -471,7 +471,7 @@ def state_changes_during_period( ) states = execute_stmt_lambda_element(session, stmt, None, end_time) return cast( - MutableMapping[str, list[State]], + dict[str, list[State]], _sorted_states_to_dict( hass, session, @@ -522,7 +522,7 @@ def _get_last_state_changes_stmt( def get_last_state_changes( hass: HomeAssistant, number_of_states: int, entity_id: str -) -> MutableMapping[str, list[State]]: +) -> dict[str, list[State]]: """Return the last number_of_states.""" entity_id_lower = entity_id.lower() entity_ids = [entity_id_lower] @@ -533,7 +533,7 @@ def get_last_state_changes( ) states = list(execute_stmt_lambda_element(session, stmt)) return cast( - MutableMapping[str, list[State]], + dict[str, list[State]], _sorted_states_to_dict( hass, session, @@ -693,7 +693,7 @@ def _sorted_states_to_dict( minimal_response: bool = False, no_attributes: bool = False, compressed_state_format: bool = False, -) -> MutableMapping[str, list[State | dict[str, Any]]]: +) -> dict[str, list[State | dict[str, Any]]]: """Convert SQL results into JSON friendly data structure. This takes our state list and turns it into a JSON friendly data diff --git a/homeassistant/components/recorder/history/modern.py b/homeassistant/components/recorder/history/modern.py index 675bb1b8cf8..96347a1f57b 100644 --- a/homeassistant/components/recorder/history/modern.py +++ b/homeassistant/components/recorder/history/modern.py @@ -2,7 +2,7 @@ from __future__ import annotations -from collections.abc import Callable, Iterable, Iterator, MutableMapping +from collections.abc import Callable, Iterable, Iterator from datetime import datetime from itertools import groupby from operator import itemgetter @@ -117,7 +117,7 @@ def get_significant_states( minimal_response: bool = False, no_attributes: bool = False, compressed_state_format: bool = False, -) -> MutableMapping[str, list[State | dict[str, Any]]]: +) -> dict[str, list[State | dict[str, Any]]]: """Wrap get_significant_states_with_session with an sql session.""" with session_scope(hass=hass, read_only=True) as session: return get_significant_states_with_session( @@ -213,7 +213,7 @@ def get_significant_states_with_session( minimal_response: bool = False, no_attributes: bool = False, compressed_state_format: bool = False, -) -> MutableMapping[str, list[State | dict[str, Any]]]: +) -> dict[str, list[State | dict[str, Any]]]: """Return states changes during UTC period start_time - end_time. entity_ids is an optional iterable of entities to include in the results. @@ -296,14 +296,14 @@ def get_full_significant_states_with_session( include_start_time_state: bool = True, significant_changes_only: bool = True, no_attributes: bool = False, -) -> MutableMapping[str, list[State]]: +) -> dict[str, list[State]]: """Variant of get_significant_states_with_session. Difference with get_significant_states_with_session is that it does not return minimal responses. """ return cast( - MutableMapping[str, list[State]], + dict[str, list[State]], get_significant_states_with_session( hass=hass, session=session, @@ -390,7 +390,7 @@ def state_changes_during_period( descending: bool = False, limit: int | None = None, include_start_time_state: bool = True, -) -> MutableMapping[str, list[State]]: +) -> dict[str, list[State]]: """Return states changes during UTC period start_time - end_time.""" has_last_reported = ( recorder.get_instance(hass).schema_version >= LAST_REPORTED_SCHEMA_VERSION @@ -438,7 +438,7 @@ def state_changes_during_period( ], ) return cast( - MutableMapping[str, list[State]], + dict[str, list[State]], _sorted_states_to_dict( execute_stmt_lambda_element( session, stmt, None, end_time, orm_rows=False @@ -504,7 +504,7 @@ def _get_last_state_changes_multiple_stmt( def get_last_state_changes( hass: HomeAssistant, number_of_states: int, entity_id: str -) -> MutableMapping[str, list[State]]: +) -> dict[str, list[State]]: """Return the last number_of_states.""" has_last_reported = ( recorder.get_instance(hass).schema_version >= LAST_REPORTED_SCHEMA_VERSION @@ -539,7 +539,7 @@ def get_last_state_changes( ) states = list(execute_stmt_lambda_element(session, stmt, orm_rows=False)) return cast( - MutableMapping[str, list[State]], + dict[str, list[State]], _sorted_states_to_dict( reversed(states), None, @@ -680,7 +680,7 @@ def _sorted_states_to_dict( compressed_state_format: bool = False, descending: bool = False, no_attributes: bool = False, -) -> MutableMapping[str, list[State | dict[str, Any]]]: +) -> dict[str, list[State | dict[str, Any]]]: """Convert SQL results into JSON friendly data structure. This takes our state list and turns it into a JSON friendly data diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 97ad49fb937..1db811599ad 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections import defaultdict -from collections.abc import Callable, Iterable, MutableMapping +from collections.abc import Callable, Iterable import datetime import itertools import logging @@ -402,7 +402,7 @@ def compile_statistics( # noqa: C901 entities_full_history = [ i.entity_id for i in sensor_states if "sum" in wanted_statistics[i.entity_id] ] - history_list: MutableMapping[str, list[State]] = {} + history_list: dict[str, list[State]] = {} if entities_full_history: history_list = history.get_full_significant_states_with_session( hass, From 2fdb420d1a9bc2a2adbe4a084ff66bc4c34f754c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 9 Apr 2024 18:04:52 -1000 Subject: [PATCH 463/967] Bump bleak-retry-connector 3.5.0 (#115328) --- 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 62296ddd8b8..58009216464 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -15,7 +15,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.21.1", - "bleak-retry-connector==3.4.0", + "bleak-retry-connector==3.5.0", "bluetooth-adapters==0.18.0", "bluetooth-auto-recovery==1.4.0", "bluetooth-data-tools==1.19.0", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 19fb7549150..4acbe3fae58 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -14,7 +14,7 @@ atomicwrites-homeassistant==1.4.1 attrs==23.2.0 awesomeversion==24.2.0 bcrypt==4.1.2 -bleak-retry-connector==3.4.0 +bleak-retry-connector==3.5.0 bleak==0.21.1 bluetooth-adapters==0.18.0 bluetooth-auto-recovery==1.4.0 diff --git a/requirements_all.txt b/requirements_all.txt index 2bcc0be7f31..26a3c6c82aa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -560,7 +560,7 @@ bizkaibus==0.1.1 bleak-esphome==1.0.0 # homeassistant.components.bluetooth -bleak-retry-connector==3.4.0 +bleak-retry-connector==3.5.0 # homeassistant.components.bluetooth bleak==0.21.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 80bce7f87f7..b3179bb9806 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -482,7 +482,7 @@ bimmer-connected[china]==0.14.6 bleak-esphome==1.0.0 # homeassistant.components.bluetooth -bleak-retry-connector==3.4.0 +bleak-retry-connector==3.5.0 # homeassistant.components.bluetooth bleak==0.21.1 From 0636ba340c4ba79fd90280d80644c837ed06dd58 Mon Sep 17 00:00:00 2001 From: Collin Fair Date: Tue, 9 Apr 2024 22:28:06 -0600 Subject: [PATCH 464/967] Fix flakiness of test_measure_sliding_window (#115322) --- tests/components/history_stats/test_sensor.py | 93 +++++++++---------- 1 file changed, 46 insertions(+), 47 deletions(-) diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index 1982ec12188..9a7d8ef110a 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -1175,53 +1175,6 @@ async def test_measure_sliding_window( ] } - await async_setup_component( - hass, - "sensor", - { - "sensor": [ - { - "platform": "history_stats", - "entity_id": "binary_sensor.test_id", - "name": "sensor1", - "state": "on", - "start": "{{ as_timestamp(now()) - 3600 }}", - "end": "{{ as_timestamp(now()) + 3600 }}", - "type": "time", - }, - { - "platform": "history_stats", - "entity_id": "binary_sensor.test_id", - "name": "sensor2", - "state": "on", - "start": "{{ as_timestamp(now()) - 3600 }}", - "end": "{{ as_timestamp(now()) + 3600 }}", - "type": "time", - "unique_id": "6b1f54e3-4065-43ca-8492-d0d4506a573a", - }, - { - "platform": "history_stats", - "entity_id": "binary_sensor.test_id", - "name": "sensor3", - "state": "on", - "start": "{{ as_timestamp(now()) - 3600 }}", - "end": "{{ as_timestamp(now()) + 3600 }}", - "type": "count", - }, - { - "platform": "history_stats", - "entity_id": "binary_sensor.test_id", - "name": "sensor4", - "state": "on", - "start": "{{ as_timestamp(now()) - 3600 }}", - "end": "{{ as_timestamp(now()) + 3600 }}", - "type": "ratio", - }, - ] - }, - ) - await hass.async_block_till_done() - with ( patch( "homeassistant.components.recorder.history.state_changes_during_period", @@ -1229,6 +1182,52 @@ async def test_measure_sliding_window( ), freeze_time(start_time), ): + await async_setup_component( + hass, + "sensor", + { + "sensor": [ + { + "platform": "history_stats", + "entity_id": "binary_sensor.test_id", + "name": "sensor1", + "state": "on", + "start": "{{ as_timestamp(now()) - 3600 }}", + "end": "{{ as_timestamp(now()) + 3600 }}", + "type": "time", + }, + { + "platform": "history_stats", + "entity_id": "binary_sensor.test_id", + "name": "sensor2", + "state": "on", + "start": "{{ as_timestamp(now()) - 3600 }}", + "end": "{{ as_timestamp(now()) + 3600 }}", + "type": "time", + "unique_id": "6b1f54e3-4065-43ca-8492-d0d4506a573a", + }, + { + "platform": "history_stats", + "entity_id": "binary_sensor.test_id", + "name": "sensor3", + "state": "on", + "start": "{{ as_timestamp(now()) - 3600 }}", + "end": "{{ as_timestamp(now()) + 3600 }}", + "type": "count", + }, + { + "platform": "history_stats", + "entity_id": "binary_sensor.test_id", + "name": "sensor4", + "state": "on", + "start": "{{ as_timestamp(now()) - 3600 }}", + "end": "{{ as_timestamp(now()) + 3600 }}", + "type": "ratio", + }, + ] + }, + ) + await hass.async_block_till_done() for i in range(1, 5): await async_update_entity(hass, f"sensor.sensor{i}") await hass.async_block_till_done() From d61db732dab3bbc655f87c1022d247337f6100c8 Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Wed, 10 Apr 2024 06:57:27 +0200 Subject: [PATCH 465/967] Enable Ruff SLOT rules (#115043) --- homeassistant/helpers/template.py | 2 ++ homeassistant/util/event_type.py | 2 ++ pyproject.toml | 1 + tests/components/bluetooth/test_wrappers.py | 2 +- tests/helpers/test_config_validation.py | 2 +- tests/helpers/test_template.py | 2 +- tests/util/test_json.py | 2 +- 7 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 501dd21d416..d344a473494 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -305,6 +305,8 @@ def gen_result_wrapper(kls: type[dict | list | set]) -> type: class TupleWrapper(tuple, ResultWrapper): """Wrap a tuple.""" + __slots__ = () + # This is all magic to be allowed to subclass a tuple. def __new__(cls, value: tuple, *, render_result: str | None = None) -> Self: diff --git a/homeassistant/util/event_type.py b/homeassistant/util/event_type.py index e96d45c80a3..509a35d33ae 100644 --- a/homeassistant/util/event_type.py +++ b/homeassistant/util/event_type.py @@ -18,3 +18,5 @@ class EventType(str, Generic[_DataT]): At runtime this is a generic subclass of str. """ + + __slots__ = () diff --git a/pyproject.toml b/pyproject.toml index bda0ee4f85f..9ea03a63ff8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -664,6 +664,7 @@ select = [ "S608", # hardcoded-sql-expression "S609", # unix-command-wildcard-injection "SIM", # flake8-simplify + "SLOT", # flake8-slots "T100", # Trace found: {name} used "T20", # flake8-print "TID251", # Banned imports diff --git a/tests/components/bluetooth/test_wrappers.py b/tests/components/bluetooth/test_wrappers.py index 0630d671038..c14fb8a58c1 100644 --- a/tests/components/bluetooth/test_wrappers.py +++ b/tests/components/bluetooth/test_wrappers.py @@ -383,7 +383,7 @@ async def test_passing_subclassed_str_as_address( _, cancel_hci0, cancel_hci1 = _generate_scanners_with_fake_devices(hass) class SubclassedStr(str): - pass + __slots__ = () address = SubclassedStr("00:00:00:00:00:01") client = bleak.BleakClient(address) diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 9816dc38189..5e9fcd9d661 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -544,7 +544,7 @@ def test_string(hass: HomeAssistant) -> None: # Test subclasses of str are returned class MyString(str): - pass + __slots__ = () my_string = MyString("hello") assert schema(my_string) is my_string diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 87279ef707b..fe152ac0d56 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -1270,7 +1270,7 @@ def test_to_json(hass: HomeAssistant) -> None: # Test special case where substring class cannot be rendered # See: https://github.com/ijl/orjson/issues/445 class MyStr(str): - pass + __slots__ = () expected_result = '{"mykey1":11.0,"mykey2":"myvalue2","mykey3":["opt3b","opt3a"]}' test_dict = { diff --git a/tests/util/test_json.py b/tests/util/test_json.py index b4a52cb4b41..3eccb524538 100644 --- a/tests/util/test_json.py +++ b/tests/util/test_json.py @@ -163,7 +163,7 @@ async def test_loading_derived_class(): """Test loading data from classes derived from str.""" class MyStr(str): - pass + __slots__ = () class MyBytes(bytes): pass From 7e1a5b19c43fb1d36b72838ecde9f0fcae6cdd84 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 10 Apr 2024 08:50:27 +0200 Subject: [PATCH 466/967] Add entity translations to Netatmo (#115104) * Add entity translations to Netatmo * Yes * Remove * Fix name --- homeassistant/components/netatmo/icons.json | 34 + homeassistant/components/netatmo/sensor.py | 299 +- homeassistant/components/netatmo/strings.json | 46 + .../netatmo/snapshots/test_sensor.ambr | 2848 +++++++++-------- tests/components/netatmo/test_sensor.py | 10 +- 5 files changed, 1693 insertions(+), 1544 deletions(-) diff --git a/homeassistant/components/netatmo/icons.json b/homeassistant/components/netatmo/icons.json index c585a9c7587..31b1740ab21 100644 --- a/homeassistant/components/netatmo/icons.json +++ b/homeassistant/components/netatmo/icons.json @@ -1,4 +1,38 @@ { + "entity": { + "sensor": { + "temp_trend": { + "default": "mdi:trending-up" + }, + "pressure_trend": { + "default": "mdi:trending-up" + }, + "wind_direction": { + "default": "mdi:compass-outline" + }, + "wind_angle": { + "default": "mdi:compass-outline" + }, + "gust_direction": { + "default": "mdi:compass-outline" + }, + "gust_angle": { + "default": "mdi:compass-outline" + }, + "reachable": { + "default": "mdi:signal" + }, + "rf_strength": { + "default": "mdi:signal" + }, + "wifi_strength": { + "default": "mdi:wifi" + }, + "health_idx": { + "default": "mdi:cloud" + } + } + }, "services": { "set_camera_light": "mdi:led-on", "set_schedule": "mdi:calendar-clock", diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 8fe3b79fbac..7e7b6029572 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -2,12 +2,14 @@ from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass import logging -from typing import cast +from typing import Any, cast import pyatmo from pyatmo import DeviceType +from pyatmo.modules import PublicWeatherArea from homeassistant.components.sensor import ( SensorDeviceClass, @@ -41,6 +43,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType from .const import ( CONF_URL_ENERGY, @@ -61,18 +64,46 @@ from .helper import NetatmoArea _LOGGER = logging.getLogger(__name__) -SUPPORTED_PUBLIC_SENSOR_TYPES: tuple[str, ...] = ( - "temperature", - "pressure", - "humidity", - "rain", - "wind_strength", - "gust_strength", - "sum_rain_1", - "sum_rain_24", - "wind_angle", - "gust_angle", -) + +def process_health(health: StateType) -> str | None: + """Process health index and return string for display.""" + if not isinstance(health, int): + return None + if health == 0: + return "Healthy" + if health == 1: + return "Fine" + if health == 2: + return "Fair" + if health == 3: + return "Poor" + return "Unhealthy" + + +def process_rf(strength: StateType) -> str | None: + """Process wifi signal strength and return string for display.""" + if not isinstance(strength, int): + return None + if strength >= 90: + return "Low" + if strength >= 76: + return "Medium" + if strength >= 60: + return "High" + return "Full" + + +def process_wifi(strength: StateType) -> str | None: + """Process wifi signal strength and return string for display.""" + if not isinstance(strength, int): + return None + if strength >= 86: + return "Low" + if strength >= 71: + return "Medium" + if strength >= 56: + return "High" + return "Full" @dataclass(frozen=True, kw_only=True) @@ -80,27 +111,25 @@ class NetatmoSensorEntityDescription(SensorEntityDescription): """Describes Netatmo sensor entity.""" netatmo_name: str + value_fn: Callable[[StateType], StateType] = lambda x: x SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( NetatmoSensorEntityDescription( key="temperature", - name="Temperature", netatmo_name="temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.TEMPERATURE, + suggested_display_precision=1, ), NetatmoSensorEntityDescription( key="temp_trend", - name="Temperature trend", netatmo_name="temp_trend", entity_registry_enabled_default=False, - icon="mdi:trending-up", ), NetatmoSensorEntityDescription( key="co2", - name="CO2", netatmo_name="co2", native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, state_class=SensorStateClass.MEASUREMENT, @@ -108,22 +137,19 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( ), NetatmoSensorEntityDescription( key="pressure", - name="Pressure", netatmo_name="pressure", native_unit_of_measurement=UnitOfPressure.MBAR, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.ATMOSPHERIC_PRESSURE, + suggested_display_precision=1, ), NetatmoSensorEntityDescription( key="pressure_trend", - name="Pressure trend", netatmo_name="pressure_trend", entity_registry_enabled_default=False, - icon="mdi:trending-up", ), NetatmoSensorEntityDescription( key="noise", - name="Noise", netatmo_name="noise", native_unit_of_measurement=UnitOfSoundPressure.DECIBEL, device_class=SensorDeviceClass.SOUND_PRESSURE, @@ -131,7 +157,6 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( ), NetatmoSensorEntityDescription( key="humidity", - name="Humidity", netatmo_name="humidity", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, @@ -139,7 +164,6 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( ), NetatmoSensorEntityDescription( key="rain", - name="Rain", netatmo_name="rain", native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS, device_class=SensorDeviceClass.PRECIPITATION, @@ -147,16 +171,15 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( ), NetatmoSensorEntityDescription( key="sum_rain_1", - name="Rain last hour", netatmo_name="sum_rain_1", entity_registry_enabled_default=False, native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS, device_class=SensorDeviceClass.PRECIPITATION, state_class=SensorStateClass.TOTAL, + suggested_display_precision=1, ), NetatmoSensorEntityDescription( key="sum_rain_24", - name="Rain today", netatmo_name="sum_rain_24", native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS, device_class=SensorDeviceClass.PRECIPITATION, @@ -164,7 +187,6 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( ), NetatmoSensorEntityDescription( key="battery_percent", - name="Battery Percent", netatmo_name="battery", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=PERCENTAGE, @@ -173,22 +195,17 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( ), NetatmoSensorEntityDescription( key="windangle", - name="Direction", netatmo_name="wind_direction", - icon="mdi:compass-outline", ), NetatmoSensorEntityDescription( key="windangle_value", - name="Angle", netatmo_name="wind_angle", entity_registry_enabled_default=False, native_unit_of_measurement=DEGREE, - icon="mdi:compass-outline", state_class=SensorStateClass.MEASUREMENT, ), NetatmoSensorEntityDescription( key="windstrength", - name="Wind Strength", netatmo_name="wind_strength", native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, device_class=SensorDeviceClass.WIND_SPEED, @@ -196,23 +213,18 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( ), NetatmoSensorEntityDescription( key="gustangle", - name="Gust Direction", netatmo_name="gust_direction", entity_registry_enabled_default=False, - icon="mdi:compass-outline", ), NetatmoSensorEntityDescription( key="gustangle_value", - name="Gust Angle", netatmo_name="gust_angle", entity_registry_enabled_default=False, native_unit_of_measurement=DEGREE, - icon="mdi:compass-outline", state_class=SensorStateClass.MEASUREMENT, ), NetatmoSensorEntityDescription( key="guststrength", - name="Gust Strength", netatmo_name="gust_strength", entity_registry_enabled_default=False, native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, @@ -221,37 +233,31 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( ), NetatmoSensorEntityDescription( key="reachable", - name="Reachability", netatmo_name="reachable", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:signal", ), NetatmoSensorEntityDescription( key="rf_status", - name="Radio", netatmo_name="rf_strength", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:signal", + value_fn=process_rf, ), NetatmoSensorEntityDescription( key="wifi_status", - name="Wifi", netatmo_name="wifi_strength", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:wifi", + value_fn=process_wifi, ), NetatmoSensorEntityDescription( key="health_idx", - name="Health", netatmo_name="health_idx", - icon="mdi:cloud", + value_fn=process_health, ), NetatmoSensorEntityDescription( key="power", - name="Power", netatmo_name="power", native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, @@ -260,9 +266,100 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( ) SENSOR_TYPES_KEYS = [desc.key for desc in SENSOR_TYPES] + +@dataclass(frozen=True, kw_only=True) +class NetatmoPublicWeatherSensorEntityDescription(SensorEntityDescription): + """Describes Netatmo sensor entity.""" + + value_fn: Callable[[PublicWeatherArea], dict[str, Any]] + + +PUBLIC_WEATHER_STATION_TYPES: tuple[ + NetatmoPublicWeatherSensorEntityDescription, ... +] = ( + NetatmoPublicWeatherSensorEntityDescription( + key="temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + suggested_display_precision=1, + value_fn=lambda area: area.get_latest_temperatures(), + ), + NetatmoPublicWeatherSensorEntityDescription( + key="pressure", + native_unit_of_measurement=UnitOfPressure.MBAR, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.ATMOSPHERIC_PRESSURE, + suggested_display_precision=1, + value_fn=lambda area: area.get_latest_pressures(), + ), + NetatmoPublicWeatherSensorEntityDescription( + key="humidity", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.HUMIDITY, + value_fn=lambda area: area.get_latest_humidities(), + ), + NetatmoPublicWeatherSensorEntityDescription( + key="rain", + native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS, + device_class=SensorDeviceClass.PRECIPITATION, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda area: area.get_latest_rain(), + ), + NetatmoPublicWeatherSensorEntityDescription( + key="sum_rain_1", + translation_key="sum_rain_1", + entity_registry_enabled_default=False, + native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS, + device_class=SensorDeviceClass.PRECIPITATION, + state_class=SensorStateClass.TOTAL, + suggested_display_precision=1, + value_fn=lambda area: area.get_60_min_rain(), + ), + NetatmoPublicWeatherSensorEntityDescription( + key="sum_rain_24", + translation_key="sum_rain_24", + native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS, + device_class=SensorDeviceClass.PRECIPITATION, + state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda area: area.get_24_h_rain(), + ), + NetatmoPublicWeatherSensorEntityDescription( + key="windangle_value", + entity_registry_enabled_default=False, + native_unit_of_measurement=DEGREE, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda area: area.get_latest_wind_angles(), + ), + NetatmoPublicWeatherSensorEntityDescription( + key="windstrength", + native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, + device_class=SensorDeviceClass.WIND_SPEED, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda area: area.get_latest_wind_strengths(), + ), + NetatmoPublicWeatherSensorEntityDescription( + key="gustangle_value", + translation_key="gust_angle", + entity_registry_enabled_default=False, + native_unit_of_measurement=DEGREE, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda area: area.get_latest_gust_angles(), + ), + NetatmoPublicWeatherSensorEntityDescription( + key="guststrength", + translation_key="gust_strength", + entity_registry_enabled_default=False, + native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, + device_class=SensorDeviceClass.WIND_SPEED, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda area: area.get_latest_gust_strengths(), + ), +) + BATTERY_SENSOR_DESCRIPTION = NetatmoSensorEntityDescription( key="battery", - name="Battery Percent", netatmo_name="battery", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=PERCENTAGE, @@ -349,7 +446,7 @@ async def async_setup_entry( if device.model == "Public Weather station" } - new_entities = [] + new_entities: list[NetatmoPublicSensor] = [] for area in [ NetatmoArea(**i) for i in entry.options.get(CONF_WEATHER_AREAS, {}).values() ]: @@ -378,11 +475,8 @@ async def async_setup_entry( ) new_entities.extend( - [ - NetatmoPublicSensor(data_handler, area, description) - for description in SENSOR_TYPES - if description.netatmo_name in SUPPORTED_PUBLIC_SENSOR_TYPES - ] + NetatmoPublicSensor(data_handler, area, description) + for description in PUBLIC_WEATHER_STATION_TYPES ) for device_id in entities.values(): @@ -411,6 +505,7 @@ class NetatmoWeatherSensor(NetatmoModuleEntity, SensorEntity): """Initialize the sensor.""" super().__init__(netatmo_device) self.entity_description = description + self._attr_translation_key = description.netatmo_name category = getattr(self.device.device_category, "name") self._publishers.extend( [ @@ -439,34 +534,20 @@ class NetatmoWeatherSensor(NetatmoModuleEntity, SensorEntity): return super().device_type return DeviceType(self.device.device_type.partition(".")[2]) + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self.device.reachable or False + @callback def async_update_callback(self) -> None: """Update the entity's state.""" - if ( - not self.device.reachable - or (state := getattr(self.device, self.entity_description.netatmo_name)) - is None - ): - if self.available: - self._attr_available = False - return - - if self.entity_description.netatmo_name in { - "temperature", - "pressure", - "sum_rain_1", - }: - self._attr_native_value = round(state, 1) - elif self.entity_description.netatmo_name == "rf_strength": - self._attr_native_value = process_rf(state) - elif self.entity_description.netatmo_name == "wifi_strength": - self._attr_native_value = process_wifi(state) - elif self.entity_description.netatmo_name == "health_idx": - self._attr_native_value = process_health(state) - else: - self._attr_native_value = state - - self._attr_available = True + value = cast( + StateType, getattr(self.device, self.entity_description.netatmo_name) + ) + if value is not None: + value = self.entity_description.value_fn(value) + self._attr_native_value = value self.async_write_ha_state() @@ -559,41 +640,6 @@ class NetatmoSensor(NetatmoModuleEntity, SensorEntity): self.async_write_ha_state() -def process_health(health: int) -> str: - """Process health index and return string for display.""" - if health == 0: - return "Healthy" - if health == 1: - return "Fine" - if health == 2: - return "Fair" - if health == 3: - return "Poor" - return "Unhealthy" - - -def process_rf(strength: int) -> str: - """Process wifi signal strength and return string for display.""" - if strength >= 90: - return "Low" - if strength >= 76: - return "Medium" - if strength >= 60: - return "High" - return "Full" - - -def process_wifi(strength: int) -> str: - """Process wifi signal strength and return string for display.""" - if strength >= 86: - return "Low" - if strength >= 71: - return "Medium" - if strength >= 56: - return "High" - return "Full" - - class NetatmoRoomSensor(NetatmoRoomEntity, SensorEntity): """Implementation of a Netatmo room sensor.""" @@ -636,13 +682,13 @@ class NetatmoRoomSensor(NetatmoRoomEntity, SensorEntity): class NetatmoPublicSensor(NetatmoBaseEntity, SensorEntity): """Represent a single sensor in a Netatmo.""" - entity_description: NetatmoSensorEntityDescription + entity_description: NetatmoPublicWeatherSensorEntityDescription def __init__( self, data_handler: NetatmoDataHandler, area: NetatmoArea, - description: NetatmoSensorEntityDescription, + description: NetatmoPublicWeatherSensorEntityDescription, ) -> None: """Initialize the sensor.""" super().__init__(data_handler) @@ -720,28 +766,7 @@ class NetatmoPublicSensor(NetatmoBaseEntity, SensorEntity): @callback def async_update_callback(self) -> None: """Update the entity's state.""" - data = None - - if self.entity_description.netatmo_name == "temperature": - data = self._station.get_latest_temperatures() - elif self.entity_description.netatmo_name == "pressure": - data = self._station.get_latest_pressures() - elif self.entity_description.netatmo_name == "humidity": - data = self._station.get_latest_humidities() - elif self.entity_description.netatmo_name == "rain": - data = self._station.get_latest_rain() - elif self.entity_description.netatmo_name == "sum_rain_1": - data = self._station.get_60_min_rain() - elif self.entity_description.netatmo_name == "sum_rain_24": - data = self._station.get_24_h_rain() - elif self.entity_description.netatmo_name == "wind_strength": - data = self._station.get_latest_wind_strengths() - elif self.entity_description.netatmo_name == "gust_strength": - data = self._station.get_latest_gust_strengths() - elif self.entity_description.netatmo_name == "wind_angle": - data = self._station.get_latest_wind_angles() - elif self.entity_description.netatmo_name == "gust_angle": - data = self._station.get_latest_gust_angles() + data = self.entity_description.value_fn(self._station) if not data: if self.available: @@ -760,5 +785,5 @@ class NetatmoPublicSensor(NetatmoBaseEntity, SensorEntity): elif self._mode == "max": self._attr_native_value = max(values) - self._attr_available = self.state is not None + self._attr_available = self.native_value is not None self.async_write_ha_state() diff --git a/homeassistant/components/netatmo/strings.json b/homeassistant/components/netatmo/strings.json index e504b27b599..f6aba92d005 100644 --- a/homeassistant/components/netatmo/strings.json +++ b/homeassistant/components/netatmo/strings.json @@ -166,5 +166,51 @@ "name": "Clear temperature setting", "description": "Clears any temperature setting for a Netatmo climate device reverting it to the current preset or schedule." } + }, + "entity": { + "sensor": { + "temp_trend": { + "name": "Temperature trend" + }, + "pressure_trend": { + "name": "Pressure trend" + }, + "noise": { + "name": "Noise" + }, + "sum_rain_1": { + "name": "Precipitation last hour" + }, + "sum_rain_24": { + "name": "Precipitation today" + }, + "wind_direction": { + "name": "Wind direction" + }, + "wind_angle": { + "name": "Wind angle" + }, + "gust_direction": { + "name": "Gust direction" + }, + "gust_angle": { + "name": "Gust angle" + }, + "gust_strength": { + "name": "Gust strength" + }, + "reachable": { + "name": "Reachability" + }, + "rf_strength": { + "name": "Radio" + }, + "wifi_strength": { + "name": "Wi-Fi" + }, + "health_idx": { + "name": "Health index" + } + } } } diff --git a/tests/components/netatmo/snapshots/test_sensor.ambr b/tests/components/netatmo/snapshots/test_sensor.ambr index 8a670140617..ed5f4decc86 100644 --- a/tests/components/netatmo/snapshots/test_sensor.ambr +++ b/tests/components/netatmo/snapshots/test_sensor.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_entity[sensor.baby_bedroom_co2-entry] +# name: test_entity[sensor.baby_bedroom_atmospheric_pressure-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -13,7 +13,67 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.baby_bedroom_co2', + 'entity_id': 'sensor.baby_bedroom_atmospheric_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': 'Atmospheric pressure', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pressure', + 'unique_id': '12:34:56:26:68:92-pressure', + 'unit_of_measurement': , + }) +# --- +# name: test_entity[sensor.baby_bedroom_atmospheric_pressure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'atmospheric_pressure', + 'friendly_name': 'Baby Bedroom Atmospheric pressure', + 'latitude': 13.377726, + 'longitude': 52.516263, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.baby_bedroom_atmospheric_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1021.4', + }) +# --- +# name: test_entity[sensor.baby_bedroom_carbon_dioxide-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.baby_bedroom_carbon_dioxide', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -25,35 +85,35 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'CO2', + 'original_name': 'Carbon dioxide', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'co2', 'unique_id': '12:34:56:26:68:92-co2', 'unit_of_measurement': 'ppm', }) # --- -# name: test_entity[sensor.baby_bedroom_co2-state] +# name: test_entity[sensor.baby_bedroom_carbon_dioxide-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'carbon_dioxide', - 'friendly_name': 'Baby Bedroom CO2', + 'friendly_name': 'Baby Bedroom Carbon dioxide', 'latitude': 13.377726, 'longitude': 52.516263, 'state_class': , 'unit_of_measurement': 'ppm', }), 'context': , - 'entity_id': 'sensor.baby_bedroom_co2', + 'entity_id': 'sensor.baby_bedroom_carbon_dioxide', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '1053', }) # --- -# name: test_entity[sensor.baby_bedroom_health-entry] +# name: test_entity[sensor.baby_bedroom_health_index-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -65,7 +125,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.baby_bedroom_health', + 'entity_id': 'sensor.baby_bedroom_health_index', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -76,27 +136,26 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:cloud', - 'original_name': 'Health', + 'original_icon': None, + 'original_name': 'Health index', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'health_idx', 'unique_id': '12:34:56:26:68:92-health_idx', 'unit_of_measurement': None, }) # --- -# name: test_entity[sensor.baby_bedroom_health-state] +# name: test_entity[sensor.baby_bedroom_health_index-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', - 'friendly_name': 'Baby Bedroom Health', - 'icon': 'mdi:cloud', + 'friendly_name': 'Baby Bedroom Health index', 'latitude': 13.377726, 'longitude': 52.516263, }), 'context': , - 'entity_id': 'sensor.baby_bedroom_health', + 'entity_id': 'sensor.baby_bedroom_health_index', 'last_changed': , 'last_reported': , 'last_updated': , @@ -133,7 +192,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'humidity', 'unique_id': '12:34:56:26:68:92-humidity', 'unit_of_measurement': '%', }) @@ -187,7 +246,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'noise', 'unique_id': '12:34:56:26:68:92-noise', 'unit_of_measurement': , }) @@ -211,63 +270,6 @@ 'state': '45', }) # --- -# name: test_entity[sensor.baby_bedroom_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.baby_bedroom_pressure', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor.private': dict({ - 'suggested_unit_of_measurement': , - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Pressure', - 'platform': 'netatmo', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '12:34:56:26:68:92-pressure', - 'unit_of_measurement': , - }) -# --- -# name: test_entity[sensor.baby_bedroom_pressure-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by Netatmo', - 'device_class': 'atmospheric_pressure', - 'friendly_name': 'Baby Bedroom Pressure', - 'latitude': 13.377726, - 'longitude': 52.516263, - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.baby_bedroom_pressure', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '1021.4', - }) -# --- # name: test_entity[sensor.baby_bedroom_pressure_trend-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -291,12 +293,12 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:trending-up', + 'original_icon': None, 'original_name': 'Pressure trend', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'pressure_trend', 'unique_id': '12:34:56:26:68:92-pressure_trend', 'unit_of_measurement': None, }) @@ -306,14 +308,15 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'friendly_name': 'Baby Bedroom Pressure trend', - 'icon': 'mdi:trending-up', + 'latitude': 13.377726, + 'longitude': 52.516263, }), 'context': , 'entity_id': 'sensor.baby_bedroom_pressure_trend', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'unknown', }) # --- # name: test_entity[sensor.baby_bedroom_reachability-entry] @@ -339,12 +342,12 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', + 'original_icon': None, 'original_name': 'Reachability', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'reachable', 'unique_id': '12:34:56:26:68:92-reachable', 'unit_of_measurement': None, }) @@ -354,7 +357,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'friendly_name': 'Baby Bedroom Reachability', - 'icon': 'mdi:signal', 'latitude': 13.377726, 'longitude': 52.516263, }), @@ -389,6 +391,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -396,7 +401,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'temperature', 'unique_id': '12:34:56:26:68:92-temperature', 'unit_of_measurement': , }) @@ -443,12 +448,12 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:trending-up', + 'original_icon': None, 'original_name': 'Temperature trend', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'temp_trend', 'unique_id': '12:34:56:26:68:92-temp_trend', 'unit_of_measurement': None, }) @@ -458,17 +463,18 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'friendly_name': 'Baby Bedroom Temperature trend', - 'icon': 'mdi:trending-up', + 'latitude': 13.377726, + 'longitude': 52.516263, }), 'context': , 'entity_id': 'sensor.baby_bedroom_temperature_trend', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'unknown', }) # --- -# name: test_entity[sensor.baby_bedroom_wifi-entry] +# name: test_entity[sensor.baby_bedroom_wi_fi-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -480,7 +486,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.baby_bedroom_wifi', + 'entity_id': 'sensor.baby_bedroom_wi_fi', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -491,34 +497,33 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:wifi', - 'original_name': 'Wifi', + 'original_icon': None, + 'original_name': 'Wi-Fi', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'wifi_strength', 'unique_id': '12:34:56:26:68:92-wifi_status', 'unit_of_measurement': None, }) # --- -# name: test_entity[sensor.baby_bedroom_wifi-state] +# name: test_entity[sensor.baby_bedroom_wi_fi-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', - 'friendly_name': 'Baby Bedroom Wifi', - 'icon': 'mdi:wifi', + 'friendly_name': 'Baby Bedroom Wi-Fi', 'latitude': 13.377726, 'longitude': 52.516263, }), 'context': , - 'entity_id': 'sensor.baby_bedroom_wifi', + 'entity_id': 'sensor.baby_bedroom_wi_fi', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'High', }) # --- -# name: test_entity[sensor.bedroom_co2-entry] +# name: test_entity[sensor.bedroom_atmospheric_pressure-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -532,7 +537,65 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.bedroom_co2', + 'entity_id': 'sensor.bedroom_atmospheric_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': 'Atmospheric pressure', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pressure', + 'unique_id': '12:34:56:26:69:0c-pressure', + 'unit_of_measurement': , + }) +# --- +# name: test_entity[sensor.bedroom_atmospheric_pressure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'atmospheric_pressure', + 'friendly_name': 'Bedroom Atmospheric pressure', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.bedroom_atmospheric_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_entity[sensor.bedroom_carbon_dioxide-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.bedroom_carbon_dioxide', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -544,33 +607,33 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'CO2', + 'original_name': 'Carbon dioxide', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'co2', 'unique_id': '12:34:56:26:69:0c-co2', 'unit_of_measurement': 'ppm', }) # --- -# name: test_entity[sensor.bedroom_co2-state] +# name: test_entity[sensor.bedroom_carbon_dioxide-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'carbon_dioxide', - 'friendly_name': 'Bedroom CO2', + 'friendly_name': 'Bedroom Carbon dioxide', 'state_class': , 'unit_of_measurement': 'ppm', }), 'context': , - 'entity_id': 'sensor.bedroom_co2', + 'entity_id': 'sensor.bedroom_carbon_dioxide', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'unavailable', }) # --- -# name: test_entity[sensor.bedroom_health-entry] +# name: test_entity[sensor.bedroom_health_index-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -582,7 +645,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.bedroom_health', + 'entity_id': 'sensor.bedroom_health_index', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -593,25 +656,24 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:cloud', - 'original_name': 'Health', + 'original_icon': None, + 'original_name': 'Health index', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'health_idx', 'unique_id': '12:34:56:26:69:0c-health_idx', 'unit_of_measurement': None, }) # --- -# name: test_entity[sensor.bedroom_health-state] +# name: test_entity[sensor.bedroom_health_index-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', - 'friendly_name': 'Bedroom Health', - 'icon': 'mdi:cloud', + 'friendly_name': 'Bedroom Health index', }), 'context': , - 'entity_id': 'sensor.bedroom_health', + 'entity_id': 'sensor.bedroom_health_index', 'last_changed': , 'last_reported': , 'last_updated': , @@ -648,7 +710,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'humidity', 'unique_id': '12:34:56:26:69:0c-humidity', 'unit_of_measurement': '%', }) @@ -700,7 +762,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'noise', 'unique_id': '12:34:56:26:69:0c-noise', 'unit_of_measurement': , }) @@ -722,61 +784,6 @@ 'state': 'unavailable', }) # --- -# name: test_entity[sensor.bedroom_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.bedroom_pressure', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor.private': dict({ - 'suggested_unit_of_measurement': , - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Pressure', - 'platform': 'netatmo', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '12:34:56:26:69:0c-pressure', - 'unit_of_measurement': , - }) -# --- -# name: test_entity[sensor.bedroom_pressure-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by Netatmo', - 'device_class': 'atmospheric_pressure', - 'friendly_name': 'Bedroom Pressure', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.bedroom_pressure', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- # name: test_entity[sensor.bedroom_pressure_trend-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -800,12 +807,12 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:trending-up', + 'original_icon': None, 'original_name': 'Pressure trend', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'pressure_trend', 'unique_id': '12:34:56:26:69:0c-pressure_trend', 'unit_of_measurement': None, }) @@ -815,7 +822,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'friendly_name': 'Bedroom Pressure trend', - 'icon': 'mdi:trending-up', }), 'context': , 'entity_id': 'sensor.bedroom_pressure_trend', @@ -848,12 +854,12 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', + 'original_icon': None, 'original_name': 'Reachability', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'reachable', 'unique_id': '12:34:56:26:69:0c-reachable', 'unit_of_measurement': None, }) @@ -863,7 +869,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'friendly_name': 'Bedroom Reachability', - 'icon': 'mdi:signal', }), 'context': , 'entity_id': 'sensor.bedroom_reachability', @@ -896,6 +901,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -903,7 +911,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'temperature', 'unique_id': '12:34:56:26:69:0c-temperature', 'unit_of_measurement': , }) @@ -948,12 +956,12 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:trending-up', + 'original_icon': None, 'original_name': 'Temperature trend', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'temp_trend', 'unique_id': '12:34:56:26:69:0c-temp_trend', 'unit_of_measurement': None, }) @@ -963,7 +971,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'friendly_name': 'Bedroom Temperature trend', - 'icon': 'mdi:trending-up', }), 'context': , 'entity_id': 'sensor.bedroom_temperature_trend', @@ -973,7 +980,7 @@ 'state': 'unavailable', }) # --- -# name: test_entity[sensor.bedroom_wifi-entry] +# name: test_entity[sensor.bedroom_wi_fi-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -985,7 +992,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.bedroom_wifi', + 'entity_id': 'sensor.bedroom_wi_fi', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -996,32 +1003,31 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:wifi', - 'original_name': 'Wifi', + 'original_icon': None, + 'original_name': 'Wi-Fi', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'wifi_strength', 'unique_id': '12:34:56:26:69:0c-wifi_status', 'unit_of_measurement': None, }) # --- -# name: test_entity[sensor.bedroom_wifi-state] +# name: test_entity[sensor.bedroom_wi_fi-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', - 'friendly_name': 'Bedroom Wifi', - 'icon': 'mdi:wifi', + 'friendly_name': 'Bedroom Wi-Fi', }), 'context': , - 'entity_id': 'sensor.bedroom_wifi', + 'entity_id': 'sensor.bedroom_wi_fi', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'unavailable', }) # --- -# name: test_entity[sensor.bureau_modulate_battery_percent-entry] +# name: test_entity[sensor.bureau_modulate_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1035,7 +1041,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.bureau_modulate_battery_percent', + 'entity_id': 'sensor.bureau_modulate_battery', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1047,7 +1053,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Battery Percent', + 'original_name': 'Battery', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -1056,23 +1062,70 @@ 'unit_of_measurement': '%', }) # --- -# name: test_entity[sensor.bureau_modulate_battery_percent-state] +# name: test_entity[sensor.bureau_modulate_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'battery', - 'friendly_name': 'Bureau Modulate Battery Percent', + 'friendly_name': 'Bureau Modulate Battery', 'state_class': , 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.bureau_modulate_battery_percent', + 'entity_id': 'sensor.bureau_modulate_battery', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '90', }) # --- +# name: test_entity[sensor.cold_water_none-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.cold_water_none', + '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': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12:34:56:00:16:0e#8-12:34:56:00:16:0e#8-reachable', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[sensor.cold_water_none-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Cold water None', + }), + 'context': , + 'entity_id': 'sensor.cold_water_none', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- # name: test_entity[sensor.cold_water_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1125,7 +1178,7 @@ 'state': 'unavailable', }) # --- -# name: test_entity[sensor.cold_water_reachability-entry] +# name: test_entity[sensor.consumption_meter_none-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1137,7 +1190,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.cold_water_reachability', + 'entity_id': 'sensor.consumption_meter_none', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1148,29 +1201,28 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', - 'original_name': 'Reachability', + 'original_icon': None, + 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '12:34:56:00:16:0e#8-12:34:56:00:16:0e#8-reachable', + 'unique_id': '12:34:56:00:00:a1:4c:da-12:34:56:00:00:a1:4c:da-reachable', 'unit_of_measurement': None, }) # --- -# name: test_entity[sensor.cold_water_reachability-state] +# name: test_entity[sensor.consumption_meter_none-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', - 'friendly_name': 'Cold water Reachability', - 'icon': 'mdi:signal', + 'friendly_name': 'Consumption meter None', }), 'context': , - 'entity_id': 'sensor.cold_water_reachability', + 'entity_id': 'sensor.consumption_meter_none', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'True', }) # --- # name: test_entity[sensor.consumption_meter_power-entry] @@ -1225,54 +1277,6 @@ 'state': '476', }) # --- -# name: test_entity[sensor.consumption_meter_reachability-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.consumption_meter_reachability', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:signal', - 'original_name': 'Reachability', - 'platform': 'netatmo', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '12:34:56:00:00:a1:4c:da-12:34:56:00:00:a1:4c:da-reachable', - 'unit_of_measurement': None, - }) -# --- -# name: test_entity[sensor.consumption_meter_reachability-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by Netatmo', - 'friendly_name': 'Consumption meter Reachability', - 'icon': 'mdi:signal', - }), - 'context': , - 'entity_id': 'sensor.consumption_meter_reachability', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'True', - }) -# --- # name: test_entity[sensor.corridor_humidity-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1325,6 +1329,53 @@ 'state': '67', }) # --- +# name: test_entity[sensor.ecocompteur_none-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.ecocompteur_none', + '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': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12:34:56:00:16:0e-12:34:56:00:16:0e-reachable', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[sensor.ecocompteur_none-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Écocompteur None', + }), + 'context': , + 'entity_id': 'sensor.ecocompteur_none', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- # name: test_entity[sensor.ecocompteur_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1377,7 +1428,7 @@ 'state': 'unavailable', }) # --- -# name: test_entity[sensor.ecocompteur_reachability-entry] +# name: test_entity[sensor.gas_none-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1389,7 +1440,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.ecocompteur_reachability', + 'entity_id': 'sensor.gas_none', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1400,25 +1451,24 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', - 'original_name': 'Reachability', + 'original_icon': None, + 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '12:34:56:00:16:0e-12:34:56:00:16:0e-reachable', + 'unique_id': '12:34:56:00:16:0e#6-12:34:56:00:16:0e#6-reachable', 'unit_of_measurement': None, }) # --- -# name: test_entity[sensor.ecocompteur_reachability-state] +# name: test_entity[sensor.gas_none-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', - 'friendly_name': 'Écocompteur Reachability', - 'icon': 'mdi:signal', + 'friendly_name': 'Gas None', }), 'context': , - 'entity_id': 'sensor.ecocompteur_reachability', + 'entity_id': 'sensor.gas_none', 'last_changed': , 'last_reported': , 'last_updated': , @@ -1477,55 +1527,7 @@ 'state': 'unavailable', }) # --- -# name: test_entity[sensor.gas_reachability-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.gas_reachability', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:signal', - 'original_name': 'Reachability', - 'platform': 'netatmo', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '12:34:56:00:16:0e#6-12:34:56:00:16:0e#6-reachable', - 'unit_of_measurement': None, - }) -# --- -# name: test_entity[sensor.gas_reachability-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by Netatmo', - 'friendly_name': 'Gas Reachability', - 'icon': 'mdi:signal', - }), - 'context': , - 'entity_id': 'sensor.gas_reachability', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- -# name: test_entity[sensor.home_avg_angle-entry] +# name: test_entity[sensor.home_avg_atmospheric_pressure-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1539,7 +1541,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.home_avg_angle', + 'entity_id': 'sensor.home_avg_atmospheric_pressure', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1548,35 +1550,41 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), - 'original_device_class': None, - 'original_icon': 'mdi:compass-outline', - 'original_name': 'Angle', + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Atmospheric pressure', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': 'Home-avg-windangle_value', - 'unit_of_measurement': '°', + 'unique_id': 'Home-avg-pressure', + 'unit_of_measurement': , }) # --- -# name: test_entity[sensor.home_avg_angle-state] +# name: test_entity[sensor.home_avg_atmospheric_pressure-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', - 'friendly_name': 'Home avg Angle', - 'icon': 'mdi:compass-outline', + 'device_class': 'atmospheric_pressure', + 'friendly_name': 'Home avg Atmospheric pressure', 'latitude': 32.17901225, 'longitude': -117.17901225, 'state_class': , - 'unit_of_measurement': '°', + 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.home_avg_angle', + 'entity_id': 'sensor.home_avg_atmospheric_pressure', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '17.0', + 'state': '1010.4', }) # --- # name: test_entity[sensor.home_avg_gust_angle-entry] @@ -1604,12 +1612,12 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:compass-outline', - 'original_name': 'Gust Angle', + 'original_icon': None, + 'original_name': 'Gust angle', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'gust_angle', 'unique_id': 'Home-avg-gustangle_value', 'unit_of_measurement': '°', }) @@ -1618,8 +1626,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', - 'friendly_name': 'Home avg Gust Angle', - 'icon': 'mdi:compass-outline', + 'friendly_name': 'Home avg Gust angle', 'latitude': 32.17901225, 'longitude': -117.17901225, 'state_class': , @@ -1659,11 +1666,11 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Gust Strength', + 'original_name': 'Gust strength', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'gust_strength', 'unique_id': 'Home-avg-guststrength', 'unit_of_measurement': , }) @@ -1673,7 +1680,7 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'wind_speed', - 'friendly_name': 'Home avg Gust Strength', + 'friendly_name': 'Home avg Gust strength', 'latitude': 32.17901225, 'longitude': -117.17901225, 'state_class': , @@ -1741,7 +1748,7 @@ 'state': '63.2', }) # --- -# name: test_entity[sensor.home_avg_pressure-entry] +# name: test_entity[sensor.home_avg_none-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1755,7 +1762,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.home_avg_pressure', + 'entity_id': 'sensor.home_avg_none', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1764,41 +1771,37 @@ }), 'name': None, 'options': dict({ - 'sensor.private': dict({ - 'suggested_unit_of_measurement': , - }), }), - 'original_device_class': , + 'original_device_class': None, 'original_icon': None, - 'original_name': 'Pressure', + 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': 'Home-avg-pressure', - 'unit_of_measurement': , + 'unique_id': 'Home-avg-windangle_value', + 'unit_of_measurement': '°', }) # --- -# name: test_entity[sensor.home_avg_pressure-state] +# name: test_entity[sensor.home_avg_none-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', - 'device_class': 'atmospheric_pressure', - 'friendly_name': 'Home avg Pressure', + 'friendly_name': 'Home avg None', 'latitude': 32.17901225, 'longitude': -117.17901225, 'state_class': , - 'unit_of_measurement': , + 'unit_of_measurement': '°', }), 'context': , - 'entity_id': 'sensor.home_avg_pressure', + 'entity_id': 'sensor.home_avg_none', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1010.4', + 'state': '17.0', }) # --- -# name: test_entity[sensor.home_avg_rain-entry] +# name: test_entity[sensor.home_avg_precipitation-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1812,7 +1815,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.home_avg_rain', + 'entity_id': 'sensor.home_avg_precipitation', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1824,7 +1827,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Rain', + 'original_name': 'Precipitation', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -1833,26 +1836,26 @@ 'unit_of_measurement': , }) # --- -# name: test_entity[sensor.home_avg_rain-state] +# name: test_entity[sensor.home_avg_precipitation-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'precipitation', - 'friendly_name': 'Home avg Rain', + 'friendly_name': 'Home avg Precipitation', 'latitude': 32.17901225, 'longitude': -117.17901225, 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.home_avg_rain', + 'entity_id': 'sensor.home_avg_precipitation', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '0.1', }) # --- -# name: test_entity[sensor.home_avg_rain_last_hour-entry] +# name: test_entity[sensor.home_avg_precipitation_last_hour-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1866,7 +1869,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.home_avg_rain_last_hour', + 'entity_id': 'sensor.home_avg_precipitation_last_hour', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1875,38 +1878,41 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Rain last hour', + 'original_name': 'Precipitation last hour', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'sum_rain_1', 'unique_id': 'Home-avg-sum_rain_1', 'unit_of_measurement': , }) # --- -# name: test_entity[sensor.home_avg_rain_last_hour-state] +# name: test_entity[sensor.home_avg_precipitation_last_hour-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'precipitation', - 'friendly_name': 'Home avg Rain last hour', + 'friendly_name': 'Home avg Precipitation last hour', 'latitude': 32.17901225, 'longitude': -117.17901225, 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.home_avg_rain_last_hour', + 'entity_id': 'sensor.home_avg_precipitation_last_hour', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '0.1', }) # --- -# name: test_entity[sensor.home_avg_rain_today-entry] +# name: test_entity[sensor.home_avg_precipitation_today-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1920,7 +1926,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.home_avg_rain_today', + 'entity_id': 'sensor.home_avg_precipitation_today', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1932,28 +1938,28 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Rain today', + 'original_name': 'Precipitation today', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'sum_rain_24', 'unique_id': 'Home-avg-sum_rain_24', 'unit_of_measurement': , }) # --- -# name: test_entity[sensor.home_avg_rain_today-state] +# name: test_entity[sensor.home_avg_precipitation_today-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'precipitation', - 'friendly_name': 'Home avg Rain today', + 'friendly_name': 'Home avg Precipitation today', 'latitude': 32.17901225, 'longitude': -117.17901225, 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.home_avg_rain_today', + 'entity_id': 'sensor.home_avg_precipitation_today', 'last_changed': , 'last_reported': , 'last_updated': , @@ -1983,6 +1989,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -2014,7 +2023,7 @@ 'state': '22.7', }) # --- -# name: test_entity[sensor.home_avg_wind_strength-entry] +# name: test_entity[sensor.home_avg_wind_speed-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2028,7 +2037,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.home_avg_wind_strength', + 'entity_id': 'sensor.home_avg_wind_speed', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2040,7 +2049,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Wind Strength', + 'original_name': 'Wind speed', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -2049,26 +2058,26 @@ 'unit_of_measurement': , }) # --- -# name: test_entity[sensor.home_avg_wind_strength-state] +# name: test_entity[sensor.home_avg_wind_speed-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'wind_speed', - 'friendly_name': 'Home avg Wind Strength', + 'friendly_name': 'Home avg Wind speed', 'latitude': 32.17901225, 'longitude': -117.17901225, 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.home_avg_wind_strength', + 'entity_id': 'sensor.home_avg_wind_speed', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '15.0', }) # --- -# name: test_entity[sensor.home_max_angle-entry] +# name: test_entity[sensor.home_max_atmospheric_pressure-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2082,7 +2091,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.home_max_angle', + 'entity_id': 'sensor.home_max_atmospheric_pressure', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2091,35 +2100,41 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), - 'original_device_class': None, - 'original_icon': 'mdi:compass-outline', - 'original_name': 'Angle', + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Atmospheric pressure', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': 'Home-max-windangle_value', - 'unit_of_measurement': '°', + 'unique_id': 'Home-max-pressure', + 'unit_of_measurement': , }) # --- -# name: test_entity[sensor.home_max_angle-state] +# name: test_entity[sensor.home_max_atmospheric_pressure-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', - 'friendly_name': 'Home max Angle', - 'icon': 'mdi:compass-outline', + 'device_class': 'atmospheric_pressure', + 'friendly_name': 'Home max Atmospheric pressure', 'latitude': 32.17901225, 'longitude': -117.17901225, 'state_class': , - 'unit_of_measurement': '°', + 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.home_max_angle', + 'entity_id': 'sensor.home_max_atmospheric_pressure', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '17', + 'state': '1014.4', }) # --- # name: test_entity[sensor.home_max_gust_angle-entry] @@ -2147,12 +2162,12 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:compass-outline', - 'original_name': 'Gust Angle', + 'original_icon': None, + 'original_name': 'Gust angle', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'gust_angle', 'unique_id': 'Home-max-gustangle_value', 'unit_of_measurement': '°', }) @@ -2161,8 +2176,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', - 'friendly_name': 'Home max Gust Angle', - 'icon': 'mdi:compass-outline', + 'friendly_name': 'Home max Gust angle', 'latitude': 32.17901225, 'longitude': -117.17901225, 'state_class': , @@ -2202,11 +2216,11 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Gust Strength', + 'original_name': 'Gust strength', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'gust_strength', 'unique_id': 'Home-max-guststrength', 'unit_of_measurement': , }) @@ -2216,7 +2230,7 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'wind_speed', - 'friendly_name': 'Home max Gust Strength', + 'friendly_name': 'Home max Gust strength', 'latitude': 32.17901225, 'longitude': -117.17901225, 'state_class': , @@ -2284,7 +2298,7 @@ 'state': '76', }) # --- -# name: test_entity[sensor.home_max_pressure-entry] +# name: test_entity[sensor.home_max_none-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2298,7 +2312,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.home_max_pressure', + 'entity_id': 'sensor.home_max_none', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2307,41 +2321,37 @@ }), 'name': None, 'options': dict({ - 'sensor.private': dict({ - 'suggested_unit_of_measurement': , - }), }), - 'original_device_class': , + 'original_device_class': None, 'original_icon': None, - 'original_name': 'Pressure', + 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': 'Home-max-pressure', - 'unit_of_measurement': , + 'unique_id': 'Home-max-windangle_value', + 'unit_of_measurement': '°', }) # --- -# name: test_entity[sensor.home_max_pressure-state] +# name: test_entity[sensor.home_max_none-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', - 'device_class': 'atmospheric_pressure', - 'friendly_name': 'Home max Pressure', + 'friendly_name': 'Home max None', 'latitude': 32.17901225, 'longitude': -117.17901225, 'state_class': , - 'unit_of_measurement': , + 'unit_of_measurement': '°', }), 'context': , - 'entity_id': 'sensor.home_max_pressure', + 'entity_id': 'sensor.home_max_none', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1014.4', + 'state': '17', }) # --- -# name: test_entity[sensor.home_max_rain-entry] +# name: test_entity[sensor.home_max_precipitation-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2355,7 +2365,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.home_max_rain', + 'entity_id': 'sensor.home_max_precipitation', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2367,7 +2377,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Rain', + 'original_name': 'Precipitation', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -2376,26 +2386,26 @@ 'unit_of_measurement': , }) # --- -# name: test_entity[sensor.home_max_rain-state] +# name: test_entity[sensor.home_max_precipitation-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'precipitation', - 'friendly_name': 'Home max Rain', + 'friendly_name': 'Home max Precipitation', 'latitude': 32.17901225, 'longitude': -117.17901225, 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.home_max_rain', + 'entity_id': 'sensor.home_max_precipitation', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '0.5', }) # --- -# name: test_entity[sensor.home_max_rain_last_hour-entry] +# name: test_entity[sensor.home_max_precipitation_last_hour-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2409,7 +2419,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.home_max_rain_last_hour', + 'entity_id': 'sensor.home_max_precipitation_last_hour', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2418,38 +2428,41 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Rain last hour', + 'original_name': 'Precipitation last hour', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'sum_rain_1', 'unique_id': 'Home-max-sum_rain_1', 'unit_of_measurement': , }) # --- -# name: test_entity[sensor.home_max_rain_last_hour-state] +# name: test_entity[sensor.home_max_precipitation_last_hour-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'precipitation', - 'friendly_name': 'Home max Rain last hour', + 'friendly_name': 'Home max Precipitation last hour', 'latitude': 32.17901225, 'longitude': -117.17901225, 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.home_max_rain_last_hour', + 'entity_id': 'sensor.home_max_precipitation_last_hour', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '0.2', }) # --- -# name: test_entity[sensor.home_max_rain_today-entry] +# name: test_entity[sensor.home_max_precipitation_today-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2463,7 +2476,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.home_max_rain_today', + 'entity_id': 'sensor.home_max_precipitation_today', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2475,28 +2488,28 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Rain today', + 'original_name': 'Precipitation today', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'sum_rain_24', 'unique_id': 'Home-max-sum_rain_24', 'unit_of_measurement': , }) # --- -# name: test_entity[sensor.home_max_rain_today-state] +# name: test_entity[sensor.home_max_precipitation_today-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'precipitation', - 'friendly_name': 'Home max Rain today', + 'friendly_name': 'Home max Precipitation today', 'latitude': 32.17901225, 'longitude': -117.17901225, 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.home_max_rain_today', + 'entity_id': 'sensor.home_max_precipitation_today', 'last_changed': , 'last_reported': , 'last_updated': , @@ -2526,6 +2539,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -2557,7 +2573,7 @@ 'state': '27.4', }) # --- -# name: test_entity[sensor.home_max_wind_strength-entry] +# name: test_entity[sensor.home_max_wind_speed-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2571,7 +2587,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.home_max_wind_strength', + 'entity_id': 'sensor.home_max_wind_speed', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2583,7 +2599,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Wind Strength', + 'original_name': 'Wind speed', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -2592,25 +2608,72 @@ 'unit_of_measurement': , }) # --- -# name: test_entity[sensor.home_max_wind_strength-state] +# name: test_entity[sensor.home_max_wind_speed-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'wind_speed', - 'friendly_name': 'Home max Wind Strength', + 'friendly_name': 'Home max Wind speed', 'latitude': 32.17901225, 'longitude': -117.17901225, 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.home_max_wind_strength', + 'entity_id': 'sensor.home_max_wind_speed', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '15', }) # --- +# name: test_entity[sensor.hot_water_none-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.hot_water_none', + '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': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12:34:56:00:16:0e#7-12:34:56:00:16:0e#7-reachable', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[sensor.hot_water_none-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Hot water None', + }), + 'context': , + 'entity_id': 'sensor.hot_water_none', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- # name: test_entity[sensor.hot_water_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -2663,55 +2726,7 @@ 'state': 'unavailable', }) # --- -# name: test_entity[sensor.hot_water_reachability-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.hot_water_reachability', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:signal', - 'original_name': 'Reachability', - 'platform': 'netatmo', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '12:34:56:00:16:0e#7-12:34:56:00:16:0e#7-reachable', - 'unit_of_measurement': None, - }) -# --- -# name: test_entity[sensor.hot_water_reachability-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by Netatmo', - 'friendly_name': 'Hot water Reachability', - 'icon': 'mdi:signal', - }), - 'context': , - 'entity_id': 'sensor.hot_water_reachability', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- -# name: test_entity[sensor.kitchen_co2-entry] +# name: test_entity[sensor.kitchen_atmospheric_pressure-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2725,7 +2740,67 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.kitchen_co2', + 'entity_id': 'sensor.kitchen_atmospheric_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': 'Atmospheric pressure', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pressure', + 'unique_id': '12:34:56:25:cf:a8-pressure', + 'unit_of_measurement': , + }) +# --- +# name: test_entity[sensor.kitchen_atmospheric_pressure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'atmospheric_pressure', + 'friendly_name': 'Kitchen Atmospheric pressure', + 'latitude': 13.377726, + 'longitude': 52.516263, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.kitchen_atmospheric_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entity[sensor.kitchen_carbon_dioxide-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.kitchen_carbon_dioxide', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2737,33 +2812,35 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'CO2', + 'original_name': 'Carbon dioxide', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'co2', 'unique_id': '12:34:56:25:cf:a8-co2', 'unit_of_measurement': 'ppm', }) # --- -# name: test_entity[sensor.kitchen_co2-state] +# name: test_entity[sensor.kitchen_carbon_dioxide-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'carbon_dioxide', - 'friendly_name': 'Kitchen CO2', + 'friendly_name': 'Kitchen Carbon dioxide', + 'latitude': 13.377726, + 'longitude': 52.516263, 'state_class': , 'unit_of_measurement': 'ppm', }), 'context': , - 'entity_id': 'sensor.kitchen_co2', + 'entity_id': 'sensor.kitchen_carbon_dioxide', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'unknown', }) # --- -# name: test_entity[sensor.kitchen_health-entry] +# name: test_entity[sensor.kitchen_health_index-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2775,7 +2852,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.kitchen_health', + 'entity_id': 'sensor.kitchen_health_index', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2786,29 +2863,30 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:cloud', - 'original_name': 'Health', + 'original_icon': None, + 'original_name': 'Health index', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'health_idx', 'unique_id': '12:34:56:25:cf:a8-health_idx', 'unit_of_measurement': None, }) # --- -# name: test_entity[sensor.kitchen_health-state] +# name: test_entity[sensor.kitchen_health_index-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', - 'friendly_name': 'Kitchen Health', - 'icon': 'mdi:cloud', + 'friendly_name': 'Kitchen Health index', + 'latitude': 13.377726, + 'longitude': 52.516263, }), 'context': , - 'entity_id': 'sensor.kitchen_health', + 'entity_id': 'sensor.kitchen_health_index', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'unknown', }) # --- # name: test_entity[sensor.kitchen_humidity-entry] @@ -2841,7 +2919,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'humidity', 'unique_id': '12:34:56:25:cf:a8-humidity', 'unit_of_measurement': '%', }) @@ -2852,6 +2930,8 @@ 'attribution': 'Data provided by Netatmo', 'device_class': 'humidity', 'friendly_name': 'Kitchen Humidity', + 'latitude': 13.377726, + 'longitude': 52.516263, 'state_class': , 'unit_of_measurement': '%', }), @@ -2860,7 +2940,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'unknown', }) # --- # name: test_entity[sensor.kitchen_noise-entry] @@ -2893,7 +2973,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'noise', 'unique_id': '12:34:56:25:cf:a8-noise', 'unit_of_measurement': , }) @@ -2904,6 +2984,8 @@ 'attribution': 'Data provided by Netatmo', 'device_class': 'sound_pressure', 'friendly_name': 'Kitchen Noise', + 'latitude': 13.377726, + 'longitude': 52.516263, 'state_class': , 'unit_of_measurement': , }), @@ -2912,62 +2994,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', - }) -# --- -# name: test_entity[sensor.kitchen_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.kitchen_pressure', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor.private': dict({ - 'suggested_unit_of_measurement': , - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Pressure', - 'platform': 'netatmo', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '12:34:56:25:cf:a8-pressure', - 'unit_of_measurement': , - }) -# --- -# name: test_entity[sensor.kitchen_pressure-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by Netatmo', - 'device_class': 'atmospheric_pressure', - 'friendly_name': 'Kitchen Pressure', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.kitchen_pressure', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', + 'state': 'unknown', }) # --- # name: test_entity[sensor.kitchen_pressure_trend-entry] @@ -2993,12 +3020,12 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:trending-up', + 'original_icon': None, 'original_name': 'Pressure trend', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'pressure_trend', 'unique_id': '12:34:56:25:cf:a8-pressure_trend', 'unit_of_measurement': None, }) @@ -3008,14 +3035,15 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'friendly_name': 'Kitchen Pressure trend', - 'icon': 'mdi:trending-up', + 'latitude': 13.377726, + 'longitude': 52.516263, }), 'context': , 'entity_id': 'sensor.kitchen_pressure_trend', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'unknown', }) # --- # name: test_entity[sensor.kitchen_reachability-entry] @@ -3041,12 +3069,12 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', + 'original_icon': None, 'original_name': 'Reachability', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'reachable', 'unique_id': '12:34:56:25:cf:a8-reachable', 'unit_of_measurement': None, }) @@ -3056,7 +3084,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'friendly_name': 'Kitchen Reachability', - 'icon': 'mdi:signal', 'latitude': 13.377726, 'longitude': 52.516263, }), @@ -3091,6 +3118,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -3098,7 +3128,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'temperature', 'unique_id': '12:34:56:25:cf:a8-temperature', 'unit_of_measurement': , }) @@ -3109,6 +3139,8 @@ 'attribution': 'Data provided by Netatmo', 'device_class': 'temperature', 'friendly_name': 'Kitchen Temperature', + 'latitude': 13.377726, + 'longitude': 52.516263, 'state_class': , 'unit_of_measurement': , }), @@ -3117,7 +3149,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'unknown', }) # --- # name: test_entity[sensor.kitchen_temperature_trend-entry] @@ -3143,12 +3175,12 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:trending-up', + 'original_icon': None, 'original_name': 'Temperature trend', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'temp_trend', 'unique_id': '12:34:56:25:cf:a8-temp_trend', 'unit_of_measurement': None, }) @@ -3158,17 +3190,18 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'friendly_name': 'Kitchen Temperature trend', - 'icon': 'mdi:trending-up', + 'latitude': 13.377726, + 'longitude': 52.516263, }), 'context': , 'entity_id': 'sensor.kitchen_temperature_trend', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'unknown', }) # --- -# name: test_entity[sensor.kitchen_wifi-entry] +# name: test_entity[sensor.kitchen_wi_fi-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3180,7 +3213,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.kitchen_wifi', + 'entity_id': 'sensor.kitchen_wi_fi', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -3191,33 +3224,79 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:wifi', - 'original_name': 'Wifi', + 'original_icon': None, + 'original_name': 'Wi-Fi', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'wifi_strength', 'unique_id': '12:34:56:25:cf:a8-wifi_status', 'unit_of_measurement': None, }) # --- -# name: test_entity[sensor.kitchen_wifi-state] +# name: test_entity[sensor.kitchen_wi_fi-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', - 'friendly_name': 'Kitchen Wifi', - 'icon': 'mdi:wifi', + 'friendly_name': 'Kitchen Wi-Fi', 'latitude': 13.377726, 'longitude': 52.516263, }), 'context': , - 'entity_id': 'sensor.kitchen_wifi', + 'entity_id': 'sensor.kitchen_wi_fi', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'Full', }) # --- +# name: test_entity[sensor.line_1_none-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.line_1_none', + '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': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12:34:56:00:16:0e#0-12:34:56:00:16:0e#0-reachable', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[sensor.line_1_none-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Line 1 None', + }), + 'context': , + 'entity_id': 'sensor.line_1_none', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- # name: test_entity[sensor.line_1_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -3270,7 +3349,7 @@ 'state': 'unavailable', }) # --- -# name: test_entity[sensor.line_1_reachability-entry] +# name: test_entity[sensor.line_2_none-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3282,7 +3361,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.line_1_reachability', + 'entity_id': 'sensor.line_2_none', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -3293,25 +3372,24 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', - 'original_name': 'Reachability', + 'original_icon': None, + 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '12:34:56:00:16:0e#0-12:34:56:00:16:0e#0-reachable', + 'unique_id': '12:34:56:00:16:0e#1-12:34:56:00:16:0e#1-reachable', 'unit_of_measurement': None, }) # --- -# name: test_entity[sensor.line_1_reachability-state] +# name: test_entity[sensor.line_2_none-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', - 'friendly_name': 'Line 1 Reachability', - 'icon': 'mdi:signal', + 'friendly_name': 'Line 2 None', }), 'context': , - 'entity_id': 'sensor.line_1_reachability', + 'entity_id': 'sensor.line_2_none', 'last_changed': , 'last_reported': , 'last_updated': , @@ -3370,7 +3448,7 @@ 'state': 'unavailable', }) # --- -# name: test_entity[sensor.line_2_reachability-entry] +# name: test_entity[sensor.line_3_none-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3382,7 +3460,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.line_2_reachability', + 'entity_id': 'sensor.line_3_none', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -3393,25 +3471,24 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', - 'original_name': 'Reachability', + 'original_icon': None, + 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '12:34:56:00:16:0e#1-12:34:56:00:16:0e#1-reachable', + 'unique_id': '12:34:56:00:16:0e#2-12:34:56:00:16:0e#2-reachable', 'unit_of_measurement': None, }) # --- -# name: test_entity[sensor.line_2_reachability-state] +# name: test_entity[sensor.line_3_none-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', - 'friendly_name': 'Line 2 Reachability', - 'icon': 'mdi:signal', + 'friendly_name': 'Line 3 None', }), 'context': , - 'entity_id': 'sensor.line_2_reachability', + 'entity_id': 'sensor.line_3_none', 'last_changed': , 'last_reported': , 'last_updated': , @@ -3470,7 +3547,7 @@ 'state': 'unavailable', }) # --- -# name: test_entity[sensor.line_3_reachability-entry] +# name: test_entity[sensor.line_4_none-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3482,7 +3559,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.line_3_reachability', + 'entity_id': 'sensor.line_4_none', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -3493,25 +3570,24 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', - 'original_name': 'Reachability', + 'original_icon': None, + 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '12:34:56:00:16:0e#2-12:34:56:00:16:0e#2-reachable', + 'unique_id': '12:34:56:00:16:0e#3-12:34:56:00:16:0e#3-reachable', 'unit_of_measurement': None, }) # --- -# name: test_entity[sensor.line_3_reachability-state] +# name: test_entity[sensor.line_4_none-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', - 'friendly_name': 'Line 3 Reachability', - 'icon': 'mdi:signal', + 'friendly_name': 'Line 4 None', }), 'context': , - 'entity_id': 'sensor.line_3_reachability', + 'entity_id': 'sensor.line_4_none', 'last_changed': , 'last_reported': , 'last_updated': , @@ -3570,7 +3646,7 @@ 'state': 'unavailable', }) # --- -# name: test_entity[sensor.line_4_reachability-entry] +# name: test_entity[sensor.line_5_none-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3582,7 +3658,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.line_4_reachability', + 'entity_id': 'sensor.line_5_none', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -3593,25 +3669,24 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', - 'original_name': 'Reachability', + 'original_icon': None, + 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '12:34:56:00:16:0e#3-12:34:56:00:16:0e#3-reachable', + 'unique_id': '12:34:56:00:16:0e#4-12:34:56:00:16:0e#4-reachable', 'unit_of_measurement': None, }) # --- -# name: test_entity[sensor.line_4_reachability-state] +# name: test_entity[sensor.line_5_none-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', - 'friendly_name': 'Line 4 Reachability', - 'icon': 'mdi:signal', + 'friendly_name': 'Line 5 None', }), 'context': , - 'entity_id': 'sensor.line_4_reachability', + 'entity_id': 'sensor.line_5_none', 'last_changed': , 'last_reported': , 'last_updated': , @@ -3670,19 +3745,21 @@ 'state': 'unavailable', }) # --- -# name: test_entity[sensor.line_5_reachability-entry] +# name: test_entity[sensor.livingroom_atmospheric_pressure-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.line_5_reachability', + 'entity_category': None, + 'entity_id': 'sensor.livingroom_atmospheric_pressure', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -3691,34 +3768,44 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), - 'original_device_class': None, - 'original_icon': 'mdi:signal', - 'original_name': 'Reachability', + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Atmospheric pressure', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, - 'unique_id': '12:34:56:00:16:0e#4-12:34:56:00:16:0e#4-reachable', - 'unit_of_measurement': None, + 'translation_key': 'pressure', + 'unique_id': '12:34:56:26:65:14-pressure', + 'unit_of_measurement': , }) # --- -# name: test_entity[sensor.line_5_reachability-state] +# name: test_entity[sensor.livingroom_atmospheric_pressure-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', - 'friendly_name': 'Line 5 Reachability', - 'icon': 'mdi:signal', + 'device_class': 'atmospheric_pressure', + 'friendly_name': 'Livingroom Atmospheric pressure', + 'latitude': 13.377726, + 'longitude': 52.516263, + 'state_class': , + 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.line_5_reachability', + 'entity_id': 'sensor.livingroom_atmospheric_pressure', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'unknown', }) # --- -# name: test_entity[sensor.livingroom_battery_percent-entry] +# name: test_entity[sensor.livingroom_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3732,7 +3819,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.livingroom_battery_percent', + 'entity_id': 'sensor.livingroom_battery', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -3744,7 +3831,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Battery Percent', + 'original_name': 'Battery', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -3753,24 +3840,24 @@ 'unit_of_measurement': '%', }) # --- -# name: test_entity[sensor.livingroom_battery_percent-state] +# name: test_entity[sensor.livingroom_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'battery', - 'friendly_name': 'Livingroom Battery Percent', + 'friendly_name': 'Livingroom Battery', 'state_class': , 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.livingroom_battery_percent', + 'entity_id': 'sensor.livingroom_battery', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '75', }) # --- -# name: test_entity[sensor.livingroom_co2-entry] +# name: test_entity[sensor.livingroom_carbon_dioxide-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3784,7 +3871,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.livingroom_co2', + 'entity_id': 'sensor.livingroom_carbon_dioxide', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -3796,33 +3883,35 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'CO2', + 'original_name': 'Carbon dioxide', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'co2', 'unique_id': '12:34:56:26:65:14-co2', 'unit_of_measurement': 'ppm', }) # --- -# name: test_entity[sensor.livingroom_co2-state] +# name: test_entity[sensor.livingroom_carbon_dioxide-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'carbon_dioxide', - 'friendly_name': 'Livingroom CO2', + 'friendly_name': 'Livingroom Carbon dioxide', + 'latitude': 13.377726, + 'longitude': 52.516263, 'state_class': , 'unit_of_measurement': 'ppm', }), 'context': , - 'entity_id': 'sensor.livingroom_co2', + 'entity_id': 'sensor.livingroom_carbon_dioxide', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'unknown', }) # --- -# name: test_entity[sensor.livingroom_health-entry] +# name: test_entity[sensor.livingroom_health_index-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3834,7 +3923,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.livingroom_health', + 'entity_id': 'sensor.livingroom_health_index', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -3845,29 +3934,30 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:cloud', - 'original_name': 'Health', + 'original_icon': None, + 'original_name': 'Health index', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'health_idx', 'unique_id': '12:34:56:26:65:14-health_idx', 'unit_of_measurement': None, }) # --- -# name: test_entity[sensor.livingroom_health-state] +# name: test_entity[sensor.livingroom_health_index-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', - 'friendly_name': 'Livingroom Health', - 'icon': 'mdi:cloud', + 'friendly_name': 'Livingroom Health index', + 'latitude': 13.377726, + 'longitude': 52.516263, }), 'context': , - 'entity_id': 'sensor.livingroom_health', + 'entity_id': 'sensor.livingroom_health_index', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'unknown', }) # --- # name: test_entity[sensor.livingroom_humidity-entry] @@ -3900,7 +3990,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'humidity', 'unique_id': '12:34:56:26:65:14-humidity', 'unit_of_measurement': '%', }) @@ -3911,6 +4001,8 @@ 'attribution': 'Data provided by Netatmo', 'device_class': 'humidity', 'friendly_name': 'Livingroom Humidity', + 'latitude': 13.377726, + 'longitude': 52.516263, 'state_class': , 'unit_of_measurement': '%', }), @@ -3919,7 +4011,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'unknown', }) # --- # name: test_entity[sensor.livingroom_noise-entry] @@ -3952,7 +4044,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'noise', 'unique_id': '12:34:56:26:65:14-noise', 'unit_of_measurement': , }) @@ -3963,6 +4055,8 @@ 'attribution': 'Data provided by Netatmo', 'device_class': 'sound_pressure', 'friendly_name': 'Livingroom Noise', + 'latitude': 13.377726, + 'longitude': 52.516263, 'state_class': , 'unit_of_measurement': , }), @@ -3971,62 +4065,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', - }) -# --- -# name: test_entity[sensor.livingroom_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.livingroom_pressure', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor.private': dict({ - 'suggested_unit_of_measurement': , - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Pressure', - 'platform': 'netatmo', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '12:34:56:26:65:14-pressure', - 'unit_of_measurement': , - }) -# --- -# name: test_entity[sensor.livingroom_pressure-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by Netatmo', - 'device_class': 'atmospheric_pressure', - 'friendly_name': 'Livingroom Pressure', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.livingroom_pressure', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', + 'state': 'unknown', }) # --- # name: test_entity[sensor.livingroom_pressure_trend-entry] @@ -4052,12 +4091,12 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:trending-up', + 'original_icon': None, 'original_name': 'Pressure trend', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'pressure_trend', 'unique_id': '12:34:56:26:65:14-pressure_trend', 'unit_of_measurement': None, }) @@ -4067,14 +4106,15 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'friendly_name': 'Livingroom Pressure trend', - 'icon': 'mdi:trending-up', + 'latitude': 13.377726, + 'longitude': 52.516263, }), 'context': , 'entity_id': 'sensor.livingroom_pressure_trend', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'unknown', }) # --- # name: test_entity[sensor.livingroom_reachability-entry] @@ -4100,12 +4140,12 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', + 'original_icon': None, 'original_name': 'Reachability', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'reachable', 'unique_id': '12:34:56:26:65:14-reachable', 'unit_of_measurement': None, }) @@ -4115,7 +4155,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'friendly_name': 'Livingroom Reachability', - 'icon': 'mdi:signal', 'latitude': 13.377726, 'longitude': 52.516263, }), @@ -4150,6 +4189,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -4157,7 +4199,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'temperature', 'unique_id': '12:34:56:26:65:14-temperature', 'unit_of_measurement': , }) @@ -4168,6 +4210,8 @@ 'attribution': 'Data provided by Netatmo', 'device_class': 'temperature', 'friendly_name': 'Livingroom Temperature', + 'latitude': 13.377726, + 'longitude': 52.516263, 'state_class': , 'unit_of_measurement': , }), @@ -4176,7 +4220,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'unknown', }) # --- # name: test_entity[sensor.livingroom_temperature_trend-entry] @@ -4202,12 +4246,12 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:trending-up', + 'original_icon': None, 'original_name': 'Temperature trend', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'temp_trend', 'unique_id': '12:34:56:26:65:14-temp_trend', 'unit_of_measurement': None, }) @@ -4217,17 +4261,18 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'friendly_name': 'Livingroom Temperature trend', - 'icon': 'mdi:trending-up', + 'latitude': 13.377726, + 'longitude': 52.516263, }), 'context': , 'entity_id': 'sensor.livingroom_temperature_trend', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'unknown', }) # --- -# name: test_entity[sensor.livingroom_wifi-entry] +# name: test_entity[sensor.livingroom_wi_fi-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -4239,7 +4284,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.livingroom_wifi', + 'entity_id': 'sensor.livingroom_wi_fi', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -4250,34 +4295,33 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:wifi', - 'original_name': 'Wifi', + 'original_icon': None, + 'original_name': 'Wi-Fi', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'wifi_strength', 'unique_id': '12:34:56:26:65:14-wifi_status', 'unit_of_measurement': None, }) # --- -# name: test_entity[sensor.livingroom_wifi-state] +# name: test_entity[sensor.livingroom_wi_fi-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', - 'friendly_name': 'Livingroom Wifi', - 'icon': 'mdi:wifi', + 'friendly_name': 'Livingroom Wi-Fi', 'latitude': 13.377726, 'longitude': 52.516263, }), 'context': , - 'entity_id': 'sensor.livingroom_wifi', + 'entity_id': 'sensor.livingroom_wi_fi', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'High', }) # --- -# name: test_entity[sensor.parents_bedroom_co2-entry] +# name: test_entity[sensor.parents_bedroom_atmospheric_pressure-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -4291,7 +4335,67 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.parents_bedroom_co2', + 'entity_id': 'sensor.parents_bedroom_atmospheric_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': 'Atmospheric pressure', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pressure', + 'unique_id': '12:34:56:3e:c5:46-pressure', + 'unit_of_measurement': , + }) +# --- +# name: test_entity[sensor.parents_bedroom_atmospheric_pressure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'atmospheric_pressure', + 'friendly_name': 'Parents Bedroom Atmospheric pressure', + 'latitude': 13.377726, + 'longitude': 52.516263, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.parents_bedroom_atmospheric_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1014.5', + }) +# --- +# name: test_entity[sensor.parents_bedroom_carbon_dioxide-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.parents_bedroom_carbon_dioxide', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -4303,35 +4407,35 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'CO2', + 'original_name': 'Carbon dioxide', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'co2', 'unique_id': '12:34:56:3e:c5:46-co2', 'unit_of_measurement': 'ppm', }) # --- -# name: test_entity[sensor.parents_bedroom_co2-state] +# name: test_entity[sensor.parents_bedroom_carbon_dioxide-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'carbon_dioxide', - 'friendly_name': 'Parents Bedroom CO2', + 'friendly_name': 'Parents Bedroom Carbon dioxide', 'latitude': 13.377726, 'longitude': 52.516263, 'state_class': , 'unit_of_measurement': 'ppm', }), 'context': , - 'entity_id': 'sensor.parents_bedroom_co2', + 'entity_id': 'sensor.parents_bedroom_carbon_dioxide', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '494', }) # --- -# name: test_entity[sensor.parents_bedroom_health-entry] +# name: test_entity[sensor.parents_bedroom_health_index-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -4343,7 +4447,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.parents_bedroom_health', + 'entity_id': 'sensor.parents_bedroom_health_index', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -4354,27 +4458,26 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:cloud', - 'original_name': 'Health', + 'original_icon': None, + 'original_name': 'Health index', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'health_idx', 'unique_id': '12:34:56:3e:c5:46-health_idx', 'unit_of_measurement': None, }) # --- -# name: test_entity[sensor.parents_bedroom_health-state] +# name: test_entity[sensor.parents_bedroom_health_index-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', - 'friendly_name': 'Parents Bedroom Health', - 'icon': 'mdi:cloud', + 'friendly_name': 'Parents Bedroom Health index', 'latitude': 13.377726, 'longitude': 52.516263, }), 'context': , - 'entity_id': 'sensor.parents_bedroom_health', + 'entity_id': 'sensor.parents_bedroom_health_index', 'last_changed': , 'last_reported': , 'last_updated': , @@ -4411,7 +4514,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'humidity', 'unique_id': '12:34:56:3e:c5:46-humidity', 'unit_of_measurement': '%', }) @@ -4465,7 +4568,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'noise', 'unique_id': '12:34:56:3e:c5:46-noise', 'unit_of_measurement': , }) @@ -4489,63 +4592,6 @@ 'state': '42', }) # --- -# name: test_entity[sensor.parents_bedroom_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.parents_bedroom_pressure', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor.private': dict({ - 'suggested_unit_of_measurement': , - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Pressure', - 'platform': 'netatmo', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '12:34:56:3e:c5:46-pressure', - 'unit_of_measurement': , - }) -# --- -# name: test_entity[sensor.parents_bedroom_pressure-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by Netatmo', - 'device_class': 'atmospheric_pressure', - 'friendly_name': 'Parents Bedroom Pressure', - 'latitude': 13.377726, - 'longitude': 52.516263, - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.parents_bedroom_pressure', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '1014.5', - }) -# --- # name: test_entity[sensor.parents_bedroom_pressure_trend-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -4569,12 +4615,12 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:trending-up', + 'original_icon': None, 'original_name': 'Pressure trend', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'pressure_trend', 'unique_id': '12:34:56:3e:c5:46-pressure_trend', 'unit_of_measurement': None, }) @@ -4584,14 +4630,15 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'friendly_name': 'Parents Bedroom Pressure trend', - 'icon': 'mdi:trending-up', + 'latitude': 13.377726, + 'longitude': 52.516263, }), 'context': , 'entity_id': 'sensor.parents_bedroom_pressure_trend', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'unknown', }) # --- # name: test_entity[sensor.parents_bedroom_reachability-entry] @@ -4617,12 +4664,12 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', + 'original_icon': None, 'original_name': 'Reachability', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'reachable', 'unique_id': '12:34:56:3e:c5:46-reachable', 'unit_of_measurement': None, }) @@ -4632,7 +4679,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'friendly_name': 'Parents Bedroom Reachability', - 'icon': 'mdi:signal', 'latitude': 13.377726, 'longitude': 52.516263, }), @@ -4667,6 +4713,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -4674,7 +4723,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'temperature', 'unique_id': '12:34:56:3e:c5:46-temperature', 'unit_of_measurement': , }) @@ -4721,12 +4770,12 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:trending-up', + 'original_icon': None, 'original_name': 'Temperature trend', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'temp_trend', 'unique_id': '12:34:56:3e:c5:46-temp_trend', 'unit_of_measurement': None, }) @@ -4736,17 +4785,18 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'friendly_name': 'Parents Bedroom Temperature trend', - 'icon': 'mdi:trending-up', + 'latitude': 13.377726, + 'longitude': 52.516263, }), 'context': , 'entity_id': 'sensor.parents_bedroom_temperature_trend', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'unknown', }) # --- -# name: test_entity[sensor.parents_bedroom_wifi-entry] +# name: test_entity[sensor.parents_bedroom_wi_fi-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -4758,7 +4808,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.parents_bedroom_wifi', + 'entity_id': 'sensor.parents_bedroom_wi_fi', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -4769,33 +4819,79 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:wifi', - 'original_name': 'Wifi', + 'original_icon': None, + 'original_name': 'Wi-Fi', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'wifi_strength', 'unique_id': '12:34:56:3e:c5:46-wifi_status', 'unit_of_measurement': None, }) # --- -# name: test_entity[sensor.parents_bedroom_wifi-state] +# name: test_entity[sensor.parents_bedroom_wi_fi-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', - 'friendly_name': 'Parents Bedroom Wifi', - 'icon': 'mdi:wifi', + 'friendly_name': 'Parents Bedroom Wi-Fi', 'latitude': 13.377726, 'longitude': 52.516263, }), 'context': , - 'entity_id': 'sensor.parents_bedroom_wifi', + 'entity_id': 'sensor.parents_bedroom_wi_fi', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'High', }) # --- +# name: test_entity[sensor.prise_none-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.prise_none', + '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': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12:34:56:80:00:12:ac:f2-12:34:56:80:00:12:ac:f2-reachable', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[sensor.prise_none-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Prise None', + }), + 'context': , + 'entity_id': 'sensor.prise_none', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'True', + }) +# --- # name: test_entity[sensor.prise_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -4848,7 +4944,7 @@ 'state': '0', }) # --- -# name: test_entity[sensor.prise_reachability-entry] +# name: test_entity[sensor.total_none-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -4860,7 +4956,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.prise_reachability', + 'entity_id': 'sensor.total_none', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -4871,29 +4967,28 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', - 'original_name': 'Reachability', + 'original_icon': None, + 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '12:34:56:80:00:12:ac:f2-12:34:56:80:00:12:ac:f2-reachable', + 'unique_id': '12:34:56:00:16:0e#5-12:34:56:00:16:0e#5-reachable', 'unit_of_measurement': None, }) # --- -# name: test_entity[sensor.prise_reachability-state] +# name: test_entity[sensor.total_none-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', - 'friendly_name': 'Prise Reachability', - 'icon': 'mdi:signal', + 'friendly_name': 'Total None', }), 'context': , - 'entity_id': 'sensor.prise_reachability', + 'entity_id': 'sensor.total_none', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'True', + 'state': 'unavailable', }) # --- # name: test_entity[sensor.total_power-entry] @@ -4948,55 +5043,7 @@ 'state': 'unavailable', }) # --- -# name: test_entity[sensor.total_reachability-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.total_reachability', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:signal', - 'original_name': 'Reachability', - 'platform': 'netatmo', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '12:34:56:00:16:0e#5-12:34:56:00:16:0e#5-reachable', - 'unit_of_measurement': None, - }) -# --- -# name: test_entity[sensor.total_reachability-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by Netatmo', - 'friendly_name': 'Total Reachability', - 'icon': 'mdi:signal', - }), - 'context': , - 'entity_id': 'sensor.total_reachability', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- -# name: test_entity[sensor.valve1_battery_percent-entry] +# name: test_entity[sensor.valve1_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -5010,7 +5057,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.valve1_battery_percent', + 'entity_id': 'sensor.valve1_battery', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -5022,7 +5069,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Battery Percent', + 'original_name': 'Battery', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -5031,24 +5078,24 @@ 'unit_of_measurement': '%', }) # --- -# name: test_entity[sensor.valve1_battery_percent-state] +# name: test_entity[sensor.valve1_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'battery', - 'friendly_name': 'Valve1 Battery Percent', + 'friendly_name': 'Valve1 Battery', 'state_class': , 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.valve1_battery_percent', + 'entity_id': 'sensor.valve1_battery', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '90', }) # --- -# name: test_entity[sensor.valve2_battery_percent-entry] +# name: test_entity[sensor.valve2_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -5062,7 +5109,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.valve2_battery_percent', + 'entity_id': 'sensor.valve2_battery', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -5074,7 +5121,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Battery Percent', + 'original_name': 'Battery', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, @@ -5083,76 +5130,24 @@ 'unit_of_measurement': '%', }) # --- -# name: test_entity[sensor.valve2_battery_percent-state] +# name: test_entity[sensor.valve2_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'battery', - 'friendly_name': 'Valve2 Battery Percent', + 'friendly_name': 'Valve2 Battery', 'state_class': , 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.valve2_battery_percent', + 'entity_id': 'sensor.valve2_battery', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '90', }) # --- -# name: test_entity[sensor.villa_bathroom_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': , - 'entity_id': 'sensor.villa_bathroom_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': 'Battery Percent', - 'platform': 'netatmo', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '12:34:56:80:7e:18-battery_percent', - 'unit_of_measurement': '%', - }) -# --- -# name: test_entity[sensor.villa_bathroom_battery_percent-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by Netatmo', - 'device_class': 'battery', - 'friendly_name': 'Villa Bathroom Battery Percent', - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.villa_bathroom_battery_percent', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '55', - }) -# --- -# name: test_entity[sensor.villa_bathroom_co2-entry] +# name: test_entity[sensor.villa_atmospheric_pressure-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -5166,7 +5161,119 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.villa_bathroom_co2', + 'entity_id': 'sensor.villa_atmospheric_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': 'Atmospheric pressure', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pressure', + 'unique_id': '12:34:56:80:bb:26-pressure', + 'unit_of_measurement': , + }) +# --- +# name: test_entity[sensor.villa_atmospheric_pressure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'atmospheric_pressure', + 'friendly_name': 'Villa Atmospheric pressure', + 'latitude': 46.123456, + 'longitude': 6.1234567, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.villa_atmospheric_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1026.8', + }) +# --- +# name: test_entity[sensor.villa_bathroom_battery-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.villa_bathroom_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': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'battery', + 'unique_id': '12:34:56:80:7e:18-battery_percent', + 'unit_of_measurement': '%', + }) +# --- +# name: test_entity[sensor.villa_bathroom_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'battery', + 'friendly_name': 'Villa Bathroom Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.villa_bathroom_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '55', + }) +# --- +# name: test_entity[sensor.villa_bathroom_carbon_dioxide-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.villa_bathroom_carbon_dioxide', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -5178,26 +5285,26 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'CO2', + 'original_name': 'Carbon dioxide', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'co2', 'unique_id': '12:34:56:80:7e:18-co2', 'unit_of_measurement': 'ppm', }) # --- -# name: test_entity[sensor.villa_bathroom_co2-state] +# name: test_entity[sensor.villa_bathroom_carbon_dioxide-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'carbon_dioxide', - 'friendly_name': 'Villa Bathroom CO2', + 'friendly_name': 'Villa Bathroom Carbon dioxide', 'state_class': , 'unit_of_measurement': 'ppm', }), 'context': , - 'entity_id': 'sensor.villa_bathroom_co2', + 'entity_id': 'sensor.villa_bathroom_carbon_dioxide', 'last_changed': , 'last_reported': , 'last_updated': , @@ -5234,7 +5341,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'humidity', 'unique_id': '12:34:56:80:7e:18-humidity', 'unit_of_measurement': '%', }) @@ -5279,12 +5386,12 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', + 'original_icon': None, 'original_name': 'Radio', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'rf_strength', 'unique_id': '12:34:56:80:7e:18-rf_status', 'unit_of_measurement': None, }) @@ -5294,7 +5401,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'friendly_name': 'Villa Bathroom Radio', - 'icon': 'mdi:signal', }), 'context': , 'entity_id': 'sensor.villa_bathroom_radio', @@ -5327,12 +5433,12 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', + 'original_icon': None, 'original_name': 'Reachability', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'reachable', 'unique_id': '12:34:56:80:7e:18-reachable', 'unit_of_measurement': None, }) @@ -5342,7 +5448,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'friendly_name': 'Villa Bathroom Reachability', - 'icon': 'mdi:signal', }), 'context': , 'entity_id': 'sensor.villa_bathroom_reachability', @@ -5375,6 +5480,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -5382,7 +5490,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'temperature', 'unique_id': '12:34:56:80:7e:18-temperature', 'unit_of_measurement': , }) @@ -5427,12 +5535,12 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:trending-up', + 'original_icon': None, 'original_name': 'Temperature trend', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'temp_trend', 'unique_id': '12:34:56:80:7e:18-temp_trend', 'unit_of_measurement': None, }) @@ -5442,7 +5550,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'friendly_name': 'Villa Bathroom Temperature trend', - 'icon': 'mdi:trending-up', }), 'context': , 'entity_id': 'sensor.villa_bathroom_temperature_trend', @@ -5452,7 +5559,7 @@ 'state': 'stable', }) # --- -# name: test_entity[sensor.villa_bedroom_battery_percent-entry] +# name: test_entity[sensor.villa_bedroom_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -5466,7 +5573,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.villa_bedroom_battery_percent', + 'entity_id': 'sensor.villa_bedroom_battery', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -5478,33 +5585,33 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Battery Percent', + 'original_name': 'Battery', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'battery', 'unique_id': '12:34:56:80:44:92-battery_percent', 'unit_of_measurement': '%', }) # --- -# name: test_entity[sensor.villa_bedroom_battery_percent-state] +# name: test_entity[sensor.villa_bedroom_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'battery', - 'friendly_name': 'Villa Bedroom Battery Percent', + 'friendly_name': 'Villa Bedroom Battery', 'state_class': , 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.villa_bedroom_battery_percent', + 'entity_id': 'sensor.villa_bedroom_battery', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '28', }) # --- -# name: test_entity[sensor.villa_bedroom_co2-entry] +# name: test_entity[sensor.villa_bedroom_carbon_dioxide-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -5518,7 +5625,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.villa_bedroom_co2', + 'entity_id': 'sensor.villa_bedroom_carbon_dioxide', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -5530,26 +5637,26 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'CO2', + 'original_name': 'Carbon dioxide', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'co2', 'unique_id': '12:34:56:80:44:92-co2', 'unit_of_measurement': 'ppm', }) # --- -# name: test_entity[sensor.villa_bedroom_co2-state] +# name: test_entity[sensor.villa_bedroom_carbon_dioxide-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'carbon_dioxide', - 'friendly_name': 'Villa Bedroom CO2', + 'friendly_name': 'Villa Bedroom Carbon dioxide', 'state_class': , 'unit_of_measurement': 'ppm', }), 'context': , - 'entity_id': 'sensor.villa_bedroom_co2', + 'entity_id': 'sensor.villa_bedroom_carbon_dioxide', 'last_changed': , 'last_reported': , 'last_updated': , @@ -5586,7 +5693,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'humidity', 'unique_id': '12:34:56:80:44:92-humidity', 'unit_of_measurement': '%', }) @@ -5631,12 +5738,12 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', + 'original_icon': None, 'original_name': 'Radio', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'rf_strength', 'unique_id': '12:34:56:80:44:92-rf_status', 'unit_of_measurement': None, }) @@ -5646,7 +5753,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'friendly_name': 'Villa Bedroom Radio', - 'icon': 'mdi:signal', }), 'context': , 'entity_id': 'sensor.villa_bedroom_radio', @@ -5679,12 +5785,12 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', + 'original_icon': None, 'original_name': 'Reachability', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'reachable', 'unique_id': '12:34:56:80:44:92-reachable', 'unit_of_measurement': None, }) @@ -5694,7 +5800,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'friendly_name': 'Villa Bedroom Reachability', - 'icon': 'mdi:signal', }), 'context': , 'entity_id': 'sensor.villa_bedroom_reachability', @@ -5727,6 +5832,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -5734,7 +5842,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'temperature', 'unique_id': '12:34:56:80:44:92-temperature', 'unit_of_measurement': , }) @@ -5779,12 +5887,12 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:trending-up', + 'original_icon': None, 'original_name': 'Temperature trend', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'temp_trend', 'unique_id': '12:34:56:80:44:92-temp_trend', 'unit_of_measurement': None, }) @@ -5794,7 +5902,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'friendly_name': 'Villa Bedroom Temperature trend', - 'icon': 'mdi:trending-up', }), 'context': , 'entity_id': 'sensor.villa_bedroom_temperature_trend', @@ -5804,7 +5911,7 @@ 'state': 'stable', }) # --- -# name: test_entity[sensor.villa_co2-entry] +# name: test_entity[sensor.villa_carbon_dioxide-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -5818,7 +5925,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.villa_co2', + 'entity_id': 'sensor.villa_carbon_dioxide', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -5830,87 +5937,35 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'CO2', + 'original_name': 'Carbon dioxide', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'co2', 'unique_id': '12:34:56:80:bb:26-co2', 'unit_of_measurement': 'ppm', }) # --- -# name: test_entity[sensor.villa_co2-state] +# name: test_entity[sensor.villa_carbon_dioxide-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'carbon_dioxide', - 'friendly_name': 'Villa CO2', + 'friendly_name': 'Villa Carbon dioxide', 'latitude': 46.123456, 'longitude': 6.1234567, 'state_class': , 'unit_of_measurement': 'ppm', }), 'context': , - 'entity_id': 'sensor.villa_co2', + 'entity_id': 'sensor.villa_carbon_dioxide', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '1339', }) # --- -# name: test_entity[sensor.villa_garden_angle-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.villa_garden_angle', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:compass-outline', - 'original_name': 'Angle', - 'platform': 'netatmo', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '12:34:56:03:1b:e4-windangle_value', - 'unit_of_measurement': '°', - }) -# --- -# name: test_entity[sensor.villa_garden_angle-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by Netatmo', - 'friendly_name': 'Villa Garden Angle', - 'icon': 'mdi:compass-outline', - 'state_class': , - 'unit_of_measurement': '°', - }), - 'context': , - 'entity_id': 'sensor.villa_garden_angle', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '217', - }) -# --- -# name: test_entity[sensor.villa_garden_battery_percent-entry] +# name: test_entity[sensor.villa_garden_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -5924,7 +5979,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.villa_garden_battery_percent', + 'entity_id': 'sensor.villa_garden_battery', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -5936,80 +5991,32 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Battery Percent', + 'original_name': 'Battery', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'battery', 'unique_id': '12:34:56:03:1b:e4-battery_percent', 'unit_of_measurement': '%', }) # --- -# name: test_entity[sensor.villa_garden_battery_percent-state] +# name: test_entity[sensor.villa_garden_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'battery', - 'friendly_name': 'Villa Garden Battery Percent', + 'friendly_name': 'Villa Garden Battery', 'state_class': , 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.villa_garden_battery_percent', + 'entity_id': 'sensor.villa_garden_battery', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '85', }) # --- -# name: test_entity[sensor.villa_garden_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.villa_garden_direction', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:compass-outline', - 'original_name': 'Direction', - 'platform': 'netatmo', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '12:34:56:03:1b:e4-windangle', - 'unit_of_measurement': None, - }) -# --- -# name: test_entity[sensor.villa_garden_direction-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by Netatmo', - 'friendly_name': 'Villa Garden Direction', - 'icon': 'mdi:compass-outline', - }), - 'context': , - 'entity_id': 'sensor.villa_garden_direction', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'SW', - }) -# --- # name: test_entity[sensor.villa_garden_gust_angle-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -6035,12 +6042,12 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:compass-outline', - 'original_name': 'Gust Angle', + 'original_icon': None, + 'original_name': 'Gust angle', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'gust_angle', 'unique_id': '12:34:56:03:1b:e4-gustangle_value', 'unit_of_measurement': '°', }) @@ -6049,8 +6056,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', - 'friendly_name': 'Villa Garden Gust Angle', - 'icon': 'mdi:compass-outline', + 'friendly_name': 'Villa Garden Gust angle', 'state_class': , 'unit_of_measurement': '°', }), @@ -6085,12 +6091,12 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:compass-outline', - 'original_name': 'Gust Direction', + 'original_icon': None, + 'original_name': 'Gust direction', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'gust_direction', 'unique_id': '12:34:56:03:1b:e4-gustangle', 'unit_of_measurement': None, }) @@ -6099,8 +6105,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', - 'friendly_name': 'Villa Garden Gust Direction', - 'icon': 'mdi:compass-outline', + 'friendly_name': 'Villa Garden Gust direction', }), 'context': , 'entity_id': 'sensor.villa_garden_gust_direction', @@ -6136,11 +6141,11 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Gust Strength', + 'original_name': 'Gust strength', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'gust_strength', 'unique_id': '12:34:56:03:1b:e4-guststrength', 'unit_of_measurement': , }) @@ -6150,7 +6155,7 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'wind_speed', - 'friendly_name': 'Villa Garden Gust Strength', + 'friendly_name': 'Villa Garden Gust strength', 'state_class': , 'unit_of_measurement': , }), @@ -6185,12 +6190,12 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', + 'original_icon': None, 'original_name': 'Radio', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'rf_strength', 'unique_id': '12:34:56:03:1b:e4-rf_status', 'unit_of_measurement': None, }) @@ -6200,7 +6205,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'friendly_name': 'Villa Garden Radio', - 'icon': 'mdi:signal', }), 'context': , 'entity_id': 'sensor.villa_garden_radio', @@ -6233,12 +6237,12 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', + 'original_icon': None, 'original_name': 'Reachability', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'reachable', 'unique_id': '12:34:56:03:1b:e4-reachable', 'unit_of_measurement': None, }) @@ -6248,7 +6252,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'friendly_name': 'Villa Garden Reachability', - 'icon': 'mdi:signal', }), 'context': , 'entity_id': 'sensor.villa_garden_reachability', @@ -6258,7 +6261,7 @@ 'state': 'True', }) # --- -# name: test_entity[sensor.villa_garden_wind_strength-entry] +# name: test_entity[sensor.villa_garden_wind_angle-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -6272,7 +6275,105 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.villa_garden_wind_strength', + 'entity_id': 'sensor.villa_garden_wind_angle', + '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': 'Wind angle', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_angle', + 'unique_id': '12:34:56:03:1b:e4-windangle_value', + 'unit_of_measurement': '°', + }) +# --- +# name: test_entity[sensor.villa_garden_wind_angle-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Villa Garden Wind angle', + 'state_class': , + 'unit_of_measurement': '°', + }), + 'context': , + 'entity_id': 'sensor.villa_garden_wind_angle', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '217', + }) +# --- +# name: test_entity[sensor.villa_garden_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.villa_garden_wind_direction', + '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': 'Wind direction', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_direction', + 'unique_id': '12:34:56:03:1b:e4-windangle', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[sensor.villa_garden_wind_direction-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Villa Garden Wind direction', + }), + 'context': , + 'entity_id': 'sensor.villa_garden_wind_direction', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'SW', + }) +# --- +# name: test_entity[sensor.villa_garden_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.villa_garden_wind_speed', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -6284,26 +6385,26 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Wind Strength', + 'original_name': 'Wind speed', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'wind_strength', 'unique_id': '12:34:56:03:1b:e4-windstrength', 'unit_of_measurement': , }) # --- -# name: test_entity[sensor.villa_garden_wind_strength-state] +# name: test_entity[sensor.villa_garden_wind_speed-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'wind_speed', - 'friendly_name': 'Villa Garden Wind Strength', + 'friendly_name': 'Villa Garden Wind speed', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.villa_garden_wind_strength', + 'entity_id': 'sensor.villa_garden_wind_speed', 'last_changed': , 'last_reported': , 'last_updated': , @@ -6340,7 +6441,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'humidity', 'unique_id': '12:34:56:80:bb:26-humidity', 'unit_of_measurement': '%', }) @@ -6394,7 +6495,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'noise', 'unique_id': '12:34:56:80:bb:26-noise', 'unit_of_measurement': , }) @@ -6418,7 +6519,7 @@ 'state': '35', }) # --- -# name: test_entity[sensor.villa_outdoor_battery_percent-entry] +# name: test_entity[sensor.villa_outdoor_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -6432,7 +6533,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.villa_outdoor_battery_percent', + 'entity_id': 'sensor.villa_outdoor_battery', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -6444,26 +6545,26 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Battery Percent', + 'original_name': 'Battery', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'battery', 'unique_id': '12:34:56:80:1c:42-battery_percent', 'unit_of_measurement': '%', }) # --- -# name: test_entity[sensor.villa_outdoor_battery_percent-state] +# name: test_entity[sensor.villa_outdoor_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'battery', - 'friendly_name': 'Villa Outdoor Battery Percent', + 'friendly_name': 'Villa Outdoor Battery', 'state_class': , 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.villa_outdoor_battery_percent', + 'entity_id': 'sensor.villa_outdoor_battery', 'last_changed': , 'last_reported': , 'last_updated': , @@ -6500,7 +6601,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'humidity', 'unique_id': '12:34:56:80:1c:42-humidity', 'unit_of_measurement': '%', }) @@ -6545,12 +6646,12 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', + 'original_icon': None, 'original_name': 'Radio', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'rf_strength', 'unique_id': '12:34:56:80:1c:42-rf_status', 'unit_of_measurement': None, }) @@ -6560,7 +6661,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'friendly_name': 'Villa Outdoor Radio', - 'icon': 'mdi:signal', }), 'context': , 'entity_id': 'sensor.villa_outdoor_radio', @@ -6593,12 +6693,12 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', + 'original_icon': None, 'original_name': 'Reachability', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'reachable', 'unique_id': '12:34:56:80:1c:42-reachable', 'unit_of_measurement': None, }) @@ -6608,7 +6708,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'friendly_name': 'Villa Outdoor Reachability', - 'icon': 'mdi:signal', }), 'context': , 'entity_id': 'sensor.villa_outdoor_reachability', @@ -6641,6 +6740,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -6648,7 +6750,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'temperature', 'unique_id': '12:34:56:80:1c:42-temperature', 'unit_of_measurement': , }) @@ -6693,12 +6795,12 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:trending-up', + 'original_icon': None, 'original_name': 'Temperature trend', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'temp_trend', 'unique_id': '12:34:56:80:1c:42-temp_trend', 'unit_of_measurement': None, }) @@ -6708,7 +6810,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'friendly_name': 'Villa Outdoor Temperature trend', - 'icon': 'mdi:trending-up', }), 'context': , 'entity_id': 'sensor.villa_outdoor_temperature_trend', @@ -6718,63 +6819,6 @@ 'state': 'unavailable', }) # --- -# name: test_entity[sensor.villa_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.villa_pressure', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor.private': dict({ - 'suggested_unit_of_measurement': , - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Pressure', - 'platform': 'netatmo', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '12:34:56:80:bb:26-pressure', - 'unit_of_measurement': , - }) -# --- -# name: test_entity[sensor.villa_pressure-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by Netatmo', - 'device_class': 'atmospheric_pressure', - 'friendly_name': 'Villa Pressure', - 'latitude': 46.123456, - 'longitude': 6.1234567, - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.villa_pressure', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '1026.8', - }) -# --- # name: test_entity[sensor.villa_pressure_trend-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -6798,12 +6842,12 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:trending-up', + 'original_icon': None, 'original_name': 'Pressure trend', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'pressure_trend', 'unique_id': '12:34:56:80:bb:26-pressure_trend', 'unit_of_measurement': None, }) @@ -6813,7 +6857,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'friendly_name': 'Villa Pressure trend', - 'icon': 'mdi:trending-up', 'latitude': 46.123456, 'longitude': 6.1234567, }), @@ -6825,7 +6868,7 @@ 'state': 'up', }) # --- -# name: test_entity[sensor.villa_rain_battery_percent-entry] +# name: test_entity[sensor.villa_rain_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -6839,7 +6882,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.villa_rain_battery_percent', + 'entity_id': 'sensor.villa_rain_battery', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -6851,32 +6894,191 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Battery Percent', + 'original_name': 'Battery', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'battery', 'unique_id': '12:34:56:80:c1:ea-battery_percent', 'unit_of_measurement': '%', }) # --- -# name: test_entity[sensor.villa_rain_battery_percent-state] +# name: test_entity[sensor.villa_rain_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'device_class': 'battery', - 'friendly_name': 'Villa Rain Battery Percent', + 'friendly_name': 'Villa Rain Battery', 'state_class': , 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.villa_rain_battery_percent', + 'entity_id': 'sensor.villa_rain_battery', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '21', }) # --- +# name: test_entity[sensor.villa_rain_precipitation-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.villa_rain_precipitation', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Precipitation', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'rain', + 'unique_id': '12:34:56:80:c1:ea-rain', + 'unit_of_measurement': , + }) +# --- +# name: test_entity[sensor.villa_rain_precipitation-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'precipitation', + 'friendly_name': 'Villa Rain Precipitation', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.villa_rain_precipitation', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.7', + }) +# --- +# name: test_entity[sensor.villa_rain_precipitation_last_hour-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.villa_rain_precipitation_last_hour', + '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': 'Precipitation last hour', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sum_rain_1', + 'unique_id': '12:34:56:80:c1:ea-sum_rain_1', + 'unit_of_measurement': , + }) +# --- +# name: test_entity[sensor.villa_rain_precipitation_last_hour-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'precipitation', + 'friendly_name': 'Villa Rain Precipitation last hour', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.villa_rain_precipitation_last_hour', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_entity[sensor.villa_rain_precipitation_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.villa_rain_precipitation_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': 'Precipitation today', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sum_rain_24', + 'unique_id': '12:34:56:80:c1:ea-sum_rain_24', + 'unit_of_measurement': , + }) +# --- +# name: test_entity[sensor.villa_rain_precipitation_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'precipitation', + 'friendly_name': 'Villa Rain Precipitation today', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.villa_rain_precipitation_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6.9', + }) +# --- # name: test_entity[sensor.villa_rain_radio-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -6900,12 +7102,12 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', + 'original_icon': None, 'original_name': 'Radio', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'rf_strength', 'unique_id': '12:34:56:80:c1:ea-rf_status', 'unit_of_measurement': None, }) @@ -6915,7 +7117,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'friendly_name': 'Villa Rain Radio', - 'icon': 'mdi:signal', }), 'context': , 'entity_id': 'sensor.villa_rain_radio', @@ -6925,162 +7126,6 @@ 'state': 'Medium', }) # --- -# name: test_entity[sensor.villa_rain_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.villa_rain_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': 'Rain', - 'platform': 'netatmo', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '12:34:56:80:c1:ea-rain', - 'unit_of_measurement': , - }) -# --- -# name: test_entity[sensor.villa_rain_rain-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by Netatmo', - 'device_class': 'precipitation', - 'friendly_name': 'Villa Rain Rain', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.villa_rain_rain', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '3.7', - }) -# --- -# name: test_entity[sensor.villa_rain_rain_last_hour-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.villa_rain_rain_last_hour', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Rain last hour', - 'platform': 'netatmo', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '12:34:56:80:c1:ea-sum_rain_1', - 'unit_of_measurement': , - }) -# --- -# name: test_entity[sensor.villa_rain_rain_last_hour-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by Netatmo', - 'device_class': 'precipitation', - 'friendly_name': 'Villa Rain Rain last hour', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.villa_rain_rain_last_hour', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0', - }) -# --- -# name: test_entity[sensor.villa_rain_rain_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.villa_rain_rain_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': 'Rain today', - 'platform': 'netatmo', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '12:34:56:80:c1:ea-sum_rain_24', - 'unit_of_measurement': , - }) -# --- -# name: test_entity[sensor.villa_rain_rain_today-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by Netatmo', - 'device_class': 'precipitation', - 'friendly_name': 'Villa Rain Rain today', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.villa_rain_rain_today', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '6.9', - }) -# --- # name: test_entity[sensor.villa_rain_reachability-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -7104,12 +7149,12 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', + 'original_icon': None, 'original_name': 'Reachability', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'reachable', 'unique_id': '12:34:56:80:c1:ea-reachable', 'unit_of_measurement': None, }) @@ -7119,7 +7164,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'friendly_name': 'Villa Rain Reachability', - 'icon': 'mdi:signal', }), 'context': , 'entity_id': 'sensor.villa_rain_reachability', @@ -7152,12 +7196,12 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:signal', + 'original_icon': None, 'original_name': 'Reachability', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'reachable', 'unique_id': '12:34:56:80:bb:26-reachable', 'unit_of_measurement': None, }) @@ -7167,7 +7211,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'friendly_name': 'Villa Reachability', - 'icon': 'mdi:signal', 'latitude': 46.123456, 'longitude': 6.1234567, }), @@ -7202,6 +7245,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -7209,7 +7255,7 @@ 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'temperature', 'unique_id': '12:34:56:80:bb:26-temperature', 'unit_of_measurement': , }) @@ -7256,12 +7302,12 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:trending-up', + 'original_icon': None, 'original_name': 'Temperature trend', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'temp_trend', 'unique_id': '12:34:56:80:bb:26-temp_trend', 'unit_of_measurement': None, }) @@ -7271,7 +7317,6 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'friendly_name': 'Villa Temperature trend', - 'icon': 'mdi:trending-up', 'latitude': 46.123456, 'longitude': 6.1234567, }), @@ -7283,7 +7328,7 @@ 'state': 'stable', }) # --- -# name: test_entity[sensor.villa_wifi-entry] +# name: test_entity[sensor.villa_wi_fi-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -7295,7 +7340,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.villa_wifi', + 'entity_id': 'sensor.villa_wi_fi', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -7306,27 +7351,26 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:wifi', - 'original_name': 'Wifi', + 'original_icon': None, + 'original_name': 'Wi-Fi', 'platform': 'netatmo', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'wifi_strength', 'unique_id': '12:34:56:80:bb:26-wifi_status', 'unit_of_measurement': None, }) # --- -# name: test_entity[sensor.villa_wifi-state] +# name: test_entity[sensor.villa_wi_fi-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', - 'friendly_name': 'Villa Wifi', - 'icon': 'mdi:wifi', + 'friendly_name': 'Villa Wi-Fi', 'latitude': 46.123456, 'longitude': 6.1234567, }), 'context': , - 'entity_id': 'sensor.villa_wifi', + 'entity_id': 'sensor.villa_wi_fi', 'last_changed': , 'last_reported': , 'last_updated': , diff --git a/tests/components/netatmo/test_sensor.py b/tests/components/netatmo/test_sensor.py index fa3ff41c3fb..d2cc20b8394 100644 --- a/tests/components/netatmo/test_sensor.py +++ b/tests/components/netatmo/test_sensor.py @@ -46,8 +46,8 @@ async def test_indoor_sensor( assert hass.states.get(f"{prefix}temperature").state == "20.3" assert hass.states.get(f"{prefix}humidity").state == "63" - assert hass.states.get(f"{prefix}co2").state == "494" - assert hass.states.get(f"{prefix}pressure").state == "1014.5" + assert hass.states.get(f"{prefix}carbon_dioxide").state == "494" + assert hass.states.get(f"{prefix}atmospheric_pressure").state == "1014.5" async def test_weather_sensor( @@ -79,13 +79,13 @@ async def test_public_weather_sensor( assert hass.states.get(f"{prefix}temperature").state == "27.4" assert hass.states.get(f"{prefix}humidity").state == "76" - assert hass.states.get(f"{prefix}pressure").state == "1014.4" + assert hass.states.get(f"{prefix}atmospheric_pressure").state == "1014.4" prefix = "sensor.home_avg_" assert hass.states.get(f"{prefix}temperature").state == "22.7" assert hass.states.get(f"{prefix}humidity").state == "63.2" - assert hass.states.get(f"{prefix}pressure").state == "1010.4" + assert hass.states.get(f"{prefix}atmospheric_pressure").state == "1010.4" entities_before_change = len(hass.states.async_all()) @@ -248,4 +248,4 @@ async def test_climate_battery_sensor( prefix = "sensor.livingroom_" - assert hass.states.get(f"{prefix}battery_percent").state == "75" + assert hass.states.get(f"{prefix}battery").state == "75" From 3efee10b9510856b3691e83ffe61c1f1b6a8869d Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Wed, 10 Apr 2024 08:55:59 +0200 Subject: [PATCH 467/967] Enable Ruff RUF013 (#115333) --- pyproject.toml | 1 + .../application_credentials/test_init.py | 6 ++++-- tests/components/bmw_connected_drive/__init__.py | 6 +++--- tests/components/bond/common.py | 10 +++++----- tests/components/command_line/__init__.py | 2 +- tests/components/device_tracker/common.py | 14 +++++++------- tests/components/discord/conftest.py | 2 +- tests/components/fan/common.py | 14 +++++++------- tests/components/fritzbox/__init__.py | 6 +++--- tests/components/google/conftest.py | 2 +- tests/components/google/test_calendar.py | 6 ++++-- tests/components/mqtt/test_humidifier.py | 4 ++-- tests/components/nest/common.py | 4 +++- tests/components/nest/test_camera.py | 4 ++-- tests/components/nest/test_config_flow.py | 2 +- tests/components/nut/util.py | 10 +++++----- tests/components/radarr/__init__.py | 2 +- tests/components/recorder/common.py | 2 +- tests/components/ruckus_unleashed/__init__.py | 2 +- tests/components/screenlogic/__init__.py | 2 +- tests/components/signal_messenger/conftest.py | 2 +- tests/components/smartthings/conftest.py | 2 +- tests/components/sql/__init__.py | 2 +- tests/components/tautulli/__init__.py | 2 +- tests/components/valve/test_init.py | 4 ++-- tests/components/vera/common.py | 4 ++-- tests/components/vera/test_sensor.py | 4 ++-- tests/components/version/common.py | 2 +- 28 files changed, 65 insertions(+), 58 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9ea03a63ff8..66c82d2e770 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -644,6 +644,7 @@ select = [ "RSE", # flake8-raise "RUF005", # Consider iterable unpacking instead of concatenation "RUF006", # Store a reference to the return value of asyncio.create_task + "RUF013", # PEP 484 prohibits implicit Optional # "RUF100", # Unused `noqa` directive; temporarily every now and then to clean them up "S102", # Use of exec detected "S103", # bad-file-permissions diff --git a/tests/components/application_credentials/test_init.py b/tests/components/application_credentials/test_init.py index af118c82279..523abc7fd84 100644 --- a/tests/components/application_credentials/test_init.py +++ b/tests/components/application_credentials/test_init.py @@ -189,7 +189,9 @@ class Client: self.client = client self.id = 0 - async def cmd(self, cmd: str, payload: dict[str, Any] = None) -> dict[str, Any]: + async def cmd( + self, cmd: str, payload: dict[str, Any] | None = None + ) -> dict[str, Any]: """Send a command and receive the json result.""" self.id += 1 await self.client.send_json( @@ -203,7 +205,7 @@ class Client: assert resp.get("id") == self.id return resp - async def cmd_result(self, cmd: str, payload: dict[str, Any] = None) -> Any: + async def cmd_result(self, cmd: str, payload: dict[str, Any] | None = None) -> Any: """Send a command and parse the result.""" resp = await self.cmd(cmd, payload) assert resp.get("success") diff --git a/tests/components/bmw_connected_drive/__init__.py b/tests/components/bmw_connected_drive/__init__.py index 84384e6b482..e737fce6897 100644 --- a/tests/components/bmw_connected_drive/__init__.py +++ b/tests/components/bmw_connected_drive/__init__.py @@ -54,9 +54,9 @@ async def setup_mocked_integration(hass: HomeAssistant) -> MockConfigEntry: def check_remote_service_call( router: respx.MockRouter, - remote_service: str = None, - remote_service_params: dict = None, - remote_service_payload: dict = None, + remote_service: str | None = None, + remote_service_params: dict | None = None, + remote_service_payload: dict | None = None, ): """Check if the last call was a successful remote service call.""" diff --git a/tests/components/bond/common.py b/tests/components/bond/common.py index 619aa03572a..0aff18e6ed1 100644 --- a/tests/components/bond/common.py +++ b/tests/components/bond/common.py @@ -83,11 +83,11 @@ async def setup_platform( discovered_device: dict[str, Any], *, bond_device_id: str = "bond-device-id", - bond_version: dict[str, Any] = None, - props: dict[str, Any] = None, - state: dict[str, Any] = None, - bridge: dict[str, Any] = None, - token: dict[str, Any] = None, + bond_version: dict[str, Any] | None = None, + props: dict[str, Any] | None = None, + state: dict[str, Any] | None = None, + bridge: dict[str, Any] | None = None, + token: dict[str, Any] | None = None, ): """Set up the specified Bond platform.""" mock_entry = MockConfigEntry( diff --git a/tests/components/command_line/__init__.py b/tests/components/command_line/__init__.py index 736ca68b43d..dc965234506 100644 --- a/tests/components/command_line/__init__.py +++ b/tests/components/command_line/__init__.py @@ -7,7 +7,7 @@ from unittest.mock import MagicMock, patch @contextmanager def mock_asyncio_subprocess_run( - response: bytes = b"", returncode: int = 0, exception: Exception = None + response: bytes = b"", returncode: int = 0, exception: Exception | None = None ): """Mock create_subprocess_shell.""" diff --git a/tests/components/device_tracker/common.py b/tests/components/device_tracker/common.py index eb88f9dfefc..a17556cfbaa 100644 --- a/tests/components/device_tracker/common.py +++ b/tests/components/device_tracker/common.py @@ -30,14 +30,14 @@ from tests.common import MockPlatform, mock_platform @bind_hass def async_see( hass: HomeAssistant, - mac: str = None, - dev_id: str = None, - host_name: str = None, - location_name: str = None, - gps: GPSType = None, + mac: str | None = None, + dev_id: str | None = None, + host_name: str | None = None, + location_name: str | None = None, + gps: GPSType | None = None, gps_accuracy=None, - battery: int = None, - attributes: dict = None, + battery: int | None = None, + attributes: dict | None = None, ): """Call service to notify you see device.""" data = { diff --git a/tests/components/discord/conftest.py b/tests/components/discord/conftest.py index 128869d0b80..c9c4e7c17d5 100644 --- a/tests/components/discord/conftest.py +++ b/tests/components/discord/conftest.py @@ -29,7 +29,7 @@ def discord_aiohttp_mock_factory( """Create Discord service mock from factory.""" def _discord_aiohttp_mock_factory( - headers: dict[str, str] = None, + headers: dict[str, str] | None = None, ) -> AiohttpClientMocker: if headers is not None: aioclient_mock.get( diff --git a/tests/components/fan/common.py b/tests/components/fan/common.py index a48e66c08f4..fbc7c7bb1bb 100644 --- a/tests/components/fan/common.py +++ b/tests/components/fan/common.py @@ -32,8 +32,8 @@ from tests.common import MockEntity async def async_turn_on( hass, entity_id=ENTITY_MATCH_ALL, - percentage: int = None, - preset_mode: str = None, + percentage: int | None = None, + preset_mode: str | None = None, ) -> None: """Turn all or specified fan on.""" data = { @@ -76,7 +76,7 @@ async def async_oscillate( async def async_set_preset_mode( - hass, entity_id=ENTITY_MATCH_ALL, preset_mode: str = None + hass, entity_id=ENTITY_MATCH_ALL, preset_mode: str | None = None ) -> None: """Set preset mode for all or specified fan.""" data = { @@ -90,7 +90,7 @@ async def async_set_preset_mode( async def async_set_percentage( - hass, entity_id=ENTITY_MATCH_ALL, percentage: int = None + hass, entity_id=ENTITY_MATCH_ALL, percentage: int | None = None ) -> None: """Set percentage for all or specified fan.""" data = { @@ -104,7 +104,7 @@ async def async_set_percentage( async def async_increase_speed( - hass, entity_id=ENTITY_MATCH_ALL, percentage_step: int = None + hass, entity_id=ENTITY_MATCH_ALL, percentage_step: int | None = None ) -> None: """Increase speed for all or specified fan.""" data = { @@ -121,7 +121,7 @@ async def async_increase_speed( async def async_decrease_speed( - hass, entity_id=ENTITY_MATCH_ALL, percentage_step: int = None + hass, entity_id=ENTITY_MATCH_ALL, percentage_step: int | None = None ) -> None: """Decrease speed for all or specified fan.""" data = { @@ -138,7 +138,7 @@ async def async_decrease_speed( async def async_set_direction( - hass, entity_id=ENTITY_MATCH_ALL, direction: str = None + hass, entity_id=ENTITY_MATCH_ALL, direction: str | None = None ) -> None: """Set direction for all or specified fan.""" data = { diff --git a/tests/components/fritzbox/__init__.py b/tests/components/fritzbox/__init__.py index 8d366e39f6d..5fb9c853bf5 100644 --- a/tests/components/fritzbox/__init__.py +++ b/tests/components/fritzbox/__init__.py @@ -23,9 +23,9 @@ async def setup_config_entry( hass: HomeAssistant, data: dict[str, Any], unique_id: str = "any", - device: Mock = None, - fritz: Mock = None, - template: Mock = None, + device: Mock | None = None, + fritz: Mock | None = None, + template: Mock | None = None, ) -> bool: """Do setup of a MockConfigEntry.""" entry = MockConfigEntry( diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index 37a652e3752..bd64a1d8a49 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -241,7 +241,7 @@ def mock_events_list( def _put_result( response: dict[str, Any], - calendar_id: str = None, + calendar_id: str | None = None, exc: ClientError | None = None, ) -> None: if calendar_id is None: diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index 3946e432497..cf138567ba9 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -79,7 +79,9 @@ class Client: self.client = client self.id = 0 - async def cmd(self, cmd: str, payload: dict[str, Any] = None) -> dict[str, Any]: + async def cmd( + self, cmd: str, payload: dict[str, Any] | None = None + ) -> dict[str, Any]: """Send a command and receive the json result.""" self.id += 1 await self.client.send_json( @@ -93,7 +95,7 @@ class Client: assert resp.get("id") == self.id return resp - async def cmd_result(self, cmd: str, payload: dict[str, Any] = None) -> Any: + async def cmd_result(self, cmd: str, payload: dict[str, Any] | None = None) -> Any: """Send a command and parse the result.""" resp = await self.cmd(cmd, payload) assert resp.get("success") diff --git a/tests/components/mqtt/test_humidifier.py b/tests/components/mqtt/test_humidifier.py index 75baca046bd..c29250bff82 100644 --- a/tests/components/mqtt/test_humidifier.py +++ b/tests/components/mqtt/test_humidifier.py @@ -101,7 +101,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 + hass: HomeAssistant, entity_id=ENTITY_MATCH_ALL, mode: str | None = None ) -> None: """Set mode for all or specified humidifier.""" data = { @@ -114,7 +114,7 @@ async def async_set_mode( async def async_set_humidity( - hass: HomeAssistant, entity_id=ENTITY_MATCH_ALL, humidity: int = None + hass: HomeAssistant, entity_id=ENTITY_MATCH_ALL, humidity: int | None = None ) -> None: """Set target humidity for all or specified humidifier.""" data = { diff --git a/tests/components/nest/common.py b/tests/components/nest/common.py index e2cd536725f..70bc88b003f 100644 --- a/tests/components/nest/common.py +++ b/tests/components/nest/common.py @@ -148,7 +148,9 @@ class CreateDevice: self.data = {"traits": {}} def create( - self, raw_traits: dict[str, Any] = None, raw_data: dict[str, Any] = None + self, + raw_traits: dict[str, Any] | None = None, + raw_data: dict[str, Any] | None = None, ) -> None: """Create a new device with the specifeid traits.""" data = copy.deepcopy(self.data) diff --git a/tests/components/nest/test_camera.py b/tests/components/nest/test_camera.py index b68173be201..33c611c9cfc 100644 --- a/tests/components/nest/test_camera.py +++ b/tests/components/nest/test_camera.py @@ -104,7 +104,7 @@ def webrtc_camera_device(create_device: CreateDevice) -> None: def make_motion_event( event_id: str = MOTION_EVENT_ID, event_session_id: str = EVENT_SESSION_ID, - timestamp: datetime.datetime = None, + timestamp: datetime.datetime | None = None, ) -> EventMessage: """Create an EventMessage for a motion event.""" if not timestamp: @@ -128,7 +128,7 @@ def make_motion_event( def make_stream_url_response( - expiration: datetime.datetime = None, token_num: int = 0 + expiration: datetime.datetime | None = None, token_num: int = 0 ) -> aiohttp.web.Response: """Make response for the API that generates a streaming url.""" if not expiration: diff --git a/tests/components/nest/test_config_flow.py b/tests/components/nest/test_config_flow.py index b7ca64db232..cef1f5e9a86 100644 --- a/tests/components/nest/test_config_flow.py +++ b/tests/components/nest/test_config_flow.py @@ -152,7 +152,7 @@ class OAuthFixture: ) async def async_finish_setup( - self, result: dict, user_input: dict = None + self, result: dict, user_input: dict | None = None ) -> ConfigEntry: """Finish the OAuth flow exchanging auth token for refresh token.""" with patch( diff --git a/tests/components/nut/util.py b/tests/components/nut/util.py index 3bc48764816..b6c9cffd390 100644 --- a/tests/components/nut/util.py +++ b/tests/components/nut/util.py @@ -33,14 +33,14 @@ def _get_mock_nutclient( async def async_init_integration( hass: HomeAssistant, - ups_fixture: str = None, + ups_fixture: str | None = None, username: str = "mock", password: str = "mock", - list_ups: dict[str, str] = None, - list_vars: dict[str, str] = None, - list_commands_return_value: dict[str, str] = None, + list_ups: dict[str, str] | None = None, + list_vars: dict[str, str] | None = None, + list_commands_return_value: dict[str, str] | None = None, list_commands_side_effect=None, - run_command: MagicMock = None, + run_command: MagicMock | None = None, ) -> MockConfigEntry: """Set up the nut integration in Home Assistant.""" diff --git a/tests/components/radarr/__init__.py b/tests/components/radarr/__init__.py index 0e6f708d329..a29b928b405 100644 --- a/tests/components/radarr/__init__.py +++ b/tests/components/radarr/__init__.py @@ -160,7 +160,7 @@ async def setup_integration( aioclient_mock: AiohttpClientMocker, url: str = URL, api_key: str = API_KEY, - unique_id: str = None, + unique_id: str | None = None, skip_entry_setup: bool = False, connection_error: bool = False, invalid_auth: bool = False, diff --git a/tests/components/recorder/common.py b/tests/components/recorder/common.py index e8fd6dbcf53..7a57b226d77 100644 --- a/tests/components/recorder/common.py +++ b/tests/components/recorder/common.py @@ -109,7 +109,7 @@ async def async_wait_recording_done(hass: HomeAssistant) -> None: await hass.async_block_till_done() -async def async_wait_purge_done(hass: HomeAssistant, max: int = None) -> None: +async def async_wait_purge_done(hass: HomeAssistant, max: int | None = None) -> None: """Wait for max number of purge events. Because a purge may insert another PurgeTask into diff --git a/tests/components/ruckus_unleashed/__init__.py b/tests/components/ruckus_unleashed/__init__.py index 97b554b1eb5..cf510b87314 100644 --- a/tests/components/ruckus_unleashed/__init__.py +++ b/tests/components/ruckus_unleashed/__init__.py @@ -111,7 +111,7 @@ class RuckusAjaxApiPatchContext: def __init__( self, - login_mock: AsyncMock = None, + login_mock: AsyncMock | None = None, system_info: dict | None = None, mesh_info: dict | None = None, active_clients: list[dict] | AsyncMock | None = None, diff --git a/tests/components/screenlogic/__init__.py b/tests/components/screenlogic/__init__.py index c9889e6b4b8..e562b84ad14 100644 --- a/tests/components/screenlogic/__init__.py +++ b/tests/components/screenlogic/__init__.py @@ -61,7 +61,7 @@ async def stub_async_connect( gtype=None, gsubtype=None, name=MOCK_ADAPTER_NAME, - connection_closed_callback: Callable = None, + connection_closed_callback: Callable | None = None, ) -> bool: """Initialize minimum attributes needed for tests.""" self._ip = ip diff --git a/tests/components/signal_messenger/conftest.py b/tests/components/signal_messenger/conftest.py index ecafff1ef4a..1c9c60c2878 100644 --- a/tests/components/signal_messenger/conftest.py +++ b/tests/components/signal_messenger/conftest.py @@ -32,7 +32,7 @@ def signal_requests_mock_factory(requests_mock: Mocker) -> Mocker: """Create signal service mock from factory.""" def _signal_requests_mock_factory( - success_send_result: bool = True, content_length_header: str = None + success_send_result: bool = True, content_length_header: str | None = None ) -> Mocker: requests_mock.register_uri( "GET", diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index f15ba85c07e..b6d34b9d98a 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -253,7 +253,7 @@ def device_factory_fixture(): api = Mock(Api) api.post_device_command.return_value = {"results": [{"status": "ACCEPTED"}]} - def _factory(label, capabilities, status: dict = None): + def _factory(label, capabilities, status: dict | None = None): device_data = { "deviceId": str(uuid4()), "name": "Device Type Handler Name", diff --git a/tests/components/sql/__init__.py b/tests/components/sql/__init__.py index fc122ad1a95..5f91cba1d94 100644 --- a/tests/components/sql/__init__.py +++ b/tests/components/sql/__init__.py @@ -262,7 +262,7 @@ YAML_CONFIG_ALL_TEMPLATES = { async def init_integration( hass: HomeAssistant, - config: dict[str, Any] = None, + config: dict[str, Any] | None = None, entry_id: str = "1", source: str = SOURCE_USER, ) -> MockConfigEntry: diff --git a/tests/components/tautulli/__init__.py b/tests/components/tautulli/__init__.py index b48488c7216..8ca0b13d198 100644 --- a/tests/components/tautulli/__init__.py +++ b/tests/components/tautulli/__init__.py @@ -72,7 +72,7 @@ async def setup_integration( aioclient_mock: AiohttpClientMocker, url: str = URL, api_key: str = API_KEY, - unique_id: str = None, + unique_id: str | None = None, skip_entry_setup: bool = False, invalid_auth: bool = False, ) -> MockConfigEntry: diff --git a/tests/components/valve/test_init.py b/tests/components/valve/test_init.py index 971e3d04f3e..eee215d2e29 100644 --- a/tests/components/valve/test_init.py +++ b/tests/components/valve/test_init.py @@ -54,7 +54,7 @@ class MockValveEntity(ValveEntity): unique_id: str = "mock_valve", name: str = "Valve", features: ValveEntityFeature = ValveEntityFeature(0), - current_position: int = None, + current_position: int | None = None, device_class: ValveDeviceClass = None, reports_position: bool = True, ) -> None: @@ -104,7 +104,7 @@ class MockBinaryValveEntity(ValveEntity): unique_id: str = "mock_valve_2", name: str = "Valve", features: ValveEntityFeature = ValveEntityFeature(0), - is_closed: bool = None, + is_closed: bool | None = None, ) -> None: """Initialize the valve.""" self._attr_name = name diff --git a/tests/components/vera/common.py b/tests/components/vera/common.py index b6be60927cf..af21bf5d3a3 100644 --- a/tests/components/vera/common.py +++ b/tests/components/vera/common.py @@ -58,8 +58,8 @@ class ControllerConfig(NamedTuple): def new_simple_controller_config( - config: dict = None, - options: dict = None, + config: dict | None = None, + options: dict | None = None, config_source=ConfigSource.CONFIG_FLOW, serial_number="1111", devices: tuple[pv.VeraDevice, ...] = (), diff --git a/tests/components/vera/test_sensor.py b/tests/components/vera/test_sensor.py index ebe8beb4e29..c31845b80af 100644 --- a/tests/components/vera/test_sensor.py +++ b/tests/components/vera/test_sensor.py @@ -20,8 +20,8 @@ async def run_sensor_test( category: int, class_property: str, assert_states: tuple[tuple[Any, Any]], - assert_unit_of_measurement: str = None, - setup_callback: Callable[[pv.VeraController], None] = None, + assert_unit_of_measurement: str | None = None, + setup_callback: Callable[[pv.VeraController], None] | None = None, ) -> None: """Test generic sensor.""" vera_device: pv.VeraSensor = MagicMock(spec=pv.VeraSensor) diff --git a/tests/components/version/common.py b/tests/components/version/common.py index c14ec2c4fbf..cd9469d08a1 100644 --- a/tests/components/version/common.py +++ b/tests/components/version/common.py @@ -43,7 +43,7 @@ async def mock_get_version_update( freezer: FrozenDateTimeFactory, version: str = MOCK_VERSION, data: dict[str, Any] = MOCK_VERSION_DATA, - side_effect: Exception = None, + side_effect: Exception | None = None, ) -> None: """Mock getting version.""" with patch( From 8aa3ea575fae4e8997322e3a371fefb0989cc78e Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 10 Apr 2024 08:56:08 +0200 Subject: [PATCH 468/967] Update pytest-xdist to 3.5.0 (#111266) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 553f44eeb25..f13e0e6a36b 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -27,7 +27,7 @@ pytest-sugar==1.0.0 pytest-timeout==2.3.1 pytest-unordered==0.6.0 pytest-picked==0.5.0 -pytest-xdist==3.4.0 +pytest-xdist==3.5.0 pytest==8.1.1 requests-mock==1.11.0 respx==0.21.0 From 012509f6839c8009cd2747c86f00e82ed6299165 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 10 Apr 2024 11:36:25 +0200 Subject: [PATCH 469/967] Add documentation link for custom integrations in diagnostics (#115336) * Add documentation link for custom integrations in diagnostics * Enable custom integrations in test_download_diagnostics --- .../components/diagnostics/__init__.py | 1 + tests/components/diagnostics/test_init.py | 140 +++++++++++++++++- 2 files changed, 138 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/diagnostics/__init__.py b/homeassistant/components/diagnostics/__init__.py index 5ada7713c33..6c70e0dc110 100644 --- a/homeassistant/components/diagnostics/__init__.py +++ b/homeassistant/components/diagnostics/__init__.py @@ -174,6 +174,7 @@ async def _async_get_json_file_response( all_custom_components = await async_get_custom_components(hass) for cc_domain, cc_obj in all_custom_components.items(): custom_components[cc_domain] = { + "documentation": cc_obj.documentation, "version": cc_obj.version, "requirements": cc_obj.requirements, } diff --git a/tests/components/diagnostics/test_init.py b/tests/components/diagnostics/test_init.py index 3303e51a5a5..dff71d9edbf 100644 --- a/tests/components/diagnostics/test_init.py +++ b/tests/components/diagnostics/test_init.py @@ -80,7 +80,9 @@ async def test_websocket( async def test_download_diagnostics( - hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + enable_custom_integrations: None, ) -> None: """Test download diagnostics.""" config_entry = MockConfigEntry(domain="fake_integration") @@ -91,7 +93,73 @@ async def test_download_diagnostics( assert await _get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "home_assistant": hass_sys_info, - "custom_components": {}, + "custom_components": { + "test": { + "documentation": "http://example.com", + "requirements": [], + "version": "1.2.3", + }, + "test_blocked_version": { + "documentation": None, + "requirements": [], + "version": "1.0.0", + }, + "test_embedded": { + "documentation": "http://test-package.io", + "requirements": [], + "version": "1.2.3", + }, + "test_integration_frame": { + "documentation": "http://example.com", + "requirements": [], + "version": "1.2.3", + }, + "test_integration_platform": { + "documentation": "http://test-package.io", + "requirements": [], + "version": "1.2.3", + }, + "test_legacy_state_translations": { + "documentation": "http://test-package.io", + "requirements": [], + "version": "1.2.3", + }, + "test_legacy_state_translations_bad_data": { + "documentation": "http://test-package.io", + "requirements": [], + "version": "1.2.3", + }, + "test_package": { + "documentation": "http://test-package.io", + "requirements": [], + "version": "1.2.3", + }, + "test_package_loaded_executor": { + "documentation": "http://test-package.io", + "requirements": [], + "version": "1.2.3", + }, + "test_package_loaded_loop": { + "documentation": "http://test-package.io", + "requirements": [], + "version": "1.2.3", + }, + "test_package_raises_cancelled_error": { + "documentation": "http://test-package.io", + "requirements": [], + "version": "1.2.3", + }, + "test_package_raises_cancelled_error_config_entry": { + "documentation": "http://test-package.io", + "requirements": [], + "version": "1.2.3", + }, + "test_with_services": { + "documentation": None, + "requirements": [], + "version": "1.0", + }, + }, "integration_manifest": { "codeowners": [], "dependencies": [], @@ -112,7 +180,73 @@ async def test_download_diagnostics( hass, hass_client, config_entry, device ) == { "home_assistant": hass_sys_info, - "custom_components": {}, + "custom_components": { + "test": { + "documentation": "http://example.com", + "requirements": [], + "version": "1.2.3", + }, + "test_blocked_version": { + "documentation": None, + "requirements": [], + "version": "1.0.0", + }, + "test_embedded": { + "documentation": "http://test-package.io", + "requirements": [], + "version": "1.2.3", + }, + "test_integration_frame": { + "documentation": "http://example.com", + "requirements": [], + "version": "1.2.3", + }, + "test_integration_platform": { + "documentation": "http://test-package.io", + "requirements": [], + "version": "1.2.3", + }, + "test_legacy_state_translations": { + "documentation": "http://test-package.io", + "requirements": [], + "version": "1.2.3", + }, + "test_legacy_state_translations_bad_data": { + "documentation": "http://test-package.io", + "requirements": [], + "version": "1.2.3", + }, + "test_package": { + "documentation": "http://test-package.io", + "requirements": [], + "version": "1.2.3", + }, + "test_package_loaded_executor": { + "documentation": "http://test-package.io", + "requirements": [], + "version": "1.2.3", + }, + "test_package_loaded_loop": { + "documentation": "http://test-package.io", + "requirements": [], + "version": "1.2.3", + }, + "test_package_raises_cancelled_error": { + "documentation": "http://test-package.io", + "requirements": [], + "version": "1.2.3", + }, + "test_package_raises_cancelled_error_config_entry": { + "documentation": "http://test-package.io", + "requirements": [], + "version": "1.2.3", + }, + "test_with_services": { + "documentation": None, + "requirements": [], + "version": "1.0", + }, + }, "integration_manifest": { "codeowners": [], "dependencies": [], From a6b93ea8ac6b2195626b4c926dfd358d95c27bd5 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Wed, 10 Apr 2024 14:58:35 +0300 Subject: [PATCH 470/967] Fix Aranet failure when the Bluetooth proxy is not providing a device name (#115298) Co-authored-by: J. Nick Koston --- .../components/aranet/config_flow.py | 20 +++++++++---------- homeassistant/components/aranet/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/aranet/__init__.py | 8 ++++++++ tests/components/aranet/test_config_flow.py | 20 +++++++++++++++++++ 6 files changed, 40 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/aranet/config_flow.py b/homeassistant/components/aranet/config_flow.py index cf5f24263dd..db89124c54d 100644 --- a/homeassistant/components/aranet/config_flow.py +++ b/homeassistant/components/aranet/config_flow.py @@ -2,10 +2,10 @@ from __future__ import annotations -import logging from typing import Any from aranet4.client import Aranet4Advertisement, Version as AranetVersion +from bluetooth_data_tools import human_readable_name import voluptuous as vol from homeassistant.components.bluetooth import ( @@ -18,11 +18,15 @@ from homeassistant.data_entry_flow import AbortFlow from .const import DOMAIN -_LOGGER = logging.getLogger(__name__) - MIN_VERSION = AranetVersion(1, 2, 0) +def _title(discovery_info: BluetoothServiceInfoBleak) -> str: + return discovery_info.device.name or human_readable_name( + None, "Aranet", discovery_info.address + ) + + class AranetConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Aranet.""" @@ -61,11 +65,8 @@ class AranetConfigFlow(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Confirm discovery.""" - assert self._discovered_device is not None - adv = self._discovered_device assert self._discovery_info is not None - discovery_info = self._discovery_info - title = adv.readings.name if adv.readings else discovery_info.name + title = _title(self._discovery_info) if user_input is not None: return self.async_create_entry(title=title, data={}) @@ -101,10 +102,7 @@ class AranetConfigFlow(ConfigFlow, domain=DOMAIN): discovery_info.device, discovery_info.advertisement ) if adv.manufacturer_data: - self._discovered_devices[address] = ( - adv.readings.name if adv.readings else discovery_info.name, - adv, - ) + self._discovered_devices[address] = (_title(discovery_info), adv) if not self._discovered_devices: return self.async_abort(reason="no_devices_found") diff --git a/homeassistant/components/aranet/manifest.json b/homeassistant/components/aranet/manifest.json index 0d22a0d1859..152c56e80f3 100644 --- a/homeassistant/components/aranet/manifest.json +++ b/homeassistant/components/aranet/manifest.json @@ -19,5 +19,5 @@ "documentation": "https://www.home-assistant.io/integrations/aranet", "integration_type": "device", "iot_class": "local_push", - "requirements": ["aranet4==2.2.2"] + "requirements": ["aranet4==2.3.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 26a3c6c82aa..773df97bfba 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -467,7 +467,7 @@ aprslib==0.7.2 aqualogic==2.6 # homeassistant.components.aranet -aranet4==2.2.2 +aranet4==2.3.3 # homeassistant.components.arcam_fmj arcam-fmj==1.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b3179bb9806..9428dcd42ca 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -428,7 +428,7 @@ apprise==1.7.4 aprslib==0.7.2 # homeassistant.components.aranet -aranet4==2.2.2 +aranet4==2.3.3 # homeassistant.components.arcam_fmj arcam-fmj==1.4.0 diff --git a/tests/components/aranet/__init__.py b/tests/components/aranet/__init__.py index b559743067d..4dc9434bd65 100644 --- a/tests/components/aranet/__init__.py +++ b/tests/components/aranet/__init__.py @@ -58,6 +58,14 @@ VALID_DATA_SERVICE_INFO = fake_service_info( }, ) +VALID_DATA_SERVICE_INFO_WITH_NO_NAME = fake_service_info( + None, + "0000fce0-0000-1000-8000-00805f9b34fb", + { + 1794: b'\x21\x00\x02\x01\x00\x00\x00\x01\x8a\x02\xa5\x01\xb1&"Y\x01,\x01\xe8\x00\x88' + }, +) + VALID_ARANET2_DATA_SERVICE_INFO = fake_service_info( "Aranet2 12345", "0000fce0-0000-1000-8000-00805f9b34fb", diff --git a/tests/components/aranet/test_config_flow.py b/tests/components/aranet/test_config_flow.py index d278e98be0c..9596507960b 100644 --- a/tests/components/aranet/test_config_flow.py +++ b/tests/components/aranet/test_config_flow.py @@ -12,6 +12,7 @@ from . import ( NOT_ARANET4_SERVICE_INFO, OLD_FIRMWARE_SERVICE_INFO, VALID_DATA_SERVICE_INFO, + VALID_DATA_SERVICE_INFO_WITH_NO_NAME, ) from tests.common import MockConfigEntry @@ -36,6 +37,25 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" +async def test_async_step_bluetooth_device_without_name(hass: HomeAssistant) -> None: + """Test discovery via bluetooth with a valid device that has no name.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=VALID_DATA_SERVICE_INFO_WITH_NO_NAME, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + with patch("homeassistant.components.aranet.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["title"] == "Aranet (EEFF)" + assert result2["data"] == {} + assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + async def test_async_step_bluetooth_not_aranet4(hass: HomeAssistant) -> None: """Test that we reject discovery via Bluetooth for an unrelated device.""" result = await hass.config_entries.flow.async_init( From f80894d56f5e8213dc571bdab4b8aaccfc862c5c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 10 Apr 2024 02:41:13 -1000 Subject: [PATCH 471/967] Stop scripts with eager tasks (#115340) --- homeassistant/helpers/script.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 3c364ed8892..ea5cc3e571a 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -1250,7 +1250,7 @@ async def _async_stop_scripts_after_shutdown( _LOGGER.warning("Stopping scripts running too long after shutdown: %s", names) await asyncio.gather( *( - script["instance"].async_stop(update_state=False) + create_eager_task(script["instance"].async_stop(update_state=False)) for script in running_scripts ) ) @@ -1269,7 +1269,10 @@ async def _async_stop_scripts_at_shutdown(hass: HomeAssistant, event: Event) -> names = ", ".join([script["instance"].name for script in running_scripts]) _LOGGER.debug("Stopping scripts running at shutdown: %s", names) await asyncio.gather( - *(script["instance"].async_stop() for script in running_scripts) + *( + create_eager_task(script["instance"].async_stop()) + for script in running_scripts + ) ) @@ -1695,6 +1698,9 @@ class Script: # return false after the other script runs were stopped until our task # resumes running. self._log("Restarting") + # Important: yield to the event loop to allow the script to start in case + # the script is restarting itself. + await asyncio.sleep(0) await self.async_stop(update_state=False, spare=run) if started_action: @@ -1724,11 +1730,13 @@ class Script: # asyncio.shield as asyncio.shield yields to the event loop, which would cause # us to wait for script runs added after the call to async_stop. aws = [ - asyncio.create_task(run.async_stop()) for run in self._runs if run != spare + create_eager_task(run.async_stop()) for run in self._runs if run != spare ] if not aws: return - await asyncio.shield(self._async_stop(aws, update_state, spare)) + await asyncio.shield( + create_eager_task(self._async_stop(aws, update_state, spare)) + ) async def _async_get_condition(self, config): if isinstance(config, template.Template): From 63545ceaa46140e487a6c1b4da4f3d1c740e3857 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 10 Apr 2024 02:42:18 -1000 Subject: [PATCH 472/967] Ensure automations do not execute from a trigger if they are disabled (#115305) * Ensure automations are stopped as soon as the stop future is set * revert script changes and move them to #115325 --- .../components/automation/__init__.py | 18 ++++- tests/components/automation/test_init.py | 80 +++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 785d5849d74..299be2c82f9 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -795,6 +795,22 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity): """Log helper callback.""" self._logger.log(level, "%s %s", msg, self.name, **kwargs) + async def _async_trigger_if_enabled( + self, + run_variables: dict[str, Any], + context: Context | None = None, + skip_condition: bool = False, + ) -> ScriptRunResult | None: + """Trigger automation if enabled. + + If the trigger starts but has a delay, the automation will be triggered + when the delay has passed so we need to make sure its still enabled before + executing the action. + """ + if not self._is_enabled: + return None + return await self.async_trigger(run_variables, context, skip_condition) + async def _async_attach_triggers( self, home_assistant_start: bool ) -> Callable[[], None] | None: @@ -818,7 +834,7 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity): return await async_initialize_triggers( self.hass, self._trigger_config, - self.async_trigger, + self._async_trigger_if_enabled, DOMAIN, str(self.name), self._log_callback, diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 7805f3ea151..5b3fc2a723e 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -2651,3 +2651,83 @@ def test_deprecated_constants( import_and_test_deprecated_constant( caplog, automation, constant_name, replacement.__name__, replacement, "2025.1" ) + + +async def test_automation_turns_off_other_automation( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test an automation that turns off another automation.""" + hass.set_state(CoreState.not_running) + calls = async_mock_service(hass, "persistent_notification", "create") + hass.states.async_set("binary_sensor.presence", "on") + await hass.async_block_till_done() + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "state", + "entity_id": "binary_sensor.presence", + "from": "on", + }, + "action": { + "service": "automation.turn_off", + "target": { + "entity_id": "automation.automation_1", + }, + "data": { + "stop_actions": True, + }, + }, + "id": "automation_0", + "mode": "single", + }, + { + "trigger": { + "platform": "state", + "entity_id": "binary_sensor.presence", + "from": "on", + "for": { + "hours": 0, + "minutes": 0, + "seconds": 5, + }, + }, + "action": { + "service": "persistent_notification.create", + "metadata": {}, + "data": { + "message": "Test race", + }, + }, + "id": "automation_1", + "mode": "single", + }, + ] + }, + ) + await hass.async_start() + await hass.async_block_till_done() + + hass.states.async_set("binary_sensor.presence", "off") + await hass.async_block_till_done() + assert len(calls) == 0 + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=5)) + await hass.async_block_till_done() + assert len(calls) == 0 + + await hass.services.async_call( + "automation", + "turn_on", + {"entity_id": "automation.automation_1"}, + blocking=True, + ) + hass.states.async_set("binary_sensor.presence", "off") + await hass.async_block_till_done() + assert len(calls) == 0 + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=5)) + await hass.async_block_till_done() + assert len(calls) == 0 From b99cdf3144f04cdb3b8b9921071921c3342520a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Wed, 10 Apr 2024 17:52:08 +0200 Subject: [PATCH 473/967] Add missing oauth2 error strings to myuplink (#115315) Add some oauth2 error strings Co-authored-by: J. Nick Koston --- homeassistant/components/myuplink/strings.json | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/myuplink/strings.json b/homeassistant/components/myuplink/strings.json index f01bb1990cc..2efc0d05b34 100644 --- a/homeassistant/components/myuplink/strings.json +++ b/homeassistant/components/myuplink/strings.json @@ -12,12 +12,15 @@ "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%]", - "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", + "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "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%]" + "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%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]" }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" From 8d6473061ca160b34e4bc8404c6a839c023f727b Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 10 Apr 2024 21:39:53 +0200 Subject: [PATCH 474/967] Solve modbus test problem (#115376) Fix test. --- tests/components/modbus/conftest.py | 12 +++++++- .../modbus/fixtures/configuration.yaml | 4 +++ tests/components/modbus/test_init.py | 28 ++++++++++--------- 3 files changed, 30 insertions(+), 14 deletions(-) diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index f6eff0fd64b..62cf12958d3 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -52,6 +52,15 @@ def mock_pymodbus_fixture(): """Mock pymodbus.""" mock_pb = mock.AsyncMock() mock_pb.close = mock.MagicMock() + read_result = ReadResult([]) + mock_pb.read_coils.return_value = read_result + mock_pb.read_discrete_inputs.return_value = read_result + mock_pb.read_input_registers.return_value = read_result + mock_pb.read_holding_registers.return_value = read_result + mock_pb.write_register.return_value = read_result + mock_pb.write_registers.return_value = read_result + mock_pb.write_coil.return_value = read_result + mock_pb.write_coils.return_value = read_result with ( mock.patch( "homeassistant.components.modbus.modbus.AsyncModbusTcpClient", @@ -156,7 +165,7 @@ async def mock_pymodbus_exception_fixture(hass, do_exception, mock_modbus): @pytest.fixture(name="mock_pymodbus_return") async def mock_pymodbus_return_fixture(hass, register_words, mock_modbus): """Trigger update call with time_changed event.""" - read_result = ReadResult(register_words) if register_words else None + read_result = ReadResult(register_words if register_words else []) mock_modbus.read_coils.return_value = read_result mock_modbus.read_discrete_inputs.return_value = read_result mock_modbus.read_input_registers.return_value = read_result @@ -165,6 +174,7 @@ async def mock_pymodbus_return_fixture(hass, register_words, mock_modbus): mock_modbus.write_registers.return_value = read_result mock_modbus.write_coil.return_value = read_result mock_modbus.write_coils.return_value = read_result + return mock_modbus @pytest.fixture(name="mock_do_cycle") diff --git a/tests/components/modbus/fixtures/configuration.yaml b/tests/components/modbus/fixtures/configuration.yaml index 0f12ac88686..0a16d85e39d 100644 --- a/tests/components/modbus/fixtures/configuration.yaml +++ b/tests/components/modbus/fixtures/configuration.yaml @@ -3,3 +3,7 @@ modbus: host: "testHost" port: 5001 name: "testModbus" + sensors: + - name: "dummy" + address: 117 + slave: 0 diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 922022741b0..f0dfd5357e7 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -1645,7 +1645,7 @@ async def test_shutdown( ], ) async def test_stop_restart( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_modbus + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_pymodbus_return ) -> None: """Run test for service stop.""" @@ -1656,7 +1656,7 @@ async def test_stop_restart( await hass.async_block_till_done() assert hass.states.get(entity_id).state == "17" - mock_modbus.reset_mock() + mock_pymodbus_return.reset_mock() caplog.clear() data = { ATTR_HUB: TEST_MODBUS_NAME, @@ -1664,23 +1664,23 @@ async def test_stop_restart( await hass.services.async_call(DOMAIN, SERVICE_STOP, data, blocking=True) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - assert mock_modbus.close.called + assert mock_pymodbus_return.close.called assert f"modbus {TEST_MODBUS_NAME} communication closed" in caplog.text - mock_modbus.reset_mock() + mock_pymodbus_return.reset_mock() caplog.clear() await hass.services.async_call(DOMAIN, SERVICE_RESTART, data, blocking=True) await hass.async_block_till_done() - assert not mock_modbus.close.called - assert mock_modbus.connect.called + assert not mock_pymodbus_return.close.called + assert mock_pymodbus_return.connect.called assert f"modbus {TEST_MODBUS_NAME} communication open" in caplog.text - mock_modbus.reset_mock() + mock_pymodbus_return.reset_mock() caplog.clear() await hass.services.async_call(DOMAIN, SERVICE_RESTART, data, blocking=True) await hass.async_block_till_done() - assert mock_modbus.close.called - assert mock_modbus.connect.called + assert mock_pymodbus_return.close.called + assert mock_pymodbus_return.connect.called assert f"modbus {TEST_MODBUS_NAME} communication closed" in caplog.text assert f"modbus {TEST_MODBUS_NAME} communication open" in caplog.text @@ -1710,7 +1710,7 @@ async def test_write_no_client(hass: HomeAssistant, mock_modbus) -> None: async def test_integration_reload( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - mock_modbus, + mock_pymodbus_return, freezer: FrozenDateTimeFactory, ) -> None: """Run test for integration reload.""" @@ -1731,7 +1731,7 @@ async def test_integration_reload( @pytest.mark.parametrize("do_config", [{}]) async def test_integration_reload_failed( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_modbus + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_pymodbus_return ) -> None: """Run test for integration connect failure on reload.""" caplog.set_level(logging.INFO) @@ -1740,7 +1740,9 @@ async def test_integration_reload_failed( yaml_path = get_fixture_path("configuration.yaml", "modbus") with ( mock.patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path), - mock.patch.object(mock_modbus, "connect", side_effect=ModbusException("error")), + mock.patch.object( + mock_pymodbus_return, "connect", side_effect=ModbusException("error") + ), ): await hass.services.async_call(DOMAIN, SERVICE_RELOAD, blocking=True) await hass.async_block_till_done() @@ -1751,7 +1753,7 @@ async def test_integration_reload_failed( @pytest.mark.parametrize("do_config", [{}]) async def test_integration_setup_failed( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_modbus + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_pymodbus_return ) -> None: """Run test for integration setup on reload.""" with mock.patch.object( From 220801bf1c55f2dc2e2ec866dea27e54ef78ddc3 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 10 Apr 2024 22:09:10 +0200 Subject: [PATCH 475/967] Secure against resetting a non active modbus (#115364) --- homeassistant/components/modbus/__init__.py | 3 +++ tests/components/modbus/test_init.py | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 2a82cf89fd5..08e927bb553 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -463,6 +463,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_reset_platform(hass: HomeAssistant, integration_name: str) -> None: """Release modbus resources.""" + if DOMAIN not in hass.data: + _LOGGER.error("Modbus cannot reload, because it was never loaded") + return _LOGGER.info("Modbus reloading") hubs = hass.data[DOMAIN] for name in hubs: diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index f0dfd5357e7..1219a04fb0c 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -25,6 +25,7 @@ import voluptuous as vol from homeassistant import config as hass_config from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.modbus import async_reset_platform from homeassistant.components.modbus.const import ( ATTR_ADDRESS, ATTR_HUB, @@ -1781,3 +1782,9 @@ async def test_no_entities(hass: HomeAssistant) -> None: ] } assert await async_setup_component(hass, DOMAIN, config) is False + + +async def test_reset_platform(hass: HomeAssistant) -> None: + """Run test for async_reset_platform.""" + await async_reset_platform(hass, "modbus") + assert DOMAIN not in hass.data From 6394e25f75926c8193d11481bc105555c0f30d0e Mon Sep 17 00:00:00 2001 From: On Freund Date: Thu, 11 Apr 2024 00:26:15 +0300 Subject: [PATCH 476/967] Improve Risco exception logging (#115232) --- homeassistant/components/risco/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/risco/__init__.py b/homeassistant/components/risco/__init__.py index 7ca18ea77c5..d25579343c8 100644 --- a/homeassistant/components/risco/__init__.py +++ b/homeassistant/components/risco/__init__.py @@ -101,7 +101,7 @@ async def _async_setup_local_entry(hass: HomeAssistant, entry: ConfigEntry) -> b return False async def _error(error: Exception) -> None: - _LOGGER.error("Error in Risco library: %s", error) + _LOGGER.error("Error in Risco library", exc_info=error) entry.async_on_unload(risco.add_error_handler(_error)) From 9d7e20f9cae3bdbf59fc2809b2e7ab177a24ae96 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 10 Apr 2024 11:38:34 -1000 Subject: [PATCH 477/967] Fix deadlock in holidays dynamic loading (#115385) --- homeassistant/components/holiday/__init__.py | 23 ++++++++++++++++- homeassistant/components/workday/__init__.py | 27 +++++++++++++++----- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/holiday/__init__.py b/homeassistant/components/holiday/__init__.py index 4f2c593d38e..c9a58f29215 100644 --- a/homeassistant/components/holiday/__init__.py +++ b/homeassistant/components/holiday/__init__.py @@ -2,15 +2,36 @@ from __future__ import annotations +from functools import partial + +from holidays import country_holidays + from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform +from homeassistant.const import CONF_COUNTRY, Platform from homeassistant.core import HomeAssistant +from homeassistant.setup import SetupPhases, async_pause_setup + +from .const import CONF_PROVINCE PLATFORMS: list[Platform] = [Platform.CALENDAR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Holiday from a config entry.""" + country: str = entry.data[CONF_COUNTRY] + province: str | None = entry.data.get(CONF_PROVINCE) + + # We only import here to ensure that that its not imported later + # in the event loop since the platforms will call country_holidays + # which loads python code from disk. + with async_pause_setup(hass, SetupPhases.WAIT_IMPORT_PACKAGES): + # import executor job is used here because multiple integrations use + # the holidays library and it is not thread safe to import it in parallel + # https://github.com/python/cpython/issues/83065 + await hass.async_add_import_executor_job( + partial(country_holidays, country, subdiv=province) + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/workday/__init__.py b/homeassistant/components/workday/__init__.py index 077a6710b8d..f25cf41b992 100644 --- a/homeassistant/components/workday/__init__.py +++ b/homeassistant/components/workday/__init__.py @@ -11,6 +11,7 @@ from homeassistant.const import CONF_COUNTRY, CONF_LANGUAGE from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryError from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue +from homeassistant.setup import SetupPhases, async_pause_setup from .const import CONF_PROVINCE, DOMAIN, PLATFORMS @@ -23,7 +24,11 @@ async def _async_validate_country_and_province( if not country: return try: - await hass.async_add_executor_job(country_holidays, country) + with async_pause_setup(hass, SetupPhases.WAIT_IMPORT_PACKAGES): + # import executor job is used here because multiple integrations use + # the holidays library and it is not thread safe to import it in parallel + # https://github.com/python/cpython/issues/83065 + await hass.async_add_import_executor_job(country_holidays, country) except NotImplementedError as ex: async_create_issue( hass, @@ -41,9 +46,13 @@ async def _async_validate_country_and_province( if not province: return try: - await hass.async_add_executor_job( - partial(country_holidays, country, subdiv=province) - ) + with async_pause_setup(hass, SetupPhases.WAIT_IMPORT_PACKAGES): + # import executor job is used here because multiple integrations use + # the holidays library and it is not thread safe to import it in parallel + # https://github.com/python/cpython/issues/83065 + await hass.async_add_import_executor_job( + partial(country_holidays, country, subdiv=province) + ) except NotImplementedError as ex: async_create_issue( hass, @@ -73,9 +82,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await _async_validate_country_and_province(hass, entry, country, province) if country and CONF_LANGUAGE not in entry.options: - cls: HolidayBase = await hass.async_add_executor_job( - partial(country_holidays, country, subdiv=province) - ) + with async_pause_setup(hass, SetupPhases.WAIT_IMPORT_PACKAGES): + # import executor job is used here because multiple integrations use + # the holidays library and it is not thread safe to import it in parallel + # https://github.com/python/cpython/issues/83065 + cls: HolidayBase = await hass.async_add_import_executor_job( + partial(country_holidays, country, subdiv=province) + ) default_language = cls.default_language new_options = entry.options.copy() new_options[CONF_LANGUAGE] = default_language From bbecb98927c23059c015eda8e78eeefa952e7ef9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 10 Apr 2024 11:44:25 -1000 Subject: [PATCH 478/967] Fix type on known_object_ids in _entity_id_available and async_generate_entity_id (#115378) --- homeassistant/helpers/entity_platform.py | 4 ++-- homeassistant/helpers/entity_registry.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 261512c14af..ec4eef1f6a7 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -801,7 +801,7 @@ class EntityPlatform: get_initial_options=entity.get_initial_entity_options, has_entity_name=entity.has_entity_name, hidden_by=hidden_by, - known_object_ids=self.entities.keys(), + known_object_ids=self.entities, original_device_class=entity.device_class, original_icon=entity.icon, original_name=entity_name, @@ -839,7 +839,7 @@ class EntityPlatform: if self.entity_namespace is not None: suggested_object_id = f"{self.entity_namespace} {suggested_object_id}" entity.entity_id = entity_registry.async_generate_entity_id( - self.domain, suggested_object_id, self.entities.keys() + self.domain, suggested_object_id, self.entities ) # Make sure it is valid in case an entity set the value themselves diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index d6e7395a2cb..3a26505c7da 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -10,7 +10,7 @@ timer. from __future__ import annotations -from collections.abc import Callable, Hashable, Iterable, KeysView, Mapping +from collections.abc import Callable, Container, Hashable, KeysView, Mapping from datetime import datetime, timedelta from enum import StrEnum from functools import cached_property @@ -714,7 +714,7 @@ class EntityRegistry(BaseRegistry): return list(self.entities.get_device_ids()) def _entity_id_available( - self, entity_id: str, known_object_ids: Iterable[str] | None + self, entity_id: str, known_object_ids: Container[str] | None ) -> bool: """Return True if the entity_id is available. @@ -740,7 +740,7 @@ class EntityRegistry(BaseRegistry): self, domain: str, suggested_object_id: str, - known_object_ids: Iterable[str] | None = None, + known_object_ids: Container[str] | None = None, ) -> str: """Generate an entity ID that does not conflict. @@ -753,7 +753,7 @@ class EntityRegistry(BaseRegistry): test_string = preferred_string[:MAX_LENGTH_STATE_ENTITY_ID] if known_object_ids is None: - known_object_ids = {} + known_object_ids = set() tries = 1 while not self._entity_id_available(test_string, known_object_ids): @@ -773,7 +773,7 @@ class EntityRegistry(BaseRegistry): unique_id: str, *, # To influence entity ID generation - known_object_ids: Iterable[str] | None = None, + known_object_ids: Container[str] | None = None, suggested_object_id: str | None = None, # To disable or hide an entity if it gets created disabled_by: RegistryEntryDisabler | None = None, From be8adf9d293274d5b7378b0c38ac705dfae6659e Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 11 Apr 2024 01:10:46 +0200 Subject: [PATCH 479/967] Fix zha test by tweaking the log level (#115368) --- tests/components/zha/test_repairs.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/tests/components/zha/test_repairs.py b/tests/components/zha/test_repairs.py index c254a9c15fe..5e128cc464a 100644 --- a/tests/components/zha/test_repairs.py +++ b/tests/components/zha/test_repairs.py @@ -265,17 +265,27 @@ async def test_no_warn_on_socket(hass: HomeAssistant) -> None: mock_probe.assert_not_called() -async def test_probe_failure_exception_handling(caplog) -> None: +async def test_probe_failure_exception_handling( + caplog: pytest.LogCaptureFixture, +) -> None: """Test that probe failures are handled gracefully.""" + logger = logging.getLogger( + "homeassistant.components.zha.repairs.wrong_silabs_firmware" + ) + orig_level = logger.level + with ( + caplog.at_level(logging.DEBUG), patch( "homeassistant.components.zha.repairs.wrong_silabs_firmware.Flasher.probe_app_type", side_effect=RuntimeError(), - ), - caplog.at_level(logging.DEBUG), + ) as mock_probe_app_type, ): + logger.setLevel(logging.DEBUG) await probe_silabs_firmware_type("/dev/ttyZigbee") + logger.setLevel(orig_level) + mock_probe_app_type.assert_awaited() assert "Failed to probe application type" in caplog.text From e17c4ab4e31523701962438e070dea1063a4eff5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 10 Apr 2024 14:18:11 -1000 Subject: [PATCH 480/967] Fix flakey tessie media_player test (#115391) --- tests/components/tessie/snapshots/test_media_player.ambr | 9 ++++++++- tests/components/tessie/test_media_player.py | 3 +++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/components/tessie/snapshots/test_media_player.ambr b/tests/components/tessie/snapshots/test_media_player.ambr index d30e6c74aef..6c355c8ddca 100644 --- a/tests/components/tessie/snapshots/test_media_player.ambr +++ b/tests/components/tessie/snapshots/test_media_player.ambr @@ -54,6 +54,13 @@ 'attributes': ReadOnlyDict({ 'device_class': 'speaker', 'friendly_name': 'Test Media player', + 'media_album_name': 'Album', + 'media_artist': 'Artist', + 'media_duration': 60.0, + 'media_playlist': 'Playlist', + 'media_position': 30.0, + 'media_title': 'Song', + 'source': 'Spotify', 'supported_features': , 'volume_level': 0.22580323309042688, }), @@ -62,6 +69,6 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'idle', + 'state': 'playing', }) # --- diff --git a/tests/components/tessie/test_media_player.py b/tests/components/tessie/test_media_player.py index c9e4c3b84bc..008607b8018 100644 --- a/tests/components/tessie/test_media_player.py +++ b/tests/components/tessie/test_media_player.py @@ -22,6 +22,8 @@ async def test_media_player( freezer: FrozenDateTimeFactory, snapshot: SnapshotAssertion, entity_registry: er.EntityRegistry, + mock_get_state, + mock_get_status, ) -> None: """Tests that the media player entity is correct when idle.""" @@ -38,6 +40,7 @@ async def test_media_player( # The refresh fixture has music playing freezer.tick(WAIT) async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_entry.entity_id) == snapshot( name=f"{entity_entry.entity_id}-playing" From 288f3d84ba3c566468430556fdb826eac5690230 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 10 Apr 2024 15:15:21 -1000 Subject: [PATCH 481/967] Fix duplicate automation entity state writes (#115386) _async_attach_triggers was writing state, async_enable was writing state, and all of them were called async_added_to_hass After entity calls async_added_to_hass via add_to_platform_finish it will also write state so there were some paths that did it 3x async_disable was also writing state when the entity was removed --- .../components/automation/__init__.py | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 299be2c82f9..afc8f9aba10 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -604,18 +604,20 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity): ) if enable_automation: - await self.async_enable() + await self._async_enable() async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on and update the state.""" - await self.async_enable() + await self._async_enable() + self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" if CONF_STOP_ACTIONS in kwargs: - await self.async_disable(kwargs[CONF_STOP_ACTIONS]) + await self._async_disable(kwargs[CONF_STOP_ACTIONS]) else: - await self.async_disable() + await self._async_disable() + self.async_write_ha_state() async def async_trigger( self, @@ -743,7 +745,7 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity): async def async_will_remove_from_hass(self) -> None: """Remove listeners when removing automation from Home Assistant.""" await super().async_will_remove_from_hass() - await self.async_disable() + await self._async_disable() async def _async_enable_automation(self, event: Event) -> None: """Start automation on startup.""" @@ -752,31 +754,34 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity): return self._async_detach_triggers = await self._async_attach_triggers(True) + self.async_write_ha_state() - async def async_enable(self) -> None: + async def _async_enable(self) -> None: """Enable this automation entity. - This method is a coroutine. + This method is not expected to write state to the + state machine. """ if self._is_enabled: return self._is_enabled = True - # HomeAssistant is starting up if self.hass.state is not CoreState.not_running: self._async_detach_triggers = await self._async_attach_triggers(False) - self.async_write_ha_state() return self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STARTED, self._async_enable_automation, ) - self.async_write_ha_state() - async def async_disable(self, stop_actions: bool = DEFAULT_STOP_ACTIONS) -> None: - """Disable the automation entity.""" + async def _async_disable(self, stop_actions: bool = DEFAULT_STOP_ACTIONS) -> None: + """Disable the automation entity. + + This method is not expected to write state to the + state machine. + """ if not self._is_enabled and not self.action_script.runs: return @@ -789,8 +794,6 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity): if stop_actions: await self.action_script.async_stop() - self.async_write_ha_state() - def _log_callback(self, level: int, msg: str, **kwargs: Any) -> None: """Log helper callback.""" self._logger.log(level, "%s %s", msg, self.name, **kwargs) @@ -816,7 +819,6 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity): ) -> Callable[[], None] | None: """Set up the triggers.""" this = None - self.async_write_ha_state() if state := self.hass.states.get(self.entity_id): this = state.as_dict() variables = {"this": this} From 6bd6adc4f54b013d0ce544ac78e4c326c2d39daf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 10 Apr 2024 15:18:47 -1000 Subject: [PATCH 482/967] Avoid calling valid_entity_id when adding entities if they are already registered (#115388) --- homeassistant/helpers/entity_platform.py | 4 +++- tests/helpers/test_entity_platform.py | 13 +++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index ec4eef1f6a7..2b9a5d436ed 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -843,7 +843,9 @@ class EntityPlatform: ) # Make sure it is valid in case an entity set the value themselves - if not valid_entity_id(entity.entity_id): + # Avoid calling valid_entity_id if we already know it is valid + # since it already made it in the registry + if not entity.registry_entry and not valid_entity_id(entity.entity_id): entity.add_to_platform_abort() raise HomeAssistantError(f"Invalid entity ID: {entity.entity_id}") diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 59c4f7357f3..64f6d6bf9f5 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -1112,6 +1112,19 @@ async def test_entity_registry_updates_invalid_entity_id(hass: HomeAssistant) -> assert hass.states.get("diff_domain.world") is None +async def test_add_entity_with_invalid_id( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test trying to add an entity with an invalid entity_id.""" + platform = MockEntityPlatform(hass) + entity = MockEntity(entity_id="i.n.v.a.l.i.d") + await platform.async_add_entities([entity]) + assert ( + "Error adding entity i.n.v.a.l.i.d for domain " + "test_domain with platform test_platform" in caplog.text + ) + + async def test_device_info_called( hass: HomeAssistant, device_registry: dr.DeviceRegistry ) -> None: From f0c8c2a6845101e30c7f8d74c69e807bce16d299 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 10 Apr 2024 15:19:17 -1000 Subject: [PATCH 483/967] Adjust importlib helper to avoid leaking memory on re-raise (#115377) --- homeassistant/helpers/importlib.py | 11 +++++------ tests/helpers/test_importlib.py | 30 +++++++++++++++++++++++++++--- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/homeassistant/helpers/importlib.py b/homeassistant/helpers/importlib.py index 00af75f6d8e..98c75939084 100644 --- a/homeassistant/helpers/importlib.py +++ b/homeassistant/helpers/importlib.py @@ -30,11 +30,9 @@ async def async_import_module(hass: HomeAssistant, name: str) -> ModuleType: if module := cache.get(name): return module - failure_cache: dict[str, BaseException] = hass.data.setdefault( - DATA_IMPORT_FAILURES, {} - ) - if exception := failure_cache.get(name): - raise exception + failure_cache: dict[str, bool] = hass.data.setdefault(DATA_IMPORT_FAILURES, {}) + if name in failure_cache: + raise ModuleNotFoundError(f"{name} not found", name=name) import_futures: dict[str, asyncio.Future[ModuleType]] import_futures = hass.data.setdefault(DATA_IMPORT_FUTURES, {}) @@ -51,7 +49,8 @@ async def async_import_module(hass: HomeAssistant, name: str) -> ModuleType: module = await hass.async_add_import_executor_job(_get_module, cache, name) import_future.set_result(module) except BaseException as ex: - failure_cache[name] = ex + if isinstance(ex, ModuleNotFoundError): + failure_cache[name] = True import_future.set_exception(ex) with suppress(BaseException): # Set the exception retrieved flag on the future since diff --git a/tests/helpers/test_importlib.py b/tests/helpers/test_importlib.py index 5683dd5cf94..5c9686233f9 100644 --- a/tests/helpers/test_importlib.py +++ b/tests/helpers/test_importlib.py @@ -41,16 +41,40 @@ async def test_async_import_module_failures(hass: HomeAssistant) -> None: with ( patch( "homeassistant.helpers.importlib.importlib.import_module", - side_effect=ImportError, + side_effect=ValueError, ), - pytest.raises(ImportError), + pytest.raises(ValueError), + ): + await importlib.async_import_module(hass, "test.module") + + mock_module = MockModule() + # The failure should be not be cached + with ( + patch( + "homeassistant.helpers.importlib.importlib.import_module", + return_value=mock_module, + ), + ): + assert await importlib.async_import_module(hass, "test.module") is mock_module + + +async def test_async_import_module_failure_caches_module_not_found( + hass: HomeAssistant, +) -> None: + """Test importing a module caches ModuleNotFound.""" + with ( + patch( + "homeassistant.helpers.importlib.importlib.import_module", + side_effect=ModuleNotFoundError, + ), + pytest.raises(ModuleNotFoundError), ): await importlib.async_import_module(hass, "test.module") mock_module = MockModule() # The failure should be cached with ( - pytest.raises(ImportError), + pytest.raises(ModuleNotFoundError), patch( "homeassistant.helpers.importlib.importlib.import_module", return_value=mock_module, From d9b74fda8965c46efb1c640243d595225694f6c0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 10 Apr 2024 19:26:30 -1000 Subject: [PATCH 484/967] Add PYTHONASYNCIODEBUG to the dev container env (#115392) --- .devcontainer/devcontainer.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 83aa88140cc..2bdb6f99aad 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -4,7 +4,10 @@ "dockerFile": "../Dockerfile.dev", "postCreateCommand": "script/setup", "postStartCommand": "script/bootstrap", - "containerEnv": { "DEVCONTAINER": "1" }, + "containerEnv": { + "DEVCONTAINER": "1", + "PYTHONASYNCIODEBUG": "1" + }, // Port 5683 udp is used by Shelly integration "appPort": ["8123:8123", "5683:5683/udp"], "runArgs": ["-e", "GIT_EDITOR=code --wait"], From 4224234b7abfd1b31f75637b910f4fb89d5b4a0d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 11 Apr 2024 07:46:07 +0200 Subject: [PATCH 485/967] Add binary sensor to Netatmo (#115119) * Add binary sensor to Netatmo * Update homeassistant/components/netatmo/binary_sensor.py Co-authored-by: Jan-Philipp Benecke * Sigh * Fix * Fix * Fix --------- Co-authored-by: Jan-Philipp Benecke --- .../components/netatmo/binary_sensor.py | 60 ++ homeassistant/components/netatmo/const.py | 1 + homeassistant/components/netatmo/entity.py | 42 +- homeassistant/components/netatmo/sensor.py | 38 +- .../netatmo/snapshots/test_binary_sensor.ambr | 541 ++++++++++++++++++ .../components/netatmo/test_binary_sensor.py | 31 + 6 files changed, 680 insertions(+), 33 deletions(-) create mode 100644 homeassistant/components/netatmo/binary_sensor.py create mode 100644 tests/components/netatmo/snapshots/test_binary_sensor.ambr create mode 100644 tests/components/netatmo/test_binary_sensor.py diff --git a/homeassistant/components/netatmo/binary_sensor.py b/homeassistant/components/netatmo/binary_sensor.py new file mode 100644 index 00000000000..c478525753a --- /dev/null +++ b/homeassistant/components/netatmo/binary_sensor.py @@ -0,0 +1,60 @@ +"""Support for Netatmo binary sensors.""" + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +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 NETATMO_CREATE_WEATHER_SENSOR +from .data_handler import NetatmoDevice +from .entity import NetatmoWeatherModuleEntity + +BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( + BinarySensorEntityDescription( + key="reachable", + device_class=BinarySensorDeviceClass.CONNECTIVITY, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Netatmo binary sensors based on a config entry.""" + + @callback + def _create_weather_binary_sensor_entity(netatmo_device: NetatmoDevice) -> None: + async_add_entities( + NetatmoWeatherBinarySensor(netatmo_device, description) + for description in BINARY_SENSOR_TYPES + if description.key in netatmo_device.device.features + ) + + entry.async_on_unload( + async_dispatcher_connect( + hass, NETATMO_CREATE_WEATHER_SENSOR, _create_weather_binary_sensor_entity + ) + ) + + +class NetatmoWeatherBinarySensor(NetatmoWeatherModuleEntity, BinarySensorEntity): + """Implementation of a Netatmo binary sensor.""" + + def __init__( + self, device: NetatmoDevice, description: BinarySensorEntityDescription + ) -> None: + """Initialize a Netatmo binary sensor.""" + super().__init__(device) + self.entity_description = description + self._attr_unique_id = f"{self.device.entity_id}-{description.key}" + + @callback + def async_update_callback(self) -> None: + """Update the entity's state.""" + self._attr_is_on = self.device.reachable + self.async_write_ha_state() diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index 8109b418066..74f2ebc84b2 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -9,6 +9,7 @@ MANUFACTURER = "Netatmo" DEFAULT_ATTRIBUTION = f"Data provided by {MANUFACTURER}" PLATFORMS = [ + Platform.BINARY_SENSOR, Platform.CAMERA, Platform.CLIMATE, Platform.COVER, diff --git a/homeassistant/components/netatmo/entity.py b/homeassistant/components/netatmo/entity.py index 5f08cb941d6..6fdebcf0c3f 100644 --- a/homeassistant/components/netatmo/entity.py +++ b/homeassistant/components/netatmo/entity.py @@ -3,12 +3,13 @@ from __future__ import annotations from abc import abstractmethod -from typing import Any +from typing import Any, cast from pyatmo import DeviceType, Home, Module, Room -from pyatmo.modules.base_class import NetatmoBase +from pyatmo.modules.base_class import NetatmoBase, Place from pyatmo.modules.device_types import DEVICE_DESCRIPTION_MAP +from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE from homeassistant.core import callback from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import DeviceInfo @@ -16,6 +17,7 @@ from homeassistant.helpers.entity import Entity from .const import ( CONF_URL_ENERGY, + CONF_URL_WEATHER, DATA_DEVICE_IDS, DEFAULT_ATTRIBUTION, DOMAIN, @@ -166,3 +168,39 @@ class NetatmoModuleEntity(NetatmoDeviceEntity): def device_type(self) -> DeviceType: """Return the device type.""" return self.device.device_type + + +class NetatmoWeatherModuleEntity(NetatmoModuleEntity): + """Netatmo weather module entity base class.""" + + _attr_configuration_url = CONF_URL_WEATHER + + def __init__(self, device: NetatmoDevice) -> None: + """Set up a Netatmo weather module entity.""" + super().__init__(device) + category = getattr(self.device.device_category, "name") + self._publishers.extend( + [ + { + "name": category, + SIGNAL_NAME: category, + }, + ] + ) + + if hasattr(self.device, "place"): + place = cast(Place, getattr(self.device, "place")) + if hasattr(place, "location") and place.location is not None: + self._attr_extra_state_attributes.update( + { + ATTR_LATITUDE: place.location.latitude, + ATTR_LONGITUDE: place.location.longitude, + } + ) + + @property + def device_type(self) -> DeviceType: + """Return the Netatmo device type.""" + if "." not in self.device.device_type: + return super().device_type + return DeviceType(self.device.device_type.partition(".")[2]) diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 7e7b6029572..4e470437f7a 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -8,7 +8,6 @@ import logging from typing import Any, cast import pyatmo -from pyatmo import DeviceType from pyatmo.modules import PublicWeatherArea from homeassistant.components.sensor import ( @@ -48,7 +47,6 @@ from homeassistant.helpers.typing import StateType from .const import ( CONF_URL_ENERGY, CONF_URL_PUBLIC_WEATHER, - CONF_URL_WEATHER, CONF_WEATHER_AREAS, DATA_HANDLER, DOMAIN, @@ -59,7 +57,12 @@ from .const import ( SIGNAL_NAME, ) from .data_handler import HOME, PUBLIC, NetatmoDataHandler, NetatmoDevice, NetatmoRoom -from .entity import NetatmoBaseEntity, NetatmoModuleEntity, NetatmoRoomEntity +from .entity import ( + NetatmoBaseEntity, + NetatmoModuleEntity, + NetatmoRoomEntity, + NetatmoWeatherModuleEntity, +) from .helper import NetatmoArea _LOGGER = logging.getLogger(__name__) @@ -491,11 +494,10 @@ async def async_setup_entry( await add_public_entities(False) -class NetatmoWeatherSensor(NetatmoModuleEntity, SensorEntity): +class NetatmoWeatherSensor(NetatmoWeatherModuleEntity, SensorEntity): """Implementation of a Netatmo weather/home coach sensor.""" entity_description: NetatmoSensorEntityDescription - _attr_configuration_url = CONF_URL_WEATHER def __init__( self, @@ -506,34 +508,8 @@ class NetatmoWeatherSensor(NetatmoModuleEntity, SensorEntity): super().__init__(netatmo_device) self.entity_description = description self._attr_translation_key = description.netatmo_name - category = getattr(self.device.device_category, "name") - self._publishers.extend( - [ - { - "name": category, - SIGNAL_NAME: category, - }, - ] - ) self._attr_unique_id = f"{self.device.entity_id}-{description.key}" - if hasattr(self.device, "place"): - place = cast(pyatmo.modules.base_class.Place, getattr(self.device, "place")) - if hasattr(place, "location") and place.location is not None: - self._attr_extra_state_attributes.update( - { - ATTR_LATITUDE: place.location.latitude, - ATTR_LONGITUDE: place.location.longitude, - } - ) - - @property - def device_type(self) -> DeviceType: - """Return the Netatmo device type.""" - if "." not in self.device.device_type: - return super().device_type - return DeviceType(self.device.device_type.partition(".")[2]) - @property def available(self) -> bool: """Return True if entity is available.""" diff --git a/tests/components/netatmo/snapshots/test_binary_sensor.ambr b/tests/components/netatmo/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..6a90b4dd77a --- /dev/null +++ b/tests/components/netatmo/snapshots/test_binary_sensor.ambr @@ -0,0 +1,541 @@ +# serializer version: 1 +# name: test_entity[binary_sensor.baby_bedroom_connectivity-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.baby_bedroom_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12:34:56:26:68:92-reachable', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[binary_sensor.baby_bedroom_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'connectivity', + 'friendly_name': 'Baby Bedroom Connectivity', + 'latitude': 13.377726, + 'longitude': 52.516263, + }), + 'context': , + 'entity_id': 'binary_sensor.baby_bedroom_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity[binary_sensor.bedroom_connectivity-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.bedroom_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12:34:56:26:69:0c-reachable', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[binary_sensor.bedroom_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'connectivity', + 'friendly_name': 'Bedroom Connectivity', + 'latitude': 13.377726, + 'longitude': 52.516263, + }), + 'context': , + 'entity_id': 'binary_sensor.bedroom_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity[binary_sensor.kitchen_connectivity-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.kitchen_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12:34:56:25:cf:a8-reachable', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[binary_sensor.kitchen_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'connectivity', + 'friendly_name': 'Kitchen Connectivity', + 'latitude': 13.377726, + 'longitude': 52.516263, + }), + 'context': , + 'entity_id': 'binary_sensor.kitchen_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity[binary_sensor.livingroom_connectivity-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.livingroom_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12:34:56:26:65:14-reachable', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[binary_sensor.livingroom_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'connectivity', + 'friendly_name': 'Livingroom Connectivity', + 'latitude': 13.377726, + 'longitude': 52.516263, + }), + 'context': , + 'entity_id': 'binary_sensor.livingroom_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity[binary_sensor.parents_bedroom_connectivity-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.parents_bedroom_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12:34:56:3e:c5:46-reachable', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[binary_sensor.parents_bedroom_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'connectivity', + 'friendly_name': 'Parents Bedroom Connectivity', + 'latitude': 13.377726, + 'longitude': 52.516263, + }), + 'context': , + 'entity_id': 'binary_sensor.parents_bedroom_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity[binary_sensor.villa_bathroom_connectivity-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.villa_bathroom_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12:34:56:80:7e:18-reachable', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[binary_sensor.villa_bathroom_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'connectivity', + 'friendly_name': 'Villa Bathroom Connectivity', + }), + 'context': , + 'entity_id': 'binary_sensor.villa_bathroom_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity[binary_sensor.villa_bedroom_connectivity-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.villa_bedroom_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12:34:56:80:44:92-reachable', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[binary_sensor.villa_bedroom_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'connectivity', + 'friendly_name': 'Villa Bedroom Connectivity', + }), + 'context': , + 'entity_id': 'binary_sensor.villa_bedroom_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity[binary_sensor.villa_connectivity-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.villa_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12:34:56:80:bb:26-reachable', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[binary_sensor.villa_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'connectivity', + 'friendly_name': 'Villa Connectivity', + 'latitude': 46.123456, + 'longitude': 6.1234567, + }), + 'context': , + 'entity_id': 'binary_sensor.villa_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity[binary_sensor.villa_garden_connectivity-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.villa_garden_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12:34:56:03:1b:e4-reachable', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[binary_sensor.villa_garden_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'connectivity', + 'friendly_name': 'Villa Garden Connectivity', + }), + 'context': , + 'entity_id': 'binary_sensor.villa_garden_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity[binary_sensor.villa_outdoor_connectivity-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.villa_outdoor_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12:34:56:80:1c:42-reachable', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[binary_sensor.villa_outdoor_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'connectivity', + 'friendly_name': 'Villa Outdoor Connectivity', + }), + 'context': , + 'entity_id': 'binary_sensor.villa_outdoor_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity[binary_sensor.villa_rain_connectivity-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.villa_rain_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12:34:56:80:c1:ea-reachable', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[binary_sensor.villa_rain_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'connectivity', + 'friendly_name': 'Villa Rain Connectivity', + }), + 'context': , + 'entity_id': 'binary_sensor.villa_rain_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/netatmo/test_binary_sensor.py b/tests/components/netatmo/test_binary_sensor.py new file mode 100644 index 00000000000..53aea461fde --- /dev/null +++ b/tests/components/netatmo/test_binary_sensor.py @@ -0,0 +1,31 @@ +"""Support for Netatmo binary sensors.""" + +from unittest.mock import AsyncMock + +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry +from tests.components.netatmo.common import snapshot_platform_entities + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_entity( + hass: HomeAssistant, + config_entry: MockConfigEntry, + netatmo_auth: AsyncMock, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, +) -> None: + """Test entities.""" + await snapshot_platform_entities( + hass, + config_entry, + Platform.BINARY_SENSOR, + entity_registry, + snapshot, + ) From 3546ca386f9d8a629859302b24967045a1f93eb6 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 11 Apr 2024 09:40:16 +0200 Subject: [PATCH 486/967] Use freezer on diagnostics test (#115398) * Use freezer on diagnostics test * Patch correctly --- tests/components/rtsp_to_webrtc/test_diagnostics.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/components/rtsp_to_webrtc/test_diagnostics.py b/tests/components/rtsp_to_webrtc/test_diagnostics.py index e020ebfd5f3..8af6b914191 100644 --- a/tests/components/rtsp_to_webrtc/test_diagnostics.py +++ b/tests/components/rtsp_to_webrtc/test_diagnostics.py @@ -2,6 +2,8 @@ from typing import Any +from freezegun.api import FrozenDateTimeFactory + from homeassistant.core import HomeAssistant from .conftest import ComponentSetup @@ -19,6 +21,7 @@ async def test_entry_diagnostics( config_entry: MockConfigEntry, rtsp_to_webrtc_client: Any, setup_integration: ComponentSetup, + freezer: FrozenDateTimeFactory, ) -> None: """Test config entry diagnostics.""" await setup_integration() From 6954fcc8ad35424e21787b1217b8110a7c880fa0 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Thu, 11 Apr 2024 09:10:56 +0100 Subject: [PATCH 487/967] Add strict typing to ring integration (#115276) --- .strict-typing | 1 + homeassistant/components/ring/__init__.py | 47 ++-- .../components/ring/binary_sensor.py | 62 +++-- homeassistant/components/ring/button.py | 21 +- homeassistant/components/ring/camera.py | 74 ++--- homeassistant/components/ring/config_flow.py | 14 +- homeassistant/components/ring/const.py | 6 - homeassistant/components/ring/coordinator.py | 74 +++-- homeassistant/components/ring/diagnostics.py | 12 +- homeassistant/components/ring/entity.py | 52 ++-- homeassistant/components/ring/light.py | 46 +-- homeassistant/components/ring/sensor.py | 261 +++++++++--------- homeassistant/components/ring/siren.py | 25 +- homeassistant/components/ring/switch.py | 36 +-- mypy.ini | 10 + tests/components/ring/common.py | 2 +- 16 files changed, 384 insertions(+), 359 deletions(-) diff --git a/.strict-typing b/.strict-typing index b1d6df7c9b8..63a867e9c50 100644 --- a/.strict-typing +++ b/.strict-typing @@ -363,6 +363,7 @@ homeassistant.components.rest_command.* homeassistant.components.rfxtrx.* homeassistant.components.rhasspy.* homeassistant.components.ridwell.* +homeassistant.components.ring.* homeassistant.components.rituals_perfume_genie.* homeassistant.components.roku.* homeassistant.components.romy.* diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py index ffa99704526..36c66550ddc 100644 --- a/homeassistant/components/ring/__init__.py +++ b/homeassistant/components/ring/__init__.py @@ -2,10 +2,12 @@ from __future__ import annotations +from dataclasses import dataclass from functools import partial import logging +from typing import Any, cast -from ring_doorbell import Auth, Ring +from ring_doorbell import Auth, Ring, RingDevices from homeassistant.config_entries import ConfigEntry from homeassistant.const import APPLICATION_NAME, CONF_TOKEN, __version__ @@ -13,23 +15,26 @@ from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from .const import ( - DOMAIN, - PLATFORMS, - RING_API, - RING_DEVICES, - RING_DEVICES_COORDINATOR, - RING_NOTIFICATIONS_COORDINATOR, -) +from .const import DOMAIN, PLATFORMS from .coordinator import RingDataCoordinator, RingNotificationsCoordinator _LOGGER = logging.getLogger(__name__) +@dataclass +class RingData: + """Class to support type hinting of ring data collection.""" + + api: Ring + devices: RingDevices + devices_coordinator: RingDataCoordinator + notifications_coordinator: RingNotificationsCoordinator + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - def token_updater(token): + def token_updater(token: dict[str, Any]) -> None: """Handle from sync context when token is updated.""" hass.loop.call_soon_threadsafe( partial( @@ -51,12 +56,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await devices_coordinator.async_config_entry_first_refresh() await notifications_coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { - RING_API: ring, - RING_DEVICES: ring.devices(), - RING_DEVICES_COORDINATOR: devices_coordinator, - RING_NOTIFICATIONS_COORDINATOR: notifications_coordinator, - } + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = RingData( + api=ring, + devices=ring.devices(), + devices_coordinator=devices_coordinator, + notifications_coordinator=notifications_coordinator, + ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -83,8 +88,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) for info in hass.data[DOMAIN].values(): - await info[RING_DEVICES_COORDINATOR].async_refresh() - await info[RING_NOTIFICATIONS_COORDINATOR].async_refresh() + ring_data = cast(RingData, info) + await ring_data.devices_coordinator.async_refresh() + await ring_data.notifications_coordinator.async_refresh() # register service hass.services.async_register(DOMAIN, "update", async_refresh_all) @@ -121,8 +127,9 @@ async def _migrate_old_unique_ids(hass: HomeAssistant, entry_id: str) -> None: @callback def _async_migrator(entity_entry: er.RegistryEntry) -> dict[str, str] | None: # Old format for camera and light was int - if isinstance(entity_entry.unique_id, int): - new_unique_id = str(entity_entry.unique_id) + unique_id = cast(str | int, entity_entry.unique_id) + if isinstance(unique_id, int): + new_unique_id = str(unique_id) if existing_entity_id := entity_registry.async_get_entity_id( entity_entry.domain, entity_entry.platform, new_unique_id ): diff --git a/homeassistant/components/ring/binary_sensor.py b/homeassistant/components/ring/binary_sensor.py index 19daebf9ce1..2db04cfd461 100644 --- a/homeassistant/components/ring/binary_sensor.py +++ b/homeassistant/components/ring/binary_sensor.py @@ -2,10 +2,13 @@ from __future__ import annotations +from collections.abc import Callable, Mapping from dataclasses import dataclass from datetime import datetime from typing import Any +from ring_doorbell import Ring, RingEvent, RingGeneric + from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, @@ -15,29 +18,32 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN, RING_API, RING_DEVICES, RING_NOTIFICATIONS_COORDINATOR +from . import RingData +from .const import DOMAIN from .coordinator import RingNotificationsCoordinator -from .entity import RingEntity +from .entity import RingBaseEntity @dataclass(frozen=True, kw_only=True) class RingBinarySensorEntityDescription(BinarySensorEntityDescription): """Describes Ring binary sensor entity.""" - category: list[str] + exists_fn: Callable[[RingGeneric], bool] BINARY_SENSOR_TYPES: tuple[RingBinarySensorEntityDescription, ...] = ( RingBinarySensorEntityDescription( key="ding", translation_key="ding", - category=["doorbots", "authorized_doorbots", "other"], device_class=BinarySensorDeviceClass.OCCUPANCY, + exists_fn=lambda device: device.family + in {"doorbots", "authorized_doorbots", "other"}, ), RingBinarySensorEntityDescription( key="motion", - category=["doorbots", "authorized_doorbots", "stickup_cams"], device_class=BinarySensorDeviceClass.MOTION, + exists_fn=lambda device: device.family + in {"doorbots", "authorized_doorbots", "stickup_cams"}, ), ) @@ -48,34 +54,36 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Ring binary sensors from a config entry.""" - ring = hass.data[DOMAIN][config_entry.entry_id][RING_API] - devices = hass.data[DOMAIN][config_entry.entry_id][RING_DEVICES] - notifications_coordinator: RingNotificationsCoordinator = hass.data[DOMAIN][ - config_entry.entry_id - ][RING_NOTIFICATIONS_COORDINATOR] + ring_data: RingData = hass.data[DOMAIN][config_entry.entry_id] entities = [ - RingBinarySensor(ring, device, notifications_coordinator, description) - for device_type in ("doorbots", "authorized_doorbots", "stickup_cams", "other") + RingBinarySensor( + ring_data.api, + device, + ring_data.notifications_coordinator, + description, + ) for description in BINARY_SENSOR_TYPES - if device_type in description.category - for device in devices[device_type] + for device in ring_data.devices.all_devices + if description.exists_fn(device) ] async_add_entities(entities) -class RingBinarySensor(RingEntity, BinarySensorEntity): +class RingBinarySensor( + RingBaseEntity[RingNotificationsCoordinator], BinarySensorEntity +): """A binary sensor implementation for Ring device.""" - _active_alert: dict[str, Any] | None = None + _active_alert: RingEvent | None = None entity_description: RingBinarySensorEntityDescription def __init__( self, - ring, - device, - coordinator, + ring: Ring, + device: RingGeneric, + coordinator: RingNotificationsCoordinator, description: RingBinarySensorEntityDescription, ) -> None: """Initialize a sensor for Ring device.""" @@ -89,13 +97,13 @@ class RingBinarySensor(RingEntity, BinarySensorEntity): self._update_alert() @callback - def _handle_coordinator_update(self, _=None): + def _handle_coordinator_update(self, _: Any = None) -> None: """Call update method.""" self._update_alert() super()._handle_coordinator_update() @callback - def _update_alert(self): + def _update_alert(self) -> None: """Update active alert.""" self._active_alert = next( ( @@ -108,21 +116,23 @@ class RingBinarySensor(RingEntity, BinarySensorEntity): ) @property - def is_on(self): + def is_on(self) -> bool: """Return True if the binary sensor is on.""" return self._active_alert is not None @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> Mapping[str, Any] | None: """Return the state attributes.""" attrs = super().extra_state_attributes if self._active_alert is None: return attrs + assert isinstance(attrs, dict) attrs["state"] = self._active_alert["state"] - attrs["expires_at"] = datetime.fromtimestamp( - self._active_alert.get("now") + self._active_alert.get("expires_in") - ).isoformat() + now = self._active_alert.get("now") + expires_in = self._active_alert.get("expires_in") + assert now and expires_in + attrs["expires_at"] = datetime.fromtimestamp(now + expires_in).isoformat() return attrs diff --git a/homeassistant/components/ring/button.py b/homeassistant/components/ring/button.py index d739dc29841..a14853a0881 100644 --- a/homeassistant/components/ring/button.py +++ b/homeassistant/components/ring/button.py @@ -2,12 +2,15 @@ from __future__ import annotations +from ring_doorbell import RingOther + 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, RING_DEVICES, RING_DEVICES_COORDINATOR +from . import RingData +from .const import DOMAIN from .coordinator import RingDataCoordinator from .entity import RingEntity, exception_wrap @@ -22,14 +25,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Create the buttons for the Ring devices.""" - devices = hass.data[DOMAIN][config_entry.entry_id][RING_DEVICES] - devices_coordinator: RingDataCoordinator = hass.data[DOMAIN][config_entry.entry_id][ - RING_DEVICES_COORDINATOR - ] + ring_data: RingData = hass.data[DOMAIN][config_entry.entry_id] + devices_coordinator = ring_data.devices_coordinator async_add_entities( RingDoorButton(device, devices_coordinator, BUTTON_DESCRIPTION) - for device in devices["other"] + for device in ring_data.devices.other if device.has_capability("open") ) @@ -37,10 +38,12 @@ async def async_setup_entry( class RingDoorButton(RingEntity, ButtonEntity): """Creates a button to open the ring intercom door.""" + _device: RingOther + def __init__( self, - device, - coordinator, + device: RingOther, + coordinator: RingDataCoordinator, description: ButtonEntityDescription, ) -> None: """Initialize the button.""" @@ -52,6 +55,6 @@ class RingDoorButton(RingEntity, ButtonEntity): self._attr_unique_id = f"{device.id}-{description.key}" @exception_wrap - def press(self): + def press(self) -> None: """Open the door.""" self._device.open_door() diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index b9d73afe6de..297e5f47627 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -3,11 +3,12 @@ from __future__ import annotations from datetime import timedelta -from itertools import chain import logging -from typing import Optional +from typing import Any +from aiohttp import web from haffmpeg.camera import CameraMjpeg +from ring_doorbell import RingDoorBell from homeassistant.components import ffmpeg from homeassistant.components.camera import Camera @@ -17,7 +18,8 @@ from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt as dt_util -from .const import DOMAIN, RING_DEVICES, RING_DEVICES_COORDINATOR +from . import RingData +from .const import DOMAIN from .coordinator import RingDataCoordinator from .entity import RingEntity, exception_wrap @@ -33,20 +35,15 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up a Ring Door Bell and StickUp Camera.""" - devices = hass.data[DOMAIN][config_entry.entry_id][RING_DEVICES] - devices_coordinator: RingDataCoordinator = hass.data[DOMAIN][config_entry.entry_id][ - RING_DEVICES_COORDINATOR - ] + ring_data: RingData = hass.data[DOMAIN][config_entry.entry_id] + devices_coordinator = ring_data.devices_coordinator ffmpeg_manager = ffmpeg.get_ffmpeg_manager(hass) - cams = [] - for camera in chain( - devices["doorbots"], devices["authorized_doorbots"], devices["stickup_cams"] - ): - if not camera.has_subscription: - continue - - cams.append(RingCam(camera, devices_coordinator, ffmpeg_manager)) + cams = [ + RingCam(camera, devices_coordinator, ffmpeg_manager) + for camera in ring_data.devices.video_devices + if camera.has_subscription + ] async_add_entities(cams) @@ -55,28 +52,34 @@ class RingCam(RingEntity, Camera): """An implementation of a Ring Door Bell camera.""" _attr_name = None + _device: RingDoorBell - def __init__(self, device, coordinator, ffmpeg_manager): + def __init__( + self, + device: RingDoorBell, + coordinator: RingDataCoordinator, + ffmpeg_manager: ffmpeg.FFmpegManager, + ) -> None: """Initialize a Ring Door Bell camera.""" super().__init__(device, coordinator) Camera.__init__(self) - self._ffmpeg_manager = ffmpeg_manager - self._last_event = None - self._last_video_id = None - self._video_url = None - self._image = None + self._last_event: dict[str, Any] | None = None + self._last_video_id: int | None = None + self._video_url: str | None = None + self._image: bytes | None = None self._expires_at = dt_util.utcnow() - FORCE_REFRESH_INTERVAL self._attr_unique_id = str(device.id) if device.has_capability(MOTION_DETECTION_CAPABILITY): self._attr_motion_detection_enabled = device.motion_detection @callback - def _handle_coordinator_update(self): + def _handle_coordinator_update(self) -> None: """Call update method.""" - history_data: Optional[list] - if not (history_data := self._get_coordinator_history()): - return + self._device = self._get_coordinator_data().get_video_device( + self._device.device_api_id + ) + history_data = self._device.last_history if history_data: self._last_event = history_data[0] self.async_schedule_update_ha_state(True) @@ -89,7 +92,7 @@ class RingCam(RingEntity, Camera): self.async_write_ha_state() @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" return { "video_url": self._video_url, @@ -100,7 +103,7 @@ class RingCam(RingEntity, Camera): self, width: int | None = None, height: int | None = None ) -> bytes | None: """Return a still image response from the camera.""" - if self._image is None and self._video_url: + if self._image is None and self._video_url is not None: image = await ffmpeg.async_get_image( self.hass, self._video_url, @@ -113,10 +116,12 @@ class RingCam(RingEntity, Camera): return self._image - async def handle_async_mjpeg_stream(self, request): + async def handle_async_mjpeg_stream( + self, request: web.Request + ) -> web.StreamResponse | None: """Generate an HTTP MJPEG stream from the camera.""" if self._video_url is None: - return + return None stream = CameraMjpeg(self._ffmpeg_manager.binary) await stream.open_camera(self._video_url) @@ -132,7 +137,7 @@ class RingCam(RingEntity, Camera): finally: await stream.close() - async def async_update(self): + async def async_update(self) -> None: """Update camera entity and refresh attributes.""" if ( self._device.has_capability(MOTION_DETECTION_CAPABILITY) @@ -160,11 +165,14 @@ class RingCam(RingEntity, Camera): self._expires_at = FORCE_REFRESH_INTERVAL + utcnow @exception_wrap - def _get_video(self): - return self._device.recording_url(self._last_event["id"]) + def _get_video(self) -> str | None: + if self._last_event is None: + return None + assert (event_id := self._last_event.get("id")) and isinstance(event_id, int) + return self._device.recording_url(event_id) @exception_wrap - def _set_motion_detection_enabled(self, new_state): + def _set_motion_detection_enabled(self, new_state: bool) -> None: if not self._device.has_capability(MOTION_DETECTION_CAPABILITY): _LOGGER.error( "Entity %s does not have motion detection capability", self.entity_id diff --git a/homeassistant/components/ring/config_flow.py b/homeassistant/components/ring/config_flow.py index 6d4f28eb311..4762017c5bc 100644 --- a/homeassistant/components/ring/config_flow.py +++ b/homeassistant/components/ring/config_flow.py @@ -28,7 +28,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema( STEP_REAUTH_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str}) -async def validate_input(hass: HomeAssistant, data): +async def validate_input(hass: HomeAssistant, data: dict[str, str]) -> dict[str, Any]: """Validate the user input allows us to connect.""" auth = Auth(f"{APPLICATION_NAME}/{ha_version}") @@ -56,9 +56,11 @@ class RingConfigFlow(ConfigFlow, domain=DOMAIN): user_pass: dict[str, Any] = {} reauth_entry: ConfigEntry | None = None - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: """Handle the initial step.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: try: token = await validate_input(self.hass, user_input) @@ -82,7 +84,9 @@ class RingConfigFlow(ConfigFlow, domain=DOMAIN): step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors ) - async def async_step_2fa(self, user_input=None): + async def async_step_2fa( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: """Handle 2fa step.""" if user_input: if self.reauth_entry: @@ -110,7 +114,7 @@ class RingConfigFlow(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Dialog that informs the user that reauth is required.""" - errors = {} + errors: dict[str, str] = {} assert self.reauth_entry is not None if user_input: diff --git a/homeassistant/components/ring/const.py b/homeassistant/components/ring/const.py index 23f378a38be..70813a78c76 100644 --- a/homeassistant/components/ring/const.py +++ b/homeassistant/components/ring/const.py @@ -28,10 +28,4 @@ PLATFORMS = [ SCAN_INTERVAL = timedelta(minutes=1) NOTIFICATIONS_SCAN_INTERVAL = timedelta(seconds=5) -RING_API = "api" -RING_DEVICES = "devices" - -RING_DEVICES_COORDINATOR = "device_data" -RING_NOTIFICATIONS_COORDINATOR = "dings_data" - CONF_2FA = "2fa" diff --git a/homeassistant/components/ring/coordinator.py b/homeassistant/components/ring/coordinator.py index fdb6fc1f296..a10f9317bab 100644 --- a/homeassistant/components/ring/coordinator.py +++ b/homeassistant/components/ring/coordinator.py @@ -2,11 +2,10 @@ from asyncio import TaskGroup from collections.abc import Callable -from dataclasses import dataclass import logging -from typing import Any, Optional +from typing import TypeVar, TypeVarTuple -from ring_doorbell import AuthenticationError, Ring, RingError, RingGeneric, RingTimeout +from ring_doorbell import AuthenticationError, Ring, RingDevices, RingError, RingTimeout from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed @@ -16,10 +15,13 @@ from .const import NOTIFICATIONS_SCAN_INTERVAL, SCAN_INTERVAL _LOGGER = logging.getLogger(__name__) +_R = TypeVar("_R") +_Ts = TypeVarTuple("_Ts") + async def _call_api( - hass: HomeAssistant, target: Callable[..., Any], *args, msg_suffix: str = "" -): + hass: HomeAssistant, target: Callable[[*_Ts], _R], *args: *_Ts, msg_suffix: str = "" +) -> _R: try: return await hass.async_add_executor_job(target, *args) except AuthenticationError as err: @@ -34,15 +36,7 @@ async def _call_api( raise UpdateFailed(f"Error communicating with API{msg_suffix}: {err}") from err -@dataclass -class RingDeviceData: - """RingDeviceData.""" - - device: RingGeneric - history: Optional[list] = None - - -class RingDataCoordinator(DataUpdateCoordinator[dict[int, RingDeviceData]]): +class RingDataCoordinator(DataUpdateCoordinator[RingDevices]): """Base class for device coordinators.""" def __init__( @@ -60,45 +54,39 @@ class RingDataCoordinator(DataUpdateCoordinator[dict[int, RingDeviceData]]): self.ring_api: Ring = ring_api self.first_call: bool = True - async def _async_update_data(self): + async def _async_update_data(self) -> RingDevices: """Fetch data from API endpoint.""" update_method: str = "update_data" if self.first_call else "update_devices" await _call_api(self.hass, getattr(self.ring_api, update_method)) self.first_call = False - data: dict[str, RingDeviceData] = {} - devices: dict[str : list[RingGeneric]] = self.ring_api.devices() + devices: RingDevices = self.ring_api.devices() subscribed_device_ids = set(self.async_contexts()) - for device_type in devices: - for device in devices[device_type]: - # Don't update all devices in the ring api, only those that set - # their device id as context when they subscribed. - if device.id in subscribed_device_ids: - data[device.id] = RingDeviceData(device=device) - try: - history_task = None - async with TaskGroup() as tg: - if device.has_capability("history"): - history_task = tg.create_task( - _call_api( - self.hass, - lambda device: device.history(limit=10), - device, - msg_suffix=f" for device {device.name}", # device_id is the mac - ) - ) + for device in devices.all_devices: + # Don't update all devices in the ring api, only those that set + # their device id as context when they subscribed. + if device.id in subscribed_device_ids: + try: + async with TaskGroup() as tg: + if device.has_capability("history"): tg.create_task( _call_api( self.hass, - device.update_health_data, - msg_suffix=f" for device {device.name}", + lambda device: device.history(limit=10), + device, + msg_suffix=f" for device {device.name}", # device_id is the mac ) ) - if history_task: - data[device.id].history = history_task.result() - except ExceptionGroup as eg: - raise eg.exceptions[0] # noqa: B904 + tg.create_task( + _call_api( + self.hass, + device.update_health_data, + msg_suffix=f" for device {device.name}", + ) + ) + except ExceptionGroup as eg: + raise eg.exceptions[0] # noqa: B904 - return data + return devices class RingNotificationsCoordinator(DataUpdateCoordinator[None]): @@ -114,6 +102,6 @@ class RingNotificationsCoordinator(DataUpdateCoordinator[None]): ) self.ring_api: Ring = ring_api - async def _async_update_data(self): + async def _async_update_data(self) -> None: """Fetch data from API endpoint.""" await _call_api(self.hass, self.ring_api.update_dings) diff --git a/homeassistant/components/ring/diagnostics.py b/homeassistant/components/ring/diagnostics.py index 5295629979a..2e7604d9f50 100644 --- a/homeassistant/components/ring/diagnostics.py +++ b/homeassistant/components/ring/diagnostics.py @@ -4,12 +4,11 @@ from __future__ import annotations from typing import Any -from ring_doorbell import Ring - from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from . import RingData from .const import DOMAIN TO_REDACT = { @@ -33,11 +32,12 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - ring: Ring = hass.data[DOMAIN][entry.entry_id]["api"] + ring_data: RingData = hass.data[DOMAIN][entry.entry_id] + devices_data = ring_data.api.devices_data devices_raw = [ - ring.devices_data[device_type][device_id] - for device_type in ring.devices_data - for device_id in ring.devices_data[device_type] + devices_data[device_type][device_id] + for device_type in devices_data + for device_id in devices_data[device_type] ] return async_redact_data( {"device_data": devices_raw}, diff --git a/homeassistant/components/ring/entity.py b/homeassistant/components/ring/entity.py index fb617ecd7d1..54f76a19c5d 100644 --- a/homeassistant/components/ring/entity.py +++ b/homeassistant/components/ring/entity.py @@ -3,7 +3,13 @@ from collections.abc import Callable from typing import Any, Concatenate, ParamSpec, TypeVar -from ring_doorbell import AuthenticationError, RingError, RingGeneric, RingTimeout +from ring_doorbell import ( + AuthenticationError, + RingDevices, + RingError, + RingGeneric, + RingTimeout, +) from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError @@ -11,26 +17,23 @@ from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ATTRIBUTION, DOMAIN -from .coordinator import ( - RingDataCoordinator, - RingDeviceData, - RingNotificationsCoordinator, -) +from .coordinator import RingDataCoordinator, RingNotificationsCoordinator _RingCoordinatorT = TypeVar( "_RingCoordinatorT", bound=(RingDataCoordinator | RingNotificationsCoordinator), ) -_T = TypeVar("_T", bound="RingEntity") +_RingBaseEntityT = TypeVar("_RingBaseEntityT", bound="RingBaseEntity[Any]") +_R = TypeVar("_R") _P = ParamSpec("_P") def exception_wrap( - func: Callable[Concatenate[_T, _P], Any], -) -> Callable[Concatenate[_T, _P], Any]: + func: Callable[Concatenate[_RingBaseEntityT, _P], _R], +) -> Callable[Concatenate[_RingBaseEntityT, _P], _R]: """Define a wrapper to catch exceptions and raise HomeAssistant errors.""" - def _wrap(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None: + def _wrap(self: _RingBaseEntityT, *args: _P.args, **kwargs: _P.kwargs) -> _R: try: return func(self, *args, **kwargs) except AuthenticationError as err: @@ -50,7 +53,7 @@ def exception_wrap( return _wrap -class RingEntity(CoordinatorEntity[_RingCoordinatorT]): +class RingBaseEntity(CoordinatorEntity[_RingCoordinatorT]): """Base implementation for Ring device.""" _attr_attribution = ATTRIBUTION @@ -73,29 +76,16 @@ class RingEntity(CoordinatorEntity[_RingCoordinatorT]): name=device.name, ) - def _get_coordinator_device_data(self) -> RingDeviceData | None: - if (data := self.coordinator.data) and ( - device_data := data.get(self._device.id) - ): - return device_data - return None - def _get_coordinator_device(self) -> RingGeneric | None: - if (device_data := self._get_coordinator_device_data()) and ( - device := device_data.device - ): - return device - return None +class RingEntity(RingBaseEntity[RingDataCoordinator]): + """Implementation for Ring devices.""" - def _get_coordinator_history(self) -> list | None: - if (device_data := self._get_coordinator_device_data()) and ( - history := device_data.history - ): - return history - return None + def _get_coordinator_data(self) -> RingDevices: + return self.coordinator.data @callback def _handle_coordinator_update(self) -> None: - if device := self._get_coordinator_device(): - self._device = device + self._device = self._get_coordinator_data().get_device( + self._device.device_api_id + ) super()._handle_coordinator_update() diff --git a/homeassistant/components/ring/light.py b/homeassistant/components/ring/light.py index b9e1c8c38b4..a4eb8df5b46 100644 --- a/homeassistant/components/ring/light.py +++ b/homeassistant/components/ring/light.py @@ -1,6 +1,7 @@ """Component providing HA switch support for Ring Door Bell/Chimes.""" from datetime import timedelta +from enum import StrEnum, auto import logging from typing import Any @@ -12,7 +13,8 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util -from .const import DOMAIN, RING_DEVICES, RING_DEVICES_COORDINATOR +from . import RingData +from .const import DOMAIN from .coordinator import RingDataCoordinator from .entity import RingEntity, exception_wrap @@ -26,8 +28,12 @@ _LOGGER = logging.getLogger(__name__) SKIP_UPDATES_DELAY = timedelta(seconds=5) -ON_STATE = "on" -OFF_STATE = "off" + +class OnOffState(StrEnum): + """Enum for allowed on off states.""" + + ON = auto() + OFF = auto() async def async_setup_entry( @@ -36,14 +42,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Create the lights for the Ring devices.""" - devices = hass.data[DOMAIN][config_entry.entry_id][RING_DEVICES] - devices_coordinator: RingDataCoordinator = hass.data[DOMAIN][config_entry.entry_id][ - RING_DEVICES_COORDINATOR - ] + ring_data: RingData = hass.data[DOMAIN][config_entry.entry_id] + devices_coordinator = ring_data.devices_coordinator async_add_entities( RingLight(device, devices_coordinator) - for device in devices["stickup_cams"] + for device in ring_data.devices.stickup_cams if device.has_capability("light") ) @@ -55,37 +59,41 @@ class RingLight(RingEntity, LightEntity): _attr_supported_color_modes = {ColorMode.ONOFF} _attr_translation_key = "light" - def __init__(self, device, coordinator): + _device: RingStickUpCam + + def __init__( + self, device: RingStickUpCam, coordinator: RingDataCoordinator + ) -> None: """Initialize the light.""" super().__init__(device, coordinator) self._attr_unique_id = str(device.id) - self._attr_is_on = device.lights == ON_STATE + self._attr_is_on = device.lights == OnOffState.ON self._no_updates_until = dt_util.utcnow() @callback - def _handle_coordinator_update(self): + def _handle_coordinator_update(self) -> None: """Call update method.""" if self._no_updates_until > dt_util.utcnow(): return - if (device := self._get_coordinator_device()) and isinstance( - device, RingStickUpCam - ): - self._attr_is_on = device.lights == ON_STATE + device = self._get_coordinator_data().get_stickup_cam( + self._device.device_api_id + ) + self._attr_is_on = device.lights == OnOffState.ON super()._handle_coordinator_update() @exception_wrap - def _set_light(self, new_state): + def _set_light(self, new_state: OnOffState) -> None: """Update light state, and causes Home Assistant to correctly update.""" self._device.lights = new_state - self._attr_is_on = new_state == ON_STATE + self._attr_is_on = new_state == OnOffState.ON self._no_updates_until = dt_util.utcnow() + SKIP_UPDATES_DELAY self.schedule_update_ha_state() def turn_on(self, **kwargs: Any) -> None: """Turn the light on for 30 seconds.""" - self._set_light(ON_STATE) + self._set_light(OnOffState.ON) def turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" - self._set_light(OFF_STATE) + self._set_light(OnOffState.OFF) diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index 9ba677e7e5b..0c4d1f4fdf5 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -2,10 +2,19 @@ from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass -from typing import Any +from typing import Any, Generic, cast -from ring_doorbell import RingGeneric +from ring_doorbell import ( + RingCapability, + RingChime, + RingDoorBell, + RingEventKind, + RingGeneric, + RingOther, +) +from typing_extensions import TypeVar from homeassistant.components.sensor import ( SensorDeviceClass, @@ -21,11 +30,15 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType -from .const import DOMAIN, RING_DEVICES, RING_DEVICES_COORDINATOR +from . import RingData +from .const import DOMAIN from .coordinator import RingDataCoordinator from .entity import RingEntity +_RingDeviceT = TypeVar("_RingDeviceT", bound=RingGeneric, default=RingGeneric) + async def async_setup_entry( hass: HomeAssistant, @@ -33,209 +46,193 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up a sensor for a Ring device.""" - devices = hass.data[DOMAIN][config_entry.entry_id][RING_DEVICES] - devices_coordinator: RingDataCoordinator = hass.data[DOMAIN][config_entry.entry_id][ - RING_DEVICES_COORDINATOR - ] + ring_data: RingData = hass.data[DOMAIN][config_entry.entry_id] + devices_coordinator = ring_data.devices_coordinator entities = [ - description.cls(device, devices_coordinator, description) - for device_type in ( - "chimes", - "doorbots", - "authorized_doorbots", - "stickup_cams", - "other", - ) + RingSensor(device, devices_coordinator, description) for description in SENSOR_TYPES - if device_type in description.category - for device in devices[device_type] - if not (device_type == "battery" and device.battery_life is None) + for device in ring_data.devices.all_devices + if description.exists_fn(device) ] async_add_entities(entities) -class RingSensor(RingEntity, SensorEntity): +class RingSensor(RingEntity, SensorEntity, Generic[_RingDeviceT]): """A sensor implementation for Ring device.""" - entity_description: RingSensorEntityDescription + entity_description: RingSensorEntityDescription[_RingDeviceT] + _device: _RingDeviceT def __init__( self, device: RingGeneric, coordinator: RingDataCoordinator, - description: RingSensorEntityDescription, + description: RingSensorEntityDescription[_RingDeviceT], ) -> None: """Initialize a sensor for Ring device.""" super().__init__(device, coordinator) self.entity_description = description self._attr_unique_id = f"{device.id}-{description.key}" - - @property - def native_value(self): - """Return the state of the sensor.""" - sensor_type = self.entity_description.key - if sensor_type == "volume": - return self._device.volume - if sensor_type == "doorbell_volume": - return self._device.doorbell_volume - if sensor_type == "mic_volume": - return self._device.mic_volume - if sensor_type == "voice_volume": - return self._device.voice_volume - - if sensor_type == "battery": - return self._device.battery_life - - -class HealthDataRingSensor(RingSensor): - """Ring sensor that relies on health data.""" - - # These sensors are data hungry and not useful. Disable by default. - _attr_entity_registry_enabled_default = False - - @property - def native_value(self): - """Return the state of the sensor.""" - sensor_type = self.entity_description.key - if sensor_type == "wifi_signal_category": - return self._device.wifi_signal_category - - if sensor_type == "wifi_signal_strength": - return self._device.wifi_signal_strength - - -class HistoryRingSensor(RingSensor): - """Ring sensor that relies on history data.""" - - _latest_event: dict[str, Any] | None = None + self._attr_entity_registry_enabled_default = ( + description.entity_registry_enabled_default + ) + self._attr_native_value = self.entity_description.value_fn(self._device) @callback - def _handle_coordinator_update(self): + def _handle_coordinator_update(self) -> None: """Call update method.""" - if not (history_data := self._get_coordinator_history()): - return - kind = self.entity_description.kind - found = None - if kind is None: - found = history_data[0] - else: - for entry in history_data: - if entry["kind"] == kind: - found = entry - break - - if not found: - return - - self._latest_event = found + self._device = cast( + _RingDeviceT, + self._get_coordinator_data().get_device(self._device.device_api_id), + ) + # History values can drop off the last 10 events so only update + # the value if it's not None + if native_value := self.entity_description.value_fn(self._device): + self._attr_native_value = native_value + if extra_attrs := self.entity_description.extra_state_attributes_fn( + self._device + ): + self._attr_extra_state_attributes = extra_attrs super()._handle_coordinator_update() - @property - def native_value(self): - """Return the state of the sensor.""" - if self._latest_event is None: - return None - return self._latest_event["created_at"] +def _get_last_event( + history_data: list[dict[str, Any]], kind: RingEventKind | None +) -> dict[str, Any] | None: + if not history_data: + return None + if kind is None: + return history_data[0] + for entry in history_data: + if entry["kind"] == kind.value: + return entry + return None - @property - def extra_state_attributes(self): - """Return the state attributes.""" - attrs = super().extra_state_attributes - if self._latest_event: - attrs["created_at"] = self._latest_event["created_at"] - attrs["answered"] = self._latest_event["answered"] - attrs["recording_status"] = self._latest_event["recording"]["status"] - attrs["category"] = self._latest_event["kind"] - - return attrs +def _get_last_event_attrs( + history_data: list[dict[str, Any]], kind: RingEventKind | None +) -> dict[str, Any] | None: + if last_event := _get_last_event(history_data, kind): + return { + "created_at": last_event.get("created_at"), + "answered": last_event.get("answered"), + "recording_status": last_event.get("recording", {}).get("status"), + "category": last_event.get("kind"), + } + return None @dataclass(frozen=True, kw_only=True) -class RingSensorEntityDescription(SensorEntityDescription): +class RingSensorEntityDescription(SensorEntityDescription, Generic[_RingDeviceT]): """Describes Ring sensor entity.""" - category: list[str] - cls: type[RingSensor] - - kind: str | None = None + value_fn: Callable[[_RingDeviceT], StateType] = lambda _: True + exists_fn: Callable[[RingGeneric], bool] = lambda _: True + extra_state_attributes_fn: Callable[[_RingDeviceT], dict[str, Any] | None] = ( + lambda _: None + ) -SENSOR_TYPES: tuple[RingSensorEntityDescription, ...] = ( - RingSensorEntityDescription( +# For some reason mypy doesn't properly type check the default TypeVar value here +# so for now the [RingGeneric] subscript needs to be specified. +# Once https://github.com/python/mypy/issues/14851 is closed this should hopefully +# be fixed and the [RingGeneric] subscript can be removed. +# https://github.com/home-assistant/core/pull/115276#discussion_r1560106576 +SENSOR_TYPES: tuple[RingSensorEntityDescription[Any], ...] = ( + RingSensorEntityDescription[RingGeneric]( key="battery", - category=["doorbots", "authorized_doorbots", "stickup_cams", "other"], native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, - cls=RingSensor, + value_fn=lambda device: device.battery_life, + exists_fn=lambda device: device.family != "chimes", ), - RingSensorEntityDescription( + RingSensorEntityDescription[RingGeneric]( key="last_activity", translation_key="last_activity", - category=["doorbots", "authorized_doorbots", "stickup_cams", "other"], device_class=SensorDeviceClass.TIMESTAMP, - cls=HistoryRingSensor, + value_fn=lambda device: last_event.get("created_at") + if (last_event := _get_last_event(device.last_history, None)) + else None, + extra_state_attributes_fn=lambda device: last_event_attrs + if (last_event_attrs := _get_last_event_attrs(device.last_history, None)) + else None, + exists_fn=lambda device: device.has_capability(RingCapability.HISTORY), ), - RingSensorEntityDescription( + RingSensorEntityDescription[RingGeneric]( key="last_ding", translation_key="last_ding", - category=["doorbots", "authorized_doorbots", "other"], - kind="ding", device_class=SensorDeviceClass.TIMESTAMP, - cls=HistoryRingSensor, + value_fn=lambda device: last_event.get("created_at") + if (last_event := _get_last_event(device.last_history, RingEventKind.DING)) + else None, + extra_state_attributes_fn=lambda device: last_event_attrs + if ( + last_event_attrs := _get_last_event_attrs( + device.last_history, RingEventKind.DING + ) + ) + else None, + exists_fn=lambda device: device.has_capability(RingCapability.HISTORY), ), - RingSensorEntityDescription( + RingSensorEntityDescription[RingGeneric]( key="last_motion", translation_key="last_motion", - category=["doorbots", "authorized_doorbots", "stickup_cams"], - kind="motion", device_class=SensorDeviceClass.TIMESTAMP, - cls=HistoryRingSensor, + value_fn=lambda device: last_event.get("created_at") + if (last_event := _get_last_event(device.last_history, RingEventKind.MOTION)) + else None, + extra_state_attributes_fn=lambda device: last_event_attrs + if ( + last_event_attrs := _get_last_event_attrs( + device.last_history, RingEventKind.MOTION + ) + ) + else None, + exists_fn=lambda device: device.has_capability(RingCapability.HISTORY), ), - RingSensorEntityDescription( + RingSensorEntityDescription[RingDoorBell | RingChime]( key="volume", translation_key="volume", - category=["chimes", "doorbots", "authorized_doorbots", "stickup_cams"], - cls=RingSensor, + value_fn=lambda device: device.volume, + exists_fn=lambda device: isinstance(device, (RingDoorBell, RingChime)), ), - RingSensorEntityDescription( + RingSensorEntityDescription[RingOther]( key="doorbell_volume", translation_key="doorbell_volume", - category=["other"], - cls=RingSensor, + value_fn=lambda device: device.doorbell_volume, + exists_fn=lambda device: isinstance(device, RingOther), ), - RingSensorEntityDescription( + RingSensorEntityDescription[RingOther]( key="mic_volume", translation_key="mic_volume", - category=["other"], - cls=RingSensor, + value_fn=lambda device: device.mic_volume, + exists_fn=lambda device: isinstance(device, RingOther), ), - RingSensorEntityDescription( + RingSensorEntityDescription[RingOther]( key="voice_volume", translation_key="voice_volume", - category=["other"], - cls=RingSensor, + value_fn=lambda device: device.voice_volume, + exists_fn=lambda device: isinstance(device, RingOther), ), - RingSensorEntityDescription( + RingSensorEntityDescription[RingGeneric]( key="wifi_signal_category", translation_key="wifi_signal_category", - category=["chimes", "doorbots", "authorized_doorbots", "stickup_cams", "other"], entity_category=EntityCategory.DIAGNOSTIC, - cls=HealthDataRingSensor, + entity_registry_enabled_default=False, + value_fn=lambda device: device.wifi_signal_category, ), - RingSensorEntityDescription( + RingSensorEntityDescription[RingGeneric]( key="wifi_signal_strength", translation_key="wifi_signal_strength", - category=["chimes", "doorbots", "authorized_doorbots", "stickup_cams", "other"], native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, entity_category=EntityCategory.DIAGNOSTIC, - cls=HealthDataRingSensor, + entity_registry_enabled_default=False, + value_fn=lambda device: device.wifi_signal_strength, ), ) diff --git a/homeassistant/components/ring/siren.py b/homeassistant/components/ring/siren.py index 4b7d9243dbf..27f68258bad 100644 --- a/homeassistant/components/ring/siren.py +++ b/homeassistant/components/ring/siren.py @@ -1,15 +1,17 @@ """Component providing HA Siren support for Ring Chimes.""" import logging +from typing import Any -from ring_doorbell.const import CHIME_TEST_SOUND_KINDS, KIND_DING +from ring_doorbell import RingChime, RingEventKind from homeassistant.components.siren import ATTR_TONE, SirenEntity, SirenEntityFeature from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN, RING_DEVICES, RING_DEVICES_COORDINATOR +from . import RingData +from .const import DOMAIN from .coordinator import RingDataCoordinator from .entity import RingEntity, exception_wrap @@ -22,32 +24,33 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Create the sirens for the Ring devices.""" - devices = hass.data[DOMAIN][config_entry.entry_id][RING_DEVICES] - coordinator: RingDataCoordinator = hass.data[DOMAIN][config_entry.entry_id][ - RING_DEVICES_COORDINATOR - ] + ring_data: RingData = hass.data[DOMAIN][config_entry.entry_id] + devices_coordinator = ring_data.devices_coordinator async_add_entities( - RingChimeSiren(device, coordinator) for device in devices["chimes"] + RingChimeSiren(device, devices_coordinator) + for device in ring_data.devices.chimes ) class RingChimeSiren(RingEntity, SirenEntity): """Creates a siren to play the test chimes of a Chime device.""" - _attr_available_tones = list(CHIME_TEST_SOUND_KINDS) + _attr_available_tones = [RingEventKind.DING.value, RingEventKind.MOTION.value] _attr_supported_features = SirenEntityFeature.TURN_ON | SirenEntityFeature.TONES _attr_translation_key = "siren" - def __init__(self, device, coordinator: RingDataCoordinator) -> None: + _device: RingChime + + def __init__(self, device: RingChime, coordinator: RingDataCoordinator) -> None: """Initialize a Ring Chime siren.""" super().__init__(device, coordinator) # Entity class attributes self._attr_unique_id = f"{self._device.id}-siren" @exception_wrap - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Play the test sound on a Ring Chime device.""" - tone = kwargs.get(ATTR_TONE) or KIND_DING + tone = kwargs.get(ATTR_TONE) or RingEventKind.DING.value self._device.test_sound(kind=tone) diff --git a/homeassistant/components/ring/switch.py b/homeassistant/components/ring/switch.py index 2221eeb7705..b5cd59ac2fb 100644 --- a/homeassistant/components/ring/switch.py +++ b/homeassistant/components/ring/switch.py @@ -4,7 +4,7 @@ from datetime import timedelta import logging from typing import Any -from ring_doorbell import RingGeneric, RingStickUpCam +from ring_doorbell import RingStickUpCam from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry @@ -12,7 +12,8 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util -from .const import DOMAIN, RING_DEVICES, RING_DEVICES_COORDINATOR +from . import RingData +from .const import DOMAIN from .coordinator import RingDataCoordinator from .entity import RingEntity, exception_wrap @@ -33,14 +34,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Create the switches for the Ring devices.""" - devices = hass.data[DOMAIN][config_entry.entry_id][RING_DEVICES] - coordinator: RingDataCoordinator = hass.data[DOMAIN][config_entry.entry_id][ - RING_DEVICES_COORDINATOR - ] + ring_data: RingData = hass.data[DOMAIN][config_entry.entry_id] + devices_coordinator = ring_data.devices_coordinator async_add_entities( - SirenSwitch(device, coordinator) - for device in devices["stickup_cams"] + SirenSwitch(device, devices_coordinator) + for device in ring_data.devices.stickup_cams if device.has_capability("siren") ) @@ -48,8 +47,10 @@ async def async_setup_entry( class BaseRingSwitch(RingEntity, SwitchEntity): """Represents a switch for controlling an aspect of a ring device.""" + _device: RingStickUpCam + def __init__( - self, device: RingGeneric, coordinator: RingDataCoordinator, device_type: str + self, device: RingStickUpCam, coordinator: RingDataCoordinator, device_type: str ) -> None: """Initialize the switch.""" super().__init__(device, coordinator) @@ -62,26 +63,27 @@ class SirenSwitch(BaseRingSwitch): _attr_translation_key = "siren" - def __init__(self, device, coordinator: RingDataCoordinator) -> None: + def __init__( + self, device: RingStickUpCam, coordinator: RingDataCoordinator + ) -> None: """Initialize the switch for a device with a siren.""" super().__init__(device, coordinator, "siren") self._no_updates_until = dt_util.utcnow() self._attr_is_on = device.siren > 0 @callback - def _handle_coordinator_update(self): + def _handle_coordinator_update(self) -> None: """Call update method.""" if self._no_updates_until > dt_util.utcnow(): return - - if (device := self._get_coordinator_device()) and isinstance( - device, RingStickUpCam - ): - self._attr_is_on = device.siren > 0 + device = self._get_coordinator_data().get_stickup_cam( + self._device.device_api_id + ) + self._attr_is_on = device.siren > 0 super()._handle_coordinator_update() @exception_wrap - def _set_switch(self, new_state): + def _set_switch(self, new_state: int) -> None: """Update switch state, and causes Home Assistant to correctly update.""" self._device.siren = new_state diff --git a/mypy.ini b/mypy.ini index 159101a21b3..3e0419be269 100644 --- a/mypy.ini +++ b/mypy.ini @@ -3391,6 +3391,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.ring.*] +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.rituals_perfume_genie.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/tests/components/ring/common.py b/tests/components/ring/common.py index c6852bf87d6..b129623aa95 100644 --- a/tests/components/ring/common.py +++ b/tests/components/ring/common.py @@ -15,4 +15,4 @@ async def setup_platform(hass, platform): ) with patch("homeassistant.components.ring.PLATFORMS", [platform]): assert await async_setup_component(hass, DOMAIN, {}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) From 5308e02c992b5f39aabc4e0252e6af200531294a Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Thu, 11 Apr 2024 05:23:10 -0400 Subject: [PATCH 488/967] Add support for adopt data disk repair (#114891) --- homeassistant/components/hassio/repairs.py | 2 +- homeassistant/components/hassio/strings.json | 11 +- tests/components/hassio/test_repairs.py | 113 +++++++++++++++++++ 3 files changed, 123 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hassio/repairs.py b/homeassistant/components/hassio/repairs.py index 8458d7eaac2..63ed3d5c8a3 100644 --- a/homeassistant/components/hassio/repairs.py +++ b/homeassistant/components/hassio/repairs.py @@ -22,7 +22,7 @@ from .const import ( from .handler import async_apply_suggestion from .issues import Issue, Suggestion -SUGGESTION_CONFIRMATION_REQUIRED = {"system_execute_reboot"} +SUGGESTION_CONFIRMATION_REQUIRED = {"system_adopt_data_disk", "system_execute_reboot"} EXTRA_PLACEHOLDERS = { "issue_mount_mount_failed": { diff --git a/homeassistant/components/hassio/strings.json b/homeassistant/components/hassio/strings.json index 77ef408cafe..63c1da4bfd8 100644 --- a/homeassistant/components/hassio/strings.json +++ b/homeassistant/components/hassio/strings.json @@ -51,8 +51,15 @@ "title": "Multiple data disks detected", "fix_flow": { "step": { - "system_rename_data_disk": { - "description": "`{reference}` is a filesystem with the name hassos-data and is not the active data disk. This can cause Home Assistant to choose the wrong data disk at system reboot.\n\nUse the fix option to rename the filesystem to prevent this. Alternatively you can move the data disk to the drive (overwriting its contents) or remove the drive from the system." + "fix_menu": { + "description": "`{reference}` is a filesystem with the name hassos-data and is not the active data disk. This can cause Home Assistant to choose the wrong data disk at system reboot.\n\nUse the 'Rename' option to rename the filesystem to prevent this. Use the 'Adopt' option to make that your data disk and rename the existing one. Alternatively you can move the data disk to the drive (overwriting its contents) or remove the drive from the system.", + "menu_options": { + "system_rename_data_disk": "Rename", + "system_adopt_data_disk": "Adopt" + } + }, + "system_adopt_data_disk": { + "description": "This fix will initiate a system reboot which will make Home Assistant and all the Add-ons inaccessible for a brief period. After the reboot `{reference}` will be the data disk of Home Assistant and your existing data disk will be renamed and ignored." } }, "abort": { diff --git a/tests/components/hassio/test_repairs.py b/tests/components/hassio/test_repairs.py index d387968da46..2dffba74fef 100644 --- a/tests/components/hassio/test_repairs.py +++ b/tests/components/hassio/test_repairs.py @@ -674,3 +674,116 @@ async def test_supervisor_issue_docker_config_repair_flow( str(aioclient_mock.mock_calls[-1][1]) == "http://127.0.0.1/resolution/suggestion/1235" ) + + +async def test_supervisor_issue_repair_flow_multiple_data_disks( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + hass_client: ClientSessionGenerator, + issue_registry: ir.IssueRegistry, + all_setup_requests, +) -> None: + """Test fix flow for multiple data disks supervisor issue.""" + mock_resolution_info( + aioclient_mock, + issues=[ + { + "uuid": "1234", + "type": "multiple_data_disks", + "context": "system", + "reference": "/dev/sda1", + "suggestions": [ + { + "uuid": "1235", + "type": "rename_data_disk", + "context": "system", + "reference": "/dev/sda1", + }, + { + "uuid": "1236", + "type": "adopt_data_disk", + "context": "system", + "reference": "/dev/sda1", + }, + ], + }, + ], + ) + + assert await async_setup_component(hass, "hassio", {}) + + repair_issue = issue_registry.async_get_issue(domain="hassio", issue_id="1234") + assert repair_issue + + client = await hass_client() + + resp = await client.post( + "/api/repairs/issues/fix", + json={"handler": "hassio", "issue_id": repair_issue.issue_id}, + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data == { + "type": "menu", + "flow_id": flow_id, + "handler": "hassio", + "step_id": "fix_menu", + "data_schema": [ + { + "type": "select", + "options": [ + ["system_rename_data_disk", "system_rename_data_disk"], + ["system_adopt_data_disk", "system_adopt_data_disk"], + ], + "name": "next_step_id", + } + ], + "menu_options": ["system_rename_data_disk", "system_adopt_data_disk"], + "description_placeholders": {"reference": "/dev/sda1"}, + } + + resp = await client.post( + f"/api/repairs/issues/fix/{flow_id}", + json={"next_step_id": "system_adopt_data_disk"}, + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data == { + "type": "form", + "flow_id": flow_id, + "handler": "hassio", + "step_id": "system_adopt_data_disk", + "data_schema": [], + "errors": None, + "description_placeholders": {"reference": "/dev/sda1"}, + "last_step": True, + "preview": None, + } + + resp = await client.post(f"/api/repairs/issues/fix/{flow_id}") + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data == { + "type": "create_entry", + "flow_id": flow_id, + "handler": "hassio", + "description": None, + "description_placeholders": None, + } + + assert not issue_registry.async_get_issue(domain="hassio", issue_id="1234") + + assert aioclient_mock.mock_calls[-1][0] == "post" + assert ( + str(aioclient_mock.mock_calls[-1][1]) + == "http://127.0.0.1/resolution/suggestion/1236" + ) From df5d818c0896d9011cc17a2ab9c757c8f3e9b759 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Thu, 11 Apr 2024 10:31:37 +0100 Subject: [PATCH 489/967] Make ring device generic in RingEntity (#115406) Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- homeassistant/components/ring/button.py | 4 +--- homeassistant/components/ring/camera.py | 3 +-- homeassistant/components/ring/entity.py | 20 +++++++++++++------- homeassistant/components/ring/light.py | 4 +--- homeassistant/components/ring/sensor.py | 22 +++++++++------------- homeassistant/components/ring/siren.py | 4 +--- homeassistant/components/ring/switch.py | 4 +--- 7 files changed, 27 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/ring/button.py b/homeassistant/components/ring/button.py index a14853a0881..15d56a8b7cf 100644 --- a/homeassistant/components/ring/button.py +++ b/homeassistant/components/ring/button.py @@ -35,11 +35,9 @@ async def async_setup_entry( ) -class RingDoorButton(RingEntity, ButtonEntity): +class RingDoorButton(RingEntity[RingOther], ButtonEntity): """Creates a button to open the ring intercom door.""" - _device: RingOther - def __init__( self, device: RingOther, diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index 297e5f47627..282f9816c4c 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -48,11 +48,10 @@ async def async_setup_entry( async_add_entities(cams) -class RingCam(RingEntity, Camera): +class RingCam(RingEntity[RingDoorBell], Camera): """An implementation of a Ring Door Bell camera.""" _attr_name = None - _device: RingDoorBell def __init__( self, diff --git a/homeassistant/components/ring/entity.py b/homeassistant/components/ring/entity.py index 54f76a19c5d..65ccbb8ece4 100644 --- a/homeassistant/components/ring/entity.py +++ b/homeassistant/components/ring/entity.py @@ -1,7 +1,7 @@ """Base class for Ring entity.""" from collections.abc import Callable -from typing import Any, Concatenate, ParamSpec, TypeVar +from typing import Any, Concatenate, Generic, ParamSpec, cast from ring_doorbell import ( AuthenticationError, @@ -10,6 +10,7 @@ from ring_doorbell import ( RingGeneric, RingTimeout, ) +from typing_extensions import TypeVar from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError @@ -19,11 +20,13 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ATTRIBUTION, DOMAIN from .coordinator import RingDataCoordinator, RingNotificationsCoordinator +RingDeviceT = TypeVar("RingDeviceT", bound=RingGeneric, default=RingGeneric) + _RingCoordinatorT = TypeVar( "_RingCoordinatorT", bound=(RingDataCoordinator | RingNotificationsCoordinator), ) -_RingBaseEntityT = TypeVar("_RingBaseEntityT", bound="RingBaseEntity[Any]") +_RingBaseEntityT = TypeVar("_RingBaseEntityT", bound="RingBaseEntity[Any, Any]") _R = TypeVar("_R") _P = ParamSpec("_P") @@ -53,7 +56,9 @@ def exception_wrap( return _wrap -class RingBaseEntity(CoordinatorEntity[_RingCoordinatorT]): +class RingBaseEntity( + CoordinatorEntity[_RingCoordinatorT], Generic[_RingCoordinatorT, RingDeviceT] +): """Base implementation for Ring device.""" _attr_attribution = ATTRIBUTION @@ -62,7 +67,7 @@ class RingBaseEntity(CoordinatorEntity[_RingCoordinatorT]): def __init__( self, - device: RingGeneric, + device: RingDeviceT, coordinator: _RingCoordinatorT, ) -> None: """Initialize a sensor for Ring device.""" @@ -77,7 +82,7 @@ class RingBaseEntity(CoordinatorEntity[_RingCoordinatorT]): ) -class RingEntity(RingBaseEntity[RingDataCoordinator]): +class RingEntity(RingBaseEntity[RingDataCoordinator, RingDeviceT]): """Implementation for Ring devices.""" def _get_coordinator_data(self) -> RingDevices: @@ -85,7 +90,8 @@ class RingEntity(RingBaseEntity[RingDataCoordinator]): @callback def _handle_coordinator_update(self) -> None: - self._device = self._get_coordinator_data().get_device( - self._device.device_api_id + self._device = cast( + RingDeviceT, + self._get_coordinator_data().get_device(self._device.device_api_id), ) super()._handle_coordinator_update() diff --git a/homeassistant/components/ring/light.py b/homeassistant/components/ring/light.py index a4eb8df5b46..5747c9e77f7 100644 --- a/homeassistant/components/ring/light.py +++ b/homeassistant/components/ring/light.py @@ -52,15 +52,13 @@ async def async_setup_entry( ) -class RingLight(RingEntity, LightEntity): +class RingLight(RingEntity[RingStickUpCam], LightEntity): """Creates a switch to turn the ring cameras light on and off.""" _attr_color_mode = ColorMode.ONOFF _attr_supported_color_modes = {ColorMode.ONOFF} _attr_translation_key = "light" - _device: RingStickUpCam - def __init__( self, device: RingStickUpCam, coordinator: RingDataCoordinator ) -> None: diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index 0c4d1f4fdf5..b6849e37d96 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -14,7 +14,6 @@ from ring_doorbell import ( RingGeneric, RingOther, ) -from typing_extensions import TypeVar from homeassistant.components.sensor import ( SensorDeviceClass, @@ -35,9 +34,7 @@ from homeassistant.helpers.typing import StateType from . import RingData from .const import DOMAIN from .coordinator import RingDataCoordinator -from .entity import RingEntity - -_RingDeviceT = TypeVar("_RingDeviceT", bound=RingGeneric, default=RingGeneric) +from .entity import RingDeviceT, RingEntity async def async_setup_entry( @@ -59,17 +56,16 @@ async def async_setup_entry( async_add_entities(entities) -class RingSensor(RingEntity, SensorEntity, Generic[_RingDeviceT]): +class RingSensor(RingEntity[RingDeviceT], SensorEntity): """A sensor implementation for Ring device.""" - entity_description: RingSensorEntityDescription[_RingDeviceT] - _device: _RingDeviceT + entity_description: RingSensorEntityDescription[RingDeviceT] def __init__( self, - device: RingGeneric, + device: RingDeviceT, coordinator: RingDataCoordinator, - description: RingSensorEntityDescription[_RingDeviceT], + description: RingSensorEntityDescription[RingDeviceT], ) -> None: """Initialize a sensor for Ring device.""" super().__init__(device, coordinator) @@ -85,7 +81,7 @@ class RingSensor(RingEntity, SensorEntity, Generic[_RingDeviceT]): """Call update method.""" self._device = cast( - _RingDeviceT, + RingDeviceT, self._get_coordinator_data().get_device(self._device.device_api_id), ) # History values can drop off the last 10 events so only update @@ -126,12 +122,12 @@ def _get_last_event_attrs( @dataclass(frozen=True, kw_only=True) -class RingSensorEntityDescription(SensorEntityDescription, Generic[_RingDeviceT]): +class RingSensorEntityDescription(SensorEntityDescription, Generic[RingDeviceT]): """Describes Ring sensor entity.""" - value_fn: Callable[[_RingDeviceT], StateType] = lambda _: True + value_fn: Callable[[RingDeviceT], StateType] = lambda _: True exists_fn: Callable[[RingGeneric], bool] = lambda _: True - extra_state_attributes_fn: Callable[[_RingDeviceT], dict[str, Any] | None] = ( + extra_state_attributes_fn: Callable[[RingDeviceT], dict[str, Any] | None] = ( lambda _: None ) diff --git a/homeassistant/components/ring/siren.py b/homeassistant/components/ring/siren.py index 27f68258bad..f63f9d33182 100644 --- a/homeassistant/components/ring/siren.py +++ b/homeassistant/components/ring/siren.py @@ -33,15 +33,13 @@ async def async_setup_entry( ) -class RingChimeSiren(RingEntity, SirenEntity): +class RingChimeSiren(RingEntity[RingChime], SirenEntity): """Creates a siren to play the test chimes of a Chime device.""" _attr_available_tones = [RingEventKind.DING.value, RingEventKind.MOTION.value] _attr_supported_features = SirenEntityFeature.TURN_ON | SirenEntityFeature.TONES _attr_translation_key = "siren" - _device: RingChime - def __init__(self, device: RingChime, coordinator: RingDataCoordinator) -> None: """Initialize a Ring Chime siren.""" super().__init__(device, coordinator) diff --git a/homeassistant/components/ring/switch.py b/homeassistant/components/ring/switch.py index b5cd59ac2fb..0e032907bae 100644 --- a/homeassistant/components/ring/switch.py +++ b/homeassistant/components/ring/switch.py @@ -44,11 +44,9 @@ async def async_setup_entry( ) -class BaseRingSwitch(RingEntity, SwitchEntity): +class BaseRingSwitch(RingEntity[RingStickUpCam], SwitchEntity): """Represents a switch for controlling an aspect of a ring device.""" - _device: RingStickUpCam - def __init__( self, device: RingStickUpCam, coordinator: RingDataCoordinator, device_type: str ) -> None: From 10076e652353ac2959babb32fb790ac0630b218f Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 11 Apr 2024 12:04:08 +0200 Subject: [PATCH 490/967] Add notify entity component (#110950) * Add notify entity component * Device classes, restore state, icons * Add icons file * Add tests for kitchen_sink * Remove notify from no_entity_platforms in hassfest icons, translation link * ruff * Remove `data` feature * Only message support * Complete initial device classes * mypy pylint * Remove device_class implementation * format * Follow up comments * Remove _attr_supported_features * Use setup_test_component_platform * User helper at other places * last comment * Add entry unload test and non async test * Avoid default mutable object in constructor --- .../components/kitchen_sink/__init__.py | 3 +- .../components/kitchen_sink/notify.py | 54 ++++ homeassistant/components/notify/__init__.py | 92 ++++++- homeassistant/components/notify/const.py | 6 +- homeassistant/components/notify/icons.json | 8 +- homeassistant/components/notify/services.yaml | 10 + homeassistant/components/notify/strings.json | 15 ++ homeassistant/helpers/service.py | 2 + tests/components/kitchen_sink/test_notify.py | 66 +++++ tests/components/notify/conftest.py | 23 ++ tests/components/notify/test_init.py | 239 ++++++++++++++++-- 11 files changed, 493 insertions(+), 25 deletions(-) create mode 100644 homeassistant/components/kitchen_sink/notify.py create mode 100644 tests/components/kitchen_sink/test_notify.py create mode 100644 tests/components/notify/conftest.py diff --git a/homeassistant/components/kitchen_sink/__init__.py b/homeassistant/components/kitchen_sink/__init__.py index 6b6694c920d..94dfca77410 100644 --- a/homeassistant/components/kitchen_sink/__init__.py +++ b/homeassistant/components/kitchen_sink/__init__.py @@ -32,6 +32,7 @@ COMPONENTS_WITH_DEMO_PLATFORM = [ Platform.IMAGE, Platform.LAWN_MOWER, Platform.LOCK, + Platform.NOTIFY, Platform.SENSOR, Platform.SWITCH, Platform.WEATHER, @@ -70,7 +71,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b return True -def _create_issues(hass): +def _create_issues(hass: HomeAssistant) -> None: """Create some issue registry issues.""" async_create_issue( hass, diff --git a/homeassistant/components/kitchen_sink/notify.py b/homeassistant/components/kitchen_sink/notify.py new file mode 100644 index 00000000000..b0418411145 --- /dev/null +++ b/homeassistant/components/kitchen_sink/notify.py @@ -0,0 +1,54 @@ +"""Demo platform that offers a fake notify entity.""" + +from __future__ import annotations + +from homeassistant.components import persistent_notification +from homeassistant.components.notify import NotifyEntity +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 DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the demo notify entity platform.""" + async_add_entities( + [ + DemoNotify( + unique_id="just_notify_me", + device_name="MyBox", + entity_name="Personal notifier", + ), + ] + ) + + +class DemoNotify(NotifyEntity): + """Representation of a demo notify entity.""" + + _attr_has_entity_name = True + _attr_should_poll = False + + def __init__( + self, + unique_id: str, + device_name: str, + entity_name: str | None, + ) -> None: + """Initialize the Demo button entity.""" + self._attr_unique_id = unique_id + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + name=device_name, + ) + self._attr_name = entity_name + + async def async_send_message(self, message: str) -> None: + """Send out a persistent notification.""" + persistent_notification.async_create(self.hass, message, "Demo notification") diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index e7390a49676..81b7d300acc 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -2,24 +2,36 @@ from __future__ import annotations +from datetime import timedelta +from functools import cached_property, partial +import logging +from typing import Any, final, override + import voluptuous as vol import homeassistant.components.persistent_notification as pn -from homeassistant.const import CONF_NAME, CONF_PLATFORM +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_NAME, CONF_PLATFORM, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant, ServiceCall import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType +from homeassistant.util import dt as dt_util from .const import ( # noqa: F401 ATTR_DATA, ATTR_MESSAGE, + ATTR_RECIPIENTS, ATTR_TARGET, ATTR_TITLE, DOMAIN, NOTIFY_SERVICE_SCHEMA, SERVICE_NOTIFY, SERVICE_PERSISTENT_NOTIFICATION, + SERVICE_SEND_MESSAGE, ) from .legacy import ( # noqa: F401 BaseNotificationService, @@ -29,9 +41,17 @@ from .legacy import ( # noqa: F401 check_templates_warn, ) +# mypy: disallow-any-generics + # Platform specific data ATTR_TITLE_DEFAULT = "Home Assistant" +ENTITY_ID_FORMAT = DOMAIN + ".{}" + +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) + +_LOGGER = logging.getLogger(__name__) + PLATFORM_SCHEMA = vol.Schema( {vol.Required(CONF_PLATFORM): cv.string, vol.Optional(CONF_NAME): cv.string}, extra=vol.ALLOW_EXTRA, @@ -50,6 +70,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # legacy platforms to finish setting up. hass.async_create_task(setup, eager_start=True) + component = hass.data[DOMAIN] = EntityComponent[NotifyEntity](_LOGGER, DOMAIN, hass) + component.async_register_entity_service( + SERVICE_SEND_MESSAGE, + {vol.Required(ATTR_MESSAGE): cv.string}, + "_async_send_message", + ) + async def persistent_notification(service: ServiceCall) -> None: """Send notification via the built-in persistent_notify integration.""" message: Template = service.data[ATTR_MESSAGE] @@ -79,3 +106,66 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) return True + + +class NotifyEntityDescription(EntityDescription, frozen_or_thawed=True): + """A class that describes button entities.""" + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up a config entry.""" + component: EntityComponent[NotifyEntity] = hass.data[DOMAIN] + return await component.async_setup_entry(entry) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + component: EntityComponent[NotifyEntity] = hass.data[DOMAIN] + return await component.async_unload_entry(entry) + + +class NotifyEntity(RestoreEntity): + """Representation of a notify entity.""" + + entity_description: NotifyEntityDescription + _attr_should_poll = False + _attr_device_class: None + _attr_state: None = None + __last_notified_isoformat: str | None = None + + @cached_property + @final + @override + def state(self) -> str | None: + """Return the entity state.""" + return self.__last_notified_isoformat + + def __set_state(self, state: str | None) -> None: + """Invalidate the cache of the cached property.""" + self.__dict__.pop("state", None) + self.__last_notified_isoformat = state + + async def async_internal_added_to_hass(self) -> None: + """Call when the notify entity is added to hass.""" + await super().async_internal_added_to_hass() + state = await self.async_get_last_state() + if state is not None and state.state not in (STATE_UNAVAILABLE, None): + self.__set_state(state.state) + + @final + async def _async_send_message(self, **kwargs: Any) -> None: + """Send a notification message (from e.g., service call). + + Should not be overridden, handle setting last notification timestamp. + """ + self.__set_state(dt_util.utcnow().isoformat()) + self.async_write_ha_state() + await self.async_send_message(**kwargs) + + def send_message(self, message: str) -> None: + """Send a message.""" + raise NotImplementedError + + async def async_send_message(self, message: str) -> None: + """Send a message.""" + await self.hass.async_add_executor_job(partial(self.send_message, message)) diff --git a/homeassistant/components/notify/const.py b/homeassistant/components/notify/const.py index b653b5d1cbf..6cd957e3afe 100644 --- a/homeassistant/components/notify/const.py +++ b/homeassistant/components/notify/const.py @@ -11,9 +11,12 @@ ATTR_DATA = "data" # Text to notify user of ATTR_MESSAGE = "message" -# Target of the notification (user, device, etc) +# Target of the (legacy) notification (user, device, etc) ATTR_TARGET = "target" +# Recipients for a notification +ATTR_RECIPIENTS = "recipients" + # Title of notification ATTR_TITLE = "title" @@ -22,6 +25,7 @@ DOMAIN = "notify" LOGGER = logging.getLogger(__package__) SERVICE_NOTIFY = "notify" +SERVICE_SEND_MESSAGE = "send_message" SERVICE_PERSISTENT_NOTIFICATION = "persistent_notification" NOTIFY_SERVICE_SCHEMA = vol.Schema( diff --git a/homeassistant/components/notify/icons.json b/homeassistant/components/notify/icons.json index 88577bc2356..ace8ee0c96b 100644 --- a/homeassistant/components/notify/icons.json +++ b/homeassistant/components/notify/icons.json @@ -1,6 +1,12 @@ { + "entity_component": { + "_": { + "default": "mdi:message" + } + }, "services": { "notify": "mdi:bell-ring", - "persistent_notification": "mdi:bell-badge" + "persistent_notification": "mdi:bell-badge", + "send_message": "mdi:message-arrow-right" } } diff --git a/homeassistant/components/notify/services.yaml b/homeassistant/components/notify/services.yaml index 8d053e3af58..ae2a0254761 100644 --- a/homeassistant/components/notify/services.yaml +++ b/homeassistant/components/notify/services.yaml @@ -20,6 +20,16 @@ notify: selector: object: +send_message: + target: + entity: + domain: notify + fields: + message: + required: true + selector: + text: + persistent_notification: fields: message: diff --git a/homeassistant/components/notify/strings.json b/homeassistant/components/notify/strings.json index cff7b265c37..b0dca501509 100644 --- a/homeassistant/components/notify/strings.json +++ b/homeassistant/components/notify/strings.json @@ -1,5 +1,10 @@ { "title": "Notifications", + "entity_component": { + "_": { + "name": "[%key:component::notify::title%]" + } + }, "services": { "notify": { "name": "Send a notification", @@ -23,6 +28,16 @@ } } }, + "send_message": { + "name": "Send a notification message", + "description": "Sends a notification message.", + "fields": { + "message": { + "name": "Message", + "description": "Your notification message." + } + } + }, "persistent_notification": { "name": "Send a persistent notification", "description": "Sends a notification that is visible in the **Notifications** panel.", diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 9af02402bc0..31e0d3648db 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -93,6 +93,7 @@ def _base_components() -> dict[str, ModuleType]: light, lock, media_player, + notify, remote, siren, todo, @@ -112,6 +113,7 @@ def _base_components() -> dict[str, ModuleType]: "light": light, "lock": lock, "media_player": media_player, + "notify": notify, "remote": remote, "siren": siren, "todo": todo, diff --git a/tests/components/kitchen_sink/test_notify.py b/tests/components/kitchen_sink/test_notify.py new file mode 100644 index 00000000000..6d02bacb7be --- /dev/null +++ b/tests/components/kitchen_sink/test_notify.py @@ -0,0 +1,66 @@ +"""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 homeassistant.components.kitchen_sink import DOMAIN +from homeassistant.components.notify import ( + 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 +from homeassistant.util import dt as dt_util + +ENTITY_DIRECT_MESSAGE = "notify.mybox_personal_notifier" + + +@pytest.fixture +async def notify_only() -> AsyncGenerator[None, None]: + """Enable only the button platform.""" + with patch( + "homeassistant.components.kitchen_sink.COMPONENTS_WITH_DEMO_PLATFORM", + [Platform.NOTIFY], + ): + yield + + +@pytest.fixture(autouse=True) +async def setup_comp(hass: HomeAssistant, notify_only: None): + """Set up demo component.""" + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + + +def test_setup_params(hass: HomeAssistant) -> None: + """Test the initial parameters.""" + state = hass.states.get(ENTITY_DIRECT_MESSAGE) + assert state + assert state.state == STATE_UNKNOWN + + +async def test_send_message( + hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: + """Test pressing the button.""" + state = hass.states.get(ENTITY_DIRECT_MESSAGE) + assert state + assert state.state == STATE_UNKNOWN + + now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") + freezer.move_to(now) + await hass.services.async_call( + NOTIFY_DOMAIN, + SERVICE_SEND_MESSAGE, + {ATTR_ENTITY_ID: ENTITY_DIRECT_MESSAGE, ATTR_MESSAGE: "You have an update!"}, + blocking=True, + ) + + state = hass.states.get(ENTITY_DIRECT_MESSAGE) + assert state + assert state.state == now.isoformat() diff --git a/tests/components/notify/conftest.py b/tests/components/notify/conftest.py new file mode 100644 index 00000000000..23930132f7b --- /dev/null +++ b/tests/components/notify/conftest.py @@ -0,0 +1,23 @@ +"""Fixtures for Notify platform tests.""" + +from collections.abc import Generator + +import pytest + +from homeassistant.config_entries import ConfigFlow +from homeassistant.core import HomeAssistant + +from tests.common import mock_config_flow, mock_platform + + +class MockFlow(ConfigFlow): + """Test flow.""" + + +@pytest.fixture +def config_flow_fixture(hass: HomeAssistant) -> Generator[None, None, None]: + """Mock config flow.""" + mock_platform(hass, "test.config_flow") + + with mock_config_flow("test", MockFlow): + yield diff --git a/tests/components/notify/test_init.py b/tests/components/notify/test_init.py index 0b75a3c4691..26ed2ddc250 100644 --- a/tests/components/notify/test_init.py +++ b/tests/components/notify/test_init.py @@ -1,28 +1,216 @@ """The tests for notify services that change targets.""" import asyncio +import copy from pathlib import Path -from unittest.mock import Mock, patch +from typing import Any +from unittest.mock import MagicMock, Mock, patch import pytest import yaml from homeassistant import config as hass_config from homeassistant.components import notify -from homeassistant.const import SERVICE_RELOAD, Platform -from homeassistant.core import HomeAssistant +from homeassistant.components.notify import ( + DOMAIN, + SERVICE_SEND_MESSAGE, + NotifyEntity, + NotifyEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + SERVICE_RELOAD, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + Platform, +) +from homeassistant.core import HomeAssistant, State from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.setup import async_setup_component -from tests.common import MockPlatform, async_get_persistent_notifications, mock_platform +from tests.common import ( + MockConfigEntry, + MockEntity, + MockModule, + MockPlatform, + async_get_persistent_notifications, + mock_integration, + mock_platform, + mock_restore_cache, + setup_test_component_platform, +) + +TEST_KWARGS = {"message": "Test message"} + + +class MockNotifyEntity(MockEntity, NotifyEntity): + """Mock Email notitier entity to use in tests.""" + + send_message_mock_calls = MagicMock() + + async def async_send_message(self, message: str) -> None: + """Send a notification message.""" + self.send_message_mock_calls(message=message) + + +class MockNotifyEntityNonAsync(MockEntity, NotifyEntity): + """Mock Email notitier entity to use in tests.""" + + send_message_mock_calls = MagicMock() + + def send_message(self, message: str) -> None: + """Send a notification message.""" + self.send_message_mock_calls(message=message) + + +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) + return True + + +async def help_async_unload_entry( + hass: HomeAssistant, config_entry: ConfigEntry +) -> bool: + """Unload test config emntry.""" + return await hass.config_entries.async_unload_platforms( + config_entry, [Platform.NOTIFY] + ) + + +@pytest.mark.parametrize( + "entity", + [ + MockNotifyEntityNonAsync(name="test", entity_id="notify.test"), + MockNotifyEntity(name="test", entity_id="notify.test"), + ], + ids=["non_async", "async"], +) +async def test_send_message_service( + hass: HomeAssistant, config_flow_fixture: None, entity: NotifyEntity +) -> None: + """Test send_message service.""" + + config_entry = MockConfigEntry(domain="test") + config_entry.add_to_hass(hass) + + mock_integration( + hass, + MockModule( + "test", + async_setup_entry=help_async_setup_entry_init, + async_unload_entry=help_async_unload_entry, + ), + ) + setup_test_component_platform(hass, DOMAIN, [entity], from_config_entry=True) + assert await hass.config_entries.async_setup(config_entry.entry_id) + + state = hass.states.get("notify.test") + assert state.state is STATE_UNKNOWN + + await hass.services.async_call( + DOMAIN, + SERVICE_SEND_MESSAGE, + copy.deepcopy(TEST_KWARGS) | {"entity_id": "notify.test"}, + blocking=True, + ) + await hass.async_block_till_done() + + entity.send_message_mock_calls.assert_called_once() + + # Test unloading the entry succeeds + assert await hass.config_entries.async_unload(config_entry.entry_id) + + +@pytest.mark.parametrize( + ("state", "init_state"), + [ + ("2021-01-01T23:59:59+00:00", "2021-01-01T23:59:59+00:00"), + (STATE_UNAVAILABLE, STATE_UNKNOWN), + ], +) +async def test_restore_state( + hass: HomeAssistant, config_flow_fixture: None, state: str, init_state: str +) -> None: + """Test we restore state integration.""" + mock_restore_cache(hass, (State("notify.test", state),)) + + mock_integration( + hass, + MockModule( + "test", + async_setup_entry=help_async_setup_entry_init, + ), + ) + + entity = MockNotifyEntity(name="test", entity_id="notify.test") + setup_test_component_platform(hass, DOMAIN, [entity], from_config_entry=True) + + config_entry = MockConfigEntry(domain="test") + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + + state = hass.states.get("notify.test") + assert state is not None + assert state.state is init_state + + +async def test_name(hass: HomeAssistant, config_flow_fixture: None) -> None: + """Test notify name.""" + + mock_platform(hass, "test.config_flow") + mock_integration( + hass, + MockModule( + "test", + async_setup_entry=help_async_setup_entry_init, + ), + ) + + # Unnamed notify entity -> no name + entity1 = NotifyEntity() + entity1.entity_id = "notify.test1" + + # Unnamed notify entity and has_entity_name True -> unnamed + entity2 = NotifyEntity() + entity2.entity_id = "notify.test3" + entity2._attr_has_entity_name = True + + # Named notify entity and has_entity_name True -> named + entity3 = NotifyEntity() + entity3.entity_id = "notify.test4" + entity3.entity_description = NotifyEntityDescription("test", has_entity_name=True) + + setup_test_component_platform( + hass, DOMAIN, [entity1, entity2, entity3], from_config_entry=True + ) + + config_entry = MockConfigEntry(domain="test") + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(entity1.entity_id) + assert state + assert state.attributes == {} + + state = hass.states.get(entity2.entity_id) + assert state + assert state.attributes == {} + + state = hass.states.get(entity3.entity_id) + assert state + assert state.attributes == {} class MockNotifyPlatform(MockPlatform): - """Help to set up test notify service.""" + """Help to set up a legacy test notify service.""" - def __init__(self, async_get_service=None, get_service=None): - """Return the notify service.""" + def __init__(self, async_get_service: Any = None, get_service: Any = None) -> None: + """Return a legacy notify service.""" super().__init__() if get_service: self.get_service = get_service @@ -31,9 +219,13 @@ class MockNotifyPlatform(MockPlatform): def mock_notify_platform( - hass, tmp_path, integration="notify", async_get_service=None, get_service=None + hass: HomeAssistant, + tmp_path: Path, + integration: str = "notify", + async_get_service: Any = None, + get_service: Any = None, ): - """Specialize the mock platform for notify.""" + """Specialize the mock platform for legacy notify service.""" loaded_platform = MockNotifyPlatform(async_get_service, get_service) mock_platform(hass, f"{integration}.notify", loaded_platform) @@ -41,7 +233,7 @@ def mock_notify_platform( async def test_same_targets(hass: HomeAssistant) -> None: - """Test not changing the targets in a notify service.""" + """Test not changing the targets in a legacy notify service.""" test = NotificationService(hass) await test.async_setup(hass, "notify", "test") await test.async_register_services() @@ -56,7 +248,7 @@ async def test_same_targets(hass: HomeAssistant) -> None: async def test_change_targets(hass: HomeAssistant) -> None: - """Test changing the targets in a notify service.""" + """Test changing the targets in a legacy notify service.""" test = NotificationService(hass) await test.async_setup(hass, "notify", "test") await test.async_register_services() @@ -73,7 +265,7 @@ async def test_change_targets(hass: HomeAssistant) -> None: async def test_add_targets(hass: HomeAssistant) -> None: - """Test adding the targets in a notify service.""" + """Test adding the targets in a legacy notify service.""" test = NotificationService(hass) await test.async_setup(hass, "notify", "test") await test.async_register_services() @@ -90,7 +282,7 @@ async def test_add_targets(hass: HomeAssistant) -> None: async def test_remove_targets(hass: HomeAssistant) -> None: - """Test removing targets from the targets in a notify service.""" + """Test removing targets from the targets in a legacy notify service.""" test = NotificationService(hass) await test.async_setup(hass, "notify", "test") await test.async_register_services() @@ -107,17 +299,22 @@ async def test_remove_targets(hass: HomeAssistant) -> None: class NotificationService(notify.BaseNotificationService): - """A test class for notification services.""" + """A test class for legacy notification services.""" - def __init__(self, hass, target_list={"a": 1, "b": 2}, name="notify"): + def __init__( + self, + hass: HomeAssistant, + target_list: dict[str, Any] | None = None, + name="notify", + ) -> None: """Initialize the service.""" - async def _async_make_reloadable(hass): + async def _async_make_reloadable(hass: HomeAssistant) -> None: """Initialize the reload service.""" await async_setup_reload_service(hass, name, [notify.DOMAIN]) self.hass = hass - self.target_list = target_list + self.target_list = target_list or {"a": 1, "b": 2} hass.async_create_task(_async_make_reloadable(hass)) @property @@ -229,7 +426,7 @@ async def test_platform_setup_with_error( async def test_reload_with_notify_builtin_platform_reload( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, tmp_path: Path ) -> None: - """Test reload using the notify platform reload method.""" + """Test reload using the legacy notify platform reload method.""" async def async_get_service(hass, config, discovery_info=None): """Get notify service for mocked platform.""" @@ -271,7 +468,7 @@ async def test_setup_platform_and_reload( return NotificationService(hass, targetlist, "testnotify") async def async_get_service2(hass, config, discovery_info=None): - """Get notify service for mocked platform.""" + """Get legacy notify service for mocked platform.""" get_service_called(config, discovery_info) targetlist = {"c": 3, "d": 4} return NotificationService(hass, targetlist, "testnotify2") @@ -351,7 +548,7 @@ async def test_setup_platform_and_reload( async def test_setup_platform_before_notify_setup( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, tmp_path: Path ) -> None: - """Test trying to setup a platform before notify is setup.""" + """Test trying to setup a platform before legacy notify service is setup.""" get_service_called = Mock() async def async_get_service(hass, config, discovery_info=None): @@ -401,7 +598,7 @@ async def test_setup_platform_before_notify_setup( async def test_setup_platform_after_notify_setup( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, tmp_path: Path ) -> None: - """Test trying to setup a platform after notify is setup.""" + """Test trying to setup a platform after legacy notify service is set up.""" get_service_called = Mock() async def async_get_service(hass, config, discovery_info=None): From f558121752dbf6cc9caa6c1f3f6f722209f98b6f Mon Sep 17 00:00:00 2001 From: Jessica Smith <8505845+NodeJSmith@users.noreply.github.com> Date: Thu, 11 Apr 2024 09:26:05 -0500 Subject: [PATCH 491/967] Bump whirlpool-sixth-sense to 0.18.8 (#115393) bump whirlpool to 0.18.8 --- homeassistant/components/whirlpool/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/whirlpool/manifest.json b/homeassistant/components/whirlpool/manifest.json index ee7861588ed..5618a3f61cb 100644 --- a/homeassistant/components/whirlpool/manifest.json +++ b/homeassistant/components/whirlpool/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "cloud_push", "loggers": ["whirlpool"], - "requirements": ["whirlpool-sixth-sense==0.18.7"] + "requirements": ["whirlpool-sixth-sense==0.18.8"] } diff --git a/requirements_all.txt b/requirements_all.txt index 773df97bfba..70dc3f56091 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2857,7 +2857,7 @@ webmin-xmlrpc==0.0.2 webrtc-noise-gain==1.2.3 # homeassistant.components.whirlpool -whirlpool-sixth-sense==0.18.7 +whirlpool-sixth-sense==0.18.8 # homeassistant.components.whois whois==0.9.27 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9428dcd42ca..08031fbe2d5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2207,7 +2207,7 @@ webmin-xmlrpc==0.0.2 webrtc-noise-gain==1.2.3 # homeassistant.components.whirlpool -whirlpool-sixth-sense==0.18.7 +whirlpool-sixth-sense==0.18.8 # homeassistant.components.whois whois==0.9.27 From 6c1bc2a9f4d734094d23d41c47e2ee36d659eccd Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 11 Apr 2024 09:13:39 -0700 Subject: [PATCH 492/967] Reduce scope of diagnostics tests for rtsp_to_webrtc to not depend on global state (#115422) --- tests/components/rtsp_to_webrtc/test_diagnostics.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/tests/components/rtsp_to_webrtc/test_diagnostics.py b/tests/components/rtsp_to_webrtc/test_diagnostics.py index 8af6b914191..ad3522686b6 100644 --- a/tests/components/rtsp_to_webrtc/test_diagnostics.py +++ b/tests/components/rtsp_to_webrtc/test_diagnostics.py @@ -2,8 +2,6 @@ from typing import Any -from freezegun.api import FrozenDateTimeFactory - from homeassistant.core import HomeAssistant from .conftest import ComponentSetup @@ -21,13 +19,9 @@ async def test_entry_diagnostics( config_entry: MockConfigEntry, rtsp_to_webrtc_client: Any, setup_integration: ComponentSetup, - freezer: FrozenDateTimeFactory, ) -> None: """Test config entry diagnostics.""" await setup_integration() - assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { - "discovery": {"attempt": 1, "web.failure": 1, "webrtc.success": 1}, - "web": {}, - "webrtc": {}, - } + result = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) + assert "webrtc" in result From 6ba7a30cc80d36907ddb840c2f9093129ff3cb8a Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Thu, 11 Apr 2024 21:51:09 +0200 Subject: [PATCH 493/967] Fix Codecov upload with token (#115384) Co-authored-by: Martin Hjelmare Co-authored-by: J. Nick Koston --- .github/workflows/ci.yaml | 44 +++++++++++++-------------------------- 1 file changed, 14 insertions(+), 30 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e1281a14b5c..4299f298122 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1088,25 +1088,17 @@ jobs: pattern: coverage-* - name: Upload coverage to Codecov (full coverage) if: needs.info.outputs.test_full_suite == 'true' - uses: Wandalen/wretry.action@v3.1.0 + uses: codecov/codecov-action@v4.3.0 with: - action: codecov/codecov-action@v4.3.0 - with: | - fail_ci_if_error: true - flags: full-suite - token: ${{ env.CODECOV_TOKEN }} - attempt_limit: 5 - attempt_delay: 30000 + fail_ci_if_error: true + flags: full-suite + token: ${{ secrets.CODECOV_TOKEN }} - name: Upload coverage to Codecov (partial coverage) if: needs.info.outputs.test_full_suite == 'false' - uses: Wandalen/wretry.action@v3.1.0 + uses: codecov/codecov-action@v4.3.0 with: - action: codecov/codecov-action@v4.3.0 - with: | - fail_ci_if_error: true - token: ${{ env.CODECOV_TOKEN }} - attempt_limit: 5 - attempt_delay: 30000 + fail_ci_if_error: true + token: ${{ secrets.CODECOV_TOKEN }} pytest-partial: runs-on: ubuntu-22.04 @@ -1234,22 +1226,14 @@ jobs: pattern: coverage-* - name: Upload coverage to Codecov (full coverage) if: needs.info.outputs.test_full_suite == 'true' - uses: Wandalen/wretry.action@v3.1.0 + uses: codecov/codecov-action@v4.3.0 with: - action: codecov/codecov-action@v4.3.0 - with: | - fail_ci_if_error: true - flags: full-suite - token: ${{ env.CODECOV_TOKEN }} - attempt_limit: 5 - attempt_delay: 30000 + fail_ci_if_error: true + flags: full-suite + token: ${{ secrets.CODECOV_TOKEN }} - name: Upload coverage to Codecov (partial coverage) if: needs.info.outputs.test_full_suite == 'false' - uses: Wandalen/wretry.action@v3.1.0 + uses: codecov/codecov-action@v4.3.0 with: - action: codecov/codecov-action@v4.3.0 - with: | - fail_ci_if_error: true - token: ${{ env.CODECOV_TOKEN }} - attempt_limit: 5 - attempt_delay: 30000 + fail_ci_if_error: true + token: ${{ secrets.CODECOV_TOKEN }} From c14f11fbf0068686e99f6be20f68a871b6983b3a Mon Sep 17 00:00:00 2001 From: Santobert Date: Thu, 11 Apr 2024 21:57:18 +0200 Subject: [PATCH 494/967] Bump pybotvac to 0.0.25 (#115435) Bump pybotvac --- homeassistant/components/neato/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/neato/manifest.json b/homeassistant/components/neato/manifest.json index 1d5edb7ca44..d6eff486b05 100644 --- a/homeassistant/components/neato/manifest.json +++ b/homeassistant/components/neato/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/neato", "iot_class": "cloud_polling", "loggers": ["pybotvac"], - "requirements": ["pybotvac==0.0.24"] + "requirements": ["pybotvac==0.0.25"] } diff --git a/requirements_all.txt b/requirements_all.txt index 70dc3f56091..76886fa4b96 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1722,7 +1722,7 @@ pybbox==0.0.5-alpha pyblackbird==0.6 # homeassistant.components.neato -pybotvac==0.0.24 +pybotvac==0.0.25 # homeassistant.components.braviatv pybravia==0.3.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 08031fbe2d5..bba41cc4d8d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1357,7 +1357,7 @@ pybalboa==1.0.1 pyblackbird==0.6 # homeassistant.components.neato -pybotvac==0.0.24 +pybotvac==0.0.25 # homeassistant.components.braviatv pybravia==0.3.3 From d9fc9f2e0c8020fe745427d60a5ce17f199217ff Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 11 Apr 2024 11:22:50 -1000 Subject: [PATCH 495/967] Convert async_setup calls for auth sub-modules to callback functions (#115443) --- homeassistant/components/auth/__init__.py | 4 ++-- homeassistant/components/auth/login_flow.py | 5 +++-- homeassistant/components/auth/mfa_setup_flow.py | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index d0e605e7c1e..ff54971eb64 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -195,8 +195,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: websocket_api.async_register_command(hass, websocket_delete_all_refresh_tokens) websocket_api.async_register_command(hass, websocket_sign_path) - await login_flow.async_setup(hass, store_result) - await mfa_setup_flow.async_setup(hass) + login_flow.async_setup(hass, store_result) + mfa_setup_flow.async_setup(hass) return True diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py index 6c33d270f5f..5bad0dbb999 100644 --- a/homeassistant/components/auth/login_flow.py +++ b/homeassistant/components/auth/login_flow.py @@ -91,7 +91,7 @@ from homeassistant.components.http.ban import ( ) from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components.http.view import HomeAssistantView -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.network import is_cloud_connection from homeassistant.util.network import is_local @@ -105,7 +105,8 @@ if TYPE_CHECKING: from . import StoreResultType -async def async_setup( +@callback +def async_setup( hass: HomeAssistant, store_result: Callable[[str, Credentials], str] ) -> None: """Component to allow users to login.""" diff --git a/homeassistant/components/auth/mfa_setup_flow.py b/homeassistant/components/auth/mfa_setup_flow.py index aaa1dbaedbf..35d87cafd4f 100644 --- a/homeassistant/components/auth/mfa_setup_flow.py +++ b/homeassistant/components/auth/mfa_setup_flow.py @@ -62,7 +62,8 @@ class MfaFlowManager(data_entry_flow.FlowManager): return result -async def async_setup(hass: HomeAssistant) -> None: +@callback +def async_setup(hass: HomeAssistant) -> None: """Init mfa setup flow manager.""" hass.data[DATA_SETUP_FLOW_MGR] = MfaFlowManager(hass) From db38da8eb8ef3e3e4b5afcad39fc0f33485236fd Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 11 Apr 2024 23:35:15 +0200 Subject: [PATCH 496/967] Update pytest warnings filter (#115275) --- pyproject.toml | 62 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 58 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 66c82d2e770..cf9b7b045d6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -488,6 +488,8 @@ filterwarnings = [ "ignore:datetime.*utcfromtimestamp\\(\\) is deprecated and scheduled for removal:DeprecationWarning:aiopurpleair.helpers.validators", # https://github.com/DataDog/datadogpy/pull/290 - >=0.23.0 "ignore:invalid escape sequence:SyntaxWarning:.*datadog.dogstatsd.base", + # https://github.com/DataDog/datadogpy/pull/566/files - >=0.37.0 + "ignore:pkg_resources is deprecated as an API:DeprecationWarning:datadog.util.compat", # https://github.com/fwestenberg/devialet/pull/6 - >1.4.5 "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:devialet.devialet_api", # https://github.com/jaraco/jaraco.abode/commit/9e3e789efc96cddcaa15f920686bbeb79a7469e0 - update jaraco.abode to >=5.1.0 @@ -504,13 +506,23 @@ filterwarnings = [ "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/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/grahamwetzler/smart-meter-texas/pull/143 - >0.5.3 "ignore:ssl.OP_NO_SSL\\*/ssl.OP_NO_TLS\\* options are deprecated:DeprecationWarning:smart_meter_texas", + # 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", + # -- fixed for Python 3.13 + # https://github.com/rhasspy/wyoming/commit/e34af30d455b6f2bb9e5cfb25fad8d276914bc54 - >=1.4.2 + "ignore:'audioop' is deprecated and slated for removal in Python 3.13:DeprecationWarning:wyoming.audio", + # -- other # Locale changes might take some time to resolve upstream "ignore:'locale.getdefaultlocale' is deprecated and slated for removal in Python 3.15:DeprecationWarning:micloud.micloud", @@ -537,15 +549,47 @@ filterwarnings = [ # https://pypi.org/project/pyblackbird/ - v0.6 - 2023-03-15 # https://github.com/koolsb/pyblackbird/pull/9 -> closed "ignore:invalid escape sequence:SyntaxWarning:.*pyblackbird", - # https://pypi.org/project/pybotvac/ - v0.0.24 - 2023-01-02 - # https://github.com/stianaske/pybotvac/pull/81 -> closed - "ignore:invalid escape sequence:SyntaxWarning:.*pybotvac.robot", - # https://github.com/pkkid/python-plexapi/pull/1244 - v4.15.10 -> new issue same file + # https://github.com/pkkid/python-plexapi/pull/1244 - v4.15.11 -> new issue same file + # https://github.com/pkkid/python-plexapi/pull/1370 -> Not fixed here "ignore:invalid escape sequence:SyntaxWarning:.*plexapi.base", # https://pypi.org/project/pyws66i/ - v1.1 - 2022-04-05 "ignore:invalid escape sequence:SyntaxWarning:.*pyws66i", # https://pypi.org/project/sleekxmppfs/ - v1.4.1 - 2022-08-18 "ignore:invalid escape sequence:SyntaxWarning:.*sleekxmppfs.thirdparty.mini_dateutil", + # - pkg_resources + # https://pypi.org/project/aiomusiccast/ - v0.14.8 - 2023-03-20 + "ignore:pkg_resources is deprecated as an API:DeprecationWarning:aiomusiccast", + # https://github.com/eavanvalkenburg/pysiaalarm/blob/v3.1.1/src/pysiaalarm/data/data.py#L7 - v3.1.1 - 2023-04-17 + "ignore:pkg_resources is deprecated as an API:DeprecationWarning:pysiaalarm.data.data", + # https://pypi.org/project/pybotvac/ - v0.0.25 - 2024-04-11 + "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.0 + # https://github.com/Cereal2nd/velbus-aio/blob/2024.4.0/velbusaio/handler.py#L13 + "ignore:pkg_resources is deprecated as an API:DeprecationWarning:velbusaio.handler", + + # -- Python 3.13 + # HomeAssistant + "ignore:'audioop' is deprecated and slated for removal in Python 3.13:DeprecationWarning:homeassistant.components.assist_pipeline.websocket_api", + # https://pypi.org/project/pylutron/ - v0.2.12 - 2024-02-12 + # https://github.com/thecynic/pylutron/issues/89 + "ignore:'telnetlib' is deprecated and slated for removal in Python 3.13:DeprecationWarning:pylutron", + # https://pypi.org/project/SpeechRecognition/ - v3.10.3 - 2024-03-30 + # https://github.com/Uberi/speech_recognition/blob/3.10.3/speech_recognition/__init__.py#L7 + "ignore:'aifc' is deprecated and slated for removal in Python 3.13:DeprecationWarning:speech_recognition", + # https://pypi.org/project/voip-utils/ - v0.1.0 - 2023-06-28 + # https://github.com/home-assistant-libs/voip-utils/blob/v0.1.0/voip_utils/rtp_audio.py#L2 + "ignore:'audioop' is deprecated and slated for removal in Python 3.13:DeprecationWarning:voip_utils.rtp_audio", + + # -- Python 3.13 - unmaintained projects, last release about 2+ years + # https://pypi.org/project/pydub/ - v0.25.1 - 2021-03-10 + "ignore:'audioop' is deprecated and slated for removal in Python 3.13:DeprecationWarning:pydub.utils", + # https://github.com/heathbar/plum-lightpad-python/issues/7 - v0.0.11 - 2018-10-16 + "ignore:'telnetlib' is deprecated and slated for removal in Python 3.13:DeprecationWarning:plumlightpad.lightpad", + # https://pypi.org/project/pyws66i/ - v1.1 - 2022-04-05 + # https://github.com/ssaenger/pyws66i/blob/v1.1/pyws66i/__init__.py#L2 + "ignore:'telnetlib' is deprecated and slated for removal in Python 3.13:DeprecationWarning:pyws66i", # -- unmaintained projects, last release about 2+ years # https://pypi.org/project/agent-py/ - v0.0.23 - 2020-06-04 @@ -559,6 +603,10 @@ filterwarnings = [ "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:directv.models", # https://pypi.org/project/foobot_async/ - v1.0.0 - 2020-11-24 "ignore:with timeout\\(\\) is deprecated:DeprecationWarning:foobot_async", + # https://pypi.org/project/habitipy/ - v0.3.0 - 2019-01-14 + "ignore:pkg_resources is deprecated as an API:DeprecationWarning:habitipy.api", + # https://pypi.org/project/httpsig/ - v1.3.0 - 2018-11-28 + "ignore:pkg_resources is deprecated as an API:DeprecationWarning:httpsig", # https://pypi.org/project/influxdb/ - v5.3.1 - 2020-11-11 (archived) "ignore:datetime.*utcfromtimestamp\\(\\) is deprecated and scheduled for removal:DeprecationWarning:influxdb.line_protocol", # https://pypi.org/project/lark-parser/ - v0.12.0 - 2021-08-30 -> moved to `lark` @@ -575,6 +623,8 @@ filterwarnings = [ "ignore:\"is not\" with 'int' literal. Did you mean \"!=\"?:SyntaxWarning:.*opuslib.api.decoder", # https://pypi.org/project/passlib/ - v1.7.4 - 2020-10-08 "ignore:'crypt' is deprecated and slated for removal in Python 3.13:DeprecationWarning:passlib.utils", + # https://pypi.org/project/pilight/ - v0.1.1 - 2016-10-19 + "ignore:pkg_resources is deprecated as an API:DeprecationWarning:pilight", # https://pypi.org/project/plumlightpad/ - v0.0.11 - 2018-10-16 "ignore:invalid escape sequence:SyntaxWarning:.*plumlightpad.plumdiscovery", "ignore:\"is\" with 'int' literal. Did you mean \"==\"?:SyntaxWarning:.*plumlightpad.(lightpad|logicalload)", @@ -586,6 +636,10 @@ filterwarnings = [ "ignore:\"is\" with 'int' literal. Did you mean \"==\"?:SyntaxWarning:.*pyiss", # https://pypi.org/project/PyMetEireann/ - v2021.8.0 - 2021-08-16 "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:meteireann", + # https://pypi.org/project/pyowm/ - v3.3.0 - 2022-02-14 + # https://github.com/csparpa/pyowm/issues/435 + # https://github.com/csparpa/pyowm/blob/3.3.0/pyowm/commons/cityidregistry.py#L7 + "ignore:pkg_resources is deprecated as an API:DeprecationWarning:pyowm.commons.cityidregistry", # https://pypi.org/project/PyPasser/ - v0.0.5 - 2021-10-21 "ignore:invalid escape sequence:SyntaxWarning:.*pypasser.utils", # https://pypi.org/project/pyqwikswitch/ - v0.94 - 2019-08-19 From 137514edb7ad0b2fbda9fa3f46f17235a68ce35b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 11 Apr 2024 11:58:56 -1000 Subject: [PATCH 497/967] Bump aiohttp to 3.9.4 (#110730) * Bump aiohttp to 3.9.4 This is rc0 for now but will be updated when the full release it out * cleanup cruft * regen * fix tests (these changes are fine) * chunk size is too small to read since boundry is now enforced * chunk size is too small to read since boundry is now enforced --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- tests/components/file_upload/test_init.py | 8 ++++---- tests/components/websocket_api/test_auth.py | 2 +- tests/components/websocket_api/test_http.py | 6 +++--- tests/components/websocket_api/test_init.py | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 4acbe3fae58..1150da9ceac 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -5,7 +5,7 @@ aiodiscover==2.0.0 aiodns==3.2.0 aiohttp-fast-url-dispatcher==0.3.0 aiohttp-zlib-ng==0.3.1 -aiohttp==3.9.3 +aiohttp==3.9.4 aiohttp_cors==0.7.0 astral==2.2 async-interrupt==1.1.1 diff --git a/pyproject.toml b/pyproject.toml index cf9b7b045d6..8be7b9b40f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ requires-python = ">=3.12.0" dependencies = [ "aiodns==3.2.0", - "aiohttp==3.9.3", + "aiohttp==3.9.4", "aiohttp_cors==0.7.0", "aiohttp-fast-url-dispatcher==0.3.0", "aiohttp-zlib-ng==0.3.1", diff --git a/requirements.txt b/requirements.txt index c5b5e54046d..3cd1e8edfa5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ # Home Assistant Core aiodns==3.2.0 -aiohttp==3.9.3 +aiohttp==3.9.4 aiohttp_cors==0.7.0 aiohttp-fast-url-dispatcher==0.3.0 aiohttp-zlib-ng==0.3.1 diff --git a/tests/components/file_upload/test_init.py b/tests/components/file_upload/test_init.py index 1ef238cafd0..fa77f6e55f5 100644 --- a/tests/components/file_upload/test_init.py +++ b/tests/components/file_upload/test_init.py @@ -90,9 +90,9 @@ async def test_upload_large_file( file_upload.TEMP_DIR_NAME + f"-{getrandbits(10):03x}", ), patch( - # Patch one megabyte to 8 bytes to prevent having to use big files in tests + # Patch one megabyte to 50 bytes to prevent having to use big files in tests "homeassistant.components.file_upload.ONE_MEGABYTE", - 8, + 50, ), ): res = await client.post("/api/file_upload", data={"file": large_file_io}) @@ -152,9 +152,9 @@ async def test_upload_large_file_fails( file_upload.TEMP_DIR_NAME + f"-{getrandbits(10):03x}", ), patch( - # Patch one megabyte to 8 bytes to prevent having to use big files in tests + # Patch one megabyte to 50 bytes to prevent having to use big files in tests "homeassistant.components.file_upload.ONE_MEGABYTE", - 8, + 50, ), patch( "homeassistant.components.file_upload.Path.open", return_value=_mock_open() diff --git a/tests/components/websocket_api/test_auth.py b/tests/components/websocket_api/test_auth.py index 35bf2402b6c..595dc7dcc32 100644 --- a/tests/components/websocket_api/test_auth.py +++ b/tests/components/websocket_api/test_auth.py @@ -221,7 +221,7 @@ async def test_auth_close_after_revoke( hass.auth.async_remove_refresh_token(refresh_token) msg = await websocket_client.receive() - assert msg.type == aiohttp.WSMsgType.CLOSED + assert msg.type is aiohttp.WSMsgType.CLOSE assert websocket_client.closed diff --git a/tests/components/websocket_api/test_http.py b/tests/components/websocket_api/test_http.py index db186e4811b..6ce46a5d9fe 100644 --- a/tests/components/websocket_api/test_http.py +++ b/tests/components/websocket_api/test_http.py @@ -43,7 +43,7 @@ async def test_pending_msg_overflow( for idx in range(10): await websocket_client.send_json({"id": idx + 1, "type": "ping"}) msg = await websocket_client.receive() - assert msg.type == WSMsgType.CLOSED + assert msg.type is WSMsgType.CLOSE async def test_cleanup_on_cancellation( @@ -249,7 +249,7 @@ async def test_pending_msg_peak( ) msg = await websocket_client.receive() - assert msg.type == WSMsgType.CLOSED + assert msg.type is WSMsgType.CLOSE assert "Client unable to keep up with pending messages" in caplog.text assert "Stayed over 5 for 5 seconds" in caplog.text assert "overload" in caplog.text @@ -297,7 +297,7 @@ async def test_pending_msg_peak_recovery( msg = await websocket_client.receive() assert msg.type == WSMsgType.TEXT msg = await websocket_client.receive() - assert msg.type == WSMsgType.CLOSED + assert msg.type is WSMsgType.CLOSE assert "Client unable to keep up with pending messages" not in caplog.text diff --git a/tests/components/websocket_api/test_init.py b/tests/components/websocket_api/test_init.py index 9360ff4ef8a..b20fd1c2f7e 100644 --- a/tests/components/websocket_api/test_init.py +++ b/tests/components/websocket_api/test_init.py @@ -41,7 +41,7 @@ async def test_quiting_hass(hass: HomeAssistant, websocket_client) -> None: msg = await websocket_client.receive() - assert msg.type == WSMsgType.CLOSED + assert msg.type is WSMsgType.CLOSE async def test_unknown_command(websocket_client) -> None: From a093f943d7136af12eb88acb993332b7882c76fe Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Fri, 12 Apr 2024 00:03:10 +0200 Subject: [PATCH 498/967] Use library classes instead of namedtuple in ipma tests (#115372) --- tests/components/ipma/__init__.py | 149 ++++++++---------- .../ipma/snapshots/test_diagnostics.ambr | 57 ++----- .../ipma/snapshots/test_weather.ambr | 8 +- 3 files changed, 83 insertions(+), 131 deletions(-) diff --git a/tests/components/ipma/__init__.py b/tests/components/ipma/__init__.py index 65cff43c8d4..799120e3966 100644 --- a/tests/components/ipma/__init__.py +++ b/tests/components/ipma/__init__.py @@ -1,8 +1,12 @@ """Tests for the IPMA component.""" -from collections import namedtuple from datetime import UTC, datetime +from pyipma.forecast import Forecast, Forecast_Location, Weather_Type +from pyipma.observation import Observation +from pyipma.rcm import RCM +from pyipma.uv import UV + from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE, CONF_NAME ENTRY_CONFIG = { @@ -18,109 +22,90 @@ class MockLocation: async def fire_risk(self, api): """Mock Fire Risk.""" - RCM = namedtuple( - "RCM", - [ - "dico", - "rcm", - "coordinates", - ], - ) return RCM("some place", 3, (0, 0)) async def uv_risk(self, api): """Mock UV Index.""" - UV = namedtuple( - "UV", - ["idPeriodo", "intervaloHora", "data", "globalIdLocal", "iUv"], - ) - return UV(0, "0", datetime.now(), 0, 5.7) + return UV(0, "0", datetime(2020, 1, 16, 0, 0, 0), 0, 5.7) async def observation(self, api): """Mock Observation.""" - Observation = namedtuple( - "Observation", - [ - "accumulated_precipitation", - "humidity", - "pressure", - "radiation", - "temperature", - "wind_direction", - "wind_intensity_km", - ], + return Observation( + precAcumulada=0.0, + humidade=71.0, + pressao=1000.0, + radiacao=0.0, + temperatura=18.0, + idDireccVento=8, + intensidadeVentoKM=3.94, + intensidadeVento=1.0944, + timestamp=datetime(2020, 1, 16, 0, 0, 0), + idEstacao=0, ) - return Observation(0.0, 71.0, 1000.0, 0.0, 18.0, "NW", 3.94) - async def forecast(self, api, period): """Mock Forecast.""" - Forecast = namedtuple( - "Forecast", - [ - "feels_like_temperature", - "forecast_date", - "forecasted_hours", - "humidity", - "max_temperature", - "min_temperature", - "precipitation_probability", - "temperature", - "update_date", - "weather_type", - "wind_direction", - "wind_strength", - ], - ) - - WeatherType = namedtuple("WeatherType", ["id", "en", "pt"]) if period == 24: return [ Forecast( - None, - datetime(2020, 1, 16, 0, 0, 0), - 24, - None, - 16.2, - 10.6, - "100.0", - 13.4, - "2020-01-15T07:51:00", - WeatherType(9, "Rain/showers", "Chuva/aguaceiros"), - "S", - "10", + utci=None, + dataPrev=datetime(2020, 1, 16, 0, 0, 0), + idPeriodo=24, + hR=None, + tMax=16.2, + tMin=10.6, + probabilidadePrecipita=100.0, + tMed=13.4, + dataUpdate=datetime(2020, 1, 15, 7, 51, 0), + idTipoTempo=Weather_Type(9, "Rain/showers", "Chuva/aguaceiros"), + ddVento="S", + ffVento=10, + idFfxVento=0, + iUv=0, + intervaloHora="", + location=Forecast_Location(0, "", 0, 0, 0, "", (0, 0)), ), ] if period == 1: return [ Forecast( - "7.7", - datetime(2020, 1, 15, 1, 0, 0, tzinfo=UTC), - 1, - "86.9", - 12.0, - None, - 80.0, - 10.6, - "2020-01-15T02:51:00", - WeatherType(10, "Light rain", "Chuva fraca ou chuvisco"), - "S", - "32.7", + utci=7.7, + dataPrev=datetime(2020, 1, 15, 1, 0, 0, tzinfo=UTC), + idPeriodo=1, + hR=86.9, + tMax=12.0, + tMin=None, + probabilidadePrecipita=80.0, + tMed=10.6, + dataUpdate=datetime(2020, 1, 15, 2, 51, 0), + idTipoTempo=Weather_Type( + 10, "Light rain", "Chuva fraca ou chuvisco" + ), + ddVento="S", + ffVento=32.7, + idFfxVento=0, + iUv=0, + intervaloHora="", + location=Forecast_Location(0, "", 0, 0, 0, "", (0, 0)), ), Forecast( - "5.7", - datetime(2020, 1, 15, 2, 0, 0, tzinfo=UTC), - 1, - "86.9", - 12.0, - None, - 80.0, - 10.6, - "2020-01-15T02:51:00", - WeatherType(1, "Clear sky", "C\u00e9u limpo"), - "S", - "32.7", + utci=5.7, + dataPrev=datetime(2020, 1, 15, 2, 0, 0, tzinfo=UTC), + idPeriodo=1, + hR=86.9, + tMax=12.0, + tMin=None, + probabilidadePrecipita=80.0, + tMed=10.6, + dataUpdate=datetime(2020, 1, 15, 2, 51, 0), + idTipoTempo=Weather_Type(1, "Clear sky", "C\u00e9u limpo"), + ddVento="S", + ffVento=32.7, + idFfxVento=0, + iUv=0, + intervaloHora="", + location=Forecast_Location(0, "", 0, 0, 0, "", (0, 0)), ), ] diff --git a/tests/components/ipma/snapshots/test_diagnostics.ambr b/tests/components/ipma/snapshots/test_diagnostics.ambr index c95364b6e4a..9d7d38db8c3 100644 --- a/tests/components/ipma/snapshots/test_diagnostics.ambr +++ b/tests/components/ipma/snapshots/test_diagnostics.ambr @@ -1,15 +1,10 @@ # serializer version: 1 # name: test_diagnostics dict({ - 'current_weather': list([ - 0.0, - 71.0, - 1000.0, - 0.0, - 18.0, - 'NW', - 3.94, - ]), + 'current_weather': dict({ + '__type': "", + 'repr': 'Observation(intensidadeVentoKM=3.94, temperatura=18.0, radiacao=0.0, idDireccVento=8, precAcumulada=0.0, intensidadeVento=1.0944, humidade=71.0, pressao=1000.0, timestamp=datetime.datetime(2020, 1, 16, 0, 0), idEstacao=0)', + }), 'location_information': dict({ 'global_id_local': 1130600, 'id_station': 1200545, @@ -19,42 +14,14 @@ 'station': 'HomeTown Station', }), 'weather_forecast': list([ - list([ - '7.7', - '2020-01-15T01:00:00+00:00', - 1, - '86.9', - 12.0, - None, - 80.0, - 10.6, - '2020-01-15T02:51:00', - list([ - 10, - 'Light rain', - 'Chuva fraca ou chuvisco', - ]), - 'S', - '32.7', - ]), - list([ - '5.7', - '2020-01-15T02:00:00+00:00', - 1, - '86.9', - 12.0, - None, - 80.0, - 10.6, - '2020-01-15T02:51:00', - list([ - 1, - 'Clear sky', - 'Céu limpo', - ]), - 'S', - '32.7', - ]), + dict({ + '__type': "", + 'repr': "Forecast(tMed=10.6, tMin=None, ffVento=32.7, idFfxVento=0, dataUpdate=datetime.datetime(2020, 1, 15, 2, 51), tMax=12.0, iUv=0, intervaloHora='', idTipoTempo=Weather_Type(id=10, en='Light rain', pt='Chuva fraca ou chuvisco'), hR=86.9, location=Forecast_Location(globalIdLocal=0, local='', idRegiao=0, idDistrito=0, idConcelho=0, idAreaAviso='', coordinates=(0, 0)), probabilidadePrecipita=80.0, idPeriodo=1, dataPrev=datetime.datetime(2020, 1, 15, 1, 0, tzinfo=datetime.timezone.utc), ddVento='S', utci=7.7)", + }), + dict({ + '__type': "", + 'repr': "Forecast(tMed=10.6, tMin=None, ffVento=32.7, idFfxVento=0, dataUpdate=datetime.datetime(2020, 1, 15, 2, 51), tMax=12.0, iUv=0, intervaloHora='', idTipoTempo=Weather_Type(id=1, en='Clear sky', pt='Céu limpo'), hR=86.9, location=Forecast_Location(globalIdLocal=0, local='', idRegiao=0, idDistrito=0, idConcelho=0, idAreaAviso='', coordinates=(0, 0)), probabilidadePrecipita=80.0, idPeriodo=1, dataPrev=datetime.datetime(2020, 1, 15, 2, 0, tzinfo=datetime.timezone.utc), ddVento='S', utci=5.7)", + }), ]), }) # --- diff --git a/tests/components/ipma/snapshots/test_weather.ambr b/tests/components/ipma/snapshots/test_weather.ambr index 0a778776329..1142cb7cfe5 100644 --- a/tests/components/ipma/snapshots/test_weather.ambr +++ b/tests/components/ipma/snapshots/test_weather.ambr @@ -83,7 +83,7 @@ dict({ 'condition': 'rainy', 'datetime': datetime.datetime(2020, 1, 16, 0, 0), - 'precipitation_probability': '100.0', + 'precipitation_probability': 100.0, 'temperature': 16.2, 'templow': 10.6, 'wind_bearing': 'S', @@ -121,7 +121,7 @@ dict({ 'condition': 'rainy', 'datetime': datetime.datetime(2020, 1, 16, 0, 0), - 'precipitation_probability': '100.0', + 'precipitation_probability': 100.0, 'temperature': 16.2, 'templow': 10.6, 'wind_bearing': 'S', @@ -160,7 +160,7 @@ dict({ 'condition': 'rainy', 'datetime': '2020-01-16T00:00:00', - 'precipitation_probability': '100.0', + 'precipitation_probability': 100.0, 'temperature': 16.2, 'templow': 10.6, 'wind_bearing': 'S', @@ -173,7 +173,7 @@ dict({ 'condition': 'rainy', 'datetime': '2020-01-16T00:00:00', - 'precipitation_probability': '100.0', + 'precipitation_probability': 100.0, 'temperature': 16.2, 'templow': 10.6, 'wind_bearing': 'S', From 2e8f4743eb89e44111e1979d5ca344ec69665ba9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 11 Apr 2024 12:17:01 -1000 Subject: [PATCH 499/967] Fix flakey mobile app webhook test (#115447) --- tests/components/mobile_app/test_webhook.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index c67312939b1..f39c963b45b 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -2,7 +2,7 @@ from binascii import unhexlify from http import HTTPStatus -from unittest.mock import patch +from unittest.mock import ANY, patch import pytest @@ -317,7 +317,7 @@ async def test_webhook_handle_get_config( "time_zone": hass_config["time_zone"], "components": set(hass_config["components"]), "version": hass_config["version"], - "theme_color": "#03A9F4", # Default frontend theme color + "theme_color": ANY, "entities": { "mock-device-id": {"disabled": False}, "battery-state-id": {"disabled": False}, From cfda8f64b4b58049b23aa5b6cde4e3ed644a55a2 Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Thu, 11 Apr 2024 19:04:51 -0400 Subject: [PATCH 500/967] Bump python-roborock to 2.0.0 (#115449) --- 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 711da78de31..d03aa68f1a6 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==1.0.0", + "python-roborock==2.0.0", "vacuum-map-parser-roborock==0.1.1" ] } diff --git a/requirements_all.txt b/requirements_all.txt index 76886fa4b96..4ee717dd0d0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2294,7 +2294,7 @@ python-rabbitair==0.0.8 python-ripple-api==0.0.3 # homeassistant.components.roborock -python-roborock==1.0.0 +python-roborock==2.0.0 # homeassistant.components.smarttub python-smarttub==0.0.36 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bba41cc4d8d..479449d92cf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1773,7 +1773,7 @@ python-qbittorrent==0.4.3 python-rabbitair==0.0.8 # homeassistant.components.roborock -python-roborock==1.0.0 +python-roborock==2.0.0 # homeassistant.components.smarttub python-smarttub==0.0.36 From e3680044fedc253ec00649103fad5fe34ae4f72a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 11 Apr 2024 13:26:03 -1000 Subject: [PATCH 501/967] Fix flakey influxdb test (#115442) --- tests/components/influxdb/test_init.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/components/influxdb/test_init.py b/tests/components/influxdb/test_init.py index cd95248eb33..ad3fddeaf6e 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.""" +import asyncio from dataclasses import dataclass import datetime from http import HTTPStatus @@ -1572,12 +1573,21 @@ async def test_invalid_inputs_error( await _setup(hass, mock_client, config_ext, get_write_api) write_api = get_write_api(mock_client) - write_api.side_effect = test_exception + + write_api_done_event = asyncio.Event() + + def wait_for_write(*args, **kwargs): + hass.loop.call_soon_threadsafe(write_api_done_event.set) + raise test_exception + + write_api.side_effect = wait_for_write with patch(f"{INFLUX_PATH}.time.sleep") as sleep: + write_api_done_event.clear() hass.states.async_set("fake.something", 1) await hass.async_block_till_done() await async_wait_for_queue_to_process(hass) + await write_api_done_event.wait() await hass.async_block_till_done() write_api.assert_called_once() From a48f2803b23354a9df3978f4bded34053bef18aa Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 12 Apr 2024 04:14:10 +0200 Subject: [PATCH 502/967] Add py.typed file (#115446) --- homeassistant/py.typed | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 homeassistant/py.typed diff --git a/homeassistant/py.typed b/homeassistant/py.typed new file mode 100644 index 00000000000..e69de29bb2d From 1b24e78dd911ec4655a9b0b354708f3972244a2b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 12 Apr 2024 04:14:37 +0200 Subject: [PATCH 503/967] Improve FlowHandler menu_options typing (#115296) --- homeassistant/data_entry_flow.py | 6 +++--- homeassistant/helpers/schema_config_entry_flow.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 649c9fdf8a4..7e7019681af 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -4,7 +4,7 @@ from __future__ import annotations import abc import asyncio -from collections.abc import Callable, Iterable, Mapping +from collections.abc import Callable, Container, Iterable, Mapping from contextlib import suppress import copy from dataclasses import dataclass @@ -153,7 +153,7 @@ class FlowResult(TypedDict, Generic[_HandlerT], total=False): flow_id: Required[str] handler: Required[_HandlerT] last_step: bool | None - menu_options: list[str] | dict[str, str] + menu_options: Container[str] options: Mapping[str, Any] preview: str | None progress_action: str @@ -843,7 +843,7 @@ class FlowHandler(Generic[_FlowResultT, _HandlerT]): self, *, step_id: str | None = None, - menu_options: list[str] | dict[str, str], + menu_options: Container[str], description_placeholders: Mapping[str, str] | None = None, ) -> _FlowResultT: """Show a navigation menu to the user. diff --git a/homeassistant/helpers/schema_config_entry_flow.py b/homeassistant/helpers/schema_config_entry_flow.py index 978ce949eb3..67624bfb368 100644 --- a/homeassistant/helpers/schema_config_entry_flow.py +++ b/homeassistant/helpers/schema_config_entry_flow.py @@ -3,7 +3,7 @@ from __future__ import annotations from abc import ABC, abstractmethod -from collections.abc import Callable, Coroutine, Mapping +from collections.abc import Callable, Container, Coroutine, Mapping import copy from dataclasses import dataclass import types @@ -102,7 +102,7 @@ class SchemaFlowMenuStep(SchemaFlowStep): """Define a config or options flow menu step.""" # Menu options - options: list[str] | dict[str, str] + options: Container[str] class SchemaCommonFlowHandler: From c7cb0237d1afb600581f4cbfb41cf4e063d5cd39 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 11 Apr 2024 19:14:52 -0700 Subject: [PATCH 504/967] Fix bug in rainbird switch when turning off a switch that is already off (#115421) Fix big in rainbird switch when turning off a switch that is already off Co-authored-by: J. Nick Koston --- homeassistant/components/rainbird/switch.py | 3 ++- tests/components/rainbird/test_switch.py | 10 +++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/rainbird/switch.py b/homeassistant/components/rainbird/switch.py index a929f5b875b..7f43553aa41 100644 --- a/homeassistant/components/rainbird/switch.py +++ b/homeassistant/components/rainbird/switch.py @@ -123,7 +123,8 @@ class RainBirdSwitch(CoordinatorEntity[RainbirdUpdateCoordinator], SwitchEntity) # The device reflects the old state for a few moments. Update the # state manually and trigger a refresh after a short debounced delay. - self.coordinator.data.active_zones.remove(self._zone) + if self.is_on: + self.coordinator.data.active_zones.remove(self._zone) self.async_write_ha_state() await self.coordinator.async_request_refresh() diff --git a/tests/components/rainbird/test_switch.py b/tests/components/rainbird/test_switch.py index f87b7f121b5..1352a4a633d 100644 --- a/tests/components/rainbird/test_switch.py +++ b/tests/components/rainbird/test_switch.py @@ -146,20 +146,24 @@ async def test_switch_on( @pytest.mark.parametrize( - "zone_state_response", - [ZONE_3_ON_RESPONSE], + ("zone_state_response", "start_state"), + [ + (ZONE_3_ON_RESPONSE, "on"), + (ZONE_OFF_RESPONSE, "off"), # Already off + ], ) async def test_switch_off( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, responses: list[AiohttpClientMockResponse], + start_state: str, ) -> None: """Test turning off irrigation switch.""" # Initially the test zone is on zone = hass.states.get("switch.rain_bird_sprinkler_3") assert zone is not None - assert zone.state == "on" + assert zone.state == start_state aioclient_mock.mock_calls.clear() responses.extend( From 28bdbec14e3960e1d91bcf7899b8dd990f1015e9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 11 Apr 2024 16:16:01 -1000 Subject: [PATCH 505/967] Bypass ConfigEntry __setattr__ in __init__ (#115405) ConfigEntries.async_initialize was trigger asyncio warnings because of the CPU time to call __setattr__ for every variable for each ConfigEntry being loaded at startup --- homeassistant/config_entries.py | 50 +++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index dd48c53160e..7c1b590b1b0 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -282,7 +282,23 @@ class ConfigEntry: pref_disable_new_entities: bool pref_disable_polling: bool version: int + source: str minor_version: int + disabled_by: ConfigEntryDisabler | None + supports_unload: bool | None + supports_remove_device: bool | None + _supports_options: bool | None + _supports_reconfigure: bool | None + update_listeners: list[UpdateListenerType] + _async_cancel_retry_setup: Callable[[], Any] | None + _on_unload: list[Callable[[], Coroutine[Any, Any, None] | None]] | None + reload_lock: asyncio.Lock + _reauth_lock: asyncio.Lock + _reconfigure_lock: asyncio.Lock + _tasks: set[asyncio.Future[Any]] + _background_tasks: set[asyncio.Future[Any]] + _integration_for_domain: loader.Integration | None + _tries: int def __init__( self, @@ -334,7 +350,7 @@ class ConfigEntry: _setter(self, "pref_disable_polling", pref_disable_polling) # Source of the configuration (user, discovery, cloud) - self.source = source + _setter(self, "source", source) # State of the entry (LOADED, NOT_LOADED) _setter(self, "state", state) @@ -355,22 +371,22 @@ class ConfigEntry: error_if_core=False, ) disabled_by = ConfigEntryDisabler(disabled_by) - self.disabled_by = disabled_by + _setter(self, "disabled_by", disabled_by) # Supports unload - self.supports_unload: bool | None = None + _setter(self, "supports_unload", None) # Supports remove device - self.supports_remove_device: bool | None = None + _setter(self, "supports_remove_device", None) # Supports options - self._supports_options: bool | None = None + _setter(self, "_supports_options", None) # Supports reconfigure - self._supports_reconfigure: bool | None = None + _setter(self, "_supports_reconfigure", None) # Listeners to call on update - self.update_listeners: list[UpdateListenerType] = [] + _setter(self, "update_listeners", []) # Reason why config entry is in a failed state _setter(self, "reason", None) @@ -378,25 +394,23 @@ class ConfigEntry: _setter(self, "error_reason_translation_placeholders", None) # Function to cancel a scheduled retry - self._async_cancel_retry_setup: Callable[[], Any] | None = None + _setter(self, "_async_cancel_retry_setup", None) # Hold list for actions to call on unload. - self._on_unload: list[Callable[[], Coroutine[Any, Any, None] | None]] | None = ( - None - ) + _setter(self, "_on_unload", None) # Reload lock to prevent conflicting reloads - self.reload_lock = asyncio.Lock() + _setter(self, "reload_lock", asyncio.Lock()) # Reauth lock to prevent concurrent reauth flows - self._reauth_lock = asyncio.Lock() + _setter(self, "_reauth_lock", asyncio.Lock()) # Reconfigure lock to prevent concurrent reconfigure flows - self._reconfigure_lock = asyncio.Lock() + _setter(self, "_reconfigure_lock", asyncio.Lock()) - self._tasks: set[asyncio.Future[Any]] = set() - self._background_tasks: set[asyncio.Future[Any]] = set() + _setter(self, "_tasks", set()) + _setter(self, "_background_tasks", set()) - self._integration_for_domain: loader.Integration | None = None - self._tries = 0 + _setter(self, "_integration_for_domain", None) + _setter(self, "_tries", 0) def __repr__(self) -> str: """Representation of ConfigEntry.""" From fb5fc136e88335dab88425280b930e141deb11e0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 11 Apr 2024 16:32:47 -1000 Subject: [PATCH 506/967] Avoid falling back to event loop import on ModuleNotFound (#115404) --- homeassistant/loader.py | 4 ++ tests/test_loader.py | 90 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index da8159ca2cf..1a72c8eb351 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -976,6 +976,8 @@ class Integration: comp = await self.hass.async_add_import_executor_job( self._get_component, True ) + except ModuleNotFoundError: + raise except ImportError as ex: load_executor = False _LOGGER.debug( @@ -1115,6 +1117,8 @@ class Integration: self._load_platforms, platform_names ) ) + except ModuleNotFoundError: + raise except ImportError as ex: _LOGGER.debug( "Failed to import %s platforms %s in executor", diff --git a/tests/test_loader.py b/tests/test_loader.py index 41796f2f7d2..404858200bc 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -1471,6 +1471,50 @@ async def test_async_get_component_deadlock_fallback( assert module is module_mock +async def test_async_get_component_deadlock_fallback_module_not_found( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Verify async_get_component fallback behavior. + + Ensure that fallback is not triggered on ModuleNotFoundError. + """ + executor_import_integration = _get_test_integration( + hass, "executor_import", True, import_executor=True + ) + assert executor_import_integration.import_executor is True + module_mock = MagicMock(__file__="__init__.py") + import_attempts = 0 + + def mock_import(module: str, *args: Any, **kwargs: Any) -> Any: + nonlocal import_attempts + if module == "homeassistant.components.executor_import": + import_attempts += 1 + + if import_attempts == 1: + raise ModuleNotFoundError( + "homeassistant.components.executor_import not found", + name="homeassistant.components.executor_import", + ) + + return module_mock + + assert "homeassistant.components.executor_import" not in sys.modules + assert "custom_components.executor_import" not in sys.modules + with ( + patch("homeassistant.loader.importlib.import_module", mock_import), + pytest.raises( + ModuleNotFoundError, match="homeassistant.components.executor_import" + ), + ): + await executor_import_integration.async_get_component() + + # We should not have tried to fall back to the event loop import + assert "loaded_executor=False" not in caplog.text + assert "homeassistant.components.executor_import" not in sys.modules + assert "custom_components.executor_import" not in sys.modules + assert import_attempts == 1 + + async def test_async_get_component_raises_after_import_failure( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: @@ -1551,6 +1595,52 @@ async def test_async_get_platform_deadlock_fallback( assert module is module_mock +async def test_async_get_platform_deadlock_fallback_module_not_found( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Verify async_get_platform fallback behavior. + + Ensure that fallback is not triggered on ModuleNotFoundError. + """ + executor_import_integration = _get_test_integration( + hass, "executor_import", True, import_executor=True + ) + assert executor_import_integration.import_executor is True + module_mock = MagicMock() + import_attempts = 0 + + def mock_import(module: str, *args: Any, **kwargs: Any) -> Any: + nonlocal import_attempts + if module == "homeassistant.components.executor_import.config_flow": + import_attempts += 1 + + if import_attempts == 1: + raise ModuleNotFoundError( + "Not found homeassistant.components.executor_import.config_flow", + name="homeassistant.components.executor_import.config_flow", + ) + + return module_mock + + assert "homeassistant.components.executor_import" not in sys.modules + assert "custom_components.executor_import" not in sys.modules + with ( + patch("homeassistant.loader.importlib.import_module", mock_import), + pytest.raises( + ModuleNotFoundError, + match="homeassistant.components.executor_import.config_flow", + ), + ): + await executor_import_integration.async_get_platform("config_flow") + + # We should not have tried to fall back to the event loop import + assert "executor=['config_flow']" in caplog.text + assert "loop=['config_flow']" not in caplog.text + assert "homeassistant.components.executor_import" not in sys.modules + assert "custom_components.executor_import" not in sys.modules + assert import_attempts == 1 + + async def test_async_get_platform_raises_after_import_failure( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: From 6cc2b1e10a14e69643dae8e4224acbd7a02ed0df Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 12 Apr 2024 08:39:59 +0200 Subject: [PATCH 507/967] Use enum device class in Netatmo wind direction (#115413) * Use enum device class in Netatmo wind direction * Use enum device class in Netatmo wind direction --------- Co-authored-by: J. Nick Koston --- homeassistant/components/netatmo/sensor.py | 17 ++++++ homeassistant/components/netatmo/strings.json | 24 +++++++- .../netatmo/snapshots/test_sensor.ambr | 56 +++++++++++++++++-- tests/components/netatmo/test_sensor.py | 6 +- 4 files changed, 92 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 4e470437f7a..6e96a73135f 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -67,6 +67,17 @@ from .helper import NetatmoArea _LOGGER = logging.getLogger(__name__) +DIRECTION_OPTIONS = [ + "n", + "ne", + "e", + "se", + "s", + "sw", + "w", + "nw", +] + def process_health(health: StateType) -> str | None: """Process health index and return string for display.""" @@ -199,6 +210,9 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( NetatmoSensorEntityDescription( key="windangle", netatmo_name="wind_direction", + device_class=SensorDeviceClass.ENUM, + options=DIRECTION_OPTIONS, + value_fn=lambda x: x.lower() if isinstance(x, str) else None, ), NetatmoSensorEntityDescription( key="windangle_value", @@ -218,6 +232,9 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( key="gustangle", netatmo_name="gust_direction", entity_registry_enabled_default=False, + device_class=SensorDeviceClass.ENUM, + options=DIRECTION_OPTIONS, + value_fn=lambda x: x.lower() if isinstance(x, str) else None, ), NetatmoSensorEntityDescription( key="gustangle_value", diff --git a/homeassistant/components/netatmo/strings.json b/homeassistant/components/netatmo/strings.json index f6aba92d005..b8840c27006 100644 --- a/homeassistant/components/netatmo/strings.json +++ b/homeassistant/components/netatmo/strings.json @@ -185,13 +185,33 @@ "name": "Precipitation today" }, "wind_direction": { - "name": "Wind direction" + "name": "Wind direction", + "state": { + "n": "North", + "ne": "North-east", + "e": "East", + "se": "South-east", + "s": "South", + "sw": "South-west", + "w": "West", + "nw": "North-west" + } }, "wind_angle": { "name": "Wind angle" }, "gust_direction": { - "name": "Gust direction" + "name": "Gust direction", + "state": { + "n": "[%key:component::netatmo::entity::sensor::wind_direction::state::n%]", + "ne": "[%key:component::netatmo::entity::sensor::wind_direction::state::ne%]", + "e": "[%key:component::netatmo::entity::sensor::wind_direction::state::e%]", + "se": "[%key:component::netatmo::entity::sensor::wind_direction::state::se%]", + "s": "[%key:component::netatmo::entity::sensor::wind_direction::state::s%]", + "sw": "[%key:component::netatmo::entity::sensor::wind_direction::state::sw%]", + "w": "[%key:component::netatmo::entity::sensor::wind_direction::state::w%]", + "nw": "[%key:component::netatmo::entity::sensor::wind_direction::state::nw%]" + } }, "gust_angle": { "name": "Gust angle" diff --git a/tests/components/netatmo/snapshots/test_sensor.ambr b/tests/components/netatmo/snapshots/test_sensor.ambr index ed5f4decc86..b6dacb1911c 100644 --- a/tests/components/netatmo/snapshots/test_sensor.ambr +++ b/tests/components/netatmo/snapshots/test_sensor.ambr @@ -6073,7 +6073,18 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'options': list([ + 'n', + 'ne', + 'e', + 'se', + 's', + 'sw', + 'w', + 'nw', + ]), + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -6090,7 +6101,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Gust direction', 'platform': 'netatmo', @@ -6105,14 +6116,25 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', + 'device_class': 'enum', 'friendly_name': 'Villa Garden Gust direction', + 'options': list([ + 'n', + 'ne', + 'e', + 'se', + 's', + 'sw', + 'w', + 'nw', + ]), }), 'context': , 'entity_id': 'sensor.villa_garden_gust_direction', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'S', + 'state': 's', }) # --- # name: test_entity[sensor.villa_garden_gust_strength-entry] @@ -6317,7 +6339,18 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'options': list([ + 'n', + 'ne', + 'e', + 'se', + 's', + 'sw', + 'w', + 'nw', + ]), + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -6334,7 +6367,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Wind direction', 'platform': 'netatmo', @@ -6349,14 +6382,25 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', + 'device_class': 'enum', 'friendly_name': 'Villa Garden Wind direction', + 'options': list([ + 'n', + 'ne', + 'e', + 'se', + 's', + 'sw', + 'w', + 'nw', + ]), }), 'context': , 'entity_id': 'sensor.villa_garden_wind_direction', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'SW', + 'state': 'sw', }) # --- # name: test_entity[sensor.villa_garden_wind_speed-entry] diff --git a/tests/components/netatmo/test_sensor.py b/tests/components/netatmo/test_sensor.py index d2cc20b8394..4a6233e17e1 100644 --- a/tests/components/netatmo/test_sensor.py +++ b/tests/components/netatmo/test_sensor.py @@ -165,17 +165,17 @@ async def test_process_health(health: int, expected: str) -> None: ), ("12:34:56:80:c1:ea-sum_rain_1", "villa_rain_rain_last_hour", "0"), ("12:34:56:80:c1:ea-sum_rain_24", "villa_rain_rain_today", "6.9"), - ("12:34:56:03:1b:e4-windangle", "netatmoindoor_garden_direction", "SW"), + ("12:34:56:03:1b:e4-windangle", "netatmoindoor_garden_direction", "sw"), ( "12:34:56:03:1b:e4-windangle_value", "netatmoindoor_garden_angle", "217", ), - ("12:34:56:03:1b:e4-gustangle", "mystation_garden_gust_direction", "S"), + ("12:34:56:03:1b:e4-gustangle", "mystation_garden_gust_direction", "s"), ( "12:34:56:03:1b:e4-gustangle", "netatmoindoor_garden_gust_direction", - "S", + "s", ), ( "12:34:56:03:1b:e4-gustangle_value", From adafdb2b2d73bca550cbc833286768a8cbc97a0c Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 12 Apr 2024 08:40:27 +0200 Subject: [PATCH 508/967] Use enum device class in Netatmo health index sensor (#115409) --- homeassistant/components/netatmo/sensor.py | 17 ++- homeassistant/components/netatmo/strings.json | 9 +- .../netatmo/snapshots/test_sensor.ambr | 104 ++++++++++++++++-- tests/components/netatmo/test_sensor.py | 4 +- 4 files changed, 110 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 6e96a73135f..fd40bbf88b6 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -83,15 +83,12 @@ def process_health(health: StateType) -> str | None: """Process health index and return string for display.""" if not isinstance(health, int): return None - if health == 0: - return "Healthy" - if health == 1: - return "Fine" - if health == 2: - return "Fair" - if health == 3: - return "Poor" - return "Unhealthy" + return { + 0: "healthy", + 1: "fine", + 2: "fair", + 3: "poor", + }.get(health, "unhealthy") def process_rf(strength: StateType) -> str | None: @@ -274,6 +271,8 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( NetatmoSensorEntityDescription( key="health_idx", netatmo_name="health_idx", + device_class=SensorDeviceClass.ENUM, + options=["healthy", "fine", "fair", "poor", "unhealthy"], value_fn=process_health, ), NetatmoSensorEntityDescription( diff --git a/homeassistant/components/netatmo/strings.json b/homeassistant/components/netatmo/strings.json index b8840c27006..3c360634147 100644 --- a/homeassistant/components/netatmo/strings.json +++ b/homeassistant/components/netatmo/strings.json @@ -229,7 +229,14 @@ "name": "Wi-Fi" }, "health_idx": { - "name": "Health index" + "name": "Health index", + "state": { + "healthy": "Healthy", + "fine": "Fine", + "fair": "Fair", + "poor": "Poor", + "unhealthy": "Unhealthy" + } } } } diff --git a/tests/components/netatmo/snapshots/test_sensor.ambr b/tests/components/netatmo/snapshots/test_sensor.ambr index b6dacb1911c..0684956adb8 100644 --- a/tests/components/netatmo/snapshots/test_sensor.ambr +++ b/tests/components/netatmo/snapshots/test_sensor.ambr @@ -118,7 +118,15 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'options': list([ + 'healthy', + 'fine', + 'fair', + 'poor', + 'unhealthy', + ]), + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -135,7 +143,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Health index', 'platform': 'netatmo', @@ -150,16 +158,24 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', + 'device_class': 'enum', 'friendly_name': 'Baby Bedroom Health index', 'latitude': 13.377726, 'longitude': 52.516263, + 'options': list([ + 'healthy', + 'fine', + 'fair', + 'poor', + 'unhealthy', + ]), }), 'context': , 'entity_id': 'sensor.baby_bedroom_health_index', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'Fine', + 'state': 'fine', }) # --- # name: test_entity[sensor.baby_bedroom_humidity-entry] @@ -638,7 +654,15 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'options': list([ + 'healthy', + 'fine', + 'fair', + 'poor', + 'unhealthy', + ]), + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -655,7 +679,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Health index', 'platform': 'netatmo', @@ -670,7 +694,15 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', + 'device_class': 'enum', 'friendly_name': 'Bedroom Health index', + 'options': list([ + 'healthy', + 'fine', + 'fair', + 'poor', + 'unhealthy', + ]), }), 'context': , 'entity_id': 'sensor.bedroom_health_index', @@ -2845,7 +2877,15 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'options': list([ + 'healthy', + 'fine', + 'fair', + 'poor', + 'unhealthy', + ]), + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -2862,7 +2902,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Health index', 'platform': 'netatmo', @@ -2877,9 +2917,17 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', + 'device_class': 'enum', 'friendly_name': 'Kitchen Health index', 'latitude': 13.377726, 'longitude': 52.516263, + 'options': list([ + 'healthy', + 'fine', + 'fair', + 'poor', + 'unhealthy', + ]), }), 'context': , 'entity_id': 'sensor.kitchen_health_index', @@ -3916,7 +3964,15 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'options': list([ + 'healthy', + 'fine', + 'fair', + 'poor', + 'unhealthy', + ]), + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -3933,7 +3989,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Health index', 'platform': 'netatmo', @@ -3948,9 +4004,17 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', + 'device_class': 'enum', 'friendly_name': 'Livingroom Health index', 'latitude': 13.377726, 'longitude': 52.516263, + 'options': list([ + 'healthy', + 'fine', + 'fair', + 'poor', + 'unhealthy', + ]), }), 'context': , 'entity_id': 'sensor.livingroom_health_index', @@ -4440,7 +4504,15 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'options': list([ + 'healthy', + 'fine', + 'fair', + 'poor', + 'unhealthy', + ]), + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -4457,7 +4529,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Health index', 'platform': 'netatmo', @@ -4472,16 +4544,24 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', + 'device_class': 'enum', 'friendly_name': 'Parents Bedroom Health index', 'latitude': 13.377726, 'longitude': 52.516263, + 'options': list([ + 'healthy', + 'fine', + 'fair', + 'poor', + 'unhealthy', + ]), }), 'context': , 'entity_id': 'sensor.parents_bedroom_health_index', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'Fine', + 'state': 'fine', }) # --- # name: test_entity[sensor.parents_bedroom_humidity-entry] diff --git a/tests/components/netatmo/test_sensor.py b/tests/components/netatmo/test_sensor.py index 4a6233e17e1..4fa64e59b11 100644 --- a/tests/components/netatmo/test_sensor.py +++ b/tests/components/netatmo/test_sensor.py @@ -136,7 +136,7 @@ async def test_process_rf(strength: int, expected: str) -> None: @pytest.mark.parametrize( ("health", "expected"), - [(4, "Unhealthy"), (3, "Poor"), (2, "Fair"), (1, "Fine"), (0, "Healthy")], + [(4, "unhealthy"), (3, "poor"), (2, "fair"), (1, "fine"), (0, "healthy")], ) async def test_process_health(health: int, expected: str) -> None: """Test health index translation.""" @@ -195,7 +195,7 @@ async def test_process_health(health: int, expected: str) -> None: ( "12:34:56:26:68:92-health_idx", "baby_bedroom_health", - "Fine", + "fine", ), ( "12:34:56:26:68:92-wifi_status", From 35d3f2b29b8b920d00f3f192da339efcf98e1ff3 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Fri, 12 Apr 2024 09:02:22 +0200 Subject: [PATCH 509/967] Support backup of add-ons with hyphens (#115274) Co-authored-by: J. Nick Koston --- homeassistant/components/hassio/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index ce3b5b05ffe..972942caf52 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -196,7 +196,7 @@ SCHEMA_BACKUP_PARTIAL = SCHEMA_BACKUP_FULL.extend( { vol.Optional(ATTR_HOMEASSISTANT): cv.boolean, vol.Optional(ATTR_FOLDERS): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(ATTR_ADDONS): vol.All(cv.ensure_list, [cv.slug]), + vol.Optional(ATTR_ADDONS): vol.All(cv.ensure_list, [VALID_ADDON_SLUG]), } ) @@ -211,7 +211,7 @@ SCHEMA_RESTORE_PARTIAL = SCHEMA_RESTORE_FULL.extend( { vol.Optional(ATTR_HOMEASSISTANT): cv.boolean, vol.Optional(ATTR_FOLDERS): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(ATTR_ADDONS): vol.All(cv.ensure_list, [cv.slug]), + vol.Optional(ATTR_ADDONS): vol.All(cv.ensure_list, [VALID_ADDON_SLUG]), } ) From 9bf87329da3d6dc9a129e37b6e5862a5c4aba89c Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Fri, 12 Apr 2024 09:04:16 +0200 Subject: [PATCH 510/967] Enable Ruff FLY002 rule (#115112) Co-authored-by: J. Nick Koston Co-authored-by: Jan Bouwhuis --- .../components/azure_devops/__init__.py | 4 +- homeassistant/components/citybikes/sensor.py | 4 +- homeassistant/components/mqtt/discovery.py | 2 +- homeassistant/components/nextbus/sensor.py | 6 +- .../components/nmap_tracker/config_flow.py | 2 +- .../components/opower/coordinator.py | 10 +-- .../components/withings/config_flow.py | 10 +-- pyproject.toml | 1 + script/hassfest/mypy_config.py | 18 +---- .../test_device_trigger.py | 30 +++----- .../binary_sensor/test_device_condition.py | 24 +++--- .../binary_sensor/test_device_trigger.py | 60 ++++++--------- tests/components/cover/test_device_trigger.py | 15 ++-- tests/components/demo/test_vacuum.py | 4 +- .../components/device_automation/test_init.py | 12 ++- .../device_automation/test_toggle_entity.py | 60 ++++++--------- tests/components/fan/test_device_trigger.py | 15 ++-- tests/components/geo_location/test_trigger.py | 62 +++++++-------- .../triggers/test_numeric_state.py | 24 +++--- .../homeassistant/triggers/test_state.py | 47 +++++------- tests/components/homekit_controller/common.py | 2 +- .../humidifier/test_device_condition.py | 12 ++- .../humidifier/test_device_trigger.py | 45 +++++------ .../components/light/test_device_condition.py | 24 +++--- tests/components/light/test_device_trigger.py | 63 ++++------------ tests/components/lock/test_device_trigger.py | 60 ++++++--------- .../media_player/test_device_trigger.py | 15 ++-- tests/components/netatmo/test_init.py | 8 +- .../remote/test_device_condition.py | 24 +++--- .../components/remote/test_device_trigger.py | 75 ++++++++----------- .../sensor/test_device_condition.py | 24 ++++-- .../components/sensor/test_device_trigger.py | 75 ++++++++----------- tests/components/sun/test_trigger.py | 7 +- .../switch/test_device_condition.py | 24 +++--- .../components/switch/test_device_trigger.py | 75 ++++++++----------- tests/components/template/test_trigger.py | 75 ++++++++----------- .../components/update/test_device_trigger.py | 60 ++++++--------- .../components/vacuum/test_device_trigger.py | 15 ++-- tests/components/xbox/test_config_flow.py | 2 +- tests/components/zone/test_trigger.py | 34 ++++----- tests/scripts/test_auth.py | 4 +- 41 files changed, 474 insertions(+), 659 deletions(-) diff --git a/homeassistant/components/azure_devops/__init__.py b/homeassistant/components/azure_devops/__init__.py index deda8f466a6..537019fb9c1 100644 --- a/homeassistant/components/azure_devops/__init__.py +++ b/homeassistant/components/azure_devops/__init__.py @@ -118,8 +118,8 @@ class AzureDevOpsEntity(CoordinatorEntity[DataUpdateCoordinator[list[DevOpsBuild """Initialize the Azure DevOps entity.""" super().__init__(coordinator) self.entity_description = entity_description - self._attr_unique_id: str = "_".join( - [entity_description.organization, entity_description.key] + 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 diff --git a/homeassistant/components/citybikes/sensor.py b/homeassistant/components/citybikes/sensor.py index de85e6309f9..4049a656caf 100644 --- a/homeassistant/components/citybikes/sensor.py +++ b/homeassistant/components/citybikes/sensor.py @@ -201,9 +201,9 @@ async def async_setup_platform( if radius > dist or stations_list.intersection((station_id, station_uid)): if name: - uid = "_".join([network.network_id, name, station_id]) + uid = f"{network.network_id}_{name}_{station_id}" else: - uid = "_".join([network.network_id, station_id]) + uid = f"{network.network_id}_{station_id}" entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, uid, hass=hass) devices.append(CityBikesStation(network, station_id, entity_id)) diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 13c56a9b48e..43f4f8cfd46 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -268,7 +268,7 @@ async def async_start( # noqa: C901 availability_conf[CONF_TOPIC] = f"{topic[:-1]}{base}" # If present, the node_id will be included in the discovered object id - discovery_id = " ".join((node_id, object_id)) if node_id else object_id + discovery_id = f"{node_id} {object_id}" if node_id else object_id discovery_hash = (component, discovery_id) if discovery_payload: diff --git a/homeassistant/components/nextbus/sensor.py b/homeassistant/components/nextbus/sensor.py index 5f89d0d79db..8cd0d177835 100644 --- a/homeassistant/components/nextbus/sensor.py +++ b/homeassistant/components/nextbus/sensor.py @@ -82,11 +82,13 @@ class NextBusDepartureSensor( def _log_debug(self, message, *args): """Log debug message with prefix.""" - _LOGGER.debug(":".join((self.agency, self.route, self.stop, message)), *args) + msg = f"{self.agency}:{self.route}:{self.stop}:{message}" + _LOGGER.debug(msg, *args) def _log_err(self, message, *args): """Log error message with prefix.""" - _LOGGER.error(":".join((self.agency, self.route, self.stop, message)), *args) + msg = f"{self.agency}:{self.route}:{self.stop}:{message}" + _LOGGER.error(msg, *args) async def async_added_to_hass(self) -> None: """Read data from coordinator after adding to hass.""" diff --git a/homeassistant/components/nmap_tracker/config_flow.py b/homeassistant/components/nmap_tracker/config_flow.py index 6128272fbbb..a89c50a2210 100644 --- a/homeassistant/components/nmap_tracker/config_flow.py +++ b/homeassistant/components/nmap_tracker/config_flow.py @@ -62,7 +62,7 @@ def _normalize_ips_and_network(hosts_str: str) -> list[str] | None: start, end = host.split("-", 1) if "." not in end: ip_1, ip_2, ip_3, _ = start.split(".", 3) - end = ".".join([ip_1, ip_2, ip_3, end]) + end = f"{ip_1}.{ip_2}.{ip_3}.{end}" summarize_address_range(ip_address(start), ip_address(end)) except ValueError: pass diff --git a/homeassistant/components/opower/coordinator.py b/homeassistant/components/opower/coordinator.py index d4cce99e1cc..94a56bb1922 100644 --- a/homeassistant/components/opower/coordinator.py +++ b/homeassistant/components/opower/coordinator.py @@ -159,13 +159,9 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): ) ) - name_prefix = " ".join( - ( - "Opower", - self.api.utility.subdomain(), - account.meter_type.name.lower(), - account.utility_account_id, - ) + name_prefix = ( + f"Opower {self.api.utility.subdomain()} " + f"{account.meter_type.name.lower()} {account.utility_account_id}" ) cost_metadata = StatisticMetaData( has_mean=False, diff --git a/homeassistant/components/withings/config_flow.py b/homeassistant/components/withings/config_flow.py index aee25da507c..c90455de7ec 100644 --- a/homeassistant/components/withings/config_flow.py +++ b/homeassistant/components/withings/config_flow.py @@ -34,14 +34,8 @@ class WithingsFlowHandler( def extra_authorize_data(self) -> dict[str, str]: """Extra data that needs to be appended to the authorize url.""" return { - "scope": ",".join( - [ - AuthScope.USER_INFO, - AuthScope.USER_METRICS, - AuthScope.USER_ACTIVITY, - AuthScope.USER_SLEEP_EVENTS, - ] - ) + "scope": f"{AuthScope.USER_INFO},{AuthScope.USER_METRICS}," + f"{AuthScope.USER_ACTIVITY},{AuthScope.USER_SLEEP_EVENTS}" } async def async_step_reauth( diff --git a/pyproject.toml b/pyproject.toml index 8be7b9b40f3..5ea335115ca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -679,6 +679,7 @@ select = [ "DTZ004", # Use datetime.fromtimestamp(ts, tz=) instead of datetime.utcfromtimestamp(ts) "E", # pycodestyle "F", # pyflakes/autoflake + "FLY", # flynt "G", # flake8-logging-format "I", # isort "INP", # flake8-no-pep420 diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index c02ebd8de2e..c6c5907cdb9 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -32,7 +32,7 @@ HEADER: Final = """ GENERAL_SETTINGS: Final[dict[str, str]] = { "python_version": ".".join(str(x) for x in REQUIRED_PYTHON_VER[:2]), - "plugins": ", ".join(["pydantic.mypy"]), + "plugins": "pydantic.mypy", "show_error_codes": "true", "follow_imports": "silent", # Enable some checks globally. @@ -43,20 +43,8 @@ GENERAL_SETTINGS: Final[dict[str, str]] = { "warn_redundant_casts": "true", "warn_unused_configs": "true", "warn_unused_ignores": "true", - "enable_error_code": ", ".join( - [ - "ignore-without-code", - "redundant-self", - "truthy-iterable", - ] - ), - "disable_error_code": ", ".join( - [ - "annotation-unchecked", - "import-not-found", - "import-untyped", - ] - ), + "enable_error_code": "ignore-without-code, redundant-self, truthy-iterable", + "disable_error_code": "annotation-unchecked, import-not-found, import-untyped", # Impractical in real code # E.g. this breaks passthrough ParamSpec typing with Concatenate "extra_checks": "false", diff --git a/tests/components/alarm_control_panel/test_device_trigger.py b/tests/components/alarm_control_panel/test_device_trigger.py index 00cdc5ddbee..fb2d4e0a504 100644 --- a/tests/components/alarm_control_panel/test_device_trigger.py +++ b/tests/components/alarm_control_panel/test_device_trigger.py @@ -497,15 +497,12 @@ async def test_if_fires_on_state_change_with_for( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -564,15 +561,12 @@ async def test_if_fires_on_state_change_legacy( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, diff --git a/tests/components/binary_sensor/test_device_condition.py b/tests/components/binary_sensor/test_device_condition.py index 93689b4f233..6837c882a01 100644 --- a/tests/components/binary_sensor/test_device_condition.py +++ b/tests/components/binary_sensor/test_device_condition.py @@ -275,8 +275,10 @@ async def test_if_state( "action": { "service": "test.automation", "data_template": { - "some": "is_on {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) + "some": ( + "is_on {{ trigger.platform }}" + " - {{ trigger.event.event_type }}" + ) }, }, }, @@ -294,8 +296,10 @@ async def test_if_state( "action": { "service": "test.automation", "data_template": { - "some": "is_off {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) + "some": ( + "is_off {{ trigger.platform }}" + " - {{ trigger.event.event_type }}" + ) }, }, }, @@ -359,8 +363,10 @@ async def test_if_state_legacy( "action": { "service": "test.automation", "data_template": { - "some": "is_on {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) + "some": ( + "is_on {{ trigger.platform }}" + " - {{ trigger.event.event_type }}" + ) }, }, }, @@ -421,9 +427,9 @@ async def test_if_fires_on_for_condition( "action": { "service": "test.automation", "data_template": { - "some": "is_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ("platform", "event.event_type") + "some": ( + "is_off {{ trigger.platform }}" + " - {{ trigger.event.event_type }}" ) }, }, diff --git a/tests/components/binary_sensor/test_device_trigger.py b/tests/components/binary_sensor/test_device_trigger.py index 76dcdb33993..dd55682fc8d 100644 --- a/tests/components/binary_sensor/test_device_trigger.py +++ b/tests/components/binary_sensor/test_device_trigger.py @@ -277,15 +277,12 @@ async def test_if_fires_on_state_change( "action": { "service": "test.automation", "data_template": { - "some": "bat_low {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "bat_low {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -301,15 +298,12 @@ async def test_if_fires_on_state_change( "action": { "service": "test.automation", "data_template": { - "some": "not_bat_low {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "not_bat_low {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -379,15 +373,12 @@ async def test_if_fires_on_state_change_with_for( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -453,15 +444,12 @@ async def test_if_fires_on_state_change_legacy( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, diff --git a/tests/components/cover/test_device_trigger.py b/tests/components/cover/test_device_trigger.py index afd39fe6d8e..8e2f794f1e0 100644 --- a/tests/components/cover/test_device_trigger.py +++ b/tests/components/cover/test_device_trigger.py @@ -625,15 +625,12 @@ async def test_if_fires_on_state_change_with_for( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, diff --git a/tests/components/demo/test_vacuum.py b/tests/components/demo/test_vacuum.py index e70f0144e6a..a3b982ab70e 100644 --- a/tests/components/demo/test_vacuum.py +++ b/tests/components/demo/test_vacuum.py @@ -219,7 +219,7 @@ async def test_services(hass: HomeAssistant) -> None: async def test_set_fan_speed(hass: HomeAssistant) -> None: """Test vacuum service to set the fan speed.""" - group_vacuums = ",".join([ENTITY_VACUUM_COMPLETE, ENTITY_VACUUM_MOST]) + group_vacuums = f"{ENTITY_VACUUM_COMPLETE},{ENTITY_VACUUM_MOST}" old_state_complete = hass.states.get(ENTITY_VACUUM_COMPLETE) old_state_most = hass.states.get(ENTITY_VACUUM_MOST) @@ -239,7 +239,7 @@ async def test_set_fan_speed(hass: HomeAssistant) -> None: async def test_send_command(hass: HomeAssistant) -> None: """Test vacuum service to send a command.""" - group_vacuums = ",".join([ENTITY_VACUUM_COMPLETE]) + group_vacuums = f"{ENTITY_VACUUM_COMPLETE}" old_state_complete = hass.states.get(ENTITY_VACUUM_COMPLETE) await common.async_send_command( diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index ac5e490b738..4526a9d9b67 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -1446,8 +1446,10 @@ async def test_automation_with_sub_condition( "action": { "service": "test.automation", "data_template": { - "some": "and {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) + "some": ( + "and {{ trigger.platform }}" + " - {{ trigger.event.event_type }}" + ) }, }, }, @@ -1477,8 +1479,10 @@ async def test_automation_with_sub_condition( "action": { "service": "test.automation", "data_template": { - "some": "or {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) + "some": ( + "or {{ trigger.platform }}" + " - {{ trigger.event.event_type }}" + ) }, }, }, diff --git a/tests/components/device_automation/test_toggle_entity.py b/tests/components/device_automation/test_toggle_entity.py index 44a29d4a9ba..a8850bf50b9 100644 --- a/tests/components/device_automation/test_toggle_entity.py +++ b/tests/components/device_automation/test_toggle_entity.py @@ -64,15 +64,12 @@ async def test_if_fires_on_state_change( "action": { "service": "test.automation", "data_template": { - "some": "turn_on {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_on {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -88,15 +85,12 @@ async def test_if_fires_on_state_change( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -112,15 +106,12 @@ async def test_if_fires_on_state_change( "action": { "service": "test.automation", "data_template": { - "some": "turn_on_or_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_on_or_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -187,15 +178,12 @@ async def test_if_fires_on_state_change_with_for( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, diff --git a/tests/components/fan/test_device_trigger.py b/tests/components/fan/test_device_trigger.py index c121569184f..a217a5d89ec 100644 --- a/tests/components/fan/test_device_trigger.py +++ b/tests/components/fan/test_device_trigger.py @@ -385,15 +385,12 @@ async def test_if_fires_on_state_change_with_for( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, diff --git a/tests/components/geo_location/test_trigger.py b/tests/components/geo_location/test_trigger.py index 85461d60aac..b8045ad495c 100644 --- a/tests/components/geo_location/test_trigger.py +++ b/tests/components/geo_location/test_trigger.py @@ -72,16 +72,13 @@ async def test_if_fires_on_zone_enter(hass: HomeAssistant, calls) -> None: "action": { "service": "test.automation", "data_template": { - "some": "{{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "zone.name", - "id", - ) + "some": ( + "{{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.zone.name }}" + " - {{ trigger.id }}" ) }, }, @@ -285,15 +282,12 @@ async def test_if_fires_on_zone_appear(hass: HomeAssistant, calls) -> None: "action": { "service": "test.automation", "data_template": { - "some": "{{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "zone.name", - ) + "some": ( + "{{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.zone.name }}" ) }, }, @@ -334,15 +328,12 @@ async def test_if_fires_on_zone_appear_2(hass: HomeAssistant, calls) -> None: "action": { "service": "test.automation", "data_template": { - "some": "{{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "zone.name", - ) + "some": ( + "{{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.zone.name }}" ) }, }, @@ -399,15 +390,12 @@ async def test_if_fires_on_zone_disappear(hass: HomeAssistant, calls) -> None: "action": { "service": "test.automation", "data_template": { - "some": "{{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "zone.name", - ) + "some": ( + "{{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.zone.name }}" ) }, }, diff --git a/tests/components/homeassistant/triggers/test_numeric_state.py b/tests/components/homeassistant/triggers/test_numeric_state.py index cf2e1938228..2e2dca5b57a 100644 --- a/tests/components/homeassistant/triggers/test_numeric_state.py +++ b/tests/components/homeassistant/triggers/test_numeric_state.py @@ -980,16 +980,13 @@ async def test_template_string(hass: HomeAssistant, calls, below) -> None: "action": { "service": "test.automation", "data_template": { - "some": "{{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "below", - "above", - "from_state.state", - "to_state.state", - ) + "some": ( + "{{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.below }}" + " - {{ trigger.above }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" ) }, }, @@ -1346,9 +1343,10 @@ async def test_wait_template_with_trigger(hass: HomeAssistant, calls, above) -> { "service": "test.automation", "data_template": { - "some": "{{ trigger.%s }}" - % "}} - {{ trigger.".join( - ("platform", "entity_id", "to_state.state") + "some": ( + "{{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.to_state.state }}" ) }, }, diff --git a/tests/components/homeassistant/triggers/test_state.py b/tests/components/homeassistant/triggers/test_state.py index aaf228c06f8..597ef0ab1a5 100644 --- a/tests/components/homeassistant/triggers/test_state.py +++ b/tests/components/homeassistant/triggers/test_state.py @@ -55,16 +55,13 @@ async def test_if_fires_on_entity_change(hass: HomeAssistant, calls) -> None: "action": { "service": "test.automation", "data_template": { - "some": "{{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - "id", - ) + "some": ( + "{{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" + " - {{ trigger.id }}" ) }, }, @@ -114,16 +111,13 @@ async def test_if_fires_on_entity_change_uuid( "action": { "service": "test.automation", "data_template": { - "some": "{{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - "id", - ) + "some": ( + "{{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" + " - {{ trigger.id }}" ) }, }, @@ -1079,14 +1073,11 @@ async def test_wait_template_with_trigger(hass: HomeAssistant, calls) -> None: { "service": "test.automation", "data_template": { - "some": "{{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - ) + "some": ( + "{{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" ) }, }, diff --git a/tests/components/homekit_controller/common.py b/tests/components/homekit_controller/common.py index 8c45080c786..95bf2530b2d 100644 --- a/tests/components/homekit_controller/common.py +++ b/tests/components/homekit_controller/common.py @@ -306,7 +306,7 @@ async def setup_test_component( config_entry, pairing = await setup_test_accessories(hass, [accessory], connection) entity = "testdevice" if suffix is None else f"testdevice_{suffix}" - return Helper(hass, ".".join((domain, entity)), pairing, accessory, config_entry) + return Helper(hass, f"{domain}.{entity}", pairing, accessory, config_entry) async def assert_devices_and_entities_created( diff --git a/tests/components/humidifier/test_device_condition.py b/tests/components/humidifier/test_device_condition.py index ad4ac78d064..14ed9fae5e0 100644 --- a/tests/components/humidifier/test_device_condition.py +++ b/tests/components/humidifier/test_device_condition.py @@ -187,8 +187,10 @@ async def test_if_state( "action": { "service": "test.automation", "data_template": { - "some": "is_on {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) + "some": ( + "is_on {{ trigger.platform }}" + " - {{ trigger.event.event_type }}" + ) }, }, }, @@ -206,8 +208,10 @@ async def test_if_state( "action": { "service": "test.automation", "data_template": { - "some": "is_off {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) + "some": ( + "is_off {{ trigger.platform }}" + " - {{ trigger.event.event_type }}" + ) }, }, }, diff --git a/tests/components/humidifier/test_device_trigger.py b/tests/components/humidifier/test_device_trigger.py index e064e82a385..fd6441588c4 100644 --- a/tests/components/humidifier/test_device_trigger.py +++ b/tests/components/humidifier/test_device_trigger.py @@ -293,15 +293,12 @@ async def test_if_fires_on_state_change( "action": { "service": "test.automation", "data_template": { - "some": "turn_on {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_on {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -317,15 +314,12 @@ async def test_if_fires_on_state_change( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -341,15 +335,12 @@ async def test_if_fires_on_state_change( "action": { "service": "test.automation", "data_template": { - "some": "turn_on_or_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_on_or_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, diff --git a/tests/components/light/test_device_condition.py b/tests/components/light/test_device_condition.py index caaa51e86fa..eeee8530085 100644 --- a/tests/components/light/test_device_condition.py +++ b/tests/components/light/test_device_condition.py @@ -219,8 +219,10 @@ async def test_if_state( "action": { "service": "test.automation", "data_template": { - "some": "is_on {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) + "some": ( + "is_on {{ trigger.platform }}" + " - {{ trigger.event.event_type }}" + ) }, }, }, @@ -238,8 +240,10 @@ async def test_if_state( "action": { "service": "test.automation", "data_template": { - "some": "is_off {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) + "some": ( + "is_off {{ trigger.platform }}" + " - {{ trigger.event.event_type }}" + ) }, }, }, @@ -302,8 +306,10 @@ async def test_if_state_legacy( "action": { "service": "test.automation", "data_template": { - "some": "is_on {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) + "some": ( + "is_on {{ trigger.platform }}" + " - {{ trigger.event.event_type }}" + ) }, }, }, @@ -367,9 +373,9 @@ async def test_if_fires_on_for_condition( "action": { "service": "test.automation", "data_template": { - "some": "is_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ("platform", "event.event_type") + "some": ( + "is_off {{ trigger.platform }}" + " - {{ trigger.event.event_type }}" ) }, }, diff --git a/tests/components/light/test_device_trigger.py b/tests/components/light/test_device_trigger.py index ea1c1c66b21..c38ab14061f 100644 --- a/tests/components/light/test_device_trigger.py +++ b/tests/components/light/test_device_trigger.py @@ -23,6 +23,14 @@ from tests.common import ( async_mock_service, ) +DATA_TEMPLATE_ATTRIBUTES = ( + "{{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" +) + @pytest.fixture(autouse=True, name="stub_blueprint_populate") def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: @@ -212,16 +220,7 @@ async def test_if_fires_on_state_change( "action": { "service": "test.automation", "data_template": { - "some": "turn_on {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) - ) + "some": "turn_on " + DATA_TEMPLATE_ATTRIBUTES }, }, }, @@ -236,16 +235,7 @@ async def test_if_fires_on_state_change( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) - ) + "some": "turn_off " + DATA_TEMPLATE_ATTRIBUTES }, }, }, @@ -260,16 +250,7 @@ async def test_if_fires_on_state_change( "action": { "service": "test.automation", "data_template": { - "some": "turn_on_or_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) - ) + "some": "turn_on_or_off " + DATA_TEMPLATE_ATTRIBUTES }, }, }, @@ -332,16 +313,7 @@ async def test_if_fires_on_state_change_legacy( "action": { "service": "test.automation", "data_template": { - "some": "turn_on {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) - ) + "some": "turn_on " + DATA_TEMPLATE_ATTRIBUTES }, }, }, @@ -396,16 +368,7 @@ async def test_if_fires_on_state_change_with_for( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) - ) + "some": "turn_off " + DATA_TEMPLATE_ATTRIBUTES }, }, } diff --git a/tests/components/lock/test_device_trigger.py b/tests/components/lock/test_device_trigger.py index 3f518143285..3ad992d4458 100644 --- a/tests/components/lock/test_device_trigger.py +++ b/tests/components/lock/test_device_trigger.py @@ -363,15 +363,12 @@ async def test_if_fires_on_state_change_with_for( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -388,15 +385,12 @@ async def test_if_fires_on_state_change_with_for( "action": { "service": "test.automation", "data_template": { - "some": "turn_on {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_on {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -413,15 +407,12 @@ async def test_if_fires_on_state_change_with_for( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -438,15 +429,12 @@ async def test_if_fires_on_state_change_with_for( "action": { "service": "test.automation", "data_template": { - "some": "turn_on {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_on {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, diff --git a/tests/components/media_player/test_device_trigger.py b/tests/components/media_player/test_device_trigger.py index ab11683889d..4c507b4bd66 100644 --- a/tests/components/media_player/test_device_trigger.py +++ b/tests/components/media_player/test_device_trigger.py @@ -412,15 +412,12 @@ async def test_if_fires_on_state_change_with_for( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, diff --git a/tests/components/netatmo/test_init.py b/tests/components/netatmo/test_init.py index 672084d644d..55af74b3373 100644 --- a/tests/components/netatmo/test_init.py +++ b/tests/components/netatmo/test_init.py @@ -393,13 +393,7 @@ async def test_setup_component_invalid_token_scope(hass: HomeAssistant) -> None: "type": "Bearer", "expires_in": 60, "expires_at": time() + 1000, - "scope": " ".join( - [ - "read_smokedetector", - "read_thermostat", - "write_thermostat", - ] - ), + "scope": "read_smokedetector read_thermostat write_thermostat", }, }, options={}, diff --git a/tests/components/remote/test_device_condition.py b/tests/components/remote/test_device_condition.py index edfa7c5adf9..4fd14e82990 100644 --- a/tests/components/remote/test_device_condition.py +++ b/tests/components/remote/test_device_condition.py @@ -217,8 +217,10 @@ async def test_if_state( "action": { "service": "test.automation", "data_template": { - "some": "is_on {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) + "some": ( + "is_on {{ trigger.platform }}" + " - {{ trigger.event.event_type }}" + ) }, }, }, @@ -236,8 +238,10 @@ async def test_if_state( "action": { "service": "test.automation", "data_template": { - "some": "is_off {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) + "some": ( + "is_off {{ trigger.platform }}" + " - {{ trigger.event.event_type }}" + ) }, }, }, @@ -300,8 +304,10 @@ async def test_if_state_legacy( "action": { "service": "test.automation", "data_template": { - "some": "is_on {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) + "some": ( + "is_on {{ trigger.platform }}" + " - {{ trigger.event.event_type }}" + ) }, }, }, @@ -361,9 +367,9 @@ async def test_if_fires_on_for_condition( "action": { "service": "test.automation", "data_template": { - "some": "is_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ("platform", "event.event_type") + "some": ( + "is_off {{ trigger.platform }}" + " - {{ trigger.event.event_type }}" ) }, }, diff --git a/tests/components/remote/test_device_trigger.py b/tests/components/remote/test_device_trigger.py index 1f80843be9a..68f7215186f 100644 --- a/tests/components/remote/test_device_trigger.py +++ b/tests/components/remote/test_device_trigger.py @@ -212,15 +212,12 @@ async def test_if_fires_on_state_change( "action": { "service": "test.automation", "data_template": { - "some": "turn_on {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_on {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -236,15 +233,12 @@ async def test_if_fires_on_state_change( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -260,15 +254,12 @@ async def test_if_fires_on_state_change( "action": { "service": "test.automation", "data_template": { - "some": "turn_on_or_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_on_or_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -331,15 +322,12 @@ async def test_if_fires_on_state_change_legacy( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -395,15 +383,12 @@ async def test_if_fires_on_state_change_with_for( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, diff --git a/tests/components/sensor/test_device_condition.py b/tests/components/sensor/test_device_condition.py index 08de630f025..2a142633ab3 100644 --- a/tests/components/sensor/test_device_condition.py +++ b/tests/components/sensor/test_device_condition.py @@ -545,8 +545,10 @@ async def test_if_state_above( "action": { "service": "test.automation", "data_template": { - "some": "{{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) + "some": ( + "{{ trigger.platform }}" + " - {{ trigger.event.event_type }}" + ) }, }, } @@ -612,8 +614,10 @@ async def test_if_state_above_legacy( "action": { "service": "test.automation", "data_template": { - "some": "{{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) + "some": ( + "{{ trigger.platform }}" + " - {{ trigger.event.event_type }}" + ) }, }, } @@ -679,8 +683,10 @@ async def test_if_state_below( "action": { "service": "test.automation", "data_template": { - "some": "{{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) + "some": ( + "{{ trigger.platform }}" + " - {{ trigger.event.event_type }}" + ) }, }, } @@ -747,8 +753,10 @@ async def test_if_state_between( "action": { "service": "test.automation", "data_template": { - "some": "{{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) + "some": ( + "{{ trigger.platform }}" + " - {{ trigger.event.event_type }}" + ) }, }, } diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index bb7337c0144..49e00a927b4 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -496,15 +496,12 @@ async def test_if_fires_on_state_above( "action": { "service": "test.automation", "data_template": { - "some": "bat_low {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "bat_low {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -564,15 +561,12 @@ async def test_if_fires_on_state_below( "action": { "service": "test.automation", "data_template": { - "some": "bat_low {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "bat_low {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -633,15 +627,12 @@ async def test_if_fires_on_state_between( "action": { "service": "test.automation", "data_template": { - "some": "bat_low {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "bat_low {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -712,15 +703,12 @@ async def test_if_fires_on_state_legacy( "action": { "service": "test.automation", "data_template": { - "some": "bat_low {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "bat_low {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -781,15 +769,12 @@ async def test_if_fires_on_state_change_with_for( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, diff --git a/tests/components/sun/test_trigger.py b/tests/components/sun/test_trigger.py index 50e070a4f68..e315ea8cdcd 100644 --- a/tests/components/sun/test_trigger.py +++ b/tests/components/sun/test_trigger.py @@ -127,8 +127,11 @@ async def test_sunset_trigger_with_offset(hass: HomeAssistant, calls) -> None: "action": { "service": "test.automation", "data_template": { - "some": "{{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event", "offset")) + "some": ( + "{{ trigger.platform }}" + " - {{ trigger.event }}" + " - {{ trigger.offset }}" + ) }, }, } diff --git a/tests/components/switch/test_device_condition.py b/tests/components/switch/test_device_condition.py index e351daf2a5b..cd0a67fa992 100644 --- a/tests/components/switch/test_device_condition.py +++ b/tests/components/switch/test_device_condition.py @@ -217,8 +217,10 @@ async def test_if_state( "action": { "service": "test.automation", "data_template": { - "some": "is_on {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) + "some": ( + "is_on {{ trigger.platform }}" + " - {{ trigger.event.event_type }}" + ) }, }, }, @@ -236,8 +238,10 @@ async def test_if_state( "action": { "service": "test.automation", "data_template": { - "some": "is_off {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) + "some": ( + "is_off {{ trigger.platform }}" + " - {{ trigger.event.event_type }}" + ) }, }, }, @@ -300,8 +304,10 @@ async def test_if_state_legacy( "action": { "service": "test.automation", "data_template": { - "some": "is_on {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) + "some": ( + "is_on {{ trigger.platform }}" + " - {{ trigger.event.event_type }}" + ) }, }, }, @@ -360,9 +366,9 @@ async def test_if_fires_on_for_condition( "action": { "service": "test.automation", "data_template": { - "some": "is_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ("platform", "event.event_type") + "some": ( + "is_off {{ trigger.platform }}" + " - {{ trigger.event.event_type }}" ) }, }, diff --git a/tests/components/switch/test_device_trigger.py b/tests/components/switch/test_device_trigger.py index 58803b0c6ac..c528f982ebb 100644 --- a/tests/components/switch/test_device_trigger.py +++ b/tests/components/switch/test_device_trigger.py @@ -212,15 +212,12 @@ async def test_if_fires_on_state_change( "action": { "service": "test.automation", "data_template": { - "some": "turn_on {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_on {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -236,15 +233,12 @@ async def test_if_fires_on_state_change( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -260,15 +254,12 @@ async def test_if_fires_on_state_change( "action": { "service": "test.automation", "data_template": { - "some": "turn_on_or_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_on_or_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -332,15 +323,12 @@ async def test_if_fires_on_state_change_legacy( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -397,15 +385,12 @@ async def test_if_fires_on_state_change_with_for( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, diff --git a/tests/components/template/test_trigger.py b/tests/components/template/test_trigger.py index 0d7d765b988..0f95503c333 100644 --- a/tests/components/template/test_trigger.py +++ b/tests/components/template/test_trigger.py @@ -329,15 +329,12 @@ async def test_if_not_fires_because_fail( "action": { "service": "test.automation", "data_template": { - "some": "{{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "{{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -430,15 +427,12 @@ async def test_if_fires_on_change_with_bad_template( { "service": "test.automation", "data_template": { - "some": "{{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "{{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -502,15 +496,12 @@ async def test_if_fires_on_change_with_for(hass: HomeAssistant, calls) -> None: "action": { "service": "test.automation", "data_template": { - "some": "{{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "{{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -549,15 +540,12 @@ async def test_if_fires_on_change_with_for_advanced( "action": { "service": "test.automation", "data_template": { - "some": "{{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "{{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -593,15 +581,12 @@ async def test_if_fires_on_change_with_for_0_advanced( "action": { "service": "test.automation", "data_template": { - "some": "{{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "{{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, diff --git a/tests/components/update/test_device_trigger.py b/tests/components/update/test_device_trigger.py index 1ffd295bbc9..6ece4f818d1 100644 --- a/tests/components/update/test_device_trigger.py +++ b/tests/components/update/test_device_trigger.py @@ -214,15 +214,12 @@ async def test_if_fires_on_state_change( "action": { "service": "test.automation", "data_template": { - "some": "update_available {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "update_available {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -238,15 +235,12 @@ async def test_if_fires_on_state_change( "action": { "service": "test.automation", "data_template": { - "some": "no_update {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "no_update {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -314,15 +308,12 @@ async def test_if_fires_on_state_change_legacy( "action": { "service": "test.automation", "data_template": { - "some": "no_update {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "no_update {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, @@ -383,15 +374,12 @@ async def test_if_fires_on_state_change_with_for( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, diff --git a/tests/components/vacuum/test_device_trigger.py b/tests/components/vacuum/test_device_trigger.py index b2273d905c1..bae57b1941f 100644 --- a/tests/components/vacuum/test_device_trigger.py +++ b/tests/components/vacuum/test_device_trigger.py @@ -356,15 +356,12 @@ async def test_if_fires_on_state_change_with_for( "action": { "service": "test.automation", "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) + "some": ( + "turn_off {{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.for }}" ) }, }, diff --git a/tests/components/xbox/test_config_flow.py b/tests/components/xbox/test_config_flow.py index 5abf9ad25d9..e547909f946 100644 --- a/tests/components/xbox/test_config_flow.py +++ b/tests/components/xbox/test_config_flow.py @@ -55,7 +55,7 @@ async def test_full_flow( }, ) - scope = "+".join(["Xboxlive.signin", "Xboxlive.offline_access"]) + scope = "Xboxlive.signin+Xboxlive.offline_access" assert result["url"] == ( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" diff --git a/tests/components/zone/test_trigger.py b/tests/components/zone/test_trigger.py index 8987481f460..7e42f41f119 100644 --- a/tests/components/zone/test_trigger.py +++ b/tests/components/zone/test_trigger.py @@ -64,16 +64,13 @@ async def test_if_fires_on_zone_enter(hass: HomeAssistant, calls) -> None: "action": { "service": "test.automation", "data_template": { - "some": "{{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "zone.name", - "id", - ) + "some": ( + "{{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.zone.name }}" + " - {{ trigger.id }}" ) }, }, @@ -143,16 +140,13 @@ async def test_if_fires_on_zone_enter_uuid(hass: HomeAssistant, calls) -> None: "action": { "service": "test.automation", "data_template": { - "some": "{{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "zone.name", - "id", - ) + "some": ( + "{{ trigger.platform }}" + " - {{ trigger.entity_id }}" + " - {{ trigger.from_state.state }}" + " - {{ trigger.to_state.state }}" + " - {{ trigger.zone.name }}" + " - {{ trigger.id }}" ) }, }, diff --git a/tests/scripts/test_auth.py b/tests/scripts/test_auth.py index 8367eda76e8..72bb4dd5b67 100644 --- a/tests/scripts/test_auth.py +++ b/tests/scripts/test_auth.py @@ -42,9 +42,7 @@ async def test_list_user(hass: HomeAssistant, provider, capsys) -> None: captured = capsys.readouterr() - assert captured.out == "\n".join( - ["test-user", "second-user", "", "Total users: 2", ""] - ) + assert captured.out == "test-user\nsecond-user\n\nTotal users: 2\n" async def test_add_user( From f3a3e6821bd2dbc81b37dc9999f7d5c52ba35b06 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 11 Apr 2024 21:14:35 -1000 Subject: [PATCH 511/967] Switch imap push coordinator to use eager_start (#115454) When I turned on eager_start here the data would always end up being None because _async_update_data always returned None. To fix this it now returns the value from the push loop. It appears this race would happen in production so this may be a bugfix but since I do not use this integration it could use a second set of eyes --- homeassistant/components/imap/coordinator.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/imap/coordinator.py b/homeassistant/components/imap/coordinator.py index 94699ae5dd4..53d24044b53 100644 --- a/homeassistant/components/imap/coordinator.py +++ b/homeassistant/components/imap/coordinator.py @@ -443,23 +443,24 @@ class ImapPushDataUpdateCoordinator(ImapDataUpdateCoordinator): _LOGGER.debug("Connected to server %s using IMAP push", entry.data[CONF_SERVER]) super().__init__(hass, imap_client, entry, None) self._push_wait_task: asyncio.Task[None] | None = None + self.number_of_messages: int | None = None async def _async_update_data(self) -> int | None: """Update the number of unread emails.""" await self.async_start() - return None + return self.number_of_messages async def async_start(self) -> None: """Start coordinator.""" self._push_wait_task = self.hass.async_create_background_task( - self._async_wait_push_loop(), "Wait for IMAP data push", eager_start=False + self._async_wait_push_loop(), "Wait for IMAP data push" ) async def _async_wait_push_loop(self) -> None: """Wait for data push from server.""" while True: try: - number_of_messages = await self._async_fetch_number_of_messages() + self.number_of_messages = await self._async_fetch_number_of_messages() except InvalidAuth as ex: self.auth_errors += 1 await self._cleanup() @@ -489,7 +490,7 @@ class ImapPushDataUpdateCoordinator(ImapDataUpdateCoordinator): continue else: self.auth_errors = 0 - self.async_set_updated_data(number_of_messages) + self.async_set_updated_data(self.number_of_messages) try: idle: asyncio.Future = await self.imap_client.idle_start() await self.imap_client.wait_server_push() From a213de3db5524b065d56f9ba7a9d7d187ec6176b Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 12 Apr 2024 10:40:17 +0200 Subject: [PATCH 512/967] Add service schema tests for notify entity platform (#115457) * Add service schema tests for notify entity platform * Use correct entity * Assert on exception value --- tests/components/notify/test_init.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/components/notify/test_init.py b/tests/components/notify/test_init.py index 26ed2ddc250..1f9ec81e36a 100644 --- a/tests/components/notify/test_init.py +++ b/tests/components/notify/test_init.py @@ -7,6 +7,7 @@ from typing import Any from unittest.mock import MagicMock, Mock, patch import pytest +import voluptuous as vol import yaml from homeassistant import config as hass_config @@ -120,6 +121,27 @@ async def test_send_message_service( await hass.async_block_till_done() entity.send_message_mock_calls.assert_called_once() + entity.send_message_mock_calls.reset_mock() + + # Test schema: `None` message fails + with pytest.raises(vol.Invalid) as exc: + await hass.services.async_call( + notify.DOMAIN, + notify.SERVICE_SEND_MESSAGE, + {"entity_id": "notify.test", notify.ATTR_MESSAGE: None}, + ) + assert ( + str(exc.value) == "string value is None for dictionary value @ data['message']" + ) + entity.send_message_mock_calls.assert_not_called() + + # Test schema: No message fails + with pytest.raises(vol.Invalid) as exc: + await hass.services.async_call( + notify.DOMAIN, notify.SERVICE_SEND_MESSAGE, {"entity_id": "notify.test"} + ) + assert str(exc.value) == "required key not provided @ data['message']" + entity.send_message_mock_calls.assert_not_called() # Test unloading the entry succeeds assert await hass.config_entries.async_unload(config_entry.entry_id) From d59af22b6996b0ce83de2dd7aee2064b1f900dcf Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 12 Apr 2024 11:50:22 +0200 Subject: [PATCH 513/967] Update frontend to 20240404.2 (#115460) --- 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 028fb28f01b..d711314cabb 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==20240404.1"] + "requirements": ["home-assistant-frontend==20240404.2"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1150da9ceac..090271e028e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -31,7 +31,7 @@ habluetooth==2.4.2 hass-nabucasa==0.78.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240404.1 +home-assistant-frontend==20240404.2 home-assistant-intents==2024.4.3 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 4ee717dd0d0..53d108fdce1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1081,7 +1081,7 @@ hole==0.8.0 holidays==0.46 # homeassistant.components.frontend -home-assistant-frontend==20240404.1 +home-assistant-frontend==20240404.2 # homeassistant.components.conversation home-assistant-intents==2024.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 479449d92cf..ca80aa78e00 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -883,7 +883,7 @@ hole==0.8.0 holidays==0.46 # homeassistant.components.frontend -home-assistant-frontend==20240404.1 +home-assistant-frontend==20240404.2 # homeassistant.components.conversation home-assistant-intents==2024.4.3 From f70ce8abf9de96893ad7fb02582d113e299521a4 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 12 Apr 2024 13:11:24 +0200 Subject: [PATCH 514/967] Fix ci Python cache key (#115467) --- .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 4299f298122..7dd6f798eef 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -94,7 +94,7 @@ jobs: id: generate_python_cache_key run: >- echo "key=venv-${{ env.CACHE_VERSION }}-${{ - hashFiles('requirements_test.txt') }}-${{ + hashFiles('requirements_test.txt', 'requirements_test_pre_commit.txt') }}-${{ hashFiles('requirements_all.txt') }}-${{ hashFiles('homeassistant/package_constraints.txt') }}" >> $GITHUB_OUTPUT - name: Generate partial pre-commit restore key From 348e1df949bae31fd1d3f805d5e892bba608e120 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Fri, 12 Apr 2024 14:47:46 +0200 Subject: [PATCH 515/967] Add strict connection (#112387) Co-authored-by: Martin Hjelmare --- homeassistant/auth/__init__.py | 8 +- homeassistant/auth/session.py | 205 +++++++++++ homeassistant/components/auth/__init__.py | 32 +- homeassistant/components/hassio/ingress.py | 1 - homeassistant/components/http/__init__.py | 93 ++++- homeassistant/components/http/auth.py | 94 ++++- homeassistant/components/http/const.py | 9 + homeassistant/components/http/icons.json | 5 + homeassistant/components/http/services.yaml | 1 + homeassistant/components/http/session.py | 160 +++++++++ .../http/strict_connection_static_page.html | 46 +++ homeassistant/components/http/strings.json | 16 + homeassistant/package_constraints.txt | 1 + pyproject.toml | 1 + requirements.txt | 1 + tests/components/api/test_init.py | 2 +- tests/components/http/test_auth.py | 339 ++++++++++++++++-- tests/components/http/test_init.py | 79 ++++ tests/components/http/test_session.py | 107 ++++++ tests/components/stream/conftest.py | 20 +- .../components/websocket_api/test_commands.py | 2 +- tests/helpers/test_service.py | 27 +- tests/scripts/test_check_config.py | 2 + 23 files changed, 1187 insertions(+), 64 deletions(-) create mode 100644 homeassistant/auth/session.py create mode 100644 homeassistant/components/http/icons.json create mode 100644 homeassistant/components/http/services.yaml create mode 100644 homeassistant/components/http/session.py create mode 100644 homeassistant/components/http/strict_connection_static_page.html create mode 100644 homeassistant/components/http/strings.json create mode 100644 tests/components/http/test_session.py diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 969fcc3529e..2a9525181f6 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 .session import SessionManager EVENT_USER_ADDED = "user_added" EVENT_USER_UPDATED = "user_updated" @@ -85,7 +86,7 @@ async def auth_manager_from_config( module_hash[module.id] = module manager = AuthManager(hass, store, provider_hash, module_hash) - manager.async_setup() + await manager.async_setup() return manager @@ -180,9 +181,9 @@ class AuthManager: self._remove_expired_job = HassJob( self._async_remove_expired_refresh_tokens, job_type=HassJobType.Callback ) + self.session = SessionManager(hass, self) - @callback - def async_setup(self) -> None: + async def async_setup(self) -> None: """Set up the auth manager.""" hass = self.hass hass.async_add_shutdown_job( @@ -191,6 +192,7 @@ class AuthManager: ) ) self._async_track_next_refresh_token_expiration() + await self.session.async_setup() @property def auth_providers(self) -> list[AuthProvider]: diff --git a/homeassistant/auth/session.py b/homeassistant/auth/session.py new file mode 100644 index 00000000000..88297b50d90 --- /dev/null +++ b/homeassistant/auth/session.py @@ -0,0 +1,205 @@ +"""Session auth module.""" + +from __future__ import annotations + +from datetime import datetime, timedelta +import secrets +from typing import TYPE_CHECKING, Final, TypedDict + +from aiohttp.web import Request +from aiohttp_session import Session, get_session, new_session +from cryptography.fernet import Fernet + +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.storage import Store +from homeassistant.util import dt as dt_util + +from .models import RefreshToken + +if TYPE_CHECKING: + from . import AuthManager + + +TEMP_TIMEOUT = timedelta(minutes=5) +TEMP_TIMEOUT_SECONDS = TEMP_TIMEOUT.total_seconds() + +SESSION_ID = "id" +STORAGE_VERSION = 1 +STORAGE_KEY = "auth.session" + + +class StrictConnectionTempSessionData: + """Data for accessing unauthorized resources for a short period of time.""" + + __slots__ = ("cancel_remove", "absolute_expiry") + + def __init__(self, cancel_remove: CALLBACK_TYPE) -> None: + """Initialize the temp session data.""" + self.cancel_remove: Final[CALLBACK_TYPE] = cancel_remove + self.absolute_expiry: Final[datetime] = dt_util.utcnow() + TEMP_TIMEOUT + + +class StoreData(TypedDict): + """Data to store.""" + + unauthorized_sessions: dict[str, str] + key: str + + +class SessionManager: + """Session manager.""" + + def __init__(self, hass: HomeAssistant, auth: AuthManager) -> None: + """Initialize the strict connection manager.""" + self._auth = auth + self._hass = hass + self._temp_sessions: dict[str, StrictConnectionTempSessionData] = {} + self._strict_connection_sessions: dict[str, str] = {} + self._store = Store[StoreData]( + hass, STORAGE_VERSION, STORAGE_KEY, private=True, atomic_writes=True + ) + self._key: str | None = None + self._refresh_token_revoke_callbacks: dict[str, CALLBACK_TYPE] = {} + + @property + def key(self) -> str: + """Return the encryption key.""" + if self._key is None: + self._key = Fernet.generate_key().decode() + self._async_schedule_save() + return self._key + + async def async_validate_request_for_strict_connection_session( + self, + request: Request, + ) -> bool: + """Check if a request has a valid strict connection session.""" + session = await get_session(request) + if session.new or session.empty: + return False + result = self.async_validate_strict_connection_session(session) + if result is False: + session.invalidate() + return result + + @callback + def async_validate_strict_connection_session( + self, + session: Session, + ) -> bool: + """Validate a strict connection session.""" + if not (session_id := session.get(SESSION_ID)): + return False + + if token_id := self._strict_connection_sessions.get(session_id): + if self._auth.async_get_refresh_token(token_id): + return True + # refresh token is invalid, delete entry + self._strict_connection_sessions.pop(session_id) + self._async_schedule_save() + + if data := self._temp_sessions.get(session_id): + if dt_util.utcnow() <= data.absolute_expiry: + return True + # session expired, delete entry + self._temp_sessions.pop(session_id).cancel_remove() + + return False + + @callback + def _async_register_revoke_token_callback(self, refresh_token_id: str) -> None: + """Register a callback to revoke all sessions for a refresh token.""" + if refresh_token_id in self._refresh_token_revoke_callbacks: + return + + @callback + def async_invalidate_auth_sessions() -> None: + """Invalidate all sessions for a refresh token.""" + self._strict_connection_sessions = { + session_id: token_id + for session_id, token_id in self._strict_connection_sessions.items() + if token_id != refresh_token_id + } + self._async_schedule_save() + + self._refresh_token_revoke_callbacks[refresh_token_id] = ( + self._auth.async_register_revoke_token_callback( + refresh_token_id, async_invalidate_auth_sessions + ) + ) + + async def async_create_session( + self, + request: Request, + refresh_token: RefreshToken, + ) -> None: + """Create new session for given refresh token. + + Caller needs to make sure that the refresh token is valid. + By creating a session, we are implicitly revoking all other + sessions for the given refresh token as there is one refresh + token per device/user case. + """ + self._strict_connection_sessions = { + session_id: token_id + for session_id, token_id in self._strict_connection_sessions.items() + if token_id != refresh_token.id + } + + self._async_register_revoke_token_callback(refresh_token.id) + session_id = await self._async_create_new_session(request) + self._strict_connection_sessions[session_id] = refresh_token.id + self._async_schedule_save() + + async def async_create_temp_unauthorized_session(self, request: Request) -> None: + """Create a temporary unauthorized session.""" + session_id = await self._async_create_new_session( + request, max_age=int(TEMP_TIMEOUT_SECONDS) + ) + + @callback + def remove(_: datetime) -> None: + self._temp_sessions.pop(session_id, None) + + self._temp_sessions[session_id] = StrictConnectionTempSessionData( + async_call_later(self._hass, TEMP_TIMEOUT_SECONDS, remove) + ) + + async def _async_create_new_session( + self, + request: Request, + *, + max_age: int | None = None, + ) -> str: + session_id = secrets.token_hex(64) + + session = await new_session(request) + session[SESSION_ID] = session_id + if max_age is not None: + session.max_age = max_age + return session_id + + @callback + def _async_schedule_save(self, delay: float = 1) -> None: + """Save sessions.""" + self._store.async_delay_save(self._data_to_save, delay) + + @callback + def _data_to_save(self) -> StoreData: + """Return the data to store.""" + return StoreData( + unauthorized_sessions=self._strict_connection_sessions, + key=self.key, + ) + + async def async_setup(self) -> None: + """Set up session manager.""" + data = await self._store.async_load() + if data is None: + return + + self._key = data["key"] + self._strict_connection_sessions = data["unauthorized_sessions"] + for token_id in self._strict_connection_sessions.values(): + self._async_register_revoke_token_callback(token_id) diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index ff54971eb64..3d825cd99b5 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -162,6 +162,7 @@ from homeassistant.util import dt as dt_util from . import indieauth, login_flow, mfa_setup_flow DOMAIN = "auth" +STRICT_CONNECTION_URL = "/auth/strict_connection/temp_token" StoreResultType = Callable[[str, Credentials], str] RetrieveResultType = Callable[[str, str], Credentials | None] @@ -187,6 +188,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.http.register_view(RevokeTokenView()) hass.http.register_view(LinkUserView(retrieve_result)) hass.http.register_view(OAuth2AuthorizeCallbackView()) + hass.http.register_view(StrictConnectionTempTokenView()) websocket_api.async_register_command(hass, websocket_current_user) websocket_api.async_register_command(hass, websocket_create_long_lived_access_token) @@ -260,10 +262,10 @@ class TokenView(HomeAssistantView): return await RevokeTokenView.post(self, request) # type: ignore[arg-type] if grant_type == "authorization_code": - return await self._async_handle_auth_code(hass, data, request.remote) + return await self._async_handle_auth_code(hass, data, request) if grant_type == "refresh_token": - return await self._async_handle_refresh_token(hass, data, request.remote) + return await self._async_handle_refresh_token(hass, data, request) return self.json( {"error": "unsupported_grant_type"}, status_code=HTTPStatus.BAD_REQUEST @@ -273,7 +275,7 @@ class TokenView(HomeAssistantView): self, hass: HomeAssistant, data: MultiDictProxy[str], - remote_addr: str | None, + request: web.Request, ) -> web.Response: """Handle authorization code request.""" client_id = data.get("client_id") @@ -313,7 +315,7 @@ class TokenView(HomeAssistantView): ) try: access_token = hass.auth.async_create_access_token( - refresh_token, remote_addr + refresh_token, request.remote ) except InvalidAuthError as exc: return self.json( @@ -321,6 +323,7 @@ class TokenView(HomeAssistantView): status_code=HTTPStatus.FORBIDDEN, ) + await hass.auth.session.async_create_session(request, refresh_token) return self.json( { "access_token": access_token, @@ -341,9 +344,9 @@ class TokenView(HomeAssistantView): self, hass: HomeAssistant, data: MultiDictProxy[str], - remote_addr: str | None, + request: web.Request, ) -> web.Response: - """Handle authorization code request.""" + """Handle refresh token request.""" client_id = data.get("client_id") if client_id is not None and not indieauth.verify_client_id(client_id): return self.json( @@ -381,7 +384,7 @@ class TokenView(HomeAssistantView): try: access_token = hass.auth.async_create_access_token( - refresh_token, remote_addr + refresh_token, request.remote ) except InvalidAuthError as exc: return self.json( @@ -389,6 +392,7 @@ class TokenView(HomeAssistantView): status_code=HTTPStatus.FORBIDDEN, ) + await hass.auth.session.async_create_session(request, refresh_token) return self.json( { "access_token": access_token, @@ -437,6 +441,20 @@ class LinkUserView(HomeAssistantView): return self.json_message("User linked") +class StrictConnectionTempTokenView(HomeAssistantView): + """View to get temporary strict connection token.""" + + url = STRICT_CONNECTION_URL + name = "api:auth:strict_connection:temp_token" + requires_auth = False + + async def get(self, request: web.Request) -> web.Response: + """Get a temporary token and redirect to main page.""" + hass = request.app[KEY_HASS] + await hass.auth.session.async_create_temp_unauthorized_session(request) + raise web.HTTPSeeOther(location="/") + + @callback def _create_auth_code_store() -> tuple[StoreResultType, RetrieveResultType]: """Create an in memory store.""" diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index 6d6faa6fe75..ed6e47145dd 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -197,7 +197,6 @@ class HassIOIngress(HomeAssistantView): content_type or simple_response.content_type ): simple_response.enable_compression() - await simple_response.prepare(request) return simple_response # Stream response diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index e89031cb265..3e5f7333cbc 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -10,7 +10,8 @@ import os import socket import ssl from tempfile import NamedTemporaryFile -from typing import Any, Final, TypedDict, cast +from typing import Any, Final, Required, TypedDict, cast +from urllib.parse import quote_plus, urljoin from aiohttp import web from aiohttp.abc import AbstractStreamWriter @@ -30,8 +31,20 @@ 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.exceptions import HomeAssistantError +from homeassistant.core import ( + Event, + HomeAssistant, + ServiceCall, + ServiceResponse, + SupportsResponse, + callback, +) +from homeassistant.exceptions import ( + HomeAssistantError, + ServiceValidationError, + Unauthorized, + UnknownUser, +) from homeassistant.helpers import storage import homeassistant.helpers.config_validation as cv from homeassistant.helpers.http import ( @@ -53,9 +66,13 @@ from homeassistant.util import dt as dt_util, ssl as ssl_util from homeassistant.util.async_ import create_eager_task from homeassistant.util.json import json_loads -from .auth import async_setup_auth +from .auth import async_setup_auth, async_sign_path from .ban import setup_bans -from .const import KEY_HASS_REFRESH_TOKEN_ID, KEY_HASS_USER # noqa: F401 +from .const import ( # noqa: F401 + KEY_HASS_REFRESH_TOKEN_ID, + KEY_HASS_USER, + StrictConnectionMode, +) from .cors import setup_cors from .decorators import require_admin # noqa: F401 from .forwarded import async_setup_forwarded @@ -80,6 +97,7 @@ CONF_TRUSTED_PROXIES: Final = "trusted_proxies" CONF_LOGIN_ATTEMPTS_THRESHOLD: Final = "login_attempts_threshold" CONF_IP_BAN_ENABLED: Final = "ip_ban_enabled" CONF_SSL_PROFILE: Final = "ssl_profile" +CONF_STRICT_CONNECTION: Final = "strict_connection" SSL_MODERN: Final = "modern" SSL_INTERMEDIATE: Final = "intermediate" @@ -129,6 +147,9 @@ HTTP_SCHEMA: Final = vol.All( [SSL_INTERMEDIATE, SSL_MODERN] ), vol.Optional(CONF_USE_X_FRAME_OPTIONS, default=True): cv.boolean, + vol.Optional( + CONF_STRICT_CONNECTION, default=StrictConnectionMode.DISABLED + ): vol.In([e.value for e in StrictConnectionMode]), } ), ) @@ -152,6 +173,7 @@ class ConfData(TypedDict, total=False): login_attempts_threshold: int ip_ban_enabled: bool ssl_profile: str + strict_connection: Required[StrictConnectionMode] @bind_hass @@ -218,6 +240,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: login_threshold=login_threshold, is_ban_enabled=is_ban_enabled, use_x_frame_options=use_x_frame_options, + strict_connection_non_cloud=conf[CONF_STRICT_CONNECTION], ) async def stop_server(event: Event) -> None: @@ -247,6 +270,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: local_ip, host, server_port, ssl_certificate is not None ) + _setup_services(hass, conf) return True @@ -331,6 +355,7 @@ class HomeAssistantHTTP: login_threshold: int, is_ban_enabled: bool, use_x_frame_options: bool, + strict_connection_non_cloud: StrictConnectionMode, ) -> None: """Initialize the server.""" self.app[KEY_HASS] = self.hass @@ -347,7 +372,7 @@ class HomeAssistantHTTP: if is_ban_enabled: setup_bans(self.hass, self.app, login_threshold) - await async_setup_auth(self.hass, self.app) + await async_setup_auth(self.hass, self.app, strict_connection_non_cloud) setup_headers(self.app, use_x_frame_options) setup_cors(self.app, cors_origins) @@ -577,3 +602,59 @@ async def start_http_server_and_save_config( ] store.async_delay_save(lambda: conf, SAVE_DELAY) + + +@callback +def _setup_services(hass: HomeAssistant, conf: ConfData) -> None: + """Set up services for HTTP component.""" + + async def create_temporary_strict_connection_url( + call: ServiceCall, + ) -> ServiceResponse: + """Create a strict connection url and return it.""" + # Copied form homeassistant/helpers/service.py#_async_admin_handler + # as the helper supports no responses yet + if call.context.user_id: + user = await hass.auth.async_get_user(call.context.user_id) + if user is None: + raise UnknownUser(context=call.context) + if not user.is_admin: + raise Unauthorized(context=call.context) + + if conf[CONF_STRICT_CONNECTION] is StrictConnectionMode.DISABLED: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="strict_connection_not_enabled_non_cloud", + ) + + try: + url = get_url(hass, prefer_external=True, allow_internal=False) + except NoURLAvailableError as ex: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="no_external_url_available", + ) from ex + + # to avoid circular import + # pylint: disable-next=import-outside-toplevel + from homeassistant.components.auth import STRICT_CONNECTION_URL + + path = async_sign_path( + hass, + STRICT_CONNECTION_URL, + datetime.timedelta(hours=1), + use_content_user=True, + ) + url = urljoin(url, path) + + return { + "url": f"https://login.home-assistant.io?u={quote_plus(url)}", + "direct_url": url, + } + + hass.services.async_register( + DOMAIN, + "create_temporary_strict_connection_url", + create_temporary_strict_connection_url, + supports_response=SupportsResponse.ONLY, + ) diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 2073c998384..1eb74289089 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -4,14 +4,18 @@ from __future__ import annotations from collections.abc import Awaitable, Callable from datetime import timedelta +from http import HTTPStatus from ipaddress import ip_address import logging +import os import secrets import time from typing import Any, Final from aiohttp import hdrs -from aiohttp.web import Application, Request, StreamResponse, middleware +from aiohttp.web import Application, Request, Response, StreamResponse, middleware +from aiohttp.web_exceptions import HTTPBadRequest +from aiohttp_session import session_middleware import jwt from jwt import api_jws from yarl import URL @@ -27,7 +31,13 @@ from homeassistant.helpers.network import is_cloud_connection from homeassistant.helpers.storage import Store from homeassistant.util.network import is_local -from .const import KEY_AUTHENTICATED, KEY_HASS_REFRESH_TOKEN_ID, KEY_HASS_USER +from .const import ( + KEY_AUTHENTICATED, + KEY_HASS_REFRESH_TOKEN_ID, + KEY_HASS_USER, + StrictConnectionMode, +) +from .session import HomeAssistantCookieStorage _LOGGER = logging.getLogger(__name__) @@ -39,6 +49,10 @@ SAFE_QUERY_PARAMS: Final = ["height", "width"] STORAGE_VERSION = 1 STORAGE_KEY = "http.auth" CONTENT_USER_NAME = "Home Assistant Content" +STRICT_CONNECTION_EXCLUDED_PATH = "/api/webhook/" +STRICT_CONNECTION_STATIC_PAGE = os.path.join( + os.path.dirname(__file__), "strict_connection_static_page.html" +) @callback @@ -48,13 +62,16 @@ def async_sign_path( expiration: timedelta, *, refresh_token_id: str | None = None, + use_content_user: bool = False, ) -> str: """Sign a path for temporary access without auth header.""" if (secret := hass.data.get(DATA_SIGN_SECRET)) is None: secret = hass.data[DATA_SIGN_SECRET] = secrets.token_hex() if refresh_token_id is None: - if connection := websocket_api.current_connection.get(): + if use_content_user: + refresh_token_id = hass.data[STORAGE_KEY] + elif connection := websocket_api.current_connection.get(): refresh_token_id = connection.refresh_token_id elif ( request := current_request.get() @@ -114,7 +131,11 @@ def async_user_not_allowed_do_auth( return "User cannot authenticate remotely" -async def async_setup_auth(hass: HomeAssistant, app: Application) -> None: +async def async_setup_auth( + hass: HomeAssistant, + app: Application, + strict_connection_mode_non_cloud: StrictConnectionMode, +) -> None: """Create auth middleware for the app.""" store = Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY) if (data := await store.async_load()) is None: @@ -135,6 +156,16 @@ async def async_setup_auth(hass: HomeAssistant, app: Application) -> None: await store.async_save(data) hass.data[STORAGE_KEY] = refresh_token.id + strict_connection_static_file_content = None + if strict_connection_mode_non_cloud is StrictConnectionMode.STATIC_PAGE: + + def read_static_page() -> str: + with open(STRICT_CONNECTION_STATIC_PAGE, encoding="utf-8") as file: + return file.read() + + strict_connection_static_file_content = await hass.async_add_executor_job( + read_static_page + ) @callback def async_validate_auth_header(request: Request) -> bool: @@ -224,6 +255,22 @@ async def async_setup_auth(hass: HomeAssistant, app: Application) -> None: authenticated = True auth_type = "signed request" + if ( + not authenticated + and strict_connection_mode_non_cloud is not StrictConnectionMode.DISABLED + and not request.path.startswith(STRICT_CONNECTION_EXCLUDED_PATH) + and not await hass.auth.session.async_validate_request_for_strict_connection_session( + request + ) + and ( + resp := _async_perform_action_on_non_local( + request, strict_connection_static_file_content + ) + ) + is not None + ): + return resp + if authenticated and _LOGGER.isEnabledFor(logging.DEBUG): _LOGGER.debug( "Authenticated %s for %s using %s", @@ -235,4 +282,43 @@ async def async_setup_auth(hass: HomeAssistant, app: Application) -> None: request[KEY_AUTHENTICATED] = authenticated return await handler(request) + app.middlewares.append(session_middleware(HomeAssistantCookieStorage(hass))) app.middlewares.append(auth_middleware) + + +@callback +def _async_perform_action_on_non_local( + request: Request, + strict_connection_static_file_content: str | None, +) -> StreamResponse | None: + """Perform strict connection mode action if the request is not local. + + The function does the following: + - Try to get the IP address of the request. If it fails, assume it's not local + - If the request is local, return None (allow the request to continue) + - If strict_connection_static_file_content is set, return a response with the content + - Otherwise close the connection and raise an exception + """ + try: + ip_address_ = ip_address(request.remote) # type: ignore[arg-type] + except ValueError: + _LOGGER.debug("Invalid IP address: %s", request.remote) + ip_address_ = None + + if ip_address_ and is_local(ip_address_): + return None + + _LOGGER.debug("Perform strict connection action for %s", ip_address_) + if strict_connection_static_file_content: + return Response( + text=strict_connection_static_file_content, + content_type="text/html", + status=HTTPStatus.IM_A_TEAPOT, + ) + + if transport := request.transport: + # it should never happen that we don't have a transport + transport.close() + + # We need to raise an exception to stop processing the request + raise HTTPBadRequest diff --git a/homeassistant/components/http/const.py b/homeassistant/components/http/const.py index 1254744f258..d02416c531b 100644 --- a/homeassistant/components/http/const.py +++ b/homeassistant/components/http/const.py @@ -1,8 +1,17 @@ """HTTP specific constants.""" +from enum import StrEnum from typing import Final from homeassistant.helpers.http import KEY_AUTHENTICATED, KEY_HASS # noqa: F401 KEY_HASS_USER: Final = "hass_user" KEY_HASS_REFRESH_TOKEN_ID: Final = "hass_refresh_token_id" + + +class StrictConnectionMode(StrEnum): + """Enum for strict connection mode.""" + + DISABLED = "disabled" + STATIC_PAGE = "static_page" + DROP_CONNECTION = "drop_connection" diff --git a/homeassistant/components/http/icons.json b/homeassistant/components/http/icons.json new file mode 100644 index 00000000000..8e8b6285db7 --- /dev/null +++ b/homeassistant/components/http/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "create_temporary_strict_connection_url": "mdi:login-variant" + } +} diff --git a/homeassistant/components/http/services.yaml b/homeassistant/components/http/services.yaml new file mode 100644 index 00000000000..16b0debb144 --- /dev/null +++ b/homeassistant/components/http/services.yaml @@ -0,0 +1 @@ +create_temporary_strict_connection_url: ~ diff --git a/homeassistant/components/http/session.py b/homeassistant/components/http/session.py new file mode 100644 index 00000000000..81668ec2ccc --- /dev/null +++ b/homeassistant/components/http/session.py @@ -0,0 +1,160 @@ +"""Session http module.""" + +from functools import lru_cache +import logging + +from aiohttp.web import Request, StreamResponse +from aiohttp_session import Session, SessionData +from aiohttp_session.cookie_storage import EncryptedCookieStorage +from cryptography.fernet import InvalidToken + +from homeassistant.auth.const import REFRESH_TOKEN_EXPIRATION +from homeassistant.core import HomeAssistant +from homeassistant.helpers.json import json_dumps +from homeassistant.helpers.network import is_cloud_connection +from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads + +from .ban import process_wrong_login + +_LOGGER = logging.getLogger(__name__) + +COOKIE_NAME = "SC" +PREFIXED_COOKIE_NAME = f"__Host-{COOKIE_NAME}" +SESSION_CACHE_SIZE = 16 + + +def _get_cookie_name(is_secure: bool) -> str: + """Return the cookie name.""" + return PREFIXED_COOKIE_NAME if is_secure else COOKIE_NAME + + +class HomeAssistantCookieStorage(EncryptedCookieStorage): + """Home Assistant cookie storage. + + Own class is required: + - to set the secure flag based on the connection type + - to use a LRU cache for session decryption + """ + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the cookie storage.""" + super().__init__( + hass.auth.session.key, + cookie_name=PREFIXED_COOKIE_NAME, + max_age=int(REFRESH_TOKEN_EXPIRATION), + httponly=True, + samesite="Lax", + secure=True, + encoder=json_dumps, + decoder=json_loads, + ) + self._hass = hass + + def _secure_connection(self, request: Request) -> bool: + """Return if the connection is secure (https).""" + return is_cloud_connection(self._hass) or request.secure + + def load_cookie(self, request: Request) -> str | None: + """Load cookie.""" + is_secure = self._secure_connection(request) + cookie_name = _get_cookie_name(is_secure) + return request.cookies.get(cookie_name) + + @lru_cache(maxsize=SESSION_CACHE_SIZE) + def _decrypt_cookie(self, cookie: str) -> Session | None: + """Decrypt and validate cookie.""" + try: + data = SessionData( # type: ignore[misc] + self._decoder( + self._fernet.decrypt( + cookie.encode("utf-8"), ttl=self.max_age + ).decode("utf-8") + ) + ) + except (InvalidToken, TypeError, ValueError, *JSON_DECODE_EXCEPTIONS): + _LOGGER.warning("Cannot decrypt/parse cookie value") + return None + + session = Session(None, data=data, new=data is None, max_age=self.max_age) + + # Validate session if not empty + if ( + not session.empty + and not self._hass.auth.session.async_validate_strict_connection_session( + session + ) + ): + # Invalidate session as it is not valid + session.invalidate() + + return session + + async def new_session(self) -> Session: + """Create a new session and mark it as changed.""" + session = Session(None, data=None, new=True, max_age=self.max_age) + session.changed() + return session + + async def load_session(self, request: Request) -> Session: + """Load session.""" + # Split parent function to use lru_cache + if (cookie := self.load_cookie(request)) is None: + return await self.new_session() + + if (session := self._decrypt_cookie(cookie)) is None: + # Decrypting/parsing failed, log wrong login and create a new session + await process_wrong_login(request) + session = await self.new_session() + + return session + + async def save_session( + self, request: Request, response: StreamResponse, session: Session + ) -> None: + """Save session.""" + + is_secure = self._secure_connection(request) + cookie_name = _get_cookie_name(is_secure) + + if session.empty: + response.del_cookie(cookie_name) + else: + params = self.cookie_params.copy() + params["secure"] = is_secure + params["max_age"] = session.max_age + + cookie_data = self._encoder(self._get_session_data(session)).encode("utf-8") + response.set_cookie( + cookie_name, + self._fernet.encrypt(cookie_data).decode("utf-8"), + **params, + ) + # Add Cache-Control header to not cache the cookie as it + # is used for session management + self._add_cache_control_header(response) + + @staticmethod + def _add_cache_control_header(response: StreamResponse) -> None: + """Add/set cache control header to no-cache="Set-Cookie".""" + # Structure of the Cache-Control header defined in + # https://datatracker.ietf.org/doc/html/rfc2068#section-14.9 + if header := response.headers.get("Cache-Control"): + directives = [] + for directive in header.split(","): + directive = directive.strip() + directive_lowered = directive.lower() + if directive_lowered.startswith("no-cache"): + if "set-cookie" in directive_lowered or directive.find("=") == -1: + # Set-Cookie is already in the no-cache directive or + # the whole request should not be cached -> Nothing to do + return + + # Add Set-Cookie to the no-cache + # [:-1] to remove the " at the end of the directive + directive = f"{directive[:-1]}, Set-Cookie" + + directives.append(directive) + header = ", ".join(directives) + else: + header = 'no-cache="Set-Cookie"' + response.headers["Cache-Control"] = header diff --git a/homeassistant/components/http/strict_connection_static_page.html b/homeassistant/components/http/strict_connection_static_page.html new file mode 100644 index 00000000000..24049d9a0eb --- /dev/null +++ b/homeassistant/components/http/strict_connection_static_page.html @@ -0,0 +1,46 @@ + + + + + + I'm a Teapot + + + +
+

Error 418: I'm a Teapot

+

+ Oops! Looks like the server is taking a coffee break.
+ Don't worry, it'll be back to brewing your requests in no time! +

+

+
+ + diff --git a/homeassistant/components/http/strings.json b/homeassistant/components/http/strings.json new file mode 100644 index 00000000000..7cd64f5f297 --- /dev/null +++ b/homeassistant/components/http/strings.json @@ -0,0 +1,16 @@ +{ + "exceptions": { + "strict_connection_not_enabled_non_cloud": { + "message": "Strict connection is not enabled for non-cloud requests" + }, + "no_external_url_available": { + "message": "No external URL available" + } + }, + "services": { + "create_temporary_strict_connection_url": { + "name": "Create a temporary strict connection URL", + "description": "Create a temporary strict connection URL, which can be used to login on another device." + } + } +} diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 090271e028e..b253d600a2d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -7,6 +7,7 @@ aiohttp-fast-url-dispatcher==0.3.0 aiohttp-zlib-ng==0.3.1 aiohttp==3.9.4 aiohttp_cors==0.7.0 +aiohttp_session==2.12.0 astral==2.2 async-interrupt==1.1.1 async-upnp-client==0.38.3 diff --git a/pyproject.toml b/pyproject.toml index 5ea335115ca..79a66cc7d82 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,7 @@ dependencies = [ "aiodns==3.2.0", "aiohttp==3.9.4", "aiohttp_cors==0.7.0", + "aiohttp_session==2.12.0", "aiohttp-fast-url-dispatcher==0.3.0", "aiohttp-zlib-ng==0.3.1", "astral==2.2", diff --git a/requirements.txt b/requirements.txt index 3cd1e8edfa5..f2f26f9bb54 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,7 @@ aiodns==3.2.0 aiohttp==3.9.4 aiohttp_cors==0.7.0 +aiohttp_session==2.12.0 aiohttp-fast-url-dispatcher==0.3.0 aiohttp-zlib-ng==0.3.1 astral==2.2 diff --git a/tests/components/api/test_init.py b/tests/components/api/test_init.py index 0ac2e5973fe..5443d48452f 100644 --- a/tests/components/api/test_init.py +++ b/tests/components/api/test_init.py @@ -306,7 +306,7 @@ async def test_api_get_services( for serv_domain in data: local = local_services.pop(serv_domain["domain"]) - assert serv_domain["services"] == local + assert serv_domain["services"].keys() == local.keys() async def test_api_call_service_no_data( diff --git a/tests/components/http/test_auth.py b/tests/components/http/test_auth.py index de6f323bc8a..f0f87e58173 100644 --- a/tests/components/http/test_auth.py +++ b/tests/components/http/test_auth.py @@ -1,22 +1,28 @@ """The tests for the Home Assistant HTTP component.""" +from collections.abc import Awaitable, Callable from datetime import timedelta from http import HTTPStatus from ipaddress import ip_network +import logging from unittest.mock import Mock, patch -from aiohttp import BasicAuth, web +from aiohttp import BasicAuth, ServerDisconnectedError, web +from aiohttp.test_utils import TestClient from aiohttp.web_exceptions import HTTPUnauthorized +from aiohttp_session import get_session import jwt import pytest import yarl +from yarl import URL from homeassistant.auth.const import GROUP_ID_READ_ONLY -from homeassistant.auth.models import User +from homeassistant.auth.models import RefreshToken, User from homeassistant.auth.providers import trusted_networks from homeassistant.auth.providers.legacy_api_password import ( LegacyApiPasswordAuthProvider, ) +from homeassistant.auth.session import SESSION_ID, TEMP_TIMEOUT from homeassistant.components import websocket_api from homeassistant.components.http import KEY_HASS from homeassistant.components.http.auth import ( @@ -24,11 +30,12 @@ from homeassistant.components.http.auth import ( DATA_SIGN_SECRET, SIGN_QUERY_PARAM, STORAGE_KEY, + STRICT_CONNECTION_STATIC_PAGE, async_setup_auth, async_sign_path, async_user_not_allowed_do_auth, ) -from homeassistant.components.http.const import KEY_AUTHENTICATED +from homeassistant.components.http.const import KEY_AUTHENTICATED, StrictConnectionMode from homeassistant.components.http.forwarded import async_setup_forwarded from homeassistant.components.http.request_context import ( current_request, @@ -36,13 +43,15 @@ from homeassistant.components.http.request_context import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.setup import async_setup_component +from homeassistant.util.dt import utcnow from . import HTTP_HEADER_HA_AUTH -from tests.common import MockUser +from tests.common import MockUser, async_fire_time_changed from tests.test_util import mock_real_ip from tests.typing import ClientSessionGenerator, WebSocketGenerator +_LOGGER = logging.getLogger(__name__) API_PASSWORD = "test-password" # Don't add 127.0.0.1/::1 as trusted, as it may interfere with other test cases @@ -54,7 +63,13 @@ TRUSTED_NETWORKS = [ ] TRUSTED_ADDRESSES = ["100.64.0.1", "192.0.2.100", "FD01:DB8::1", "2001:DB8:ABCD::1"] EXTERNAL_ADDRESSES = ["198.51.100.1", "2001:DB8:FA1::1"] -UNTRUSTED_ADDRESSES = [*EXTERNAL_ADDRESSES, "127.0.0.1", "::1"] +LOCALHOST_ADDRESSES = ["127.0.0.1", "::1"] +UNTRUSTED_ADDRESSES = [*EXTERNAL_ADDRESSES, *LOCALHOST_ADDRESSES] +PRIVATE_ADDRESSES = [ + "192.168.10.10", + "172.16.4.20", + "10.100.50.5", +] async def mock_handler(request): @@ -122,7 +137,7 @@ async def test_cant_access_with_password_in_header( hass: HomeAssistant, ) -> None: """Test access with password in header.""" - await async_setup_auth(hass, app) + await async_setup_auth(hass, app, StrictConnectionMode.DISABLED) client = await aiohttp_client(app) req = await client.get("/", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}) @@ -139,7 +154,7 @@ async def test_cant_access_with_password_in_query( hass: HomeAssistant, ) -> None: """Test access with password in URL.""" - await async_setup_auth(hass, app) + await async_setup_auth(hass, app, StrictConnectionMode.DISABLED) client = await aiohttp_client(app) resp = await client.get("/", params={"api_password": API_PASSWORD}) @@ -159,7 +174,7 @@ async def test_basic_auth_does_not_work( legacy_auth: LegacyApiPasswordAuthProvider, ) -> None: """Test access with basic authentication.""" - await async_setup_auth(hass, app) + await async_setup_auth(hass, app, StrictConnectionMode.DISABLED) client = await aiohttp_client(app) req = await client.get("/", auth=BasicAuth("homeassistant", API_PASSWORD)) @@ -183,7 +198,7 @@ async def test_cannot_access_with_trusted_ip( hass_owner_user: MockUser, ) -> None: """Test access with an untrusted ip address.""" - await async_setup_auth(hass, app2) + await async_setup_auth(hass, app2, StrictConnectionMode.DISABLED) set_mock_ip = mock_real_ip(app2) client = await aiohttp_client(app2) @@ -211,7 +226,7 @@ async def test_auth_active_access_with_access_token_in_header( ) -> None: """Test access with access token in header.""" token = hass_access_token - await async_setup_auth(hass, app) + await async_setup_auth(hass, app, StrictConnectionMode.DISABLED) client = await aiohttp_client(app) refresh_token = hass.auth.async_validate_access_token(hass_access_token) @@ -247,7 +262,7 @@ async def test_auth_active_access_with_trusted_ip( hass_owner_user: MockUser, ) -> None: """Test access with an untrusted ip address.""" - await async_setup_auth(hass, app2) + await async_setup_auth(hass, app2, StrictConnectionMode.DISABLED) set_mock_ip = mock_real_ip(app2) client = await aiohttp_client(app2) @@ -274,7 +289,7 @@ async def test_auth_legacy_support_api_password_cannot_access( hass: HomeAssistant, ) -> None: """Test access using api_password if auth.support_legacy.""" - await async_setup_auth(hass, app) + await async_setup_auth(hass, app, StrictConnectionMode.DISABLED) client = await aiohttp_client(app) req = await client.get("/", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}) @@ -296,7 +311,7 @@ async def test_auth_access_signed_path_with_refresh_token( """Test access with signed url.""" app.router.add_post("/", mock_handler) app.router.add_get("/another_path", mock_handler) - await async_setup_auth(hass, app) + await async_setup_auth(hass, app, StrictConnectionMode.DISABLED) client = await aiohttp_client(app) refresh_token = hass.auth.async_validate_access_token(hass_access_token) @@ -341,7 +356,7 @@ async def test_auth_access_signed_path_with_query_param( """Test access with signed url and query params.""" app.router.add_post("/", mock_handler) app.router.add_get("/another_path", mock_handler) - await async_setup_auth(hass, app) + await async_setup_auth(hass, app, StrictConnectionMode.DISABLED) client = await aiohttp_client(app) refresh_token = hass.auth.async_validate_access_token(hass_access_token) @@ -371,7 +386,7 @@ async def test_auth_access_signed_path_with_query_param_order( """Test access with signed url and query params different order.""" app.router.add_post("/", mock_handler) app.router.add_get("/another_path", mock_handler) - await async_setup_auth(hass, app) + await async_setup_auth(hass, app, StrictConnectionMode.DISABLED) client = await aiohttp_client(app) refresh_token = hass.auth.async_validate_access_token(hass_access_token) @@ -412,7 +427,7 @@ async def test_auth_access_signed_path_with_query_param_safe_param( """Test access with signed url and changing a safe param.""" app.router.add_post("/", mock_handler) app.router.add_get("/another_path", mock_handler) - await async_setup_auth(hass, app) + await async_setup_auth(hass, app, StrictConnectionMode.DISABLED) client = await aiohttp_client(app) refresh_token = hass.auth.async_validate_access_token(hass_access_token) @@ -451,7 +466,7 @@ async def test_auth_access_signed_path_with_query_param_tamper( """Test access with signed url and query params that have been tampered with.""" app.router.add_post("/", mock_handler) app.router.add_get("/another_path", mock_handler) - await async_setup_auth(hass, app) + await async_setup_auth(hass, app, StrictConnectionMode.DISABLED) client = await aiohttp_client(app) refresh_token = hass.auth.async_validate_access_token(hass_access_token) @@ -520,7 +535,7 @@ async def test_auth_access_signed_path_with_http( ) app.router.add_get("/hello", mock_handler) - await async_setup_auth(hass, app) + await async_setup_auth(hass, app, StrictConnectionMode.DISABLED) client = await aiohttp_client(app) refresh_token = hass.auth.async_validate_access_token(hass_access_token) @@ -544,7 +559,7 @@ async def test_auth_access_signed_path_with_content_user( hass: HomeAssistant, app, aiohttp_client: ClientSessionGenerator ) -> None: """Test access signed url uses content user.""" - await async_setup_auth(hass, app) + await async_setup_auth(hass, app, StrictConnectionMode.DISABLED) signed_path = async_sign_path(hass, "/", timedelta(seconds=5)) signature = yarl.URL(signed_path).query["authSig"] claims = jwt.decode( @@ -564,7 +579,7 @@ async def test_local_only_user_rejected( ) -> None: """Test access with access token in header.""" token = hass_access_token - await async_setup_auth(hass, app) + await async_setup_auth(hass, app, StrictConnectionMode.DISABLED) set_mock_ip = mock_real_ip(app) client = await aiohttp_client(app) refresh_token = hass.auth.async_validate_access_token(hass_access_token) @@ -630,7 +645,7 @@ async def test_create_user_once(hass: HomeAssistant) -> None: """Test that we reuse the user.""" cur_users = len(await hass.auth.async_get_users()) app = web.Application() - await async_setup_auth(hass, app) + await async_setup_auth(hass, app, StrictConnectionMode.DISABLED) users = await hass.auth.async_get_users() assert len(users) == cur_users + 1 @@ -642,7 +657,287 @@ async def test_create_user_once(hass: HomeAssistant) -> None: assert len(user.refresh_tokens) == 1 assert user.system_generated - await async_setup_auth(hass, app) + await async_setup_auth(hass, app, StrictConnectionMode.DISABLED) # test it did not create a user assert len(await hass.auth.async_get_users()) == cur_users + 1 + + +@pytest.fixture +def app_strict_connection(hass): + """Fixture to set up a web.Application.""" + + async def handler(request): + """Return if request was authenticated.""" + return web.json_response(data={"authenticated": request[KEY_AUTHENTICATED]}) + + app = web.Application() + app[KEY_HASS] = hass + app.router.add_get("/", handler) + async_setup_forwarded(app, True, []) + return app + + +@pytest.mark.parametrize( + "strict_connection_mode", [e.value for e in StrictConnectionMode] +) +async def test_strict_connection_non_cloud_authenticated_requests( + hass: HomeAssistant, + app_strict_connection: web.Application, + aiohttp_client: ClientSessionGenerator, + hass_access_token: str, + strict_connection_mode: StrictConnectionMode, +) -> None: + """Test authenticated requests with strict connection.""" + token = hass_access_token + await async_setup_auth(hass, app_strict_connection, strict_connection_mode) + set_mock_ip = mock_real_ip(app_strict_connection) + client = await aiohttp_client(app_strict_connection) + refresh_token = hass.auth.async_validate_access_token(hass_access_token) + assert refresh_token + assert hass.auth.session._strict_connection_sessions == {} + + signed_path = async_sign_path( + hass, "/", timedelta(seconds=5), refresh_token_id=refresh_token.id + ) + + for remote_addr in (*LOCALHOST_ADDRESSES, *PRIVATE_ADDRESSES, *EXTERNAL_ADDRESSES): + set_mock_ip(remote_addr) + + # authorized requests should work normally + req = await client.get("/", headers={"Authorization": f"Bearer {token}"}) + assert req.status == HTTPStatus.OK + assert await req.json() == {"authenticated": True} + req = await client.get(signed_path) + assert req.status == HTTPStatus.OK + assert await req.json() == {"authenticated": True} + + +@pytest.mark.parametrize( + "strict_connection_mode", [e.value for e in StrictConnectionMode] +) +async def test_strict_connection_non_cloud_local_unauthenticated_requests( + hass: HomeAssistant, + app_strict_connection: web.Application, + aiohttp_client: ClientSessionGenerator, + strict_connection_mode: StrictConnectionMode, +) -> None: + """Test local unauthenticated requests with strict connection.""" + await async_setup_auth(hass, app_strict_connection, strict_connection_mode) + set_mock_ip = mock_real_ip(app_strict_connection) + client = await aiohttp_client(app_strict_connection) + assert hass.auth.session._strict_connection_sessions == {} + + for remote_addr in (*LOCALHOST_ADDRESSES, *PRIVATE_ADDRESSES): + set_mock_ip(remote_addr) + # local requests should work normally + req = await client.get("/") + assert req.status == HTTPStatus.OK + assert await req.json() == {"authenticated": False} + + +def _add_set_cookie_endpoint(app: web.Application, refresh_token: RefreshToken) -> None: + """Add an endpoint to set a cookie.""" + + async def set_cookie(request: web.Request) -> web.Response: + hass = request.app[KEY_HASS] + # Clear all sessions + hass.auth.session._temp_sessions.clear() + hass.auth.session._strict_connection_sessions.clear() + + if request.query["token"] == "refresh": + await hass.auth.session.async_create_session(request, refresh_token) + else: + await hass.auth.session.async_create_temp_unauthorized_session(request) + session = await get_session(request) + return web.Response(text=session[SESSION_ID]) + + app.router.add_get("/test/cookie", set_cookie) + + +async def _test_strict_connection_non_cloud_enabled_setup( + hass: HomeAssistant, + app: web.Application, + aiohttp_client: ClientSessionGenerator, + hass_access_token: str, + strict_connection_mode: StrictConnectionMode, +) -> tuple[TestClient, Callable[[str], None], RefreshToken]: + """Test external unauthenticated requests with strict connection non cloud enabled.""" + refresh_token = hass.auth.async_validate_access_token(hass_access_token) + assert refresh_token + session = hass.auth.session + assert session._strict_connection_sessions == {} + assert session._temp_sessions == {} + + _add_set_cookie_endpoint(app, refresh_token) + await async_setup_auth(hass, app, strict_connection_mode) + set_mock_ip = mock_real_ip(app) + client = await aiohttp_client(app) + return (client, set_mock_ip, refresh_token) + + +async def _test_strict_connection_non_cloud_enabled_external_unauthenticated_requests( + hass: HomeAssistant, + app: web.Application, + aiohttp_client: ClientSessionGenerator, + hass_access_token: str, + perform_unauthenticated_request: Callable[ + [HomeAssistant, TestClient], Awaitable[None] + ], + strict_connection_mode: StrictConnectionMode, +) -> None: + """Test external unauthenticated requests with strict connection non cloud enabled.""" + client, set_mock_ip, _ = await _test_strict_connection_non_cloud_enabled_setup( + hass, app, aiohttp_client, hass_access_token, strict_connection_mode + ) + + for remote_addr in EXTERNAL_ADDRESSES: + set_mock_ip(remote_addr) + await perform_unauthenticated_request(hass, client) + + +async def _test_strict_connection_non_cloud_enabled_external_unauthenticated_requests_refresh_token( + hass: HomeAssistant, + app: web.Application, + aiohttp_client: ClientSessionGenerator, + hass_access_token: str, + perform_unauthenticated_request: Callable[ + [HomeAssistant, TestClient], Awaitable[None] + ], + strict_connection_mode: StrictConnectionMode, +) -> None: + """Test external unauthenticated requests with strict connection non cloud enabled and refresh token cookie.""" + ( + client, + set_mock_ip, + refresh_token, + ) = await _test_strict_connection_non_cloud_enabled_setup( + hass, app, aiohttp_client, hass_access_token, strict_connection_mode + ) + session = hass.auth.session + + # set strict connection cookie with refresh token + set_mock_ip(LOCALHOST_ADDRESSES[0]) + session_id = await (await client.get("/test/cookie?token=refresh")).text() + assert session._strict_connection_sessions == {session_id: refresh_token.id} + for remote_addr in EXTERNAL_ADDRESSES: + set_mock_ip(remote_addr) + req = await client.get("/") + assert req.status == HTTPStatus.OK + assert await req.json() == {"authenticated": False} + + # Invalidate refresh token, which should also invalidate session + hass.auth.async_remove_refresh_token(refresh_token) + assert session._strict_connection_sessions == {} + for remote_addr in EXTERNAL_ADDRESSES: + set_mock_ip(remote_addr) + await perform_unauthenticated_request(hass, client) + + +async def _test_strict_connection_non_cloud_enabled_external_unauthenticated_requests_temp_session( + hass: HomeAssistant, + app: web.Application, + aiohttp_client: ClientSessionGenerator, + hass_access_token: str, + perform_unauthenticated_request: Callable[ + [HomeAssistant, TestClient], Awaitable[None] + ], + strict_connection_mode: StrictConnectionMode, +) -> None: + """Test external unauthenticated requests with strict connection non cloud enabled and temp cookie.""" + client, set_mock_ip, _ = await _test_strict_connection_non_cloud_enabled_setup( + hass, app, aiohttp_client, hass_access_token, strict_connection_mode + ) + session = hass.auth.session + + # set strict connection cookie with temp session + assert session._temp_sessions == {} + set_mock_ip(LOCALHOST_ADDRESSES[0]) + session_id = await (await client.get("/test/cookie?token=temp")).text() + assert client.session.cookie_jar.filter_cookies(URL("http://127.0.0.1")) + assert session_id in session._temp_sessions + for remote_addr in EXTERNAL_ADDRESSES: + set_mock_ip(remote_addr) + resp = await client.get("/") + assert resp.status == HTTPStatus.OK + assert await resp.json() == {"authenticated": False} + + async_fire_time_changed(hass, utcnow() + TEMP_TIMEOUT + timedelta(minutes=1)) + await hass.async_block_till_done(wait_background_tasks=True) + + assert session._temp_sessions == {} + for remote_addr in EXTERNAL_ADDRESSES: + set_mock_ip(remote_addr) + await perform_unauthenticated_request(hass, client) + + +async def _drop_connection_unauthorized_request( + _: HomeAssistant, client: TestClient +) -> None: + with pytest.raises(ServerDisconnectedError): + # unauthorized requests should raise ServerDisconnectedError + await client.get("/") + + +async def _static_page_unauthorized_request( + hass: HomeAssistant, client: TestClient +) -> None: + req = await client.get("/") + assert req.status == HTTPStatus.IM_A_TEAPOT + + def read_static_page() -> str: + with open(STRICT_CONNECTION_STATIC_PAGE, encoding="utf-8") as file: + return file.read() + + assert await req.text() == await hass.async_add_executor_job(read_static_page) + + +@pytest.mark.parametrize( + "test_func", + [ + _test_strict_connection_non_cloud_enabled_external_unauthenticated_requests, + _test_strict_connection_non_cloud_enabled_external_unauthenticated_requests_refresh_token, + _test_strict_connection_non_cloud_enabled_external_unauthenticated_requests_temp_session, + ], + ids=[ + "no cookie", + "refresh token cookie", + "temp session cookie", + ], +) +@pytest.mark.parametrize( + ("strict_connection_mode", "request_func"), + [ + (StrictConnectionMode.DROP_CONNECTION, _drop_connection_unauthorized_request), + (StrictConnectionMode.STATIC_PAGE, _static_page_unauthorized_request), + ], + ids=["drop connection", "static page"], +) +async def test_strict_connection_non_cloud_external_unauthenticated_requests( + hass: HomeAssistant, + app_strict_connection: web.Application, + aiohttp_client: ClientSessionGenerator, + hass_access_token: str, + test_func: Callable[ + [ + HomeAssistant, + web.Application, + ClientSessionGenerator, + str, + Callable[[HomeAssistant, TestClient], Awaitable[None]], + StrictConnectionMode, + ], + Awaitable[None], + ], + strict_connection_mode: StrictConnectionMode, + request_func: Callable[[HomeAssistant, TestClient], Awaitable[None]], +) -> None: + """Test external unauthenticated requests with strict connection non cloud.""" + await test_func( + hass, + app_strict_connection, + aiohttp_client, + hass_access_token, + request_func, + strict_connection_mode, + ) diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index 9e892e2ee43..b84da595ab1 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -7,6 +7,7 @@ from ipaddress import ip_network import logging from pathlib import Path from unittest.mock import Mock, patch +from urllib.parse import quote_plus import pytest @@ -14,7 +15,10 @@ from homeassistant.auth.providers.legacy_api_password import ( LegacyApiPasswordAuthProvider, ) from homeassistant.components import http +from homeassistant.components.http.const import StrictConnectionMode +from homeassistant.config import async_process_ha_core_config from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers.http import KEY_HASS from homeassistant.helpers.network import NoURLAvailableError from homeassistant.setup import async_setup_component @@ -521,3 +525,78 @@ 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_service_create_temporary_strict_connection_url_strict_connection_disabled( + hass: HomeAssistant, +) -> None: + """Test service create_temporary_strict_connection_url with strict_connection not enabled.""" + assert await async_setup_component(hass, http.DOMAIN, {"http": {}}) + with pytest.raises( + ServiceValidationError, + match="Strict connection is not enabled for non-cloud requests", + ): + await hass.services.async_call( + http.DOMAIN, + "create_temporary_strict_connection_url", + blocking=True, + return_response=True, + ) + + +@pytest.mark.parametrize( + ("mode"), + [ + StrictConnectionMode.DROP_CONNECTION, + StrictConnectionMode.STATIC_PAGE, + ], +) +async def test_service_create_temporary_strict_connection( + hass: HomeAssistant, mode: StrictConnectionMode +) -> None: + """Test service create_temporary_strict_connection_url.""" + assert await async_setup_component( + hass, http.DOMAIN, {"http": {"strict_connection": mode}} + ) + + # No external url set + assert hass.config.external_url is None + assert hass.config.internal_url is None + with pytest.raises(ServiceValidationError, match="No external URL available"): + await hass.services.async_call( + http.DOMAIN, + "create_temporary_strict_connection_url", + blocking=True, + return_response=True, + ) + + # Raise if only internal url is available + hass.config.api = Mock(use_ssl=False, port=8123, local_ip="192.168.123.123") + with pytest.raises(ServiceValidationError, match="No external URL available"): + await hass.services.async_call( + http.DOMAIN, + "create_temporary_strict_connection_url", + blocking=True, + return_response=True, + ) + + # Set external url too + external_url = "https://example.com" + await async_process_ha_core_config( + hass, + {"external_url": external_url}, + ) + assert hass.config.external_url == external_url + response = await hass.services.async_call( + http.DOMAIN, + "create_temporary_strict_connection_url", + blocking=True, + return_response=True, + ) + assert isinstance(response, dict) + direct_url_prefix = f"{external_url}/auth/strict_connection/temp_token?authSig=" + assert response.pop("direct_url").startswith(direct_url_prefix) + assert response.pop("url").startswith( + f"https://login.home-assistant.io?u={quote_plus(direct_url_prefix)}" + ) + assert response == {} # No more keys in response diff --git a/tests/components/http/test_session.py b/tests/components/http/test_session.py new file mode 100644 index 00000000000..ae62365749a --- /dev/null +++ b/tests/components/http/test_session.py @@ -0,0 +1,107 @@ +"""Tests for HTTP session.""" + +from collections.abc import Callable +import logging +from typing import Any +from unittest.mock import patch + +from aiohttp import web +from aiohttp.test_utils import make_mocked_request +import pytest + +from homeassistant.auth.session import SESSION_ID +from homeassistant.components.http.session import ( + COOKIE_NAME, + HomeAssistantCookieStorage, +) +from homeassistant.core import HomeAssistant + + +def fake_request_with_strict_connection_cookie(cookie_value: str) -> web.Request: + """Return a fake request with a strict connection cookie.""" + request = make_mocked_request( + "GET", "/", headers={"Cookie": f"{COOKIE_NAME}={cookie_value}"} + ) + assert COOKIE_NAME in request.cookies + return request + + +@pytest.fixture +def cookie_storage(hass: HomeAssistant) -> HomeAssistantCookieStorage: + """Fixture for the cookie storage.""" + return HomeAssistantCookieStorage(hass) + + +def _encrypt_cookie_data(cookie_storage: HomeAssistantCookieStorage, data: Any) -> str: + """Encrypt cookie data.""" + cookie_data = cookie_storage._encoder(data).encode("utf-8") + return cookie_storage._fernet.encrypt(cookie_data).decode("utf-8") + + +@pytest.mark.parametrize( + "func", + [ + lambda _: "invalid", + lambda storage: _encrypt_cookie_data(storage, "bla"), + lambda storage: _encrypt_cookie_data(storage, None), + ], +) +async def test_load_session_modified_cookies( + cookie_storage: HomeAssistantCookieStorage, + caplog: pytest.LogCaptureFixture, + func: Callable[[HomeAssistantCookieStorage], str], +) -> None: + """Test that on modified cookies the session is empty and the request will be logged for ban.""" + request = fake_request_with_strict_connection_cookie(func(cookie_storage)) + with patch( + "homeassistant.components.http.session.process_wrong_login", + ) as mock_process_wrong_login: + session = await cookie_storage.load_session(request) + assert session.empty + assert ( + "homeassistant.components.http.session", + logging.WARNING, + "Cannot decrypt/parse cookie value", + ) in caplog.record_tuples + mock_process_wrong_login.assert_called() + + +async def test_load_session_validate_session( + hass: HomeAssistant, + cookie_storage: HomeAssistantCookieStorage, +) -> None: + """Test load session validates the session.""" + session = await cookie_storage.new_session() + session[SESSION_ID] = "bla" + request = fake_request_with_strict_connection_cookie( + _encrypt_cookie_data(cookie_storage, cookie_storage._get_session_data(session)) + ) + + with patch.object( + hass.auth.session, "async_validate_strict_connection_session", return_value=True + ) as mock_validate: + session = await cookie_storage.load_session(request) + assert not session.empty + assert session[SESSION_ID] == "bla" + mock_validate.assert_called_with(session) + + # verify lru_cache is working + mock_validate.reset_mock() + await cookie_storage.load_session(request) + mock_validate.assert_not_called() + + session = await cookie_storage.new_session() + session[SESSION_ID] = "something" + request = fake_request_with_strict_connection_cookie( + _encrypt_cookie_data(cookie_storage, cookie_storage._get_session_data(session)) + ) + + with patch.object( + hass.auth.session, + "async_validate_strict_connection_session", + return_value=False, + ): + session = await cookie_storage.load_session(request) + assert session.empty + assert SESSION_ID not in session + assert session._changed diff --git a/tests/components/stream/conftest.py b/tests/components/stream/conftest.py index 9ce23d99152..280d15cd1ef 100644 --- a/tests/components/stream/conftest.py +++ b/tests/components/stream/conftest.py @@ -14,7 +14,6 @@ from __future__ import annotations import asyncio from collections.abc import Generator -from http import HTTPStatus import logging import threading from unittest.mock import Mock, patch @@ -87,6 +86,17 @@ class HLSSync: self._num_recvs = 0 self._num_finished = 0 + def on_resp(): + self._num_finished += 1 + self.check_requests_ready() + + class SyncResponse(web.Response): + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + on_resp() + + self.response = SyncResponse + def reset_request_pool(self, num_requests: int, reset_finished=True): """Use to reset the request counter between segments.""" self._num_recvs = 0 @@ -120,12 +130,6 @@ class HLSSync: self.check_requests_ready() return self._original_not_found() - def response(self, body, headers=None, status=HTTPStatus.OK): - """Intercept the Response call so we know when the web handler is finished.""" - self._num_finished += 1 - self.check_requests_ready() - return self._original_response(body=body, headers=headers, status=status) - async def recv(self, output: StreamOutput, **kw): """Intercept the recv call so we know when the response is blocking on recv.""" self._num_recvs += 1 @@ -164,7 +168,7 @@ def hls_sync(): ), patch( "homeassistant.components.stream.hls.web.Response", - side_effect=sync.response, + new=sync.response, ), ): yield sync diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index e96f1c4f903..2bd76accfdd 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -701,7 +701,7 @@ async def test_get_services( assert msg["id"] == id_ assert msg["type"] == const.TYPE_RESULT assert msg["success"] - assert msg["result"] == hass.services.async_services() + assert msg["result"].keys() == hass.services.async_services().keys() async def test_get_config( diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 74b8a86ce7c..b5e71f4c9d8 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -7,6 +7,7 @@ from typing import Any from unittest.mock import AsyncMock, Mock, patch import pytest +from pytest_unordered import unordered import voluptuous as vol # To prevent circular import when running just this file @@ -16,6 +17,7 @@ import homeassistant.components # noqa: F401 from homeassistant.components.group import DOMAIN as DOMAIN_GROUP, Group from homeassistant.components.logger import DOMAIN as DOMAIN_LOGGER from homeassistant.components.shell_command import DOMAIN as DOMAIN_SHELL_COMMAND +from homeassistant.components.system_health import DOMAIN as DOMAIN_SYSTEM_HEALTH from homeassistant.const import ( ATTR_ENTITY_ID, ENTITY_MATCH_ALL, @@ -785,7 +787,7 @@ async def test_async_get_all_descriptions(hass: HomeAssistant) -> None: """Test async_get_all_descriptions.""" group_config = {DOMAIN_GROUP: {}} assert await async_setup_component(hass, DOMAIN_GROUP, group_config) - assert await async_setup_component(hass, "system_health", {}) + assert await async_setup_component(hass, DOMAIN_SYSTEM_HEALTH, {}) with patch( "homeassistant.helpers.service._load_services_files", @@ -795,17 +797,20 @@ async def test_async_get_all_descriptions(hass: HomeAssistant) -> None: # Test we only load services.yaml for integrations with services.yaml # And system_health has no services - assert proxy_load_services_files.mock_calls[0][1][1] == [ - await async_get_integration(hass, "group") - ] + assert proxy_load_services_files.mock_calls[0][1][1] == unordered( + [ + await async_get_integration(hass, DOMAIN_GROUP), + await async_get_integration(hass, "http"), # system_health requires http + ] + ) - assert len(descriptions) == 1 - - assert "description" in descriptions["group"]["reload"] - assert "fields" in descriptions["group"]["reload"] + assert len(descriptions) == 2 + assert DOMAIN_GROUP in descriptions + assert "description" in descriptions[DOMAIN_GROUP]["reload"] + assert "fields" in descriptions[DOMAIN_GROUP]["reload"] # Does not have services - assert "system_health" not in descriptions + assert DOMAIN_SYSTEM_HEALTH not in descriptions logger_config = {DOMAIN_LOGGER: {}} @@ -833,8 +838,8 @@ async def test_async_get_all_descriptions(hass: HomeAssistant) -> None: await async_setup_component(hass, DOMAIN_LOGGER, logger_config) descriptions = await service.async_get_all_descriptions(hass) - assert len(descriptions) == 2 - + assert len(descriptions) == 3 + assert DOMAIN_LOGGER in descriptions assert descriptions[DOMAIN_LOGGER]["set_default_level"]["name"] == "Translated name" assert ( descriptions[DOMAIN_LOGGER]["set_default_level"]["description"] diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index 79c64259f8b..76acb2ff678 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -5,6 +5,7 @@ from unittest.mock import patch import pytest +from homeassistant.components.http.const import StrictConnectionMode from homeassistant.config import YAML_CONFIG_FILE from homeassistant.scripts import check_config @@ -134,6 +135,7 @@ def test_secrets(mock_is_file, event_loop, mock_hass_config_yaml: None) -> None: "login_attempts_threshold": -1, "server_port": 8123, "ssl_profile": "modern", + "strict_connection": StrictConnectionMode.DISABLED, "use_x_frame_options": True, "server_host": ["0.0.0.0", "::"], } From 6d22dd073c9ceb0abf92e66fbe9236eafe7e8afa Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Fri, 12 Apr 2024 16:03:23 +0200 Subject: [PATCH 516/967] Bump ruff to 0.3.7 (#115451) Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- .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 760e7e20676..326346a9e81 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.3.5 + rev: v0.3.7 hooks: - id: ruff args: diff --git a/pyproject.toml b/pyproject.toml index 79a66cc7d82..92b3649d3e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -659,7 +659,7 @@ filterwarnings = [ ] [tool.ruff] -required-version = ">=0.3.4" +required-version = ">=0.3.7" [tool.ruff.lint] select = [ diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index dacdb752a8d..46ade953da2 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.2.6 -ruff==0.3.5 +ruff==0.3.7 yamllint==1.35.1 From bea4c52d107ee9f9e5ad19f9734cae4b4d16f84f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 12 Apr 2024 16:33:05 +0200 Subject: [PATCH 517/967] Ignore coverage for aiohttp_resolver backport helper (#115177) * Ignore coverage for aiohttp_resolver backport helper * Adjust generate to sort core items * Adjust validate to sort core items * Split line * Apply suggestion Co-authored-by: Dave T <17680170+davet2001@users.noreply.github.com> * Fix suggestion --------- Co-authored-by: Dave T <17680170+davet2001@users.noreply.github.com> --- .coveragerc | 3 +- script/hassfest/coverage.py | 85 ++++++++++++++++++++----------------- 2 files changed, 49 insertions(+), 39 deletions(-) diff --git a/.coveragerc b/.coveragerc index c02a6fefe75..ceff3384202 100644 --- a/.coveragerc +++ b/.coveragerc @@ -6,11 +6,12 @@ source = homeassistant omit = homeassistant/__main__.py + homeassistant/helpers/backports/aiohttp_resolver.py homeassistant/helpers/signal.py homeassistant/scripts/__init__.py + homeassistant/scripts/benchmark/__init__.py homeassistant/scripts/check_config.py homeassistant/scripts/ensure_config.py - homeassistant/scripts/benchmark/__init__.py homeassistant/scripts/macos/__init__.py # omit pieces of code that rely on external devices being present diff --git a/script/hassfest/coverage.py b/script/hassfest/coverage.py index 264960a42e1..686a6697e49 100644 --- a/script/hassfest/coverage.py +++ b/script/hassfest/coverage.py @@ -20,24 +20,17 @@ DONT_IGNORE = ( "scene.py", ) -PREFIX = """# Sorted by hassfest. +CORE_PREFIX = """# Sorted by hassfest. # # To sort, run python3 -m script.hassfest -p coverage [run] source = homeassistant omit = - homeassistant/__main__.py - homeassistant/helpers/signal.py - homeassistant/scripts/__init__.py - homeassistant/scripts/check_config.py - homeassistant/scripts/ensure_config.py - homeassistant/scripts/benchmark/__init__.py - homeassistant/scripts/macos/__init__.py - - # omit pieces of code that rely on external devices being present """ - +COMPONENTS_PREFIX = ( + " # omit pieces of code that rely on external devices being present\n" +) SUFFIX = """[report] # Regexes for lines to exclude from consideration exclude_lines = @@ -62,6 +55,7 @@ def validate(integrations: dict[str, Integration], config: Config) -> None: coverage_path = config.root / ".coveragerc" not_found: list[str] = [] + unsorted: list[str] = [] checking = False previous_line = "" @@ -69,6 +63,10 @@ def validate(integrations: dict[str, Integration], config: Config) -> None: for line in fp: line = line.strip() + if line == COMPONENTS_PREFIX.strip(): + previous_line = "" + continue + if not line or line.startswith("#"): continue @@ -92,27 +90,21 @@ def validate(integrations: dict[str, Integration], config: Config) -> None: not_found.append(line) continue + if line < previous_line: + unsorted.append(line) + previous_line = line + if not line.startswith("homeassistant/components/"): continue - integration_path = path.parent - while len(integration_path.parts) > 3: - integration_path = integration_path.parent - - integration = integrations[integration_path.name] - - # Ensure sorted - if line < previous_line: - integration.add_error( - "coverage", - f"{line} is unsorted in .coveragerc file", - ) - previous_line = line - - # Ignore sub-directories for further checks + # Ignore sub-directories if len(path.parts) > 4: continue + integration_path = path.parent + + integration = integrations[integration_path.name] + if ( path.parts[-1] == "*" and Path(f"tests/components/{integration.domain}/__init__.py").exists() @@ -132,6 +124,15 @@ def validate(integrations: dict[str, Integration], config: Config) -> None: f"{check} must not be ignored by the .coveragerc file", ) + if unsorted: + config.add_error( + "coverage", + "Paths are unsorted in .coveragerc file. " + "Run python3 -m script.hassfest\n - " + f"{'\n - '.join(unsorted)}", + fixable=True, + ) + if not_found: raise RuntimeError( f".coveragerc references files that don't exist: {', '.join(not_found)}." @@ -141,23 +142,31 @@ def validate(integrations: dict[str, Integration], config: Config) -> None: def generate(integrations: dict[str, Integration], config: Config) -> None: """Sort coverage.""" coverage_path = config.root / ".coveragerc" - lines = [] - start = False + core = [] + components = [] + section = "header" with coverage_path.open("rt") as fp: for line in fp: - if ( - not start - and line - == " # omit pieces of code that rely on external devices being present\n" - ): - start = True - elif line == "[report]\n": + if line == "[report]\n": break - elif start and line != "\n": - lines.append(line) - content = f"{PREFIX}{"".join(sorted(lines))}\n\n{SUFFIX}" + if section != "core" and line == "omit =\n": + section = "core" + elif section != "components" and line == COMPONENTS_PREFIX: + section = "components" + elif section == "core" and line != "\n": + core.append(line) + elif section == "components" and line != "\n": + components.append(line) + + assert core, "core should be a non-empty list" + assert components, "components should be a non-empty list" + content = ( + f"{CORE_PREFIX}{"".join(sorted(core))}\n" + f"{COMPONENTS_PREFIX}{"".join(sorted(components))}\n" + f"\n{SUFFIX}" + ) with coverage_path.open("w") as fp: fp.write(content) From b266224ccdb1c773e390c69f500c4d07461f63c4 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Sat, 13 Apr 2024 04:27:38 +1000 Subject: [PATCH 518/967] Add diagnostics to Teslemetry (#115195) * Add diag * Add diag and tests * Fix redaction * Add another energy redact * Review Feedback * Update snapshot * Fixed the wrong integration * Fix snapshot again * Update tests/components/teslemetry/test_diagnostics.py --------- Co-authored-by: G Johansson --- .../components/teslemetry/diagnostics.py | 46 +++ .../snapshots/test_diagnostics.ambr | 295 ++++++++++++++++++ .../components/teslemetry/test_diagnostics.py | 23 ++ 3 files changed, 364 insertions(+) create mode 100644 homeassistant/components/teslemetry/diagnostics.py create mode 100644 tests/components/teslemetry/snapshots/test_diagnostics.ambr create mode 100644 tests/components/teslemetry/test_diagnostics.py diff --git a/homeassistant/components/teslemetry/diagnostics.py b/homeassistant/components/teslemetry/diagnostics.py new file mode 100644 index 00000000000..f8a8e6727a7 --- /dev/null +++ b/homeassistant/components/teslemetry/diagnostics.py @@ -0,0 +1,46 @@ +"""Provides diagnostics for Teslemetry.""" + +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 .const import DOMAIN + +VEHICLE_REDACT = [ + "id", + "user_id", + "vehicle_id", + "vin", + "tokens", + "id_s", + "drive_state_active_route_latitude", + "drive_state_active_route_longitude", + "drive_state_latitude", + "drive_state_longitude", + "drive_state_native_latitude", + "drive_state_native_longitude", +] + +ENERGY_REDACT = ["vin"] + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + vehicles = [ + x.coordinator.data for x in hass.data[DOMAIN][config_entry.entry_id].vehicles + ] + energysites = [ + x.coordinator.data for x in hass.data[DOMAIN][config_entry.entry_id].energysites + ] + + # Return only the relevant children + return { + "vehicles": async_redact_data(vehicles, VEHICLE_REDACT), + "energysites": async_redact_data(energysites, ENERGY_REDACT), + } diff --git a/tests/components/teslemetry/snapshots/test_diagnostics.ambr b/tests/components/teslemetry/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..74eff27c4a0 --- /dev/null +++ b/tests/components/teslemetry/snapshots/test_diagnostics.ambr @@ -0,0 +1,295 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'energysites': list([ + dict({ + 'backup_capable': True, + 'battery_power': 5060, + 'energy_left': 38896.47368421053, + 'generator_power': 0, + 'grid_power': 0, + 'grid_services_active': False, + 'grid_services_power': 0, + 'grid_status': 'Active', + 'island_status': 'on_grid', + 'load_power': 6245, + 'percentage_charged': 95.50537403739663, + 'solar_power': 1185, + 'storm_mode_active': False, + 'timestamp': '2024-01-01T00:00:00+00:00', + 'total_pack_energy': 40727, + 'wall_connectors': dict({ + 'abd-123': dict({ + 'din': 'abd-123', + 'wall_connector_fault_state': 2, + 'wall_connector_power': 0, + 'wall_connector_state': 2, + }), + 'bcd-234': dict({ + 'din': 'bcd-234', + 'wall_connector_fault_state': 2, + 'wall_connector_power': 0, + 'wall_connector_state': 2, + }), + }), + }), + ]), + 'vehicles': list([ + dict({ + 'access_type': 'OWNER', + 'api_version': 71, + 'backseat_token': None, + 'backseat_token_updated_at': None, + 'ble_autopair_enrolled': False, + 'calendar_enabled': True, + 'charge_state_battery_heater_on': False, + 'charge_state_battery_level': 77, + 'charge_state_battery_range': 266.87, + 'charge_state_charge_amps': 16, + 'charge_state_charge_current_request': 16, + 'charge_state_charge_current_request_max': 16, + 'charge_state_charge_enable_request': True, + 'charge_state_charge_energy_added': 0, + 'charge_state_charge_limit_soc': 80, + 'charge_state_charge_limit_soc_max': 100, + 'charge_state_charge_limit_soc_min': 50, + 'charge_state_charge_limit_soc_std': 80, + 'charge_state_charge_miles_added_ideal': 0, + 'charge_state_charge_miles_added_rated': 0, + 'charge_state_charge_port_cold_weather_mode': False, + 'charge_state_charge_port_color': '', + 'charge_state_charge_port_door_open': True, + 'charge_state_charge_port_latch': 'Engaged', + 'charge_state_charge_rate': 0, + 'charge_state_charger_actual_current': 0, + 'charge_state_charger_phases': None, + 'charge_state_charger_pilot_current': 16, + 'charge_state_charger_power': 0, + 'charge_state_charger_voltage': 2, + 'charge_state_charging_state': 'Stopped', + 'charge_state_conn_charge_cable': 'IEC', + 'charge_state_est_battery_range': 275.04, + 'charge_state_fast_charger_brand': '', + 'charge_state_fast_charger_present': False, + 'charge_state_fast_charger_type': 'ACSingleWireCAN', + 'charge_state_ideal_battery_range': 266.87, + 'charge_state_max_range_charge_counter': 0, + 'charge_state_minutes_to_full_charge': 0, + 'charge_state_not_enough_power_to_heat': None, + 'charge_state_off_peak_charging_enabled': False, + 'charge_state_off_peak_charging_times': 'all_week', + 'charge_state_off_peak_hours_end_time': 900, + 'charge_state_preconditioning_enabled': False, + 'charge_state_preconditioning_times': 'all_week', + 'charge_state_scheduled_charging_mode': 'Off', + 'charge_state_scheduled_charging_pending': False, + 'charge_state_scheduled_charging_start_time': None, + 'charge_state_scheduled_charging_start_time_app': 600, + 'charge_state_scheduled_departure_time': 1704837600, + 'charge_state_scheduled_departure_time_minutes': 480, + 'charge_state_supercharger_session_trip_planner': False, + 'charge_state_time_to_full_charge': 0, + 'charge_state_timestamp': 1705707520649, + 'charge_state_trip_charging': False, + 'charge_state_usable_battery_level': 77, + 'charge_state_user_charge_enable_request': None, + 'climate_state_allow_cabin_overheat_protection': True, + 'climate_state_auto_seat_climate_left': False, + 'climate_state_auto_seat_climate_right': True, + 'climate_state_auto_steering_wheel_heat': False, + 'climate_state_battery_heater': False, + 'climate_state_battery_heater_no_power': None, + 'climate_state_cabin_overheat_protection': 'On', + 'climate_state_cabin_overheat_protection_actively_cooling': False, + 'climate_state_climate_keeper_mode': 'off', + 'climate_state_cop_activation_temperature': 'High', + 'climate_state_defrost_mode': 0, + 'climate_state_driver_temp_setting': 22, + 'climate_state_fan_status': 0, + 'climate_state_hvac_auto_request': 'On', + 'climate_state_inside_temp': 29.8, + 'climate_state_is_auto_conditioning_on': False, + 'climate_state_is_climate_on': False, + 'climate_state_is_front_defroster_on': False, + 'climate_state_is_preconditioning': False, + 'climate_state_is_rear_defroster_on': False, + 'climate_state_left_temp_direction': 251, + 'climate_state_max_avail_temp': 28, + 'climate_state_min_avail_temp': 15, + 'climate_state_outside_temp': 30, + 'climate_state_passenger_temp_setting': 22, + 'climate_state_remote_heater_control_enabled': False, + 'climate_state_right_temp_direction': 251, + 'climate_state_seat_heater_left': 0, + 'climate_state_seat_heater_rear_center': 0, + 'climate_state_seat_heater_rear_left': 0, + 'climate_state_seat_heater_rear_right': 0, + 'climate_state_seat_heater_right': 0, + 'climate_state_side_mirror_heaters': False, + 'climate_state_steering_wheel_heat_level': 0, + 'climate_state_steering_wheel_heater': False, + 'climate_state_supports_fan_only_cabin_overheat_protection': True, + 'climate_state_timestamp': 1705707520649, + 'climate_state_wiper_blade_heater': False, + 'color': None, + 'drive_state_active_route_latitude': '**REDACTED**', + 'drive_state_active_route_longitude': '**REDACTED**', + 'drive_state_active_route_miles_to_arrival': 0.039491, + 'drive_state_active_route_minutes_to_arrival': 0.103577, + 'drive_state_active_route_traffic_minutes_delay': 0, + 'drive_state_gps_as_of': 1701129612, + 'drive_state_heading': 185, + 'drive_state_latitude': '**REDACTED**', + 'drive_state_longitude': '**REDACTED**', + 'drive_state_native_latitude': '**REDACTED**', + 'drive_state_native_location_supported': 1, + 'drive_state_native_longitude': '**REDACTED**', + 'drive_state_native_type': 'wgs', + 'drive_state_power': -7, + 'drive_state_shift_state': None, + 'drive_state_speed': None, + 'drive_state_timestamp': 1705707520649, + 'granular_access_hide_private': False, + 'gui_settings_gui_24_hour_time': False, + 'gui_settings_gui_charge_rate_units': 'kW', + 'gui_settings_gui_distance_units': 'km/hr', + 'gui_settings_gui_range_display': 'Rated', + 'gui_settings_gui_temperature_units': 'C', + 'gui_settings_gui_tirepressure_units': 'Psi', + 'gui_settings_show_range_units': False, + 'gui_settings_timestamp': 1705707520649, + 'id': '**REDACTED**', + 'id_s': '**REDACTED**', + 'in_service': False, + 'state': 'online', + 'tokens': '**REDACTED**', + 'user_id': '**REDACTED**', + 'vehicle_config_aux_park_lamps': 'Eu', + 'vehicle_config_badge_version': 1, + 'vehicle_config_can_accept_navigation_requests': True, + 'vehicle_config_can_actuate_trunks': True, + '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_dashcam_clip_save_supported': True, + 'vehicle_config_default_charge_to_max': False, + 'vehicle_config_driver_assist': 'TeslaAP3', + 'vehicle_config_ece_restrictions': False, + 'vehicle_config_efficiency_package': 'M32021', + 'vehicle_config_eu_vehicle': True, + 'vehicle_config_exterior_color': 'DeepBlue', + 'vehicle_config_exterior_trim': 'Black', + 'vehicle_config_exterior_trim_override': '', + 'vehicle_config_has_air_suspension': False, + 'vehicle_config_has_ludicrous_mode': False, + 'vehicle_config_has_seat_cooling': False, + 'vehicle_config_headlamp_type': 'Global', + 'vehicle_config_interior_trim_type': 'White2', + 'vehicle_config_key_version': 2, + 'vehicle_config_motorized_charge_port': True, + 'vehicle_config_paint_color_override': '0,9,25,0.7,0.04', + 'vehicle_config_performance_package': 'Base', + 'vehicle_config_plg': True, + 'vehicle_config_pws': True, + 'vehicle_config_rear_drive_unit': 'PM216MOSFET', + 'vehicle_config_rear_seat_heaters': 1, + 'vehicle_config_rear_seat_type': 0, + 'vehicle_config_rhd': True, + 'vehicle_config_roof_color': 'RoofColorGlass', + 'vehicle_config_seat_type': None, + 'vehicle_config_spoiler_type': 'None', + 'vehicle_config_sun_roof_installed': None, + 'vehicle_config_supports_qr_pairing': False, + 'vehicle_config_third_row_seats': 'None', + 'vehicle_config_timestamp': 1705707520649, + 'vehicle_config_trim_badging': '74d', + 'vehicle_config_use_range_badging': True, + 'vehicle_config_utc_offset': 36000, + 'vehicle_config_webcam_selfie_supported': True, + 'vehicle_config_webcam_supported': True, + 'vehicle_config_wheel_type': 'Pinwheel18CapKit', + 'vehicle_id': '**REDACTED**', + 'vehicle_state_api_version': 71, + 'vehicle_state_autopark_state_v2': 'unavailable', + 'vehicle_state_calendar_supported': True, + 'vehicle_state_car_version': '2023.44.30.8 06f534d46010', + 'vehicle_state_center_display_state': 0, + 'vehicle_state_dashcam_clip_save_available': True, + 'vehicle_state_dashcam_state': 'Recording', + 'vehicle_state_df': 0, + 'vehicle_state_dr': 0, + 'vehicle_state_fd_window': 0, + 'vehicle_state_feature_bitmask': 'fbdffbff,187f', + 'vehicle_state_fp_window': 0, + 'vehicle_state_ft': 0, + 'vehicle_state_is_user_present': False, + 'vehicle_state_locked': False, + 'vehicle_state_media_info_audio_volume': 2.6667, + 'vehicle_state_media_info_audio_volume_increment': 0.333333, + 'vehicle_state_media_info_audio_volume_max': 10.333333, + 'vehicle_state_media_info_media_playback_status': 'Stopped', + 'vehicle_state_media_info_now_playing_album': '', + 'vehicle_state_media_info_now_playing_artist': '', + 'vehicle_state_media_info_now_playing_duration': 0, + 'vehicle_state_media_info_now_playing_elapsed': 0, + 'vehicle_state_media_info_now_playing_source': 'Spotify', + 'vehicle_state_media_info_now_playing_station': '', + 'vehicle_state_media_info_now_playing_title': '', + 'vehicle_state_media_state_remote_control_enabled': True, + 'vehicle_state_notifications_supported': True, + 'vehicle_state_odometer': 6481.019282, + 'vehicle_state_parsed_calendar_supported': True, + 'vehicle_state_pf': 0, + 'vehicle_state_pr': 0, + 'vehicle_state_rd_window': 0, + 'vehicle_state_remote_start': False, + 'vehicle_state_remote_start_enabled': True, + 'vehicle_state_remote_start_supported': True, + 'vehicle_state_rp_window': 0, + 'vehicle_state_rt': 0, + 'vehicle_state_santa_mode': 0, + 'vehicle_state_sentry_mode': False, + 'vehicle_state_sentry_mode_available': True, + 'vehicle_state_service_mode': False, + 'vehicle_state_service_mode_plus': False, + 'vehicle_state_software_update_download_perc': 0, + 'vehicle_state_software_update_expected_duration_sec': 2700, + 'vehicle_state_software_update_install_perc': 1, + 'vehicle_state_software_update_status': '', + 'vehicle_state_software_update_version': ' ', + 'vehicle_state_speed_limit_mode_active': False, + 'vehicle_state_speed_limit_mode_current_limit_mph': 69, + 'vehicle_state_speed_limit_mode_max_limit_mph': 120, + 'vehicle_state_speed_limit_mode_min_limit_mph': 50, + 'vehicle_state_speed_limit_mode_pin_code_set': True, + 'vehicle_state_timestamp': 1705707520649, + 'vehicle_state_tpms_hard_warning_fl': False, + 'vehicle_state_tpms_hard_warning_fr': False, + 'vehicle_state_tpms_hard_warning_rl': False, + 'vehicle_state_tpms_hard_warning_rr': False, + 'vehicle_state_tpms_last_seen_pressure_time_fl': 1705700812, + 'vehicle_state_tpms_last_seen_pressure_time_fr': 1705700793, + 'vehicle_state_tpms_last_seen_pressure_time_rl': 1705700794, + 'vehicle_state_tpms_last_seen_pressure_time_rr': 1705700823, + 'vehicle_state_tpms_pressure_fl': 2.775, + 'vehicle_state_tpms_pressure_fr': 2.8, + 'vehicle_state_tpms_pressure_rl': 2.775, + 'vehicle_state_tpms_pressure_rr': 2.775, + 'vehicle_state_tpms_rcp_front_value': 2.9, + 'vehicle_state_tpms_rcp_rear_value': 2.9, + 'vehicle_state_tpms_soft_warning_fl': False, + 'vehicle_state_tpms_soft_warning_fr': False, + 'vehicle_state_tpms_soft_warning_rl': False, + 'vehicle_state_tpms_soft_warning_rr': False, + 'vehicle_state_valet_mode': False, + 'vehicle_state_valet_pin_needed': False, + 'vehicle_state_vehicle_name': 'Test', + 'vehicle_state_vehicle_self_test_progress': 0, + 'vehicle_state_vehicle_self_test_requested': False, + 'vehicle_state_webcam_available': True, + 'vin': '**REDACTED**', + }), + ]), + }) +# --- diff --git a/tests/components/teslemetry/test_diagnostics.py b/tests/components/teslemetry/test_diagnostics.py new file mode 100644 index 00000000000..fb8eb79a918 --- /dev/null +++ b/tests/components/teslemetry/test_diagnostics.py @@ -0,0 +1,23 @@ +"""Test the Telemetry Diagnostics.""" + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.core import HomeAssistant + +from . import setup_platform + +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + + +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + snapshot: SnapshotAssertion, +) -> None: + """Test diagnostics.""" + + entry = await setup_platform(hass) + + diag = await get_diagnostics_for_config_entry(hass, hass_client, entry) + assert diag == snapshot From 77d1e2c81257d4b06212e7e011078c21373744eb Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Fri, 12 Apr 2024 11:31:51 -0700 Subject: [PATCH 519/967] Allow customizing display name for energy device (#112834) * Allow customizing display name for energy device * optional typing and comment --- homeassistant/components/energy/data.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/energy/data.py b/homeassistant/components/energy/data.py index d4533b2fcc8..d0da07da37c 100644 --- a/homeassistant/components/energy/data.py +++ b/homeassistant/components/energy/data.py @@ -136,6 +136,9 @@ class DeviceConsumption(TypedDict): # This is an ever increasing value stat_consumption: str + # An optional custom name for display in energy graphs + name: str | None + class EnergyPreferences(TypedDict): """Dictionary holding the energy data.""" @@ -287,6 +290,7 @@ ENERGY_SOURCE_SCHEMA = vol.All( DEVICE_CONSUMPTION_SCHEMA = vol.Schema( { vol.Required("stat_consumption"): str, + vol.Optional("name"): str, } ) From 6eaf3402c6534d6db5d9d7157c79bf6246f00fe9 Mon Sep 17 00:00:00 2001 From: dontinelli <73341522+dontinelli@users.noreply.github.com> Date: Fri, 12 Apr 2024 20:33:24 +0200 Subject: [PATCH 520/967] Add re-auth-flow to fyta integration (#114972) * add re-auth-flow to fyta integration * add strings for reauth-flow * resolve typing error * update based on review comments * Update homeassistant/components/fyta/config_flow.py Co-authored-by: G Johansson * add async_auth * adjustment based on review commet * Update test_config_flow.py * remove credentials * Update homeassistant/components/fyta/config_flow.py Co-authored-by: G Johansson * Update tests/components/fyta/test_config_flow.py Co-authored-by: G Johansson * Update tests/components/fyta/test_config_flow.py Co-authored-by: G Johansson * Update conftest.py * Update test_config_flow.py * Aktualisieren von conftest.py * Update test_config_flow.py --------- Co-authored-by: G Johansson --- homeassistant/components/fyta/config_flow.py | 71 +++++++++++++++----- homeassistant/components/fyta/coordinator.py | 4 +- homeassistant/components/fyta/strings.json | 11 +++ tests/components/fyta/conftest.py | 7 +- tests/components/fyta/test_config_flow.py | 65 +++++++++++++++++- 5 files changed, 129 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/fyta/config_flow.py b/homeassistant/components/fyta/config_flow.py index 8419352dc44..e11c024ec1f 100644 --- a/homeassistant/components/fyta/config_flow.py +++ b/homeassistant/components/fyta/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any @@ -13,7 +14,7 @@ from fyta_cli.fyta_exceptions import ( ) import voluptuous as vol -from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from .const import DOMAIN @@ -30,36 +31,70 @@ class FytaConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Fyta.""" VERSION = 1 + _entry: ConfigEntry | None = None + + async def async_auth(self, user_input: Mapping[str, Any]) -> dict[str, str]: + """Reusable Auth Helper.""" + fyta = FytaConnector(user_input[CONF_USERNAME], user_input[CONF_PASSWORD]) + + try: + await fyta.login() + except FytaConnectionError: + return {"base": "cannot_connect"} + except FytaAuthentificationError: + return {"base": "invalid_auth"} + except FytaPasswordError: + return {"base": "invalid_auth", CONF_PASSWORD: "password_error"} + except Exception as e: # pylint: disable=broad-except + _LOGGER.error(e) + return {"base": "unknown"} + finally: + await fyta.client.close() + + return {} async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle the initial step.""" - errors = {} if user_input: self._async_abort_entries_match({CONF_USERNAME: user_input[CONF_USERNAME]}) - fyta = FytaConnector(user_input[CONF_USERNAME], user_input[CONF_PASSWORD]) - - try: - await fyta.login() - except FytaConnectionError: - errors["base"] = "cannot_connect" - except FytaAuthentificationError: - errors["base"] = "invalid_auth" - except FytaPasswordError: - errors["base"] = "invalid_auth" - errors[CONF_PASSWORD] = "password_error" - except Exception: # pylint: disable=broad-except - errors["base"] = "unknown" - else: + if not (errors := await self.async_auth(user_input)): return self.async_create_entry( title=user_input[CONF_USERNAME], data=user_input ) - finally: - await fyta.client.close() return self.async_show_form( step_id="user", data_schema=DATA_SCHEMA, errors=errors ) + + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: + """Handle flow upon an API authentication error.""" + self._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: + """Handle reauthorization flow.""" + errors = {} + assert self._entry is not None + + if user_input and not (errors := await self.async_auth(user_input)): + return self.async_update_reload_and_abort( + self._entry, data={**self._entry.data, **user_input} + ) + + data_schema = self.add_suggested_values_to_schema( + DATA_SCHEMA, + {CONF_USERNAME: self._entry.data[CONF_USERNAME], **(user_input or {})}, + ) + return self.async_show_form( + step_id="reauth_confirm", + data_schema=data_schema, + errors=errors, + ) diff --git a/homeassistant/components/fyta/coordinator.py b/homeassistant/components/fyta/coordinator.py index c132ee75e72..65bd0cb532c 100644 --- a/homeassistant/components/fyta/coordinator.py +++ b/homeassistant/components/fyta/coordinator.py @@ -13,7 +13,7 @@ from fyta_cli.fyta_exceptions import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.update_coordinator import DataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -52,4 +52,4 @@ class FytaCoordinator(DataUpdateCoordinator[dict[int, dict[str, Any]]]): except FytaConnectionError as ex: raise ConfigEntryNotReady from ex except (FytaAuthentificationError, FytaPasswordError) as ex: - raise ConfigEntryError from ex + raise ConfigEntryAuthFailed from ex diff --git a/homeassistant/components/fyta/strings.json b/homeassistant/components/fyta/strings.json index 6d4fe68a86c..3df851489bc 100644 --- a/homeassistant/components/fyta/strings.json +++ b/homeassistant/components/fyta/strings.json @@ -8,8 +8,19 @@ "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]" } + }, + "reauth_confirm": { + "description": "Update your credentials for FYTA API", + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } } }, + "abort": { + "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%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", diff --git a/tests/components/fyta/conftest.py b/tests/components/fyta/conftest.py index e35012a02e8..efebf9827b9 100644 --- a/tests/components/fyta/conftest.py +++ b/tests/components/fyta/conftest.py @@ -5,8 +5,6 @@ from unittest.mock import AsyncMock, patch import pytest -from .test_config_flow import ACCESS_TOKEN, EXPIRATION - @pytest.fixture def mock_fyta(): @@ -17,10 +15,7 @@ def mock_fyta(): "homeassistant.components.fyta.config_flow.FytaConnector", return_value=mock_fyta_api, ) as mock_fyta_api: - mock_fyta_api.return_value.login.return_value = { - "access_token": ACCESS_TOKEN, - "expiration": EXPIRATION, - } + mock_fyta_api.return_value.login.return_value = {} yield mock_fyta_api diff --git a/tests/components/fyta/test_config_flow.py b/tests/components/fyta/test_config_flow.py index 60e6fc76c5b..6aad6295819 100644 --- a/tests/components/fyta/test_config_flow.py +++ b/tests/components/fyta/test_config_flow.py @@ -1,6 +1,5 @@ """Test the fyta config flow.""" -from datetime import datetime from unittest.mock import AsyncMock from fyta_cli.fyta_exceptions import ( @@ -20,8 +19,6 @@ from tests.common import MockConfigEntry USERNAME = "fyta_user" PASSWORD = "fyta_pass" -ACCESS_TOKEN = "123xyz" -EXPIRATION = datetime.now() async def test_user_flow( @@ -121,3 +118,65 @@ async def test_duplicate_entry(hass: HomeAssistant, mock_fyta: AsyncMock) -> Non assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" + + +@pytest.mark.parametrize( + ("exception", "error"), + [ + (FytaConnectionError, {"base": "cannot_connect"}), + (FytaAuthentificationError, {"base": "invalid_auth"}), + (FytaPasswordError, {"base": "invalid_auth", CONF_PASSWORD: "password_error"}), + (Exception, {"base": "unknown"}), + ], +) +async def test_reauth( + hass: HomeAssistant, + exception: Exception, + error: dict[str, str], + mock_fyta: AsyncMock, + mock_setup_entry, +) -> None: + """Test reauth-flow works.""" + + entry = MockConfigEntry( + domain=DOMAIN, + title=USERNAME, + data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_REAUTH, "entry_id": entry.entry_id}, + data=entry.data, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + mock_fyta.return_value.login.side_effect = exception + + # tests with connection error + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert result["errors"] == error + + mock_fyta.return_value.login.side_effect = None + + # tests with all information provided + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_USERNAME: "other_username", CONF_PASSWORD: "other_password"}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert entry.data[CONF_USERNAME] == "other_username" + assert entry.data[CONF_PASSWORD] == "other_password" + + assert len(mock_setup_entry.mock_calls) == 1 From f16ee2ded9d905ebeb251c2faf2462c67322f167 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 12 Apr 2024 23:35:59 +0200 Subject: [PATCH 521/967] Update strict connection static page (#115473) --- .pre-commit-config.yaml | 2 +- .../http/strict_connection_static_page.html | 142 +++++++++++++++--- 2 files changed, 119 insertions(+), 25 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 326346a9e81..cd42fecbfa1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - --ignore-words-list=additionals,alle,alot,astroid,bund,caf,convencional,currenty,datas,farenheit,falsy,fo,frequence,haa,hass,iif,incomfort,ines,ist,nam,nd,pres,pullrequests,resset,rime,ser,serie,te,technik,ue,unsecure,vor,withing,zar - --skip="./.*,*.csv,*.json,*.ambr" - --quiet-level=2 - exclude_types: [csv, json] + exclude_types: [csv, json, html] exclude: ^tests/fixtures/|homeassistant/generated/|tests/components/.*/snapshots/ - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 diff --git a/homeassistant/components/http/strict_connection_static_page.html b/homeassistant/components/http/strict_connection_static_page.html index 24049d9a0eb..86ea8e00e90 100644 --- a/homeassistant/components/http/strict_connection_static_page.html +++ b/homeassistant/components/http/strict_connection_static_page.html @@ -1,46 +1,140 @@ - - I'm a Teapot + + Home Assistant - Access denied + -
-

Error 418: I'm a Teapot

-

- Oops! Looks like the server is taking a coffee break.
- Don't worry, it'll be back to brewing your requests in no time! -

-

+
+ + + + +
+
+

You need access

+

+ This device is not known on + Home Assistant. +

+ + + Learn how to get access +
From d74be6d5fed752058a2a1eba3f09fe3422a11e8a Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Sat, 13 Apr 2024 00:51:36 +0200 Subject: [PATCH 522/967] Set Ruff RUF001-003 to ignore (#115477) --- homeassistant/components/climate/const.py | 2 +- pyproject.toml | 3 +++ tests/components/flux/test_switch.py | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/climate/const.py b/homeassistant/components/climate/const.py index c790b8596a9..b1bf78063c7 100644 --- a/homeassistant/components/climate/const.py +++ b/homeassistant/components/climate/const.py @@ -33,7 +33,7 @@ class HVACMode(StrEnum): # Device is in Dry/Humidity mode DRY = "dry" - # Only the fan is on, not fan and another mode like cool + # Only the fan is on, not fan and another mode like cool FAN_ONLY = "fan_only" diff --git a/pyproject.toml b/pyproject.toml index 92b3649d3e5..da975078d01 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -750,6 +750,9 @@ ignore = [ "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. + "RUF003", # Comment contains ambiguous unicode character. "SIM102", # Use a single if statement instead of nested if statements "SIM108", # Use ternary operator {contents} instead of if-else-block "SIM115", # Use context handler for opening files diff --git a/tests/components/flux/test_switch.py b/tests/components/flux/test_switch.py index a3eeec10fa5..018d1c43b70 100644 --- a/tests/components/flux/test_switch.py +++ b/tests/components/flux/test_switch.py @@ -1115,7 +1115,7 @@ async def test_flux_with_mired( hass: HomeAssistant, mock_light_entities: list[MockLight], ) -> None: - """Test the flux switch´s mode mired.""" + """Test the flux switch's mode mired.""" setup_test_component_platform(hass, light.DOMAIN, mock_light_entities) assert await async_setup_component( @@ -1176,7 +1176,7 @@ async def test_flux_with_rgb( hass: HomeAssistant, mock_light_entities: list[MockLight], ) -> None: - """Test the flux switch´s mode rgb.""" + """Test the flux switch's mode rgb.""" setup_test_component_platform(hass, light.DOMAIN, mock_light_entities) assert await async_setup_component( From b9899a441c035bf28aec1e30a5167db31adac580 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 12 Apr 2024 14:13:06 -1000 Subject: [PATCH 523/967] Bump zeroconf to 0.132.1 (#115501) --- homeassistant/components/zeroconf/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/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 7c489517dd7..3bddbfea576 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["zeroconf"], "quality_scale": "internal", - "requirements": ["zeroconf==0.132.0"] + "requirements": ["zeroconf==0.132.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b253d600a2d..3b4309bcbfa 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -62,7 +62,7 @@ voluptuous-serialize==2.6.0 voluptuous==0.13.1 webrtc-noise-gain==1.2.3 yarl==1.9.4 -zeroconf==0.132.0 +zeroconf==0.132.1 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index 53d108fdce1..5ab882c2d44 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2935,7 +2935,7 @@ zamg==0.3.6 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.132.0 +zeroconf==0.132.1 # homeassistant.components.zeversolar zeversolar==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ca80aa78e00..8be3570bf07 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2273,7 +2273,7 @@ yt-dlp==2024.04.09 zamg==0.3.6 # homeassistant.components.zeroconf -zeroconf==0.132.0 +zeroconf==0.132.1 # homeassistant.components.zeversolar zeversolar==0.3.1 From af2c381a0cda8c6046863bebb82151d3580a15b3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 12 Apr 2024 19:05:08 -1000 Subject: [PATCH 524/967] Bump zeroconf to 0.132.2 (#115505) --- homeassistant/components/zeroconf/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/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 3bddbfea576..0a76af3b9c2 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["zeroconf"], "quality_scale": "internal", - "requirements": ["zeroconf==0.132.1"] + "requirements": ["zeroconf==0.132.2"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3b4309bcbfa..07885c8a067 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -62,7 +62,7 @@ voluptuous-serialize==2.6.0 voluptuous==0.13.1 webrtc-noise-gain==1.2.3 yarl==1.9.4 -zeroconf==0.132.1 +zeroconf==0.132.2 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index 5ab882c2d44..130ff6644c6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2935,7 +2935,7 @@ zamg==0.3.6 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.132.1 +zeroconf==0.132.2 # homeassistant.components.zeversolar zeversolar==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8be3570bf07..d1b55f5c1ff 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2273,7 +2273,7 @@ yt-dlp==2024.04.09 zamg==0.3.6 # homeassistant.components.zeroconf -zeroconf==0.132.1 +zeroconf==0.132.2 # homeassistant.components.zeversolar zeversolar==0.3.1 From 0d0b77c9e4cc9852f7ebbcb101bb4de02d155169 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 12 Apr 2024 19:09:42 -1000 Subject: [PATCH 525/967] Remove eager_start=False from zeroconf (#115498) --- homeassistant/components/zeroconf/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 7b4c06ffb62..bbc89e77a76 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -428,7 +428,6 @@ class ZeroconfDiscovery: zeroconf, async_service_info, service_type, name ), name=f"zeroconf lookup {name}.{service_type}", - eager_start=False, ) async def _async_lookup_and_process_service_update( From 76fefaafb0450740c8338025b9d7e4982a387f9c Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sat, 13 Apr 2024 08:18:45 +0200 Subject: [PATCH 526/967] Move out demo notify tests to the notify platform (#115504) * Move test file * Make independent of demo platform * Restore tests for demo platform for coverage --- tests/components/notify/test_legacy.py | 206 +++++++++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 tests/components/notify/test_legacy.py diff --git a/tests/components/notify/test_legacy.py b/tests/components/notify/test_legacy.py new file mode 100644 index 00000000000..6653e70275d --- /dev/null +++ b/tests/components/notify/test_legacy.py @@ -0,0 +1,206 @@ +"""The tests for legacy notify services.""" + +from collections.abc import Mapping +from pathlib import Path +from typing import Any +from unittest.mock import MagicMock + +import pytest +import voluptuous as vol + +from homeassistant.components import notify +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.setup import async_setup_component + +from tests.common import MockPlatform, mock_platform + + +class MockNotifyPlatform(MockPlatform): + """Help to set up a legacy test notify service.""" + + def __init__(self, async_get_service: Any = None, get_service: Any = None) -> None: + """Return a legacy notify service.""" + super().__init__() + if get_service: + self.get_service = get_service + if async_get_service: + self.async_get_service = async_get_service + + +def mock_notify_platform( + hass: HomeAssistant, + tmp_path: Path, + integration: str = "notify", + async_get_service: Any = None, + get_service: Any = None, +): + """Specialize the mock platform for legacy notify service.""" + loaded_platform = MockNotifyPlatform(async_get_service, get_service) + mock_platform(hass, f"{integration}.notify", loaded_platform) + + return loaded_platform + + +async def help_setup_notify( + hass: HomeAssistant, tmp_path: Path, targets: dict[str, None] | None = None +) -> MagicMock: + """Help set up a platform notify service.""" + send_message_mock = MagicMock() + + class _TestNotifyService(notify.BaseNotificationService): + def __init__(self, targets: dict[str, None] | None) -> None: + """Initialize service.""" + self._targets = targets + super().__init__() + + @property + def targets(self) -> Mapping[str, Any] | None: + """Return a dictionary of registered targets.""" + return self._targets + + def send_message(self, message: str, **kwargs: Any) -> None: + """Send a message.""" + send_message_mock(message, kwargs) + + async def async_get_service( + hass: HomeAssistant, + config: ConfigType, + discovery_info: DiscoveryInfoType | None = None, + ) -> notify.BaseNotificationService: + """Get notify service for mocked platform.""" + return _TestNotifyService(targets) + + # Mock platform with service + mock_notify_platform(hass, tmp_path, "test", async_get_service=async_get_service) + # Setup the platform + await async_setup_component(hass, "notify", {"notify": [{"platform": "test"}]}) + await hass.async_block_till_done() + + # Return mock for assertion service calls + return send_message_mock + + +async def test_sending_none_message(hass: HomeAssistant, tmp_path: Path) -> None: + """Test send with None as message.""" + send_message_mock = await help_setup_notify(hass, tmp_path) + with pytest.raises(vol.Invalid) as exc: + 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']" + ) + send_message_mock.assert_not_called() + + +async def test_sending_templated_message(hass: HomeAssistant, tmp_path: Path) -> None: + """Send a templated message.""" + send_message_mock = await help_setup_notify(hass, tmp_path) + hass.states.async_set("sensor.temperature", 10) + data = { + notify.ATTR_MESSAGE: "{{states.sensor.temperature.state}}", + notify.ATTR_TITLE: "{{ states.sensor.temperature.name }}", + } + await hass.services.async_call(notify.DOMAIN, notify.SERVICE_NOTIFY, data) + await hass.async_block_till_done() + send_message_mock.assert_called_once_with( + "10", {"title": "temperature", "data": None} + ) + + +async def test_method_forwards_correct_data( + hass: HomeAssistant, tmp_path: Path +) -> None: + """Test that all data from the service gets forwarded to service.""" + send_message_mock = await help_setup_notify(hass, tmp_path) + data = { + notify.ATTR_MESSAGE: "my message", + notify.ATTR_TITLE: "my title", + notify.ATTR_DATA: {"hello": "world"}, + } + await hass.services.async_call(notify.DOMAIN, notify.SERVICE_NOTIFY, data) + await hass.async_block_till_done() + send_message_mock.assert_called_once_with( + "my message", {"title": "my title", "data": {"hello": "world"}} + ) + + +async def test_calling_notify_from_script_loaded_from_yaml_without_title( + hass: HomeAssistant, tmp_path: Path +) -> None: + """Test if we can call a notify from a script.""" + send_message_mock = await help_setup_notify(hass, tmp_path) + step = { + "service": "notify.notify", + "data": { + "data": {"push": {"sound": "US-EN-Morgan-Freeman-Roommate-Is-Arriving.wav"}} + }, + "data_template": {"message": "Test 123 {{ 2 + 2 }}\n"}, + } + await async_setup_component( + hass, "script", {"script": {"test": {"sequence": step}}} + ) + await hass.services.async_call("script", "test") + await hass.async_block_till_done() + send_message_mock.assert_called_once_with( + "Test 123 4", + {"data": {"push": {"sound": "US-EN-Morgan-Freeman-Roommate-Is-Arriving.wav"}}}, + ) + + +async def test_calling_notify_from_script_loaded_from_yaml_with_title( + hass: HomeAssistant, tmp_path: Path +) -> None: + """Test if we can call a notify from a script.""" + send_message_mock = await help_setup_notify(hass, tmp_path) + step = { + "service": "notify.notify", + "data": { + "data": {"push": {"sound": "US-EN-Morgan-Freeman-Roommate-Is-Arriving.wav"}} + }, + "data_template": {"message": "Test 123 {{ 2 + 2 }}\n", "title": "Test"}, + } + await async_setup_component( + hass, "script", {"script": {"test": {"sequence": step}}} + ) + await hass.services.async_call("script", "test") + await hass.async_block_till_done() + send_message_mock.assert_called_once_with( + "Test 123 4", + { + "title": "Test", + "data": { + "push": {"sound": "US-EN-Morgan-Freeman-Roommate-Is-Arriving.wav"} + }, + }, + ) + + +async def test_targets_are_services(hass: HomeAssistant, tmp_path: Path) -> None: + """Test that all targets are exposed as individual services.""" + await help_setup_notify(hass, tmp_path, targets={"a": 1, "b": 2}) + assert hass.services.has_service("notify", "notify") is not None + assert hass.services.has_service("notify", "test_a") is not None + assert hass.services.has_service("notify", "test_b") is not None + + +async def test_messages_to_targets_route(hass: HomeAssistant, tmp_path: Path) -> None: + """Test message routing to specific target services.""" + send_message_mock = await help_setup_notify( + hass, tmp_path, targets={"target_name": "test target id"} + ) + + await hass.services.async_call( + "notify", + "test_target_name", + {"message": "my message", "title": "my title", "data": {"hello": "world"}}, + ) + await hass.async_block_till_done() + + send_message_mock.assert_called_once_with( + "my message", + {"target": ["test target id"], "title": "my title", "data": {"hello": "world"}}, + ) From bb9330135dd870503a8a84f1376fe15546960907 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 12 Apr 2024 21:13:01 -1000 Subject: [PATCH 527/967] Fix race in influxdb test (#115514) The patch was still too late in #115442 There is no good candidate to patch here since the late operation is the error log that is being tested. Patching the logger did not seem like a good idea so I went with patching to wait for the error to be emitted since emit is the public API of the log handler and was less likely to change --- tests/components/influxdb/test_init.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/tests/components/influxdb/test_init.py b/tests/components/influxdb/test_init.py index ad3fddeaf6e..9d672b7ceb0 100644 --- a/tests/components/influxdb/test_init.py +++ b/tests/components/influxdb/test_init.py @@ -1,9 +1,9 @@ """The tests for the InfluxDB component.""" -import asyncio from dataclasses import dataclass import datetime from http import HTTPStatus +import logging from unittest.mock import ANY, MagicMock, Mock, call, patch import pytest @@ -1573,21 +1573,25 @@ async def test_invalid_inputs_error( await _setup(hass, mock_client, config_ext, get_write_api) write_api = get_write_api(mock_client) + write_api.side_effect = test_exception - write_api_done_event = asyncio.Event() + log_emit_done = hass.loop.create_future() - def wait_for_write(*args, **kwargs): - hass.loop.call_soon_threadsafe(write_api_done_event.set) - raise test_exception + original_emit = caplog.handler.emit - write_api.side_effect = wait_for_write + def wait_for_emit(record: logging.LogRecord) -> None: + original_emit(record) + if record.levelname == "ERROR": + hass.loop.call_soon_threadsafe(log_emit_done.set_result, None) - with patch(f"{INFLUX_PATH}.time.sleep") as sleep: - write_api_done_event.clear() + with ( + patch(f"{INFLUX_PATH}.time.sleep") as sleep, + patch.object(caplog.handler, "emit", wait_for_emit), + ): hass.states.async_set("fake.something", 1) await hass.async_block_till_done() await async_wait_for_queue_to_process(hass) - await write_api_done_event.wait() + await log_emit_done await hass.async_block_till_done() write_api.assert_called_once() From 1a9ff8c8fae6fe914f2eb2e7d77e7d9d75a1e200 Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Sat, 13 Apr 2024 09:46:04 +0200 Subject: [PATCH 528/967] Ignore Ruff RUF015 (#115481) --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index da975078d01..6b61766d4b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -753,6 +753,7 @@ ignore = [ "RUF001", # String contains ambiguous unicode character. "RUF002", # Docstring contains ambiguous unicode character. "RUF003", # Comment contains ambiguous unicode character. + "RUF015", # Prefer next(...) over single element slice "SIM102", # Use a single if statement instead of nested if statements "SIM108", # Use ternary operator {contents} instead of if-else-block "SIM115", # Use context handler for opening files From 223fefbbfacc813cd1a9ffde9499b96302c76da7 Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Sat, 13 Apr 2024 09:56:33 +0200 Subject: [PATCH 529/967] Enable Ruff RUF018 (#115485) --- homeassistant/components/api/__init__.py | 3 ++- homeassistant/components/light/__init__.py | 12 ++++++------ .../components/mqtt_statestream/__init__.py | 3 ++- homeassistant/components/ring/camera.py | 3 ++- homeassistant/components/zwave_js/diagnostics.py | 3 ++- pyproject.toml | 1 + tests/ruff.toml | 1 + 7 files changed, 16 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py index 496b6fa5fb1..73751daa6cb 100644 --- a/homeassistant/components/api/__init__.py +++ b/homeassistant/components/api/__init__.py @@ -284,7 +284,8 @@ class APIEntityStateView(HomeAssistantView): # Read the state back for our response status_code = HTTPStatus.CREATED if is_new_state else HTTPStatus.OK - assert (state := hass.states.get(entity_id)) + state = hass.states.get(entity_id) + assert state resp = self.json(state.as_dict(), status_code) resp.headers.add("Location", f"/api/states/{entity_id}") diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 332d701148e..b3b1330b3a1 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -517,13 +517,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: params[ATTR_COLOR_TEMP_KELVIN] ) elif ATTR_RGB_COLOR in params and ColorMode.RGB not in supported_color_modes: - assert (rgb_color := params.pop(ATTR_RGB_COLOR)) is not None + rgb_color = params.pop(ATTR_RGB_COLOR) + assert rgb_color is not None if ColorMode.RGBW in supported_color_modes: params[ATTR_RGBW_COLOR] = color_util.color_rgb_to_rgbw(*rgb_color) elif ColorMode.RGBWW in supported_color_modes: - # https://github.com/python/mypy/issues/13673 params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww( - *rgb_color, # type: ignore[call-arg] + *rgb_color, light.min_color_temp_kelvin, light.max_color_temp_kelvin, ) @@ -584,9 +584,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: elif ( ATTR_RGBWW_COLOR in params and ColorMode.RGBWW not in supported_color_modes ): - assert (rgbww_color := params.pop(ATTR_RGBWW_COLOR)) is not None - # https://github.com/python/mypy/issues/13673 - rgb_color = color_util.color_rgbww_to_rgb( # type: ignore[call-arg] + rgbww_color = params.pop(ATTR_RGBWW_COLOR) + assert rgbww_color is not None + rgb_color = color_util.color_rgbww_to_rgb( *rgbww_color, light.min_color_temp_kelvin, light.max_color_temp_kelvin ) if ColorMode.RGB in supported_color_modes: diff --git a/homeassistant/components/mqtt_statestream/__init__.py b/homeassistant/components/mqtt_statestream/__init__.py index 6a1a791d7ac..3a0953a0158 100644 --- a/homeassistant/components/mqtt_statestream/__init__.py +++ b/homeassistant/components/mqtt_statestream/__init__.py @@ -57,7 +57,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def _state_publisher(evt: Event[EventStateChangedData]) -> None: entity_id = evt.data["entity_id"] - assert (new_state := evt.data["new_state"]) + new_state = evt.data["new_state"] + assert new_state payload = new_state.state diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index 282f9816c4c..a5144777eaa 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -167,7 +167,8 @@ class RingCam(RingEntity[RingDoorBell], Camera): def _get_video(self) -> str | None: if self._last_event is None: return None - assert (event_id := self._last_event.get("id")) and isinstance(event_id, int) + event_id = self._last_event.get("id") + assert event_id and isinstance(event_id, int) return self._device.recording_url(event_id) @exception_wrap diff --git a/homeassistant/components/zwave_js/diagnostics.py b/homeassistant/components/zwave_js/diagnostics.py index 777d45efddb..3d61699472d 100644 --- a/homeassistant/components/zwave_js/diagnostics.py +++ b/homeassistant/components/zwave_js/diagnostics.py @@ -151,7 +151,8 @@ async def async_get_device_diagnostics( client: Client = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] identifiers = get_home_and_node_id_from_device_entry(device) node_id = identifiers[1] if identifiers else None - assert (driver := client.driver) + driver = client.driver + assert driver if node_id is None or node_id not in driver.controller.nodes: raise ValueError(f"Node for device {device.id} can't be found") node = driver.controller.nodes[node_id] diff --git a/pyproject.toml b/pyproject.toml index 6b61766d4b8..b9111f505c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -701,6 +701,7 @@ select = [ "RUF005", # Consider iterable unpacking instead of concatenation "RUF006", # Store a reference to the return value of asyncio.create_task "RUF013", # PEP 484 prohibits implicit Optional + "RUF018", # Avoid assignment expressions in assert statements # "RUF100", # Unused `noqa` directive; temporarily every now and then to clean them up "S102", # Use of exec detected "S103", # bad-file-permissions diff --git a/tests/ruff.toml b/tests/ruff.toml index 5455e211762..87725160751 100644 --- a/tests/ruff.toml +++ b/tests/ruff.toml @@ -6,6 +6,7 @@ extend = "../pyproject.toml" extend-ignore = [ "B904", # Use raise from to specify exception cause "N815", # Variable {name} in class scope should not be mixedCase + "RUF018", # Avoid assignment expressions in assert statements ] [lint.isort] From 127c27c9a7b11b1048b0e1a153a8ee3eb26733fb Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sat, 13 Apr 2024 10:14:58 +0200 Subject: [PATCH 530/967] Isolate legacy notify tests (#115470) * Isolate legacy notify tests * Rebase * Refactor --- tests/components/notify/test_init.py | 460 +------------------------ tests/components/notify/test_legacy.py | 423 ++++++++++++++++++++++- 2 files changed, 424 insertions(+), 459 deletions(-) diff --git a/tests/components/notify/test_init.py b/tests/components/notify/test_init.py index 1f9ec81e36a..1ecfc0d9ecf 100644 --- a/tests/components/notify/test_init.py +++ b/tests/components/notify/test_init.py @@ -1,16 +1,11 @@ -"""The tests for notify services that change targets.""" +"""The tests for notify entity platform.""" -import asyncio import copy -from pathlib import Path -from typing import Any -from unittest.mock import MagicMock, Mock, patch +from unittest.mock import MagicMock import pytest import voluptuous as vol -import yaml -from homeassistant import config as hass_config from homeassistant.components import notify from homeassistant.components.notify import ( DOMAIN, @@ -19,23 +14,13 @@ from homeassistant.components.notify import ( NotifyEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - SERVICE_RELOAD, - STATE_UNAVAILABLE, - STATE_UNKNOWN, - Platform, -) +from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant, State -from homeassistant.helpers.discovery import async_load_platform -from homeassistant.helpers.reload import async_setup_reload_service -from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, MockEntity, MockModule, - MockPlatform, - async_get_persistent_notifications, mock_integration, mock_platform, mock_restore_cache, @@ -226,442 +211,3 @@ async def test_name(hass: HomeAssistant, config_flow_fixture: None) -> None: state = hass.states.get(entity3.entity_id) assert state assert state.attributes == {} - - -class MockNotifyPlatform(MockPlatform): - """Help to set up a legacy test notify service.""" - - def __init__(self, async_get_service: Any = None, get_service: Any = None) -> None: - """Return a legacy notify service.""" - super().__init__() - if get_service: - self.get_service = get_service - if async_get_service: - self.async_get_service = async_get_service - - -def mock_notify_platform( - hass: HomeAssistant, - tmp_path: Path, - integration: str = "notify", - async_get_service: Any = None, - get_service: Any = None, -): - """Specialize the mock platform for legacy notify service.""" - loaded_platform = MockNotifyPlatform(async_get_service, get_service) - mock_platform(hass, f"{integration}.notify", loaded_platform) - - return loaded_platform - - -async def test_same_targets(hass: HomeAssistant) -> None: - """Test not changing the targets in a legacy notify service.""" - test = NotificationService(hass) - await test.async_setup(hass, "notify", "test") - await test.async_register_services() - await hass.async_block_till_done() - - assert hasattr(test, "registered_targets") - assert test.registered_targets == {"test_a": 1, "test_b": 2} - - await test.async_register_services() - await hass.async_block_till_done() - assert test.registered_targets == {"test_a": 1, "test_b": 2} - - -async def test_change_targets(hass: HomeAssistant) -> None: - """Test changing the targets in a legacy notify service.""" - test = NotificationService(hass) - await test.async_setup(hass, "notify", "test") - await test.async_register_services() - await hass.async_block_till_done() - - assert hasattr(test, "registered_targets") - assert test.registered_targets == {"test_a": 1, "test_b": 2} - - test.target_list = {"a": 0} - await test.async_register_services() - await hass.async_block_till_done() - assert test.target_list == {"a": 0} - assert test.registered_targets == {"test_a": 0} - - -async def test_add_targets(hass: HomeAssistant) -> None: - """Test adding the targets in a legacy notify service.""" - test = NotificationService(hass) - await test.async_setup(hass, "notify", "test") - await test.async_register_services() - await hass.async_block_till_done() - - assert hasattr(test, "registered_targets") - assert test.registered_targets == {"test_a": 1, "test_b": 2} - - test.target_list = {"a": 1, "b": 2, "c": 3} - await test.async_register_services() - await hass.async_block_till_done() - assert test.target_list == {"a": 1, "b": 2, "c": 3} - assert test.registered_targets == {"test_a": 1, "test_b": 2, "test_c": 3} - - -async def test_remove_targets(hass: HomeAssistant) -> None: - """Test removing targets from the targets in a legacy notify service.""" - test = NotificationService(hass) - await test.async_setup(hass, "notify", "test") - await test.async_register_services() - await hass.async_block_till_done() - - assert hasattr(test, "registered_targets") - assert test.registered_targets == {"test_a": 1, "test_b": 2} - - test.target_list = {"c": 1} - await test.async_register_services() - await hass.async_block_till_done() - assert test.target_list == {"c": 1} - assert test.registered_targets == {"test_c": 1} - - -class NotificationService(notify.BaseNotificationService): - """A test class for legacy notification services.""" - - def __init__( - self, - hass: HomeAssistant, - target_list: dict[str, Any] | None = None, - name="notify", - ) -> None: - """Initialize the service.""" - - async def _async_make_reloadable(hass: HomeAssistant) -> None: - """Initialize the reload service.""" - await async_setup_reload_service(hass, name, [notify.DOMAIN]) - - self.hass = hass - self.target_list = target_list or {"a": 1, "b": 2} - hass.async_create_task(_async_make_reloadable(hass)) - - @property - def targets(self): - """Return a dictionary of devices.""" - return self.target_list - - -async def test_warn_template( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture -) -> None: - """Test warning when template used.""" - assert await async_setup_component(hass, "notify", {}) - - await hass.services.async_call( - "notify", - "persistent_notification", - {"message": "{{ 1 + 1 }}", "title": "Test notif {{ 1 + 1 }}"}, - blocking=True, - ) - # We should only log it once - assert caplog.text.count("Passing templates to notify service is deprecated") == 1 - notifications = async_get_persistent_notifications(hass) - assert len(notifications) == 1 - - -async def test_invalid_platform( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, tmp_path: Path -) -> None: - """Test service setup with an invalid platform.""" - mock_notify_platform(hass, tmp_path, "testnotify1") - # Setup the platform - await async_setup_component( - hass, "notify", {"notify": [{"platform": "testnotify1"}]} - ) - await hass.async_block_till_done() - assert "Invalid notify platform" in caplog.text - caplog.clear() - # Setup the second testnotify2 platform dynamically - mock_notify_platform(hass, tmp_path, "testnotify2") - await async_load_platform( - hass, - "notify", - "testnotify2", - {}, - hass_config={"notify": [{"platform": "testnotify2"}]}, - ) - await hass.async_block_till_done() - assert "Invalid notify platform" in caplog.text - - -async def test_invalid_service( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, tmp_path: Path -) -> None: - """Test service setup with an invalid service object or platform.""" - - def get_service(hass, config, discovery_info=None): - """Return None for an invalid notify service.""" - return None - - mock_notify_platform(hass, tmp_path, "testnotify", get_service=get_service) - # Setup the second testnotify2 platform dynamically - await async_load_platform( - hass, - "notify", - "testnotify", - {}, - hass_config={"notify": [{"platform": "testnotify"}]}, - ) - await hass.async_block_till_done() - assert "Failed to initialize notification service testnotify" in caplog.text - caplog.clear() - - await async_load_platform( - hass, - "notify", - "testnotifyinvalid", - {"notify": [{"platform": "testnotifyinvalid"}]}, - hass_config={"notify": [{"platform": "testnotifyinvalid"}]}, - ) - await hass.async_block_till_done() - assert "Unknown notification service specified" in caplog.text - - -async def test_platform_setup_with_error( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, tmp_path: Path -) -> None: - """Test service setup with an invalid setup.""" - - async def async_get_service(hass, config, discovery_info=None): - """Return None for an invalid notify service.""" - raise Exception("Setup error") - - mock_notify_platform( - hass, tmp_path, "testnotify", async_get_service=async_get_service - ) - # Setup the second testnotify2 platform dynamically - await async_load_platform( - hass, - "notify", - "testnotify", - {}, - hass_config={"notify": [{"platform": "testnotify"}]}, - ) - await hass.async_block_till_done() - assert "Error setting up platform testnotify" in caplog.text - - -async def test_reload_with_notify_builtin_platform_reload( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, tmp_path: Path -) -> None: - """Test reload using the legacy notify platform reload method.""" - - async def async_get_service(hass, config, discovery_info=None): - """Get notify service for mocked platform.""" - targetlist = {"a": 1, "b": 2} - return NotificationService(hass, targetlist, "testnotify") - - # platform with service - mock_notify_platform( - hass, tmp_path, "testnotify", async_get_service=async_get_service - ) - - # Perform a reload using the notify module for testnotify (without services) - await notify.async_reload(hass, "testnotify") - - # Setup the platform - await async_setup_component( - hass, "notify", {"notify": [{"platform": "testnotify"}]} - ) - await hass.async_block_till_done() - assert hass.services.has_service(notify.DOMAIN, "testnotify_a") - assert hass.services.has_service(notify.DOMAIN, "testnotify_b") - - # Perform a reload using the notify module for testnotify (with services) - await notify.async_reload(hass, "testnotify") - assert hass.services.has_service(notify.DOMAIN, "testnotify_a") - assert hass.services.has_service(notify.DOMAIN, "testnotify_b") - - -async def test_setup_platform_and_reload( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, tmp_path: Path -) -> None: - """Test service setup and reload.""" - get_service_called = Mock() - - async def async_get_service(hass, config, discovery_info=None): - """Get notify service for mocked platform.""" - get_service_called(config, discovery_info) - targetlist = {"a": 1, "b": 2} - return NotificationService(hass, targetlist, "testnotify") - - async def async_get_service2(hass, config, discovery_info=None): - """Get legacy notify service for mocked platform.""" - get_service_called(config, discovery_info) - targetlist = {"c": 3, "d": 4} - return NotificationService(hass, targetlist, "testnotify2") - - # Mock first platform - mock_notify_platform( - hass, tmp_path, "testnotify", async_get_service=async_get_service - ) - - # Initialize a second platform testnotify2 - mock_notify_platform( - hass, tmp_path, "testnotify2", async_get_service=async_get_service2 - ) - - # Setup the testnotify platform - await async_setup_component( - hass, "notify", {"notify": [{"platform": "testnotify"}]} - ) - await hass.async_block_till_done() - assert hass.services.has_service("testnotify", SERVICE_RELOAD) - assert hass.services.has_service(notify.DOMAIN, "testnotify_a") - assert hass.services.has_service(notify.DOMAIN, "testnotify_b") - assert get_service_called.call_count == 1 - assert get_service_called.call_args[0][0] == {"platform": "testnotify"} - assert get_service_called.call_args[0][1] is None - get_service_called.reset_mock() - - # Setup the second testnotify2 platform dynamically - await async_load_platform( - hass, - "notify", - "testnotify2", - {}, - hass_config={"notify": [{"platform": "testnotify"}]}, - ) - await hass.async_block_till_done() - assert hass.services.has_service("testnotify2", SERVICE_RELOAD) - assert hass.services.has_service(notify.DOMAIN, "testnotify2_c") - assert hass.services.has_service(notify.DOMAIN, "testnotify2_d") - assert get_service_called.call_count == 1 - assert get_service_called.call_args[0][0] == {} - assert get_service_called.call_args[0][1] == {} - get_service_called.reset_mock() - - # Perform a reload - new_yaml_config_file = tmp_path / "configuration.yaml" - new_yaml_config = yaml.dump({"notify": [{"platform": "testnotify"}]}) - new_yaml_config_file.write_text(new_yaml_config) - - with patch.object(hass_config, "YAML_CONFIG_FILE", new_yaml_config_file): - await hass.services.async_call( - "testnotify", - SERVICE_RELOAD, - {}, - blocking=True, - ) - await hass.services.async_call( - "testnotify2", - SERVICE_RELOAD, - {}, - blocking=True, - ) - await hass.async_block_till_done() - - # Check if the notify services from setup still exist - assert hass.services.has_service(notify.DOMAIN, "testnotify_a") - assert hass.services.has_service(notify.DOMAIN, "testnotify_b") - assert get_service_called.call_count == 1 - assert get_service_called.call_args[0][0] == {"platform": "testnotify"} - assert get_service_called.call_args[0][1] is None - - # Check if the dynamically notify services from setup were removed - assert not hass.services.has_service(notify.DOMAIN, "testnotify2_c") - assert not hass.services.has_service(notify.DOMAIN, "testnotify2_d") - - -async def test_setup_platform_before_notify_setup( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, tmp_path: Path -) -> None: - """Test trying to setup a platform before legacy notify service is setup.""" - get_service_called = Mock() - - async def async_get_service(hass, config, discovery_info=None): - """Get notify service for mocked platform.""" - get_service_called(config, discovery_info) - targetlist = {"a": 1, "b": 2} - return NotificationService(hass, targetlist, "testnotify") - - async def async_get_service2(hass, config, discovery_info=None): - """Get notify service for mocked platform.""" - get_service_called(config, discovery_info) - targetlist = {"c": 3, "d": 4} - return NotificationService(hass, targetlist, "testnotify2") - - # Mock first platform - mock_notify_platform( - hass, tmp_path, "testnotify", async_get_service=async_get_service - ) - - # Initialize a second platform testnotify2 - mock_notify_platform( - hass, tmp_path, "testnotify2", async_get_service=async_get_service2 - ) - - hass_config = {"notify": [{"platform": "testnotify"}]} - - # Setup the second testnotify2 platform from discovery - load_coro = async_load_platform( - hass, Platform.NOTIFY, "testnotify2", {}, hass_config=hass_config - ) - - # Setup the testnotify platform - setup_coro = async_setup_component(hass, "notify", hass_config) - - load_task = asyncio.create_task(load_coro) - setup_task = asyncio.create_task(setup_coro) - - await asyncio.gather(load_task, setup_task) - - await hass.async_block_till_done() - assert hass.services.has_service(notify.DOMAIN, "testnotify_a") - assert hass.services.has_service(notify.DOMAIN, "testnotify_b") - assert hass.services.has_service(notify.DOMAIN, "testnotify2_c") - assert hass.services.has_service(notify.DOMAIN, "testnotify2_d") - - -async def test_setup_platform_after_notify_setup( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, tmp_path: Path -) -> None: - """Test trying to setup a platform after legacy notify service is set up.""" - get_service_called = Mock() - - async def async_get_service(hass, config, discovery_info=None): - """Get notify service for mocked platform.""" - get_service_called(config, discovery_info) - targetlist = {"a": 1, "b": 2} - return NotificationService(hass, targetlist, "testnotify") - - async def async_get_service2(hass, config, discovery_info=None): - """Get notify service for mocked platform.""" - get_service_called(config, discovery_info) - targetlist = {"c": 3, "d": 4} - return NotificationService(hass, targetlist, "testnotify2") - - # Mock first platform - mock_notify_platform( - hass, tmp_path, "testnotify", async_get_service=async_get_service - ) - - # Initialize a second platform testnotify2 - mock_notify_platform( - hass, tmp_path, "testnotify2", async_get_service=async_get_service2 - ) - - hass_config = {"notify": [{"platform": "testnotify"}]} - - # Setup the second testnotify2 platform from discovery - load_coro = async_load_platform( - hass, Platform.NOTIFY, "testnotify2", {}, hass_config=hass_config - ) - - # Setup the testnotify platform - setup_coro = async_setup_component(hass, "notify", hass_config) - - setup_task = asyncio.create_task(setup_coro) - load_task = asyncio.create_task(load_coro) - - await asyncio.gather(load_task, setup_task) - - await hass.async_block_till_done() - assert hass.services.has_service(notify.DOMAIN, "testnotify_a") - assert hass.services.has_service(notify.DOMAIN, "testnotify_b") - assert hass.services.has_service(notify.DOMAIN, "testnotify2_c") - assert hass.services.has_service(notify.DOMAIN, "testnotify2_d") diff --git a/tests/components/notify/test_legacy.py b/tests/components/notify/test_legacy.py index 6653e70275d..71424beeda9 100644 --- a/tests/components/notify/test_legacy.py +++ b/tests/components/notify/test_legacy.py @@ -1,19 +1,50 @@ """The tests for legacy notify services.""" +import asyncio from collections.abc import Mapping from pathlib import Path from typing import Any -from unittest.mock import MagicMock +from unittest.mock import MagicMock, Mock, patch import pytest import voluptuous as vol +import yaml +from homeassistant import config as hass_config from homeassistant.components import notify +from homeassistant.const import SERVICE_RELOAD, Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers.discovery import async_load_platform +from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.setup import async_setup_component -from tests.common import MockPlatform, mock_platform +from tests.common import MockPlatform, async_get_persistent_notifications, mock_platform + + +class NotificationService(notify.BaseNotificationService): + """A test class for legacy notification services.""" + + def __init__( + self, + hass: HomeAssistant, + target_list: dict[str, Any] | None = None, + name="notify", + ) -> None: + """Initialize the service.""" + + async def _async_make_reloadable(hass: HomeAssistant) -> None: + """Initialize the reload service.""" + await async_setup_reload_service(hass, name, [notify.DOMAIN]) + + self.hass = hass + self.target_list = target_list or {"a": 1, "b": 2} + hass.async_create_task(_async_make_reloadable(hass)) + + @property + def targets(self): + """Return a dictionary of devices.""" + return self.target_list class MockNotifyPlatform(MockPlatform): @@ -81,6 +112,394 @@ async def help_setup_notify( return send_message_mock +async def test_same_targets(hass: HomeAssistant) -> None: + """Test not changing the targets in a legacy notify service.""" + test = NotificationService(hass) + await test.async_setup(hass, "notify", "test") + await test.async_register_services() + await hass.async_block_till_done() + + assert hasattr(test, "registered_targets") + assert test.registered_targets == {"test_a": 1, "test_b": 2} + + await test.async_register_services() + await hass.async_block_till_done() + assert test.registered_targets == {"test_a": 1, "test_b": 2} + + +async def test_change_targets(hass: HomeAssistant) -> None: + """Test changing the targets in a legacy notify service.""" + test = NotificationService(hass) + await test.async_setup(hass, "notify", "test") + await test.async_register_services() + await hass.async_block_till_done() + + assert hasattr(test, "registered_targets") + assert test.registered_targets == {"test_a": 1, "test_b": 2} + + test.target_list = {"a": 0} + await test.async_register_services() + await hass.async_block_till_done() + assert test.target_list == {"a": 0} + assert test.registered_targets == {"test_a": 0} + + +async def test_add_targets(hass: HomeAssistant) -> None: + """Test adding the targets in a legacy notify service.""" + test = NotificationService(hass) + await test.async_setup(hass, "notify", "test") + await test.async_register_services() + await hass.async_block_till_done() + + assert hasattr(test, "registered_targets") + assert test.registered_targets == {"test_a": 1, "test_b": 2} + + test.target_list = {"a": 1, "b": 2, "c": 3} + await test.async_register_services() + await hass.async_block_till_done() + assert test.target_list == {"a": 1, "b": 2, "c": 3} + assert test.registered_targets == {"test_a": 1, "test_b": 2, "test_c": 3} + + +async def test_remove_targets(hass: HomeAssistant) -> None: + """Test removing targets from the targets in a legacy notify service.""" + test = NotificationService(hass) + await test.async_setup(hass, "notify", "test") + await test.async_register_services() + await hass.async_block_till_done() + + assert hasattr(test, "registered_targets") + assert test.registered_targets == {"test_a": 1, "test_b": 2} + + test.target_list = {"c": 1} + await test.async_register_services() + await hass.async_block_till_done() + assert test.target_list == {"c": 1} + assert test.registered_targets == {"test_c": 1} + + +async def test_warn_template( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test warning when template used.""" + assert await async_setup_component(hass, "notify", {}) + + await hass.services.async_call( + "notify", + "persistent_notification", + {"message": "{{ 1 + 1 }}", "title": "Test notif {{ 1 + 1 }}"}, + blocking=True, + ) + # We should only log it once + assert caplog.text.count("Passing templates to notify service is deprecated") == 1 + notifications = async_get_persistent_notifications(hass) + assert len(notifications) == 1 + + +async def test_invalid_platform( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, tmp_path: Path +) -> None: + """Test service setup with an invalid platform.""" + mock_notify_platform(hass, tmp_path, "testnotify1") + # Setup the platform + await async_setup_component( + hass, "notify", {"notify": [{"platform": "testnotify1"}]} + ) + await hass.async_block_till_done() + assert "Invalid notify platform" in caplog.text + caplog.clear() + # Setup the second testnotify2 platform dynamically + mock_notify_platform(hass, tmp_path, "testnotify2") + await async_load_platform( + hass, + "notify", + "testnotify2", + {}, + hass_config={"notify": [{"platform": "testnotify2"}]}, + ) + await hass.async_block_till_done() + assert "Invalid notify platform" in caplog.text + + +async def test_invalid_service( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, tmp_path: Path +) -> None: + """Test service setup with an invalid service object or platform.""" + + def get_service(hass, config, discovery_info=None): + """Return None for an invalid notify service.""" + return None + + mock_notify_platform(hass, tmp_path, "testnotify", get_service=get_service) + # Setup the second testnotify2 platform dynamically + await async_load_platform( + hass, + "notify", + "testnotify", + {}, + hass_config={"notify": [{"platform": "testnotify"}]}, + ) + await hass.async_block_till_done() + assert "Failed to initialize notification service testnotify" in caplog.text + caplog.clear() + + await async_load_platform( + hass, + "notify", + "testnotifyinvalid", + {"notify": [{"platform": "testnotifyinvalid"}]}, + hass_config={"notify": [{"platform": "testnotifyinvalid"}]}, + ) + await hass.async_block_till_done() + assert "Unknown notification service specified" in caplog.text + + +async def test_platform_setup_with_error( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, tmp_path: Path +) -> None: + """Test service setup with an invalid setup.""" + + async def async_get_service(hass, config, discovery_info=None): + """Return None for an invalid notify service.""" + raise Exception("Setup error") + + mock_notify_platform( + hass, tmp_path, "testnotify", async_get_service=async_get_service + ) + # Setup the second testnotify2 platform dynamically + await async_load_platform( + hass, + "notify", + "testnotify", + {}, + hass_config={"notify": [{"platform": "testnotify"}]}, + ) + await hass.async_block_till_done() + assert "Error setting up platform testnotify" in caplog.text + + +async def test_reload_with_notify_builtin_platform_reload( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, tmp_path: Path +) -> None: + """Test reload using the legacy notify platform reload method.""" + + async def async_get_service(hass, config, discovery_info=None): + """Get notify service for mocked platform.""" + targetlist = {"a": 1, "b": 2} + return NotificationService(hass, targetlist, "testnotify") + + # platform with service + mock_notify_platform( + hass, tmp_path, "testnotify", async_get_service=async_get_service + ) + + # Perform a reload using the notify module for testnotify (without services) + await notify.async_reload(hass, "testnotify") + + # Setup the platform + await async_setup_component( + hass, "notify", {"notify": [{"platform": "testnotify"}]} + ) + await hass.async_block_till_done() + assert hass.services.has_service(notify.DOMAIN, "testnotify_a") + assert hass.services.has_service(notify.DOMAIN, "testnotify_b") + + # Perform a reload using the notify module for testnotify (with services) + await notify.async_reload(hass, "testnotify") + assert hass.services.has_service(notify.DOMAIN, "testnotify_a") + assert hass.services.has_service(notify.DOMAIN, "testnotify_b") + + +async def test_setup_platform_and_reload( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, tmp_path: Path +) -> None: + """Test service setup and reload.""" + get_service_called = Mock() + + async def async_get_service(hass, config, discovery_info=None): + """Get notify service for mocked platform.""" + get_service_called(config, discovery_info) + targetlist = {"a": 1, "b": 2} + return NotificationService(hass, targetlist, "testnotify") + + async def async_get_service2(hass, config, discovery_info=None): + """Get legacy notify service for mocked platform.""" + get_service_called(config, discovery_info) + targetlist = {"c": 3, "d": 4} + return NotificationService(hass, targetlist, "testnotify2") + + # Mock first platform + mock_notify_platform( + hass, tmp_path, "testnotify", async_get_service=async_get_service + ) + + # Initialize a second platform testnotify2 + mock_notify_platform( + hass, tmp_path, "testnotify2", async_get_service=async_get_service2 + ) + + # Setup the testnotify platform + await async_setup_component( + hass, "notify", {"notify": [{"platform": "testnotify"}]} + ) + await hass.async_block_till_done() + assert hass.services.has_service("testnotify", SERVICE_RELOAD) + assert hass.services.has_service(notify.DOMAIN, "testnotify_a") + assert hass.services.has_service(notify.DOMAIN, "testnotify_b") + assert get_service_called.call_count == 1 + assert get_service_called.call_args[0][0] == {"platform": "testnotify"} + assert get_service_called.call_args[0][1] is None + get_service_called.reset_mock() + + # Setup the second testnotify2 platform dynamically + await async_load_platform( + hass, + "notify", + "testnotify2", + {}, + hass_config={"notify": [{"platform": "testnotify"}]}, + ) + await hass.async_block_till_done() + assert hass.services.has_service("testnotify2", SERVICE_RELOAD) + assert hass.services.has_service(notify.DOMAIN, "testnotify2_c") + assert hass.services.has_service(notify.DOMAIN, "testnotify2_d") + assert get_service_called.call_count == 1 + assert get_service_called.call_args[0][0] == {} + assert get_service_called.call_args[0][1] == {} + get_service_called.reset_mock() + + # Perform a reload + new_yaml_config_file = tmp_path / "configuration.yaml" + new_yaml_config = yaml.dump({"notify": [{"platform": "testnotify"}]}) + new_yaml_config_file.write_text(new_yaml_config) + + with patch.object(hass_config, "YAML_CONFIG_FILE", new_yaml_config_file): + await hass.services.async_call( + "testnotify", + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.services.async_call( + "testnotify2", + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + # Check if the notify services from setup still exist + assert hass.services.has_service(notify.DOMAIN, "testnotify_a") + assert hass.services.has_service(notify.DOMAIN, "testnotify_b") + assert get_service_called.call_count == 1 + assert get_service_called.call_args[0][0] == {"platform": "testnotify"} + assert get_service_called.call_args[0][1] is None + + # Check if the dynamically notify services from setup were removed + assert not hass.services.has_service(notify.DOMAIN, "testnotify2_c") + assert not hass.services.has_service(notify.DOMAIN, "testnotify2_d") + + +async def test_setup_platform_before_notify_setup( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, tmp_path: Path +) -> None: + """Test trying to setup a platform before legacy notify service is setup.""" + get_service_called = Mock() + + async def async_get_service(hass, config, discovery_info=None): + """Get notify service for mocked platform.""" + get_service_called(config, discovery_info) + targetlist = {"a": 1, "b": 2} + return NotificationService(hass, targetlist, "testnotify") + + async def async_get_service2(hass, config, discovery_info=None): + """Get notify service for mocked platform.""" + get_service_called(config, discovery_info) + targetlist = {"c": 3, "d": 4} + return NotificationService(hass, targetlist, "testnotify2") + + # Mock first platform + mock_notify_platform( + hass, tmp_path, "testnotify", async_get_service=async_get_service + ) + + # Initialize a second platform testnotify2 + mock_notify_platform( + hass, tmp_path, "testnotify2", async_get_service=async_get_service2 + ) + + hass_config = {"notify": [{"platform": "testnotify"}]} + + # Setup the second testnotify2 platform from discovery + load_coro = async_load_platform( + hass, Platform.NOTIFY, "testnotify2", {}, hass_config=hass_config + ) + + # Setup the testnotify platform + setup_coro = async_setup_component(hass, "notify", hass_config) + + load_task = asyncio.create_task(load_coro) + setup_task = asyncio.create_task(setup_coro) + + await asyncio.gather(load_task, setup_task) + + await hass.async_block_till_done() + assert hass.services.has_service(notify.DOMAIN, "testnotify_a") + assert hass.services.has_service(notify.DOMAIN, "testnotify_b") + assert hass.services.has_service(notify.DOMAIN, "testnotify2_c") + assert hass.services.has_service(notify.DOMAIN, "testnotify2_d") + + +async def test_setup_platform_after_notify_setup( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, tmp_path: Path +) -> None: + """Test trying to setup a platform after legacy notify service is set up.""" + get_service_called = Mock() + + async def async_get_service(hass, config, discovery_info=None): + """Get notify service for mocked platform.""" + get_service_called(config, discovery_info) + targetlist = {"a": 1, "b": 2} + return NotificationService(hass, targetlist, "testnotify") + + async def async_get_service2(hass, config, discovery_info=None): + """Get notify service for mocked platform.""" + get_service_called(config, discovery_info) + targetlist = {"c": 3, "d": 4} + return NotificationService(hass, targetlist, "testnotify2") + + # Mock first platform + mock_notify_platform( + hass, tmp_path, "testnotify", async_get_service=async_get_service + ) + + # Initialize a second platform testnotify2 + mock_notify_platform( + hass, tmp_path, "testnotify2", async_get_service=async_get_service2 + ) + + hass_config = {"notify": [{"platform": "testnotify"}]} + + # Setup the second testnotify2 platform from discovery + load_coro = async_load_platform( + hass, Platform.NOTIFY, "testnotify2", {}, hass_config=hass_config + ) + + # Setup the testnotify platform + setup_coro = async_setup_component(hass, "notify", hass_config) + + setup_task = asyncio.create_task(setup_coro) + load_task = asyncio.create_task(load_coro) + + await asyncio.gather(load_task, setup_task) + + await hass.async_block_till_done() + assert hass.services.has_service(notify.DOMAIN, "testnotify_a") + assert hass.services.has_service(notify.DOMAIN, "testnotify_b") + assert hass.services.has_service(notify.DOMAIN, "testnotify2_c") + assert hass.services.has_service(notify.DOMAIN, "testnotify2_d") + + async def test_sending_none_message(hass: HomeAssistant, tmp_path: Path) -> None: """Test send with None as message.""" send_message_mock = await help_setup_notify(hass, tmp_path) From 84a975b61ee16964929a8a15381da7d7f803dd0c Mon Sep 17 00:00:00 2001 From: Toni Korhonen Date: Sat, 13 Apr 2024 11:44:02 +0300 Subject: [PATCH 531/967] Add Balboa spa temperature range state control (high/low) (#115285) * Add temperature range switch (high/low) to Balboa spa integration. * Change Balboa spa integration temperature range control from switch to select * Balboa spa integration: Fix ruff formatting * Balboa spa integration: increase test coverage * Balboa spa integration review fixes: Move instance attributes as class attributes. Fix code comments. --- homeassistant/components/balboa/__init__.py | 8 +- homeassistant/components/balboa/icons.json | 5 ++ homeassistant/components/balboa/select.py | 52 ++++++++++++ homeassistant/components/balboa/strings.json | 9 +++ tests/components/balboa/conftest.py | 3 +- tests/components/balboa/test_select.py | 85 ++++++++++++++++++++ 6 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/balboa/select.py create mode 100644 tests/components/balboa/test_select.py diff --git a/homeassistant/components/balboa/__init__.py b/homeassistant/components/balboa/__init__.py index d6a80e8fa8f..7e220bd46f8 100644 --- a/homeassistant/components/balboa/__init__.py +++ b/homeassistant/components/balboa/__init__.py @@ -18,7 +18,13 @@ from .const import CONF_SYNC_TIME, DEFAULT_SYNC_TIME, DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.FAN, Platform.LIGHT] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.CLIMATE, + Platform.FAN, + Platform.LIGHT, + Platform.SELECT, +] KEEP_ALIVE_INTERVAL = timedelta(minutes=1) diff --git a/homeassistant/components/balboa/icons.json b/homeassistant/components/balboa/icons.json index 7261f71bd00..7454366f692 100644 --- a/homeassistant/components/balboa/icons.json +++ b/homeassistant/components/balboa/icons.json @@ -27,6 +27,11 @@ "off": "mdi:pump-off" } } + }, + "select": { + "temperature_range": { + "default": "mdi:thermometer-lines" + } } } } diff --git a/homeassistant/components/balboa/select.py b/homeassistant/components/balboa/select.py new file mode 100644 index 00000000000..3fdd8c4d014 --- /dev/null +++ b/homeassistant/components/balboa/select.py @@ -0,0 +1,52 @@ +"""Support for Spa Client selects.""" + +from pybalboa import SpaClient, SpaControl +from pybalboa.enums import LowHighRange + +from homeassistant.components.select import SelectEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .entity import BalboaEntity + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the spa select entity.""" + spa: SpaClient = hass.data[DOMAIN][entry.entry_id] + async_add_entities([BalboaTempRangeSelectEntity(spa.temperature_range)]) + + +class BalboaTempRangeSelectEntity(BalboaEntity, SelectEntity): + """Representation of a Temperature Range select.""" + + _attr_icon = "mdi:thermometer-lines" + _attr_name = "Temperature range" + _attr_unique_id = "temperature_range" + _attr_translation_key = "temperature_range" + _attr_options = [ + LowHighRange.LOW.name.lower(), + LowHighRange.HIGH.name.lower(), + ] + + def __init__(self, control: SpaControl) -> None: + """Initialise the select.""" + super().__init__(control.client, "TempHiLow") + self._control = control + + @property + def current_option(self) -> str | None: + """Return current select option.""" + if self._control.state == LowHighRange.HIGH: + return LowHighRange.HIGH.name.lower() + return LowHighRange.LOW.name.lower() + + async def async_select_option(self, option: str) -> None: + """Select temperature range high/low mode.""" + if option == LowHighRange.HIGH.name.lower(): + await self._client.set_temperature_range(LowHighRange.HIGH) + else: + await self._client.set_temperature_range(LowHighRange.LOW) diff --git a/homeassistant/components/balboa/strings.json b/homeassistant/components/balboa/strings.json index 3c8f82764d4..6ced7dfd8c3 100644 --- a/homeassistant/components/balboa/strings.json +++ b/homeassistant/components/balboa/strings.json @@ -65,6 +65,15 @@ "only_light": { "name": "Light" } + }, + "select": { + "temperature_range": { + "name": "Temperature range", + "state": { + "low": "Low", + "high": "High" + } + } } } } diff --git a/tests/components/balboa/conftest.py b/tests/components/balboa/conftest.py index fce022572c3..7f679773f93 100644 --- a/tests/components/balboa/conftest.py +++ b/tests/components/balboa/conftest.py @@ -5,7 +5,7 @@ from __future__ import annotations from collections.abc import Callable, Generator from unittest.mock import AsyncMock, MagicMock, patch -from pybalboa.enums import HeatMode +from pybalboa.enums import HeatMode, LowHighRange import pytest from homeassistant.core import HomeAssistant @@ -60,5 +60,6 @@ def client_fixture() -> Generator[MagicMock, None, None]: client.heat_state = 2 client.lights = [] client.pumps = [] + client.temperature_range.state = LowHighRange.LOW yield client diff --git a/tests/components/balboa/test_select.py b/tests/components/balboa/test_select.py new file mode 100644 index 00000000000..bd79f024817 --- /dev/null +++ b/tests/components/balboa/test_select.py @@ -0,0 +1,85 @@ +"""Tests of the select entity of the balboa integration.""" + +from __future__ import annotations + +from unittest.mock import MagicMock, call + +from pybalboa import SpaControl +from pybalboa.enums import LowHighRange +import pytest + +from homeassistant.components.select import ( + ATTR_OPTION, + DOMAIN as SELECT_DOMAIN, + SERVICE_SELECT_OPTION, +) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant + +from . import client_update, init_integration + +ENTITY_SELECT = "select.fakespa_temperature_range" + + +@pytest.fixture +def mock_select(client: MagicMock): + """Return a mock switch.""" + select = MagicMock(SpaControl) + + async def set_state(state: LowHighRange): + select.state = state # mock the spacontrol state + + select.client = client + select.state = LowHighRange.LOW + select.set_state = set_state + client.temperature_range = select + return select + + +async def test_select(hass: HomeAssistant, client: MagicMock, mock_select) -> None: + """Test spa temperature range select.""" + await init_integration(hass) + + # check if the initial state is off + state = hass.states.get(ENTITY_SELECT) + assert state.state == LowHighRange.LOW.name.lower() + + # test high state + await _select_option_and_wait(hass, ENTITY_SELECT, LowHighRange.HIGH.name.lower()) + assert client.set_temperature_range.call_count == 1 + assert client.set_temperature_range.call_args == call(LowHighRange.HIGH) + + # test back to low state + await _select_option_and_wait(hass, ENTITY_SELECT, LowHighRange.LOW.name.lower()) + assert client.set_temperature_range.call_count == 2 # total call count + assert client.set_temperature_range.call_args == call(LowHighRange.LOW) + + +async def test_selected_option( + hass: HomeAssistant, client: MagicMock, mock_select +) -> None: + """Test spa temperature range selected option.""" + + await init_integration(hass) + + # ensure initial low state + state = hass.states.get(ENTITY_SELECT) + assert state.state == LowHighRange.LOW.name.lower() + + # ensure high state + mock_select.state = LowHighRange.HIGH + state = await client_update(hass, client, ENTITY_SELECT) + assert state.state == LowHighRange.HIGH.name.lower() + + +async def _select_option_and_wait(hass: HomeAssistant | None, entity, option): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: entity, + ATTR_OPTION: option, + }, + blocking=True, + ) + await hass.async_block_till_done() From 27f6a7de43420ff32b990c41af256467fc01d143 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 13 Apr 2024 10:48:34 +0200 Subject: [PATCH 532/967] Revert mypy_config formatting (#115518) --- script/hassfest/mypy_config.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index c6c5907cdb9..76fe47837e4 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -43,8 +43,20 @@ GENERAL_SETTINGS: Final[dict[str, str]] = { "warn_redundant_casts": "true", "warn_unused_configs": "true", "warn_unused_ignores": "true", - "enable_error_code": "ignore-without-code, redundant-self, truthy-iterable", - "disable_error_code": "annotation-unchecked, import-not-found, import-untyped", + "enable_error_code": ", ".join( # noqa: FLY002 + [ + "ignore-without-code", + "redundant-self", + "truthy-iterable", + ] + ), + "disable_error_code": ", ".join( # noqa: FLY002 + [ + "annotation-unchecked", + "import-not-found", + "import-untyped", + ] + ), # Impractical in real code # E.g. this breaks passthrough ParamSpec typing with Concatenate "extra_checks": "false", From 38c7b99aef304db3e1f0a3b7705eae012db835ba Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sat, 13 Apr 2024 12:58:31 +0200 Subject: [PATCH 533/967] Make legacy notify group tests independent of demo platform (#115494) --- tests/components/group/test_notify.py | 214 +++++++++++++++++--------- 1 file changed, 139 insertions(+), 75 deletions(-) diff --git a/tests/components/group/test_notify.py b/tests/components/group/test_notify.py index 5709e648508..2f9afdf5aa5 100644 --- a/tests/components/group/test_notify.py +++ b/tests/components/group/test_notify.py @@ -1,31 +1,91 @@ """The tests for the notify.group platform.""" -from unittest.mock import MagicMock, patch +from collections.abc import Mapping +from pathlib import Path +from typing import Any +from unittest.mock import MagicMock, call, patch from homeassistant import config as hass_config from homeassistant.components import notify -import homeassistant.components.demo.notify as demo from homeassistant.components.group import SERVICE_RELOAD import homeassistant.components.group.notify as group from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.setup import async_setup_component -from tests.common import get_fixture_path +from tests.common import MockPlatform, get_fixture_path, mock_platform -async def test_send_message_with_data(hass: HomeAssistant) -> None: +class MockNotifyPlatform(MockPlatform): + """Help to set up a legacy test notify platform.""" + + def __init__(self, async_get_service: Any) -> None: + """Initialize platform.""" + super().__init__() + self.async_get_service = async_get_service + + +def mock_notify_platform( + hass: HomeAssistant, + tmp_path: Path, + async_get_service: Any = None, +): + """Specialize the mock platform for legacy notify service.""" + loaded_platform = MockNotifyPlatform(async_get_service) + mock_platform(hass, "test.notify", loaded_platform) + + return loaded_platform + + +async def help_setup_notify( + hass: HomeAssistant, + tmp_path: Path, + targets: dict[str, None] | None = None, + group_setup: list[dict[str, None]] | None = None, +) -> MagicMock: + """Help set up a platform notify service.""" + send_message_mock = MagicMock() + + class _TestNotifyService(notify.BaseNotificationService): + def __init__(self, targets: dict[str, None] | None) -> None: + """Initialize service.""" + self._targets = targets + super().__init__() + + @property + def targets(self) -> Mapping[str, Any] | None: + """Return a dictionary of registered targets.""" + return self._targets + + def send_message(self, message: str, **kwargs: Any) -> None: + """Send a message.""" + send_message_mock(message, kwargs) + + async def async_get_service( + hass: HomeAssistant, + config: ConfigType, + discovery_info: DiscoveryInfoType | None = None, + ) -> notify.BaseNotificationService: + """Get notify service for mocked platform.""" + return _TestNotifyService(targets) + + # Mock platform with service + mock_notify_platform(hass, tmp_path, async_get_service=async_get_service) + # Setup the platform + items: list[dict[str, Any]] = [{"platform": "test"}] + items.extend(group_setup or []) + await async_setup_component(hass, "notify", {"notify": items}) + await hass.async_block_till_done() + + # Return mock for assertion service calls + return send_message_mock + + +async def test_send_message_with_data(hass: HomeAssistant, tmp_path: Path) -> None: """Test sending a message with to a notify group.""" - service1 = demo.DemoNotificationService(hass) - service2 = demo.DemoNotificationService(hass) - - service1.send_message = MagicMock(autospec=True) - service2.send_message = MagicMock(autospec=True) - - def mock_get_service(hass, config, discovery_info=None): - if config["name"] == "demo1": - return service1 - return service2 - + send_message_mock = await help_setup_notify( + hass, tmp_path, {"service1": 1, "service2": 2} + ) assert await async_setup_component( hass, "group", @@ -33,26 +93,13 @@ async def test_send_message_with_data(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - with patch.object(demo, "get_service", mock_get_service): - await async_setup_component( - hass, - notify.DOMAIN, - { - "notify": [ - {"name": "demo1", "platform": "demo"}, - {"name": "demo2", "platform": "demo"}, - ] - }, - ) - await hass.async_block_till_done() - service = await group.async_get_service( hass, { "services": [ - {"service": "demo1"}, + {"service": "test_service1"}, { - "service": "demo2", + "service": "test_service2", "data": { "target": "unnamed device", "data": {"test": "message", "default": "default"}, @@ -62,26 +109,35 @@ async def test_send_message_with_data(hass: HomeAssistant) -> None: }, ) - """Test sending a message to a notify group.""" + # Test sending a message to a notify group. await service.async_send_message( "Hello", title="Test notification", data={"hello": "world"} ) await hass.async_block_till_done() + send_message_mock.assert_has_calls( + [ + call( + "Hello", + { + "title": "Test notification", + "target": [1], + "data": {"hello": "world"}, + }, + ), + call( + "Hello", + { + "title": "Test notification", + "target": [2], + "data": {"hello": "world", "test": "message", "default": "default"}, + }, + ), + ] + ) + send_message_mock.reset_mock() - assert service1.send_message.mock_calls[0][1][0] == "Hello" - assert service1.send_message.mock_calls[0][2] == { - "title": "Test notification", - "data": {"hello": "world"}, - } - assert service2.send_message.mock_calls[0][1][0] == "Hello" - assert service2.send_message.mock_calls[0][2] == { - "target": ["unnamed device"], - "title": "Test notification", - "data": {"hello": "world", "test": "message", "default": "default"}, - } - - """Test sending a message which overrides service defaults to a notify group.""" + # Test sending a message which overrides service defaults to a notify group await service.async_send_message( "Hello", title="Test notification", @@ -90,22 +146,34 @@ async def test_send_message_with_data(hass: HomeAssistant) -> None: await hass.async_block_till_done() - assert service1.send_message.mock_calls[1][1][0] == "Hello" - assert service1.send_message.mock_calls[1][2] == { - "title": "Test notification", - "data": {"hello": "world", "default": "override"}, - } - assert service2.send_message.mock_calls[1][1][0] == "Hello" - assert service2.send_message.mock_calls[1][2] == { - "target": ["unnamed device"], - "title": "Test notification", - "data": {"hello": "world", "test": "message", "default": "override"}, - } + send_message_mock.assert_has_calls( + [ + call( + "Hello", + { + "title": "Test notification", + "target": [1], + "data": {"hello": "world", "default": "override"}, + }, + ), + call( + "Hello", + { + "title": "Test notification", + "target": [2], + "data": { + "hello": "world", + "test": "message", + "default": "override", + }, + }, + ), + ] + ) -async def test_reload_notify(hass: HomeAssistant) -> None: +async def test_reload_notify(hass: HomeAssistant, tmp_path: Path) -> None: """Verify we can reload the notify service.""" - assert await async_setup_component( hass, "group", @@ -113,25 +181,21 @@ async def test_reload_notify(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert await async_setup_component( + await help_setup_notify( hass, - notify.DOMAIN, - { - notify.DOMAIN: [ - {"name": "demo1", "platform": "demo"}, - {"name": "demo2", "platform": "demo"}, - { - "name": "group_notify", - "platform": "group", - "services": [{"service": "demo1"}], - }, - ] - }, + tmp_path, + {"service1": 1, "service2": 2}, + [ + { + "name": "group_notify", + "platform": "group", + "services": [{"service": "test_service1"}], + } + ], ) - await hass.async_block_till_done() - assert hass.services.has_service(notify.DOMAIN, "demo1") - assert hass.services.has_service(notify.DOMAIN, "demo2") + assert hass.services.has_service(notify.DOMAIN, "test_service1") + assert hass.services.has_service(notify.DOMAIN, "test_service2") assert hass.services.has_service(notify.DOMAIN, "group_notify") yaml_path = get_fixture_path("configuration.yaml", "group") @@ -145,7 +209,7 @@ async def test_reload_notify(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert hass.services.has_service(notify.DOMAIN, "demo1") - assert hass.services.has_service(notify.DOMAIN, "demo2") + assert hass.services.has_service(notify.DOMAIN, "test_service1") + assert hass.services.has_service(notify.DOMAIN, "test_service2") assert not hass.services.has_service(notify.DOMAIN, "group_notify") assert hass.services.has_service(notify.DOMAIN, "new_group_notify") From 5e8b46c670153d11ead7e660101cc1b586981873 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 13 Apr 2024 13:04:39 +0200 Subject: [PATCH 534/967] Make color extractor single config entry (#115016) * Make color extractor single config entry * Make color extractor single config entry * Fix --- homeassistant/components/color_extractor/config_flow.py | 4 ---- homeassistant/components/color_extractor/manifest.json | 3 ++- homeassistant/components/color_extractor/strings.json | 3 --- homeassistant/generated/integrations.json | 3 ++- 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/color_extractor/config_flow.py b/homeassistant/components/color_extractor/config_flow.py index aacb07d8982..aab56eb9537 100644 --- a/homeassistant/components/color_extractor/config_flow.py +++ b/homeassistant/components/color_extractor/config_flow.py @@ -18,10 +18,6 @@ class ColorExtractorConfigFlow(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - if user_input is not None: return self.async_create_entry(title=DEFAULT_NAME, data={}) - return self.async_show_form(step_id="user") diff --git a/homeassistant/components/color_extractor/manifest.json b/homeassistant/components/color_extractor/manifest.json index c87ac2540a6..a86adaac495 100644 --- a/homeassistant/components/color_extractor/manifest.json +++ b/homeassistant/components/color_extractor/manifest.json @@ -4,5 +4,6 @@ "codeowners": ["@GenericStudent"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/color_extractor", - "requirements": ["colorthief==0.2.1"] + "requirements": ["colorthief==0.2.1"], + "single_config_entry": true } diff --git a/homeassistant/components/color_extractor/strings.json b/homeassistant/components/color_extractor/strings.json index f66c448f7c2..e501879e881 100644 --- a/homeassistant/components/color_extractor/strings.json +++ b/homeassistant/components/color_extractor/strings.json @@ -4,9 +4,6 @@ "user": { "description": "[%key:common::config_flow::description::confirm_setup%]" } - }, - "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" } }, "services": { diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 667639226a1..20fbc883207 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -957,7 +957,8 @@ "color_extractor": { "name": "ColorExtractor", "integration_type": "hub", - "config_flow": true + "config_flow": true, + "single_config_entry": true }, "comed": { "name": "Commonwealth Edison (ComEd)", From 36bdda5669a33163131e03fa36387fcea668824c Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sat, 13 Apr 2024 14:27:07 +0200 Subject: [PATCH 535/967] Migrate demo notify platform (#115448) * Migrate demo notify platform * Update homeassistant/components/demo/notify.py Co-authored-by: Paulus Schoutsen * Remove no needed tests * Cleanup redundant attribute assignment --------- Co-authored-by: Paulus Schoutsen --- homeassistant/components/demo/__init__.py | 2 +- homeassistant/components/demo/notify.py | 52 +++--- tests/components/demo/test_notify.py | 197 ++++------------------ 3 files changed, 67 insertions(+), 184 deletions(-) diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index 6fa7e0d973b..738f6af38dd 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -38,6 +38,7 @@ COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM = [ Platform.LIGHT, Platform.LOCK, Platform.MEDIA_PLAYER, + Platform.NOTIFY, Platform.NUMBER, Platform.SELECT, Platform.SENSOR, @@ -55,7 +56,6 @@ COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM = [ COMPONENTS_WITH_DEMO_PLATFORM = [ Platform.TTS, Platform.MAILBOX, - Platform.NOTIFY, Platform.IMAGE_PROCESSING, Platform.DEVICE_TRACKER, ] diff --git a/homeassistant/components/demo/notify.py b/homeassistant/components/demo/notify.py index c6a9483b328..94999d26d10 100644 --- a/homeassistant/components/demo/notify.py +++ b/homeassistant/components/demo/notify.py @@ -1,38 +1,44 @@ -"""Demo notification service.""" +"""Demo notification entity.""" from __future__ import annotations -from typing import Any - -from homeassistant.components.notify import BaseNotificationService +from homeassistant.components.notify import DOMAIN, NotifyEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback EVENT_NOTIFY = "notify" -def get_service( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, - discovery_info: DiscoveryInfoType | None = None, -) -> BaseNotificationService: - """Get the demo notification service.""" - return DemoNotificationService(hass) + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the demo entity platform.""" + async_add_entities([DemoNotifyEntity(unique_id="notify", device_name="Notifier")]) -class DemoNotificationService(BaseNotificationService): - """Implement demo notification service.""" +class DemoNotifyEntity(NotifyEntity): + """Implement demo notification platform.""" - def __init__(self, hass: HomeAssistant) -> None: - """Initialize the service.""" - self.hass = hass + _attr_has_entity_name = True + _attr_name = None - @property - def targets(self) -> dict[str, str]: - """Return a dictionary of registered targets.""" - return {"test target name": "test target id"} + def __init__( + self, + unique_id: str, + device_name: str, + ) -> None: + """Initialize the Demo button entity.""" + self._attr_unique_id = unique_id + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + name=device_name, + ) - def send_message(self, message: str = "", **kwargs: Any) -> None: + async def async_send_message(self, message: str) -> None: """Send a message to a user.""" - kwargs["message"] = message - self.hass.bus.fire(EVENT_NOTIFY, kwargs) + event_notitifcation = {"message": message} + self.hass.bus.async_fire(EVENT_NOTIFY, event_notitifcation) diff --git a/tests/components/demo/test_notify.py b/tests/components/demo/test_notify.py index 0bc7a8bc1d8..b0536873d66 100644 --- a/tests/components/demo/test_notify.py +++ b/tests/components/demo/test_notify.py @@ -1,29 +1,43 @@ """The tests for the notify demo platform.""" -import logging +from collections.abc import Generator from unittest.mock import patch import pytest -import voluptuous as vol from homeassistant.components import notify +from homeassistant.components.demo import DOMAIN import homeassistant.components.demo.notify as demo -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import discovery +from homeassistant.const import Platform +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.setup import async_setup_component -from tests.common import assert_setup_component, async_capture_events - -CONFIG = {notify.DOMAIN: {"platform": "demo"}} - - -@pytest.fixture(autouse=True) -def autouse_disable_platforms(disable_platforms): - """Auto use the disable_platforms fixture.""" +from tests.common import MockConfigEntry, async_capture_events @pytest.fixture -def events(hass): +def notify_only() -> Generator[None, None]: + """Enable only the notify platform.""" + with patch( + "homeassistant.components.demo.COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM", + [Platform.NOTIFY], + ): + yield + + +@pytest.fixture(autouse=True) +async def setup_notify(hass: HomeAssistant, notify_only: None) -> None: + """Initialize setup demo Notify entity.""" + entry = MockConfigEntry(domain=DOMAIN) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + state = hass.states.get("notify.notifier") + assert state is not None + + +@pytest.fixture +def events(hass: HomeAssistant) -> list[Event]: """Fixture that catches notify events.""" return async_capture_events(hass, demo.EVENT_NOTIFY) @@ -46,104 +60,26 @@ def record_calls(calls): return record_calls -@pytest.fixture(name="mock_demo_notify") -def mock_demo_notify_fixture(): - """Mock demo notify service.""" - with patch("homeassistant.components.demo.notify.get_service", autospec=True) as ns: - yield ns - - -async def setup_notify(hass): - """Test setup.""" - with assert_setup_component(1, notify.DOMAIN) as config: - assert await async_setup_component(hass, notify.DOMAIN, CONFIG) - assert config[notify.DOMAIN] - await hass.async_block_till_done() - - -async def test_no_notify_service( - hass: HomeAssistant, mock_demo_notify, caplog: pytest.LogCaptureFixture -) -> None: - """Test missing platform notify service instance.""" - caplog.set_level(logging.ERROR) - mock_demo_notify.return_value = None - await setup_notify(hass) - await hass.async_block_till_done() - assert mock_demo_notify.called - assert "Failed to initialize notification service demo" in caplog.text - - -async def test_discover_notify(hass: HomeAssistant, mock_demo_notify) -> None: - """Test discovery of notify demo platform.""" - assert notify.DOMAIN not in hass.config.components - mock_demo_notify.return_value = None - await discovery.async_load_platform( - hass, "notify", "demo", {"test_key": "test_val"}, {"notify": {}} - ) - await hass.async_block_till_done() - assert notify.DOMAIN in hass.config.components - assert mock_demo_notify.called - assert mock_demo_notify.mock_calls[0][1] == ( - hass, - {}, - {"test_key": "test_val"}, - ) - - -async def test_sending_none_message(hass: HomeAssistant, events) -> None: - """Test send with None as message.""" - await setup_notify(hass) - with pytest.raises(vol.Invalid): - await hass.services.async_call( - notify.DOMAIN, notify.SERVICE_NOTIFY, {notify.ATTR_MESSAGE: None} - ) - await hass.async_block_till_done() - assert len(events) == 0 - - -async def test_sending_templated_message(hass: HomeAssistant, events) -> None: - """Send a templated message.""" - await setup_notify(hass) - hass.states.async_set("sensor.temperature", 10) +async def test_sending_message(hass: HomeAssistant, events: list[Event]) -> None: + """Test sending a message.""" data = { - notify.ATTR_MESSAGE: "{{states.sensor.temperature.state}}", - notify.ATTR_TITLE: "{{ states.sensor.temperature.name }}", + "entity_id": "notify.notifier", + notify.ATTR_MESSAGE: "Test message", } - await hass.services.async_call(notify.DOMAIN, notify.SERVICE_NOTIFY, data) + await hass.services.async_call(notify.DOMAIN, notify.SERVICE_SEND_MESSAGE, data) await hass.async_block_till_done() last_event = events[-1] - assert last_event.data[notify.ATTR_TITLE] == "temperature" - assert last_event.data[notify.ATTR_MESSAGE] == "10" + assert last_event.data[notify.ATTR_MESSAGE] == "Test message" -async def test_method_forwards_correct_data(hass: HomeAssistant, events) -> None: - """Test that all data from the service gets forwarded to service.""" - await setup_notify(hass) - data = { - notify.ATTR_MESSAGE: "my message", - notify.ATTR_TITLE: "my title", - notify.ATTR_DATA: {"hello": "world"}, - } - await hass.services.async_call(notify.DOMAIN, notify.SERVICE_NOTIFY, data) - await hass.async_block_till_done() - assert len(events) == 1 - data = events[0].data - assert { - "message": "my message", - "title": "my title", - "data": {"hello": "world"}, - } == data - - -async def test_calling_notify_from_script_loaded_from_yaml_without_title( - hass: HomeAssistant, events +async def test_calling_notify_from_script_loaded_from_yaml( + hass: HomeAssistant, events: list[Event] ) -> None: """Test if we can call a notify from a script.""" - await setup_notify(hass) step = { - "service": "notify.notify", + "service": "notify.send_message", "data": { - "data": {"push": {"sound": "US-EN-Morgan-Freeman-Roommate-Is-Arriving.wav"}} + "entity_id": "notify.notifier", }, "data_template": {"message": "Test 123 {{ 2 + 2 }}\n"}, } @@ -155,63 +91,4 @@ async def test_calling_notify_from_script_loaded_from_yaml_without_title( assert len(events) == 1 assert { "message": "Test 123 4", - "data": {"push": {"sound": "US-EN-Morgan-Freeman-Roommate-Is-Arriving.wav"}}, } == events[0].data - - -async def test_calling_notify_from_script_loaded_from_yaml_with_title( - hass: HomeAssistant, events -) -> None: - """Test if we can call a notify from a script.""" - await setup_notify(hass) - step = { - "service": "notify.notify", - "data": { - "data": {"push": {"sound": "US-EN-Morgan-Freeman-Roommate-Is-Arriving.wav"}} - }, - "data_template": {"message": "Test 123 {{ 2 + 2 }}\n", "title": "Test"}, - } - await async_setup_component( - hass, "script", {"script": {"test": {"sequence": step}}} - ) - await hass.services.async_call("script", "test") - await hass.async_block_till_done() - assert len(events) == 1 - assert { - "message": "Test 123 4", - "title": "Test", - "data": {"push": {"sound": "US-EN-Morgan-Freeman-Roommate-Is-Arriving.wav"}}, - } == events[0].data - - -async def test_targets_are_services(hass: HomeAssistant) -> None: - """Test that all targets are exposed as individual services.""" - await setup_notify(hass) - assert hass.services.has_service("notify", "demo") is not None - service = "demo_test_target_name" - assert hass.services.has_service("notify", service) is not None - - -async def test_messages_to_targets_route( - hass: HomeAssistant, calls, record_calls -) -> None: - """Test message routing to specific target services.""" - await setup_notify(hass) - hass.bus.async_listen_once("notify", record_calls) - - await hass.services.async_call( - "notify", - "demo_test_target_name", - {"message": "my message", "title": "my title", "data": {"hello": "world"}}, - ) - - await hass.async_block_till_done() - - data = calls[0][0].data - - assert { - "message": "my message", - "target": ["test target id"], - "title": "my title", - "data": {"hello": "world"}, - } == data From 197070486f636ff99f4c9a9a344d314b8493af2a Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sat, 13 Apr 2024 17:10:16 +0200 Subject: [PATCH 536/967] Set up notify group with the notify services in test (#115526) --- tests/components/group/test_notify.py | 42 +++++++++++++++------------ 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/tests/components/group/test_notify.py b/tests/components/group/test_notify.py index 2f9afdf5aa5..dfd200a1542 100644 --- a/tests/components/group/test_notify.py +++ b/tests/components/group/test_notify.py @@ -8,7 +8,6 @@ from unittest.mock import MagicMock, call, patch from homeassistant import config as hass_config from homeassistant.components import notify from homeassistant.components.group import SERVICE_RELOAD -import homeassistant.components.group.notify as group from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.setup import async_setup_component @@ -83,9 +82,6 @@ async def help_setup_notify( async def test_send_message_with_data(hass: HomeAssistant, tmp_path: Path) -> None: """Test sending a message with to a notify group.""" - send_message_mock = await help_setup_notify( - hass, tmp_path, {"service1": 1, "service2": 2} - ) assert await async_setup_component( hass, "group", @@ -93,9 +89,10 @@ async def test_send_message_with_data(hass: HomeAssistant, tmp_path: Path) -> No ) await hass.async_block_till_done() - service = await group.async_get_service( - hass, + group_setup = [ { + "platform": "group", + "name": "My notification group", "services": [ {"service": "test_service1"}, { @@ -105,16 +102,21 @@ async def test_send_message_with_data(hass: HomeAssistant, tmp_path: Path) -> No "data": {"test": "message", "default": "default"}, }, }, - ] - }, + ], + } + ] + send_message_mock = await help_setup_notify( + hass, tmp_path, {"service1": 1, "service2": 2}, group_setup ) + assert hass.services.has_service("notify", "my_notification_group") # Test sending a message to a notify group. - await service.async_send_message( - "Hello", title="Test notification", data={"hello": "world"} + await hass.services.async_call( + "notify", + "my_notification_group", + {"message": "Hello", "title": "Test notification", "data": {"hello": "world"}}, + blocking=True, ) - - await hass.async_block_till_done() send_message_mock.assert_has_calls( [ call( @@ -138,14 +140,16 @@ async def test_send_message_with_data(hass: HomeAssistant, tmp_path: Path) -> No send_message_mock.reset_mock() # Test sending a message which overrides service defaults to a notify group - await service.async_send_message( - "Hello", - title="Test notification", - data={"hello": "world", "default": "override"}, + await hass.services.async_call( + "notify", + "my_notification_group", + { + "message": "Hello", + "title": "Test notification", + "data": {"hello": "world", "default": "override"}, + }, + blocking=True, ) - - await hass.async_block_till_done() - send_message_mock.assert_has_calls( [ call( From 68ba4d57d54d6b315b8552016934c132ac151603 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Sat, 13 Apr 2024 17:24:02 +0200 Subject: [PATCH 537/967] Remove unused CI code (#115300) --- .github/workflows/ci.yaml | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7dd6f798eef..d619fd8c7dc 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1086,19 +1086,13 @@ jobs: uses: actions/download-artifact@v4.1.4 with: pattern: coverage-* - - name: Upload coverage to Codecov (full coverage) + - name: Upload coverage to Codecov if: needs.info.outputs.test_full_suite == 'true' uses: codecov/codecov-action@v4.3.0 with: fail_ci_if_error: true flags: full-suite token: ${{ secrets.CODECOV_TOKEN }} - - name: Upload coverage to Codecov (partial coverage) - if: needs.info.outputs.test_full_suite == 'false' - uses: codecov/codecov-action@v4.3.0 - with: - fail_ci_if_error: true - token: ${{ secrets.CODECOV_TOKEN }} pytest-partial: runs-on: ubuntu-22.04 @@ -1224,14 +1218,7 @@ jobs: uses: actions/download-artifact@v4.1.4 with: pattern: coverage-* - - name: Upload coverage to Codecov (full coverage) - if: needs.info.outputs.test_full_suite == 'true' - uses: codecov/codecov-action@v4.3.0 - with: - fail_ci_if_error: true - flags: full-suite - token: ${{ secrets.CODECOV_TOKEN }} - - name: Upload coverage to Codecov (partial coverage) + - name: Upload coverage to Codecov if: needs.info.outputs.test_full_suite == 'false' uses: codecov/codecov-action@v4.3.0 with: From 64a4d52b3cca5fe4eb2843f27066fb7cd795aee3 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 13 Apr 2024 18:26:33 +0200 Subject: [PATCH 538/967] Update pillow to 10.3.0 (#115524) --- homeassistant/components/doods/manifest.json | 2 +- homeassistant/components/generic/manifest.json | 2 +- homeassistant/components/image_upload/manifest.json | 2 +- homeassistant/components/matrix/manifest.json | 2 +- homeassistant/components/proxy/manifest.json | 2 +- homeassistant/components/qrcode/manifest.json | 2 +- homeassistant/components/seven_segments/manifest.json | 2 +- homeassistant/components/sighthound/manifest.json | 2 +- homeassistant/components/tensorflow/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/doods/manifest.json b/homeassistant/components/doods/manifest.json index 73d7d3754ce..6a198ab34e7 100644 --- a/homeassistant/components/doods/manifest.json +++ b/homeassistant/components/doods/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/doods", "iot_class": "local_polling", "loggers": ["pydoods"], - "requirements": ["pydoods==1.0.2", "Pillow==10.2.0"] + "requirements": ["pydoods==1.0.2", "Pillow==10.3.0"] } diff --git a/homeassistant/components/generic/manifest.json b/homeassistant/components/generic/manifest.json index 861e2cf26c2..65f6aa751ca 100644 --- a/homeassistant/components/generic/manifest.json +++ b/homeassistant/components/generic/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["http"], "documentation": "https://www.home-assistant.io/integrations/generic", "iot_class": "local_push", - "requirements": ["ha-av==10.1.1", "Pillow==10.2.0"] + "requirements": ["ha-av==10.1.1", "Pillow==10.3.0"] } diff --git a/homeassistant/components/image_upload/manifest.json b/homeassistant/components/image_upload/manifest.json index ba9140b4ed8..7cbc484b830 100644 --- a/homeassistant/components/image_upload/manifest.json +++ b/homeassistant/components/image_upload/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/image_upload", "integration_type": "system", "quality_scale": "internal", - "requirements": ["Pillow==10.2.0"] + "requirements": ["Pillow==10.3.0"] } diff --git a/homeassistant/components/matrix/manifest.json b/homeassistant/components/matrix/manifest.json index 0838bcc3764..2ea310aa5a6 100644 --- a/homeassistant/components/matrix/manifest.json +++ b/homeassistant/components/matrix/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/matrix", "iot_class": "cloud_push", "loggers": ["matrix_client"], - "requirements": ["matrix-nio==0.24.0", "Pillow==10.2.0"] + "requirements": ["matrix-nio==0.24.0", "Pillow==10.3.0"] } diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json index 1b05a768b64..42770d71792 100644 --- a/homeassistant/components/proxy/manifest.json +++ b/homeassistant/components/proxy/manifest.json @@ -3,5 +3,5 @@ "name": "Camera Proxy", "codeowners": [], "documentation": "https://www.home-assistant.io/integrations/proxy", - "requirements": ["Pillow==10.2.0"] + "requirements": ["Pillow==10.3.0"] } diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json index e3b202a9950..476f4e8c3c9 100644 --- a/homeassistant/components/qrcode/manifest.json +++ b/homeassistant/components/qrcode/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/qrcode", "iot_class": "calculated", "loggers": ["pyzbar"], - "requirements": ["Pillow==10.2.0", "pyzbar==0.1.7"] + "requirements": ["Pillow==10.3.0", "pyzbar==0.1.7"] } diff --git a/homeassistant/components/seven_segments/manifest.json b/homeassistant/components/seven_segments/manifest.json index 6c511e3f44e..5e05f496d1d 100644 --- a/homeassistant/components/seven_segments/manifest.json +++ b/homeassistant/components/seven_segments/manifest.json @@ -4,5 +4,5 @@ "codeowners": ["@fabaff"], "documentation": "https://www.home-assistant.io/integrations/seven_segments", "iot_class": "local_polling", - "requirements": ["Pillow==10.2.0"] + "requirements": ["Pillow==10.3.0"] } diff --git a/homeassistant/components/sighthound/manifest.json b/homeassistant/components/sighthound/manifest.json index e63864af707..b97ccc5f9cf 100644 --- a/homeassistant/components/sighthound/manifest.json +++ b/homeassistant/components/sighthound/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/sighthound", "iot_class": "cloud_polling", "loggers": ["simplehound"], - "requirements": ["Pillow==10.2.0", "simplehound==0.3"] + "requirements": ["Pillow==10.3.0", "simplehound==0.3"] } diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index b98c4c6e428..40dbadca64d 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -10,6 +10,6 @@ "tf-models-official==2.5.0", "pycocotools==2.0.6", "numpy==1.26.0", - "Pillow==10.2.0" + "Pillow==10.3.0" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 07885c8a067..f3ee84392a7 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -42,7 +42,7 @@ mutagen==1.47.0 orjson==3.9.15 packaging>=23.1 paho-mqtt==1.6.1 -Pillow==10.2.0 +Pillow==10.3.0 pip>=21.3.1 psutil-home-assistant==0.0.1 PyJWT==2.8.0 diff --git a/pyproject.toml b/pyproject.toml index b9111f505c2..7f5154f297f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,7 +51,7 @@ dependencies = [ "PyJWT==2.8.0", # PyJWT has loose dependency. We want the latest one. "cryptography==42.0.5", - "Pillow==10.2.0", + "Pillow==10.3.0", "pyOpenSSL==24.1.0", "orjson==3.9.15", "packaging>=23.1", diff --git a/requirements.txt b/requirements.txt index f2f26f9bb54..3c2a453b762 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,7 +26,7 @@ Jinja2==3.1.3 lru-dict==1.3.0 PyJWT==2.8.0 cryptography==42.0.5 -Pillow==10.2.0 +Pillow==10.3.0 pyOpenSSL==24.1.0 orjson==3.9.15 packaging>=23.1 diff --git a/requirements_all.txt b/requirements_all.txt index 130ff6644c6..4d7edd2301e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -42,7 +42,7 @@ Mastodon.py==1.8.1 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -Pillow==10.2.0 +Pillow==10.3.0 # homeassistant.components.plex PlexAPI==4.15.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d1b55f5c1ff..ddb7061aee9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -36,7 +36,7 @@ HATasmota==0.8.0 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -Pillow==10.2.0 +Pillow==10.3.0 # homeassistant.components.plex PlexAPI==4.15.11 From 008c42e282f90b25a04bfc578d012d3b191a605e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Apr 2024 09:38:37 -1000 Subject: [PATCH 539/967] Bump py-synologydsm-api to 2.4.2 (#115499) Co-authored-by: mib1185 --- .../components/synology_dsm/__init__.py | 12 ++-- .../components/synology_dsm/binary_sensor.py | 4 +- .../components/synology_dsm/button.py | 4 +- .../components/synology_dsm/camera.py | 37 +++++----- .../components/synology_dsm/common.py | 67 +++++++++++++------ .../components/synology_dsm/config_flow.py | 2 +- .../components/synology_dsm/coordinator.py | 40 ++++++++--- .../components/synology_dsm/diagnostics.py | 6 -- .../components/synology_dsm/entity.py | 41 ++++++++---- .../components/synology_dsm/manifest.json | 2 +- .../components/synology_dsm/media_source.py | 10 ++- .../components/synology_dsm/sensor.py | 10 +-- .../components/synology_dsm/switch.py | 7 ++ .../components/synology_dsm/update.py | 11 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../synology_dsm/test_media_source.py | 4 +- 17 files changed, 170 insertions(+), 91 deletions(-) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index ec93c92a698..ec13ec929a5 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -11,10 +11,10 @@ from synology_dsm.api.surveillance_station.camera import SynoCamera from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MAC, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, device_registry as dr -from .common import SynoApi +from .common import SynoApi, raise_config_entry_auth_error from .const import ( DEFAULT_VERIFY_SSL, DOMAIN, @@ -68,11 +68,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await api.async_setup() except SYNOLOGY_AUTH_FAILED_EXCEPTIONS as err: - if err.args[0] and isinstance(err.args[0], dict): - details = err.args[0].get(EXCEPTION_DETAILS, EXCEPTION_UNKNOWN) - else: - details = EXCEPTION_UNKNOWN - raise ConfigEntryAuthFailed(f"reason: {details}") from err + raise_config_entry_auth_error(err) except SYNOLOGY_CONNECTION_EXCEPTIONS as err: if err.args[0] and isinstance(err.args[0], dict): details = err.args[0].get(EXCEPTION_DETAILS, EXCEPTION_UNKNOWN) @@ -147,8 +143,10 @@ async def async_remove_config_entry_device( """Remove synology_dsm config entry from a device.""" data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id] api = data.api + assert api.information is not None serial = api.information.serial storage = api.storage + assert storage is not None all_cameras: list[SynoCamera] = [] if api.surveillance_station is not None: # get_all_cameras does not do I/O diff --git a/homeassistant/components/synology_dsm/binary_sensor.py b/homeassistant/components/synology_dsm/binary_sensor.py index 28dc750bc91..b9c7ff483ea 100644 --- a/homeassistant/components/synology_dsm/binary_sensor.py +++ b/homeassistant/components/synology_dsm/binary_sensor.py @@ -69,6 +69,7 @@ async def async_setup_entry( data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id] api = data.api coordinator = data.coordinator_central + assert api.storage is not None entities: list[SynoDSMSecurityBinarySensor | SynoDSMStorageBinarySensor] = [ SynoDSMSecurityBinarySensor(api, coordinator, description) @@ -121,7 +122,8 @@ class SynoDSMSecurityBinarySensor(SynoDSMBinarySensor): @property def extra_state_attributes(self) -> dict[str, str]: """Return security checks details.""" - return self._api.security.status_by_check # type: ignore[no-any-return] + assert self._api.security is not None + return self._api.security.status_by_check class SynoDSMStorageBinarySensor(SynologyDSMDeviceEntity, SynoDSMBinarySensor): diff --git a/homeassistant/components/synology_dsm/button.py b/homeassistant/components/synology_dsm/button.py index 529682b4c6e..fccd0860036 100644 --- a/homeassistant/components/synology_dsm/button.py +++ b/homeassistant/components/synology_dsm/button.py @@ -73,7 +73,8 @@ class SynologyDSMButton(ButtonEntity): """Initialize the Synology DSM binary_sensor entity.""" self.entity_description = description self.syno_api = api - + assert api.network is not None + assert api.information is not None self._attr_name = f"{api.network.hostname} {description.name}" self._attr_unique_id = f"{api.information.serial}_{description.key}" self._attr_device_info = DeviceInfo( @@ -82,6 +83,7 @@ class SynologyDSMButton(ButtonEntity): async def async_press(self) -> None: """Triggers the Synology DSM button press service.""" + assert self.syno_api.network is not None LOGGER.debug( "Trigger %s for %s", self.entity_description.key, diff --git a/homeassistant/components/synology_dsm/camera.py b/homeassistant/components/synology_dsm/camera.py index 82d15138f05..901fcb1d565 100644 --- a/homeassistant/components/synology_dsm/camera.py +++ b/homeassistant/components/synology_dsm/camera.py @@ -42,6 +42,8 @@ class SynologyDSMCameraEntityDescription( ): """Describes Synology DSM camera entity.""" + camera_id: int + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -65,12 +67,13 @@ class SynoDSMCamera(SynologyDSMBaseEntity[SynologyDSMCameraUpdateCoordinator], C self, api: SynoApi, coordinator: SynologyDSMCameraUpdateCoordinator, - camera_id: str, + camera_id: int, ) -> None: """Initialize a Synology camera.""" description = SynologyDSMCameraEntityDescription( api_key=SynoSurveillanceStation.CAMERA_API_KEY, - key=camera_id, + key=str(camera_id), + camera_id=camera_id, name=coordinator.data["cameras"][camera_id].name, entity_registry_enabled_default=coordinator.data["cameras"][ camera_id @@ -85,23 +88,20 @@ class SynoDSMCamera(SynologyDSMBaseEntity[SynologyDSMCameraUpdateCoordinator], C @property def camera_data(self) -> SynoCamera: """Camera data.""" - return self.coordinator.data["cameras"][self.entity_description.key] + return self.coordinator.data["cameras"][self.entity_description.camera_id] @property def device_info(self) -> DeviceInfo: """Return the device information.""" + information = self._api.information + assert information is not None return DeviceInfo( - identifiers={ - ( - DOMAIN, - f"{self._api.information.serial}_{self.camera_data.id}", - ) - }, + identifiers={(DOMAIN, f"{information.serial}_{self.camera_data.id}")}, name=self.camera_data.name, model=self.camera_data.model, via_device=( DOMAIN, - f"{self._api.information.serial}_{SynoSurveillanceStation.INFO_API_KEY}", + f"{information.serial}_{SynoSurveillanceStation.INFO_API_KEY}", ), ) @@ -113,12 +113,12 @@ class SynoDSMCamera(SynologyDSMBaseEntity[SynologyDSMCameraUpdateCoordinator], C @property def is_recording(self) -> bool: """Return true if the device is recording.""" - return self.camera_data.is_recording # type: ignore[no-any-return] + return self.camera_data.is_recording @property def motion_detection_enabled(self) -> bool: """Return the camera motion detection status.""" - return self.camera_data.is_motion_detection_enabled # type: ignore[no-any-return] + return bool(self.camera_data.is_motion_detection_enabled) def _listen_source_updates(self) -> None: """Listen for camera source changed events.""" @@ -153,9 +153,10 @@ class SynoDSMCamera(SynologyDSMBaseEntity[SynologyDSMCameraUpdateCoordinator], C ) if not self.available: return None + assert self._api.surveillance_station is not None try: - return await self._api.surveillance_station.get_camera_image( # type: ignore[no-any-return] - self.entity_description.key, self.snapshot_quality + return await self._api.surveillance_station.get_camera_image( + self.entity_description.camera_id, self.snapshot_quality ) except ( SynologyDSMAPIErrorException, @@ -178,7 +179,7 @@ class SynoDSMCamera(SynologyDSMBaseEntity[SynologyDSMCameraUpdateCoordinator], C if not self.available: return None - return self.camera_data.live_view.rtsp # type: ignore[no-any-return] + return self.camera_data.live_view.rtsp async def async_enable_motion_detection(self) -> None: """Enable motion detection in the camera.""" @@ -186,8 +187,9 @@ class SynoDSMCamera(SynologyDSMBaseEntity[SynologyDSMCameraUpdateCoordinator], C "SynoDSMCamera.enable_motion_detection(%s)", self.camera_data.name, ) + assert self._api.surveillance_station is not None await self._api.surveillance_station.enable_motion_detection( - self.entity_description.key + self.entity_description.camera_id ) async def async_disable_motion_detection(self) -> None: @@ -196,6 +198,7 @@ class SynoDSMCamera(SynologyDSMBaseEntity[SynologyDSMCameraUpdateCoordinator], C "SynoDSMCamera.disable_motion_detection(%s)", self.camera_data.name, ) + assert self._api.surveillance_station is not None await self._api.surveillance_station.disable_motion_detection( - self.entity_description.key + self.entity_description.camera_id ) diff --git a/homeassistant/components/synology_dsm/common.py b/homeassistant/components/synology_dsm/common.py index 42ec45e94a4..04e8ae29ceb 100644 --- a/homeassistant/components/synology_dsm/common.py +++ b/homeassistant/components/synology_dsm/common.py @@ -33,9 +33,15 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import CONF_DEVICE_TOKEN, SYNOLOGY_CONNECTION_EXCEPTIONS +from .const import ( + CONF_DEVICE_TOKEN, + EXCEPTION_DETAILS, + EXCEPTION_UNKNOWN, + SYNOLOGY_CONNECTION_EXCEPTIONS, +) LOGGER = logging.getLogger(__name__) @@ -43,6 +49,8 @@ LOGGER = logging.getLogger(__name__) class SynoApi: """Class to interface with Synology DSM API.""" + dsm: SynologyDSM + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: """Initialize the API wrapper class.""" self._hass = hass @@ -53,16 +61,15 @@ class SynoApi: self.config_url = f"http://{entry.data[CONF_HOST]}:{entry.data[CONF_PORT]}" # DSM APIs - self.dsm: SynologyDSM = None - self.information: SynoDSMInformation = None - self.network: SynoDSMNetwork = None - self.security: SynoCoreSecurity = None - self.storage: SynoStorage = None - self.photos: SynoPhotos = None - self.surveillance_station: SynoSurveillanceStation = None - self.system: SynoCoreSystem = None - self.upgrade: SynoCoreUpgrade = None - self.utilisation: SynoCoreUtilization = None + self.information: SynoDSMInformation | None = None + self.network: SynoDSMNetwork | None = None + self.security: SynoCoreSecurity | None = None + self.storage: SynoStorage | None = None + self.photos: SynoPhotos | None = None + self.surveillance_station: SynoSurveillanceStation | None = None + self.system: SynoCoreSystem | None = None + self.upgrade: SynoCoreUpgrade | None = None + self.utilisation: SynoCoreUtilization | None = None # Should we fetch them self._fetching_entities: dict[str, set[str]] = {} @@ -85,7 +92,7 @@ class SynoApi: self._entry.data[CONF_USERNAME], self._entry.data[CONF_PASSWORD], self._entry.data[CONF_SSL], - timeout=self._entry.options.get(CONF_TIMEOUT), + timeout=self._entry.options.get(CONF_TIMEOUT) or 10, device_token=self._entry.data.get(CONF_DEVICE_TOKEN), ) await self.dsm.login() @@ -159,7 +166,8 @@ class SynoApi: return # surveillance_station is updated by own coordinator - self.dsm.reset(self.surveillance_station) + if self.surveillance_station: + self.dsm.reset(self.surveillance_station) # Determine if we should fetch an API self._with_system = bool(self.dsm.apis.get(SynoCoreSystem.API_KEY)) @@ -182,35 +190,40 @@ class SynoApi: "Disable security api from being updated for '%s'", self._entry.unique_id, ) - self.dsm.reset(self.security) + if self.security: + self.dsm.reset(self.security) self.security = None if not self._with_photos: LOGGER.debug( "Disable photos api from being updated or '%s'", self._entry.unique_id ) - self.dsm.reset(self.photos) + if self.photos: + self.dsm.reset(self.photos) self.photos = None if not self._with_storage: LOGGER.debug( "Disable storage api from being updatedf or '%s'", self._entry.unique_id ) - self.dsm.reset(self.storage) + if self.storage: + self.dsm.reset(self.storage) self.storage = None if not self._with_system: LOGGER.debug( "Disable system api from being updated for '%s'", self._entry.unique_id ) - self.dsm.reset(self.system) + if self.system: + self.dsm.reset(self.system) self.system = None if not self._with_upgrade: LOGGER.debug( "Disable upgrade api from being updated for '%s'", self._entry.unique_id ) - self.dsm.reset(self.upgrade) + if self.upgrade: + self.dsm.reset(self.upgrade) self.upgrade = None if not self._with_utilisation: @@ -218,7 +231,8 @@ class SynoApi: "Disable utilisation api from being updated for '%s'", self._entry.unique_id, ) - self.dsm.reset(self.utilisation) + if self.utilisation: + self.dsm.reset(self.utilisation) self.utilisation = None async def _fetch_device_configuration(self) -> None: @@ -272,11 +286,13 @@ class SynoApi: async def async_reboot(self) -> None: """Reboot NAS.""" - await self._syno_api_executer(self.system.reboot) + if self.system: + await self._syno_api_executer(self.system.reboot) async def async_shutdown(self) -> None: """Shutdown NAS.""" - await self._syno_api_executer(self.system.shutdown) + if self.system: + await self._syno_api_executer(self.system.shutdown) async def async_unload(self) -> None: """Stop interacting with the NAS and prepare for removal from hass.""" @@ -293,3 +309,12 @@ class SynoApi: LOGGER.debug("Start data update for '%s'", self._entry.unique_id) self._setup_api_requests() await self.dsm.update(self._with_information) + + +def raise_config_entry_auth_error(err: Exception) -> None: + """Raise ConfigEntryAuthFailed if error is related to authentication.""" + if err.args[0] and isinstance(err.args[0], dict): + details = err.args[0].get(EXCEPTION_DETAILS, EXCEPTION_UNKNOWN) + else: + details = EXCEPTION_UNKNOWN + raise ConfigEntryAuthFailed(f"reason: {details}") from err diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py index c77b8196faf..785baa50b29 100644 --- a/homeassistant/components/synology_dsm/config_flow.py +++ b/homeassistant/components/synology_dsm/config_flow.py @@ -425,7 +425,7 @@ async def _login_and_fetch_syno_info(api: SynologyDSM, otp_code: str | None) -> ): raise InvalidData - return api.information.serial # type: ignore[no-any-return] + return api.information.serial class InvalidData(HomeAssistantError): diff --git a/homeassistant/components/synology_dsm/coordinator.py b/homeassistant/components/synology_dsm/coordinator.py index bc896b1ad45..34886828a58 100644 --- a/homeassistant/components/synology_dsm/coordinator.py +++ b/homeassistant/components/synology_dsm/coordinator.py @@ -7,7 +7,10 @@ import logging from typing import Any, TypeVar from synology_dsm.api.surveillance_station.camera import SynoCamera -from synology_dsm.exceptions import SynologyDSMAPIErrorException +from synology_dsm.exceptions import ( + SynologyDSMAPIErrorException, + SynologyDSMNotLoggedInException, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_SCAN_INTERVAL @@ -15,10 +18,11 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .common import SynoApi +from .common import SynoApi, raise_config_entry_auth_error from .const import ( DEFAULT_SCAN_INTERVAL, SIGNAL_CAMERA_SOURCE_CHANGED, + SYNOLOGY_AUTH_FAILED_EXCEPTIONS, SYNOLOGY_CONNECTION_EXCEPTIONS, ) @@ -65,13 +69,17 @@ class SynologyDSMSwitchUpdateCoordinator( async def async_setup(self) -> None: """Set up the coordinator initial data.""" info = await self.api.dsm.surveillance_station.get_info() + assert info is not None self.version = info["data"]["CMSMinVersion"] async def _async_update_data(self) -> dict[str, dict[str, Any]]: """Fetch all data from api.""" surveillance_station = self.api.surveillance_station + assert surveillance_station is not None return { - "switches": {"home_mode": await surveillance_station.get_home_mode_status()} + "switches": { + "home_mode": bool(await surveillance_station.get_home_mode_status()) + } } @@ -96,14 +104,23 @@ class SynologyDSMCentralUpdateCoordinator(SynologyDSMUpdateCoordinator[None]): async def _async_update_data(self) -> None: """Fetch all data from api.""" - try: - await self.api.async_update() - except SYNOLOGY_CONNECTION_EXCEPTIONS as err: - raise UpdateFailed(f"Error communicating with API: {err}") from err + for attempts in range(2): + try: + await self.api.async_update() + except SynologyDSMNotLoggedInException: + # If login is expired, try to login again + try: + await self.api.dsm.login() + except SYNOLOGY_AUTH_FAILED_EXCEPTIONS as err: + raise_config_entry_auth_error(err) + if attempts == 0: + continue + except SYNOLOGY_CONNECTION_EXCEPTIONS as err: + raise UpdateFailed(f"Error communicating with API: {err}") from err class SynologyDSMCameraUpdateCoordinator( - SynologyDSMUpdateCoordinator[dict[str, dict[str, SynoCamera]]] + SynologyDSMUpdateCoordinator[dict[str, dict[int, SynoCamera]]] ): """DataUpdateCoordinator to gather data for a synology_dsm cameras.""" @@ -116,10 +133,11 @@ class SynologyDSMCameraUpdateCoordinator( """Initialize DataUpdateCoordinator for cameras.""" super().__init__(hass, entry, api, timedelta(seconds=30)) - async def _async_update_data(self) -> dict[str, dict[str, SynoCamera]]: + async def _async_update_data(self) -> dict[str, dict[int, SynoCamera]]: """Fetch all camera data from api.""" surveillance_station = self.api.surveillance_station - current_data: dict[str, SynoCamera] = { + assert surveillance_station is not None + current_data: dict[int, SynoCamera] = { camera.id: camera for camera in surveillance_station.get_all_cameras() } @@ -128,7 +146,7 @@ class SynologyDSMCameraUpdateCoordinator( except SynologyDSMAPIErrorException as err: raise UpdateFailed(f"Error communicating with API: {err}") from err - new_data: dict[str, SynoCamera] = { + new_data: dict[int, SynoCamera] = { camera.id: camera for camera in surveillance_station.get_all_cameras() } diff --git a/homeassistant/components/synology_dsm/diagnostics.py b/homeassistant/components/synology_dsm/diagnostics.py index d9b4131b078..42a8ab8d60f 100644 --- a/homeassistant/components/synology_dsm/diagnostics.py +++ b/homeassistant/components/synology_dsm/diagnostics.py @@ -4,8 +4,6 @@ from __future__ import annotations from typing import Any -from synology_dsm.api.surveillance_station.camera import SynoCamera - from homeassistant.components.camera import diagnostics as camera_diagnostics from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry @@ -47,7 +45,6 @@ async def async_get_config_entry_diagnostics( } if syno_api.network is not None: - intf: dict for intf in syno_api.network.interfaces: diag_data["network"]["interfaces"][intf["id"]] = { "type": intf["type"], @@ -55,7 +52,6 @@ async def async_get_config_entry_diagnostics( } if syno_api.storage is not None: - disk: dict for disk in syno_api.storage.disks: diag_data["storage"]["disks"][disk["id"]] = { "name": disk["name"], @@ -66,7 +62,6 @@ async def async_get_config_entry_diagnostics( "size_total": disk["size_total"], } - volume: dict for volume in syno_api.storage.volumes: diag_data["storage"]["volumes"][volume["id"]] = { "name": volume["fs_type"], @@ -74,7 +69,6 @@ async def async_get_config_entry_diagnostics( } if syno_api.surveillance_station is not None: - camera: SynoCamera for camera in syno_api.surveillance_station.get_all_cameras(): diag_data["surveillance_station"]["cameras"][camera.id] = { "name": camera.name, diff --git a/homeassistant/components/synology_dsm/entity.py b/homeassistant/components/synology_dsm/entity.py index 4bd1e526194..1a2e07af9e1 100644 --- a/homeassistant/components/synology_dsm/entity.py +++ b/homeassistant/components/synology_dsm/entity.py @@ -45,16 +45,21 @@ class SynologyDSMBaseEntity(CoordinatorEntity[_CoordinatorT]): self.entity_description = description self._api = api + information = api.information + network = api.network + assert information is not None + assert network is not None + self._attr_unique_id: str = ( - f"{api.information.serial}_{description.api_key}:{description.key}" + f"{information.serial}_{description.api_key}:{description.key}" ) self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, self._api.information.serial)}, - name=self._api.network.hostname, + identifiers={(DOMAIN, information.serial)}, + name=network.hostname, manufacturer="Synology", - model=self._api.information.model, - sw_version=self._api.information.version_string, - configuration_url=self._api.config_url, + model=information.model, + sw_version=information.version_string, + configuration_url=api.config_url, ) async def async_added_to_hass(self) -> None: @@ -85,14 +90,22 @@ class SynologyDSMDeviceEntity( self._device_model: str | None = None self._device_firmware: str | None = None self._device_type = None + storage = api.storage + information = api.information + network = api.network + assert information is not None + assert storage is not None + assert network is not None if "volume" in description.key: - volume = self._api.storage.get_volume(self._device_id) + assert self._device_id is not None + volume = storage.get_volume(self._device_id) + assert volume is not None # Volume does not have a name self._device_name = volume["id"].replace("_", " ").capitalize() self._device_manufacturer = "Synology" - self._device_model = self._api.information.model - self._device_firmware = self._api.information.version_string + self._device_model = information.model + self._device_firmware = information.version_string self._device_type = ( volume["device_type"] .replace("_", " ") @@ -100,7 +113,9 @@ class SynologyDSMDeviceEntity( .replace("shr", "SHR") ) elif "disk" in description.key: - disk = self._api.storage.get_disk(self._device_id) + assert self._device_id is not None + disk = storage.get_disk(self._device_id) + assert disk is not None self._device_name = disk["name"] self._device_manufacturer = disk["vendor"] self._device_model = disk["model"].strip() @@ -109,11 +124,11 @@ class SynologyDSMDeviceEntity( self._attr_unique_id += f"_{self._device_id}" self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, f"{self._api.information.serial}_{self._device_id}")}, - name=f"{self._api.network.hostname} ({self._device_name})", + identifiers={(DOMAIN, f"{information.serial}_{self._device_id}")}, + name=f"{network.hostname} ({self._device_name})", manufacturer=self._device_manufacturer, model=self._device_model, sw_version=self._device_firmware, - via_device=(DOMAIN, self._api.information.serial), + via_device=(DOMAIN, information.serial), configuration_url=self._api.config_url, ) diff --git a/homeassistant/components/synology_dsm/manifest.json b/homeassistant/components/synology_dsm/manifest.json index 8060bce5c9b..caecfcbd0c9 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.1.4"], + "requirements": ["py-synologydsm-api==2.4.2"], "ssdp": [ { "manufacturer": "Synology", diff --git a/homeassistant/components/synology_dsm/media_source.py b/homeassistant/components/synology_dsm/media_source.py index 9a393813c3e..4699a1a5c20 100644 --- a/homeassistant/components/synology_dsm/media_source.py +++ b/homeassistant/components/synology_dsm/media_source.py @@ -105,6 +105,7 @@ class SynologyPhotosMediaSource(MediaSource): ] identifier = SynologyPhotosMediaSourceIdentifier(item.identifier) diskstation: SynologyDSMData = self.hass.data[DOMAIN][identifier.unique_id] + assert diskstation.api.photos is not None if identifier.album_id is None: # Get Albums @@ -112,6 +113,7 @@ class SynologyPhotosMediaSource(MediaSource): albums = await diskstation.api.photos.get_albums() except SynologyDSMException: return [] + assert albums is not None ret = [ BrowseMediaSource( @@ -148,6 +150,7 @@ class SynologyPhotosMediaSource(MediaSource): ) except SynologyDSMException: return [] + assert album_items is not None ret = [] for album_item in album_items: @@ -190,6 +193,8 @@ class SynologyPhotosMediaSource(MediaSource): self, item: SynoPhotosItem, diskstation: SynologyDSMData ) -> str | None: """Get thumbnail.""" + assert diskstation.api.photos is not None + try: thumbnail = await diskstation.api.photos.get_item_thumbnail_url(item) except SynologyDSMException: @@ -215,13 +220,14 @@ class SynologyDsmMediaView(http.HomeAssistantView): raise web.HTTPNotFound # location: {cache_key}/{filename} cache_key, file_name = location.split("/") - image_id = cache_key.split("_")[0] + image_id = int(cache_key.split("_")[0]) 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] - item = SynoPhotosItem(image_id, "", "", "", cache_key, "") + assert diskstation.api.photos is not None + item = SynoPhotosItem(image_id, "", "", "", cache_key, "", False) try: image = await diskstation.api.photos.download_item(item) except SynologyDSMException as exc: diff --git a/homeassistant/components/synology_dsm/sensor.py b/homeassistant/components/synology_dsm/sensor.py index 6769c1e4901..b29a33f7253 100644 --- a/homeassistant/components/synology_dsm/sensor.py +++ b/homeassistant/components/synology_dsm/sensor.py @@ -292,6 +292,8 @@ async def async_setup_entry( data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id] api = data.api coordinator = data.coordinator_central + storage = api.storage + assert storage is not None entities: list[SynoDSMUtilSensor | SynoDSMStorageSensor | SynoDSMInfoSensor] = [ SynoDSMUtilSensor(api, coordinator, description) @@ -299,21 +301,21 @@ async def async_setup_entry( ] # Handle all volumes - if api.storage.volumes_ids: + if storage.volumes_ids: entities.extend( [ SynoDSMStorageSensor(api, coordinator, description, volume) - for volume in entry.data.get(CONF_VOLUMES, api.storage.volumes_ids) + for volume in entry.data.get(CONF_VOLUMES, storage.volumes_ids) for description in STORAGE_VOL_SENSORS ] ) # Handle all disks - if api.storage.disks_ids: + if storage.disks_ids: entities.extend( [ SynoDSMStorageSensor(api, coordinator, description, disk) - for disk in entry.data.get(CONF_DISKS, api.storage.disks_ids) + for disk in entry.data.get(CONF_DISKS, storage.disks_ids) for description in STORAGE_DISK_SENSORS ] ) diff --git a/homeassistant/components/synology_dsm/switch.py b/homeassistant/components/synology_dsm/switch.py index c19cdb8c815..facce824bda 100644 --- a/homeassistant/components/synology_dsm/switch.py +++ b/homeassistant/components/synology_dsm/switch.py @@ -79,6 +79,8 @@ class SynoDSMSurveillanceHomeModeToggle( async def async_turn_on(self, **kwargs: Any) -> None: """Turn on Home mode.""" + assert self._api.surveillance_station is not None + assert self._api.information _LOGGER.debug( "SynoDSMSurveillanceHomeModeToggle.turn_on(%s)", self._api.information.serial, @@ -88,6 +90,8 @@ class SynoDSMSurveillanceHomeModeToggle( async def async_turn_off(self, **kwargs: Any) -> None: """Turn off Home mode.""" + assert self._api.surveillance_station is not None + assert self._api.information _LOGGER.debug( "SynoDSMSurveillanceHomeModeToggle.turn_off(%s)", self._api.information.serial, @@ -103,6 +107,9 @@ class SynoDSMSurveillanceHomeModeToggle( @property def device_info(self) -> DeviceInfo: """Return the device information.""" + assert self._api.surveillance_station is not None + assert self._api.information is not None + assert self._api.network is not None return DeviceInfo( identifiers={ ( diff --git a/homeassistant/components/synology_dsm/update.py b/homeassistant/components/synology_dsm/update.py index c7bcff48cea..ed60191f296 100644 --- a/homeassistant/components/synology_dsm/update.py +++ b/homeassistant/components/synology_dsm/update.py @@ -64,24 +64,29 @@ class SynoDSMUpdateEntity( @property def installed_version(self) -> str | None: """Version installed and in use.""" - return self._api.information.version_string # type: ignore[no-any-return] + assert self._api.information is not None + return self._api.information.version_string @property def latest_version(self) -> str | None: """Latest version available for install.""" + assert self._api.upgrade is not None if not self._api.upgrade.update_available: return self.installed_version - return self._api.upgrade.available_version # type: ignore[no-any-return] + return self._api.upgrade.available_version @property def release_url(self) -> str | None: """URL to the full release notes of the latest version available.""" + assert self._api.information is not None + assert self._api.upgrade is not None + if (details := self._api.upgrade.available_version_details) is None: return None url = URL("http://update.synology.com/autoupdate/whatsnew.php") query = {"model": self._api.information.model} - if details.get("nano") > 0: + if details["nano"] > 0: query["update_version"] = f"{details['buildnumber']}-{details['nano']}" else: query["update_version"] = details["buildnumber"] diff --git a/requirements_all.txt b/requirements_all.txt index 4d7edd2301e..54d136c0b83 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1634,7 +1634,7 @@ py-schluter==0.1.7 py-sucks==0.9.9 # homeassistant.components.synology_dsm -py-synologydsm-api==2.1.4 +py-synologydsm-api==2.4.2 # homeassistant.components.zabbix py-zabbix==1.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ddb7061aee9..32bfdcd0968 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1293,7 +1293,7 @@ py-nightscout==1.2.2 py-sucks==0.9.9 # homeassistant.components.synology_dsm -py-synologydsm-api==2.1.4 +py-synologydsm-api==2.4.2 # homeassistant.components.seventeentrack py17track==2021.12.2 diff --git a/tests/components/synology_dsm/test_media_source.py b/tests/components/synology_dsm/test_media_source.py index 24e9a378c02..2a792d174f8 100644 --- a/tests/components/synology_dsm/test_media_source.py +++ b/tests/components/synology_dsm/test_media_source.py @@ -49,7 +49,9 @@ 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")] + return_value=[ + SynoPhotosItem(10, "", "filename.jpg", 12345, "10_1298753", "sm", False) + ] ) dsm.photos.get_item_thumbnail_url = AsyncMock( return_value="http://my.thumbnail.url" From b9d4d0e15de420447b6690f75b2e7761f94e384a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Apr 2024 10:24:46 -1000 Subject: [PATCH 540/967] Avoid removing websocket_api subscription in mobile_app teardown (#115540) --- homeassistant/components/mobile_app/websocket_api.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/components/mobile_app/websocket_api.py b/homeassistant/components/mobile_app/websocket_api.py index 83c3cb29cea..e862e4c8bd5 100644 --- a/homeassistant/components/mobile_app/websocket_api.py +++ b/homeassistant/components/mobile_app/websocket_api.py @@ -112,9 +112,6 @@ async def handle_push_notification_channel( if registered_channels.get(webhook_id) == channel: registered_channels.pop(webhook_id) - # Remove subscription from connection if still exists - connection.subscriptions.pop(msg["id"], None) - channel = registered_channels[webhook_id] = PushChannel( hass, webhook_id, From 82d0f478a5ae5e39d9a3b9a8afe6efcc0ce8f9d3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Apr 2024 10:26:41 -1000 Subject: [PATCH 541/967] Hold the reload lock while attempting config entry setup retry (#115538) --- homeassistant/config_entries.py | 22 ++++++++++++++- tests/test_config_entries.py | 47 +++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 7c1b590b1b0..572b6583d94 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -699,11 +699,20 @@ class ConfigEntry: # has started so we do not block shutdown if not hass.is_stopping: hass.async_create_task( - self.async_setup(hass), + self._async_setup_retry(hass), f"config entry retry {self.domain} {self.title}", eager_start=True, ) + async def _async_setup_retry(self, hass: HomeAssistant) -> None: + """Retry setup. + + We hold the reload lock during setup retry to ensure + that nothing can reload the entry while we are retrying. + """ + async with self.reload_lock: + await self.async_setup(hass) + @callback def async_shutdown(self) -> None: """Call when Home Assistant is stopping.""" @@ -1762,11 +1771,22 @@ class ConfigEntries: async def async_reload(self, entry_id: str) -> bool: """Reload an entry. + When reloading from an integration is is preferable to + call async_schedule_reload instead of this method since + it will cancel setup retry before starting this method + in a task which eliminates a race condition where the + setup retry can fire during the reload. + If an entry was not loaded, will just load. """ if (entry := self.async_get_entry(entry_id)) is None: raise UnknownEntry + # Cancel the setup retry task before waiting for the + # reload lock to reduce the chance of concurrent reload + # attempts. + entry.async_cancel_retry_setup() + async with entry.reload_lock: unload_result = await self.async_unload(entry_id) diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index b817aaddf5d..d911458e719 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -1284,6 +1284,53 @@ async def test_setup_does_not_retry_during_shutdown(hass: HomeAssistant) -> None assert len(mock_setup_entry.mock_calls) == 1 +async def test_reload_during_setup_retrying_waits(hass: HomeAssistant) -> None: + """Test reloading during setup retry waits.""" + entry = MockConfigEntry(domain="test") + entry.add_to_hass(hass) + load_attempts = [] + sleep_duration = 0 + + async def _mock_setup_entry(hass, entry): + """Mock setup entry.""" + nonlocal sleep_duration + await asyncio.sleep(sleep_duration) + load_attempts.append(entry.entry_id) + raise ConfigEntryNotReady + + mock_integration(hass, MockModule("test", async_setup_entry=_mock_setup_entry)) + mock_platform(hass, "test.config_flow", None) + + await hass.async_create_task( + hass.config_entries.async_setup(entry.entry_id), eager_start=True + ) + assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY + + # Now make the setup take a while so that the setup retry + # will still be in progress when the reload request comes in + sleep_duration = 0.1 + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) + await asyncio.sleep(0) + + # Should not raise homeassistant.config_entries.OperationNotAllowed + await hass.config_entries.async_reload(entry.entry_id) + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10)) + await asyncio.sleep(0) + + # Should not raise homeassistant.config_entries.OperationNotAllowed + hass.config_entries.async_schedule_reload(entry.entry_id) + await hass.async_block_till_done() + + assert load_attempts == [ + entry.entry_id, + entry.entry_id, + entry.entry_id, + entry.entry_id, + entry.entry_id, + ] + + async def test_create_entry_options( hass: HomeAssistant, manager: config_entries.ConfigEntries ) -> None: From 1a8857aa2e786f7643a42004bef88a4676fc90a6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Apr 2024 10:29:55 -1000 Subject: [PATCH 542/967] Migrate homekit ffmpeg task to use eager_start (#115543) --- homeassistant/components/homekit/type_cameras.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homekit/type_cameras.py b/homeassistant/components/homekit/type_cameras.py index 84c834f5cc6..d14fef8eabf 100644 --- a/homeassistant/components/homekit/type_cameras.py +++ b/homeassistant/components/homekit/type_cameras.py @@ -28,6 +28,7 @@ from homeassistant.helpers.event import ( async_track_state_change_event, async_track_time_interval, ) +from homeassistant.util.async_ import create_eager_task from .accessories import TYPES, HomeAccessory, HomeDriver from .const import ( @@ -431,7 +432,7 @@ class Camera(HomeAccessory, PyhapCamera): # type: ignore[misc] async def watch_session(_: Any) -> None: await self._async_ffmpeg_watch(session_info["id"]) - session_info[FFMPEG_LOGGER] = asyncio.create_task( + session_info[FFMPEG_LOGGER] = create_eager_task( self._async_log_stderr_stream(stderr_reader) ) session_info[FFMPEG_WATCHER] = async_track_time_interval( From 08e2b655be16c82ea64e77847cdd552a0f0459e5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Apr 2024 10:30:59 -1000 Subject: [PATCH 543/967] Migrate EntityRegistryDisabledHandler to use async_schedule_reload (#115544) async_schedule_reload ensures that any setup retries are cancelled before executing the reload. Its unlikely in this case but its still possible --- homeassistant/config_entries.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 572b6583d94..194f09bcca8 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -2578,10 +2578,11 @@ class EntityRegistryDisabledHandler: self._remove_call_later = async_call_later( self.hass, RELOAD_AFTER_UPDATE_DELAY, - HassJob(self._handle_reload, cancel_on_shutdown=True), + HassJob(self._async_handle_reload, cancel_on_shutdown=True), ) - async def _handle_reload(self, _now: Any) -> None: + @callback + def _async_handle_reload(self, _now: Any) -> None: """Handle a reload.""" self._remove_call_later = None to_reload = self.changed @@ -2594,16 +2595,8 @@ class EntityRegistryDisabledHandler: ), ", ".join(to_reload), ) - - await asyncio.gather( - *( - asyncio.create_task( - self.hass.config_entries.async_reload(entry_id), - name="config entry reload {entry.title} {entry.domain} {entry.entry_id}", - ) - for entry_id in to_reload - ) - ) + for entry_id in to_reload: + self.hass.config_entries.async_schedule_reload(entry_id) @callback From edd75a9d5fb0aa9f7d4dc9d8686bfaf3d43c7e7e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Apr 2024 10:35:07 -1000 Subject: [PATCH 544/967] Fix race in TimestampDataUpdateCoordinator (#115542) * Fix race in TimestampDataUpdateCoordinator The last_update_success_time value was being set after the listeners were fired which could lead to a loop because the listener may re-trigger an update because it thinks the data is stale * coverage * docstring --- homeassistant/helpers/update_coordinator.py | 28 ++++++++-------- tests/helpers/test_update_coordinator.py | 36 +++++++++++++++++++-- 2 files changed, 48 insertions(+), 16 deletions(-) diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index 76472327d97..17a690dfc37 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -401,6 +401,8 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]): if not auth_failed and self._listeners and not self.hass.is_stopping: self._schedule_refresh() + self._async_refresh_finished() + if not self.last_update_success and not previous_update_success: return @@ -411,6 +413,15 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]): ): self.async_update_listeners() + @callback + def _async_refresh_finished(self) -> None: + """Handle when a refresh has finished. + + Called when refresh is finished before listeners are updated. + + To be overridden by subclasses. + """ + @callback def async_set_update_error(self, err: Exception) -> None: """Manually set an error, log the message and notify listeners.""" @@ -444,20 +455,9 @@ class TimestampDataUpdateCoordinator(DataUpdateCoordinator[_DataT]): last_update_success_time: datetime | None = None - async def _async_refresh( - self, - log_failures: bool = True, - raise_on_auth_failed: bool = False, - scheduled: bool = False, - raise_on_entry_error: bool = False, - ) -> None: - """Refresh data.""" - await super()._async_refresh( - log_failures, - raise_on_auth_failed, - scheduled, - raise_on_entry_error, - ) + @callback + def _async_refresh_finished(self) -> None: + """Handle when a refresh has finished.""" if self.last_update_success: self.last_update_success_time = utcnow() diff --git a/tests/helpers/test_update_coordinator.py b/tests/helpers/test_update_coordinator.py index be1bbf0580e..8633bf862a5 100644 --- a/tests/helpers/test_update_coordinator.py +++ b/tests/helpers/test_update_coordinator.py @@ -1,6 +1,6 @@ """Tests for the update coordinator.""" -from datetime import timedelta +from datetime import datetime, timedelta import logging from unittest.mock import AsyncMock, Mock, patch import urllib.error @@ -12,7 +12,7 @@ import requests from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import CoreState, HomeAssistant +from homeassistant.core import CoreState, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import update_coordinator from homeassistant.util.dt import utcnow @@ -715,3 +715,35 @@ async def test_always_callback_when_always_update_is_true( update_callback.reset_mock() remove_callbacks() + + +async def test_timestamp_date_update_coordinator(hass: HomeAssistant) -> None: + """Test last_update_success_time is set before calling listeners.""" + last_update_success_times: list[datetime | None] = [] + + async def refresh() -> int: + return 1 + + crd = update_coordinator.TimestampDataUpdateCoordinator[int]( + hass, + _LOGGER, + name="test", + update_method=refresh, + update_interval=timedelta(seconds=10), + ) + + @callback + def listener(): + last_update_success_times.append(crd.last_update_success_time) + + unsub = crd.async_add_listener(listener) + + await crd.async_refresh() + + assert len(last_update_success_times) == 1 + # Ensure the time is set before the listener is called + assert last_update_success_times != [None] + + unsub() + await crd.async_refresh() + assert len(last_update_success_times) == 1 From f1ac33c2465009ff9757afefa8ae674825d0eb79 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Apr 2024 10:36:33 -1000 Subject: [PATCH 545/967] Fix unmocked remote socket calls in sunweg tests (#115546) Fix unmocked calls in sunweg tests --- tests/components/sunweg/test_config_flow.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/components/sunweg/test_config_flow.py b/tests/components/sunweg/test_config_flow.py index 427e540f21b..80b6a946749 100644 --- a/tests/components/sunweg/test_config_flow.py +++ b/tests/components/sunweg/test_config_flow.py @@ -59,7 +59,7 @@ async def test_server_unavailable(hass: HomeAssistant) -> None: assert result["errors"] == {"base": "timeout_connect"} -async def test_reauth(hass: HomeAssistant) -> None: +async def test_reauth(hass: HomeAssistant, plant_fixture, inverter_fixture) -> None: """Test reauth flow.""" mock_entry = SUNWEG_MOCK_ENTRY mock_entry.add_to_hass(hass) @@ -103,11 +103,18 @@ async def test_reauth(hass: HomeAssistant) -> None: assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "timeout_connect"} - with patch.object(APIHelper, "authenticate", return_value=True): + with ( + patch.object(APIHelper, "authenticate", return_value=True), + patch.object(APIHelper, "listPlants", return_value=[plant_fixture]), + patch.object(APIHelper, "plant", return_value=plant_fixture), + patch.object(APIHelper, "inverter", return_value=inverter_fixture), + patch.object(APIHelper, "complete_inverter"), + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=SUNWEG_USER_INPUT, ) + await hass.async_block_till_done() assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" From f452c5b84ea35c3d7c3fcea4d156d1d8eaf44b0d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Apr 2024 10:48:23 -1000 Subject: [PATCH 546/967] Add forecast subscription failure test case to nws (#115541) --- tests/components/nws/conftest.py | 18 +++++++++++ tests/components/nws/test_weather.py | 46 ++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/tests/components/nws/conftest.py b/tests/components/nws/conftest.py index 7ffde0c5731..ac2c281c57b 100644 --- a/tests/components/nws/conftest.py +++ b/tests/components/nws/conftest.py @@ -1,5 +1,6 @@ """Fixtures for National Weather Service tests.""" +import asyncio from unittest.mock import AsyncMock, patch import pytest @@ -24,6 +25,23 @@ def mock_simple_nws(): yield mock_nws +@pytest.fixture +def mock_simple_nws_times_out(): + """Mock pynws SimpleNWS that times out.""" + with patch("homeassistant.components.nws.SimpleNWS") as mock_nws: + instance = mock_nws.return_value + instance.set_station = AsyncMock(side_effect=asyncio.TimeoutError) + instance.update_observation = AsyncMock(side_effect=asyncio.TimeoutError) + instance.update_forecast = AsyncMock(side_effect=asyncio.TimeoutError) + instance.update_forecast_hourly = AsyncMock(side_effect=asyncio.TimeoutError) + instance.station = "ABC" + instance.stations = ["ABC"] + instance.observation = None + instance.forecast = None + instance.forecast_hourly = None + yield mock_nws + + @pytest.fixture def mock_simple_nws_config(): """Mock pynws SimpleNWS with default values in config_flow.""" diff --git a/tests/components/nws/test_weather.py b/tests/components/nws/test_weather.py index 0fb5654d7ee..ad40b576a8a 100644 --- a/tests/components/nws/test_weather.py +++ b/tests/components/nws/test_weather.py @@ -476,3 +476,49 @@ async def test_forecast_subscription( assert forecast2 != [] assert forecast2 == snapshot + + +@pytest.mark.parametrize( + ("forecast_type", "entity_id"), + [("hourly", "weather.abc")], +) +async def test_forecast_subscription_with_failing_coordinator( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + freezer: FrozenDateTimeFactory, + snapshot: SnapshotAssertion, + mock_simple_nws_times_out, + no_sensor, + forecast_type: str, + entity_id: str, +) -> None: + """Test a forecast subscription when the coordinator is failing to update.""" + client = await hass_ws_client(hass) + + registry = er.async_get(hass) + # Pre-create the hourly entity + registry.async_get_or_create( + WEATHER_DOMAIN, + nws.DOMAIN, + "35_-75_hourly", + suggested_object_id="abc_hourly", + ) + + entry = MockConfigEntry( + domain=nws.DOMAIN, + data=NWS_CONFIG, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + await client.send_json_auto_id( + { + "type": "weather/subscribe_forecast", + "forecast_type": forecast_type, + "entity_id": entity_id, + } + ) + msg = await client.receive_json() + assert not msg["success"] From 0bd40642124ee50e372dbd3ddda1a8e059d9cb66 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Apr 2024 10:54:21 -1000 Subject: [PATCH 547/967] Update ollama config_flow task to use eager_start (#115455) The test now adds a delay because it finished too fast --- homeassistant/components/ollama/config_flow.py | 1 - tests/components/ollama/test_config_flow.py | 6 +++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ollama/config_flow.py b/homeassistant/components/ollama/config_flow.py index 4c59a38bfe0..e192aeb1fca 100644 --- a/homeassistant/components/ollama/config_flow.py +++ b/homeassistant/components/ollama/config_flow.py @@ -151,7 +151,6 @@ class OllamaConfigFlow(ConfigFlow, domain=DOMAIN): self.download_task = self.hass.async_create_background_task( self.client.pull(self.model), f"Downloading {self.model}", - eager_start=False, ) if self.download_task.done(): diff --git a/tests/components/ollama/test_config_flow.py b/tests/components/ollama/test_config_flow.py index c58f14a8c87..b1b74197139 100644 --- a/tests/components/ollama/test_config_flow.py +++ b/tests/components/ollama/test_config_flow.py @@ -205,6 +205,10 @@ async def test_download_error(hass: HomeAssistant) -> None: ollama.DOMAIN, context={"source": config_entries.SOURCE_USER} ) + async def _delayed_runtime_error(*args, **kwargs): + await asyncio.sleep(0) + raise RuntimeError + with ( patch( "homeassistant.components.ollama.config_flow.ollama.AsyncClient.list", @@ -212,7 +216,7 @@ async def test_download_error(hass: HomeAssistant) -> None: ), patch( "homeassistant.components.ollama.config_flow.ollama.AsyncClient.pull", - side_effect=RuntimeError(), + _delayed_runtime_error, ), ): result2 = await hass.config_entries.flow.async_configure( From d9617a8e3aa0a3fec3e62756909a5ddd2bdd3140 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Apr 2024 10:56:14 -1000 Subject: [PATCH 548/967] Enable eager_start for weather platform update (#115496) --- homeassistant/components/weather/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index aa4989de2fe..95655f439c9 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -1163,7 +1163,7 @@ class CoordinatorWeatherEntity( assert coordinator.config_entry is not None getattr(self, f"_handle_{forecast_type}_forecast_coordinator_update")() coordinator.config_entry.async_create_task( - self.hass, self.async_update_listeners((forecast_type,)), eager_start=False + self.hass, self.async_update_listeners((forecast_type,)) ) @callback @@ -1273,5 +1273,5 @@ class SingleCoordinatorWeatherEntity( super()._handle_coordinator_update() assert self.coordinator.config_entry self.coordinator.config_entry.async_create_task( - self.hass, self.async_update_listeners(None), eager_start=False + self.hass, self.async_update_listeners(None) ) From ee535ee6112c6dcc9c295caedf37b2e7551a3ab0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Apr 2024 10:58:52 -1000 Subject: [PATCH 549/967] Ensure test async_create_task eager start behavior matches production (#115517) --- homeassistant/components/cloud/repairs.py | 4 +- .../components/github/config_flow.py | 4 +- .../components/hassio/addon_manager.py | 2 +- .../silabs_multiprotocol_addon.py | 12 +++- .../homematicip_cloud/generic_entity.py | 4 +- .../components/hyperion/config_flow.py | 2 +- .../components/idasen_desk/__init__.py | 2 +- .../components/improv_ble/config_flow.py | 8 ++- homeassistant/components/matrix/__init__.py | 15 +++-- homeassistant/components/mqtt/mixins.py | 6 +- .../components/octoprint/config_flow.py | 6 +- homeassistant/components/rflink/__init__.py | 9 +-- homeassistant/components/snooz/config_flow.py | 2 +- .../components/tplink/config_flow.py | 12 +++- homeassistant/components/tts/__init__.py | 2 +- homeassistant/components/zwave_js/__init__.py | 6 +- homeassistant/config_entries.py | 1 + tests/common.py | 2 +- tests/components/cert_expiry/test_init.py | 12 ++-- tests/components/hyperion/test_config_flow.py | 3 +- tests/components/plex/test_init.py | 1 + tests/components/plex/test_server.py | 2 + tests/components/reolink/test_config_flow.py | 2 +- .../unifiprotect/test_config_flow.py | 1 + tests/components/wallbox/test_config_flow.py | 1 + tests/helpers/test_restore_state.py | 55 ++++++++++--------- tests/helpers/test_script.py | 4 +- tests/test_config_entries.py | 8 --- tests/test_core.py | 16 +++--- tests/test_data_entry_flow.py | 3 +- 30 files changed, 125 insertions(+), 82 deletions(-) diff --git a/homeassistant/components/cloud/repairs.py b/homeassistant/components/cloud/repairs.py index 1c5a8f1f86d..9042a010589 100644 --- a/homeassistant/components/cloud/repairs.py +++ b/homeassistant/components/cloud/repairs.py @@ -94,7 +94,9 @@ class LegacySubscriptionRepairFlow(RepairsFlow): ) if not self.wait_task: - self.wait_task = self.hass.async_create_task(_async_wait_for_plan_change()) + self.wait_task = self.hass.async_create_task( + _async_wait_for_plan_change(), eager_start=False + ) migration = await async_migrate_paypal_agreement(cloud) return self.async_external_step( step_id="change_plan", diff --git a/homeassistant/components/github/config_flow.py b/homeassistant/components/github/config_flow.py index 25d8782618f..1f0fbc71efe 100644 --- a/homeassistant/components/github/config_flow.py +++ b/homeassistant/components/github/config_flow.py @@ -148,7 +148,9 @@ class GitHubConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_abort(reason="could_not_register") if self.login_task is None: - self.login_task = self.hass.async_create_task(_wait_for_login()) + self.login_task = self.hass.async_create_task( + _wait_for_login(), eager_start=False + ) if self.login_task.done(): if self.login_task.exception(): diff --git a/homeassistant/components/hassio/addon_manager.py b/homeassistant/components/hassio/addon_manager.py index fcdcd38f776..674a828c3b8 100644 --- a/homeassistant/components/hassio/addon_manager.py +++ b/homeassistant/components/hassio/addon_manager.py @@ -381,7 +381,7 @@ class AddonManager: self._logger.error(err) break - return self._hass.async_create_task(addon_operation()) + return self._hass.async_create_task(addon_operation(), eager_start=False) class AddonError(HomeAssistantError): diff --git a/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py b/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py index 535d1706737..31032ff6a8c 100644 --- a/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py +++ b/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py @@ -417,6 +417,7 @@ class OptionsFlowHandler(OptionsFlow, ABC): self.install_task = self.hass.async_create_task( multipan_manager.async_install_addon_waiting(), "SiLabs Multiprotocol addon install", + eager_start=False, ) if not self.install_task.done(): @@ -524,7 +525,7 @@ class OptionsFlowHandler(OptionsFlow, ABC): if not self.start_task: self.start_task = self.hass.async_create_task( - multipan_manager.async_start_addon_waiting() + multipan_manager.async_start_addon_waiting(), eager_start=False ) if not self.start_task.done(): @@ -561,7 +562,8 @@ class OptionsFlowHandler(OptionsFlow, ABC): """Prepare info needed to complete the config entry update.""" # Always reload entry after installing the addon. self.hass.async_create_task( - self.hass.config_entries.async_reload(self.config_entry.entry_id) + self.hass.config_entries.async_reload(self.config_entry.entry_id), + eager_start=False, ) # Finish ZHA migration if needed @@ -721,6 +723,7 @@ class OptionsFlowHandler(OptionsFlow, ABC): self.install_task = self.hass.async_create_task( flasher_manager.async_install_addon_waiting(), "SiLabs Flasher addon install", + eager_start=False, ) if not self.install_task.done(): @@ -811,6 +814,7 @@ class OptionsFlowHandler(OptionsFlow, ABC): self.stop_task = self.hass.async_create_task( multipan_manager.async_uninstall_addon_waiting(), "SiLabs Multiprotocol addon uninstall", + eager_start=False, ) if not self.stop_task.done(): @@ -843,7 +847,9 @@ class OptionsFlowHandler(OptionsFlow, ABC): AddonState.NOT_RUNNING ) - self.start_task = self.hass.async_create_task(start_and_wait_until_done()) + self.start_task = self.hass.async_create_task( + start_and_wait_until_done(), eager_start=False + ) if not self.start_task.done(): return self.async_show_progress( diff --git a/homeassistant/components/homematicip_cloud/generic_entity.py b/homeassistant/components/homematicip_cloud/generic_entity.py index c75649e6886..a2e6f8a145f 100644 --- a/homeassistant/components/homematicip_cloud/generic_entity.py +++ b/homeassistant/components/homematicip_cloud/generic_entity.py @@ -176,7 +176,9 @@ class HomematicipGenericEntity(Entity): """Handle hmip device removal.""" # Set marker showing that the HmIP device hase been removed. self.hmip_device_removed = True - self.hass.async_create_task(self.async_remove(force_remove=True)) + self.hass.async_create_task( + self.async_remove(force_remove=True), eager_start=False + ) @property def name(self) -> str: diff --git a/homeassistant/components/hyperion/config_flow.py b/homeassistant/components/hyperion/config_flow.py index d9c808b83a4..64a9831800f 100644 --- a/homeassistant/components/hyperion/config_flow.py +++ b/homeassistant/components/hyperion/config_flow.py @@ -344,7 +344,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): # UI to approve the request for a new token). assert self._auth_id is not None self._request_token_task = self.hass.async_create_task( - self._request_token_task_func(self._auth_id) + self._request_token_task_func(self._auth_id), eager_start=False ) return self.async_external_step( step_id="create_token_external", url=self._get_hyperion_url() diff --git a/homeassistant/components/idasen_desk/__init__.py b/homeassistant/components/idasen_desk/__init__.py index 75ef70fcb50..2edd04b1d59 100644 --- a/homeassistant/components/idasen_desk/__init__.py +++ b/homeassistant/components/idasen_desk/__init__.py @@ -75,7 +75,7 @@ class IdasenDeskCoordinator(DataUpdateCoordinator[int | None]): # pylint: disab if not self.desk.is_connected: _LOGGER.debug("Desk disconnected. Reconnecting") self._connection_lost = True - self.hass.async_create_task(self.async_connect()) + self.hass.async_create_task(self.async_connect(), eager_start=False) elif self._connection_lost: _LOGGER.info("Reconnected to desk") self._connection_lost = False diff --git a/homeassistant/components/improv_ble/config_flow.py b/homeassistant/components/improv_ble/config_flow.py index a1a2d6b1b65..370b244dac2 100644 --- a/homeassistant/components/improv_ble/config_flow.py +++ b/homeassistant/components/improv_ble/config_flow.py @@ -326,7 +326,9 @@ class ImprovBLEConfigFlow(ConfigFlow, domain=DOMAIN): return if not self._provision_task: - self._provision_task = self.hass.async_create_task(_do_provision()) + self._provision_task = self.hass.async_create_task( + _do_provision(), eager_start=False + ) if not self._provision_task.done(): return self.async_show_progress( @@ -372,7 +374,9 @@ class ImprovBLEConfigFlow(ConfigFlow, domain=DOMAIN): except AbortFlow as err: return self.async_abort(reason=err.reason) - self._authorize_task = self.hass.async_create_task(authorized_event.wait()) + self._authorize_task = self.hass.async_create_task( + authorized_event.wait(), eager_start=False + ) if not self._authorize_task.done(): return self.async_show_progress( diff --git a/homeassistant/components/matrix/__init__.py b/homeassistant/components/matrix/__init__.py index b8f1ec08fe0..4c9af45e63f 100644 --- a/homeassistant/components/matrix/__init__.py +++ b/homeassistant/components/matrix/__init__.py @@ -308,7 +308,9 @@ class MatrixBot: async def _resolve_room_aliases(self, listening_rooms: list[RoomAnyID]) -> None: """Resolve any RoomAliases into RoomIDs for the purpose of client interactions.""" resolved_rooms = [ - self.hass.async_create_task(self._resolve_room_alias(room_alias_or_id)) + self.hass.async_create_task( + self._resolve_room_alias(room_alias_or_id), eager_start=False + ) for room_alias_or_id in listening_rooms ] for resolved_room in asyncio.as_completed(resolved_rooms): @@ -330,7 +332,9 @@ class MatrixBot: async def _join_rooms(self) -> None: """Join the Matrix rooms that we listen for commands in.""" rooms = [ - self.hass.async_create_task(self._join_room(room_id, room_alias_or_id)) + self.hass.async_create_task( + self._join_room(room_id, room_alias_or_id), eager_start=False + ) for room_alias_or_id, room_id in self._listening_rooms.items() ] await asyncio.wait(rooms) @@ -438,7 +442,8 @@ class MatrixBot: target_room=target_room, message_type=message_type, content=content, - ) + ), + eager_start=False, ) for target_room in target_rooms ) @@ -514,7 +519,9 @@ class MatrixBot: and len(target_rooms) > 0 ): image_tasks = [ - self.hass.async_create_task(self._send_image(image_path, target_rooms)) + self.hass.async_create_task( + self._send_image(image_path, target_rooms), eager_start=False + ) for image_path in image_paths ] await asyncio.wait(image_tasks) diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index aa0ca3f8585..63df7c71c09 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -1015,7 +1015,8 @@ class MqttDiscoveryUpdate(Entity): self.hass.async_create_task( _async_process_discovery_update_and_remove( payload, self._discovery_data - ) + ), + eager_start=False, ) elif self._discovery_update: if old_payload != self._discovery_data[ATTR_DISCOVERY_PAYLOAD]: @@ -1024,7 +1025,8 @@ class MqttDiscoveryUpdate(Entity): self.hass.async_create_task( _async_process_discovery_update( payload, self._discovery_update, self._discovery_data - ) + ), + eager_start=False, ) else: # Non-empty, unchanged payload: Ignore to avoid changing states diff --git a/homeassistant/components/octoprint/config_flow.py b/homeassistant/components/octoprint/config_flow.py index 0e2fad21871..f99a151292d 100644 --- a/homeassistant/components/octoprint/config_flow.py +++ b/homeassistant/components/octoprint/config_flow.py @@ -105,7 +105,9 @@ class OctoPrintConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_get_api_key(self, user_input=None): """Get an Application Api Key.""" if not self.api_key_task: - self.api_key_task = self.hass.async_create_task(self._async_get_auth_key()) + self.api_key_task = self.hass.async_create_task( + self._async_get_auth_key(), eager_start=False + ) if not self.api_key_task.done(): return self.async_show_progress( step_id="get_api_key", @@ -133,7 +135,7 @@ class OctoPrintConfigFlow(ConfigFlow, domain=DOMAIN): self.hass.config_entries.async_update_entry(existing_entry, data=user_input) # Reload the config entry otherwise devices will remain unavailable self.hass.async_create_task( - self.hass.config_entries.async_reload(existing_entry.entry_id) + self.hass.config_entries.async_reload(existing_entry.entry_id), ) return self.async_abort(reason="reauth_successful") diff --git a/homeassistant/components/rflink/__init__.py b/homeassistant/components/rflink/__init__.py index 74b7d4aa4c0..e5d5e97fa84 100644 --- a/homeassistant/components/rflink/__init__.py +++ b/homeassistant/components/rflink/__init__.py @@ -209,7 +209,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: TMP_ENTITY.format(event_id) ) hass.async_create_task( - hass.data[DATA_DEVICE_REGISTER][event_type](event) + hass.data[DATA_DEVICE_REGISTER][event_type](event), + eager_start=False, ) else: _LOGGER.debug("device_id not known and automatic add disabled") @@ -257,7 +258,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # If HA is not stopping, initiate new connection if hass.state is not CoreState.stopping: _LOGGER.warning("Disconnected from Rflink, reconnecting") - hass.async_create_task(connect()) + hass.async_create_task(connect(), eager_start=False) _reconnect_job = HassJob(reconnect, "Rflink reconnect", cancel_on_shutdown=True) @@ -312,7 +313,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: _LOGGER.info("Connected to Rflink") - hass.async_create_task(connect()) + hass.async_create_task(connect(), eager_start=False) async_dispatcher_connect(hass, SIGNAL_EVENT, event_callback) return True @@ -580,7 +581,7 @@ class RflinkCommand(RflinkDevice): if repetitions > 1: self._repetition_task = self.hass.async_create_task( - self._async_send_command(cmd, repetitions - 1) + self._async_send_command(cmd, repetitions - 1), eager_start=False ) diff --git a/homeassistant/components/snooz/config_flow.py b/homeassistant/components/snooz/config_flow.py index 3962a44d8b9..9992a68ef69 100644 --- a/homeassistant/components/snooz/config_flow.py +++ b/homeassistant/components/snooz/config_flow.py @@ -132,7 +132,7 @@ class SnoozConfigFlow(ConfigFlow, domain=DOMAIN): """Wait for device to enter pairing mode.""" if not self._pairing_task: self._pairing_task = self.hass.async_create_task( - self._async_wait_for_pairing_mode() + self._async_wait_for_pairing_mode(), eager_start=False ) if not self._pairing_task.done(): diff --git a/homeassistant/components/tplink/config_flow.py b/homeassistant/components/tplink/config_flow.py index 919e1a537e5..df3291561fa 100644 --- a/homeassistant/components/tplink/config_flow.py +++ b/homeassistant/components/tplink/config_flow.py @@ -174,7 +174,9 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): else: self._discovered_device = device await set_credentials(self.hass, username, password) - self.hass.async_create_task(self._async_reload_requires_auth_entries()) + self.hass.async_create_task( + self._async_reload_requires_auth_entries(), eager_start=False + ) return self._async_create_entry_from_device(self._discovered_device) self.context["title_placeholders"] = placeholders @@ -267,7 +269,9 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): placeholders["error"] = str(ex) else: await set_credentials(self.hass, username, password) - self.hass.async_create_task(self._async_reload_requires_auth_entries()) + self.hass.async_create_task( + self._async_reload_requires_auth_entries(), eager_start=False + ) return self._async_create_entry_from_device(device) return self.async_show_form( @@ -446,7 +450,9 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): placeholders["error"] = str(ex) else: await set_credentials(self.hass, username, password) - self.hass.async_create_task(self._async_reload_requires_auth_entries()) + self.hass.async_create_task( + self._async_reload_requires_auth_entries(), eager_start=False + ) return self.async_abort(reason="reauth_successful") # Old config entries will not have these values. diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 8ea4617bbf3..3055bf46ca7 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -782,7 +782,7 @@ class SpeechManager: return filename - audio_task = self.hass.async_create_task(get_tts_data()) + audio_task = self.hass.async_create_task(get_tts_data(), eager_start=False) def handle_error(_future: asyncio.Future) -> None: """Handle error.""" diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 52c7804bb8f..090a5ecfdf8 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -307,7 +307,8 @@ class DriverEvents: controller.on( "node added", lambda event: self.hass.async_create_task( - self.controller_events.async_on_node_added(event["node"]) + self.controller_events.async_on_node_added(event["node"]), + eager_start=False, ), ) ) @@ -415,7 +416,8 @@ class ControllerEvents: node.on( "ready", lambda event: self.hass.async_create_task( - self.node_events.async_on_node_ready(event["node"]) + self.node_events.async_on_node_ready(event["node"]), + eager_start=False, ), ) ) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 194f09bcca8..0b4e4a0f170 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -971,6 +971,7 @@ class ConfigEntry: hass.async_create_task( self._async_init_reauth(hass, context, data), f"config entry reauth {self.title} {self.domain} {self.entry_id}", + eager_start=False, ) async def _async_init_reauth( diff --git a/tests/common.py b/tests/common.py index ba106d70e00..b12f0ed37da 100644 --- a/tests/common.py +++ b/tests/common.py @@ -262,7 +262,7 @@ async def async_test_home_assistant( return orig_async_add_executor_job(target, *args) - def async_create_task(coroutine, name=None, eager_start=False): + def async_create_task(coroutine, name=None, eager_start=True): """Create task.""" if isinstance(coroutine, Mock) and not isinstance(coroutine, AsyncMock): fut = asyncio.Future() diff --git a/tests/components/cert_expiry/test_init.py b/tests/components/cert_expiry/test_init.py index 312b87affd3..e2c333cc6f3 100644 --- a/tests/components/cert_expiry/test_init.py +++ b/tests/components/cert_expiry/test_init.py @@ -35,12 +35,6 @@ async def test_setup_with_config(hass: HomeAssistant) -> None: {"platform": DOMAIN, CONF_HOST: HOST, CONF_PORT: 888}, ], } - assert await async_setup_component(hass, SENSOR_DOMAIN, config) is True - await hass.async_block_till_done() - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() - next_update = dt_util.utcnow() + timedelta(seconds=20) - async_fire_time_changed(hass, next_update) with ( patch( @@ -51,7 +45,13 @@ async def test_setup_with_config(hass: HomeAssistant) -> None: return_value=future_timestamp(1), ), ): + assert await async_setup_component(hass, SENSOR_DOMAIN, config) is True await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + next_update = dt_util.utcnow() + timedelta(seconds=20) + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done(wait_background_tasks=True) assert len(hass.config_entries.async_entries(DOMAIN)) == 2 diff --git a/tests/components/hyperion/test_config_flow.py b/tests/components/hyperion/test_config_flow.py index a05316a4bc9..57749f5eedc 100644 --- a/tests/components/hyperion/test_config_flow.py +++ b/tests/components/hyperion/test_config_flow.py @@ -438,7 +438,7 @@ async def test_auth_create_token_approval_declined_task_canceled( mock_task = CanceledAwaitableMock() task_coro: Awaitable | None = None - def create_task(arg: Any) -> CanceledAwaitableMock: + def create_task(arg: Any, **kwargs: Any) -> CanceledAwaitableMock: nonlocal task_coro task_coro = arg return mock_task @@ -458,6 +458,7 @@ async def test_auth_create_token_approval_declined_task_canceled( ) assert result["step_id"] == "create_token" + # Tests should not patch the async_create_task function with patch.object(hass, "async_create_task", side_effect=create_task): result = await _configure_flow(hass, result) assert result["step_id"] == "create_token_external" diff --git a/tests/components/plex/test_init.py b/tests/components/plex/test_init.py index b8dc42d5472..a14c65daa43 100644 --- a/tests/components/plex/test_init.py +++ b/tests/components/plex/test_init.py @@ -360,6 +360,7 @@ async def test_trigger_reauth( ): trigger_plex_update(mock_websocket) await wait_for_debouncer(hass) + await hass.async_block_till_done(wait_background_tasks=True) assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1 assert entry.state is not ConfigEntryState.LOADED diff --git a/tests/components/plex/test_server.py b/tests/components/plex/test_server.py index ec798af6d03..050bf77dd02 100644 --- a/tests/components/plex/test_server.py +++ b/tests/components/plex/test_server.py @@ -68,6 +68,7 @@ async def test_new_ignored_users_available( ) trigger_plex_update(mock_websocket) await wait_for_debouncer(hass) + await hass.async_block_till_done(wait_background_tasks=True) server_id = mock_plex_server.machine_identifier @@ -89,6 +90,7 @@ async def test_new_ignored_users_available( ) await wait_for_debouncer(hass) + await hass.async_block_till_done(wait_background_tasks=True) sensor = hass.states.get("sensor.plex_server_1") assert sensor.state == str(len(active_sessions)) diff --git a/tests/components/reolink/test_config_flow.py b/tests/components/reolink/test_config_flow.py index 92a65fd638b..de1e7a0bc83 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_HOST, TEST_HOST2, TEST_HOST2], ), ( True, diff --git a/tests/components/unifiprotect/test_config_flow.py b/tests/components/unifiprotect/test_config_flow.py index fda5c91afae..845766809b2 100644 --- a/tests/components/unifiprotect/test_config_flow.py +++ b/tests/components/unifiprotect/test_config_flow.py @@ -332,6 +332,7 @@ async def test_form_options(hass: HomeAssistant, ufp_client: ProtectApiClient) - "max_media": 1000, "allow_ea_channel": False, } + await hass.async_block_till_done() await hass.config_entries.async_unload(mock_config.entry_id) diff --git a/tests/components/wallbox/test_config_flow.py b/tests/components/wallbox/test_config_flow.py index 0a243735cb4..c0ff0b19c94 100644 --- a/tests/components/wallbox/test_config_flow.py +++ b/tests/components/wallbox/test_config_flow.py @@ -180,6 +180,7 @@ async def test_form_reauth(hass: HomeAssistant, entry: MockConfigEntry) -> None: assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" + await hass.async_block_till_done() await hass.config_entries.async_unload(entry.entry_id) diff --git a/tests/helpers/test_restore_state.py b/tests/helpers/test_restore_state.py index 4f8c51c5397..729212f4c1d 100644 --- a/tests/helpers/test_restore_state.py +++ b/tests/helpers/test_restore_state.py @@ -58,9 +58,12 @@ async def test_caching_data(hass: HomeAssistant) -> None: # Emulate a fresh load hass.data.pop(DATA_RESTORE_STATE) - with patch( - "homeassistant.helpers.restore_state.Store.async_load", - side_effect=HomeAssistantError, + with ( + patch( + "homeassistant.helpers.restore_state.Store.async_load", + side_effect=HomeAssistantError, + ), + patch("homeassistant.helpers.restore_state.Store.async_save"), ): # Failure to load should not be treated as fatal await async_load(hass) @@ -68,7 +71,13 @@ async def test_caching_data(hass: HomeAssistant) -> None: data = async_get(hass) assert data.last_states == {} - await async_load(hass) + # Mock that only b1 is present this run + with patch( + "homeassistant.helpers.restore_state.Store.async_save" + ) as mock_write_data: + await async_load(hass) + await hass.async_block_till_done() + data = async_get(hass) entity = RestoreEntity() @@ -76,11 +85,7 @@ async def test_caching_data(hass: HomeAssistant) -> None: entity.entity_id = "input_boolean.b1" # Mock that only b1 is present this run - with patch( - "homeassistant.helpers.restore_state.Store.async_save" - ) as mock_write_data: - state = await entity.async_get_last_state() - await hass.async_block_till_done() + state = await entity.async_get_last_state() assert state is not None assert state.entity_id == "input_boolean.b1" @@ -110,17 +115,17 @@ async def test_periodic_write(hass: HomeAssistant) -> None: await data.store.async_save([]) # Emulate a fresh load - hass.data.pop(DATA_RESTORE_STATE) - await async_load(hass) - data = async_get(hass) - - entity = RestoreEntity() - entity.hass = hass - entity.entity_id = "input_boolean.b1" - with patch( "homeassistant.helpers.restore_state.Store.async_save" ) as mock_write_data: + hass.data.pop(DATA_RESTORE_STATE) + await async_load(hass) + data = async_get(hass) + + entity = RestoreEntity() + entity.hass = hass + entity.entity_id = "input_boolean.b1" + await entity.async_get_last_state() await hass.async_block_till_done() @@ -158,17 +163,17 @@ async def test_save_persistent_states(hass: HomeAssistant) -> None: await data.store.async_save([]) # Emulate a fresh load - hass.data.pop(DATA_RESTORE_STATE) - await async_load(hass) - data = async_get(hass) - - entity = RestoreEntity() - entity.hass = hass - entity.entity_id = "input_boolean.b1" - with patch( "homeassistant.helpers.restore_state.Store.async_save" ) as mock_write_data: + hass.data.pop(DATA_RESTORE_STATE) + await async_load(hass) + data = async_get(hass) + + entity = RestoreEntity() + entity.hass = hass + entity.entity_id = "input_boolean.b1" + await entity.async_get_last_state() await hass.async_block_till_done() diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index cdc5a6092c4..9d8170f9953 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -3144,7 +3144,9 @@ async def test_multiple_runs_repeat_choose( events = async_capture_events(hass, "abc") for _ in range(max_runs): - hass.async_create_task(script_obj.async_run(context=Context())) + hass.async_create_task( + script_obj.async_run(context=Context()), eager_start=False + ) await hass.async_block_till_done() assert "WARNING" not in caplog.text diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index d911458e719..d31e968a025 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -2688,10 +2688,6 @@ async def test_unignore_step_form( await manager.async_remove(entry.entry_id) - # Right after removal there shouldn't be an entry or active flows - assert len(hass.config_entries.async_entries("comp")) == 0 - assert len(hass.config_entries.flow.async_progress_by_handler("comp")) == 0 - # But after a 'tick' the unignore step has run and we can see an active flow again. await hass.async_block_till_done() assert len(hass.config_entries.flow.async_progress_by_handler("comp")) == 1 @@ -2735,10 +2731,6 @@ async def test_unignore_create_entry( await manager.async_remove(entry.entry_id) - # Right after removal there shouldn't be an entry or flow - assert len(hass.config_entries.flow.async_progress_by_handler("comp")) == 0 - assert len(hass.config_entries.async_entries("comp")) == 0 - # But after a 'tick' the unignore step has run and we can see a config entry. await hass.async_block_till_done() entry = hass.config_entries.async_entries("comp")[0] diff --git a/tests/test_core.py b/tests/test_core.py index c78a455f089..9d02fc46e24 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -794,7 +794,7 @@ async def test_async_create_task_pending_tasks_coro(hass: HomeAssistant) -> None call_count.append("call") for _ in range(2): - hass.async_create_task(test_coro()) + hass.async_create_task(test_coro(), eager_start=False) assert len(hass._tasks) == 2 await hass.async_block_till_done() @@ -2376,11 +2376,11 @@ async def test_log_blocking_events( async def _wait_a_bit_2(): await asyncio.sleep(0.1) - hass.async_create_task(_wait_a_bit_1()) + hass.async_create_task(_wait_a_bit_1(), eager_start=False) await hass.async_block_till_done() with patch.object(ha, "BLOCK_LOG_TIMEOUT", 0.0001): - hass.async_create_task(_wait_a_bit_2()) + hass.async_create_task(_wait_a_bit_2(), eager_start=False) await hass.async_block_till_done() assert "_wait_a_bit_2" in caplog.text @@ -2400,14 +2400,14 @@ async def test_chained_logging_hits_log_timeout( created += 1 if created > 1000: return - hass.async_create_task(_task_chain_2()) + hass.async_create_task(_task_chain_2(), eager_start=False) async def _task_chain_2(): nonlocal created created += 1 if created > 1000: return - hass.async_create_task(_task_chain_1()) + hass.async_create_task(_task_chain_1(), eager_start=False) with patch.object(ha, "BLOCK_LOG_TIMEOUT", 0.0): hass.async_create_task(_task_chain_1()) @@ -2429,16 +2429,16 @@ async def test_chained_logging_misses_log_timeout( created += 1 if created > 10: return - hass.async_create_task(_task_chain_2()) + hass.async_create_task(_task_chain_2(), eager_start=False) async def _task_chain_2(): nonlocal created created += 1 if created > 10: return - hass.async_create_task(_task_chain_1()) + hass.async_create_task(_task_chain_1(), eager_start=False) - hass.async_create_task(_task_chain_1()) + hass.async_create_task(_task_chain_1(), eager_start=False) await hass.async_block_till_done() assert "_task_chain_" not in caplog.text diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index ab82ef65269..312e2be7602 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -460,6 +460,7 @@ async def test_show_progress_error(hass: HomeAssistant, manager) -> None: async def async_step_init(self, user_input=None): async def long_running_task() -> None: + await asyncio.sleep(0) raise TypeError if not self.progress_task: @@ -518,7 +519,7 @@ async def test_show_progress_hidden_from_frontend(hass: HomeAssistant, manager) nonlocal progress_task async def long_running_job() -> None: - return + await asyncio.sleep(0) if not progress_task: progress_task = hass.async_create_task(long_running_job()) From dee99c764b23f8123c2788af3f2a9502d9e0722b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Apr 2024 16:29:14 -0500 Subject: [PATCH 550/967] Complete ESPHome media_player coverage (#114352) --- tests/components/esphome/test_media_player.py | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/tests/components/esphome/test_media_player.py b/tests/components/esphome/test_media_player.py index ffbe8f50e48..8a3630b92a4 100644 --- a/tests/components/esphome/test_media_player.py +++ b/tests/components/esphome/test_media_player.py @@ -1,6 +1,6 @@ """Test ESPHome media_players.""" -from unittest.mock import AsyncMock, Mock, call +from unittest.mock import AsyncMock, Mock, call, patch from aioesphomeapi import ( APIClient, @@ -152,6 +152,8 @@ async def test_media_player_entity_with_source( mock_generic_device_entry, ) -> None: """Test a generic media_player entity media source.""" + await async_setup_component(hass, "media_source", {"media_source": {}}) + await hass.async_block_till_done() esphome_platform_mock = Mock( async_get_media_browser_root_object=AsyncMock( return_value=[ @@ -221,6 +223,33 @@ async def test_media_player_entity_with_source( ) mock_client.media_player_command.reset_mock() + + play_media = media_source.PlayMedia( + url="http://www.example.com/xy.mp3", + mime_type="audio/mp3", + ) + + await hass.async_block_till_done() + + with patch( + "homeassistant.components.media_source.async_resolve_media", + return_value=play_media, + ): + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: "media_player.test_mymedia_player", + ATTR_MEDIA_CONTENT_TYPE: "audio/mp3", + ATTR_MEDIA_CONTENT_ID: "media-source://local/xy", + }, + blocking=True, + ) + + mock_client.media_player_command.assert_has_calls( + [call(1, media_url="http://www.example.com/xy.mp3")] + ) + client = await hass_ws_client() await client.send_json( { From 0b3627b59e5a1dc7215531ba54f98b999a4a807e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Apr 2024 16:50:11 -0500 Subject: [PATCH 551/967] Add additional cached_property to camera entities (#115075) --- homeassistant/components/camera/__init__.py | 5 ++-- tests/components/camera/test_init.py | 27 ++++++++++++++++++++- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 2430ccebb4f..861b184975b 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -511,14 +511,14 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): self._create_stream_lock: asyncio.Lock | None = None self._rtsp_to_webrtc = False - @property + @cached_property def entity_picture(self) -> str: """Return a link to the camera feed as entity picture.""" if self._attr_entity_picture is not None: return self._attr_entity_picture return ENTITY_IMAGE_URL.format(self.entity_id, self.access_tokens[-1]) - @property + @cached_property def use_stream_for_stills(self) -> bool: """Whether or not to use stream to generate stills.""" return False @@ -745,6 +745,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def async_update_token(self) -> None: """Update the used token.""" self.access_tokens.append(hex(_RND.getrandbits(256))[2:]) + self.__dict__.pop("entity_picture", None) async def async_internal_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index ccec2b6f50c..dffc7e5aa53 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -24,10 +24,15 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util from .common import EMPTY_8_6_JPEG, WEBRTC_ANSWER, mock_turbo_jpeg -from tests.common import help_test_all, import_and_test_deprecated_constant_enum +from tests.common import ( + async_fire_time_changed, + help_test_all, + import_and_test_deprecated_constant_enum, +) from tests.typing import ClientSessionGenerator, WebSocketGenerator STREAM_SOURCE = "rtsp://127.0.0.1/stream" @@ -1073,3 +1078,23 @@ def test_deprecated_supported_features_ints(caplog: pytest.LogCaptureFixture) -> caplog.clear() assert entity.supported_features_compat is camera.CameraEntityFeature(1) 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: + """Test the token is rotated and entity entity picture cache is cleared.""" + await async_setup_component(hass, "camera", {}) + await hass.async_block_till_done() + + camera_state = hass.states.get("camera.demo_camera") + original_picture = camera_state.attributes["entity_picture"] + assert "token=" in original_picture + + async_fire_time_changed(hass, dt_util.utcnow() + camera.TOKEN_CHANGE_INTERVAL) + await hass.async_block_till_done(wait_background_tasks=True) + + camera_state = hass.states.get("camera.demo_camera") + new_entity_picture = camera_state.attributes["entity_picture"] + assert new_entity_picture != original_picture + assert "token=" in new_entity_picture From 49553649482b03e57da36866507698519f28324d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Apr 2024 17:13:19 -0500 Subject: [PATCH 552/967] Fix advantage_air disabled entity tests (#115548) * Fix advantage_air disabled entity test * fix both --- tests/components/advantage_air/test_binary_sensor.py | 6 +++--- tests/components/advantage_air/test_sensor.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/components/advantage_air/test_binary_sensor.py b/tests/components/advantage_air/test_binary_sensor.py index 4395fb82542..2eb95c18b7d 100644 --- a/tests/components/advantage_air/test_binary_sensor.py +++ b/tests/components/advantage_air/test_binary_sensor.py @@ -76,8 +76,8 @@ async def test_binary_sensor_async_setup_entry( hass, dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), ) - await hass.async_block_till_done() - assert len(mock_get.mock_calls) == 2 + await hass.async_block_till_done(wait_background_tasks=True) + assert len(mock_get.mock_calls) == 1 state = hass.states.get(entity_id) assert state @@ -100,7 +100,7 @@ async def test_binary_sensor_async_setup_entry( hass, dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), ) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert len(mock_get.mock_calls) == 2 state = hass.states.get(entity_id) diff --git a/tests/components/advantage_air/test_sensor.py b/tests/components/advantage_air/test_sensor.py index 967afe20ddb..ced1ff3a9e7 100644 --- a/tests/components/advantage_air/test_sensor.py +++ b/tests/components/advantage_air/test_sensor.py @@ -125,14 +125,14 @@ async def test_sensor_platform_disabled_entity( mock_get.reset_mock() entity_registry.async_update_entity(entity_id=entity_id, disabled_by=None) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) async_fire_time_changed( hass, dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), ) - await hass.async_block_till_done() - assert len(mock_get.mock_calls) == 2 + await hass.async_block_till_done(wait_background_tasks=True) + assert len(mock_get.mock_calls) == 1 state = hass.states.get(entity_id) assert state From 927ea14562b4c1f925f89f7cb339926b85ae9b38 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Sun, 14 Apr 2024 00:26:37 +0200 Subject: [PATCH 553/967] Add exception translations to Bring integration (#115547) * Add exception translations * Add test for exceptions * Remove unnecessary logging --- homeassistant/components/bring/__init__.py | 24 +++++++++----------- homeassistant/components/bring/strings.json | 23 +++++++++++++++++++ homeassistant/components/bring/todo.py | 24 ++++++++++++++++---- tests/components/bring/test_init.py | 25 +++++++++++++++++++++ 4 files changed, 78 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/bring/__init__.py b/homeassistant/components/bring/__init__.py index 7c300a0e013..e408001e458 100644 --- a/homeassistant/components/bring/__init__.py +++ b/homeassistant/components/bring/__init__.py @@ -39,23 +39,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await bring.load_lists() except BringRequestException as e: raise ConfigEntryNotReady( - f"Timeout while connecting for email '{email}'" - ) from e - except BringAuthException as e: - _LOGGER.error( - "Authentication failed for '%s', check your email and password", - email, - ) - raise ConfigEntryError( - f"Authentication failed for '{email}', check your email and password" + translation_domain=DOMAIN, + translation_key="setup_request_exception", ) from e except BringParseException as e: - _LOGGER.error( - "Failed to parse request '%s', check your email and password", - email, - ) raise ConfigEntryNotReady( - "Failed to parse response request from server, try again later" + translation_domain=DOMAIN, + translation_key="setup_request_exception", + ) from e + except BringAuthException as e: + raise ConfigEntryError( + translation_domain=DOMAIN, + translation_key="setup_authentication_exception", + translation_placeholders={CONF_EMAIL: email}, ) from e coordinator = BringDataUpdateCoordinator(hass, bring) diff --git a/homeassistant/components/bring/strings.json b/homeassistant/components/bring/strings.json index de3677bf5f1..6d61034bea8 100644 --- a/homeassistant/components/bring/strings.json +++ b/homeassistant/components/bring/strings.json @@ -16,5 +16,28 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" } + }, + "exceptions": { + "todo_save_item_failed": { + "message": "Failed to save item {name} to Bring! list" + }, + "todo_update_item_failed": { + "message": "Failed to update item {name} to Bring! list" + }, + "todo_rename_item_failed": { + "message": "Failed to rename item {name} to Bring! list" + }, + "todo_delete_item_failed": { + "message": "Failed to delete {count} item(s) from Bring! list" + }, + "setup_request_exception": { + "message": "Failed to connect to server, try again later" + }, + "setup_parse_exception": { + "message": "Failed to parse server response, try again later" + }, + "setup_authentication_exception": { + "message": "Authentication failed for {email}, check your email and password" + } } } diff --git a/homeassistant/components/bring/todo.py b/homeassistant/components/bring/todo.py index a1988e667b5..e631dc32951 100644 --- a/homeassistant/components/bring/todo.py +++ b/homeassistant/components/bring/todo.py @@ -112,7 +112,11 @@ class BringTodoListEntity( str(uuid.uuid4()), ) except BringRequestException as e: - raise HomeAssistantError("Unable to save todo item for bring") from e + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="todo_save_item_failed", + translation_placeholders={"name": item.summary or ""}, + ) from e await self.coordinator.async_refresh() @@ -167,7 +171,11 @@ class BringTodoListEntity( else BringItemOperation.COMPLETE, ) except BringRequestException as e: - raise HomeAssistantError("Unable to update todo item for bring") from e + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="todo_update_item_failed", + translation_placeholders={"name": item.summary or ""}, + ) from e else: try: await self.coordinator.bring.batch_update_list( @@ -191,7 +199,11 @@ class BringTodoListEntity( ) except BringRequestException as e: - raise HomeAssistantError("Unable to replace todo item for bring") from e + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="todo_rename_item_failed", + translation_placeholders={"name": item.summary or ""}, + ) from e await self.coordinator.async_refresh() @@ -212,6 +224,10 @@ class BringTodoListEntity( BringItemOperation.REMOVE, ) except BringRequestException as e: - raise HomeAssistantError("Unable to delete todo item for bring") from e + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="todo_delete_item_failed", + translation_placeholders={"count": str(len(uids))}, + ) from e await self.coordinator.async_refresh() diff --git a/tests/components/bring/test_init.py b/tests/components/bring/test_init.py index 8604648d916..db402bdd6d1 100644 --- a/tests/components/bring/test_init.py +++ b/tests/components/bring/test_init.py @@ -8,10 +8,12 @@ from homeassistant.components.bring import ( BringAuthException, BringParseException, BringRequestException, + async_setup_entry, ) 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 tests.common import MockConfigEntry @@ -62,3 +64,26 @@ async def test_init_failure( mock_bring_client.login.side_effect = exception await setup_integration(hass, bring_config_entry) assert bring_config_entry.state == status + + +@pytest.mark.parametrize( + ("exception", "expected"), + [ + (BringRequestException, ConfigEntryNotReady), + (BringAuthException, ConfigEntryError), + (BringParseException, ConfigEntryNotReady), + ], +) +async def test_init_exceptions( + hass: HomeAssistant, + mock_bring_client: AsyncMock, + exception: Exception, + expected: Exception, + bring_config_entry: MockConfigEntry | None, +) -> None: + """Test an initialization error on integration load.""" + bring_config_entry.add_to_hass(hass) + mock_bring_client.login.side_effect = exception + + with pytest.raises(expected): + await async_setup_entry(hass, bring_config_entry) From 09b209245a8b149859296a6b7d8fd1372395f708 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Apr 2024 17:57:12 -0500 Subject: [PATCH 554/967] Only calculate native_value once in mobile_app (#115550) native_value is read a few times during the state write. Use _attr_native_value so its only calculated once --- homeassistant/components/mobile_app/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mobile_app/sensor.py b/homeassistant/components/mobile_app/sensor.py index 6f049d6f2d5..dd70cf1e22e 100644 --- a/homeassistant/components/mobile_app/sensor.py +++ b/homeassistant/components/mobile_app/sensor.py @@ -103,8 +103,7 @@ class MobileAppSensor(MobileAppEntity, RestoreSensor): self._async_update_attr_from_config() - @property - def native_value(self) -> StateType | date | datetime: + def _calculate_native_value(self) -> StateType | date | datetime: """Return the state of the sensor.""" if (state := self._config[ATTR_SENSOR_STATE]) in (None, STATE_UNKNOWN): return None @@ -131,3 +130,4 @@ class MobileAppSensor(MobileAppEntity, RestoreSensor): config = self._config self._attr_native_unit_of_measurement = config.get(ATTR_SENSOR_UOM) self._attr_state_class = config.get(ATTR_SENSOR_STATE_CLASS) + self._attr_native_value = self._calculate_native_value() From 7e84158fad827d5c62326d2a7a0041d87e38d343 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Apr 2024 18:08:38 -0500 Subject: [PATCH 555/967] Avoid double dict conversion in bluetooth serialize_entity_description (#115551) --- .../components/bluetooth/passive_update_processor.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bluetooth/passive_update_processor.py b/homeassistant/components/bluetooth/passive_update_processor.py index 1d1078633fe..87f7c7a9b20 100644 --- a/homeassistant/components/bluetooth/passive_update_processor.py +++ b/homeassistant/components/bluetooth/passive_update_processor.py @@ -116,11 +116,10 @@ def deserialize_entity_description( def serialize_entity_description(description: EntityDescription) -> dict[str, Any]: """Serialize an entity description.""" - as_dict = dataclasses.asdict(description) return { - field.name: as_dict[field.name] + field.name: value for field in cached_fields(type(description)) - if field.default != as_dict.get(field.name) + if (value := getattr(description, field.name)) != field.default } From 14b794b0f7f06637edcc0106752aebc756c14e2a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Apr 2024 18:38:33 -0500 Subject: [PATCH 556/967] Migrate config entry reauth to use eager_start (#115549) --- homeassistant/config_entries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 0b4e4a0f170..a3f31ff8715 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -971,7 +971,7 @@ class ConfigEntry: hass.async_create_task( self._async_init_reauth(hass, context, data), f"config entry reauth {self.title} {self.domain} {self.entry_id}", - eager_start=False, + eager_start=True, ) async def _async_init_reauth( From dad03e72837660189bceb9ed033ec8abfbb39a4b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Apr 2024 19:34:28 -0500 Subject: [PATCH 557/967] Remove sleep in async_setup_component (#115515) Co-authored-by: TheJulianJES --- homeassistant/setup.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 9979ebeafd5..fcb389e07a5 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -428,10 +428,9 @@ async def _async_setup_component( # noqa: C901 await load_translations_task if integration.platforms_exists(("config_flow",)): - # If the integration has a config_flow, flush out async_setup calling create_task - # with an asyncio.sleep(0) so we can wait for import flows. - # Fragile but covered by test. - await asyncio.sleep(0) + # If the integration has a config_flow, wait for import flows. + # As these are all created with eager tasks, we do not sleep here, + # as the tasks will always be started before we reach this point. await hass.config_entries.flow.async_wait_import_flow_initialized(domain) # Add to components before the entry.async_setup From 8da7de1fea4820003b16db627fb84d3a4e2261c7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Apr 2024 19:46:23 -0500 Subject: [PATCH 558/967] Remove attr usage in event helper (#115554) --- homeassistant/helpers/event.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index f689f15725d..648a118f175 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -13,8 +13,6 @@ from random import randint import time from typing import TYPE_CHECKING, Any, Concatenate, Generic, ParamSpec, TypeVar -import attr - from homeassistant.const import ( EVENT_CORE_CONFIG_UPDATE, EVENT_STATE_CHANGED, @@ -1626,16 +1624,16 @@ def async_track_time_interval( track_time_interval = threaded_listener_factory(async_track_time_interval) -@attr.s +@dataclass(slots=True) class SunListener: """Helper class to help listen to sun events.""" - hass: HomeAssistant = attr.ib() - job: HassJob[[], Coroutine[Any, Any, None] | None] = attr.ib() - event: str = attr.ib() - offset: timedelta | None = attr.ib() - _unsub_sun: CALLBACK_TYPE | None = attr.ib(default=None) - _unsub_config: CALLBACK_TYPE | None = attr.ib(default=None) + hass: HomeAssistant + job: HassJob[[], Coroutine[Any, Any, None] | None] + event: str + offset: timedelta | None + _unsub_sun: CALLBACK_TYPE | None = None + _unsub_config: CALLBACK_TYPE | None = None @callback def async_attach(self) -> None: From 41f5325ce303de293dcb9146851f523cbadf2dbe Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Apr 2024 19:46:37 -0500 Subject: [PATCH 559/967] Refactor _async_setup_component to remove need for C901 (#115553) --- homeassistant/setup.py | 43 +++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index fcb389e07a5..643bb8983b8 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -8,6 +8,7 @@ from collections.abc import Awaitable, Callable, Generator, Mapping import contextlib import contextvars from enum import StrEnum +from functools import partial import logging.handlers import time from types import ModuleType @@ -253,34 +254,39 @@ async def _async_process_dependencies( return failed -async def _async_setup_component( # noqa: C901 +def _log_error_setup_error( + hass: HomeAssistant, + domain: str, + integration: loader.Integration | None, + msg: str, + exc_info: Exception | None = None, +) -> None: + """Log helper.""" + if integration is None: + custom = "" + link = None + else: + custom = "" if integration.is_built_in else "custom integration " + link = integration.documentation + _LOGGER.error("Setup failed for %s'%s': %s", custom, domain, msg, exc_info=exc_info) + async_notify_setup_error(hass, domain, link) + + +async def _async_setup_component( hass: core.HomeAssistant, domain: str, config: ConfigType ) -> bool: """Set up a component for Home Assistant. This method is a coroutine. """ - integration: loader.Integration | None = None - - def log_error(msg: str, exc_info: Exception | None = None) -> None: - """Log helper.""" - if integration is None: - custom = "" - link = None - else: - custom = "" if integration.is_built_in else "custom integration " - link = integration.documentation - _LOGGER.error( - "Setup failed for %s'%s': %s", custom, domain, msg, exc_info=exc_info - ) - async_notify_setup_error(hass, domain, link) - try: integration = await loader.async_get_integration(hass, domain) except loader.IntegrationNotFound: - log_error("Integration not found.") + _log_error_setup_error(hass, domain, None, "Integration not found.") return False + log_error = partial(_log_error_setup_error, hass, domain, integration) + if integration.disabled: log_error(f"Dependency is disabled - {integration.disabled}") return False @@ -451,8 +457,7 @@ async def _async_setup_component( # noqa: C901 ) # Cleanup - if domain in hass.data[DATA_SETUP]: - hass.data[DATA_SETUP].pop(domain) + hass.data[DATA_SETUP].pop(domain, None) hass.bus.async_fire(EVENT_COMPONENT_LOADED, EventComponentLoaded(component=domain)) From 0feea624f98677443ae6df4a589df3f6e3b20902 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Apr 2024 20:37:01 -0500 Subject: [PATCH 560/967] Migrate rfxtrx to use async_track_state_change_event (#115556) --- .../components/rfxtrx/config_flow.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/rfxtrx/config_flow.py b/homeassistant/components/rfxtrx/config_flow.py index c1b52962f32..1fbb2e8fc29 100644 --- a/homeassistant/components/rfxtrx/config_flow.py +++ b/homeassistant/components/rfxtrx/config_flow.py @@ -30,14 +30,14 @@ from homeassistant.const import ( CONF_PORT, CONF_TYPE, ) -from homeassistant.core import State, callback +from homeassistant.core import Event, EventStateChangedData, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import ( config_validation as cv, device_registry as dr, entity_registry as er, ) -from homeassistant.helpers.event import async_track_state_change +from homeassistant.helpers.event import async_track_state_change_event from . import ( DOMAIN, @@ -353,10 +353,10 @@ class RfxtrxOptionsFlow(OptionsFlow): entity_migration_map[new_entity_id] = entry @callback - def _handle_state_removed( - entity_id: str, old_state: State | None, new_state: State | None - ) -> None: + def _handle_state_removed(event: Event[EventStateChangedData]) -> None: # Wait for entities to finish cleanup + new_state = event.data["new_state"] + entity_id = event.data["entity_id"] if new_state is None and entity_id in entities_to_be_removed: entities_to_be_removed.remove(entity_id) if not entities_to_be_removed: @@ -370,7 +370,7 @@ class RfxtrxOptionsFlow(OptionsFlow): if not self.hass.states.async_available(entry.entity_id) } wait_for_entities = asyncio.Event() - remove_track_state_changes = async_track_state_change( + remove_track_state_changes = async_track_state_change_event( self.hass, entities_to_be_removed, _handle_state_removed ) @@ -384,10 +384,10 @@ class RfxtrxOptionsFlow(OptionsFlow): remove_track_state_changes() @callback - def _handle_state_added( - entity_id: str, old_state: State | None, new_state: State | None - ) -> None: + def _handle_state_added(event: Event[EventStateChangedData]) -> None: # Wait for entities to be added + old_state = event.data["old_state"] + entity_id = event.data["entity_id"] if old_state is None and entity_id in entities_to_be_added: entities_to_be_added.remove(entity_id) if not entities_to_be_added: @@ -400,7 +400,7 @@ class RfxtrxOptionsFlow(OptionsFlow): if self.hass.states.async_available(entry.entity_id) } wait_for_entities = asyncio.Event() - remove_track_state_changes = async_track_state_change( + remove_track_state_changes = async_track_state_change_event( self.hass, entities_to_be_added, _handle_state_added ) From b70edb89bf9205601fb6253b320b06e98bc7fd94 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Apr 2024 20:44:07 -0500 Subject: [PATCH 561/967] Fix missing Home in listener deprecation message (#115559) --- homeassistant/core.py | 4 ++-- tests/test_core.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 0cd7c29cf52..d957953b609 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1556,7 +1556,7 @@ class EventBus: frame.report( "calls `async_listen` with run_immediately, which is" - " deprecated and will be removed in Assistant 2025.5", + " deprecated and will be removed in Home Assistant 2025.5", error_if_core=False, ) @@ -1630,7 +1630,7 @@ class EventBus: frame.report( "calls `async_listen_once` with run_immediately, which is " - "deprecated and will be removed in Assistant 2025.5", + "deprecated and will be removed in Home Assistant 2025.5", error_if_core=False, ) diff --git a/tests/test_core.py b/tests/test_core.py index 9d02fc46e24..58738e3e52a 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -3409,5 +3409,5 @@ async def test_async_listen_with_run_immediately_deprecated( func(EVENT_HOMEASSISTANT_START, _test, run_immediately=run_immediately) assert ( f"Detected code that calls `{method}` with run_immediately, which is " - "deprecated and will be removed in Assistant 2025.5." + "deprecated and will be removed in Home Assistant 2025.5." ) in caplog.text From 3799d20d436828323c3b4ff8c06cd6dac4d4f5c1 Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Sun, 14 Apr 2024 07:14:26 +0200 Subject: [PATCH 562/967] Enable Ruff B905 (#114197) --- homeassistant/components/asuswrt/bridge.py | 4 +-- homeassistant/components/calendar/trigger.py | 2 +- .../components/compensation/__init__.py | 2 +- .../components/environment_canada/weather.py | 4 ++- .../components/google_assistant/smart_home.py | 2 +- homeassistant/components/group/util.py | 2 +- homeassistant/components/hassio/data.py | 2 +- homeassistant/components/kraken/utils.py | 4 ++- homeassistant/components/lcn/services.py | 4 ++- .../components/lg_netcast/media_player.py | 2 +- homeassistant/components/lifx/util.py | 2 +- .../components/mqtt/light/schema_basic.py | 2 +- .../components/recorder/migration.py | 2 +- homeassistant/components/recorder/purge.py | 4 +-- homeassistant/components/rfxtrx/__init__.py | 2 +- homeassistant/components/roborock/number.py | 4 ++- homeassistant/components/roborock/switch.py | 4 ++- homeassistant/components/roborock/time.py | 4 ++- homeassistant/components/sensor/recorder.py | 8 ++++-- homeassistant/components/slack/notify.py | 2 +- homeassistant/components/statistics/sensor.py | 5 ++-- .../components/system_health/__init__.py | 1 + homeassistant/components/temper/sensor.py | 2 +- .../components/tensorflow/image_processing.py | 2 +- homeassistant/components/zha/config_flow.py | 2 +- homeassistant/components/zha/core/device.py | 2 +- homeassistant/components/zha/core/endpoint.py | 2 +- homeassistant/components/zha/core/helpers.py | 2 +- homeassistant/components/zha/websocket_api.py | 2 +- homeassistant/components/zwave_js/helpers.py | 2 +- homeassistant/components/zwave_js/services.py | 6 +++-- homeassistant/helpers/intent.py | 4 ++- homeassistant/helpers/service.py | 4 +-- homeassistant/util/yaml/loader.py | 2 +- pyproject.toml | 1 + .../components/bayesian/test_binary_sensor.py | 4 +-- tests/components/derivative/test_sensor.py | 8 +++--- .../dlna_dms/test_dms_device_source.py | 20 +++++++++----- .../google_assistant/test_google_assistant.py | 1 + tests/components/group/test_sensor.py | 16 ++++++------ tests/components/home_connect/test_sensor.py | 12 ++++++--- tests/components/http/test_ban.py | 1 + tests/components/min_max/test_sensor.py | 26 +++++++++---------- tests/components/zha/common.py | 2 +- tests/components/zha/test_discover.py | 2 +- tests/test_config_entries.py | 2 +- 46 files changed, 116 insertions(+), 79 deletions(-) diff --git a/homeassistant/components/asuswrt/bridge.py b/homeassistant/components/asuswrt/bridge.py index f255a3faad4..c177fb1bb20 100644 --- a/homeassistant/components/asuswrt/bridge.py +++ b/homeassistant/components/asuswrt/bridge.py @@ -85,10 +85,10 @@ def handle_errors_and_zip( return data if isinstance(data, dict): - return dict(zip(keys, list(data.values()))) + return dict(zip(keys, list(data.values()), strict=False)) if not isinstance(data, (list, tuple)): raise UpdateFailed("Received invalid data type") - return dict(zip(keys, data)) + return dict(zip(keys, data, strict=False)) return _wrapper diff --git a/homeassistant/components/calendar/trigger.py b/homeassistant/components/calendar/trigger.py index 844232c4b22..ad86ab1957d 100644 --- a/homeassistant/components/calendar/trigger.py +++ b/homeassistant/components/calendar/trigger.py @@ -137,7 +137,7 @@ def queued_event_fetcher( # time span, but need to be triggered later when the end happens. results = [] for trigger_time, event in zip( - map(get_trigger_time, active_events), active_events + map(get_trigger_time, active_events), active_events, strict=False ): if trigger_time not in offset_timespan: continue diff --git a/homeassistant/components/compensation/__init__.py b/homeassistant/components/compensation/__init__.py index dc1f903e8f6..fae416e7fc2 100644 --- a/homeassistant/components/compensation/__init__.py +++ b/homeassistant/components/compensation/__init__.py @@ -90,7 +90,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: sorted_coefficients = sorted(initial_coefficients, key=itemgetter(0)) # get x values and y values from the x,y point pairs - x_values, y_values = zip(*initial_coefficients) + x_values, y_values = zip(*initial_coefficients, strict=False) # try to get valid coefficients for a polynomial coefficients = None diff --git a/homeassistant/components/environment_canada/weather.py b/homeassistant/components/environment_canada/weather.py index 643e7951c23..a3036c55659 100644 --- a/homeassistant/components/environment_canada/weather.py +++ b/homeassistant/components/environment_canada/weather.py @@ -222,7 +222,9 @@ def get_forecast(ec_data, hourly) -> list[Forecast] | None: forecast_array.append(today) - for day, high, low in zip(range(1, 6), range(0, 9, 2), range(1, 10, 2)): + for day, high, low in zip( + range(1, 6), range(0, 9, 2), range(1, 10, 2), strict=False + ): forecast_array.append( { ATTR_FORECAST_TIME: ( diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index bee1c8443fa..a03d7c397cc 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -262,7 +262,7 @@ async def handle_devices_execute( ), EXECUTE_LIMIT, ) - for entity_id, result in zip(executions, execute_results): + for entity_id, result in zip(executions, execute_results, strict=False): if result is not None: results[entity_id] = result except TimeoutError: diff --git a/homeassistant/components/group/util.py b/homeassistant/components/group/util.py index 14f5064290f..1ba8934d021 100644 --- a/homeassistant/components/group/util.py +++ b/homeassistant/components/group/util.py @@ -29,7 +29,7 @@ def mean_int(*args: Any) -> int: def mean_tuple(*args: Any) -> tuple[float | Any, ...]: """Return the mean values along the columns of the supplied values.""" - return tuple(sum(x) / len(x) for x in zip(*args)) + return tuple(sum(x) / len(x) for x in zip(*args, strict=False)) def attribute_equal(states: list[State], key: str) -> bool: diff --git a/homeassistant/components/hassio/data.py b/homeassistant/components/hassio/data.py index 678d0666c05..3d684d6cd7c 100644 --- a/homeassistant/components/hassio/data.py +++ b/homeassistant/components/hassio/data.py @@ -421,7 +421,7 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): # pylint: disable=has updates[DATA_SUPERVISOR_STATS] = hassio.get_supervisor_stats() results = await asyncio.gather(*updates.values()) - for key, result in zip(updates, results): + for key, result in zip(updates, results, strict=False): data[key] = result _addon_data = data[DATA_SUPERVISOR_INFO].get("addons", []) diff --git a/homeassistant/components/kraken/utils.py b/homeassistant/components/kraken/utils.py index 210756a7792..ec89d1b1584 100644 --- a/homeassistant/components/kraken/utils.py +++ b/homeassistant/components/kraken/utils.py @@ -9,7 +9,9 @@ def get_tradable_asset_pairs(kraken_api: KrakenAPI) -> dict[str, str]: """Get a list of tradable asset pairs.""" tradable_asset_pairs = {} asset_pairs_df = kraken_api.get_tradable_asset_pairs() - for pair in zip(asset_pairs_df.index.values, asset_pairs_df["wsname"]): + for pair in zip( + asset_pairs_df.index.values, asset_pairs_df["wsname"], strict=False + ): # Remove darkpools # https://support.kraken.com/hc/en-us/articles/360001391906-Introducing-the-Kraken-Dark-Pool if not pair[0].endswith(".d"): diff --git a/homeassistant/components/lcn/services.py b/homeassistant/components/lcn/services.py index ca07dbe0ef6..49b54fc0c8d 100644 --- a/homeassistant/components/lcn/services.py +++ b/homeassistant/components/lcn/services.py @@ -299,7 +299,9 @@ class SendKeys(LcnServiceCall): keys = [[False] * 8 for i in range(4)] - key_strings = zip(service.data[CONF_KEYS][::2], service.data[CONF_KEYS][1::2]) + key_strings = zip( + service.data[CONF_KEYS][::2], service.data[CONF_KEYS][1::2], strict=False + ) for table, key in key_strings: table_id = ord(table) - 65 diff --git a/homeassistant/components/lg_netcast/media_player.py b/homeassistant/components/lg_netcast/media_player.py index 81927710299..9f6e88dc614 100644 --- a/homeassistant/components/lg_netcast/media_player.py +++ b/homeassistant/components/lg_netcast/media_player.py @@ -131,7 +131,7 @@ class LgTVDevice(MediaPlayerEntity): channel_name = channel.find("chname") if channel_name is not None: channel_names.append(str(channel_name.text)) - self._sources = dict(zip(channel_names, channel_list)) + self._sources = dict(zip(channel_names, channel_list, strict=False)) # sort source names by the major channel number source_tuples = [ (k, source.find("major").text) diff --git a/homeassistant/components/lifx/util.py b/homeassistant/components/lifx/util.py index bb77c7595d3..9782fe4adba 100644 --- a/homeassistant/components/lifx/util.py +++ b/homeassistant/components/lifx/util.py @@ -149,7 +149,7 @@ def merge_hsbk( Hue, Saturation, Brightness, Kelvin """ - return [b if c is None else c for b, c in zip(base, change)] + return [b if c is None else c for b, c in zip(base, change, strict=False)] def _get_mac_offset(mac_addr: str, offset: int) -> str: diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 052fa394248..bf0de319df0 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -712,7 +712,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): keys.append("white") elif color_mode == ColorMode.RGBWW: keys.extend(["cold_white", "warm_white"]) - variables = dict(zip(keys, color)) + variables = dict(zip(keys, color, strict=False)) return self._command_templates[template](rgb_color_str, variables) def set_optimistic( diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 630628b2045..8724846def5 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -492,7 +492,7 @@ def _modify_columns( if engine.dialect.name == SupportedDialect.POSTGRESQL: columns_def = [ "ALTER {column} TYPE {type}".format( - **dict(zip(["column", "type"], col_def.split(" ", 1))) + **dict(zip(["column", "type"], col_def.split(" ", 1), strict=False)) ) for col_def in columns_def ] diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index f42bae00abe..c78f8a4a89d 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -694,7 +694,7 @@ def _purge_filtered_states( ) if not to_purge: return True - state_ids, attributes_ids, event_ids = zip(*to_purge) + state_ids, attributes_ids, event_ids = zip(*to_purge, strict=False) filtered_event_ids = {id_ for id_ in event_ids if id_ is not None} _LOGGER.debug( "Selected %s state_ids to remove that should be filtered", len(state_ids) @@ -735,7 +735,7 @@ def _purge_filtered_events( ) if not to_purge: return True - event_ids, data_ids = zip(*to_purge) + event_ids, data_ids = zip(*to_purge, strict=False) event_ids_set = set(event_ids) _LOGGER.debug( "Selected %s event_ids to remove that should be filtered", len(event_ids_set) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 6f0e5932adc..fb339f4ba5a 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -412,7 +412,7 @@ def find_possible_pt2262_device(device_ids: set[str], device_id: str) -> str | N for dev_id in device_ids: if len(dev_id) == len(device_id): size = None - for i, (char1, char2) in enumerate(zip(dev_id, device_id)): + for i, (char1, char2) in enumerate(zip(dev_id, device_id, strict=False)): if char1 != char2: break size = i diff --git a/homeassistant/components/roborock/number.py b/homeassistant/components/roborock/number.py index 0a7abf5a090..f761d0b2274 100644 --- a/homeassistant/components/roborock/number.py +++ b/homeassistant/components/roborock/number.py @@ -73,7 +73,9 @@ async def async_setup_entry( return_exceptions=True, ) valid_entities: list[RoborockNumberEntity] = [] - for (coordinator, description), result in zip(possible_entities, results): + for (coordinator, description), result in zip( + possible_entities, results, strict=False + ): if result is None or isinstance(result, RoborockException): _LOGGER.debug("Not adding entity because of %s", result) else: diff --git a/homeassistant/components/roborock/switch.py b/homeassistant/components/roborock/switch.py index 090c3219fd3..694bf864809 100644 --- a/homeassistant/components/roborock/switch.py +++ b/homeassistant/components/roborock/switch.py @@ -121,7 +121,9 @@ async def async_setup_entry( return_exceptions=True, ) valid_entities: list[RoborockSwitch] = [] - for (coordinator, description), result in zip(possible_entities, results): + for (coordinator, description), result in zip( + possible_entities, results, strict=False + ): if result is None or isinstance(result, Exception): _LOGGER.debug("Not adding entity because of %s", result) else: diff --git a/homeassistant/components/roborock/time.py b/homeassistant/components/roborock/time.py index c90fc7fa438..7c9c08bce4d 100644 --- a/homeassistant/components/roborock/time.py +++ b/homeassistant/components/roborock/time.py @@ -137,7 +137,9 @@ async def async_setup_entry( return_exceptions=True, ) valid_entities: list[RoborockTimeEntity] = [] - for (coordinator, description), result in zip(possible_entities, results): + for (coordinator, description), result in zip( + possible_entities, results, strict=False + ): if result is None or isinstance(result, RoborockException): _LOGGER.debug("Not adding entity because of %s", result) else: diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 1db811599ad..26bb4f4376b 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -511,9 +511,13 @@ def compile_statistics( # noqa: C901 # Make calculations stat: StatisticData = {"start": start} if "max" in wanted_statistics[entity_id]: - stat["max"] = max(*itertools.islice(zip(*valid_float_states), 1)) + stat["max"] = max( + *itertools.islice(zip(*valid_float_states, strict=False), 1) + ) if "min" in wanted_statistics[entity_id]: - stat["min"] = min(*itertools.islice(zip(*valid_float_states), 1)) + stat["min"] = min( + *itertools.islice(zip(*valid_float_states, strict=False), 1) + ) if "mean" in wanted_statistics[entity_id]: stat["mean"] = _time_weighted_average(valid_float_states, start, end) diff --git a/homeassistant/components/slack/notify.py b/homeassistant/components/slack/notify.py index 06fc76e217a..a18b211962a 100644 --- a/homeassistant/components/slack/notify.py +++ b/homeassistant/components/slack/notify.py @@ -262,7 +262,7 @@ class SlackNotificationService(BaseNotificationService): } results = await asyncio.gather(*tasks.values(), return_exceptions=True) - for target, result in zip(tasks, results): + for target, result in zip(tasks, results, strict=False): if isinstance(result, SlackApiError): _LOGGER.error( "There was a Slack API error while sending to %s: %r", diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index 36513dfd851..713a8d3e894 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -763,7 +763,8 @@ class StatisticsSensor(SensorEntity): def _stat_sum_differences(self) -> StateType: if len(self.states) >= 2: return sum( - abs(j - i) for i, j in zip(list(self.states), list(self.states)[1:]) + abs(j - i) + for i, j in zip(list(self.states), list(self.states)[1:], strict=False) ) return None @@ -771,7 +772,7 @@ class StatisticsSensor(SensorEntity): if len(self.states) >= 2: return sum( (j - i if j >= i else j - 0) - for i, j in zip(list(self.states), list(self.states)[1:]) + for i, j in zip(list(self.states), list(self.states)[1:], strict=False) ) return None diff --git a/homeassistant/components/system_health/__init__.py b/homeassistant/components/system_health/__init__.py index f61745a1407..bb050d5052e 100644 --- a/homeassistant/components/system_health/__init__.py +++ b/homeassistant/components/system_health/__init__.py @@ -127,6 +127,7 @@ async def handle_info( for registration in registrations.values() ) ), + strict=False, ): for key, value in domain_data["info"].items(): if asyncio.iscoroutine(value): diff --git a/homeassistant/components/temper/sensor.py b/homeassistant/components/temper/sensor.py index 06ba656dd0d..7138f40a653 100644 --- a/homeassistant/components/temper/sensor.py +++ b/homeassistant/components/temper/sensor.py @@ -66,7 +66,7 @@ def reset_devices(): This assumes the same sensor devices are present in the same order. """ temper_devices = get_temper_devices() - for sensor, device in zip(TEMPER_SENSORS, temper_devices): + for sensor, device in zip(TEMPER_SENSORS, temper_devices, strict=False): sensor.set_temper_device(device) diff --git a/homeassistant/components/tensorflow/image_processing.py b/homeassistant/components/tensorflow/image_processing.py index 40e30ca3848..c78c2bc2312 100644 --- a/homeassistant/components/tensorflow/image_processing.py +++ b/homeassistant/components/tensorflow/image_processing.py @@ -376,7 +376,7 @@ class TensorFlowImageProcessor(ImageProcessingEntity): matches = {} total_matches = 0 - for box, score, obj_class in zip(boxes, scores, classes): + for box, score, obj_class in zip(boxes, scores, classes, strict=False): score = score * 100 boxes = box.tolist() diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index 42febb3b36d..037ad4192bd 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -224,7 +224,7 @@ class BaseZhaFlow(ConfigEntryBaseFlow): default_port = vol.UNDEFINED if self._radio_mgr.device_path is not None: - for description, port in zip(list_of_ports, ports): + for description, port in zip(list_of_ports, ports, strict=False): if port.device == self._radio_mgr.device_path: default_port = description break diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 65292e275de..e2c725ee529 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -996,7 +996,7 @@ class ZHADevice(LogMixin): ) ) res = await asyncio.gather(*(t[0] for t in tasks), return_exceptions=True) - for outcome, log_msg in zip(res, tasks): + for outcome, log_msg in zip(res, tasks, strict=False): if isinstance(outcome, Exception): fmt = f"{log_msg[1]} failed: %s" else: diff --git a/homeassistant/components/zha/core/endpoint.py b/homeassistant/components/zha/core/endpoint.py index b0d617eb2c2..1bb1750b6ac 100644 --- a/homeassistant/components/zha/core/endpoint.py +++ b/homeassistant/components/zha/core/endpoint.py @@ -198,7 +198,7 @@ class Endpoint: gather = functools.partial(gather_with_limited_concurrency, max_concurrency) results = await gather(*tasks, return_exceptions=True) - for cluster_handler, outcome in zip(cluster_handlers, results): + for cluster_handler, outcome in zip(cluster_handlers, results, strict=False): if isinstance(outcome, Exception): cluster_handler.debug( "'%s' stage failed: %s", func_name, str(outcome), exc_info=outcome diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index b44fa9e83e1..3f8090f4080 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -292,7 +292,7 @@ def mean_int(*args): def mean_tuple(*args): """Return the mean values along the columns of the supplied values.""" - return tuple(sum(x) / len(x) for x in zip(*args)) + return tuple(sum(x) / len(x) for x in zip(*args, strict=False)) def reduce_attribute( diff --git a/homeassistant/components/zha/websocket_api.py b/homeassistant/components/zha/websocket_api.py index f9a92acdd4c..758c3715980 100644 --- a/homeassistant/components/zha/websocket_api.py +++ b/homeassistant/components/zha/websocket_api.py @@ -1034,7 +1034,7 @@ async def async_binding_operation( ) ) res = await asyncio.gather(*(t[0] for t in bind_tasks), return_exceptions=True) - for outcome, log_msg in zip(res, bind_tasks): + for outcome, log_msg in zip(res, bind_tasks, strict=False): if isinstance(outcome, Exception): fmt = f"{log_msg[1]} failed: %s" else: diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py index 4f1b902d8ba..4a4c1030812 100644 --- a/homeassistant/components/zwave_js/helpers.py +++ b/homeassistant/components/zwave_js/helpers.py @@ -96,7 +96,7 @@ def value_matches_matcher( return all( redacted_field_val is None or redacted_field_val == zwave_value_field_val for redacted_field_val, zwave_value_field_val in zip( - astuple(matcher), astuple(zwave_value_id) + astuple(matcher), astuple(zwave_value_id), strict=False ) ) diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py index 5567c64ab97..bdd5090bcf8 100644 --- a/homeassistant/components/zwave_js/services.py +++ b/homeassistant/components/zwave_js/services.py @@ -85,7 +85,7 @@ def get_valid_responses_from_results( zwave_objects: Sequence[T], results: Sequence[Any] ) -> Generator[tuple[T, Any], None, None]: """Return valid responses from a list of results.""" - for zwave_object, result in zip(zwave_objects, results): + for zwave_object, result in zip(zwave_objects, results, strict=False): if not isinstance(result, Exception): yield zwave_object, result @@ -96,7 +96,9 @@ def raise_exceptions_from_results( """Raise list of exceptions from a list of results.""" errors: Sequence[tuple[T, Any]] if errors := [ - tup for tup in zip(zwave_objects, results) if isinstance(tup[1], Exception) + tup + for tup in zip(zwave_objects, results, strict=True) + if isinstance(tup[1], Exception) ]: lines = [ *( diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index ab747043a04..0ddf4a1e329 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -610,7 +610,9 @@ class DynamicServiceIntentHandler(IntentHandler): # Handle service calls in parallel, noting failures as they occur. failed_results: list[IntentResponseTarget] = [] - for state, service_coro in zip(states, asyncio.as_completed(service_coros)): + for state, service_coro in zip( + states, asyncio.as_completed(service_coros), strict=False + ): target = IntentResponseTarget( type=IntentResponseTargetType.ENTITY, name=state.name, diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 31e0d3648db..3947bc9cbf8 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -709,7 +709,7 @@ async def async_get_all_descriptions( contents = await hass.async_add_executor_job( _load_services_files, hass, integrations ) - loaded = dict(zip(domains_with_missing_services, contents)) + loaded = dict(zip(domains_with_missing_services, contents, strict=False)) # Load translations for all service domains translations = await translation.async_get_translations( @@ -993,7 +993,7 @@ async def entity_service_call( ) response_data: EntityServiceResponse = {} - for entity, result in zip(entities, results): + for entity, result in zip(entities, results, strict=False): if isinstance(result, BaseException): raise result from None response_data[entity.entity_id] = result diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index 3a779b5e944..0809e86460b 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -423,7 +423,7 @@ def _handle_mapping_tag( nodes = loader.construct_pairs(node) seen: dict = {} - for (key, _), (child_node, _) in zip(nodes, node.value): + for (key, _), (child_node, _) in zip(nodes, node.value, strict=False): line = child_node.start_mark.line try: diff --git a/pyproject.toml b/pyproject.toml index 7f5154f297f..122d9715ab1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -673,6 +673,7 @@ select = [ "B026", # Star-arg unpacking after a keyword argument is strongly discouraged "B032", # Possible unintentional type annotation (using :). Did you mean to assign (using =)? "B904", # Use raise from to specify exception cause + "B905", # zip() without an explicit strict= parameter "C", # complexity "COM818", # Trailing comma on bare tuple prohibited "D", # docstrings diff --git a/tests/components/bayesian/test_binary_sensor.py b/tests/components/bayesian/test_binary_sensor.py index 2c94da10ce8..ac80878c836 100644 --- a/tests/components/bayesian/test_binary_sensor.py +++ b/tests/components/bayesian/test_binary_sensor.py @@ -651,7 +651,7 @@ async def test_probability_updates(hass: HomeAssistant) -> None: prob_given_false = [0.7, 0.4, 0.2] prior = 0.5 - for p_t, p_f in zip(prob_given_true, prob_given_false): + for p_t, p_f in zip(prob_given_true, prob_given_false, strict=False): prior = bayesian.update_probability(prior, p_t, p_f) assert round(abs(0.720000 - prior), 7) == 0 @@ -660,7 +660,7 @@ async def test_probability_updates(hass: HomeAssistant) -> None: prob_given_false = [0.6, 0.4, 0.2] prior = 0.7 - for p_t, p_f in zip(prob_given_true, prob_given_false): + for p_t, p_f in zip(prob_given_true, prob_given_false, strict=False): prior = bayesian.update_probability(prior, p_t, p_f) assert round(abs(0.9130434782608695 - prior), 7) == 0 diff --git a/tests/components/derivative/test_sensor.py b/tests/components/derivative/test_sensor.py index e4f57437d24..df050c58f10 100644 --- a/tests/components/derivative/test_sensor.py +++ b/tests/components/derivative/test_sensor.py @@ -74,7 +74,7 @@ async def setup_tests(hass, config, times, values, expected_state): # Testing a energy sensor with non-monotonic intervals and values base = dt_util.utcnow() with freeze_time(base) as freezer: - for time, value in zip(times, values): + for time, value in zip(times, values, strict=False): freezer.move_to(base + timedelta(seconds=time)) hass.states.async_set(entity_id, value, {}, force_update=True) await hass.async_block_till_done() @@ -175,7 +175,7 @@ async def test_data_moving_average_for_discrete_sensor(hass: HomeAssistant) -> N base = dt_util.utcnow() with freeze_time(base) as freezer: - for time, value in zip(times, temperature_values): + for time, value in zip(times, temperature_values, strict=False): now = base + timedelta(seconds=time) freezer.move_to(now) hass.states.async_set(entity_id, value, {}, force_update=True) @@ -219,7 +219,7 @@ async def test_data_moving_average_for_irregular_times(hass: HomeAssistant) -> N base = dt_util.utcnow() with freeze_time(base) as freezer: - for time, value in zip(times, temperature_values): + for time, value in zip(times, temperature_values, strict=False): now = base + timedelta(seconds=time) freezer.move_to(now) hass.states.async_set(entity_id, value, {}, force_update=True) @@ -257,7 +257,7 @@ async def test_double_signal_after_delay(hass: HomeAssistant) -> None: base = dt_util.utcnow() previous = 0 with freeze_time(base) as freezer: - for time, value in zip(times, temperature_values): + for time, value in zip(times, temperature_values, strict=False): now = base + timedelta(seconds=time) freezer.move_to(now) hass.states.async_set(entity_id, value, {}, force_update=True) diff --git a/tests/components/dlna_dms/test_dms_device_source.py b/tests/components/dlna_dms/test_dms_device_source.py index 47bd7b0b39b..f0eedebb4b3 100644 --- a/tests/components/dlna_dms/test_dms_device_source.py +++ b/tests/components/dlna_dms/test_dms_device_source.py @@ -249,7 +249,7 @@ async def test_resolve_media_path(hass: HomeAssistant, dms_device_mock: Mock) -> res_mime: Final = "audio/mpeg" search_directory_result = [] - for ob_id, ob_title in zip(object_ids, path.split("/")): + for ob_id, ob_title in zip(object_ids, path.split("/"), strict=False): didl_item = didl_lite.Item( id=ob_id, restricted="false", @@ -274,7 +274,9 @@ async def test_resolve_media_path(hass: HomeAssistant, dms_device_mock: Mock) -> metadata_filter=["id", "upnp:class", "dc:title"], requested_count=1, ) - for parent_id, title in zip(["0"] + object_ids[:-1], path.split("/")) + for parent_id, title in zip( + ["0"] + object_ids[:-1], path.split("/"), strict=False + ) ] assert result.url == res_abs_url assert result.mime_type == res_mime @@ -290,7 +292,9 @@ async def test_resolve_media_path(hass: HomeAssistant, dms_device_mock: Mock) -> metadata_filter=["id", "upnp:class", "dc:title"], requested_count=1, ) - for parent_id, title in zip(["0"] + object_ids[:-1], path.split("/")) + for parent_id, title in zip( + ["0"] + object_ids[:-1], path.split("/"), strict=False + ) ] assert result.url == res_abs_url assert result.mime_type == res_mime @@ -305,7 +309,7 @@ async def test_resolve_path_browsed(hass: HomeAssistant, dms_device_mock: Mock) # Setup expected calls search_directory_result = [] - for ob_id, ob_title in zip(object_ids, path.split("/")): + for ob_id, ob_title in zip(object_ids, path.split("/"), strict=False): didl_item = didl_lite.Item( id=ob_id, restricted="false", @@ -346,7 +350,9 @@ async def test_resolve_path_browsed(hass: HomeAssistant, dms_device_mock: Mock) metadata_filter=["id", "upnp:class", "dc:title"], requested_count=1, ) - for parent_id, title in zip(["0"] + object_ids[:-1], path.split("/")) + for parent_id, title in zip( + ["0"] + object_ids[:-1], path.split("/"), strict=False + ) ] assert result.didl_metadata.id == object_ids[-1] # 2nd level should also be browsed @@ -608,7 +614,7 @@ async def test_browse_media_object(hass: HomeAssistant, dms_device_mock: Mock) - assert not result.can_play assert result.can_expand assert result.children - for child, title in zip(result.children, child_titles): + for child, title in zip(result.children, child_titles, strict=False): assert isinstance(child, BrowseMediaSource) assert child.identifier == f"{MOCK_SOURCE_ID}/:{title}_id" assert child.title == title @@ -746,7 +752,7 @@ async def test_browse_media_search(hass: HomeAssistant, dms_device_mock: Mock) - assert result.title == "Search results" assert result.children - for obj, child in zip(object_details, result.children): + for obj, child in zip(object_details, result.children, strict=False): assert isinstance(child, BrowseMediaSource) assert child.identifier == f"{MOCK_SOURCE_ID}/:{obj[0]}" assert child.title == obj[1] diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index 4198e648b53..648feb1cc8e 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -155,6 +155,7 @@ async def test_sync_request( for dev, demo in zip( sorted(devices, key=lambda d: d["id"]), sorted(DEMO_DEVICES, key=lambda d: d["id"]), + strict=False, ): assert dev["name"] == demo["name"] assert set(dev["traits"]) == set(demo["traits"]) diff --git a/tests/components/group/test_sensor.py b/tests/components/group/test_sensor.py index fb3c1b6d215..4a8c434c742 100644 --- a/tests/components/group/test_sensor.py +++ b/tests/components/group/test_sensor.py @@ -85,7 +85,7 @@ async def test_sensors2( entity_ids = config["sensor"]["entities"] - for entity_id, value in dict(zip(entity_ids, VALUES)).items(): + for entity_id, value in dict(zip(entity_ids, VALUES, strict=False)).items(): hass.states.async_set( entity_id, value, @@ -135,7 +135,7 @@ async def test_sensors_attributes_defined(hass: HomeAssistant) -> None: entity_ids = config["sensor"]["entities"] - for entity_id, value in dict(zip(entity_ids, VALUES)).items(): + for entity_id, value in dict(zip(entity_ids, VALUES, strict=False)).items(): hass.states.async_set( entity_id, value, @@ -269,7 +269,7 @@ async def test_sensor_incorrect_state_with_ignore_non_numeric( entity_ids = config["sensor"]["entities"] # Check that the final sensor value ignores the non numeric input - for entity_id, value in dict(zip(entity_ids, VALUES_ERROR)).items(): + for entity_id, value in dict(zip(entity_ids, VALUES_ERROR, strict=False)).items(): hass.states.async_set(entity_id, value) await hass.async_block_till_done() @@ -280,7 +280,7 @@ async def test_sensor_incorrect_state_with_ignore_non_numeric( ) # Check that the final sensor value with all numeric inputs - for entity_id, value in dict(zip(entity_ids, VALUES)).items(): + for entity_id, value in dict(zip(entity_ids, VALUES, strict=False)).items(): hass.states.async_set(entity_id, value) await hass.async_block_till_done() @@ -310,7 +310,7 @@ async def test_sensor_incorrect_state_with_not_ignore_non_numeric( entity_ids = config["sensor"]["entities"] # Check that the final sensor value is unavailable if a non numeric input exists - for entity_id, value in dict(zip(entity_ids, VALUES_ERROR)).items(): + for entity_id, value in dict(zip(entity_ids, VALUES_ERROR, strict=False)).items(): hass.states.async_set(entity_id, value) await hass.async_block_till_done() @@ -319,7 +319,7 @@ async def test_sensor_incorrect_state_with_not_ignore_non_numeric( assert "Unable to use state. Only numerical states are supported" in caplog.text # Check that the final sensor value is correct with all numeric inputs - for entity_id, value in dict(zip(entity_ids, VALUES)).items(): + for entity_id, value in dict(zip(entity_ids, VALUES, strict=False)).items(): hass.states.async_set(entity_id, value) await hass.async_block_till_done() @@ -346,7 +346,7 @@ async def test_sensor_require_all_states(hass: HomeAssistant) -> None: entity_ids = config["sensor"]["entities"] - for entity_id, value in dict(zip(entity_ids, VALUES_ERROR)).items(): + for entity_id, value in dict(zip(entity_ids, VALUES_ERROR, strict=False)).items(): hass.states.async_set(entity_id, value) await hass.async_block_till_done() @@ -755,7 +755,7 @@ async def test_last_sensor(hass: HomeAssistant) -> None: entity_ids = config["sensor"]["entities"] - for entity_id, value in dict(zip(entity_ids, VALUES)).items(): + for entity_id, value in dict(zip(entity_ids, VALUES, strict=False)).items(): hass.states.async_set(entity_id, value) await hass.async_block_till_done() state = hass.states.get("sensor.test_last") diff --git a/tests/components/home_connect/test_sensor.py b/tests/components/home_connect/test_sensor.py index 77dec8c615b..f30f017d6d3 100644 --- a/tests/components/home_connect/test_sensor.py +++ b/tests/components/home_connect/test_sensor.py @@ -123,7 +123,13 @@ ENTITY_ID_STATES = { @pytest.mark.parametrize("appliance", [TEST_HC_APP], indirect=True) @pytest.mark.parametrize( ("states", "event_run"), - list(zip(list(zip(*ENTITY_ID_STATES.values())), PROGRAM_SEQUENCE_EVENTS)), + list( + zip( + list(zip(*ENTITY_ID_STATES.values(), strict=False)), + PROGRAM_SEQUENCE_EVENTS, + strict=False, + ) + ), ) async def test_event_sensors( appliance: Mock, @@ -150,7 +156,7 @@ async def test_event_sensors( assert config_entry.state == ConfigEntryState.LOADED appliance.status.update(event_run) - for entity_id, state in zip(entity_ids, states): + for entity_id, state in zip(entity_ids, states, strict=False): await async_update_entity(hass, entity_id) await hass.async_block_till_done() assert hass.states.is_state(entity_id, state) @@ -197,7 +203,7 @@ async def test_remaining_prog_time_edge_cases( for ( event, expected_state, - ) in zip(PROGRAM_SEQUENCE_EDGE_CASE, ENTITY_ID_EDGE_CASE_STATES): + ) in zip(PROGRAM_SEQUENCE_EDGE_CASE, ENTITY_ID_EDGE_CASE_STATES, strict=False): appliance.status.update(event) await async_update_entity(hass, entity_id) await hass.async_block_till_done() diff --git a/tests/components/http/test_ban.py b/tests/components/http/test_ban.py index 91476bf4698..41f36dad2df 100644 --- a/tests/components/http/test_ban.py +++ b/tests/components/http/test_ban.py @@ -190,6 +190,7 @@ async def test_ip_ban_manager_never_started( BANNED_IPS_WITH_SUPERVISOR, [1, 1, 0], [HTTPStatus.FORBIDDEN, HTTPStatus.FORBIDDEN, HTTPStatus.UNAUTHORIZED], + strict=False, ) ), ) diff --git a/tests/components/min_max/test_sensor.py b/tests/components/min_max/test_sensor.py index 4d86ee72cc6..c875697bf2f 100644 --- a/tests/components/min_max/test_sensor.py +++ b/tests/components/min_max/test_sensor.py @@ -51,7 +51,7 @@ async def test_default_name_sensor(hass: HomeAssistant) -> None: entity_ids = config["sensor"]["entity_ids"] - for entity_id, value in dict(zip(entity_ids, VALUES)).items(): + for entity_id, value in dict(zip(entity_ids, VALUES, strict=False)).items(): hass.states.async_set(entity_id, value) await hass.async_block_till_done() @@ -80,7 +80,7 @@ async def test_min_sensor( entity_ids = config["sensor"]["entity_ids"] - for entity_id, value in dict(zip(entity_ids, VALUES)).items(): + for entity_id, value in dict(zip(entity_ids, VALUES, strict=False)).items(): hass.states.async_set(entity_id, value) await hass.async_block_till_done() @@ -110,7 +110,7 @@ async def test_max_sensor(hass: HomeAssistant) -> None: entity_ids = config["sensor"]["entity_ids"] - for entity_id, value in dict(zip(entity_ids, VALUES)).items(): + for entity_id, value in dict(zip(entity_ids, VALUES, strict=False)).items(): hass.states.async_set(entity_id, value) await hass.async_block_till_done() @@ -137,7 +137,7 @@ async def test_mean_sensor(hass: HomeAssistant) -> None: entity_ids = config["sensor"]["entity_ids"] - for entity_id, value in dict(zip(entity_ids, VALUES)).items(): + for entity_id, value in dict(zip(entity_ids, VALUES, strict=False)).items(): hass.states.async_set(entity_id, value) await hass.async_block_till_done() @@ -164,7 +164,7 @@ async def test_mean_1_digit_sensor(hass: HomeAssistant) -> None: entity_ids = config["sensor"]["entity_ids"] - for entity_id, value in dict(zip(entity_ids, VALUES)).items(): + for entity_id, value in dict(zip(entity_ids, VALUES, strict=False)).items(): hass.states.async_set(entity_id, value) await hass.async_block_till_done() @@ -190,7 +190,7 @@ async def test_mean_4_digit_sensor(hass: HomeAssistant) -> None: entity_ids = config["sensor"]["entity_ids"] - for entity_id, value in dict(zip(entity_ids, VALUES)).items(): + for entity_id, value in dict(zip(entity_ids, VALUES, strict=False)).items(): hass.states.async_set(entity_id, value) await hass.async_block_till_done() @@ -215,7 +215,7 @@ async def test_median_sensor(hass: HomeAssistant) -> None: entity_ids = config["sensor"]["entity_ids"] - for entity_id, value in dict(zip(entity_ids, VALUES)).items(): + for entity_id, value in dict(zip(entity_ids, VALUES, strict=False)).items(): hass.states.async_set(entity_id, value) await hass.async_block_till_done() @@ -242,7 +242,7 @@ async def test_range_4_digit_sensor(hass: HomeAssistant) -> None: entity_ids = config["sensor"]["entity_ids"] - for entity_id, value in dict(zip(entity_ids, VALUES)).items(): + for entity_id, value in dict(zip(entity_ids, VALUES, strict=False)).items(): hass.states.async_set(entity_id, value) await hass.async_block_till_done() @@ -268,7 +268,7 @@ async def test_range_1_digit_sensor(hass: HomeAssistant) -> None: entity_ids = config["sensor"]["entity_ids"] - for entity_id, value in dict(zip(entity_ids, VALUES)).items(): + for entity_id, value in dict(zip(entity_ids, VALUES, strict=False)).items(): hass.states.async_set(entity_id, value) await hass.async_block_till_done() @@ -394,7 +394,7 @@ async def test_last_sensor(hass: HomeAssistant) -> None: entity_ids = config["sensor"]["entity_ids"] - for entity_id, value in dict(zip(entity_ids, VALUES)).items(): + for entity_id, value in dict(zip(entity_ids, VALUES, strict=False)).items(): hass.states.async_set(entity_id, value) await hass.async_block_till_done() state = hass.states.get("sensor.test_last") @@ -462,7 +462,7 @@ async def test_sensor_incorrect_state( entity_ids = config["sensor"]["entity_ids"] - for entity_id, value in dict(zip(entity_ids, VALUES_ERROR)).items(): + for entity_id, value in dict(zip(entity_ids, VALUES_ERROR, strict=False)).items(): hass.states.async_set(entity_id, value) await hass.async_block_till_done() @@ -491,7 +491,7 @@ async def test_sum_sensor( entity_ids = config["sensor"]["entity_ids"] - for entity_id, value in dict(zip(entity_ids, VALUES)).items(): + for entity_id, value in dict(zip(entity_ids, VALUES, strict=False)).items(): hass.states.async_set(entity_id, value) await hass.async_block_till_done() @@ -521,7 +521,7 @@ async def test_sum_sensor_no_state(hass: HomeAssistant) -> None: entity_ids = config["sensor"]["entity_ids"] - for entity_id, value in dict(zip(entity_ids, VALUES_ERROR)).items(): + for entity_id, value in dict(zip(entity_ids, VALUES_ERROR, strict=False)).items(): hass.states.async_set(entity_id, value) await hass.async_block_till_done() diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 6cda8b98e1e..addf1e24ea9 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -203,7 +203,7 @@ async def async_test_rejoin(hass, zigpy_device, clusters, report_counts, ep_id=1 zha_gateway = get_zha_gateway(hass) await zha_gateway.async_device_initialized(zigpy_device) await hass.async_block_till_done() - for cluster, reports in zip(clusters, report_counts): + for cluster, reports in zip(clusters, report_counts, strict=False): assert cluster.bind.call_count == 1 assert cluster.bind.await_count == 1 if reports: diff --git a/tests/components/zha/test_discover.py b/tests/components/zha/test_discover.py index 16733d69109..a7e466f1caa 100644 --- a/tests/components/zha/test_discover.py +++ b/tests/components/zha/test_discover.py @@ -420,7 +420,7 @@ def _test_single_input_cluster_device_class(probe_mock): (Platform.BINARY_SENSOR, ias_ch), (Platform.SENSOR, analog_ch), ) - for call, details in zip(probe_mock.call_args_list, probes): + for call, details in zip(probe_mock.call_args_list, probes, strict=False): platform, ch = details assert call[0][0] == platform assert call[0][1] == ch diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index d31e968a025..63dea5ea735 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -803,7 +803,7 @@ async def test_saving_and_loading( # Ensure same order for orig, loaded in zip( - hass.config_entries.async_entries(), manager.async_entries() + hass.config_entries.async_entries(), manager.async_entries(), strict=False ): assert orig.as_dict() == loaded.as_dict() From c24ae01a4303315d9910f1b2e9078ff1b912d1fb Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Sun, 14 Apr 2024 07:15:10 +0200 Subject: [PATCH 563/967] Unignore Ruff E731 (#115564) --- homeassistant/components/homekit/type_fans.py | 8 +++----- pyproject.toml | 1 - tests/components/screenlogic/test_init.py | 16 ++++++++-------- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/homekit/type_fans.py b/homeassistant/components/homekit/type_fans.py index 0dee5fa2b71..64c121878a9 100644 --- a/homeassistant/components/homekit/type_fans.py +++ b/homeassistant/components/homekit/type_fans.py @@ -125,11 +125,9 @@ class Fan(HomeAccessory): ), ) - setter_callback = ( - lambda value, preset_mode=preset_mode: self.set_preset_mode( - value, preset_mode - ) - ) + def setter_callback(value: int, preset_mode: str = preset_mode) -> None: + return self.set_preset_mode(value, preset_mode) + self.preset_mode_chars[preset_mode] = preset_serv.configure_char( CHAR_ON, value=False, diff --git a/pyproject.toml b/pyproject.toml index 122d9715ab1..49eff55a9dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -739,7 +739,6 @@ ignore = [ "D406", # Section name should end with a newline "D407", # Section name underlining "E501", # line too long - "E731", # do not assign a lambda expression, use a def "PLC1901", # {existing} can be simplified to {replacement} as an empty string is falsey; too many false positives "PLR0911", # Too many return statements ({returns} > {max_returns}) diff --git a/tests/components/screenlogic/test_init.py b/tests/components/screenlogic/test_init.py index 9c296fd8afd..6aab9ecec93 100644 --- a/tests/components/screenlogic/test_init.py +++ b/tests/components/screenlogic/test_init.py @@ -89,9 +89,9 @@ TEST_MIGRATING_ENTITIES = [ ), ] -MIGRATION_CONNECT = lambda *args, **kwargs: stub_async_connect( - DATA_MIN_MIGRATION, *args, **kwargs -) + +def _migration_connect(*args, **kwargs): + return stub_async_connect(DATA_MIN_MIGRATION, *args, **kwargs) @pytest.mark.parametrize( @@ -164,7 +164,7 @@ async def test_async_migrate_entries( ), patch.multiple( ScreenLogicGateway, - async_connect=MIGRATION_CONNECT, + async_connect=_migration_connect, is_connected=True, _async_connected_request=DEFAULT, ), @@ -236,7 +236,7 @@ async def test_entity_migration_data( ), patch.multiple( ScreenLogicGateway, - async_connect=MIGRATION_CONNECT, + async_connect=_migration_connect, is_connected=True, _async_connected_request=DEFAULT, ), @@ -257,9 +257,9 @@ async def test_platform_setup( hass: HomeAssistant, mock_config_entry: MockConfigEntry ) -> None: """Test setup for platforms that define expected data.""" - stub_connect = lambda *args, **kwargs: stub_async_connect( - DATA_MISSING_VALUES_CHEM_CHLOR, *args, **kwargs - ) + + def stub_connect(*args, **kwargs): + return stub_async_connect(DATA_MISSING_VALUES_CHEM_CHLOR, *args, **kwargs) device_prefix = slugify(MOCK_ADAPTER_NAME) From 0200d1aa662f074d936cc502f8f9bc70cf1af0ed Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Sun, 14 Apr 2024 07:26:58 +0200 Subject: [PATCH 564/967] Unignore Ruff UP006, UP007 (#115533) --- homeassistant/components/aprilaire/coordinator.py | 4 ++-- homeassistant/components/powerwall/__init__.py | 3 +-- pyproject.toml | 2 -- tests/components/crownstone/test_config_flow.py | 3 +-- tests/components/dlna_dms/test_dms_device_source.py | 4 ++-- tests/components/fints/test_client.py | 8 +++----- tests/components/seventeentrack/conftest.py | 3 +-- 7 files changed, 10 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/aprilaire/coordinator.py b/homeassistant/components/aprilaire/coordinator.py index 7a67dee46a8..7674ff070a6 100644 --- a/homeassistant/components/aprilaire/coordinator.py +++ b/homeassistant/components/aprilaire/coordinator.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Awaitable, Callable import logging -from typing import Any, Optional +from typing import Any import pyaprilaire.client from pyaprilaire.const import MODELS, Attribute, FunctionalDomain @@ -155,7 +155,7 @@ class AprilaireCoordinator(BaseDataUpdateCoordinatorProtocol): return self.create_device_name(self.data) - def create_device_name(self, data: Optional[dict[str, Any]]) -> str: + def create_device_name(self, data: dict[str, Any] | None) -> str: """Create the name of the thermostat.""" name = data.get(Attribute.NAME) if data else None diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index 5257e5a6299..e9334edb6d5 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -5,7 +5,6 @@ from __future__ import annotations from contextlib import AsyncExitStack from datetime import timedelta import logging -from typing import Optional from aiohttp import CookieJar from tesla_powerwall import ( @@ -244,7 +243,7 @@ async def _call_base_info(power_wall: Powerwall, host: str) -> PowerwallBaseInfo ) -async def get_backup_reserve_percentage(power_wall: Powerwall) -> Optional[float]: +async def get_backup_reserve_percentage(power_wall: Powerwall) -> float | None: """Return the backup reserve percentage.""" try: return await power_wall.get_backup_reserve_percentage() diff --git a/pyproject.toml b/pyproject.toml index 49eff55a9dd..8701d67c930 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -760,8 +760,6 @@ ignore = [ "SIM115", # Use context handler for opening files "TRY003", # Avoid specifying long messages outside the exception class "TRY400", # Use `logging.exception` instead of `logging.error` - "UP006", # keep type annotation style as is - "UP007", # keep type annotation style as is # Ignored due to performance: https://github.com/charliermarsh/ruff/issues/2923 "UP038", # Use `X | Y` in `isinstance` call instead of `(X, Y)` # Ignored due to incompatible with mypy: https://github.com/python/mypy/issues/15238 diff --git a/tests/components/crownstone/test_config_flow.py b/tests/components/crownstone/test_config_flow.py index d7705e6026b..3525d8c3f53 100644 --- a/tests/components/crownstone/test_config_flow.py +++ b/tests/components/crownstone/test_config_flow.py @@ -3,7 +3,6 @@ from __future__ import annotations from collections.abc import Generator -from typing import Union from unittest.mock import AsyncMock, MagicMock, patch from crownstone_cloud.cloud_models.spheres import Spheres @@ -31,7 +30,7 @@ from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry -MockFixture = Generator[Union[MagicMock, AsyncMock], None, None] +MockFixture = Generator[MagicMock | AsyncMock, None, None] @pytest.fixture(name="crownstone_setup") diff --git a/tests/components/dlna_dms/test_dms_device_source.py b/tests/components/dlna_dms/test_dms_device_source.py index f0eedebb4b3..bb3c9230534 100644 --- a/tests/components/dlna_dms/test_dms_device_source.py +++ b/tests/components/dlna_dms/test_dms_device_source.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Final, Union +from typing import Final from unittest.mock import ANY, Mock, call from async_upnp_client.exceptions import UpnpActionError, UpnpConnectionError, UpnpError @@ -38,7 +38,7 @@ pytestmark = [ ] -BrowseResultList = list[Union[didl_lite.DidlObject, didl_lite.Descriptor]] +BrowseResultList = list[didl_lite.DidlObject | didl_lite.Descriptor] async def async_resolve_media( diff --git a/tests/components/fints/test_client.py b/tests/components/fints/test_client.py index 429d391b07e..398d539d5b9 100644 --- a/tests/components/fints/test_client.py +++ b/tests/components/fints/test_client.py @@ -1,7 +1,5 @@ """Tests for the FinTS client.""" -from typing import Optional - from fints.client import BankIdentifier, FinTSOperations import pytest @@ -51,10 +49,10 @@ BANK_INFORMATION = { ], ) async def test_account_type( - account_number: Optional[str], - iban: Optional[str], + account_number: str | None, + iban: str | None, product_name: str, - account_type: Optional[int], + account_type: int | None, expected_balance_result: bool, expected_holdings_result: bool, ) -> None: diff --git a/tests/components/seventeentrack/conftest.py b/tests/components/seventeentrack/conftest.py index 052d66a4696..2865b3f2599 100644 --- a/tests/components/seventeentrack/conftest.py +++ b/tests/components/seventeentrack/conftest.py @@ -1,7 +1,6 @@ """Configuration for 17Track tests.""" from collections.abc import Generator -from typing import Optional from unittest.mock import AsyncMock, patch from py17track.package import Package @@ -129,7 +128,7 @@ def mock_seventeentrack(): def get_package( tracking_number: str = "456", destination_country: int = 206, - friendly_name: Optional[str] = "friendly name 1", + friendly_name: str | None = "friendly name 1", info_text: str = "info text 1", location: str = "location 1", timestamp: str = "2020-08-10 10:32", From 33412dd9f687938f6c49d6579e54be35f87db663 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 14 Apr 2024 06:13:17 -0500 Subject: [PATCH 565/967] Remove unused legacy state translations (#112023) * Remove unused state translations There have been replaced with entity translations https://github.com/home-assistant/developers.home-assistant/pull/1557 https://github.com/home-assistant/core/pull/82701 * nothing does merging anymore * useless dispatch * remove * remove platform code from hassfest * preen * Update homeassistant/helpers/translation.py * ruff * fix merge * check is impossible now since we already know if translations exist or not * keep the function for now * remove unreachable code since we filter out `.` before now * reduce * reduce * fix merge conflict (again) --- homeassistant/helpers/translation.py | 157 ++----- script/hassfest/translations.py | 71 ---- tests/helpers/test_template.py | 17 +- tests/helpers/test_translation.py | 387 +++++------------- .../{_broken.en.json => _broken.json} | 0 .../test/translations/en.json | 3 +- .../test/translations/switch.de.json | 6 - .../test/translations/switch.en.json | 7 - .../test/translations/switch.es.json | 5 - 9 files changed, 139 insertions(+), 514 deletions(-) rename tests/testing_config/custom_components/test/translations/{_broken.en.json => _broken.json} (100%) delete mode 100644 tests/testing_config/custom_components/test/translations/switch.de.json delete mode 100644 tests/testing_config/custom_components/test/translations/switch.en.json delete mode 100644 tests/testing_config/custom_components/test/translations/switch.es.json diff --git a/homeassistant/helpers/translation.py b/homeassistant/helpers/translation.py index acc4f146e8b..1fc2c3d075b 100644 --- a/homeassistant/helpers/translation.py +++ b/homeassistant/helpers/translation.py @@ -6,6 +6,7 @@ import asyncio from collections.abc import Iterable, Mapping from contextlib import suppress import logging +import pathlib import string from typing import Any @@ -41,40 +42,18 @@ def recursive_flatten(prefix: Any, data: dict[str, Any]) -> dict[str, Any]: @callback -def component_translation_path( - component: str, language: str, integration: Integration -) -> str | None: +def component_translation_path(language: str, integration: Integration) -> pathlib.Path: """Return the translation json file location for a component. For component: - components/hue/translations/nl.json - For platform: - - components/hue/translations/light.nl.json - - If component is just a single file, will return None. """ - parts = component.split(".") - domain = parts[0] - is_platform = len(parts) == 2 - - # If it's a component that is just one file, we don't support translations - # Example custom_components/my_component.py - if integration.file_path.name != domain: - return None - - if is_platform: - filename = f"{parts[1]}.{language}.json" - else: - filename = f"{language}.json" - - translation_path = integration.file_path / "translations" - - return str(translation_path / filename) + return integration.file_path / "translations" / f"{language}.json" def _load_translations_files_by_language( - translation_files: dict[str, dict[str, str]], + translation_files: dict[str, dict[str, pathlib.Path]], ) -> dict[str, dict[str, Any]]: """Load and parse translation.json files.""" loaded: dict[str, dict[str, Any]] = {} @@ -98,47 +77,6 @@ def _load_translations_files_by_language( return loaded -def _merge_resources( - translation_strings: dict[str, dict[str, Any]], - components: set[str], - category: str, -) -> dict[str, dict[str, Any]]: - """Build and merge the resources response for the given components and platforms.""" - # Build response - resources: dict[str, dict[str, Any]] = {} - for component in components: - domain = component.rpartition(".")[-1] - - domain_resources = resources.setdefault(domain, {}) - - # Integrations are able to provide translations for their entities under other - # integrations if they don't have an existing device class. This is done by - # using a custom device class prefixed with their domain and two underscores. - # These files are in platform specific files in the integration folder with - # names like `strings.sensor.json`. - # We are going to merge the translations for the custom device classes into - # the translations of sensor. - - new_value = translation_strings.get(component, {}).get(category) - - if new_value is None: - continue - - if isinstance(new_value, dict): - domain_resources.update(new_value) - else: - _LOGGER.error( - ( - "An integration providing translations for %s provided invalid" - " data: %s" - ), - domain, - new_value, - ) - - return resources - - def build_resources( translation_strings: dict[str, dict[str, dict[str, Any] | str]], components: set[str], @@ -163,32 +101,20 @@ async def _async_get_component_strings( """Load translations.""" translations_by_language: dict[str, dict[str, Any]] = {} # Determine paths of missing components/platforms - files_to_load_by_language: dict[str, dict[str, str]] = {} + files_to_load_by_language: dict[str, dict[str, pathlib.Path]] = {} loaded_translations_by_language: dict[str, dict[str, Any]] = {} has_files_to_load = False for language in languages: - files_to_load: dict[str, str] = {} - files_to_load_by_language[language] = files_to_load - translations_by_language[language] = {} - - for comp in components: - domain, _, platform = comp.partition(".") + files_to_load: dict[str, pathlib.Path] = { + domain: component_translation_path(language, integration) + for domain in components if ( - not (integration := integrations.get(domain)) - or not integration.has_translations - ): - continue - - if platform and integration.is_built_in: - # Legacy state translations are no longer used for built-in integrations - # and we avoid trying to load them. This is a temporary measure to allow - # them to keep working for custom integrations until we can fully remove - # them. - continue - - if path := component_translation_path(comp, language, integration): - files_to_load[comp] = path - has_files_to_load = True + (integration := integrations.get(domain)) + and integration.has_translations + ) + } + files_to_load_by_language[language] = files_to_load + has_files_to_load |= bool(files_to_load) if has_files_to_load: loaded_translations_by_language = await hass.async_add_executor_job( @@ -197,18 +123,15 @@ async def _async_get_component_strings( for language in languages: loaded_translations = loaded_translations_by_language.setdefault(language, {}) - for comp in components: - if "." in comp: - continue - + for domain in components: # Translations that miss "title" will get integration put in. - component_translations = loaded_translations.setdefault(comp, {}) + component_translations = loaded_translations.setdefault(domain, {}) if "title" not in component_translations and ( - integration := integrations.get(comp) + integration := integrations.get(domain) ): component_translations["title"] = integration.name - translations_by_language[language].update(loaded_translations) + translations_by_language.setdefault(language, {}).update(loaded_translations) return translations_by_language @@ -355,10 +278,12 @@ class _TranslationCache: _LOGGER.error( ( "Validation of translation placeholders for localized (%s) string " - "%s failed" + "%s failed: (%s != %s)" ), language, key, + updated_placeholders, + cached_placeholders, ) mismatches.add(key) @@ -382,17 +307,7 @@ class _TranslationCache: categories.update(resource) for category in categories: - new_resources: Mapping[str, dict[str, Any] | str] - - if category in ("state", "entity_component"): - new_resources = _merge_resources( - translation_strings, components, category - ) - else: - new_resources = build_resources( - translation_strings, components, category - ) - + new_resources = build_resources(translation_strings, components, category) category_cache = cached.setdefault(category, {}) for component, resource in new_resources.items(): @@ -430,7 +345,7 @@ async def async_get_translations( elif integrations is not None: components = set(integrations) else: - components = _async_get_components(hass, category) + components = {comp for comp in hass.config.components if "." not in comp} return await _async_get_translations_cache(hass).async_fetch( language, category, components @@ -452,7 +367,7 @@ def async_get_cached_translations( if integration is not None: components = {integration} else: - components = _async_get_components(hass, category) + components = {comp for comp in hass.config.components if "." not in comp} return _async_get_translations_cache(hass).get_cached( language, category, components @@ -466,21 +381,6 @@ def _async_get_translations_cache(hass: HomeAssistant) -> _TranslationCache: return cache -_DIRECT_MAPPED_CATEGORIES = {"state", "entity_component", "services"} - - -@callback -def _async_get_components( - hass: HomeAssistant, - category: str, -) -> set[str]: - """Return a set of components for which translations should be loaded.""" - if category in _DIRECT_MAPPED_CATEGORIES: - return hass.config.components - # Only 'state' supports merging, so remove platforms from selection - return {component for component in hass.config.components if "." not in component} - - @callback def async_setup(hass: HomeAssistant) -> None: """Create translation cache and register listeners for translation loaders. @@ -590,13 +490,4 @@ def async_translate_state( if localize_key in translations: return translations[localize_key] - translations = async_get_cached_translations(hass, language, "state", domain) - if device_class is not None: - localize_key = f"component.{domain}.state.{device_class}.{state}" - if localize_key in translations: - return translations[localize_key] - localize_key = f"component.{domain}.state._.{state}" - if localize_key in translations: - return translations[localize_key] - return state diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index b893902af69..e815a66b4bb 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -3,7 +3,6 @@ from __future__ import annotations from functools import partial -from itertools import chain import json import re from typing import Any @@ -12,7 +11,6 @@ import voluptuous as vol from voluptuous.humanize import humanize_error import homeassistant.helpers.config_validation as cv -from homeassistant.util import slugify from script.translations import upload from .model import Config, Integration @@ -414,49 +412,6 @@ def gen_ha_hardware_schema(config: Config, integration: Integration): ) -def gen_platform_strings_schema(config: Config, integration: Integration) -> vol.Schema: - """Generate platform strings schema like strings.sensor.json. - - Example of valid data: - { - "state": { - "moon__phase": { - "full": "Full" - } - } - } - """ - - def device_class_validator(value: str) -> str: - """Key validator for platform states. - - Platform states are only allowed to provide states for device classes they prefix. - """ - if not value.startswith(f"{integration.domain}__"): - raise vol.Invalid( - f"Device class need to start with '{integration.domain}__'. Key {value} is invalid. See https://developers.home-assistant.io/docs/internationalization/core#stringssensorjson" - ) - - slug_friendly = value.replace("__", "_", 1) - slugged = slugify(slug_friendly) - - if slug_friendly != slugged: - raise vol.Invalid( - f"invalid device class {value}. After domain__, needs to be all lowercase, no spaces." - ) - - return value - - return vol.Schema( - { - vol.Optional("state"): cv.schema_with_slug_keys( - cv.schema_with_slug_keys(str, slug_validator=translation_key_validator), - slug_validator=device_class_validator, - ) - } - ) - - ONBOARDING_SCHEMA = vol.Schema( { vol.Required("area"): {str: translation_value_validator}, @@ -525,32 +480,6 @@ def validate_translation_file( # noqa: C901 "name or add exception to ALLOW_NAME_TRANSLATION", ) - platform_string_schema = gen_platform_strings_schema(config, integration) - platform_strings = [integration.path.glob("strings.*.json")] - - if config.specific_integrations: - platform_strings.append(integration.path.glob("translations/*.en.json")) - - for path in chain(*platform_strings): - name = str(path.relative_to(integration.path)) - - try: - strings = json.loads(path.read_text()) - except ValueError as err: - integration.add_error("translations", f"Invalid JSON in {name}: {err}") - continue - - try: - platform_string_schema(strings) - except vol.Invalid as err: - msg = f"Invalid {path.name}: {humanize_error(strings, err)}" - if config.specific_integrations: - integration.add_warning("translations", msg) - else: - integration.add_error("translations", msg) - else: - find_references(strings, path.name, references) - if config.specific_integrations: return diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index fe152ac0d56..524b8f47dfe 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -2050,12 +2050,7 @@ async def test_state_translated( ): if category == "entity": return { - "component.hue.entity.light.translation_key.state.on": "state_is_on" - } - if category == "state": - return { - "component.some_domain.state.some_device_class.off": "state_is_off", - "component.some_domain.state._.foo": "state_is_foo", + "component.hue.entity.light.translation_key.state.on": "state_is_on", } return {} @@ -2066,16 +2061,6 @@ async def test_state_translated( tpl8 = template.Template('{{ state_translated("light.hue_5678") }}', hass) assert tpl8.async_render() == "state_is_on" - tpl9 = template.Template( - '{{ state_translated("some_domain.with_device_class_1") }}', hass - ) - assert tpl9.async_render() == "state_is_off" - - tpl10 = template.Template( - '{{ state_translated("some_domain.with_device_class_2") }}', hass - ) - assert tpl10.async_render() == "state_is_foo" - tpl11 = template.Template('{{ state_translated("domain.is_unavailable") }}', hass) assert tpl11.async_render() == "unavailable" diff --git a/tests/helpers/test_translation.py b/tests/helpers/test_translation.py index 1bba23c51a1..b841e1ab5ac 100644 --- a/tests/helpers/test_translation.py +++ b/tests/helpers/test_translation.py @@ -47,35 +47,10 @@ async def test_component_translation_path( {"switch": [{"platform": "test"}, {"platform": "test_embedded"}]}, ) assert await async_setup_component(hass, "test_package", {"test_package": None}) - - ( - int_test, - int_test_embedded, - int_test_package, - ) = await asyncio.gather( - async_get_integration(hass, "test"), - async_get_integration(hass, "test_embedded"), - async_get_integration(hass, "test_package"), - ) + int_test_package = await async_get_integration(hass, "test_package") assert path.normpath( - translation.component_translation_path("test.switch", "en", int_test) - ) == path.normpath( - hass.config.path("custom_components", "test", "translations", "switch.en.json") - ) - - assert path.normpath( - translation.component_translation_path( - "test_embedded.switch", "en", int_test_embedded - ) - ) == path.normpath( - hass.config.path( - "custom_components", "test_embedded", "translations", "switch.en.json" - ) - ) - - assert path.normpath( - translation.component_translation_path("test_package", "en", int_test_package) + translation.component_translation_path("en", int_test_package) ) == path.normpath( hass.config.path("custom_components", "test_package", "translations", "en.json") ) @@ -86,28 +61,39 @@ def test__load_translations_files_by_language( ) -> None: """Test the load translation files function.""" # Test one valid and one invalid file - file1 = hass.config.path( - "custom_components", "test", "translations", "switch.en.json" - ) - file2 = hass.config.path( + en_file = hass.config.path("custom_components", "test", "translations", "en.json") + invalid_file = hass.config.path( "custom_components", "test", "translations", "invalid.json" ) - file3 = hass.config.path( - "custom_components", "test", "translations", "_broken.en.json" + broken_file = hass.config.path( + "custom_components", "test", "translations", "_broken.json" ) assert translation._load_translations_files_by_language( - {"en": {"switch.test": file1, "invalid": file2, "broken": file3}} - ) == { - "en": { - "switch.test": { - "state": {"string1": "Value 1", "string2": "Value 2"}, - "something": "else", - }, - "invalid": {}, + { + "en": {"test": en_file}, + "invalid": {"test": invalid_file}, + "broken": {"test": broken_file}, } + ) == { + "broken": {}, + "en": { + "test": { + "entity": { + "switch": { + "other1": {"name": "Other 1"}, + "other2": {"name": "Other 2"}, + "other3": {"name": "Other 3"}, + "other4": {"name": "Other 4"}, + "outlet": {"name": "Outlet " "{placeholder}"}, + } + }, + "something": "else", + } + }, + "invalid": {"test": {}}, } assert "Translation file is unexpected type" in caplog.text - assert "_broken.en.json" in caplog.text + assert "_broken.json" in caplog.text @pytest.mark.parametrize( @@ -185,33 +171,61 @@ async def test_get_translations( hass: HomeAssistant, mock_config_flows, enable_custom_integrations: None ) -> None: """Test the get translations helper.""" - translations = await translation.async_get_translations(hass, "en", "state") + translations = await translation.async_get_translations(hass, "en", "entity") assert translations == {} assert await async_setup_component(hass, "switch", {"switch": {"platform": "test"}}) await hass.async_block_till_done() - translations = await translation.async_get_translations(hass, "en", "state") + translations = await translation.async_get_translations( + hass, "en", "entity", {"test"} + ) - assert translations["component.switch.state.string1"] == "Value 1" - assert translations["component.switch.state.string2"] == "Value 2" + assert translations == { + "component.test.entity.switch.other1.name": "Other 1", + "component.test.entity.switch.other2.name": "Other 2", + "component.test.entity.switch.other3.name": "Other 3", + "component.test.entity.switch.other4.name": "Other 4", + "component.test.entity.switch.outlet.name": "Outlet {placeholder}", + } - translations = await translation.async_get_translations(hass, "de", "state") - assert "component.switch.something" not in translations - assert translations["component.switch.state.string1"] == "German Value 1" - assert translations["component.switch.state.string2"] == "German Value 2" + translations = await translation.async_get_translations( + hass, "de", "entity", {"test"} + ) + + assert translations == { + "component.test.entity.switch.other1.name": "Anderes 1", + "component.test.entity.switch.other2.name": "Other 2", + "component.test.entity.switch.other3.name": "", + "component.test.entity.switch.other4.name": "Other 4", + "component.test.entity.switch.outlet.name": "Outlet {placeholder}", + } # Test a partial translation - translations = await translation.async_get_translations(hass, "es", "state") - assert translations["component.switch.state.string1"] == "Spanish Value 1" - assert translations["component.switch.state.string2"] == "Value 2" + translations = await translation.async_get_translations( + hass, "es", "entity", {"test"} + ) + + assert translations == { + "component.test.entity.switch.other1.name": "Otra 1", + "component.test.entity.switch.other2.name": "Otra 2", + "component.test.entity.switch.other3.name": "Otra 3", + "component.test.entity.switch.other4.name": "Otra 4", + "component.test.entity.switch.outlet.name": "Enchufe {placeholder}", + } # Test that an untranslated language falls back to English. translations = await translation.async_get_translations( - hass, "invalid-language", "state" + hass, "invalid-language", "entity", {"test"} ) - assert translations["component.switch.state.string1"] == "Value 1" - assert translations["component.switch.state.string2"] == "Value 2" + + assert translations == { + "component.test.entity.switch.other1.name": "Other 1", + "component.test.entity.switch.other2.name": "Other 2", + "component.test.entity.switch.other3.name": "Other 3", + "component.test.entity.switch.other4.name": "Other 4", + "component.test.entity.switch.outlet.name": "Outlet {placeholder}", + } async def test_get_translations_loads_config_flows( @@ -348,162 +362,6 @@ async def test_get_translation_categories(hass: HomeAssistant) -> None: assert "component.light.device_automation.action_type.turn_on" in translations -async def test_legacy_platform_translations_not_used_built_in_integrations( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture -) -> None: - """Test legacy platform translations are not used for built-in integrations.""" - hass.config.components.add("moon.sensor") - hass.config.components.add("sensor") - - load_requests = [] - - def mock_load_translations_files_by_language(files): - load_requests.append(files) - return {} - - with patch( - "homeassistant.helpers.translation._load_translations_files_by_language", - mock_load_translations_files_by_language, - ): - await translation.async_get_translations(hass, "en", "state") - - assert len(load_requests) == 1 - to_load = load_requests[0] - assert len(to_load) == 1 - en_load = to_load["en"] - assert len(en_load) == 1 - assert "sensor" in en_load - assert "moon.sensor" not in en_load - - -async def test_translation_merging_custom_components( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, -) -> None: - """Test we merge translations of two integrations. - - Legacy state translations only used for custom integrations. - """ - hass.config.components.add("test_legacy_state_translations.sensor") - hass.config.components.add("sensor") - - orig_load_translations = translation._load_translations_files_by_language - - def mock_load_translations_files(files): - """Mock loading.""" - result = orig_load_translations(files) - result["en"]["test_legacy_state_translations.sensor"] = { - "state": { - "test_legacy_state_translations__phase": { - "first_quarter": "First Quarter" - } - } - } - return result - - with patch( - "homeassistant.helpers.translation._load_translations_files_by_language", - side_effect=mock_load_translations_files, - ): - translations = await translation.async_get_translations(hass, "en", "state") - - assert ( - "component.sensor.state.test_legacy_state_translations__phase.first_quarter" - in translations - ) - - hass.config.components.add("test_legacy_state_translations_bad_data.sensor") - - # Patch in some bad translation data - def mock_load_bad_translations_files(files): - """Mock loading.""" - result = orig_load_translations(files) - result["en"]["test_legacy_state_translations_bad_data.sensor"] = { - "state": "bad data" - } - return result - - with patch( - "homeassistant.helpers.translation._load_translations_files_by_language", - side_effect=mock_load_bad_translations_files, - ): - translations = await translation.async_get_translations(hass, "en", "state") - - assert ( - "component.sensor.state.test_legacy_state_translations__phase.first_quarter" - in translations - ) - - assert ( - "An integration providing translations for sensor provided invalid data:" - " bad data" - ) in caplog.text - - -async def test_translation_merging_loaded_apart_custom_integrations( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, -) -> None: - """Test we merge translations of two integrations when they are not loaded at the same time. - - Legacy state translations only used for custom integrations. - """ - orig_load_translations = translation._load_translations_files_by_language - - def mock_load_translations_files(files): - """Mock loading.""" - result = orig_load_translations(files) - result["en"]["test_legacy_state_translations.sensor"] = { - "state": { - "test_legacy_state_translations__phase": { - "first_quarter": "First Quarter" - } - } - } - return result - - hass.config.components.add("sensor") - - with patch( - "homeassistant.helpers.translation._load_translations_files_by_language", - side_effect=mock_load_translations_files, - ): - translations = await translation.async_get_translations(hass, "en", "state") - - assert ( - "component.sensor.state.test_legacy_state_translations__phase.first_quarter" - not in translations - ) - - hass.config.components.add("test_legacy_state_translations.sensor") - - with patch( - "homeassistant.helpers.translation._load_translations_files_by_language", - side_effect=mock_load_translations_files, - ): - translations = await translation.async_get_translations(hass, "en", "state") - - assert ( - "component.sensor.state.test_legacy_state_translations__phase.first_quarter" - in translations - ) - - with patch( - "homeassistant.helpers.translation._load_translations_files_by_language", - side_effect=mock_load_translations_files, - ): - translations = await translation.async_get_translations( - hass, "en", "state", integrations={"sensor"} - ) - - assert ( - "component.sensor.state.test_legacy_state_translations__phase.first_quarter" - in translations - ) - - async def test_translation_merging_loaded_together( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: @@ -592,14 +450,14 @@ async def test_caching(hass: HomeAssistant) -> None: # Patch with same method so we can count invocations with patch( - "homeassistant.helpers.translation._merge_resources", - side_effect=translation._merge_resources, - ) as mock_merge: + "homeassistant.helpers.translation.build_resources", + side_effect=translation.build_resources, + ) as mock_build_resources: load1 = await translation.async_get_translations(hass, "en", "entity_component") - assert len(mock_merge.mock_calls) == 1 + assert len(mock_build_resources.mock_calls) == 5 load2 = await translation.async_get_translations(hass, "en", "entity_component") - assert len(mock_merge.mock_calls) == 1 + assert len(mock_build_resources.mock_calls) == 5 assert load1 == load2 @@ -665,47 +523,58 @@ async def test_custom_component_translations( async def test_get_cached_translations( hass: HomeAssistant, mock_config_flows, enable_custom_integrations: None -): +) -> None: """Test the get cached translations helper.""" - translations = translation.async_get_cached_translations(hass, "en", "state") + translations = await translation.async_get_translations(hass, "en", "entity") assert translations == {} assert await async_setup_component(hass, "switch", {"switch": {"platform": "test"}}) await hass.async_block_till_done() - await translation._async_get_translations_cache(hass).async_load( - "en", hass.config.components - ) - translations = translation.async_get_cached_translations(hass, "en", "state") + await translation._async_get_translations_cache(hass).async_load("en", {"test"}) - assert translations["component.switch.state.string1"] == "Value 1" - assert translations["component.switch.state.string2"] == "Value 2" - - await translation._async_get_translations_cache(hass).async_load( - "de", hass.config.components + translations = translation.async_get_cached_translations( + hass, "en", "entity", "test" ) - translations = translation.async_get_cached_translations(hass, "de", "state") - assert "component.switch.something" not in translations - assert translations["component.switch.state.string1"] == "German Value 1" - assert translations["component.switch.state.string2"] == "German Value 2" + assert translations == { + "component.test.entity.switch.other1.name": "Other 1", + "component.test.entity.switch.other2.name": "Other 2", + "component.test.entity.switch.other3.name": "Other 3", + "component.test.entity.switch.other4.name": "Other 4", + "component.test.entity.switch.outlet.name": "Outlet {placeholder}", + } + + await translation._async_get_translations_cache(hass).async_load("es", {"test"}) # Test a partial translation - await translation._async_get_translations_cache(hass).async_load( - "es", hass.config.components + translations = translation.async_get_cached_translations( + hass, "es", "entity", "test" + ) + + assert translations == { + "component.test.entity.switch.other1.name": "Otra 1", + "component.test.entity.switch.other2.name": "Otra 2", + "component.test.entity.switch.other3.name": "Otra 3", + "component.test.entity.switch.other4.name": "Otra 4", + "component.test.entity.switch.outlet.name": "Enchufe {placeholder}", + } + + await translation._async_get_translations_cache(hass).async_load( + "invalid-language", {"test"} ) - translations = translation.async_get_cached_translations(hass, "es", "state") - assert translations["component.switch.state.string1"] == "Spanish Value 1" - assert translations["component.switch.state.string2"] == "Value 2" # Test that an untranslated language falls back to English. - await translation._async_get_translations_cache(hass).async_load( - "invalid-language", hass.config.components - ) translations = translation.async_get_cached_translations( - hass, "invalid-language", "state" + hass, "invalid-language", "entity", "test" ) - assert translations["component.switch.state.string1"] == "Value 1" - assert translations["component.switch.state.string2"] == "Value 2" + + assert translations == { + "component.test.entity.switch.other1.name": "Other 1", + "component.test.entity.switch.other2.name": "Other 2", + "component.test.entity.switch.other3.name": "Other 3", + "component.test.entity.switch.other4.name": "Other 4", + "component.test.entity.switch.outlet.name": "Outlet {placeholder}", + } async def test_setup(hass: HomeAssistant): @@ -784,36 +653,6 @@ async def test_translate_state(hass: HomeAssistant): mock.assert_called_once_with(hass, hass.config.language, "entity_component") assert result == "TRANSLATED" - with patch( - "homeassistant.helpers.translation.async_get_cached_translations", - return_value={"component.binary_sensor.state.device_class.on": "TRANSLATED"}, - ) as mock: - result = translation.async_translate_state( - hass, "on", "binary_sensor", "platform", None, "device_class" - ) - mock.assert_has_calls( - [ - call(hass, hass.config.language, "entity_component"), - call(hass, hass.config.language, "state", "binary_sensor"), - ] - ) - assert result == "TRANSLATED" - - with patch( - "homeassistant.helpers.translation.async_get_cached_translations", - return_value={"component.binary_sensor.state._.on": "TRANSLATED"}, - ) as mock: - result = translation.async_translate_state( - hass, "on", "binary_sensor", "platform", None, None - ) - mock.assert_has_calls( - [ - call(hass, hass.config.language, "entity_component"), - call(hass, hass.config.language, "state", "binary_sensor"), - ] - ) - assert result == "TRANSLATED" - with patch( "homeassistant.helpers.translation.async_get_cached_translations", return_value={}, @@ -824,7 +663,6 @@ async def test_translate_state(hass: HomeAssistant): mock.assert_has_calls( [ call(hass, hass.config.language, "entity_component"), - call(hass, hass.config.language, "state", "binary_sensor"), ] ) assert result == "on" @@ -840,7 +678,6 @@ async def test_translate_state(hass: HomeAssistant): [ call(hass, hass.config.language, "entity"), call(hass, hass.config.language, "entity_component"), - call(hass, hass.config.language, "state", "binary_sensor"), ] ) assert result == "on" diff --git a/tests/testing_config/custom_components/test/translations/_broken.en.json b/tests/testing_config/custom_components/test/translations/_broken.json similarity index 100% rename from tests/testing_config/custom_components/test/translations/_broken.en.json rename to tests/testing_config/custom_components/test/translations/_broken.json diff --git a/tests/testing_config/custom_components/test/translations/en.json b/tests/testing_config/custom_components/test/translations/en.json index 56404508c4c..7ed32c224a7 100644 --- a/tests/testing_config/custom_components/test/translations/en.json +++ b/tests/testing_config/custom_components/test/translations/en.json @@ -7,5 +7,6 @@ "other4": { "name": "Other 4" }, "outlet": { "name": "Outlet {placeholder}" } } - } + }, + "something": "else" } diff --git a/tests/testing_config/custom_components/test/translations/switch.de.json b/tests/testing_config/custom_components/test/translations/switch.de.json deleted file mode 100644 index fad78b12d63..00000000000 --- a/tests/testing_config/custom_components/test/translations/switch.de.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "state": { - "string1": "German Value 1", - "string2": "German Value 2" - } -} diff --git a/tests/testing_config/custom_components/test/translations/switch.en.json b/tests/testing_config/custom_components/test/translations/switch.en.json deleted file mode 100644 index 1cc764adb21..00000000000 --- a/tests/testing_config/custom_components/test/translations/switch.en.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "state": { - "string1": "Value 1", - "string2": "Value 2" - }, - "something": "else" -} diff --git a/tests/testing_config/custom_components/test/translations/switch.es.json b/tests/testing_config/custom_components/test/translations/switch.es.json deleted file mode 100644 index b3590a6d321..00000000000 --- a/tests/testing_config/custom_components/test/translations/switch.es.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "state": { - "string1": "Spanish Value 1" - } -} From 291df6dafe44090cb13ebae4672d2c1de4ba6e9c Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sun, 14 Apr 2024 18:07:26 +0300 Subject: [PATCH 566/967] Bump aioshelly to 9.0.0 (#114025) * Update Shelly to use initialize from aioshelly * Save indentation in _async_device_connect * Use firmware_supported property from aioshelly * Review comments * Bump aioshelly * Fix lint errors * Test RPC initialized update --- homeassistant/components/shelly/__init__.py | 116 +++++------------- .../components/shelly/config_flow.py | 11 +- .../components/shelly/coordinator.py | 89 ++++++++++---- homeassistant/components/shelly/entity.py | 2 +- homeassistant/components/shelly/light.py | 2 +- homeassistant/components/shelly/manifest.json | 2 +- homeassistant/components/shelly/strings.json | 1 - homeassistant/components/shelly/switch.py | 8 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/shelly/conftest.py | 18 +++ tests/components/shelly/test_binary_sensor.py | 13 +- tests/components/shelly/test_climate.py | 18 +-- tests/components/shelly/test_config_flow.py | 50 +------- tests/components/shelly/test_coordinator.py | 115 ++++++++++------- tests/components/shelly/test_init.py | 69 ++++++++--- tests/components/shelly/test_number.py | 44 +++++-- tests/components/shelly/test_sensor.py | 33 +++-- tests/components/shelly/test_update.py | 11 +- 19 files changed, 349 insertions(+), 257 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 7d23a1cd57d..cfeab531687 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -3,23 +3,22 @@ from __future__ import annotations import contextlib -from typing import Any, Final +from typing import Final -from aioshelly.block_device import BlockDevice, BlockUpdateType +from aioshelly.block_device import BlockDevice from aioshelly.common import ConnectionOptions from aioshelly.const import DEFAULT_COAP_PORT, RPC_GENERATIONS from aioshelly.exceptions import ( DeviceConnectionError, - FirmwareUnsupported, InvalidAuthError, MacAddressMismatchError, ) -from aioshelly.rpc_device import RpcDevice, RpcUpdateType +from aioshelly.rpc_device import RpcDevice import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -53,12 +52,9 @@ from .coordinator import ( ) from .utils import ( async_create_issue_unsupported_firmware, - async_shutdown_device, - get_block_device_sleep_period, get_coap_context, get_device_entry_gen, get_http_port, - get_rpc_device_wakeup_period, get_ws_context, ) @@ -154,7 +150,6 @@ async def _async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> b async_get_clientsession(hass), coap_context, options, - False, ) dev_reg = dr_async_get(hass) @@ -186,57 +181,38 @@ async def _async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> b data[CONF_SLEEP_PERIOD] = sleep_period = BLOCK_EXPECTED_SLEEP_PERIOD hass.config_entries.async_update_entry(entry, data=data) - async def _async_block_device_setup() -> None: - """Set up a block based device that is online.""" - shelly_entry_data.block = ShellyBlockCoordinator(hass, entry, device) - shelly_entry_data.block.async_setup() - - platforms = BLOCK_SLEEPING_PLATFORMS - - if not entry.data.get(CONF_SLEEP_PERIOD): - shelly_entry_data.rest = ShellyRestCoordinator(hass, device, entry) - platforms = BLOCK_PLATFORMS - - await hass.config_entries.async_forward_entry_setups(entry, platforms) - - @callback - def _async_device_online(_: Any, update_type: BlockUpdateType) -> None: - LOGGER.debug("Device %s is online, resuming setup", entry.title) - shelly_entry_data.device = None - - if sleep_period is None: - data = {**entry.data} - data[CONF_SLEEP_PERIOD] = get_block_device_sleep_period(device.settings) - data["model"] = device.settings["device"]["type"] - hass.config_entries.async_update_entry(entry, data=data) - - hass.async_create_task(_async_block_device_setup(), eager_start=True) - if sleep_period == 0: # Not a sleeping device, finish setup LOGGER.debug("Setting up online block device %s", entry.title) try: await device.initialize() + if not device.firmware_supported: + async_create_issue_unsupported_firmware(hass, entry) + raise ConfigEntryNotReady except (DeviceConnectionError, MacAddressMismatchError) as err: raise ConfigEntryNotReady(repr(err)) from err except InvalidAuthError as err: raise ConfigEntryAuthFailed(repr(err)) from err - except FirmwareUnsupported as err: - async_create_issue_unsupported_firmware(hass, entry) - raise ConfigEntryNotReady from err - await _async_block_device_setup() + 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) elif sleep_period is None or device_entry is None: # Need to get sleep info or first time sleeping device setup, wait for device - shelly_entry_data.device = device LOGGER.debug( "Setup for device %s will resume when device is online", entry.title ) - device.subscribe_updates(_async_device_online) + shelly_entry_data.block = ShellyBlockCoordinator(hass, entry, device) + shelly_entry_data.block.async_setup(BLOCK_SLEEPING_PLATFORMS) else: # Restore sensors for sleeping device LOGGER.debug("Setting up offline block device %s", entry.title) - await _async_block_device_setup() + shelly_entry_data.block = ShellyBlockCoordinator(hass, entry, device) + shelly_entry_data.block.async_setup() + await hass.config_entries.async_forward_entry_setups( + entry, BLOCK_SLEEPING_PLATFORMS + ) ir.async_delete_issue( hass, DOMAIN, FIRMWARE_UNSUPPORTED_ISSUE_ID.format(unique=entry.unique_id) @@ -260,7 +236,6 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> boo async_get_clientsession(hass), ws_context, options, - False, ) dev_reg = dr_async_get(hass) @@ -276,58 +251,38 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> boo sleep_period = entry.data.get(CONF_SLEEP_PERIOD) shelly_entry_data = get_entry_data(hass)[entry.entry_id] - async def _async_rpc_device_setup() -> None: - """Set up a RPC based device that is online.""" - shelly_entry_data.rpc = ShellyRpcCoordinator(hass, entry, device) - shelly_entry_data.rpc.async_setup() - - platforms = RPC_SLEEPING_PLATFORMS - - if not entry.data.get(CONF_SLEEP_PERIOD): - shelly_entry_data.rpc_poll = ShellyRpcPollingCoordinator( - hass, entry, device - ) - platforms = RPC_PLATFORMS - - await hass.config_entries.async_forward_entry_setups(entry, platforms) - - @callback - def _async_device_online(_: Any, update_type: RpcUpdateType) -> None: - LOGGER.debug("Device %s is online, resuming setup", entry.title) - shelly_entry_data.device = None - - if sleep_period is None: - data = {**entry.data} - data[CONF_SLEEP_PERIOD] = get_rpc_device_wakeup_period(device.status) - hass.config_entries.async_update_entry(entry, data=data) - - hass.async_create_task(_async_rpc_device_setup(), eager_start=True) - if sleep_period == 0: # Not a sleeping device, finish setup LOGGER.debug("Setting up online RPC device %s", entry.title) try: await device.initialize() - except FirmwareUnsupported as err: - async_create_issue_unsupported_firmware(hass, entry) - raise ConfigEntryNotReady from err + if not device.firmware_supported: + async_create_issue_unsupported_firmware(hass, entry) + raise ConfigEntryNotReady except (DeviceConnectionError, MacAddressMismatchError) as err: raise ConfigEntryNotReady(repr(err)) from err except InvalidAuthError as err: raise ConfigEntryAuthFailed(repr(err)) from err - await _async_rpc_device_setup() + 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) elif sleep_period is None or device_entry is None: # Need to get sleep info or first time sleeping device setup, wait for device - shelly_entry_data.device = device LOGGER.debug( "Setup for device %s will resume when device is online", entry.title ) - device.subscribe_updates(_async_device_online) + shelly_entry_data.rpc = ShellyRpcCoordinator(hass, entry, device) + shelly_entry_data.rpc.async_setup(RPC_SLEEPING_PLATFORMS) else: # Restore sensors for sleeping device - LOGGER.debug("Setting up offline block device %s", entry.title) - await _async_rpc_device_setup() + LOGGER.debug("Setting up offline RPC device %s", entry.title) + shelly_entry_data.rpc = ShellyRpcCoordinator(hass, entry, device) + shelly_entry_data.rpc.async_setup() + await hass.config_entries.async_forward_entry_setups( + entry, RPC_SLEEPING_PLATFORMS + ) ir.async_delete_issue( hass, DOMAIN, FIRMWARE_UNSUPPORTED_ISSUE_ID.format(unique=entry.unique_id) @@ -339,11 +294,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" shelly_entry_data = get_entry_data(hass)[entry.entry_id] - # If device is present, block/rpc coordinator is not setup yet - if (device := shelly_entry_data.device) is not None: - await async_shutdown_device(device) - return True - platforms = RPC_SLEEPING_PLATFORMS if not entry.data.get(CONF_SLEEP_PERIOD): platforms = RPC_PLATFORMS diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 24b66e15893..46cea4e49a4 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -11,7 +11,6 @@ from aioshelly.const import BLOCK_GENERATIONS, DEFAULT_HTTP_PORT, RPC_GENERATION from aioshelly.exceptions import ( CustomPortNotSupported, DeviceConnectionError, - FirmwareUnsupported, InvalidAuthError, ) from aioshelly.rpc_device import RpcDevice @@ -103,6 +102,7 @@ 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) @@ -121,6 +121,7 @@ async def validate_input( coap_context, options, ) + await block_device.initialize() block_device.shutdown() return { "title": block_device.name, @@ -154,8 +155,6 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): self.info = await self._async_get_info(host, port) except DeviceConnectionError: errors["base"] = "cannot_connect" - except FirmwareUnsupported: - return self.async_abort(reason="unsupported_firmware") except Exception: # pylint: disable=broad-except LOGGER.exception("Unexpected exception") errors["base"] = "unknown" @@ -287,8 +286,6 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): self.info = await self._async_get_info(host, DEFAULT_HTTP_PORT) except DeviceConnectionError: return self.async_abort(reason="cannot_connect") - except FirmwareUnsupported: - return self.async_abort(reason="unsupported_firmware") if not mac: # We could not get the mac address from the name @@ -366,14 +363,14 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): if user_input is not None: try: info = await self._async_get_info(host, port) - except (DeviceConnectionError, InvalidAuthError, FirmwareUnsupported): + except (DeviceConnectionError, InvalidAuthError): return self.async_abort(reason="reauth_unsuccessful") if get_device_entry_gen(self.entry) != 1: user_input[CONF_USERNAME] = "admin" try: await validate_input(self.hass, host, port, info, user_input) - except (DeviceConnectionError, InvalidAuthError, FirmwareUnsupported): + except (DeviceConnectionError, InvalidAuthError): return self.async_abort(reason="reauth_unsuccessful") return self.async_update_reload_and_abort( diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 18f96dd9c2e..bd6686198ed 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -15,7 +15,12 @@ from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCal from aioshelly.rpc_device import RpcDevice, RpcUpdateType from homeassistant.config_entries import ConfigEntry, ConfigEntryState -from homeassistant.const import ATTR_DEVICE_ID, CONF_HOST, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ( + ATTR_DEVICE_ID, + CONF_HOST, + EVENT_HOMEASSISTANT_STOP, + Platform, +) from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.debounce import Debouncer @@ -58,7 +63,9 @@ from .const import ( BLEScannerMode, ) from .utils import ( + async_create_issue_unsupported_firmware, async_shutdown_device, + get_block_device_sleep_period, get_device_entry_gen, get_http_port, get_rpc_device_wakeup_period, @@ -73,7 +80,6 @@ class ShellyEntryData: """Class for sharing data within a given config entry.""" block: ShellyBlockCoordinator | None = None - device: BlockDevice | RpcDevice | None = None rest: ShellyRestCoordinator | None = None rpc: ShellyRpcCoordinator | None = None rpc_poll: ShellyRpcPollingCoordinator | None = None @@ -98,6 +104,7 @@ class ShellyCoordinatorBase(DataUpdateCoordinator[None], Generic[_DeviceT]): self.entry = entry self.device = device self.device_id: str | None = None + self._pending_platforms: list[Platform] | None = None device_name = device.name if device.initialized else entry.title interval_td = timedelta(seconds=update_interval) super().__init__(hass, LOGGER, name=device_name, update_interval=interval_td) @@ -131,8 +138,9 @@ class ShellyCoordinatorBase(DataUpdateCoordinator[None], Generic[_DeviceT]): """Sleep period of the device.""" return self.entry.data.get(CONF_SLEEP_PERIOD, 0) - def async_setup(self) -> None: + def async_setup(self, pending_platforms: list[Platform] | None = None) -> None: """Set up the coordinator.""" + self._pending_platforms = pending_platforms dev_reg = dr_async_get(self.hass) device_entry = dev_reg.async_get_or_create( config_entry_id=self.entry.entry_id, @@ -146,6 +154,45 @@ class ShellyCoordinatorBase(DataUpdateCoordinator[None], Generic[_DeviceT]): ) self.device_id = device_entry.id + async def _async_device_connect(self) -> None: + """Connect to a Shelly Block device.""" + LOGGER.debug("Connecting to Shelly Device - %s", self.name) + try: + await self.device.initialize() + update_device_fw_info(self.hass, self.device, self.entry) + except DeviceConnectionError as err: + raise UpdateFailed(f"Device disconnected: {repr(err)}") from err + except InvalidAuthError: + self.entry.async_start_reauth(self.hass) + return + + if not self.device.firmware_supported: + async_create_issue_unsupported_firmware(self.hass, self.entry) + return + + if not self._pending_platforms: + return + + LOGGER.debug("Device %s is online, resuming setup", self.entry.title) + platforms = self._pending_platforms + self._pending_platforms = None + + data = {**self.entry.data} + + # Update sleep_period + old_sleep_period = data[CONF_SLEEP_PERIOD] + if isinstance(self.device, RpcDevice): + new_sleep_period = get_rpc_device_wakeup_period(self.device.status) + elif isinstance(self.device, BlockDevice): + new_sleep_period = get_block_device_sleep_period(self.device.settings) + + if new_sleep_period != old_sleep_period: + data[CONF_SLEEP_PERIOD] = new_sleep_period + 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) + async def _async_reload_entry(self) -> None: """Reload entry.""" self._debounced_reload.async_cancel() @@ -179,7 +226,7 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): self._last_cfg_changed: int | None = None self._last_mode: str | None = None - self._last_effect: int | None = None + self._last_effect: str | None = None self._last_input_events_count: dict = {} self._last_target_temp: float | None = None self._push_update_failures: int = 0 @@ -211,15 +258,14 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): if not self.device.initialized: return - assert self.device.blocks - # For buttons which are battery powered - set initial value for last_event_count if self.model in SHBTN_MODELS and self._last_input_events_count.get(1) is None: for block in self.device.blocks: if block.type != "device": continue - if len(block.wakeupEvent) == 1 and block.wakeupEvent[0] == "button": + wakeup_event = cast(list, block.wakeupEvent) + if len(wakeup_event) == 1 and wakeup_event[0] == "button": self._last_input_events_count[1] = -1 break @@ -228,7 +274,7 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): cfg_changed = 0 for block in self.device.blocks: if block.type == "device" and block.cfgChanged is not None: - cfg_changed = block.cfgChanged + cfg_changed = cast(int, block.cfgChanged) # Shelly TRV sends information about changing the configuration for no # reason, reloading the config entry is not needed for it. @@ -314,14 +360,16 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): self, device_: BlockDevice, update_type: BlockUpdateType ) -> None: """Handle device update.""" - if update_type == BlockUpdateType.COAP_PERIODIC: + if update_type is BlockUpdateType.ONLINE: + self.hass.async_create_task(self._async_device_connect(), eager_start=True) + elif update_type is BlockUpdateType.COAP_PERIODIC: self._push_update_failures = 0 ir.async_delete_issue( self.hass, DOMAIN, PUSH_UPDATE_ISSUE_ID.format(unique=self.mac), ) - elif update_type == BlockUpdateType.COAP_REPLY: + elif update_type is BlockUpdateType.COAP_REPLY: self._push_update_failures += 1 if self._push_update_failures == MAX_PUSH_UPDATE_FAILURES: LOGGER.debug( @@ -346,9 +394,9 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): ) self.async_set_updated_data(None) - def async_setup(self) -> None: + def async_setup(self, pending_platforms: list[Platform] | None = None) -> None: """Set up the coordinator.""" - super().async_setup() + super().async_setup(pending_platforms) self.device.subscribe_updates(self._async_handle_update) def shutdown(self) -> None: @@ -538,14 +586,7 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): if self.device.connected: return - LOGGER.debug("Reconnecting to Shelly RPC Device - %s", self.name) - try: - await self.device.initialize() - update_device_fw_info(self.hass, self.device, self.entry) - except DeviceConnectionError as err: - raise UpdateFailed(f"Device disconnected: {repr(err)}") from err - except InvalidAuthError: - await self.async_shutdown_device_and_start_reauth() + await self._async_device_connect() async def _async_disconnected(self) -> None: """Handle device disconnected.""" @@ -612,7 +653,9 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): self, device_: RpcDevice, update_type: RpcUpdateType ) -> None: """Handle device update.""" - if update_type is RpcUpdateType.INITIALIZED: + if update_type is RpcUpdateType.ONLINE: + self.hass.async_create_task(self._async_device_connect(), eager_start=True) + elif update_type is RpcUpdateType.INITIALIZED: self.hass.async_create_task(self._async_connected(), eager_start=True) self.async_set_updated_data(None) elif update_type is RpcUpdateType.DISCONNECTED: @@ -624,9 +667,9 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): elif update_type is RpcUpdateType.EVENT and (event := self.device.event): self._async_device_event_handler(event) - def async_setup(self) -> None: + def async_setup(self, pending_platforms: list[Platform] | None = None) -> None: """Set up the coordinator.""" - super().async_setup() + super().async_setup(pending_platforms) self.device.subscribe_updates(self._async_handle_update) if self.device.initialized: # If we are already initialized, we are connected diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index accca5f1a64..150244e2e47 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -74,7 +74,7 @@ def async_setup_block_attribute_entities( for block in coordinator.device.blocks: for sensor_id in block.sensor_ids: - description = sensors.get((block.type, sensor_id)) + description = sensors.get((cast(str, block.type), sensor_id)) if description is None: continue diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index d0590fc7c20..0650e2d15e5 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -78,7 +78,7 @@ def async_setup_block_entry( for block in coordinator.device.blocks: if block.type == "light": blocks.append(block) - elif block.type == "relay": + elif block.type == "relay" and block.channel is not None: if not is_block_channel_type_light( coordinator.device.settings, int(block.channel) ): diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index 06159cb543b..08971713ced 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==8.2.0"], + "requirements": ["aioshelly==9.0.0"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/homeassistant/components/shelly/strings.json b/homeassistant/components/shelly/strings.json index d4a8b117f4c..cee27e9ca07 100644 --- a/homeassistant/components/shelly/strings.json +++ b/homeassistant/components/shelly/strings.json @@ -38,7 +38,6 @@ }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "unsupported_firmware": "The device is using an unsupported firmware version.", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "reauth_unsuccessful": "Re-authentication was unsuccessful, please remove the integration and set it up again." } diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py index 48ff337d22a..14fec43c58b 100644 --- a/homeassistant/components/shelly/switch.py +++ b/homeassistant/components/shelly/switch.py @@ -104,8 +104,12 @@ def async_setup_block_entry( relay_blocks = [] assert coordinator.device.blocks for block in coordinator.device.blocks: - if block.type != "relay" or is_block_channel_type_light( - coordinator.device.settings, int(block.channel) + if ( + block.type != "relay" + or block.channel is not None + and is_block_channel_type_light( + coordinator.device.settings, int(block.channel) + ) ): continue diff --git a/requirements_all.txt b/requirements_all.txt index 54d136c0b83..14f5749a02a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -368,7 +368,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==8.2.0 +aioshelly==9.0.0 # homeassistant.components.skybell aioskybell==22.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 32bfdcd0968..a58259c096c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -341,7 +341,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==8.2.0 +aioshelly==9.0.0 # homeassistant.components.skybell aioskybell==22.7.0 diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index 3cd27101f76..18813ff7eba 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -319,6 +319,11 @@ async def mock_block_device(): {}, BlockUpdateType.COAP_REPLY ) + def online(): + block_device_mock.return_value.subscribe_updates.call_args[0][0]( + {}, BlockUpdateType.ONLINE + ) + device = Mock( spec=BlockDevice, blocks=MOCK_BLOCKS, @@ -337,6 +342,7 @@ async def mock_block_device(): block_device_mock.return_value.mock_update_reply = Mock( side_effect=update_reply ) + block_device_mock.return_value.mock_online = Mock(side_effect=online) yield block_device_mock.return_value @@ -376,16 +382,28 @@ async def mock_rpc_device(): {}, RpcUpdateType.EVENT ) + def online(): + rpc_device_mock.return_value.subscribe_updates.call_args[0][0]( + {}, RpcUpdateType.ONLINE + ) + def disconnected(): rpc_device_mock.return_value.subscribe_updates.call_args[0][0]( {}, RpcUpdateType.DISCONNECTED ) + def initialized(): + rpc_device_mock.return_value.subscribe_updates.call_args[0][0]( + {}, RpcUpdateType.INITIALIZED + ) + device = _mock_rpc_device() rpc_device_mock.return_value = device rpc_device_mock.return_value.mock_disconnected = Mock(side_effect=disconnected) rpc_device_mock.return_value.mock_update = Mock(side_effect=update) rpc_device_mock.return_value.mock_event = Mock(side_effect=event) + rpc_device_mock.return_value.mock_online = Mock(side_effect=online) + rpc_device_mock.return_value.mock_initialized = Mock(side_effect=initialized) yield rpc_device_mock.return_value diff --git a/tests/components/shelly/test_binary_sensor.py b/tests/components/shelly/test_binary_sensor.py index 00a430cd4b1..624eb82f060 100644 --- a/tests/components/shelly/test_binary_sensor.py +++ b/tests/components/shelly/test_binary_sensor.py @@ -144,7 +144,7 @@ async def test_block_sleeping_binary_sensor( assert hass.states.get(entity_id) is None # Make device online - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_OFF @@ -180,7 +180,7 @@ async def test_block_restored_sleeping_binary_sensor( # Make device online monkeypatch.setattr(mock_block_device, "initialized", True) - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_OFF @@ -206,7 +206,7 @@ async def test_block_restored_sleeping_binary_sensor_no_last_state( # Make device online monkeypatch.setattr(mock_block_device, "initialized", True) - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_OFF @@ -263,6 +263,7 @@ async def test_rpc_sleeping_binary_sensor( ) -> None: """Test RPC online sleeping binary sensor.""" entity_id = f"{BINARY_SENSOR_DOMAIN}.test_name_cloud" + monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", 1000) config_entry = await init_integration(hass, 2, sleep_period=1000) # Sensor should be created when device is online @@ -273,7 +274,7 @@ async def test_rpc_sleeping_binary_sensor( ) # Make device online - mock_rpc_device.mock_update() + mock_rpc_device.mock_online() await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_OFF @@ -344,6 +345,10 @@ async def test_rpc_restored_sleeping_binary_sensor_no_last_state( # Make device online monkeypatch.setattr(mock_rpc_device, "initialized", True) + mock_rpc_device.mock_online() + await hass.async_block_till_done() + + # Mock update mock_rpc_device.mock_update() await hass.async_block_till_done() diff --git a/tests/components/shelly/test_climate.py b/tests/components/shelly/test_climate.py index 0bdab979a0e..9fee3468f11 100644 --- a/tests/components/shelly/test_climate.py +++ b/tests/components/shelly/test_climate.py @@ -64,7 +64,7 @@ async def test_climate_hvac_mode( await init_integration(hass, 1, sleep_period=1000, model=MODEL_VALVE) # Make device online - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() # Test initial hvac mode - off @@ -125,7 +125,7 @@ async def test_climate_set_temperature( await init_integration(hass, 1, sleep_period=1000) # Make device online - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) @@ -192,7 +192,7 @@ async def test_climate_set_preset_mode( await init_integration(hass, 1, sleep_period=1000, model=MODEL_VALVE) # Make device online - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) @@ -278,7 +278,7 @@ async def test_block_restored_climate( # Make device online monkeypatch.setattr(mock_block_device, "initialized", True) - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() assert hass.states.get(entity_id).state == HVACMode.OFF @@ -349,7 +349,7 @@ async def test_block_restored_climate_us_customery( monkeypatch.setattr(mock_block_device, "initialized", True) monkeypatch.setattr(mock_block_device.blocks[SENSOR_BLOCK_ID], "targetTemp", 4.0) monkeypatch.setattr(mock_block_device.blocks[SENSOR_BLOCK_ID], "temp", 18.2) - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() assert hass.states.get(entity_id).state == HVACMode.OFF @@ -451,7 +451,7 @@ async def test_block_set_mode_connection_error( await init_integration(hass, 1, sleep_period=1000) # Make device online - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() with pytest.raises(HomeAssistantError): @@ -476,7 +476,7 @@ async def test_block_set_mode_auth_error( entry = await init_integration(hass, 1, sleep_period=1000) # Make device online - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() assert entry.state is ConfigEntryState.LOADED @@ -534,7 +534,7 @@ async def test_block_restored_climate_auth_error( type(mock_block_device).settings = PropertyMock( return_value={}, side_effect=InvalidAuthError ) - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() assert entry.state is ConfigEntryState.LOADED @@ -561,7 +561,7 @@ async def test_device_not_calibrated( await init_integration(hass, 1, sleep_period=1000, model=MODEL_VALVE) # Make device online - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() mock_status = MOCK_STATUS_COAP.copy() diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index f2b0736f867..c73b93f9fdb 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -10,7 +10,6 @@ from aioshelly.const import DEFAULT_HTTP_PORT, MODEL_1, MODEL_PLUS_2PM from aioshelly.exceptions import ( CustomPortNotSupported, DeviceConnectionError, - FirmwareUnsupported, InvalidAuthError, ) import pytest @@ -433,25 +432,6 @@ async def test_user_setup_ignored_device( assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_firmware_unsupported(hass: HomeAssistant) -> None: - """Test we abort if device firmware is unsupported.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with patch( - "homeassistant.components.shelly.config_flow.get_info", - side_effect=FirmwareUnsupported, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"host": "1.1.1.1"}, - ) - - assert result2["type"] is FlowResultType.ABORT - assert result2["reason"] == "unsupported_firmware" - - @pytest.mark.parametrize( ("exc", "base_error"), [ @@ -757,22 +737,6 @@ async def test_zeroconf_with_wifi_ap_ip(hass: HomeAssistant) -> None: assert entry.data["host"] == "2.2.2.2" -async def test_zeroconf_firmware_unsupported(hass: HomeAssistant) -> None: - """Test we abort if device firmware is unsupported.""" - with patch( - "homeassistant.components.shelly.config_flow.get_info", - side_effect=FirmwareUnsupported, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - data=DISCOVERY_INFO, - context={"source": config_entries.SOURCE_ZEROCONF}, - ) - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "unsupported_firmware" - - async def test_zeroconf_cannot_connect(hass: HomeAssistant) -> None: """Test we get the form.""" with patch( @@ -927,11 +891,7 @@ async def test_reauth_unsuccessful( assert result["reason"] == "reauth_unsuccessful" -@pytest.mark.parametrize( - "error", - [DeviceConnectionError, FirmwareUnsupported], -) -async def test_reauth_get_info_error(hass: HomeAssistant, error: Exception) -> None: +async def test_reauth_get_info_error(hass: HomeAssistant) -> None: """Test reauthentication flow failed with error in get_info().""" entry = MockConfigEntry( domain="shelly", unique_id="test-mac", data={"host": "0.0.0.0", "gen": 2} @@ -940,7 +900,7 @@ async def test_reauth_get_info_error(hass: HomeAssistant, error: Exception) -> N with patch( "homeassistant.components.shelly.config_flow.get_info", - side_effect=error, + side_effect=DeviceConnectionError, ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -1154,6 +1114,7 @@ async def test_zeroconf_sleeping_device_not_triggers_refresh( caplog: pytest.LogCaptureFixture, ) -> None: """Test zeroconf discovery does not triggers refresh for sleeping device.""" + monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", 1000) entry = MockConfigEntry( domain="shelly", unique_id="AABBCCDDEEFF", @@ -1163,10 +1124,11 @@ async def test_zeroconf_sleeping_device_not_triggers_refresh( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - mock_rpc_device.mock_update() + mock_rpc_device.mock_online() await hass.async_block_till_done() assert "online, resuming setup" in caplog.text + assert len(mock_rpc_device.initialize.mock_calls) == 1 with patch( "homeassistant.components.shelly.config_flow.get_info", @@ -1186,7 +1148,7 @@ async def test_zeroconf_sleeping_device_not_triggers_refresh( hass, dt_util.utcnow() + timedelta(seconds=ENTRY_RELOAD_COOLDOWN) ) await hass.async_block_till_done() - assert len(mock_rpc_device.initialize.mock_calls) == 0 + assert len(mock_rpc_device.initialize.mock_calls) == 1 assert "device did not update" not in caplog.text diff --git a/tests/components/shelly/test_coordinator.py b/tests/components/shelly/test_coordinator.py index b155176dccd..9f251d1e008 100644 --- a/tests/components/shelly/test_coordinator.py +++ b/tests/components/shelly/test_coordinator.py @@ -1,14 +1,10 @@ """Tests for Shelly coordinator.""" from datetime import timedelta -from unittest.mock import AsyncMock, Mock, patch +from unittest.mock import AsyncMock, Mock, call, patch from aioshelly.const import MODEL_BULB, MODEL_BUTTON1 -from aioshelly.exceptions import ( - DeviceConnectionError, - FirmwareUnsupported, - InvalidAuthError, -) +from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError from freezegun.api import FrozenDateTimeFactory import pytest @@ -29,13 +25,13 @@ 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 +from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.device_registry import ( CONNECTION_NETWORK_MAC, async_entries_for_config_entry, async_get as async_get_dev_reg, format_mac, ) -import homeassistant.helpers.issue_registry as ir from . import ( MOCK_MAC, @@ -216,28 +212,25 @@ async def test_block_rest_update_auth_error( assert flow["context"].get("entry_id") == entry.entry_id -async def test_block_firmware_unsupported( +async def test_block_sleeping_device_firmware_unsupported( hass: HomeAssistant, - freezer: FrozenDateTimeFactory, mock_block_device: Mock, monkeypatch: pytest.MonkeyPatch, + issue_registry: ir.IssueRegistry, ) -> None: - """Test block device polling authentication error.""" - monkeypatch.setattr( - mock_block_device, - "update", - AsyncMock(side_effect=FirmwareUnsupported), - ) - entry = await init_integration(hass, 1) + """Test block sleeping device firmware not supported.""" + monkeypatch.setattr(mock_block_device, "firmware_supported", False) + entry = await init_integration(hass, 1, sleep_period=3600) - assert entry.state is ConfigEntryState.LOADED - - # Move time to generate polling - freezer.tick(timedelta(seconds=UPDATE_PERIOD_MULTIPLIER * 15)) - async_fire_time_changed(hass) + # Make device online + mock_block_device.mock_online() await hass.async_block_till_done() assert entry.state is ConfigEntryState.LOADED + assert ( + DOMAIN, + "firmware_unsupported_123456789ABC", + ) in issue_registry.issues async def test_block_polling_connection_error( @@ -290,20 +283,28 @@ async def test_block_rest_update_connection_error( async def test_block_sleeping_device_no_periodic_updates( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_block_device: Mock + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_block_device: Mock, + monkeypatch: pytest.MonkeyPatch, ) -> None: """Test block sleeping device no periodic updates.""" entity_id = f"{SENSOR_DOMAIN}.test_name_temperature" - await init_integration(hass, 1, sleep_period=1000) + monkeypatch.setitem( + mock_block_device.settings, + "sleep_mode", + {"period": 60, "unit": "m"}, + ) + await init_integration(hass, 1, sleep_period=3600) # Make device online - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() assert get_entity_state(hass, entity_id) == "22.1" # Move time to generate polling - freezer.tick(timedelta(seconds=UPDATE_PERIOD_MULTIPLIER * 1000)) + freezer.tick(timedelta(seconds=UPDATE_PERIOD_MULTIPLIER * 3600)) async_fire_time_changed(hass) await hass.async_block_till_done() @@ -352,7 +353,7 @@ async def test_block_button_click_event( entry = await init_integration(hass, 1, model=MODEL_BUTTON1, sleep_period=1000) # Make device online - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() dev_reg = async_get_dev_reg(hass) @@ -529,6 +530,7 @@ async def test_rpc_update_entry_sleep_period( monkeypatch: pytest.MonkeyPatch, ) -> None: """Test RPC update entry sleep period.""" + monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", 600) entry = await init_integration(hass, 2, sleep_period=600) register_entity( hass, @@ -539,7 +541,7 @@ async def test_rpc_update_entry_sleep_period( ) # Make device online - mock_rpc_device.mock_update() + mock_rpc_device.mock_online() await hass.async_block_till_done() assert entry.data["sleep_period"] == 600 @@ -554,10 +556,14 @@ async def test_rpc_update_entry_sleep_period( async def test_rpc_sleeping_device_no_periodic_updates( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_rpc_device: Mock + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_rpc_device: Mock, + monkeypatch: pytest.MonkeyPatch, ) -> None: """Test RPC sleeping device no periodic updates.""" entity_id = f"{SENSOR_DOMAIN}.test_name_temperature" + monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", 1000) entry = await init_integration(hass, 2, sleep_period=1000) register_entity( hass, @@ -568,7 +574,7 @@ async def test_rpc_sleeping_device_no_periodic_updates( ) # Make device online - mock_rpc_device.mock_update() + mock_rpc_device.mock_online() await hass.async_block_till_done() assert get_entity_state(hass, entity_id) == "22.9" @@ -581,25 +587,25 @@ async def test_rpc_sleeping_device_no_periodic_updates( assert get_entity_state(hass, entity_id) is STATE_UNAVAILABLE -async def test_rpc_firmware_unsupported( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_rpc_device: Mock +async def test_rpc_sleeping_device_firmware_unsupported( + hass: HomeAssistant, + mock_rpc_device: Mock, + monkeypatch: pytest.MonkeyPatch, + issue_registry: ir.IssueRegistry, ) -> None: - """Test RPC update entry unsupported firmware.""" - entry = await init_integration(hass, 2) - register_entity( - hass, - SENSOR_DOMAIN, - "test_name_temperature", - "temperature:0-temperature_0", - entry, - ) + """Test RPC sleeping device firmware not supported.""" + monkeypatch.setattr(mock_rpc_device, "firmware_supported", False) + entry = await init_integration(hass, 2, sleep_period=3600) - # Move time to generate sleep period update - freezer.tick(timedelta(seconds=600 * SLEEP_PERIOD_MULTIPLIER)) - async_fire_time_changed(hass) + # Make device online + mock_rpc_device.mock_online() await hass.async_block_till_done() assert entry.state is ConfigEntryState.LOADED + assert ( + DOMAIN, + "firmware_unsupported_123456789ABC", + ) in issue_registry.issues async def test_rpc_reconnect_auth_error( @@ -753,11 +759,12 @@ async def test_rpc_update_entry_fw_ver( hass: HomeAssistant, 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_update() + mock_rpc_device.mock_online() await hass.async_block_till_done() assert entry.unique_id @@ -779,3 +786,23 @@ async def test_rpc_update_entry_fw_ver( ) assert device assert device.sw_version == "99.0.0" + + +async def test_rpc_runs_connected_events_when_initialized( + hass: HomeAssistant, + mock_rpc_device: Mock, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test RPC runs connected events when initialized.""" + monkeypatch.setattr(mock_rpc_device, "initialized", False) + await init_integration(hass, 2) + + assert call.script_list() not in mock_rpc_device.mock_calls + + # Mock initialized event + monkeypatch.setattr(mock_rpc_device, "initialized", True) + mock_rpc_device.mock_initialized() + await hass.async_block_till_done() + + # BLE script list is called during connected events + assert call.script_list() in mock_rpc_device.mock_calls diff --git a/tests/components/shelly/test_init.py b/tests/components/shelly/test_init.py index de658cd0d16..61ec8ce6779 100644 --- a/tests/components/shelly/test_init.py +++ b/tests/components/shelly/test_init.py @@ -8,7 +8,6 @@ from aioshelly.common import ConnectionOptions from aioshelly.const import MODEL_PLUS_2PM from aioshelly.exceptions import ( DeviceConnectionError, - FirmwareUnsupported, InvalidAuthError, MacAddressMismatchError, ) @@ -27,6 +26,7 @@ from homeassistant.components.shelly.const import ( from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import CONF_HOST, CONF_PORT, STATE_ON, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.device_registry import ( CONNECTION_NETWORK_MAC, DeviceRegistry, @@ -145,27 +145,46 @@ async def test_setup_entry_not_shelly( @pytest.mark.parametrize("gen", [1, 2, 3]) -@pytest.mark.parametrize("side_effect", [DeviceConnectionError, FirmwareUnsupported]) async def test_device_connection_error( hass: HomeAssistant, gen: int, - side_effect: Exception, mock_block_device: Mock, mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test device connection error.""" monkeypatch.setattr( - mock_block_device, "initialize", AsyncMock(side_effect=side_effect) + mock_block_device, "initialize", AsyncMock(side_effect=DeviceConnectionError) ) monkeypatch.setattr( - mock_rpc_device, "initialize", AsyncMock(side_effect=side_effect) + mock_rpc_device, "initialize", AsyncMock(side_effect=DeviceConnectionError) ) entry = await init_integration(hass, gen) assert entry.state is ConfigEntryState.SETUP_RETRY +@pytest.mark.parametrize("gen", [1, 2, 3]) +async def test_device_unsupported_firmware( + hass: HomeAssistant, + gen: int, + mock_block_device: Mock, + mock_rpc_device: Mock, + monkeypatch: pytest.MonkeyPatch, + issue_registry: ir.IssueRegistry, +) -> None: + """Test device init with unsupported firmware.""" + monkeypatch.setattr(mock_block_device, "firmware_supported", False) + monkeypatch.setattr(mock_rpc_device, "firmware_supported", False) + + entry = await init_integration(hass, gen) + assert entry.state is ConfigEntryState.SETUP_RETRY + assert ( + DOMAIN, + "firmware_unsupported_123456789ABC", + ) in issue_registry.issues + + @pytest.mark.parametrize("gen", [1, 2, 3]) async def test_mac_mismatch_error( hass: HomeAssistant, @@ -217,12 +236,13 @@ async def test_device_auth_error( assert flow["context"].get("entry_id") == entry.entry_id -@pytest.mark.parametrize(("entry_sleep", "device_sleep"), [(None, 0), (1000, 1000)]) +@pytest.mark.parametrize(("entry_sleep", "device_sleep"), [(None, 0), (3600, 3600)]) async def test_sleeping_block_device_online( hass: HomeAssistant, entry_sleep: int | None, device_sleep: int, mock_block_device: Mock, + monkeypatch: pytest.MonkeyPatch, device_reg: DeviceRegistry, caplog: pytest.LogCaptureFixture, ) -> None: @@ -234,10 +254,17 @@ async def test_sleeping_block_device_online( connections={(CONNECTION_NETWORK_MAC, format_mac(MOCK_MAC))}, ) + monkeypatch.setitem( + mock_block_device.settings, + "sleep_mode", + {"period": int(device_sleep / 60), "unit": "m"}, + ) entry = await init_integration(hass, 1, sleep_period=entry_sleep) assert "will resume when device is online" in caplog.text - mock_block_device.mock_update() + mock_block_device.mock_online() + await hass.async_block_till_done() + assert "online, resuming setup" in caplog.text assert entry.data["sleep_period"] == device_sleep @@ -248,13 +275,17 @@ async def test_sleeping_rpc_device_online( entry_sleep: int | None, device_sleep: int, mock_rpc_device: Mock, + monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, ) -> None: """Test sleeping RPC device online.""" + monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", device_sleep) entry = await init_integration(hass, 2, sleep_period=entry_sleep) assert "will resume when device is online" in caplog.text - mock_rpc_device.mock_update() + mock_rpc_device.mock_online() + await hass.async_block_till_done() + assert "online, resuming setup" in caplog.text assert entry.data["sleep_period"] == device_sleep @@ -270,7 +301,9 @@ async def test_sleeping_rpc_device_online_new_firmware( assert "will resume when device is online" in caplog.text mutate_rpc_device_status(monkeypatch, mock_rpc_device, "sys", "wakeup_period", 1500) - mock_rpc_device.mock_update() + mock_rpc_device.mock_online() + await hass.async_block_till_done() + assert "online, resuming setup" in caplog.text assert entry.data["sleep_period"] == 1500 @@ -413,9 +446,12 @@ async def test_entry_missing_port(hass: HomeAssistant) -> None: } entry = MockConfigEntry(domain=DOMAIN, data=data, unique_id=MOCK_MAC) entry.add_to_hass(hass) - with patch( - "homeassistant.components.shelly.RpcDevice.create", return_value=Mock() - ) as rpc_device_mock: + with ( + patch("homeassistant.components.shelly.RpcDevice.initialize"), + patch( + "homeassistant.components.shelly.RpcDevice.create", return_value=Mock() + ) as rpc_device_mock, + ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -435,9 +471,12 @@ async def test_rpc_entry_custom_port(hass: HomeAssistant) -> None: } entry = MockConfigEntry(domain=DOMAIN, data=data, unique_id=MOCK_MAC) entry.add_to_hass(hass) - with patch( - "homeassistant.components.shelly.RpcDevice.create", return_value=Mock() - ) as rpc_device_mock: + with ( + patch("homeassistant.components.shelly.RpcDevice.initialize"), + patch( + "homeassistant.components.shelly.RpcDevice.create", return_value=Mock() + ) as rpc_device_mock, + ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/shelly/test_number.py b/tests/components/shelly/test_number.py index c138ef71b7d..99ad5709d29 100644 --- a/tests/components/shelly/test_number.py +++ b/tests/components/shelly/test_number.py @@ -33,12 +33,17 @@ async def test_block_number_update( ) -> None: """Test block device number update.""" entity_id = "number.test_name_valve_position" - await init_integration(hass, 1, sleep_period=1000) + monkeypatch.setitem( + mock_block_device.settings, + "sleep_mode", + {"period": 60, "unit": "m"}, + ) + await init_integration(hass, 1, sleep_period=3600) assert hass.states.get(entity_id) is None # Make device online - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() assert hass.states.get(entity_id).state == "50" @@ -93,7 +98,7 @@ async def test_block_restored_number( # Make device online monkeypatch.setattr(mock_block_device, "initialized", True) - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() assert hass.states.get(entity_id).state == "50" @@ -130,20 +135,27 @@ async def test_block_restored_number_no_last_state( # Make device online monkeypatch.setattr(mock_block_device, "initialized", True) - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() assert hass.states.get(entity_id).state == "50" async def test_block_number_set_value( - hass: HomeAssistant, mock_block_device: Mock + hass: HomeAssistant, + mock_block_device: Mock, + monkeypatch: pytest.MonkeyPatch, ) -> None: """Test block device number set value.""" - await init_integration(hass, 1, sleep_period=1000) + monkeypatch.setitem( + mock_block_device.settings, + "sleep_mode", + {"period": 60, "unit": "m"}, + ) + await init_integration(hass, 1, sleep_period=3600) # Make device online - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() mock_block_device.reset_mock() @@ -162,15 +174,20 @@ async def test_block_set_value_connection_error( hass: HomeAssistant, mock_block_device: Mock, monkeypatch: pytest.MonkeyPatch ) -> None: """Test block device set value connection error.""" + monkeypatch.setitem( + mock_block_device.settings, + "sleep_mode", + {"period": 60, "unit": "m"}, + ) monkeypatch.setattr( mock_block_device, "http_request", AsyncMock(side_effect=DeviceConnectionError), ) - await init_integration(hass, 1, sleep_period=1000) + await init_integration(hass, 1, sleep_period=3600) # Make device online - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() with pytest.raises(HomeAssistantError): @@ -186,15 +203,20 @@ async def test_block_set_value_auth_error( hass: HomeAssistant, mock_block_device: Mock, monkeypatch: pytest.MonkeyPatch ) -> None: """Test block device set value authentication error.""" + monkeypatch.setitem( + mock_block_device.settings, + "sleep_mode", + {"period": 60, "unit": "m"}, + ) monkeypatch.setattr( mock_block_device, "http_request", AsyncMock(side_effect=InvalidAuthError), ) - entry = await init_integration(hass, 1, sleep_period=1000) + entry = await init_integration(hass, 1, sleep_period=3600) # Make device online - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() assert entry.state is ConfigEntryState.LOADED diff --git a/tests/components/shelly/test_sensor.py b/tests/components/shelly/test_sensor.py index 0a15b78994b..6151cac10ab 100644 --- a/tests/components/shelly/test_sensor.py +++ b/tests/components/shelly/test_sensor.py @@ -164,7 +164,7 @@ async def test_block_sleeping_sensor( assert hass.states.get(entity_id) is None # Make device online - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() assert hass.states.get(entity_id).state == "22.1" @@ -206,7 +206,7 @@ async def test_block_restored_sleeping_sensor( # Make device online monkeypatch.setattr(mock_block_device, "initialized", True) - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() assert hass.states.get(entity_id).state == "22.1" @@ -232,7 +232,7 @@ async def test_block_restored_sleeping_sensor_no_last_state( # Make device online monkeypatch.setattr(mock_block_device, "initialized", True) - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() assert hass.states.get(entity_id).state == "22.1" @@ -305,7 +305,7 @@ async def test_block_not_matched_restored_sleeping_sensor( mock_block_device.blocks[SENSOR_BLOCK_ID], "description", "other_desc" ) monkeypatch.setattr(mock_block_device, "initialized", True) - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() assert hass.states.get(entity_id).state == "20.4" @@ -448,6 +448,7 @@ async def test_rpc_sleeping_sensor( ) -> None: """Test RPC online sleeping sensor.""" entity_id = f"{SENSOR_DOMAIN}.test_name_temperature" + monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", 1000) entry = await init_integration(hass, 2, sleep_period=1000) # Sensor should be created when device is online @@ -462,7 +463,7 @@ async def test_rpc_sleeping_sensor( ) # Make device online - mock_rpc_device.mock_update() + mock_rpc_device.mock_online() await hass.async_block_till_done() assert hass.states.get(entity_id).state == "22.9" @@ -501,6 +502,10 @@ async def test_rpc_restored_sleeping_sensor( # Make device online monkeypatch.setattr(mock_rpc_device, "initialized", True) + mock_rpc_device.mock_online() + await hass.async_block_till_done() + + # Mock update mock_rpc_device.mock_update() await hass.async_block_till_done() @@ -533,6 +538,10 @@ async def test_rpc_restored_sleeping_sensor_no_last_state( # Make device online monkeypatch.setattr(mock_rpc_device, "initialized", True) + mock_rpc_device.mock_online() + await hass.async_block_till_done() + + # Mock update mock_rpc_device.mock_update() await hass.async_block_till_done() @@ -583,19 +592,21 @@ async def test_rpc_sleeping_update_entity_service( hass: HomeAssistant, mock_rpc_device: Mock, entity_registry: EntityRegistry, + monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, ) -> None: """Test RPC sleeping device when the update_entity service is used.""" await async_setup_component(hass, "homeassistant", {}) entity_id = f"{SENSOR_DOMAIN}.test_name_temperature" + monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", 1000) await init_integration(hass, 2, sleep_period=1000) # Entity should be created when device is online assert hass.states.get(entity_id) is None # Make device online - mock_rpc_device.mock_update() + mock_rpc_device.mock_online() await hass.async_block_till_done() state = hass.states.get(entity_id) @@ -627,19 +638,25 @@ async def test_block_sleeping_update_entity_service( hass: HomeAssistant, mock_block_device: Mock, entity_registry: EntityRegistry, + monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, ) -> None: """Test block sleeping device when the update_entity service is used.""" await async_setup_component(hass, "homeassistant", {}) entity_id = f"{SENSOR_DOMAIN}.test_name_temperature" - await init_integration(hass, 1, sleep_period=1000) + monkeypatch.setitem( + mock_block_device.settings, + "sleep_mode", + {"period": 60, "unit": "m"}, + ) + await init_integration(hass, 1, sleep_period=3600) # Sensor should be created when device is online assert hass.states.get(entity_id) is None # Make device online - mock_block_device.mock_update() + mock_block_device.mock_online() await hass.async_block_till_done() assert hass.states.get(entity_id).state == "22.1" diff --git a/tests/components/shelly/test_update.py b/tests/components/shelly/test_update.py index 73320b2c65f..93b0f55c415 100644 --- a/tests/components/shelly/test_update.py +++ b/tests/components/shelly/test_update.py @@ -335,6 +335,7 @@ async def test_rpc_sleeping_update( monkeypatch: pytest.MonkeyPatch, ) -> None: """Test RPC sleeping device update entity.""" + monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", 1000) monkeypatch.setitem(mock_rpc_device.shelly, "ver", "1") monkeypatch.setitem( mock_rpc_device.status["sys"], @@ -350,7 +351,7 @@ async def test_rpc_sleeping_update( assert hass.states.get(entity_id) is None # Make device online - mock_rpc_device.mock_update() + mock_rpc_device.mock_online() await hass.async_block_till_done() state = hass.states.get(entity_id) @@ -411,6 +412,10 @@ async def test_rpc_restored_sleeping_update( # Make device online monkeypatch.setattr(mock_rpc_device, "initialized", True) + mock_rpc_device.mock_online() + await hass.async_block_till_done() + + # Mock update mock_rpc_device.mock_update() await hass.async_block_till_done() @@ -456,6 +461,10 @@ async def test_rpc_restored_sleeping_update_no_last_state( # Make device online monkeypatch.setattr(mock_rpc_device, "initialized", True) + mock_rpc_device.mock_online() + await hass.async_block_till_done() + + # Mock update mock_rpc_device.mock_update() await hass.async_block_till_done() From 6092894ce55ced2ab9c635a84a742eda0ac948b2 Mon Sep 17 00:00:00 2001 From: Arie Catsman <120491684+catsmanac@users.noreply.github.com> Date: Sun, 14 Apr 2024 17:41:00 +0200 Subject: [PATCH 567/967] For new installs make enphase_envoy phase entities default disabled (#115577) --- .../components/enphase_envoy/sensor.py | 2 + .../snapshots/test_diagnostics.ambr | 402 ++------------ .../enphase_envoy/snapshots/test_sensor.ambr | 498 ++---------------- 3 files changed, 98 insertions(+), 804 deletions(-) diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index 344bb47e025..df06502e94e 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -146,6 +146,7 @@ PRODUCTION_PHASE_SENSORS = { sensor, key=f"{sensor.key}_l{phase + 1}", translation_key=f"{sensor.translation_key}_phase", + entity_registry_enabled_default=False, on_phase=on_phase, translation_placeholders={"phase_name": f"l{phase + 1}"}, ) @@ -216,6 +217,7 @@ CONSUMPTION_PHASE_SENSORS = { sensor, key=f"{sensor.key}_l{phase + 1}", translation_key=f"{sensor.translation_key}_phase", + entity_registry_enabled_default=False, on_phase=on_phase, translation_placeholders={"phase_name": f"l{phase + 1}"}, ) diff --git a/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr b/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr index 51cda1cc478..c2ab51a7dbd 100644 --- a/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr +++ b/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr @@ -475,7 +475,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_current_power_production_l1', @@ -486,9 +486,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'kW', }), @@ -503,17 +500,7 @@ 'unique_id': '<>_production_l1', 'unit_of_measurement': 'kW', }), - 'state': dict({ - 'attributes': dict({ - 'device_class': 'power', - 'friendly_name': 'Envoy <> Current power production l1', - 'icon': 'mdi:flash', - 'state_class': 'measurement', - 'unit_of_measurement': 'kW', - }), - 'entity_id': 'sensor.envoy_<>_current_power_production_l1', - 'state': '1.234', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -527,7 +514,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_energy_production_today_l1', @@ -538,9 +525,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'kWh', }), @@ -555,17 +539,7 @@ 'unique_id': '<>_daily_production_l1', 'unit_of_measurement': 'kWh', }), - 'state': dict({ - 'attributes': dict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy <> Energy production today l1', - 'icon': 'mdi:flash', - 'state_class': 'total_increasing', - 'unit_of_measurement': 'kWh', - }), - 'entity_id': 'sensor.envoy_<>_energy_production_today_l1', - 'state': '1.233', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -577,7 +551,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_energy_production_last_seven_days_l1', @@ -588,9 +562,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'kWh', }), @@ -605,16 +576,7 @@ 'unique_id': '<>_seven_days_production_l1', 'unit_of_measurement': 'kWh', }), - 'state': dict({ - 'attributes': dict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy <> Energy production last seven days l1', - 'icon': 'mdi:flash', - 'unit_of_measurement': 'kWh', - }), - 'entity_id': 'sensor.envoy_<>_energy_production_last_seven_days_l1', - 'state': '1.231', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -628,7 +590,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_lifetime_energy_production_l1', @@ -639,9 +601,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'MWh', }), @@ -656,17 +615,7 @@ 'unique_id': '<>_lifetime_production_l1', 'unit_of_measurement': 'MWh', }), - 'state': dict({ - 'attributes': dict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy <> Lifetime energy production l1', - 'icon': 'mdi:flash', - 'state_class': 'total_increasing', - 'unit_of_measurement': 'MWh', - }), - 'entity_id': 'sensor.envoy_<>_lifetime_energy_production_l1', - 'state': '0.001232', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -680,7 +629,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_current_power_production_l2', @@ -691,9 +640,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'kW', }), @@ -708,17 +654,7 @@ 'unique_id': '<>_production_l2', 'unit_of_measurement': 'kW', }), - 'state': dict({ - 'attributes': dict({ - 'device_class': 'power', - 'friendly_name': 'Envoy <> Current power production l2', - 'icon': 'mdi:flash', - 'state_class': 'measurement', - 'unit_of_measurement': 'kW', - }), - 'entity_id': 'sensor.envoy_<>_current_power_production_l2', - 'state': '2.234', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -732,7 +668,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_energy_production_today_l2', @@ -743,9 +679,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'kWh', }), @@ -760,17 +693,7 @@ 'unique_id': '<>_daily_production_l2', 'unit_of_measurement': 'kWh', }), - 'state': dict({ - 'attributes': dict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy <> Energy production today l2', - 'icon': 'mdi:flash', - 'state_class': 'total_increasing', - 'unit_of_measurement': 'kWh', - }), - 'entity_id': 'sensor.envoy_<>_energy_production_today_l2', - 'state': '2.233', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -782,7 +705,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_energy_production_last_seven_days_l2', @@ -793,9 +716,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'kWh', }), @@ -810,16 +730,7 @@ 'unique_id': '<>_seven_days_production_l2', 'unit_of_measurement': 'kWh', }), - 'state': dict({ - 'attributes': dict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy <> Energy production last seven days l2', - 'icon': 'mdi:flash', - 'unit_of_measurement': 'kWh', - }), - 'entity_id': 'sensor.envoy_<>_energy_production_last_seven_days_l2', - 'state': '2.231', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -833,7 +744,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_lifetime_energy_production_l2', @@ -844,9 +755,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'MWh', }), @@ -861,17 +769,7 @@ 'unique_id': '<>_lifetime_production_l2', 'unit_of_measurement': 'MWh', }), - 'state': dict({ - 'attributes': dict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy <> Lifetime energy production l2', - 'icon': 'mdi:flash', - 'state_class': 'total_increasing', - 'unit_of_measurement': 'MWh', - }), - 'entity_id': 'sensor.envoy_<>_lifetime_energy_production_l2', - 'state': '0.002232', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -885,7 +783,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_current_power_production_l3', @@ -896,9 +794,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'kW', }), @@ -913,17 +808,7 @@ 'unique_id': '<>_production_l3', 'unit_of_measurement': 'kW', }), - 'state': dict({ - 'attributes': dict({ - 'device_class': 'power', - 'friendly_name': 'Envoy <> Current power production l3', - 'icon': 'mdi:flash', - 'state_class': 'measurement', - 'unit_of_measurement': 'kW', - }), - 'entity_id': 'sensor.envoy_<>_current_power_production_l3', - 'state': '3.234', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -937,7 +822,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_energy_production_today_l3', @@ -948,9 +833,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'kWh', }), @@ -965,17 +847,7 @@ 'unique_id': '<>_daily_production_l3', 'unit_of_measurement': 'kWh', }), - 'state': dict({ - 'attributes': dict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy <> Energy production today l3', - 'icon': 'mdi:flash', - 'state_class': 'total_increasing', - 'unit_of_measurement': 'kWh', - }), - 'entity_id': 'sensor.envoy_<>_energy_production_today_l3', - 'state': '3.233', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -987,7 +859,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_energy_production_last_seven_days_l3', @@ -998,9 +870,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'kWh', }), @@ -1015,16 +884,7 @@ 'unique_id': '<>_seven_days_production_l3', 'unit_of_measurement': 'kWh', }), - 'state': dict({ - 'attributes': dict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy <> Energy production last seven days l3', - 'icon': 'mdi:flash', - 'unit_of_measurement': 'kWh', - }), - 'entity_id': 'sensor.envoy_<>_energy_production_last_seven_days_l3', - 'state': '3.231', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -1038,7 +898,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_lifetime_energy_production_l3', @@ -1049,9 +909,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'MWh', }), @@ -1066,17 +923,7 @@ 'unique_id': '<>_lifetime_production_l3', 'unit_of_measurement': 'MWh', }), - 'state': dict({ - 'attributes': dict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy <> Lifetime energy production l3', - 'icon': 'mdi:flash', - 'state_class': 'total_increasing', - 'unit_of_measurement': 'MWh', - }), - 'entity_id': 'sensor.envoy_<>_lifetime_energy_production_l3', - 'state': '0.003232', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -1090,7 +937,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_current_power_consumption_l1', @@ -1101,9 +948,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'kW', }), @@ -1118,17 +962,7 @@ 'unique_id': '<>_consumption_l1', 'unit_of_measurement': 'kW', }), - 'state': dict({ - 'attributes': dict({ - 'device_class': 'power', - 'friendly_name': 'Envoy <> Current power consumption l1', - 'icon': 'mdi:flash', - 'state_class': 'measurement', - 'unit_of_measurement': 'kW', - }), - 'entity_id': 'sensor.envoy_<>_current_power_consumption_l1', - 'state': '1.324', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -1142,7 +976,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_energy_consumption_today_l1', @@ -1153,9 +987,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'kWh', }), @@ -1170,17 +1001,7 @@ 'unique_id': '<>_daily_consumption_l1', 'unit_of_measurement': 'kWh', }), - 'state': dict({ - 'attributes': dict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy <> Energy consumption today l1', - 'icon': 'mdi:flash', - 'state_class': 'total_increasing', - 'unit_of_measurement': 'kWh', - }), - 'entity_id': 'sensor.envoy_<>_energy_consumption_today_l1', - 'state': '1.323', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -1192,7 +1013,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_energy_consumption_last_seven_days_l1', @@ -1203,9 +1024,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'kWh', }), @@ -1220,16 +1038,7 @@ 'unique_id': '<>_seven_days_consumption_l1', 'unit_of_measurement': 'kWh', }), - 'state': dict({ - 'attributes': dict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy <> Energy consumption last seven days l1', - 'icon': 'mdi:flash', - 'unit_of_measurement': 'kWh', - }), - 'entity_id': 'sensor.envoy_<>_energy_consumption_last_seven_days_l1', - 'state': '1.321', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -1243,7 +1052,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_lifetime_energy_consumption_l1', @@ -1254,9 +1063,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'MWh', }), @@ -1271,17 +1077,7 @@ 'unique_id': '<>_lifetime_consumption_l1', 'unit_of_measurement': 'MWh', }), - 'state': dict({ - 'attributes': dict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy <> Lifetime energy consumption l1', - 'icon': 'mdi:flash', - 'state_class': 'total_increasing', - 'unit_of_measurement': 'MWh', - }), - 'entity_id': 'sensor.envoy_<>_lifetime_energy_consumption_l1', - 'state': '0.001322', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -1295,7 +1091,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_current_power_consumption_l2', @@ -1306,9 +1102,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'kW', }), @@ -1323,17 +1116,7 @@ 'unique_id': '<>_consumption_l2', 'unit_of_measurement': 'kW', }), - 'state': dict({ - 'attributes': dict({ - 'device_class': 'power', - 'friendly_name': 'Envoy <> Current power consumption l2', - 'icon': 'mdi:flash', - 'state_class': 'measurement', - 'unit_of_measurement': 'kW', - }), - 'entity_id': 'sensor.envoy_<>_current_power_consumption_l2', - 'state': '2.324', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -1347,7 +1130,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_energy_consumption_today_l2', @@ -1358,9 +1141,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'kWh', }), @@ -1375,17 +1155,7 @@ 'unique_id': '<>_daily_consumption_l2', 'unit_of_measurement': 'kWh', }), - 'state': dict({ - 'attributes': dict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy <> Energy consumption today l2', - 'icon': 'mdi:flash', - 'state_class': 'total_increasing', - 'unit_of_measurement': 'kWh', - }), - 'entity_id': 'sensor.envoy_<>_energy_consumption_today_l2', - 'state': '2.323', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -1397,7 +1167,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_energy_consumption_last_seven_days_l2', @@ -1408,9 +1178,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'kWh', }), @@ -1425,16 +1192,7 @@ 'unique_id': '<>_seven_days_consumption_l2', 'unit_of_measurement': 'kWh', }), - 'state': dict({ - 'attributes': dict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy <> Energy consumption last seven days l2', - 'icon': 'mdi:flash', - 'unit_of_measurement': 'kWh', - }), - 'entity_id': 'sensor.envoy_<>_energy_consumption_last_seven_days_l2', - 'state': '2.321', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -1448,7 +1206,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_lifetime_energy_consumption_l2', @@ -1459,9 +1217,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'MWh', }), @@ -1476,17 +1231,7 @@ 'unique_id': '<>_lifetime_consumption_l2', 'unit_of_measurement': 'MWh', }), - 'state': dict({ - 'attributes': dict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy <> Lifetime energy consumption l2', - 'icon': 'mdi:flash', - 'state_class': 'total_increasing', - 'unit_of_measurement': 'MWh', - }), - 'entity_id': 'sensor.envoy_<>_lifetime_energy_consumption_l2', - 'state': '0.002322', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -1500,7 +1245,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_current_power_consumption_l3', @@ -1511,9 +1256,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'kW', }), @@ -1528,17 +1270,7 @@ 'unique_id': '<>_consumption_l3', 'unit_of_measurement': 'kW', }), - 'state': dict({ - 'attributes': dict({ - 'device_class': 'power', - 'friendly_name': 'Envoy <> Current power consumption l3', - 'icon': 'mdi:flash', - 'state_class': 'measurement', - 'unit_of_measurement': 'kW', - }), - 'entity_id': 'sensor.envoy_<>_current_power_consumption_l3', - 'state': '3.324', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -1552,7 +1284,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_energy_consumption_today_l3', @@ -1563,9 +1295,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'kWh', }), @@ -1580,17 +1309,7 @@ 'unique_id': '<>_daily_consumption_l3', 'unit_of_measurement': 'kWh', }), - 'state': dict({ - 'attributes': dict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy <> Energy consumption today l3', - 'icon': 'mdi:flash', - 'state_class': 'total_increasing', - 'unit_of_measurement': 'kWh', - }), - 'entity_id': 'sensor.envoy_<>_energy_consumption_today_l3', - 'state': '3.323', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -1602,7 +1321,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_energy_consumption_last_seven_days_l3', @@ -1613,9 +1332,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'kWh', }), @@ -1630,16 +1346,7 @@ 'unique_id': '<>_seven_days_consumption_l3', 'unit_of_measurement': 'kWh', }), - 'state': dict({ - 'attributes': dict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy <> Energy consumption last seven days l3', - 'icon': 'mdi:flash', - 'unit_of_measurement': 'kWh', - }), - 'entity_id': 'sensor.envoy_<>_energy_consumption_last_seven_days_l3', - 'state': '3.321', - }), + 'state': None, }), dict({ 'entity': dict({ @@ -1653,7 +1360,7 @@ }), 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', 'device_class': None, - 'disabled_by': None, + 'disabled_by': 'integration', 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_<>_lifetime_energy_consumption_l3', @@ -1664,9 +1371,6 @@ ]), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': 'MWh', }), @@ -1681,17 +1385,7 @@ 'unique_id': '<>_lifetime_consumption_l3', 'unit_of_measurement': 'MWh', }), - 'state': dict({ - 'attributes': dict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy <> Lifetime energy consumption l3', - 'icon': 'mdi:flash', - 'state_class': 'total_increasing', - 'unit_of_measurement': 'MWh', - }), - 'entity_id': 'sensor.envoy_<>_lifetime_energy_consumption_l3', - 'state': '0.003322', - }), + 'state': None, }), dict({ 'entity': dict({ diff --git a/tests/components/enphase_envoy/snapshots/test_sensor.ambr b/tests/components/enphase_envoy/snapshots/test_sensor.ambr index 4024c43c655..cec9d5141cd 100644 --- a/tests/components/enphase_envoy/snapshots/test_sensor.ambr +++ b/tests/components/enphase_envoy/snapshots/test_sensor.ambr @@ -319,7 +319,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_current_power_production_l1', @@ -331,9 +331,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -358,7 +355,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_energy_production_today_l1', @@ -370,9 +367,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -395,7 +389,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_energy_production_last_seven_days_l1', @@ -407,9 +401,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -434,7 +425,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_lifetime_energy_production_l1', @@ -446,9 +437,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -473,7 +461,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_current_power_production_l2', @@ -485,9 +473,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -512,7 +497,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_energy_production_today_l2', @@ -524,9 +509,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -549,7 +531,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_energy_production_last_seven_days_l2', @@ -561,9 +543,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -588,7 +567,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_lifetime_energy_production_l2', @@ -600,9 +579,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -627,7 +603,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_current_power_production_l3', @@ -639,9 +615,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -666,7 +639,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_energy_production_today_l3', @@ -678,9 +651,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -703,7 +673,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_energy_production_last_seven_days_l3', @@ -715,9 +685,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -742,7 +709,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_lifetime_energy_production_l3', @@ -754,9 +721,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -781,7 +745,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_current_power_consumption_l1', @@ -793,9 +757,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -820,7 +781,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_energy_consumption_today_l1', @@ -832,9 +793,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -857,7 +815,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_energy_consumption_last_seven_days_l1', @@ -869,9 +827,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -896,7 +851,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_lifetime_energy_consumption_l1', @@ -908,9 +863,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -935,7 +887,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_current_power_consumption_l2', @@ -947,9 +899,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -974,7 +923,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_energy_consumption_today_l2', @@ -986,9 +935,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -1011,7 +957,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_energy_consumption_last_seven_days_l2', @@ -1023,9 +969,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -1050,7 +993,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_lifetime_energy_consumption_l2', @@ -1062,9 +1005,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -1089,7 +1029,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_current_power_consumption_l3', @@ -1101,9 +1041,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -1128,7 +1065,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_energy_consumption_today_l3', @@ -1140,9 +1077,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -1165,7 +1099,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_energy_consumption_last_seven_days_l3', @@ -1177,9 +1111,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -1204,7 +1135,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': None, + 'disabled_by': , 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.envoy_1234_lifetime_energy_consumption_l3', @@ -1216,9 +1147,6 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 3, - }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -3487,55 +3415,13 @@ }) # --- # name: test_sensor[sensor.envoy_1234_current_power_consumption_l1-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Envoy 1234 Current power consumption l1', - 'icon': 'mdi:flash', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_current_power_consumption_l1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '1.324', - }) + None # --- # name: test_sensor[sensor.envoy_1234_current_power_consumption_l2-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Envoy 1234 Current power consumption l2', - 'icon': 'mdi:flash', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_current_power_consumption_l2', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '2.324', - }) + None # --- # name: test_sensor[sensor.envoy_1234_current_power_consumption_l3-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Envoy 1234 Current power consumption l3', - 'icon': 'mdi:flash', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_current_power_consumption_l3', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '3.324', - }) + None # --- # name: test_sensor[sensor.envoy_1234_current_power_production-state] StateSnapshot({ @@ -3555,55 +3441,13 @@ }) # --- # name: test_sensor[sensor.envoy_1234_current_power_production_l1-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Envoy 1234 Current power production l1', - 'icon': 'mdi:flash', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_current_power_production_l1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '1.234', - }) + None # --- # name: test_sensor[sensor.envoy_1234_current_power_production_l2-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Envoy 1234 Current power production l2', - 'icon': 'mdi:flash', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_current_power_production_l2', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '2.234', - }) + None # --- # name: test_sensor[sensor.envoy_1234_current_power_production_l3-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Envoy 1234 Current power production l3', - 'icon': 'mdi:flash', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_current_power_production_l3', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '3.234', - }) + None # --- # name: test_sensor[sensor.envoy_1234_energy_consumption_last_seven_days-state] StateSnapshot({ @@ -3622,52 +3466,13 @@ }) # --- # name: test_sensor[sensor.envoy_1234_energy_consumption_last_seven_days_l1-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy 1234 Energy consumption last seven days l1', - 'icon': 'mdi:flash', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_energy_consumption_last_seven_days_l1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '1.321', - }) + None # --- # name: test_sensor[sensor.envoy_1234_energy_consumption_last_seven_days_l2-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy 1234 Energy consumption last seven days l2', - 'icon': 'mdi:flash', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_energy_consumption_last_seven_days_l2', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '2.321', - }) + None # --- # name: test_sensor[sensor.envoy_1234_energy_consumption_last_seven_days_l3-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy 1234 Energy consumption last seven days l3', - 'icon': 'mdi:flash', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_energy_consumption_last_seven_days_l3', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '3.321', - }) + None # --- # name: test_sensor[sensor.envoy_1234_energy_consumption_today-state] StateSnapshot({ @@ -3687,55 +3492,13 @@ }) # --- # name: test_sensor[sensor.envoy_1234_energy_consumption_today_l1-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy 1234 Energy consumption today l1', - 'icon': 'mdi:flash', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_energy_consumption_today_l1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '1.323', - }) + None # --- # name: test_sensor[sensor.envoy_1234_energy_consumption_today_l2-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy 1234 Energy consumption today l2', - 'icon': 'mdi:flash', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_energy_consumption_today_l2', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '2.323', - }) + None # --- # name: test_sensor[sensor.envoy_1234_energy_consumption_today_l3-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy 1234 Energy consumption today l3', - 'icon': 'mdi:flash', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_energy_consumption_today_l3', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '3.323', - }) + None # --- # name: test_sensor[sensor.envoy_1234_energy_production_last_seven_days-state] StateSnapshot({ @@ -3754,52 +3517,13 @@ }) # --- # name: test_sensor[sensor.envoy_1234_energy_production_last_seven_days_l1-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy 1234 Energy production last seven days l1', - 'icon': 'mdi:flash', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_energy_production_last_seven_days_l1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '1.231', - }) + None # --- # name: test_sensor[sensor.envoy_1234_energy_production_last_seven_days_l2-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy 1234 Energy production last seven days l2', - 'icon': 'mdi:flash', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_energy_production_last_seven_days_l2', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '2.231', - }) + None # --- # name: test_sensor[sensor.envoy_1234_energy_production_last_seven_days_l3-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy 1234 Energy production last seven days l3', - 'icon': 'mdi:flash', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_energy_production_last_seven_days_l3', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '3.231', - }) + None # --- # name: test_sensor[sensor.envoy_1234_energy_production_today-state] StateSnapshot({ @@ -3819,55 +3543,13 @@ }) # --- # name: test_sensor[sensor.envoy_1234_energy_production_today_l1-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy 1234 Energy production today l1', - 'icon': 'mdi:flash', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_energy_production_today_l1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '1.233', - }) + None # --- # name: test_sensor[sensor.envoy_1234_energy_production_today_l2-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy 1234 Energy production today l2', - 'icon': 'mdi:flash', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_energy_production_today_l2', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '2.233', - }) + None # --- # name: test_sensor[sensor.envoy_1234_energy_production_today_l3-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy 1234 Energy production today l3', - 'icon': 'mdi:flash', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_energy_production_today_l3', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '3.233', - }) + None # --- # name: test_sensor[sensor.envoy_1234_frequency_net_consumption_ct-state] None @@ -3951,55 +3633,13 @@ }) # --- # name: test_sensor[sensor.envoy_1234_lifetime_energy_consumption_l1-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy 1234 Lifetime energy consumption l1', - 'icon': 'mdi:flash', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_lifetime_energy_consumption_l1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0.001322', - }) + None # --- # name: test_sensor[sensor.envoy_1234_lifetime_energy_consumption_l2-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy 1234 Lifetime energy consumption l2', - 'icon': 'mdi:flash', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_lifetime_energy_consumption_l2', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0.002322', - }) + None # --- # name: test_sensor[sensor.envoy_1234_lifetime_energy_consumption_l3-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy 1234 Lifetime energy consumption l3', - 'icon': 'mdi:flash', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_lifetime_energy_consumption_l3', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0.003322', - }) + None # --- # name: test_sensor[sensor.envoy_1234_lifetime_energy_production-state] StateSnapshot({ @@ -4019,55 +3659,13 @@ }) # --- # name: test_sensor[sensor.envoy_1234_lifetime_energy_production_l1-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy 1234 Lifetime energy production l1', - 'icon': 'mdi:flash', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_lifetime_energy_production_l1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0.001232', - }) + None # --- # name: test_sensor[sensor.envoy_1234_lifetime_energy_production_l2-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy 1234 Lifetime energy production l2', - 'icon': 'mdi:flash', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_lifetime_energy_production_l2', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0.002232', - }) + None # --- # name: test_sensor[sensor.envoy_1234_lifetime_energy_production_l3-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Envoy 1234 Lifetime energy production l3', - 'icon': 'mdi:flash', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.envoy_1234_lifetime_energy_production_l3', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0.003232', - }) + None # --- # name: test_sensor[sensor.envoy_1234_lifetime_net_energy_consumption-state] StateSnapshot({ From 131edea576df941fea2f2c91560c13b297ec3049 Mon Sep 17 00:00:00 2001 From: Arie Catsman <120491684+catsmanac@users.noreply.github.com> Date: Sun, 14 Apr 2024 17:41:25 +0200 Subject: [PATCH 568/967] Replace lambda by attrgetter in enphase_envoy platform value_fn (#115569) --- .../components/enphase_envoy/binary_sensor.py | 5 +- .../components/enphase_envoy/number.py | 7 ++- .../components/enphase_envoy/sensor.py | 59 ++++++++++--------- 3 files changed, 37 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/enphase_envoy/binary_sensor.py b/homeassistant/components/enphase_envoy/binary_sensor.py index dfa619f07d8..dbd8498467f 100644 --- a/homeassistant/components/enphase_envoy/binary_sensor.py +++ b/homeassistant/components/enphase_envoy/binary_sensor.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +from operator import attrgetter from pyenphase import EnvoyEncharge, EnvoyEnpower @@ -36,7 +37,7 @@ ENCHARGE_SENSORS = ( translation_key="communicating", device_class=BinarySensorDeviceClass.CONNECTIVITY, entity_category=EntityCategory.DIAGNOSTIC, - value_fn=lambda encharge: encharge.communicating, + value_fn=attrgetter("communicating"), ), EnvoyEnchargeBinarySensorEntityDescription( key="dc_switch", @@ -60,7 +61,7 @@ ENPOWER_SENSORS = ( translation_key="communicating", device_class=BinarySensorDeviceClass.CONNECTIVITY, entity_category=EntityCategory.DIAGNOSTIC, - value_fn=lambda enpower: enpower.communicating, + value_fn=attrgetter("communicating"), ), EnvoyEnpowerBinarySensorEntityDescription( key="mains_oper_state", diff --git a/homeassistant/components/enphase_envoy/number.py b/homeassistant/components/enphase_envoy/number.py index 61d9aabb469..38bb18ad768 100644 --- a/homeassistant/components/enphase_envoy/number.py +++ b/homeassistant/components/enphase_envoy/number.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Awaitable, Callable from dataclasses import dataclass +from operator import attrgetter from typing import Any from pyenphase import Envoy, EnvoyDryContactSettings @@ -47,14 +48,14 @@ RELAY_ENTITIES = ( translation_key="cutoff_battery_level", device_class=NumberDeviceClass.BATTERY, entity_category=EntityCategory.CONFIG, - value_fn=lambda relay: relay.soc_low, + value_fn=attrgetter("soc_low"), ), EnvoyRelayNumberEntityDescription( key="soc_high", translation_key="restore_battery_level", device_class=NumberDeviceClass.BATTERY, entity_category=EntityCategory.CONFIG, - value_fn=lambda relay: relay.soc_high, + value_fn=attrgetter("soc_high"), ), ) @@ -63,7 +64,7 @@ STORAGE_RESERVE_SOC_ENTITY = EnvoyStorageSettingsNumberEntityDescription( translation_key="reserve_soc", native_unit_of_measurement=PERCENTAGE, device_class=NumberDeviceClass.BATTERY, - value_fn=lambda storage_settings: storage_settings.reserved_soc, + value_fn=attrgetter("reserved_soc"), update_fn=lambda envoy, value: envoy.set_reserve_soc(int(value)), ) diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index df06502e94e..13445d8897a 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -6,6 +6,7 @@ from collections.abc import Callable from dataclasses import dataclass, replace import datetime import logging +from operator import attrgetter from typing import TYPE_CHECKING from pyenphase import ( @@ -73,7 +74,7 @@ INVERTER_SENSORS = ( native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.POWER, - value_fn=lambda inverter: inverter.last_report_watts, + value_fn=attrgetter("last_report_watts"), ), EnvoyInverterSensorEntityDescription( key=LAST_REPORTED_KEY, @@ -102,7 +103,7 @@ PRODUCTION_SENSORS = ( device_class=SensorDeviceClass.POWER, suggested_unit_of_measurement=UnitOfPower.KILO_WATT, suggested_display_precision=3, - value_fn=lambda production: production.watts_now, + value_fn=attrgetter("watts_now"), on_phase=None, ), EnvoyProductionSensorEntityDescription( @@ -113,7 +114,7 @@ PRODUCTION_SENSORS = ( device_class=SensorDeviceClass.ENERGY, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, suggested_display_precision=2, - value_fn=lambda production: production.watt_hours_today, + value_fn=attrgetter("watt_hours_today"), on_phase=None, ), EnvoyProductionSensorEntityDescription( @@ -123,7 +124,7 @@ PRODUCTION_SENSORS = ( device_class=SensorDeviceClass.ENERGY, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, suggested_display_precision=1, - value_fn=lambda production: production.watt_hours_last_7_days, + value_fn=attrgetter("watt_hours_last_7_days"), on_phase=None, ), EnvoyProductionSensorEntityDescription( @@ -134,7 +135,7 @@ PRODUCTION_SENSORS = ( device_class=SensorDeviceClass.ENERGY, suggested_unit_of_measurement=UnitOfEnergy.MEGA_WATT_HOUR, suggested_display_precision=3, - value_fn=lambda production: production.watt_hours_lifetime, + value_fn=attrgetter("watt_hours_lifetime"), on_phase=None, ), ) @@ -173,7 +174,7 @@ CONSUMPTION_SENSORS = ( device_class=SensorDeviceClass.POWER, suggested_unit_of_measurement=UnitOfPower.KILO_WATT, suggested_display_precision=3, - value_fn=lambda consumption: consumption.watts_now, + value_fn=attrgetter("watts_now"), on_phase=None, ), EnvoyConsumptionSensorEntityDescription( @@ -184,7 +185,7 @@ CONSUMPTION_SENSORS = ( device_class=SensorDeviceClass.ENERGY, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, suggested_display_precision=2, - value_fn=lambda consumption: consumption.watt_hours_today, + value_fn=attrgetter("watt_hours_today"), on_phase=None, ), EnvoyConsumptionSensorEntityDescription( @@ -194,7 +195,7 @@ CONSUMPTION_SENSORS = ( device_class=SensorDeviceClass.ENERGY, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, suggested_display_precision=1, - value_fn=lambda consumption: consumption.watt_hours_last_7_days, + value_fn=attrgetter("watt_hours_last_7_days"), on_phase=None, ), EnvoyConsumptionSensorEntityDescription( @@ -205,7 +206,7 @@ CONSUMPTION_SENSORS = ( device_class=SensorDeviceClass.ENERGY, suggested_unit_of_measurement=UnitOfEnergy.MEGA_WATT_HOUR, suggested_display_precision=3, - value_fn=lambda consumption: consumption.watt_hours_lifetime, + value_fn=attrgetter("watt_hours_lifetime"), on_phase=None, ), ) @@ -247,7 +248,7 @@ CT_NET_CONSUMPTION_SENSORS = ( device_class=SensorDeviceClass.ENERGY, suggested_unit_of_measurement=UnitOfEnergy.MEGA_WATT_HOUR, suggested_display_precision=3, - value_fn=lambda ct: ct.energy_delivered, + value_fn=attrgetter("energy_delivered"), on_phase=None, ), EnvoyCTSensorEntityDescription( @@ -258,7 +259,7 @@ CT_NET_CONSUMPTION_SENSORS = ( device_class=SensorDeviceClass.ENERGY, suggested_unit_of_measurement=UnitOfEnergy.MEGA_WATT_HOUR, suggested_display_precision=3, - value_fn=lambda ct: ct.energy_received, + value_fn=attrgetter("energy_received"), on_phase=None, ), EnvoyCTSensorEntityDescription( @@ -269,7 +270,7 @@ CT_NET_CONSUMPTION_SENSORS = ( device_class=SensorDeviceClass.POWER, suggested_unit_of_measurement=UnitOfPower.KILO_WATT, suggested_display_precision=3, - value_fn=lambda ct: ct.active_power, + value_fn=attrgetter("active_power"), on_phase=None, ), EnvoyCTSensorEntityDescription( @@ -280,7 +281,7 @@ CT_NET_CONSUMPTION_SENSORS = ( device_class=SensorDeviceClass.FREQUENCY, suggested_display_precision=1, entity_registry_enabled_default=False, - value_fn=lambda ct: ct.frequency, + value_fn=attrgetter("frequency"), on_phase=None, ), EnvoyCTSensorEntityDescription( @@ -292,7 +293,7 @@ CT_NET_CONSUMPTION_SENSORS = ( suggested_unit_of_measurement=UnitOfElectricPotential.VOLT, suggested_display_precision=1, entity_registry_enabled_default=False, - value_fn=lambda ct: ct.voltage, + value_fn=attrgetter("voltage"), on_phase=None, ), EnvoyCTSensorEntityDescription( @@ -301,7 +302,7 @@ CT_NET_CONSUMPTION_SENSORS = ( device_class=SensorDeviceClass.ENUM, options=list(CtMeterStatus), entity_registry_enabled_default=False, - value_fn=lambda ct: ct.metering_status, + value_fn=attrgetter("metering_status"), on_phase=None, ), EnvoyCTSensorEntityDescription( @@ -337,7 +338,7 @@ CT_PRODUCTION_SENSORS = ( device_class=SensorDeviceClass.ENUM, options=list(CtMeterStatus), entity_registry_enabled_default=False, - value_fn=lambda ct: ct.metering_status, + value_fn=attrgetter("metering_status"), on_phase=None, ), EnvoyCTSensorEntityDescription( @@ -374,7 +375,7 @@ CT_STORAGE_SENSORS = ( device_class=SensorDeviceClass.ENERGY, suggested_unit_of_measurement=UnitOfEnergy.MEGA_WATT_HOUR, suggested_display_precision=3, - value_fn=lambda ct: ct.energy_delivered, + value_fn=attrgetter("energy_delivered"), on_phase=None, ), EnvoyCTSensorEntityDescription( @@ -385,7 +386,7 @@ CT_STORAGE_SENSORS = ( device_class=SensorDeviceClass.ENERGY, suggested_unit_of_measurement=UnitOfEnergy.MEGA_WATT_HOUR, suggested_display_precision=3, - value_fn=lambda ct: ct.energy_received, + value_fn=attrgetter("energy_received"), on_phase=None, ), EnvoyCTSensorEntityDescription( @@ -396,7 +397,7 @@ CT_STORAGE_SENSORS = ( device_class=SensorDeviceClass.POWER, suggested_unit_of_measurement=UnitOfPower.KILO_WATT, suggested_display_precision=3, - value_fn=lambda ct: ct.active_power, + value_fn=attrgetter("active_power"), on_phase=None, ), EnvoyCTSensorEntityDescription( @@ -408,7 +409,7 @@ CT_STORAGE_SENSORS = ( suggested_unit_of_measurement=UnitOfElectricPotential.VOLT, suggested_display_precision=1, entity_registry_enabled_default=False, - value_fn=lambda ct: ct.voltage, + value_fn=attrgetter("voltage"), on_phase=None, ), EnvoyCTSensorEntityDescription( @@ -417,7 +418,7 @@ CT_STORAGE_SENSORS = ( device_class=SensorDeviceClass.ENUM, options=list(CtMeterStatus), entity_registry_enabled_default=False, - value_fn=lambda ct: ct.metering_status, + value_fn=attrgetter("metering_status"), on_phase=None, ), EnvoyCTSensorEntityDescription( @@ -471,7 +472,7 @@ ENCHARGE_INVENTORY_SENSORS = ( key="temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, - value_fn=lambda encharge: encharge.temperature, + value_fn=attrgetter("temperature"), ), EnvoyEnchargeSensorEntityDescription( key=LAST_REPORTED_KEY, @@ -486,7 +487,7 @@ ENCHARGE_POWER_SENSORS = ( key="soc", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, - value_fn=lambda encharge: encharge.soc, + value_fn=attrgetter("soc"), ), EnvoyEnchargePowerSensorEntityDescription( key="apparent_power_mva", @@ -515,7 +516,7 @@ ENPOWER_SENSORS = ( key="temperature", native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, - value_fn=lambda enpower: enpower.temperature, + value_fn=attrgetter("temperature"), ), EnvoyEnpowerSensorEntityDescription( key=LAST_REPORTED_KEY, @@ -543,35 +544,35 @@ ENCHARGE_AGGREGATE_SENSORS = ( key="battery_level", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, - value_fn=lambda encharge: encharge.state_of_charge, + value_fn=attrgetter("state_of_charge"), ), EnvoyEnchargeAggregateSensorEntityDescription( key="reserve_soc", translation_key="reserve_soc", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, - value_fn=lambda encharge: encharge.reserve_state_of_charge, + value_fn=attrgetter("reserve_state_of_charge"), ), EnvoyEnchargeAggregateSensorEntityDescription( key="available_energy", translation_key="available_energy", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, device_class=SensorDeviceClass.ENERGY, - value_fn=lambda encharge: encharge.available_energy, + value_fn=attrgetter("available_energy"), ), EnvoyEnchargeAggregateSensorEntityDescription( key="reserve_energy", translation_key="reserve_energy", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, device_class=SensorDeviceClass.ENERGY, - value_fn=lambda encharge: encharge.backup_reserve, + value_fn=attrgetter("backup_reserve"), ), EnvoyEnchargeAggregateSensorEntityDescription( key="max_capacity", translation_key="max_capacity", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, device_class=SensorDeviceClass.ENERGY, - value_fn=lambda encharge: encharge.max_available_capacity, + value_fn=attrgetter("max_available_capacity"), ), ) From 5f00b9207d559af81dce4a4e2a96bb5bf2ce03de Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 14 Apr 2024 10:47:13 -0500 Subject: [PATCH 569/967] Small cleanups to script (#115565) --- homeassistant/components/script/__init__.py | 37 ++++++++++++--------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index 6f7974dcb04..f83aed68590 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -7,7 +7,7 @@ import asyncio from dataclasses import dataclass from functools import cached_property import logging -from typing import Any, cast +from typing import TYPE_CHECKING, Any, cast import voluptuous as vol @@ -39,7 +39,6 @@ from homeassistant.core import ( SupportsResponse, callback, ) -from homeassistant.helpers import entity_registry as er import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import make_entity_service_schema from homeassistant.helpers.entity import ToggleEntity @@ -532,15 +531,16 @@ class ScriptEntity(BaseScriptEntity, RestoreEntity): @property def extra_state_attributes(self): """Return the state attributes.""" + script = self.script attrs = { - ATTR_LAST_TRIGGERED: self.script.last_triggered, - ATTR_MODE: self.script.script_mode, - ATTR_CUR: self.script.runs, + ATTR_LAST_TRIGGERED: script.last_triggered, + ATTR_MODE: script.script_mode, + ATTR_CUR: script.runs, } - if self.script.supports_max: - attrs[ATTR_MAX] = self.script.max_runs - if self.script.last_action: - attrs[ATTR_LAST_ACTION] = self.script.last_action + if script.supports_max: + attrs[ATTR_MAX] = script.max_runs + if script.last_action: + attrs[ATTR_LAST_ACTION] = script.last_action return attrs @property @@ -660,9 +660,13 @@ class ScriptEntity(BaseScriptEntity, RestoreEntity): async def async_added_to_hass(self) -> None: """Restore last triggered on startup and register service.""" + if TYPE_CHECKING: + assert self.unique_id is not None + assert self.registry_entry is not None - unique_id = cast(str, self.unique_id) - self.hass.services.async_register( + unique_id = self.unique_id + hass = self.hass + hass.services.async_register( DOMAIN, unique_id, self._service_handler, @@ -672,15 +676,16 @@ class ScriptEntity(BaseScriptEntity, RestoreEntity): # Register the service description service_desc = { - CONF_NAME: cast(er.RegistryEntry, self.registry_entry).name or self.name, + CONF_NAME: self.registry_entry.name or self.name, CONF_DESCRIPTION: self.description, CONF_FIELDS: self.fields, } - async_set_service_schema(self.hass, DOMAIN, unique_id, service_desc) + async_set_service_schema(hass, DOMAIN, unique_id, service_desc) - if state := await self.async_get_last_state(): - if last_triggered := state.attributes.get("last_triggered"): - self.script.last_triggered = parse_datetime(last_triggered) + if (state := await self.async_get_last_state()) and ( + last_triggered := state.attributes.get("last_triggered") + ): + self.script.last_triggered = parse_datetime(last_triggered) async def async_will_remove_from_hass(self): """Stop script and remove service when it will be removed from HA.""" From 7cfd6a04d39a9d2033b74ed7d32f8f3149f1b15c Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sun, 14 Apr 2024 19:22:42 +0200 Subject: [PATCH 570/967] Modbus: Bump pymodbus v3.6.8 (#115574) --- homeassistant/components/modbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/modbus/manifest.json b/homeassistant/components/modbus/manifest.json index 0fe8c7bc42d..5635adc9392 100644 --- a/homeassistant/components/modbus/manifest.json +++ b/homeassistant/components/modbus/manifest.json @@ -6,5 +6,5 @@ "iot_class": "local_polling", "loggers": ["pymodbus"], "quality_scale": "platinum", - "requirements": ["pymodbus==3.6.7"] + "requirements": ["pymodbus==3.6.8"] } diff --git a/requirements_all.txt b/requirements_all.txt index 14f5749a02a..672fac5cff9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1980,7 +1980,7 @@ pymitv==1.4.3 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==3.6.7 +pymodbus==3.6.8 # homeassistant.components.monoprice pymonoprice==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a58259c096c..eb12f451178 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1543,7 +1543,7 @@ pymeteoclimatic==0.1.0 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==3.6.7 +pymodbus==3.6.8 # homeassistant.components.monoprice pymonoprice==0.4 From 269429aa0c6dcd0ffa1fe5f2c87f08a542fac31b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 14 Apr 2024 13:38:28 -0500 Subject: [PATCH 571/967] Only calculate the tplink emeter values once per update cycle (#115587) The sensor platform has to read the native_value multiple times during the state write cycle which means the integration calculated the value multiple times. Switch to using _attr_native_value to ensure the calculations in the library are only done once per state write. To demonstrate this issue, + _LOGGER.warning("Fetch name value for %s", self.entity_id) was added to `def native_value`: ``` 2024-04-14 06:58:52.506 WARNING (MainThread) [homeassistant.components.tplink.sensor] Fetch name value for sensor.kasa_smart_plug_542b_0_kasa_smart_plug_542b_0_current_consumption 2024-04-14 06:58:52.506 WARNING (MainThread) [homeassistant.components.tplink.sensor] Fetch name value for sensor.kasa_smart_plug_542b_0_kasa_smart_plug_542b_0_total_consumption 2024-04-14 06:58:52.507 WARNING (MainThread) [homeassistant.components.tplink.sensor] Fetch name value for sensor.kasa_smart_plug_542b_0_kasa_smart_plug_542b_0_today_s_consumption 2024-04-14 06:58:52.507 WARNING (MainThread) [homeassistant.components.tplink.sensor] Fetch name value for sensor.kasa_smart_plug_542b_0_kasa_smart_plug_542b_0_voltage 2024-04-14 06:58:52.508 WARNING (MainThread) [homeassistant.components.tplink.sensor] Fetch name value for sensor.kasa_smart_plug_542b_0_kasa_smart_plug_542b_0_current 2024-04-14 06:58:52.509 WARNING (MainThread) [homeassistant.components.tplink.sensor] Fetch name value for sensor.kasa_smart_plug_542b_0_kasa_smart_plug_542b_0_current_consumption 2024-04-14 06:58:52.510 WARNING (MainThread) [homeassistant.components.tplink.sensor] Fetch name value for sensor.kasa_smart_plug_542b_0_kasa_smart_plug_542b_0_total_consumption 2024-04-14 06:58:52.510 WARNING (MainThread) [homeassistant.components.tplink.sensor] Fetch name value for sensor.kasa_smart_plug_542b_0_kasa_smart_plug_542b_0_today_s_consumption 2024-04-14 06:58:52.510 WARNING (MainThread) [homeassistant.components.tplink.sensor] Fetch name value for sensor.kasa_smart_plug_542b_0_kasa_smart_plug_542b_0_voltage 2024-04-14 06:58:52.510 WARNING (MainThread) [homeassistant.components.tplink.sensor] Fetch name value for sensor.kasa_smart_plug_542b_0_kasa_smart_plug_542b_0_current ``` --- homeassistant/components/tplink/sensor.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/tplink/sensor.py b/homeassistant/components/tplink/sensor.py index 1f6b07365b5..d7563dd0401 100644 --- a/homeassistant/components/tplink/sensor.py +++ b/homeassistant/components/tplink/sensor.py @@ -21,7 +21,7 @@ from homeassistant.const import ( UnitOfEnergy, UnitOfPower, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import legacy_device_id @@ -171,8 +171,17 @@ class SmartPlugSensor(CoordinatedTPLinkEntity, SensorEntity): else: assert description.device_class self._attr_translation_key = f"{description.device_class.value}_child" + self._async_update_attrs() - @property - def native_value(self) -> float | None: - """Return the sensors state.""" - return async_emeter_from_device(self.device, self.entity_description) + @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 + ) + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._async_update_attrs() + super()._handle_coordinator_update() From e08301f362fde2e7daa781cd4c87f7ae48d76c94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20H=C3=B6rsken?= Date: Sun, 14 Apr 2024 23:11:42 +0200 Subject: [PATCH 572/967] Move Alexa entity id generation into abstract config class (#115593) This makes it possible to change the id schema in implementations. --- homeassistant/components/alexa/config.py | 5 +++++ homeassistant/components/alexa/entities.py | 7 +------ homeassistant/components/alexa/state_report.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/alexa/config.py b/homeassistant/components/alexa/config.py index fb589dde566..0801a32a607 100644 --- a/homeassistant/components/alexa/config.py +++ b/homeassistant/components/alexa/config.py @@ -13,6 +13,7 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.storage import Store from .const import DOMAIN +from .entities import TRANSLATION_TABLE from .state_report import async_enable_proactive_mode STORE_AUTHORIZED = "authorized" @@ -101,6 +102,10 @@ class AbstractConfig(ABC): """If an entity should be exposed.""" return False + def generate_alexa_id(self, entity_id: str) -> str: + """Return the alexa ID for an entity ID.""" + return entity_id.replace(".", "#").translate(TRANSLATION_TABLE) + @callback def async_invalidate_access_token(self) -> None: """Invalidate access token.""" diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 240f676b5f3..ca7b78f7ff5 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -259,11 +259,6 @@ class DisplayCategory: WEARABLE = "WEARABLE" -def generate_alexa_id(entity_id: str) -> str: - """Return the alexa ID for an entity ID.""" - return entity_id.replace(".", "#").translate(TRANSLATION_TABLE) - - class AlexaEntity: """An adaptation of an entity, expressed in Alexa's terms. @@ -298,7 +293,7 @@ class AlexaEntity: def alexa_id(self) -> str: """Return the Alexa API entity id.""" - return generate_alexa_id(self.entity.entity_id) + return self.config.generate_alexa_id(self.entity.entity_id) def display_categories(self) -> list[str] | None: """Return a list of display categories.""" diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index 24d750e7cb7..dc6c8ee3186 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -41,7 +41,7 @@ from .const import ( Cause, ) from .diagnostics import async_redact_auth_data -from .entities import ENTITY_ADAPTERS, AlexaEntity, generate_alexa_id +from .entities import ENTITY_ADAPTERS, AlexaEntity from .errors import AlexaInvalidEndpointError, NoTokenAvailable, RequireRelink if TYPE_CHECKING: @@ -492,7 +492,7 @@ async def async_send_delete_message( if domain not in ENTITY_ADAPTERS: continue - endpoints.append({"endpointId": generate_alexa_id(entity_id)}) + endpoints.append({"endpointId": config.generate_alexa_id(entity_id)}) payload: dict[str, Any] = { "endpoints": endpoints, From 6422bc4c19250397a41d1592cc3339c9568aec7c Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 15 Apr 2024 00:26:06 +0200 Subject: [PATCH 573/967] Set follow_imports to normal [mypy] (#115521) --- mypy.ini | 2 +- script/hassfest/mypy_config.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mypy.ini b/mypy.ini index 3e0419be269..0ce41821f51 100644 --- a/mypy.ini +++ b/mypy.ini @@ -6,7 +6,7 @@ python_version = 3.12 plugins = pydantic.mypy show_error_codes = true -follow_imports = silent +follow_imports = normal local_partial_types = true strict_equality = true no_implicit_optional = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 76fe47837e4..40d2c9718d6 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -34,7 +34,7 @@ GENERAL_SETTINGS: Final[dict[str, str]] = { "python_version": ".".join(str(x) for x in REQUIRED_PYTHON_VER[:2]), "plugins": "pydantic.mypy", "show_error_codes": "true", - "follow_imports": "silent", + "follow_imports": "normal", # Enable some checks globally. "local_partial_types": "true", "strict_equality": "true", From d40fc613aa7e9462abd4832d1ee2e8f8f2b9398d Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Sun, 14 Apr 2024 19:39:07 -0400 Subject: [PATCH 574/967] Bump soco to 0.30.3 (#115607) bump soco to 0.30.3 --- homeassistant/components/sonos/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index b6375eb7f16..ec5ef90a0c1 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -8,7 +8,7 @@ "documentation": "https://www.home-assistant.io/integrations/sonos", "iot_class": "local_push", "loggers": ["soco"], - "requirements": ["soco==0.30.2", "sonos-websocket==0.1.3"], + "requirements": ["soco==0.30.3", "sonos-websocket==0.1.3"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:ZonePlayer:1" diff --git a/requirements_all.txt b/requirements_all.txt index 672fac5cff9..1c9bc045a3c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2572,7 +2572,7 @@ smhi-pkg==1.0.16 snapcast==2.3.6 # homeassistant.components.sonos -soco==0.30.2 +soco==0.30.3 # homeassistant.components.solaredge_local solaredge-local==0.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index eb12f451178..b663d78085f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1982,7 +1982,7 @@ smhi-pkg==1.0.16 snapcast==2.3.6 # homeassistant.components.sonos -soco==0.30.2 +soco==0.30.3 # homeassistant.components.solaredge solaredge==0.0.2 From 5e1de6842da9602d651d4eb492e132deac2f208b Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Mon, 15 Apr 2024 09:48:22 +1000 Subject: [PATCH 575/967] Fix Teslemetry sensor values (#115571) --- homeassistant/components/teslemetry/sensor.py | 5 + .../teslemetry/snapshots/test_sensor.ambr | 100 +++++++++--------- 2 files changed, 55 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/teslemetry/sensor.py b/homeassistant/components/teslemetry/sensor.py index 6284a0e5368..cced1090e2a 100644 --- a/homeassistant/components/teslemetry/sensor.py +++ b/homeassistant/components/teslemetry/sensor.py @@ -449,6 +449,11 @@ class TeslemetryVehicleSensorEntity(TeslemetryVehicleEntity, SensorEntity): """Initialize the sensor.""" super().__init__(vehicle, description.key) + @property + def native_value(self) -> StateType: + """Return the state of the sensor.""" + return self._value + class TeslemetryVehicleTimeSensorEntity(TeslemetryVehicleEntity, SensorEntity): """Base class for Teslemetry vehicle metric sensors.""" diff --git a/tests/components/teslemetry/snapshots/test_sensor.ambr b/tests/components/teslemetry/snapshots/test_sensor.ambr index fad04d341c9..81142e40901 100644 --- a/tests/components/teslemetry/snapshots/test_sensor.ambr +++ b/tests/components/teslemetry/snapshots/test_sensor.ambr @@ -757,7 +757,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '77', }) # --- # name: test_sensors[sensor.test_battery_level-statealt] @@ -770,7 +770,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '77', }) # --- # name: test_sensors[sensor.test_battery_range-entry] @@ -816,7 +816,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '266.87', }) # --- # name: test_sensors[sensor.test_battery_range-statealt] @@ -829,7 +829,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '266.87', }) # --- # name: test_sensors[sensor.test_charge_cable-entry] @@ -875,7 +875,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'IEC', }) # --- # name: test_sensors[sensor.test_charge_cable-statealt] @@ -888,7 +888,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'IEC', }) # --- # name: test_sensors[sensor.test_charge_energy_added-entry] @@ -934,7 +934,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0', }) # --- # name: test_sensors[sensor.test_charge_energy_added-statealt] @@ -947,7 +947,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0', }) # --- # name: test_sensors[sensor.test_charge_rate-entry] @@ -993,7 +993,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0', }) # --- # name: test_sensors[sensor.test_charge_rate-statealt] @@ -1006,7 +1006,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0', }) # --- # name: test_sensors[sensor.test_charger_current-entry] @@ -1052,7 +1052,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0', }) # --- # name: test_sensors[sensor.test_charger_current-statealt] @@ -1065,7 +1065,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0', }) # --- # name: test_sensors[sensor.test_charger_power-entry] @@ -1111,7 +1111,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0', }) # --- # name: test_sensors[sensor.test_charger_power-statealt] @@ -1124,7 +1124,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0', }) # --- # name: test_sensors[sensor.test_charger_voltage-entry] @@ -1170,7 +1170,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '2', }) # --- # name: test_sensors[sensor.test_charger_voltage-statealt] @@ -1183,7 +1183,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '2', }) # --- # name: test_sensors[sensor.test_charging-entry] @@ -1229,7 +1229,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'Stopped', }) # --- # name: test_sensors[sensor.test_charging-statealt] @@ -1242,7 +1242,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'Stopped', }) # --- # name: test_sensors[sensor.test_distance_to_arrival-entry] @@ -1288,7 +1288,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0.039491', }) # --- # name: test_sensors[sensor.test_distance_to_arrival-statealt] @@ -1301,7 +1301,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0', }) # --- # name: test_sensors[sensor.test_driver_temperature_setting-entry] @@ -1347,7 +1347,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '22', }) # --- # name: test_sensors[sensor.test_driver_temperature_setting-statealt] @@ -1360,7 +1360,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '22', }) # --- # name: test_sensors[sensor.test_estimate_battery_range-entry] @@ -1406,7 +1406,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '275.04', }) # --- # name: test_sensors[sensor.test_estimate_battery_range-statealt] @@ -1419,7 +1419,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '275.04', }) # --- # name: test_sensors[sensor.test_fast_charger_type-entry] @@ -1465,7 +1465,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'ACSingleWireCAN', }) # --- # name: test_sensors[sensor.test_fast_charger_type-statealt] @@ -1478,7 +1478,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'ACSingleWireCAN', }) # --- # name: test_sensors[sensor.test_ideal_battery_range-entry] @@ -1524,7 +1524,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '266.87', }) # --- # name: test_sensors[sensor.test_ideal_battery_range-statealt] @@ -1537,7 +1537,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '266.87', }) # --- # name: test_sensors[sensor.test_inside_temperature-entry] @@ -1583,7 +1583,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '29.8', }) # --- # name: test_sensors[sensor.test_inside_temperature-statealt] @@ -1596,7 +1596,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '29.8', }) # --- # name: test_sensors[sensor.test_odometer-entry] @@ -1642,7 +1642,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '6481.019282', }) # --- # name: test_sensors[sensor.test_odometer-statealt] @@ -1655,7 +1655,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '6481.019282', }) # --- # name: test_sensors[sensor.test_outside_temperature-entry] @@ -1701,7 +1701,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '30', }) # --- # name: test_sensors[sensor.test_outside_temperature-statealt] @@ -1714,7 +1714,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '30', }) # --- # name: test_sensors[sensor.test_passenger_temperature_setting-entry] @@ -1760,7 +1760,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '22', }) # --- # name: test_sensors[sensor.test_passenger_temperature_setting-statealt] @@ -1773,7 +1773,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '22', }) # --- # name: test_sensors[sensor.test_power-entry] @@ -1819,7 +1819,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '-7', }) # --- # name: test_sensors[sensor.test_power-statealt] @@ -1832,7 +1832,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '-7', }) # --- # name: test_sensors[sensor.test_shift_state-entry] @@ -2177,7 +2177,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '2.775', }) # --- # name: test_sensors[sensor.test_tire_pressure_front_left-statealt] @@ -2190,7 +2190,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '2.775', }) # --- # name: test_sensors[sensor.test_tire_pressure_front_right-entry] @@ -2236,7 +2236,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '2.8', }) # --- # name: test_sensors[sensor.test_tire_pressure_front_right-statealt] @@ -2249,7 +2249,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '2.8', }) # --- # name: test_sensors[sensor.test_tire_pressure_rear_left-entry] @@ -2295,7 +2295,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '2.775', }) # --- # name: test_sensors[sensor.test_tire_pressure_rear_left-statealt] @@ -2308,7 +2308,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '2.775', }) # --- # name: test_sensors[sensor.test_tire_pressure_rear_right-entry] @@ -2354,7 +2354,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '2.775', }) # --- # name: test_sensors[sensor.test_tire_pressure_rear_right-statealt] @@ -2367,7 +2367,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '2.775', }) # --- # name: test_sensors[sensor.test_traffic_delay-entry] @@ -2413,7 +2413,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0', }) # --- # name: test_sensors[sensor.test_traffic_delay-statealt] @@ -2426,7 +2426,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0', }) # --- # name: test_sensors[sensor.test_usable_battery_level-entry] @@ -2472,7 +2472,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '77', }) # --- # name: test_sensors[sensor.test_usable_battery_level-statealt] @@ -2485,7 +2485,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '77', }) # --- # name: test_sensors[sensor.wall_connector_fault_state_code-entry] From b1bd9dc22cde0af241d217c2a36a773442fcb6b8 Mon Sep 17 00:00:00 2001 From: Shawn Weeks Date: Sun, 14 Apr 2024 21:43:11 -0500 Subject: [PATCH 576/967] Bump emulated-roku to 0.3.0 to fix Sofabaton Support (#115452) --- homeassistant/components/emulated_roku/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/emulated_roku/manifest.json b/homeassistant/components/emulated_roku/manifest.json index 739f3b04ec0..214658b7c0e 100644 --- a/homeassistant/components/emulated_roku/manifest.json +++ b/homeassistant/components/emulated_roku/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/emulated_roku", "iot_class": "local_push", "loggers": ["emulated_roku"], - "requirements": ["emulated-roku==0.2.1"] + "requirements": ["emulated-roku==0.3.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1c9bc045a3c..5a1ad65fd8b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -795,7 +795,7 @@ elvia==0.1.0 emoji==2.8.0 # homeassistant.components.emulated_roku -emulated-roku==0.2.1 +emulated-roku==0.3.0 # homeassistant.components.huisbaasje energyflip-client==0.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b663d78085f..5cfbf1e80a3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -652,7 +652,7 @@ elmax-api==0.0.4 elvia==0.1.0 # homeassistant.components.emulated_roku -emulated-roku==0.2.1 +emulated-roku==0.3.0 # homeassistant.components.huisbaasje energyflip-client==0.2.2 From 15ecd3ae31a150ea47a2b22d0ecc21a9146d1d10 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 15 Apr 2024 06:12:09 -0500 Subject: [PATCH 577/967] Fix flaky zwave update entity delay test (#115552) The test assumed the node updates would happen in a specific order but they can switch order based on timing. Adjust to check to make sure all the nodes are called but make it order independent --- tests/components/zwave_js/test_update.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/components/zwave_js/test_update.py b/tests/components/zwave_js/test_update.py index c5cfba18569..338d1511fc3 100644 --- a/tests/components/zwave_js/test_update.py +++ b/tests/components/zwave_js/test_update.py @@ -650,20 +650,25 @@ async def test_update_entity_delay( assert len(client.async_send_command.call_args_list) == 2 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) + + nodes: set[int] = set() assert len(client.async_send_command.call_args_list) == 3 args = client.async_send_command.call_args_list[2][0][0] assert args["command"] == "controller.get_available_firmware_updates" - assert args["nodeId"] == ge_in_wall_dimmer_switch.node_id + nodes.add(args["nodeId"]) async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert len(client.async_send_command.call_args_list) == 4 args = client.async_send_command.call_args_list[3][0][0] assert args["command"] == "controller.get_available_firmware_updates" - assert args["nodeId"] == zen_31.node_id + nodes.add(args["nodeId"]) + + assert len(nodes) == 2 + assert nodes == {ge_in_wall_dimmer_switch.node_id, zen_31.node_id} async def test_update_entity_partial_restore_data( From 3963b3994b14ccf5aa37bd9755e88fed7355ba72 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 15 Apr 2024 06:42:28 -0500 Subject: [PATCH 578/967] Small cleanups to the rate limit helper (#115621) --- homeassistant/helpers/ratelimit.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/helpers/ratelimit.py b/homeassistant/helpers/ratelimit.py index 516d4134f76..020c7c3a0d3 100644 --- a/homeassistant/helpers/ratelimit.py +++ b/homeassistant/helpers/ratelimit.py @@ -30,7 +30,7 @@ class KeyedRateLimit: @callback def async_has_timer(self, key: Hashable) -> bool: """Check if a rate limit timer is running.""" - return bool(self._rate_limit_timers and key in self._rate_limit_timers) + return key in self._rate_limit_timers @callback def async_triggered(self, key: Hashable, now: float | None = None) -> None: @@ -41,10 +41,8 @@ class KeyedRateLimit: @callback def async_cancel_timer(self, key: Hashable) -> None: """Cancel a rate limit time that will call the action.""" - if not self._rate_limit_timers or key not in self._rate_limit_timers: - return - - self._rate_limit_timers.pop(key).cancel() + if handle := self._rate_limit_timers.pop(key, None): + handle.cancel() @callback def async_remove(self) -> None: From 881e201a152a114bf23f39c29cad6f90ab6a9838 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 15 Apr 2024 14:12:26 +0200 Subject: [PATCH 579/967] Set platform for mypy (#115638) --- mypy.ini | 1 + script/hassfest/mypy_config.py | 1 + 2 files changed, 2 insertions(+) diff --git a/mypy.ini b/mypy.ini index 0ce41821f51..546ae52f972 100644 --- a/mypy.ini +++ b/mypy.ini @@ -4,6 +4,7 @@ [mypy] python_version = 3.12 +platform = linux plugins = pydantic.mypy show_error_codes = true follow_imports = normal diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 40d2c9718d6..fab3d5fcd7f 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -32,6 +32,7 @@ HEADER: Final = """ GENERAL_SETTINGS: Final[dict[str, str]] = { "python_version": ".".join(str(x) for x in REQUIRED_PYTHON_VER[:2]), + "platform": "linux", "plugins": "pydantic.mypy", "show_error_codes": "true", "follow_imports": "normal", From 9f852c6a5811df1e59d25fea28c10be4f8fd1c2e Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Mon, 15 Apr 2024 10:06:44 -0400 Subject: [PATCH 580/967] Bump vacuum-map-parser-roborock to 0.1.2 (#115579) bump to 0.1.2 --- 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 d03aa68f1a6..0646f8ee083 100644 --- a/homeassistant/components/roborock/manifest.json +++ b/homeassistant/components/roborock/manifest.json @@ -8,6 +8,6 @@ "loggers": ["roborock"], "requirements": [ "python-roborock==2.0.0", - "vacuum-map-parser-roborock==0.1.1" + "vacuum-map-parser-roborock==0.1.2" ] } diff --git a/requirements_all.txt b/requirements_all.txt index 5a1ad65fd8b..67d731ab587 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2796,7 +2796,7 @@ url-normalize==1.4.3 uvcclient==0.11.0 # homeassistant.components.roborock -vacuum-map-parser-roborock==0.1.1 +vacuum-map-parser-roborock==0.1.2 # homeassistant.components.vallox vallox-websocket-api==5.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5cfbf1e80a3..5de1f7f5164 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2155,7 +2155,7 @@ url-normalize==1.4.3 uvcclient==0.11.0 # homeassistant.components.roborock -vacuum-map-parser-roborock==0.1.1 +vacuum-map-parser-roborock==0.1.2 # homeassistant.components.vallox vallox-websocket-api==5.1.1 From dbc5109fd839269b84b5a6c841bebae62420268d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 15 Apr 2024 10:42:18 -0500 Subject: [PATCH 581/967] Avoid update calls in state writes when attributes are empty (#115624) --- homeassistant/helpers/entity.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index fb071d438b1..20948a7130a 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -1052,8 +1052,10 @@ class Entity( available = self.available # only call self.available once per update cycle state = self._stringify_state(available) if available: - attr.update(self.state_attributes or {}) - attr.update(self.extra_state_attributes or {}) + if state_attributes := self.state_attributes: + attr.update(state_attributes) + if extra_state_attributes := self.extra_state_attributes: + attr.update(extra_state_attributes) if (unit_of_measurement := self.unit_of_measurement) is not None: attr[ATTR_UNIT_OF_MEASUREMENT] = unit_of_measurement From 2ec588d2a6e391089687bffce307d64c07e2aaa6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 15 Apr 2024 13:40:31 -0500 Subject: [PATCH 582/967] Migrate websocket_api sensor to use shorthand attrs (#115620) --- .../components/websocket_api/sensor.py | 22 +++++-------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/websocket_api/sensor.py b/homeassistant/components/websocket_api/sensor.py index 7d668466bc2..4d874bca74e 100644 --- a/homeassistant/components/websocket_api/sensor.py +++ b/homeassistant/components/websocket_api/sensor.py @@ -30,9 +30,12 @@ async def async_setup_platform( class APICount(SensorEntity): """Entity to represent how many people are connected to the stream API.""" + _attr_name = "Connected clients" + _attr_native_unit_of_measurement = "clients" + def __init__(self) -> None: """Initialize the API count.""" - self.count = 0 + self._attr_native_value = 0 async def async_added_to_hass(self) -> None: """Handle addition to hass.""" @@ -47,22 +50,7 @@ class APICount(SensorEntity): ) ) - @property - def name(self) -> str: - """Return name of entity.""" - return "Connected clients" - - @property - def native_value(self) -> int: - """Return current API count.""" - return self.count - - @property - def native_unit_of_measurement(self) -> str: - """Return the unit of measurement.""" - return "clients" - @callback def _update_count(self) -> None: - self.count = self.hass.data.get(DATA_CONNECTIONS, 0) + self._attr_native_value = self.hass.data.get(DATA_CONNECTIONS, 0) self.async_write_ha_state() From a6a47c0b4426b034c79bcafe34f3a9ce89fd7933 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 15 Apr 2024 14:06:59 -0500 Subject: [PATCH 583/967] Make aiohttp_cors a top level import (#115563) * Make aiohttp_cors a top level import This was moved to a late import in #27935 but there is no longer any need to import it late in the event loop as aiohttp_cors is listed in pyproject.toml so it will always be available * drop requirements as they are all top level now * drop requirements as they are all top level now * adjust --- homeassistant/components/emulated_hue/manifest.json | 3 +-- homeassistant/components/http/cors.py | 6 +----- homeassistant/components/http/manifest.json | 7 +------ requirements_all.txt | 10 ---------- requirements_test_all.txt | 10 ---------- tests/test_requirements.py | 9 ++++----- 6 files changed, 7 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/emulated_hue/manifest.json b/homeassistant/components/emulated_hue/manifest.json index ff3591e0066..14baa5b5d04 100644 --- a/homeassistant/components/emulated_hue/manifest.json +++ b/homeassistant/components/emulated_hue/manifest.json @@ -6,6 +6,5 @@ "dependencies": ["network"], "documentation": "https://www.home-assistant.io/integrations/emulated_hue", "iot_class": "local_push", - "quality_scale": "internal", - "requirements": ["aiohttp_cors==0.7.0"] + "quality_scale": "internal" } diff --git a/homeassistant/components/http/cors.py b/homeassistant/components/http/cors.py index ebae2480589..d97ac9922a2 100644 --- a/homeassistant/components/http/cors.py +++ b/homeassistant/components/http/cors.py @@ -13,6 +13,7 @@ from aiohttp.web_urldispatcher import ( ResourceRoute, StaticResource, ) +import aiohttp_cors from homeassistant.const import HTTP_HEADER_X_REQUESTED_WITH from homeassistant.core import callback @@ -35,11 +36,6 @@ VALID_CORS_TYPES: Final = (Resource, ResourceRoute, StaticResource) @callback def setup_cors(app: Application, origins: list[str]) -> None: """Set up CORS.""" - # This import should remain here. That way the HTTP integration can always - # be imported by other integrations without it's requirements being installed. - # pylint: disable-next=import-outside-toplevel - import aiohttp_cors - cors = aiohttp_cors.setup( app, defaults={ diff --git a/homeassistant/components/http/manifest.json b/homeassistant/components/http/manifest.json index 647b7e42a3a..fb804251edc 100644 --- a/homeassistant/components/http/manifest.json +++ b/homeassistant/components/http/manifest.json @@ -5,10 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/http", "integration_type": "system", "iot_class": "local_push", - "quality_scale": "internal", - "requirements": [ - "aiohttp_cors==0.7.0", - "aiohttp-fast-url-dispatcher==0.3.0", - "aiohttp-zlib-ng==0.3.1" - ] + "quality_scale": "internal" } diff --git a/requirements_all.txt b/requirements_all.txt index 67d731ab587..be4e0d16451 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -262,16 +262,6 @@ aioharmony==0.2.10 # homeassistant.components.homekit_controller aiohomekit==3.1.5 -# homeassistant.components.http -aiohttp-fast-url-dispatcher==0.3.0 - -# homeassistant.components.http -aiohttp-zlib-ng==0.3.1 - -# homeassistant.components.emulated_hue -# homeassistant.components.http -aiohttp_cors==0.7.0 - # homeassistant.components.hue aiohue==4.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5de1f7f5164..5f471c4d7a0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -238,16 +238,6 @@ aioharmony==0.2.10 # homeassistant.components.homekit_controller aiohomekit==3.1.5 -# homeassistant.components.http -aiohttp-fast-url-dispatcher==0.3.0 - -# homeassistant.components.http -aiohttp-zlib-ng==0.3.1 - -# homeassistant.components.emulated_hue -# homeassistant.components.http -aiohttp_cors==0.7.0 - # homeassistant.components.hue aiohue==4.7.1 diff --git a/tests/test_requirements.py b/tests/test_requirements.py index ed04ef8649b..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 # mqtt also depends on http + 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], - } == {"http", "network", "recorder"} + } == {"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 # zeroconf also depends on http + assert len(mock_process.mock_calls) == 3 assert mock_process.mock_calls[0][1][1] == zeroconf.requirements From 2c6ec506adf219df62005d6270bd260cf488bcc0 Mon Sep 17 00:00:00 2001 From: Heiko Carrasco <4395770+miterion@users.noreply.github.com> Date: Mon, 15 Apr 2024 15:09:34 -0400 Subject: [PATCH 584/967] Update switchbot_api to 2.1.0 (#115529) --- homeassistant/components/switchbot_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/switchbot_cloud/manifest.json b/homeassistant/components/switchbot_cloud/manifest.json index cb651e5c84f..2b50f39925f 100644 --- a/homeassistant/components/switchbot_cloud/manifest.json +++ b/homeassistant/components/switchbot_cloud/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/switchbot_cloud", "iot_class": "cloud_polling", "loggers": ["switchbot-api"], - "requirements": ["switchbot-api==2.0.0"] + "requirements": ["switchbot-api==2.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index be4e0d16451..8090db881bc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2643,7 +2643,7 @@ surepy==0.9.0 swisshydrodata==0.1.0 # homeassistant.components.switchbot_cloud -switchbot-api==2.0.0 +switchbot-api==2.1.0 # homeassistant.components.synology_srm synology-srm==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5f471c4d7a0..2782ae4e9bd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2044,7 +2044,7 @@ sunweg==2.1.1 surepy==0.9.0 # homeassistant.components.switchbot_cloud -switchbot-api==2.0.0 +switchbot-api==2.1.0 # homeassistant.components.system_bridge systembridgeconnector==4.0.3 From c7e6f3696f16fc3a5626387cec5fac7e29d2b32f Mon Sep 17 00:00:00 2001 From: Brian Rogers Date: Mon, 15 Apr 2024 15:22:44 -0400 Subject: [PATCH 585/967] Create base class for Rachio smart hose timer entities (#115475) --- homeassistant/components/rachio/device.py | 4 +- homeassistant/components/rachio/entity.py | 57 ++++++++++++++++++++++- homeassistant/components/rachio/switch.py | 50 ++++---------------- 3 files changed, 67 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/rachio/device.py b/homeassistant/components/rachio/device.py index c018d7e6f86..09f7eaf1b06 100644 --- a/homeassistant/components/rachio/device.py +++ b/homeassistant/components/rachio/device.py @@ -350,11 +350,9 @@ class RachioBaseStation: def __init__( self, rachio: Rachio, data: dict[str, Any], coordinator: RachioUpdateCoordinator ) -> None: - """Initialize a hose time base station.""" + """Initialize a smart hose timer base station.""" self.rachio = rachio self._id = data[KEY_ID] - self.serial_number = data[KEY_SERIAL_NUMBER] - self.mac_address = data[KEY_MAC_ADDRESS] self.coordinator = coordinator def start_watering(self, valve_id: str, duration: int) -> None: diff --git a/homeassistant/components/rachio/entity.py b/homeassistant/components/rachio/entity.py index fc0dc1f1aae..27564f1caca 100644 --- a/homeassistant/components/rachio/entity.py +++ b/homeassistant/components/rachio/entity.py @@ -1,10 +1,24 @@ """Adapter to wrap the rachiopy api for home assistant.""" +from abc import abstractmethod +from typing import Any + +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.update_coordinator import CoordinatorEntity -from .const import DEFAULT_NAME, DOMAIN +from .const import ( + DEFAULT_NAME, + DOMAIN, + KEY_CONNECTED, + KEY_ID, + KEY_NAME, + KEY_REPORTED_STATE, + KEY_STATE, +) +from .coordinator import RachioUpdateCoordinator from .device import RachioIro @@ -35,3 +49,44 @@ class RachioDevice(Entity): manufacturer=DEFAULT_NAME, configuration_url="https://app.rach.io", ) + + +class RachioHoseTimerEntity(CoordinatorEntity[RachioUpdateCoordinator]): + """Base class for smart hose timer entities.""" + + _attr_has_entity_name = True + + def __init__( + self, data: dict[str, Any], coordinator: RachioUpdateCoordinator + ) -> None: + """Initialize a Rachio smart hose timer entity.""" + super().__init__(coordinator) + self.id = data[KEY_ID] + self._name = data[KEY_NAME] + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self.id)}, + model="Smart Hose Timer", + name=self._name, + manufacturer=DEFAULT_NAME, + configuration_url="https://app.rach.io", + ) + + @property + def available(self) -> bool: + """Return if the entity is available.""" + return ( + super().available + and self.coordinator.data[self.id][KEY_STATE][KEY_REPORTED_STATE][ + KEY_CONNECTED + ] + ) + + @abstractmethod + def _update_attr(self) -> None: + """Update the state and attributes.""" + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._update_attr() + super()._handle_coordinator_update() diff --git a/homeassistant/components/rachio/switch.py b/homeassistant/components/rachio/switch.py index fe3d455df3c..0f696baad3a 100644 --- a/homeassistant/components/rachio/switch.py +++ b/homeassistant/components/rachio/switch.py @@ -13,25 +13,17 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID, ATTR_ID from homeassistant.core import CALLBACK_TYPE, HomeAssistant, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import ( - config_validation as cv, - device_registry as dr, - entity_platform, -) -from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.dt import as_timestamp, now, parse_datetime, utc_from_timestamp from .const import ( CONF_MANUAL_RUN_MINS, DEFAULT_MANUAL_RUN_MINS, - DEFAULT_NAME, DOMAIN as DOMAIN_RACHIO, - KEY_CONNECTED, KEY_CURRENT_STATUS, KEY_CUSTOM_CROP, KEY_CUSTOM_SHADE, @@ -67,9 +59,8 @@ from .const import ( SLOPE_SLIGHT, SLOPE_STEEP, ) -from .coordinator import RachioUpdateCoordinator from .device import RachioPerson -from .entity import RachioDevice +from .entity import RachioDevice, RachioHoseTimerEntity from .webhooks import ( SUBTYPE_RAIN_DELAY_OFF, SUBTYPE_RAIN_DELAY_ON, @@ -546,39 +537,19 @@ class RachioSchedule(RachioSwitch): ) -class RachioValve(CoordinatorEntity[RachioUpdateCoordinator], SwitchEntity): +class RachioValve(RachioHoseTimerEntity, SwitchEntity): """Representation of one smart hose timer valve.""" - def __init__( - self, person, base, data, coordinator: RachioUpdateCoordinator - ) -> None: + _attr_name = None + + def __init__(self, person, base, data, coordinator) -> None: """Initialize a new smart hose valve.""" - super().__init__(coordinator) + super().__init__(data, coordinator) self._person = person self._base = base - self.id = data[KEY_ID] - self._attr_name = data[KEY_NAME] self._attr_unique_id = f"{self.id}-valve" self._static_attrs = data[KEY_STATE][KEY_REPORTED_STATE] self._attr_is_on = KEY_CURRENT_STATUS in self._static_attrs - self._attr_device_info = DeviceInfo( - identifiers={ - ( - DOMAIN_RACHIO, - self.id, - ) - }, - connections={(dr.CONNECTION_NETWORK_MAC, self._base.mac_address)}, - manufacturer=DEFAULT_NAME, - model="Smart Hose Timer", - name=self._attr_name, - configuration_url="https://app.rach.io", - ) - - @property - def available(self) -> bool: - """Return if the valve is available.""" - return super().available and self._static_attrs[KEY_CONNECTED] def turn_on(self, **kwargs: Any) -> None: """Turn on this valve.""" @@ -594,20 +565,19 @@ class RachioValve(CoordinatorEntity[RachioUpdateCoordinator], SwitchEntity): self._base.start_watering(self.id, manual_run_time.seconds) self._attr_is_on = True self.schedule_update_ha_state(force_refresh=True) - _LOGGER.debug("Starting valve %s for %s", self.name, str(manual_run_time)) + _LOGGER.debug("Starting valve %s for %s", self._name, str(manual_run_time)) def turn_off(self, **kwargs: Any) -> None: """Turn off this valve.""" self._base.stop_watering(self.id) self._attr_is_on = False self.schedule_update_ha_state(force_refresh=True) - _LOGGER.debug("Stopping watering on valve %s", self.name) + _LOGGER.debug("Stopping watering on valve %s", self._name) @callback - def _handle_coordinator_update(self) -> None: + def _update_attr(self) -> None: """Handle updated coordinator data.""" data = self.coordinator.data[self.id] self._static_attrs = data[KEY_STATE][KEY_REPORTED_STATE] self._attr_is_on = KEY_CURRENT_STATUS in self._static_attrs - super()._handle_coordinator_update() From 5f055a64bbd895e7c595e0e64bec9856c40ebae6 Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Mon, 15 Apr 2024 22:25:09 +0200 Subject: [PATCH 586/967] Enable Ruff B017 (#115335) --- pyproject.toml | 1 + tests/components/bluetooth/test_wrappers.py | 7 ++++--- tests/components/iaqualink/test_utils.py | 5 +++-- tests/components/repairs/test_init.py | 3 ++- tests/components/smartthings/test_init.py | 8 ++++---- tests/test_setup.py | 3 ++- 6 files changed, 16 insertions(+), 11 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8701d67c930..3db19fe6851 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -668,6 +668,7 @@ select = [ "B007", # Loop control variable {name} not used within loop body "B014", # Exception handler with duplicate exception "B015", # Pointless comparison. Did you mean to assign a value? Otherwise, prepend assert or remove it. + "B017", # pytest.raises(BaseException) should be considered evil "B018", # Found useless attribute access. Either assign it to a variable or remove it. "B023", # Function definition does not bind loop variable {name} "B026", # Star-arg unpacking after a keyword argument is strongly discouraged diff --git a/tests/components/bluetooth/test_wrappers.py b/tests/components/bluetooth/test_wrappers.py index c14fb8a58c1..2acc2b0ddfc 100644 --- a/tests/components/bluetooth/test_wrappers.py +++ b/tests/components/bluetooth/test_wrappers.py @@ -107,7 +107,7 @@ class FakeBleakClientRaisesOnConnect(BaseFakeBleakClient): async def connect(self, *args, **kwargs): """Connect.""" - raise Exception("Test exception") + raise ConnectionError("Test exception") def _generate_ble_device_and_adv_data( @@ -304,8 +304,9 @@ async def test_release_slot_on_connect_exception( ): ble_device = hci0_device_advs["00:00:00:00:00:01"][0] client = bleak.BleakClient(ble_device) - with pytest.raises(Exception): - assert await client.connect() is False + with pytest.raises(ConnectionError) as exc_info: + await client.connect() + assert str(exc_info.value) == "Test exception" assert allocate_slot_mock.call_count == 1 assert release_slot_mock.call_count == 1 diff --git a/tests/components/iaqualink/test_utils.py b/tests/components/iaqualink/test_utils.py index c803fb48b09..b9aba93523c 100644 --- a/tests/components/iaqualink/test_utils.py +++ b/tests/components/iaqualink/test_utils.py @@ -15,9 +15,10 @@ async def test_await_or_reraise(hass: HomeAssistant) -> None: async_noop = async_returns(None) await await_or_reraise(async_noop()) - with pytest.raises(Exception): - async_ex = async_raises(Exception) + with pytest.raises(Exception) as exc_info: + async_ex = async_raises(Exception("Test exception")) await await_or_reraise(async_ex()) + assert str(exc_info.value) == "Test exception" with pytest.raises(HomeAssistantError): async_ex = async_raises(AqualinkServiceException) diff --git a/tests/components/repairs/test_init.py b/tests/components/repairs/test_init.py index ec34409eb74..75088f6c370 100644 --- a/tests/components/repairs/test_init.py +++ b/tests/components/repairs/test_init.py @@ -2,6 +2,7 @@ from unittest.mock import AsyncMock, Mock +from awesomeversion.exceptions import AwesomeVersionStrategyException from freezegun.api import FrozenDateTimeFactory import pytest @@ -145,7 +146,7 @@ async def test_create_issue_invalid_version( "translation_placeholders": {"abc": "123"}, } - with pytest.raises(Exception): + with pytest.raises(AwesomeVersionStrategyException): async_create_issue( hass, issue["domain"], diff --git a/tests/components/smartthings/test_init.py b/tests/components/smartthings/test_init.py index 6ff640e012a..ae8a288e3a5 100644 --- a/tests/components/smartthings/test_init.py +++ b/tests/components/smartthings/test_init.py @@ -370,9 +370,9 @@ async def test_remove_entry_installedapp_unknown_error( ) -> None: """Test raises exceptions removing the installed app.""" # Arrange - smartthings_mock.delete_installed_app.side_effect = Exception + smartthings_mock.delete_installed_app.side_effect = ValueError # Act - with pytest.raises(Exception): + with pytest.raises(ValueError): await smartthings.async_remove_entry(hass, config_entry) # Assert assert smartthings_mock.delete_installed_app.call_count == 1 @@ -403,9 +403,9 @@ async def test_remove_entry_app_unknown_error( ) -> None: """Test raises exceptions removing the app.""" # Arrange - smartthings_mock.delete_app.side_effect = Exception + smartthings_mock.delete_app.side_effect = ValueError # Act - with pytest.raises(Exception): + with pytest.raises(ValueError): await smartthings.async_remove_entry(hass, config_entry) # Assert assert smartthings_mock.delete_installed_app.call_count == 1 diff --git a/tests/test_setup.py b/tests/test_setup.py index e3d9a322862..65472643adb 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -346,8 +346,9 @@ async def test_component_base_exception_setup(hass: HomeAssistant) -> None: mock_integration(hass, MockModule("comp", setup=exception_setup)) - with pytest.raises(BaseException): + with pytest.raises(BaseException) as exc_info: await setup.async_setup_component(hass, "comp", {}) + assert str(exc_info.value) == "fail!" assert "comp" not in hass.config.components From a16d98854ae0a396e1e2c929b4f94bc3f6b4a9af Mon Sep 17 00:00:00 2001 From: John Luetke Date: Mon, 15 Apr 2024 13:32:14 -0700 Subject: [PATCH 587/967] Remove pihole codeowner (#110384) * Update codeowners Removing myself from codeowners as I have been unable to dedicate time to this * Update CODEOWNERS --------- Co-authored-by: Joost Lekkerkerker --- CODEOWNERS | 4 ++-- homeassistant/components/pi_hole/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 6e7b7e6f8f4..919777391ab 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1023,8 +1023,8 @@ build.json @home-assistant/supervisor /tests/components/persistent_notification/ @home-assistant/core /homeassistant/components/philips_js/ @elupus /tests/components/philips_js/ @elupus -/homeassistant/components/pi_hole/ @johnluetke @shenxn -/tests/components/pi_hole/ @johnluetke @shenxn +/homeassistant/components/pi_hole/ @shenxn +/tests/components/pi_hole/ @shenxn /homeassistant/components/picnic/ @corneyl /tests/components/picnic/ @corneyl /homeassistant/components/pilight/ @trekky12 diff --git a/homeassistant/components/pi_hole/manifest.json b/homeassistant/components/pi_hole/manifest.json index 99439ba3a17..975d8a1494c 100644 --- a/homeassistant/components/pi_hole/manifest.json +++ b/homeassistant/components/pi_hole/manifest.json @@ -1,7 +1,7 @@ { "domain": "pi_hole", "name": "Pi-hole", - "codeowners": ["@johnluetke", "@shenxn"], + "codeowners": ["@shenxn"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/pi_hole", "iot_class": "local_polling", From 4aab073bd207e02ecbd89f5a1c3c4e9a724ae3c5 Mon Sep 17 00:00:00 2001 From: Collin Fair Date: Mon, 15 Apr 2024 15:42:33 -0500 Subject: [PATCH 588/967] Remove cloud dependency from `islamic-prayer-times` (#115146) Co-authored-by: J. Nick Koston --- CODEOWNERS | 4 +- .../islamic_prayer_times/config_flow.py | 42 ++-------- .../islamic_prayer_times/coordinator.py | 15 +--- .../islamic_prayer_times/manifest.json | 6 +- homeassistant/generated/integrations.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../islamic_prayer_times/__init__.py | 10 --- .../islamic_prayer_times/test_config_flow.py | 46 +---------- .../islamic_prayer_times/test_init.py | 81 ++----------------- .../islamic_prayer_times/test_sensor.py | 2 +- 11 files changed, 33 insertions(+), 179 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 919777391ab..39fa804314d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -683,8 +683,8 @@ build.json @home-assistant/supervisor /homeassistant/components/iqvia/ @bachya /tests/components/iqvia/ @bachya /homeassistant/components/irish_rail_transport/ @ttroy50 -/homeassistant/components/islamic_prayer_times/ @engrbm87 -/tests/components/islamic_prayer_times/ @engrbm87 +/homeassistant/components/islamic_prayer_times/ @engrbm87 @cpfair +/tests/components/islamic_prayer_times/ @engrbm87 @cpfair /homeassistant/components/iss/ @DurgNomis-drol /tests/components/iss/ @DurgNomis-drol /homeassistant/components/isy994/ @bdraco @shbatm diff --git a/homeassistant/components/islamic_prayer_times/config_flow.py b/homeassistant/components/islamic_prayer_times/config_flow.py index 12730c9be08..2db89183499 100644 --- a/homeassistant/components/islamic_prayer_times/config_flow.py +++ b/homeassistant/components/islamic_prayer_times/config_flow.py @@ -4,8 +4,6 @@ from __future__ import annotations from typing import Any -from prayer_times_calculator import InvalidResponseError, PrayerTimesCalculator -from requests.exceptions import ConnectionError as ConnError import voluptuous as vol from homeassistant.config_entries import ( @@ -15,7 +13,7 @@ from homeassistant.config_entries import ( OptionsFlow, ) from homeassistant.const import CONF_LATITUDE, CONF_LOCATION, CONF_LONGITUDE, CONF_NAME -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import callback from homeassistant.helpers.selector import ( LocationSelector, SelectSelector, @@ -23,7 +21,6 @@ from homeassistant.helpers.selector import ( SelectSelectorMode, TextSelector, ) -import homeassistant.util.dt as dt_util from .const import ( CALC_METHODS, @@ -43,26 +40,6 @@ from .const import ( ) -async def async_validate_location( - hass: HomeAssistant, lat: float, lon: float -) -> dict[str, str]: - """Check if the selected location is valid.""" - errors = {} - calc = PrayerTimesCalculator( - latitude=lat, - longitude=lon, - calculation_method=DEFAULT_CALC_METHOD, - date=str(dt_util.now().date()), - ) - try: - await hass.async_add_executor_job(calc.fetch_prayer_times) - except InvalidResponseError: - errors["base"] = "invalid_location" - except ConnError: - errors["base"] = "conn_error" - return errors - - class IslamicPrayerFlowHandler(ConfigFlow, domain=DOMAIN): """Handle the Islamic Prayer config flow.""" @@ -81,7 +58,6 @@ class IslamicPrayerFlowHandler(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" - errors = {} if user_input is not None: lat: float = user_input[CONF_LOCATION][CONF_LATITUDE] @@ -89,14 +65,13 @@ class IslamicPrayerFlowHandler(ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(f"{lat}-{lon}") self._abort_if_unique_id_configured() - if not (errors := await async_validate_location(self.hass, lat, lon)): - return self.async_create_entry( - title=user_input[CONF_NAME], - data={ - CONF_LATITUDE: lat, - CONF_LONGITUDE: lon, - }, - ) + return self.async_create_entry( + title=user_input[CONF_NAME], + data={ + CONF_LATITUDE: lat, + CONF_LONGITUDE: lon, + }, + ) home_location = { CONF_LATITUDE: self.hass.config.latitude, @@ -112,7 +87,6 @@ class IslamicPrayerFlowHandler(ConfigFlow, domain=DOMAIN): ): LocationSelector(), } ), - errors=errors, ) diff --git a/homeassistant/components/islamic_prayer_times/coordinator.py b/homeassistant/components/islamic_prayer_times/coordinator.py index d70d0e2f4fe..2785f69534c 100644 --- a/homeassistant/components/islamic_prayer_times/coordinator.py +++ b/homeassistant/components/islamic_prayer_times/coordinator.py @@ -6,14 +6,13 @@ from datetime import datetime, timedelta import logging from typing import Any, cast -from prayer_times_calculator import PrayerTimesCalculator, exceptions -from requests.exceptions import ConnectionError as ConnError +from prayer_times_calculator_offline import PrayerTimesCalculator from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback -from homeassistant.helpers.event import async_call_later, async_track_point_in_time -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.helpers.event import async_track_point_in_time +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator import homeassistant.util.dt as dt_util from .const import ( @@ -142,13 +141,7 @@ class IslamicPrayerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, datetim async def _async_update_data(self) -> dict[str, datetime]: """Update sensors with new prayer times.""" - try: - prayer_times = await self.hass.async_add_executor_job( - self.get_new_prayer_times - ) - except (exceptions.InvalidResponseError, ConnError) as err: - async_call_later(self.hass, 60, self.async_request_update) - raise UpdateFailed from err + prayer_times = self.get_new_prayer_times() # introduced in prayer-times-calculator 0.0.8 prayer_times.pop("date", None) diff --git a/homeassistant/components/islamic_prayer_times/manifest.json b/homeassistant/components/islamic_prayer_times/manifest.json index 5f7e52dd3db..cae3d31feb2 100644 --- a/homeassistant/components/islamic_prayer_times/manifest.json +++ b/homeassistant/components/islamic_prayer_times/manifest.json @@ -1,10 +1,10 @@ { "domain": "islamic_prayer_times", "name": "Islamic Prayer Times", - "codeowners": ["@engrbm87"], + "codeowners": ["@engrbm87", "@cpfair"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/islamic_prayer_times", - "iot_class": "cloud_polling", + "iot_class": "calculated", "loggers": ["prayer_times_calculator"], - "requirements": ["prayer-times-calculator==0.0.12"] + "requirements": ["prayer-times-calculator-offline==1.0.3"] } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 20fbc883207..340be50978d 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -2879,7 +2879,7 @@ "islamic_prayer_times": { "integration_type": "hub", "config_flow": true, - "iot_class": "cloud_polling" + "iot_class": "calculated" }, "ismartwindow": { "name": "iSmartWindow", diff --git a/requirements_all.txt b/requirements_all.txt index 8090db881bc..82d1e50a479 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1556,7 +1556,7 @@ poolsense==0.0.8 praw==7.5.0 # homeassistant.components.islamic_prayer_times -prayer-times-calculator==0.0.12 +prayer-times-calculator-offline==1.0.3 # homeassistant.components.proliphix proliphix==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2782ae4e9bd..a85c110477b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1227,7 +1227,7 @@ poolsense==0.0.8 praw==7.5.0 # homeassistant.components.islamic_prayer_times -prayer-times-calculator==0.0.12 +prayer-times-calculator-offline==1.0.3 # homeassistant.components.prometheus prometheus-client==0.17.1 diff --git a/tests/components/islamic_prayer_times/__init__.py b/tests/components/islamic_prayer_times/__init__.py index 4df733a93fc..1e6d6815921 100644 --- a/tests/components/islamic_prayer_times/__init__.py +++ b/tests/components/islamic_prayer_times/__init__.py @@ -22,14 +22,4 @@ PRAYER_TIMES = { "Midnight": "2020-01-01T00:45:00+00:00", } -NEW_PRAYER_TIMES = { - "Fajr": "2020-01-02T06:00:00+00:00", - "Sunrise": "2020-01-02T07:25:00+00:00", - "Dhuhr": "2020-01-02T12:30:00+00:00", - "Asr": "2020-01-02T15:32:00+00:00", - "Maghrib": "2020-01-02T17:45:00+00:00", - "Isha": "2020-01-02T18:53:00+00:00", - "Midnight": "2020-01-02T00:43:00+00:00", -} - NOW = datetime(2020, 1, 1, 00, 00, 0, tzinfo=dt_util.UTC) diff --git a/tests/components/islamic_prayer_times/test_config_flow.py b/tests/components/islamic_prayer_times/test_config_flow.py index be8eca210d3..cb37a6b147d 100644 --- a/tests/components/islamic_prayer_times/test_config_flow.py +++ b/tests/components/islamic_prayer_times/test_config_flow.py @@ -1,10 +1,6 @@ """Tests for Islamic Prayer Times config flow.""" -from unittest.mock import patch - -from prayer_times_calculator import InvalidResponseError import pytest -from requests.exceptions import ConnectionError as ConnError from homeassistant import config_entries from homeassistant.components import islamic_prayer_times @@ -33,49 +29,15 @@ async def test_flow_works(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" - with patch( - "homeassistant.components.islamic_prayer_times.config_flow.async_validate_location", - return_value={}, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_USER_INPUT - ) - await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_USER_INPUT + ) + await hass.async_block_till_done() assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Home" -@pytest.mark.parametrize( - ("exception", "error"), - [ - (InvalidResponseError, "invalid_location"), - (ConnError, "conn_error"), - ], -) -async def test_flow_error( - hass: HomeAssistant, exception: Exception, error: str -) -> None: - """Test flow errors.""" - result = await hass.config_entries.flow.async_init( - islamic_prayer_times.DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "user" - - with patch( - "homeassistant.components.islamic_prayer_times.config_flow.PrayerTimesCalculator.fetch_prayer_times", - side_effect=exception, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_USER_INPUT - ) - await hass.async_block_till_done() - - assert result["type"] is FlowResultType.FORM - assert result["errors"]["base"] == error - - async def test_options(hass: HomeAssistant) -> None: """Test updating options.""" entry = MockConfigEntry( diff --git a/tests/components/islamic_prayer_times/test_init.py b/tests/components/islamic_prayer_times/test_init.py index aa865ee05a4..c5d4933e24a 100644 --- a/tests/components/islamic_prayer_times/test_init.py +++ b/tests/components/islamic_prayer_times/test_init.py @@ -1,24 +1,21 @@ """Tests for Islamic Prayer Times init.""" -from datetime import timedelta from unittest.mock import patch from freezegun import freeze_time -from prayer_times_calculator.exceptions import InvalidResponseError import pytest from homeassistant.components import islamic_prayer_times from homeassistant.components.islamic_prayer_times.const import CONF_CALC_METHOD from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, STATE_UNAVAILABLE +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -import homeassistant.util.dt as dt_util -from . import NEW_PRAYER_TIMES, NOW, PRAYER_TIMES +from . import NOW, PRAYER_TIMES -from tests.common import MockConfigEntry, async_fire_time_changed +from tests.common import MockConfigEntry @pytest.fixture(autouse=True) @@ -37,7 +34,7 @@ async def test_successful_config_entry(hass: HomeAssistant) -> None: entry.add_to_hass(hass) with patch( - "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times", + "prayer_times_calculator_offline.PrayerTimesCalculator.fetch_prayer_times", return_value=PRAYER_TIMES, ): await hass.config_entries.async_setup(entry.entry_id) @@ -46,25 +43,6 @@ async def test_successful_config_entry(hass: HomeAssistant) -> None: assert entry.state is ConfigEntryState.LOADED -async def test_setup_failed(hass: HomeAssistant) -> None: - """Test Islamic Prayer Times failed due to an error.""" - - entry = MockConfigEntry( - domain=islamic_prayer_times.DOMAIN, - data={}, - ) - entry.add_to_hass(hass) - - # test request error raising ConfigEntryNotReady - with patch( - "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times", - side_effect=InvalidResponseError(), - ): - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - assert entry.state is ConfigEntryState.SETUP_RETRY - - async def test_unload_entry(hass: HomeAssistant) -> None: """Test removing Islamic Prayer Times.""" entry = MockConfigEntry( @@ -74,7 +52,7 @@ async def test_unload_entry(hass: HomeAssistant) -> None: entry.add_to_hass(hass) with patch( - "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times", + "prayer_times_calculator_offline.PrayerTimesCalculator.fetch_prayer_times", return_value=PRAYER_TIMES, ): await hass.config_entries.async_setup(entry.entry_id) @@ -91,7 +69,7 @@ async def test_options_listener(hass: HomeAssistant) -> None: with ( patch( - "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times", + "prayer_times_calculator_offline.PrayerTimesCalculator.fetch_prayer_times", return_value=PRAYER_TIMES, ) as mock_fetch_prayer_times, freeze_time(NOW), @@ -107,49 +85,6 @@ async def test_options_listener(hass: HomeAssistant) -> None: assert mock_fetch_prayer_times.call_count == 2 -async def test_update_failed(hass: HomeAssistant) -> None: - """Test integrations tries to update after 1 min if update fails.""" - entry = MockConfigEntry(domain=islamic_prayer_times.DOMAIN, data={}) - entry.add_to_hass(hass) - - with ( - patch( - "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times", - return_value=PRAYER_TIMES, - ), - freeze_time(NOW), - ): - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - assert entry.state is ConfigEntryState.LOADED - - with patch( - "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times" - ) as FetchPrayerTimes: - FetchPrayerTimes.side_effect = [ - InvalidResponseError, - NEW_PRAYER_TIMES, - ] - midnight_time = dt_util.parse_datetime(PRAYER_TIMES["Midnight"]) - assert midnight_time - future = midnight_time + timedelta(days=1, minutes=1) - with freeze_time(future): - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - - state = hass.states.get("sensor.islamic_prayer_times_fajr_prayer") - assert state.state == STATE_UNAVAILABLE - - # coordinator tries to update after 1 minute - future = future + timedelta(minutes=1) - with freeze_time(future): - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - state = hass.states.get("sensor.islamic_prayer_times_fajr_prayer") - assert state.state == "2020-01-02T06:00:00+00:00" - - @pytest.mark.parametrize( ("object_id", "old_unique_id"), [ @@ -184,7 +119,7 @@ async def test_migrate_unique_id( with ( patch( - "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times", + "prayer_times_calculator_offline.PrayerTimesCalculator.fetch_prayer_times", return_value=PRAYER_TIMES, ), freeze_time(NOW), @@ -207,7 +142,7 @@ async def test_migration_from_1_1_to_1_2(hass: HomeAssistant) -> None: with ( patch( - "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times", + "prayer_times_calculator_offline.PrayerTimesCalculator.fetch_prayer_times", return_value=PRAYER_TIMES, ), freeze_time(NOW), diff --git a/tests/components/islamic_prayer_times/test_sensor.py b/tests/components/islamic_prayer_times/test_sensor.py index 22629819e05..1f8d28dfb6f 100644 --- a/tests/components/islamic_prayer_times/test_sensor.py +++ b/tests/components/islamic_prayer_times/test_sensor.py @@ -40,7 +40,7 @@ async def test_islamic_prayer_times_sensors( with ( patch( - "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times", + "prayer_times_calculator_offline.PrayerTimesCalculator.fetch_prayer_times", return_value=PRAYER_TIMES, ), freeze_time(NOW), From 11ff00f6377325f3ad5a1f7abcaa6ce701fc3c93 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 15 Apr 2024 17:30:05 -0500 Subject: [PATCH 589/967] Small speed up to async_prepare_setup_platform (#115662) --- homeassistant/setup.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 643bb8983b8..5772fce6955 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -487,14 +487,6 @@ async def async_prepare_setup_platform( log_error("Integration not found") return None - # Process deps and reqs as soon as possible, so that requirements are - # available when we import the platform. - try: - await async_process_deps_reqs(hass, hass_config, integration) - except HomeAssistantError as err: - log_error(str(err)) - return None - # Platforms cannot exist on their own, they are part of their integration. # If the integration is not set up yet, and can be set up, set it up. # @@ -502,6 +494,16 @@ async def async_prepare_setup_platform( # where the top level component is. # if load_top_level_component := integration.domain not in hass.config.components: + # Process deps and reqs as soon as possible, so that requirements are + # available when we import the platform. We only do this if the integration + # is not in hass.config.components yet, as we already processed them in + # async_setup_component if it is. + try: + await async_process_deps_reqs(hass, hass_config, integration) + except HomeAssistantError as err: + log_error(str(err)) + return None + try: component = await integration.async_get_component() except ImportError as exc: From 6a7a44c9987598fec19adcce194548b949c5104c Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 16 Apr 2024 08:02:27 +0200 Subject: [PATCH 590/967] Add dataclass to store AdGuard data (#115668) * Add dataclass to store AdGuard data * Unify version call --- homeassistant/components/adguard/__init__.py | 17 +++++++++--- homeassistant/components/adguard/const.py | 3 -- homeassistant/components/adguard/entity.py | 14 +++++----- homeassistant/components/adguard/sensor.py | 25 ++++++----------- homeassistant/components/adguard/switch.py | 29 ++++++++++---------- 5 files changed, 43 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py index b3cbb3300bf..874a4cae963 100644 --- a/homeassistant/components/adguard/__init__.py +++ b/homeassistant/components/adguard/__init__.py @@ -2,6 +2,8 @@ from __future__ import annotations +from dataclasses import dataclass + from adguardhome import AdGuardHome, AdGuardHomeConnectionError import voluptuous as vol @@ -24,7 +26,6 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( CONF_FORCE, - DATA_ADGUARD_CLIENT, DOMAIN, SERVICE_ADD_URL, SERVICE_DISABLE_URL, @@ -44,6 +45,14 @@ SERVICE_REFRESH_SCHEMA = vol.Schema( PLATFORMS = [Platform.SENSOR, Platform.SWITCH] +@dataclass +class AdGuardData: + """Adguard data type.""" + + client: AdGuardHome + version: str + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up AdGuard Home from a config entry.""" session = async_get_clientsession(hass, entry.data[CONF_VERIFY_SSL]) @@ -57,13 +66,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: session=session, ) - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {DATA_ADGUARD_CLIENT: adguard} - try: - await adguard.version() + version = await adguard.version() except AdGuardHomeConnectionError as exception: raise ConfigEntryNotReady from exception + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = AdGuardData(adguard, version) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async def add_url(call: ServiceCall) -> None: diff --git a/homeassistant/components/adguard/const.py b/homeassistant/components/adguard/const.py index 7b6827c19d4..5af739a8f0b 100644 --- a/homeassistant/components/adguard/const.py +++ b/homeassistant/components/adguard/const.py @@ -6,9 +6,6 @@ DOMAIN = "adguard" LOGGER = logging.getLogger(__package__) -DATA_ADGUARD_CLIENT = "adguard_client" -DATA_ADGUARD_VERSION = "adguard_version" - CONF_FORCE = "force" SERVICE_ADD_URL = "add_url" diff --git a/homeassistant/components/adguard/entity.py b/homeassistant/components/adguard/entity.py index 8cb71a861e8..a4e16f1b995 100644 --- a/homeassistant/components/adguard/entity.py +++ b/homeassistant/components/adguard/entity.py @@ -2,13 +2,14 @@ from __future__ import annotations -from adguardhome import AdGuardHome, AdGuardHomeError +from adguardhome import AdGuardHomeError from homeassistant.config_entries import SOURCE_HASSIO, ConfigEntry from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity import Entity -from .const import DATA_ADGUARD_VERSION, DOMAIN, LOGGER +from . import AdGuardData +from .const import DOMAIN, LOGGER class AdGuardHomeEntity(Entity): @@ -19,12 +20,13 @@ class AdGuardHomeEntity(Entity): def __init__( self, - adguard: AdGuardHome, + data: AdGuardData, entry: ConfigEntry, ) -> None: """Initialize the AdGuard Home entity.""" self._entry = entry - self.adguard = adguard + self.data = data + self.adguard = data.client async def async_update(self) -> None: """Update AdGuard Home entity.""" @@ -68,8 +70,6 @@ class AdGuardHomeEntity(Entity): }, manufacturer="AdGuard Team", name="AdGuard Home", - sw_version=self.hass.data[DOMAIN][self._entry.entry_id].get( - DATA_ADGUARD_VERSION - ), + sw_version=self.data.version, configuration_url=config_url, ) diff --git a/homeassistant/components/adguard/sensor.py b/homeassistant/components/adguard/sensor.py index 1e95a07bffa..ce112f49531 100644 --- a/homeassistant/components/adguard/sensor.py +++ b/homeassistant/components/adguard/sensor.py @@ -7,16 +7,16 @@ from dataclasses import dataclass from datetime import timedelta from typing import Any -from adguardhome import AdGuardHome, AdGuardHomeConnectionError +from adguardhome import AdGuardHome from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, UnitOfTime from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERSION, DOMAIN +from . import AdGuardData +from .const import DOMAIN from .entity import AdGuardHomeEntity SCAN_INTERVAL = timedelta(seconds=300) @@ -89,17 +89,10 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up AdGuard Home sensor based on a config entry.""" - adguard = hass.data[DOMAIN][entry.entry_id][DATA_ADGUARD_CLIENT] - - try: - version = await adguard.version() - except AdGuardHomeConnectionError as exception: - raise PlatformNotReady from exception - - hass.data[DOMAIN][entry.entry_id][DATA_ADGUARD_VERSION] = version + data: AdGuardData = hass.data[DOMAIN][entry.entry_id] async_add_entities( - [AdGuardHomeSensor(adguard, entry, description) for description in SENSORS], + [AdGuardHomeSensor(data, entry, description) for description in SENSORS], True, ) @@ -111,18 +104,18 @@ class AdGuardHomeSensor(AdGuardHomeEntity, SensorEntity): def __init__( self, - adguard: AdGuardHome, + data: AdGuardData, entry: ConfigEntry, description: AdGuardHomeEntityDescription, ) -> None: """Initialize AdGuard Home sensor.""" - super().__init__(adguard, entry) + super().__init__(data, entry) self.entity_description = description self._attr_unique_id = "_".join( [ DOMAIN, - adguard.host, - str(adguard.port), + self.adguard.host, + str(self.adguard.port), "sensor", description.key, ] diff --git a/homeassistant/components/adguard/switch.py b/homeassistant/components/adguard/switch.py index ae4bee85d23..e084ed2f349 100644 --- a/homeassistant/components/adguard/switch.py +++ b/homeassistant/components/adguard/switch.py @@ -7,15 +7,15 @@ from dataclasses import dataclass from datetime import timedelta from typing import Any -from adguardhome import AdGuardHome, AdGuardHomeConnectionError, AdGuardHomeError +from adguardhome import AdGuardHome, AdGuardHomeError from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERSION, DOMAIN, LOGGER +from . import AdGuardData +from .const import DOMAIN, LOGGER from .entity import AdGuardHomeEntity SCAN_INTERVAL = timedelta(seconds=10) @@ -83,17 +83,10 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up AdGuard Home switch based on a config entry.""" - adguard = hass.data[DOMAIN][entry.entry_id][DATA_ADGUARD_CLIENT] - - try: - version = await adguard.version() - except AdGuardHomeConnectionError as exception: - raise PlatformNotReady from exception - - hass.data[DOMAIN][entry.entry_id][DATA_ADGUARD_VERSION] = version + data: AdGuardData = hass.data[DOMAIN][entry.entry_id] async_add_entities( - [AdGuardHomeSwitch(adguard, entry, description) for description in SWITCHES], + [AdGuardHomeSwitch(data, entry, description) for description in SWITCHES], True, ) @@ -105,15 +98,21 @@ class AdGuardHomeSwitch(AdGuardHomeEntity, SwitchEntity): def __init__( self, - adguard: AdGuardHome, + data: AdGuardData, entry: ConfigEntry, description: AdGuardHomeSwitchEntityDescription, ) -> None: """Initialize AdGuard Home switch.""" - super().__init__(adguard, entry) + super().__init__(data, entry) self.entity_description = description self._attr_unique_id = "_".join( - [DOMAIN, adguard.host, str(adguard.port), "switch", description.key] + [ + DOMAIN, + self.adguard.host, + str(self.adguard.port), + "switch", + description.key, + ] ) async def async_turn_off(self, **kwargs: Any) -> None: From 7e35fcf11a6c863d0cab429f9e02d28e91c12021 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 16 Apr 2024 01:30:51 -0500 Subject: [PATCH 591/967] Bump sqlparse to 0.5.0 (#115681) fixes https://github.com/home-assistant/core/security/dependabot/54 fixes https://github.com/home-assistant/core/security/dependabot/55 --- homeassistant/components/sql/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index dd44af89237..30d071f25af 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.29", "sqlparse==0.4.4"] + "requirements": ["SQLAlchemy==2.0.29", "sqlparse==0.5.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 82d1e50a479..7a9a99c2628 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2592,7 +2592,7 @@ spiderpy==1.6.1 spotipy==2.23.0 # homeassistant.components.sql -sqlparse==0.4.4 +sqlparse==0.5.0 # homeassistant.components.srp_energy srpenergy==1.3.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a85c110477b..6172bb97007 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1999,7 +1999,7 @@ spiderpy==1.6.1 spotipy==2.23.0 # homeassistant.components.sql -sqlparse==0.4.4 +sqlparse==0.5.0 # homeassistant.components.srp_energy srpenergy==1.3.6 From 1dfabf34c4adb9f3de114aa4d2687dc249de7bea Mon Sep 17 00:00:00 2001 From: theminer3746 Date: Tue, 16 Apr 2024 14:04:00 +0700 Subject: [PATCH 592/967] Fix typo in modbus integration strings.json (#115685) --- homeassistant/components/modbus/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/modbus/strings.json b/homeassistant/components/modbus/strings.json index fd93185b891..72d7a3ec5f1 100644 --- a/homeassistant/components/modbus/strings.json +++ b/homeassistant/components/modbus/strings.json @@ -88,7 +88,7 @@ }, "duplicate_entity_entry": { "title": "Modbus {sub_1} address {sub_2} is duplicate, second entry not loaded.", - "description": "An address can only be associated with on entity, Please correct the entry in your configuration.yaml file and restart Home Assistant to fix this issue." + "description": "An address can only be associated with one entity, Please correct the entry in your configuration.yaml file and restart Home Assistant to fix this issue." }, "duplicate_entity_name": { "title": "Modbus {sub_1} is duplicate, second entry not loaded.", From c5c407b3bb30dba6c24583988359c064457d5dc5 Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Tue, 16 Apr 2024 03:10:32 -0400 Subject: [PATCH 593/967] Move Insteon configuration panel to config entry (#105581) * Move Insteon panel to the config menu * Bump pyinsteon to 1.5.3 * Undo devcontainer.json changes * Bump Insteon frontend * Update config_flow.py * Code cleanup * Code review changes * Fix failing tests * Fix format * Remove unnecessary exception * codecov * Remove return from try * Fix merge mistake --------- Co-authored-by: Erik Montnemery --- homeassistant/components/insteon/__init__.py | 11 +- .../components/insteon/api/__init__.py | 20 +- .../components/insteon/api/config.py | 272 ++++++++++++ .../components/insteon/api/device.py | 69 +++ .../components/insteon/config_flow.py | 221 +--------- homeassistant/components/insteon/const.py | 2 + .../components/insteon/insteon_entity.py | 1 + .../components/insteon/manifest.json | 2 +- homeassistant/components/insteon/schemas.py | 96 +---- homeassistant/components/insteon/utils.py | 25 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/insteon/mock_setup.py | 44 ++ tests/components/insteon/test_api_config.py | 391 +++++++++++++++++ tests/components/insteon/test_api_device.py | 169 ++++++-- tests/components/insteon/test_config_flow.py | 404 +----------------- tests/components/insteon/test_init.py | 23 +- 17 files changed, 988 insertions(+), 766 deletions(-) create mode 100644 homeassistant/components/insteon/api/config.py create mode 100644 tests/components/insteon/mock_setup.py create mode 100644 tests/components/insteon/test_api_config.py diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index 529ac20df52..0ec2434bc82 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -16,6 +16,7 @@ from homeassistant.helpers.typing import ConfigType from . import api from .const import ( CONF_CAT, + CONF_DEV_PATH, CONF_DIM_STEPS, CONF_HOUSECODE, CONF_OVERRIDE, @@ -84,6 +85,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up an Insteon entry.""" + if dev_path := entry.options.get(CONF_DEV_PATH): + hass.data[DOMAIN] = {} + hass.data[DOMAIN][CONF_DEV_PATH] = dev_path + + api.async_load_api(hass) + await api.async_register_insteon_frontend(hass) + if not devices.modem: try: await async_connect(**entry.data) @@ -149,9 +157,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: create_insteon_device(hass, devices.modem, entry.entry_id) - api.async_load_api(hass) - await api.async_register_insteon_frontend(hass) - entry.async_create_background_task( hass, async_get_device_config(hass, entry), "insteon-get-device-config" ) diff --git a/homeassistant/components/insteon/api/__init__.py b/homeassistant/components/insteon/api/__init__.py index fa006c6a6d9..1f671aa1343 100644 --- a/homeassistant/components/insteon/api/__init__.py +++ b/homeassistant/components/insteon/api/__init__.py @@ -16,10 +16,19 @@ from .aldb import ( websocket_reset_aldb, websocket_write_aldb, ) +from .config import ( + websocket_add_device_override, + websocket_get_config, + websocket_get_modem_schema, + websocket_remove_device_override, + websocket_update_modem_config, +) from .device import ( websocket_add_device, + websocket_add_x10_device, websocket_cancel_add_device, websocket_get_device, + websocket_remove_device, ) from .properties import ( websocket_change_properties_record, @@ -58,6 +67,8 @@ def async_load_api(hass): websocket_api.async_register_command(hass, websocket_reset_aldb) websocket_api.async_register_command(hass, websocket_add_default_links) websocket_api.async_register_command(hass, websocket_notify_on_aldb_status) + websocket_api.async_register_command(hass, websocket_add_x10_device) + websocket_api.async_register_command(hass, websocket_remove_device) websocket_api.async_register_command(hass, websocket_get_properties) websocket_api.async_register_command(hass, websocket_change_properties_record) @@ -65,6 +76,12 @@ def async_load_api(hass): websocket_api.async_register_command(hass, websocket_load_properties) websocket_api.async_register_command(hass, websocket_reset_properties) + websocket_api.async_register_command(hass, websocket_get_config) + websocket_api.async_register_command(hass, websocket_get_modem_schema) + websocket_api.async_register_command(hass, websocket_update_modem_config) + websocket_api.async_register_command(hass, websocket_add_device_override) + websocket_api.async_register_command(hass, websocket_remove_device_override) + async def async_register_insteon_frontend(hass: HomeAssistant): """Register the Insteon frontend configuration panel.""" @@ -80,8 +97,7 @@ async def async_register_insteon_frontend(hass: HomeAssistant): hass=hass, frontend_url_path=DOMAIN, webcomponent_name="insteon-frontend", - sidebar_title=DOMAIN.capitalize(), - sidebar_icon="mdi:power", + config_panel_domain=DOMAIN, module_url=f"{URL_BASE}/entrypoint-{build_id}.js", embed_iframe=True, require_admin=True, diff --git a/homeassistant/components/insteon/api/config.py b/homeassistant/components/insteon/api/config.py new file mode 100644 index 00000000000..8a617911d1e --- /dev/null +++ b/homeassistant/components/insteon/api/config.py @@ -0,0 +1,272 @@ +"""API calls to manage Insteon configuration changes.""" + +from __future__ import annotations + +from typing import Any, TypedDict + +from pyinsteon import async_close, async_connect, devices +from pyinsteon.address import Address +import voluptuous as vol +import voluptuous_serialize + +from homeassistant.components import websocket_api +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ADDRESS, CONF_DEVICE +from homeassistant.core import HomeAssistant +from homeassistant.helpers.dispatcher import async_dispatcher_send + +from ..const import ( + CONF_HOUSECODE, + CONF_OVERRIDE, + CONF_UNITCODE, + CONF_X10, + DEVICE_ADDRESS, + DOMAIN, + ID, + SIGNAL_ADD_DEVICE_OVERRIDE, + SIGNAL_ADD_X10_DEVICE, + SIGNAL_REMOVE_DEVICE_OVERRIDE, + TYPE, +) +from ..schemas import ( + build_device_override_schema, + build_hub_schema, + build_plm_manual_schema, + build_plm_schema, +) +from ..utils import async_get_usb_ports + +HUB_V1_SCHEMA = build_hub_schema(hub_version=1) +HUB_V2_SCHEMA = build_hub_schema(hub_version=2) +PLM_SCHEMA = build_plm_manual_schema() +DEVICE_OVERRIDE_SCHEMA = build_device_override_schema() +OVERRIDE = "override" + + +class X10DeviceConfig(TypedDict): + """X10 Device Configuration Definition.""" + + housecode: str + unitcode: int + platform: str + dim_steps: int + + +class DeviceOverride(TypedDict): + """X10 Device Configuration Definition.""" + + address: Address | str + cat: int + subcat: str + + +def get_insteon_config_entry(hass: HomeAssistant) -> ConfigEntry: + """Return the Insteon configuration entry.""" + return hass.config_entries.async_entries(DOMAIN)[0] + + +def add_x10_device(hass: HomeAssistant, x10_device: X10DeviceConfig): + """Add an X10 device to the Insteon integration.""" + + config_entry = get_insteon_config_entry(hass) + x10_config = config_entry.options.get(CONF_X10, []) + if any( + device[CONF_HOUSECODE] == x10_device["housecode"] + and device[CONF_UNITCODE] == x10_device["unitcode"] + for device in x10_config + ): + raise ValueError("Duplicate X10 device") + + hass.config_entries.async_update_entry( + entry=config_entry, + options=config_entry.options | {CONF_X10: [*x10_config, x10_device]}, + ) + async_dispatcher_send(hass, SIGNAL_ADD_X10_DEVICE, x10_device) + + +def remove_x10_device(hass: HomeAssistant, housecode: str, unitcode: int): + """Remove an X10 device from the config.""" + + config_entry = get_insteon_config_entry(hass) + new_options = {**config_entry.options} + new_x10 = [ + existing_device + for existing_device in config_entry.options.get(CONF_X10, []) + if existing_device[CONF_HOUSECODE].lower() != housecode.lower() + or existing_device[CONF_UNITCODE] != unitcode + ] + + new_options[CONF_X10] = new_x10 + hass.config_entries.async_update_entry(entry=config_entry, options=new_options) + + +def add_device_overide(hass: HomeAssistant, override: DeviceOverride): + """Add an Insteon device override.""" + + config_entry = get_insteon_config_entry(hass) + override_config = config_entry.options.get(CONF_OVERRIDE, []) + address = Address(override[CONF_ADDRESS]) + if any( + Address(existing_override[CONF_ADDRESS]) == address + for existing_override in override_config + ): + raise ValueError("Duplicate override") + + hass.config_entries.async_update_entry( + entry=config_entry, + options=config_entry.options | {CONF_OVERRIDE: [*override_config, override]}, + ) + async_dispatcher_send(hass, SIGNAL_ADD_DEVICE_OVERRIDE, override) + + +def remove_device_override(hass: HomeAssistant, address: Address): + """Remove a device override from config.""" + + config_entry = get_insteon_config_entry(hass) + new_options = {**config_entry.options} + + new_overrides = [ + existing_override + for existing_override in config_entry.options.get(CONF_OVERRIDE, []) + if Address(existing_override[CONF_ADDRESS]) != address + ] + new_options[CONF_OVERRIDE] = new_overrides + hass.config_entries.async_update_entry(entry=config_entry, options=new_options) + + +async def _async_connect(**kwargs): + """Connect to the Insteon modem.""" + if devices.modem: + await async_close() + try: + await async_connect(**kwargs) + except ConnectionError: + return False + return True + + +@websocket_api.websocket_command({vol.Required(TYPE): "insteon/config/get"}) +@websocket_api.require_admin +@websocket_api.async_response +async def websocket_get_config( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Get Insteon configuration.""" + config_entry = get_insteon_config_entry(hass) + modem_config = config_entry.data + options_config = config_entry.options + x10_config = options_config.get(CONF_X10) + override_config = options_config.get(CONF_OVERRIDE) + connection.send_result( + msg[ID], + { + "modem_config": {**modem_config}, + "x10_config": x10_config, + "override_config": override_config, + }, + ) + + +@websocket_api.websocket_command( + { + vol.Required(TYPE): "insteon/config/get_modem_schema", + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def websocket_get_modem_schema( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Get the schema for the modem configuration.""" + config_entry = get_insteon_config_entry(hass) + config_data = config_entry.data + if device := config_data.get(CONF_DEVICE): + ports = await async_get_usb_ports(hass=hass) + plm_schema = voluptuous_serialize.convert( + build_plm_schema(ports=ports, device=device) + ) + connection.send_result(msg[ID], plm_schema) + else: + hub_schema = voluptuous_serialize.convert(build_hub_schema(**config_data)) + connection.send_result(msg[ID], hub_schema) + + +@websocket_api.websocket_command( + { + vol.Required(TYPE): "insteon/config/update_modem_config", + vol.Required("config"): vol.Any(PLM_SCHEMA, HUB_V2_SCHEMA, HUB_V1_SCHEMA), + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def websocket_update_modem_config( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Get the schema for the modem configuration.""" + config = msg["config"] + config_entry = get_insteon_config_entry(hass) + is_connected = devices.modem.connected + + if not await _async_connect(**config): + connection.send_error( + msg_id=msg[ID], code="connection_failed", message="Connection failed" + ) + # Try to reconnect using old info + if is_connected: + await _async_connect(**config_entry.data) + return + + hass.config_entries.async_update_entry( + entry=config_entry, + data=config, + ) + connection.send_result(msg[ID], {"status": "success"}) + + +@websocket_api.websocket_command( + { + vol.Required(TYPE): "insteon/config/device_override/add", + vol.Required(OVERRIDE): DEVICE_OVERRIDE_SCHEMA, + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def websocket_add_device_override( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Get the schema for the modem configuration.""" + override = msg[OVERRIDE] + try: + add_device_overide(hass, override) + except ValueError: + connection.send_error(msg[ID], "duplicate", "Duplicate device address") + + connection.send_result(msg[ID]) + + +@websocket_api.websocket_command( + { + vol.Required(TYPE): "insteon/config/device_override/remove", + vol.Required(DEVICE_ADDRESS): str, + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def websocket_remove_device_override( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Get the schema for the modem configuration.""" + address = Address(msg[DEVICE_ADDRESS]) + remove_device_override(hass, address) + async_dispatcher_send(hass, SIGNAL_REMOVE_DEVICE_OVERRIDE, address) + connection.send_result(msg[ID]) diff --git a/homeassistant/components/insteon/api/device.py b/homeassistant/components/insteon/api/device.py index d48d87fa347..e8bd08bc4ee 100644 --- a/homeassistant/components/insteon/api/device.py +++ b/homeassistant/components/insteon/api/device.py @@ -3,12 +3,14 @@ from typing import Any from pyinsteon import devices +from pyinsteon.address import Address from pyinsteon.constants import DeviceAction import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.dispatcher import async_dispatcher_send from ..const import ( DEVICE_ADDRESS, @@ -18,8 +20,17 @@ from ..const import ( ID, INSTEON_DEVICE_NOT_FOUND, MULTIPLE, + SIGNAL_REMOVE_HA_DEVICE, + SIGNAL_REMOVE_INSTEON_DEVICE, + SIGNAL_REMOVE_X10_DEVICE, TYPE, ) +from ..schemas import build_x10_schema +from .config import add_x10_device, remove_device_override, remove_x10_device + +X10_DEVICE = "x10_device" +X10_DEVICE_SCHEMA = build_x10_schema() +REMOVE_ALL_REFS = "remove_all_refs" def compute_device_name(ha_device): @@ -139,3 +150,61 @@ async def websocket_cancel_add_device( """Cancel the Insteon all-linking process.""" await devices.async_cancel_all_linking() connection.send_result(msg[ID]) + + +@websocket_api.websocket_command( + { + vol.Required(TYPE): "insteon/device/remove", + vol.Required(DEVICE_ADDRESS): str, + vol.Required(REMOVE_ALL_REFS): bool, + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def websocket_remove_device( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Remove an Insteon device.""" + + address = msg[DEVICE_ADDRESS] + remove_all_refs = msg[REMOVE_ALL_REFS] + if address.startswith("X10"): + _, housecode, unitcode = address.split(".") + unitcode = int(unitcode) + async_dispatcher_send(hass, SIGNAL_REMOVE_X10_DEVICE, housecode, unitcode) + remove_x10_device(hass, housecode, unitcode) + else: + address = Address(address) + remove_device_override(hass, address) + async_dispatcher_send(hass, SIGNAL_REMOVE_HA_DEVICE, address) + async_dispatcher_send( + hass, SIGNAL_REMOVE_INSTEON_DEVICE, address, remove_all_refs + ) + + connection.send_result(msg[ID]) + + +@websocket_api.websocket_command( + { + vol.Required(TYPE): "insteon/device/add_x10", + vol.Required(X10_DEVICE): X10_DEVICE_SCHEMA, + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def websocket_add_x10_device( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Get the schema for the X10 devices configuration.""" + x10_device = msg[X10_DEVICE] + try: + add_x10_device(hass, x10_device) + except ValueError: + connection.send_error(msg[ID], code="duplicate", message="Duplicate X10 device") + return + + connection.send_result(msg[ID]) diff --git a/homeassistant/components/insteon/config_flow.py b/homeassistant/components/insteon/config_flow.py index 44aa1e18646..baf06b13860 100644 --- a/homeassistant/components/insteon/config_flow.py +++ b/homeassistant/components/insteon/config_flow.py @@ -4,52 +4,19 @@ from __future__ import annotations import logging -from pyinsteon import async_close, async_connect, devices +from pyinsteon import async_connect from homeassistant.components import dhcp, usb from homeassistant.config_entries import ( DEFAULT_DISCOVERY_UNIQUE_ID, - ConfigEntry, ConfigFlow, ConfigFlowResult, - OptionsFlow, ) -from homeassistant.const import ( - CONF_ADDRESS, - CONF_DEVICE, - CONF_HOST, - CONF_NAME, - CONF_PASSWORD, - CONF_PORT, - CONF_USERNAME, -) -from homeassistant.core import callback +from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_NAME from homeassistant.helpers.device_registry import format_mac -from homeassistant.helpers.dispatcher import async_dispatcher_send -from .const import ( - CONF_HOUSECODE, - CONF_HUB_VERSION, - CONF_OVERRIDE, - CONF_UNITCODE, - CONF_X10, - DOMAIN, - SIGNAL_ADD_DEVICE_OVERRIDE, - SIGNAL_ADD_X10_DEVICE, - SIGNAL_REMOVE_DEVICE_OVERRIDE, - SIGNAL_REMOVE_X10_DEVICE, -) -from .schemas import ( - add_device_override, - add_x10_device, - build_device_override_schema, - build_hub_schema, - build_plm_manual_schema, - build_plm_schema, - build_remove_override_schema, - build_remove_x10_schema, - build_x10_schema, -) +from .const import CONF_HUB_VERSION, DOMAIN +from .schemas import build_hub_schema, build_plm_manual_schema, build_plm_schema from .utils import async_get_usb_ports STEP_PLM = "plm" @@ -80,41 +47,6 @@ async def _async_connect(**kwargs): return True -def _remove_override(address, options): - """Remove a device override from config.""" - new_options = {} - if options.get(CONF_X10): - new_options[CONF_X10] = options.get(CONF_X10) - new_overrides = [ - override - for override in options[CONF_OVERRIDE] - if override[CONF_ADDRESS] != address - ] - if new_overrides: - new_options[CONF_OVERRIDE] = new_overrides - return new_options - - -def _remove_x10(device, options): - """Remove an X10 device from the config.""" - housecode = device[11].lower() - unitcode = int(device[24:]) - new_options = {} - if options.get(CONF_OVERRIDE): - new_options[CONF_OVERRIDE] = options.get(CONF_OVERRIDE) - new_x10 = [ - existing_device - for existing_device in options[CONF_X10] - if ( - existing_device[CONF_HOUSECODE].lower() != housecode - or existing_device[CONF_UNITCODE] != unitcode - ) - ] - if new_x10: - new_options[CONF_X10] = new_x10 - return new_options, housecode, unitcode - - class InsteonFlowHandler(ConfigFlow, domain=DOMAIN): """Insteon config flow handler.""" @@ -122,14 +54,6 @@ class InsteonFlowHandler(ConfigFlow, domain=DOMAIN): _device_name: str | None = None discovered_conf: dict[str, str] = {} - @staticmethod - @callback - def async_get_options_flow( - config_entry: ConfigEntry, - ) -> InsteonOptionsFlowHandler: - """Define the config flow to handle options.""" - return InsteonOptionsFlowHandler(config_entry) - async def async_step_user(self, user_input=None): """Init the config flow.""" if self._async_current_entries(): @@ -237,140 +161,3 @@ class InsteonFlowHandler(ConfigFlow, domain=DOMAIN): } await self.async_set_unique_id(format_mac(discovery_info.macaddress)) return await self.async_step_user() - - -class InsteonOptionsFlowHandler(OptionsFlow): - """Handle an Insteon options flow.""" - - def __init__(self, config_entry: ConfigEntry) -> None: - """Init the InsteonOptionsFlowHandler class.""" - self.config_entry = config_entry - - async def async_step_init(self, user_input=None) -> ConfigFlowResult: - """Init the options config flow.""" - menu_options = [STEP_ADD_OVERRIDE, STEP_ADD_X10] - - if self.config_entry.data.get(CONF_HOST): - menu_options.append(STEP_CHANGE_HUB_CONFIG) - else: - menu_options.append(STEP_CHANGE_PLM_CONFIG) - - options = {**self.config_entry.options} - if options.get(CONF_OVERRIDE): - menu_options.append(STEP_REMOVE_OVERRIDE) - if options.get(CONF_X10): - menu_options.append(STEP_REMOVE_X10) - - return self.async_show_menu(step_id="init", menu_options=menu_options) - - async def async_step_change_hub_config(self, user_input=None) -> ConfigFlowResult: - """Change the Hub configuration.""" - errors = {} - if user_input is not None: - data = { - **self.config_entry.data, - CONF_HOST: user_input[CONF_HOST], - CONF_PORT: user_input[CONF_PORT], - } - if self.config_entry.data[CONF_HUB_VERSION] == 2: - data[CONF_USERNAME] = user_input[CONF_USERNAME] - data[CONF_PASSWORD] = user_input[CONF_PASSWORD] - if devices.modem: - await async_close() - - if await _async_connect(**data): - self.hass.config_entries.async_update_entry( - self.config_entry, data=data - ) - return self.async_create_entry(data={**self.config_entry.options}) - errors["base"] = "cannot_connect" - data_schema = build_hub_schema(**self.config_entry.data) - return self.async_show_form( - step_id=STEP_CHANGE_HUB_CONFIG, data_schema=data_schema, errors=errors - ) - - async def async_step_change_plm_config(self, user_input=None) -> ConfigFlowResult: - """Change the PLM configuration.""" - errors = {} - if user_input is not None: - data = { - **self.config_entry.data, - CONF_DEVICE: user_input[CONF_DEVICE], - } - if devices.modem: - await async_close() - if await _async_connect(**data): - self.hass.config_entries.async_update_entry( - self.config_entry, data=data - ) - return self.async_create_entry(data={**self.config_entry.options}) - errors["base"] = "cannot_connect" - - ports = await async_get_usb_ports(self.hass) - data_schema = build_plm_schema(ports, **self.config_entry.data) - return self.async_show_form( - step_id=STEP_CHANGE_PLM_CONFIG, data_schema=data_schema, errors=errors - ) - - async def async_step_add_override(self, user_input=None) -> ConfigFlowResult: - """Add a device override.""" - errors = {} - if user_input is not None: - try: - data = add_device_override({**self.config_entry.options}, user_input) - async_dispatcher_send(self.hass, SIGNAL_ADD_DEVICE_OVERRIDE, user_input) - return self.async_create_entry(data=data) - except ValueError: - errors["base"] = "input_error" - schema_defaults = user_input if user_input is not None else {} - data_schema = build_device_override_schema(**schema_defaults) - return self.async_show_form( - step_id=STEP_ADD_OVERRIDE, data_schema=data_schema, errors=errors - ) - - async def async_step_add_x10(self, user_input=None) -> ConfigFlowResult: - """Add an X10 device.""" - errors: dict[str, str] = {} - if user_input is not None: - options = add_x10_device({**self.config_entry.options}, user_input) - async_dispatcher_send(self.hass, SIGNAL_ADD_X10_DEVICE, user_input) - return self.async_create_entry(data=options) - schema_defaults: dict[str, str] = user_input if user_input is not None else {} - data_schema = build_x10_schema(**schema_defaults) - return self.async_show_form( - step_id=STEP_ADD_X10, data_schema=data_schema, errors=errors - ) - - async def async_step_remove_override(self, user_input=None) -> ConfigFlowResult: - """Remove a device override.""" - errors: dict[str, str] = {} - options = self.config_entry.options - if user_input is not None: - options = _remove_override(user_input[CONF_ADDRESS], options) - async_dispatcher_send( - self.hass, - SIGNAL_REMOVE_DEVICE_OVERRIDE, - user_input[CONF_ADDRESS], - ) - return self.async_create_entry(data=options) - - data_schema = build_remove_override_schema(options[CONF_OVERRIDE]) - return self.async_show_form( - step_id=STEP_REMOVE_OVERRIDE, data_schema=data_schema, errors=errors - ) - - async def async_step_remove_x10(self, user_input=None) -> ConfigFlowResult: - """Remove an X10 device.""" - errors: dict[str, str] = {} - options = self.config_entry.options - if user_input is not None: - options, housecode, unitcode = _remove_x10(user_input[CONF_DEVICE], options) - async_dispatcher_send( - self.hass, SIGNAL_REMOVE_X10_DEVICE, housecode, unitcode - ) - return self.async_create_entry(data=options) - - data_schema = build_remove_x10_schema(options[CONF_X10]) - return self.async_show_form( - step_id=STEP_REMOVE_X10, data_schema=data_schema, errors=errors - ) diff --git a/homeassistant/components/insteon/const.py b/homeassistant/components/insteon/const.py index b7e6e6055e1..11e1943aa73 100644 --- a/homeassistant/components/insteon/const.py +++ b/homeassistant/components/insteon/const.py @@ -101,6 +101,8 @@ SIGNAL_SAVE_DEVICES = "save_devices" SIGNAL_ADD_ENTITIES = "insteon_add_entities" SIGNAL_ADD_DEFAULT_LINKS = "add_default_links" SIGNAL_ADD_DEVICE_OVERRIDE = "add_device_override" +SIGNAL_REMOVE_HA_DEVICE = "insteon_remove_ha_device" +SIGNAL_REMOVE_INSTEON_DEVICE = "insteon_remove_insteon_device" SIGNAL_REMOVE_DEVICE_OVERRIDE = "insteon_remove_device_override" SIGNAL_REMOVE_ENTITY = "insteon_remove_entity" SIGNAL_ADD_X10_DEVICE = "insteon_add_x10_device" diff --git a/homeassistant/components/insteon/insteon_entity.py b/homeassistant/components/insteon/insteon_entity.py index f81298dfe48..79e5c18a934 100644 --- a/homeassistant/components/insteon/insteon_entity.py +++ b/homeassistant/components/insteon/insteon_entity.py @@ -95,6 +95,7 @@ class InsteonEntity(Entity): f" {self._insteon_device.engine_version}" ), via_device=(DOMAIN, str(devices.modem.address)), + configuration_url=f"homeassistant://insteon/device/config/{self._insteon_device.id}", ) @callback diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index cf210963841..7d12436d0fb 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -18,7 +18,7 @@ "loggers": ["pyinsteon", "pypubsub"], "requirements": [ "pyinsteon==1.5.3", - "insteon-frontend-home-assistant==0.4.0" + "insteon-frontend-home-assistant==0.5.0" ], "usb": [ { diff --git a/homeassistant/components/insteon/schemas.py b/homeassistant/components/insteon/schemas.py index e277281c240..837c6224014 100644 --- a/homeassistant/components/insteon/schemas.py +++ b/homeassistant/components/insteon/schemas.py @@ -2,9 +2,6 @@ from __future__ import annotations -from binascii import Error as HexError, unhexlify - -from pyinsteon.address import Address from pyinsteon.constants import HC_LOOKUP import voluptuous as vol @@ -25,10 +22,8 @@ from .const import ( CONF_CAT, CONF_DIM_STEPS, CONF_HOUSECODE, - CONF_OVERRIDE, CONF_SUBCAT, CONF_UNITCODE, - CONF_X10, HOUSECODES, PORT_HUB_V1, PORT_HUB_V2, @@ -76,76 +71,6 @@ TRIGGER_SCENE_SCHEMA = vol.Schema( ADD_DEFAULT_LINKS_SCHEMA = vol.Schema({vol.Required(CONF_ENTITY_ID): cv.entity_id}) -def normalize_byte_entry_to_int(entry: int | bytes | str): - """Format a hex entry value.""" - if isinstance(entry, int): - if entry in range(256): - return entry - raise ValueError("Must be single byte") - if isinstance(entry, str): - if entry[0:2].lower() == "0x": - entry = entry[2:] - if len(entry) != 2: - raise ValueError("Not a valid hex code") - try: - entry = unhexlify(entry) - except HexError as err: - raise ValueError("Not a valid hex code") from err - return int.from_bytes(entry, byteorder="big") - - -def add_device_override(config_data, new_override): - """Add a new device override.""" - try: - address = str(Address(new_override[CONF_ADDRESS])) - cat = normalize_byte_entry_to_int(new_override[CONF_CAT]) - subcat = normalize_byte_entry_to_int(new_override[CONF_SUBCAT]) - except ValueError as err: - raise ValueError("Incorrect values") from err - - overrides = [ - override - for override in config_data.get(CONF_OVERRIDE, []) - if override[CONF_ADDRESS] != address - ] - overrides.append( - { - CONF_ADDRESS: address, - CONF_CAT: cat, - CONF_SUBCAT: subcat, - } - ) - - new_config = {} - if config_data.get(CONF_X10): - new_config[CONF_X10] = config_data[CONF_X10] - new_config[CONF_OVERRIDE] = overrides - return new_config - - -def add_x10_device(config_data, new_x10): - """Add a new X10 device to X10 device list.""" - x10_devices = [ - x10_device - for x10_device in config_data.get(CONF_X10, []) - if x10_device[CONF_HOUSECODE] != new_x10[CONF_HOUSECODE] - or x10_device[CONF_UNITCODE] != new_x10[CONF_UNITCODE] - ] - x10_devices.append( - { - CONF_HOUSECODE: new_x10[CONF_HOUSECODE], - CONF_UNITCODE: new_x10[CONF_UNITCODE], - CONF_PLATFORM: new_x10[CONF_PLATFORM], - CONF_DIM_STEPS: new_x10[CONF_DIM_STEPS], - } - ) - new_config = {} - if config_data.get(CONF_OVERRIDE): - new_config[CONF_OVERRIDE] = config_data[CONF_OVERRIDE] - new_config[CONF_X10] = x10_devices - return new_config - - def build_device_override_schema( address=vol.UNDEFINED, cat=vol.UNDEFINED, @@ -169,12 +94,16 @@ def build_x10_schema( dim_steps=22, ): """Build the X10 schema for config flow.""" + if platform == "light": + dim_steps_schema = vol.Required(CONF_DIM_STEPS, default=dim_steps) + else: + dim_steps_schema = vol.Optional(CONF_DIM_STEPS, default=dim_steps) return vol.Schema( { vol.Required(CONF_HOUSECODE, default=housecode): vol.In(HC_LOOKUP.keys()), vol.Required(CONF_UNITCODE, default=unitcode): vol.In(range(1, 17)), vol.Required(CONF_PLATFORM, default=platform): vol.In(X10_PLATFORMS), - vol.Optional(CONF_DIM_STEPS, default=dim_steps): vol.In(range(1, 255)), + dim_steps_schema: vol.Range(min=0, max=255), } ) @@ -219,18 +148,3 @@ def build_hub_schema( schema[vol.Required(CONF_USERNAME, default=username)] = str schema[vol.Required(CONF_PASSWORD, default=password)] = str return vol.Schema(schema) - - -def build_remove_override_schema(data): - """Build the schema to remove device overrides in config flow options.""" - selection = [override[CONF_ADDRESS] for override in data] - return vol.Schema({vol.Required(CONF_ADDRESS): vol.In(selection)}) - - -def build_remove_x10_schema(data): - """Build the schema to remove an X10 device in config flow options.""" - selection = [ - f"Housecode: {device[CONF_HOUSECODE].upper()}, Unitcode: {device[CONF_UNITCODE]}" - for device in data - ] - return vol.Schema({vol.Required(CONF_DEVICE): vol.In(selection)}) diff --git a/homeassistant/components/insteon/utils.py b/homeassistant/components/insteon/utils.py index 272018ea507..db25d8c97a9 100644 --- a/homeassistant/components/insteon/utils.py +++ b/homeassistant/components/insteon/utils.py @@ -65,6 +65,8 @@ from .const import ( SIGNAL_PRINT_ALDB, SIGNAL_REMOVE_DEVICE_OVERRIDE, SIGNAL_REMOVE_ENTITY, + SIGNAL_REMOVE_HA_DEVICE, + SIGNAL_REMOVE_INSTEON_DEVICE, SIGNAL_REMOVE_X10_DEVICE, SIGNAL_SAVE_DEVICES, SRV_ADD_ALL_LINK, @@ -179,7 +181,7 @@ def register_new_device_callback(hass): @callback -def async_register_services(hass): +def async_register_services(hass): # noqa: C901 """Register services used by insteon component.""" save_lock = asyncio.Lock() @@ -270,14 +272,14 @@ def async_register_services(hass): async def async_add_device_override(override): """Remove an Insten device and associated entities.""" address = Address(override[CONF_ADDRESS]) - await async_remove_device(address) + await async_remove_ha_device(address) devices.set_id(address, override[CONF_CAT], override[CONF_SUBCAT], 0) await async_srv_save_devices() async def async_remove_device_override(address): """Remove an Insten device and associated entities.""" address = Address(address) - await async_remove_device(address) + await async_remove_ha_device(address) devices.set_id(address, None, None, None) await devices.async_identify_device(address) await async_srv_save_devices() @@ -304,9 +306,9 @@ def async_register_services(hass): """Remove an X10 device and associated entities.""" address = create_x10_address(housecode, unitcode) devices.pop(address) - await async_remove_device(address) + await async_remove_ha_device(address) - async def async_remove_device(address): + async def async_remove_ha_device(address: Address, remove_all_refs: bool = False): """Remove the device and all entities from hass.""" signal = f"{address.id}_{SIGNAL_REMOVE_ENTITY}" async_dispatcher_send(hass, signal) @@ -315,6 +317,15 @@ def async_register_services(hass): if device: dev_registry.async_remove_device(device.id) + async def async_remove_insteon_device( + address: Address, remove_all_refs: bool = False + ): + """Remove the underlying Insteon device from the network.""" + await devices.async_remove_device( + address=address, force=False, remove_all_refs=remove_all_refs + ) + await async_srv_save_devices() + hass.services.async_register( DOMAIN, SRV_ADD_ALL_LINK, async_srv_add_all_link, schema=ADD_ALL_LINK_SCHEMA ) @@ -368,6 +379,10 @@ def async_register_services(hass): ) async_dispatcher_connect(hass, SIGNAL_ADD_X10_DEVICE, async_add_x10_device) async_dispatcher_connect(hass, SIGNAL_REMOVE_X10_DEVICE, async_remove_x10_device) + async_dispatcher_connect(hass, SIGNAL_REMOVE_HA_DEVICE, async_remove_ha_device) + async_dispatcher_connect( + hass, SIGNAL_REMOVE_INSTEON_DEVICE, async_remove_insteon_device + ) _LOGGER.debug("Insteon Services registered") diff --git a/requirements_all.txt b/requirements_all.txt index 7a9a99c2628..653e481d2fb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1142,7 +1142,7 @@ influxdb==5.3.1 inkbird-ble==0.5.6 # homeassistant.components.insteon -insteon-frontend-home-assistant==0.4.0 +insteon-frontend-home-assistant==0.5.0 # homeassistant.components.intellifire intellifire4py==2.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6172bb97007..0decf82fe0c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -926,7 +926,7 @@ influxdb==5.3.1 inkbird-ble==0.5.6 # homeassistant.components.insteon -insteon-frontend-home-assistant==0.4.0 +insteon-frontend-home-assistant==0.5.0 # homeassistant.components.intellifire intellifire4py==2.2.2 diff --git a/tests/components/insteon/mock_setup.py b/tests/components/insteon/mock_setup.py new file mode 100644 index 00000000000..c0d90509a50 --- /dev/null +++ b/tests/components/insteon/mock_setup.py @@ -0,0 +1,44 @@ +"""Utility to setup the Insteon integration.""" + +from homeassistant.components.insteon.api import async_load_api +from homeassistant.components.insteon.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from .const import MOCK_USER_INPUT_PLM +from .mock_devices import MockDevices + +from tests.common import MockConfigEntry +from tests.typing import WebSocketGenerator + + +async def async_mock_setup( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + config_data: dict | None = None, + config_options: dict | None = None, +): + """Set up for tests.""" + config_data = MOCK_USER_INPUT_PLM if config_data is None else config_data + config_options = {} if config_options is None else config_options + config_entry = MockConfigEntry( + domain=DOMAIN, + entry_id="abcde12345", + data=config_data, + options=config_options, + ) + config_entry.add_to_hass(hass) + async_load_api(hass) + + ws_client = await hass_ws_client(hass) + devices = MockDevices() + await devices.async_load() + + dev_reg = dr.async_get(hass) + # Create device registry entry for mock node + ha_device = dev_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={(DOMAIN, "11.11.11")}, + name="Device 11.11.11", + ) + return ws_client, devices, ha_device, dev_reg diff --git a/tests/components/insteon/test_api_config.py b/tests/components/insteon/test_api_config.py new file mode 100644 index 00000000000..7c922338638 --- /dev/null +++ b/tests/components/insteon/test_api_config.py @@ -0,0 +1,391 @@ +"""Test the Insteon APIs for configuring the integration.""" + +from unittest.mock import patch + +from homeassistant.components.insteon.api.device import ID, TYPE +from homeassistant.components.insteon.const import ( + CONF_HUB_VERSION, + CONF_OVERRIDE, + CONF_X10, +) +from homeassistant.core import HomeAssistant + +from .const import ( + MOCK_DEVICE, + MOCK_HOSTNAME, + MOCK_USER_INPUT_HUB_V1, + MOCK_USER_INPUT_HUB_V2, + MOCK_USER_INPUT_PLM, +) +from .mock_connection import mock_failed_connection, mock_successful_connection +from .mock_setup import async_mock_setup + +from tests.typing import WebSocketGenerator + + +class MockProtocol: + """A mock Insteon protocol object.""" + + connected = True + + +async def test_get_config( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test getting the Insteon configuration.""" + + ws_client, _, _, _ = await async_mock_setup(hass, hass_ws_client) + await ws_client.send_json({ID: 2, TYPE: "insteon/config/get"}) + msg = await ws_client.receive_json() + result = msg["result"] + + assert result["modem_config"] == {"device": MOCK_DEVICE} + + +async def test_get_modem_schema_plm( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test getting the Insteon PLM modem configuration schema.""" + + ws_client, _, _, _ = await async_mock_setup(hass, hass_ws_client) + await ws_client.send_json({ID: 2, TYPE: "insteon/config/get_modem_schema"}) + msg = await ws_client.receive_json() + result = msg["result"][0] + + assert result["default"] == MOCK_DEVICE + assert result["name"] == "device" + assert result["required"] + + +async def test_get_modem_schema_hub( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test getting the Insteon PLM modem configuration schema.""" + + ws_client, devices, _, _ = await async_mock_setup( + hass, + hass_ws_client, + config_data={**MOCK_USER_INPUT_HUB_V2, CONF_HUB_VERSION: 2}, + ) + await ws_client.send_json({ID: 2, TYPE: "insteon/config/get_modem_schema"}) + msg = await ws_client.receive_json() + result = msg["result"][0] + + assert result["default"] == MOCK_HOSTNAME + assert result["name"] == "host" + assert result["required"] + + +async def test_update_modem_config_plm( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test getting the Insteon PLM modem configuration schema.""" + + ws_client, mock_devices, _, _ = await async_mock_setup( + hass, + hass_ws_client, + ) + with ( + patch( + "homeassistant.components.insteon.api.config.async_connect", + new=mock_successful_connection, + ), + patch("homeassistant.components.insteon.api.config.devices", mock_devices), + patch("homeassistant.components.insteon.api.config.async_close"), + ): + await ws_client.send_json( + { + ID: 2, + TYPE: "insteon/config/update_modem_config", + "config": MOCK_USER_INPUT_PLM, + } + ) + msg = await ws_client.receive_json() + result = msg["result"] + + assert result["status"] == "success" + + +async def test_update_modem_config_hub_v2( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test getting the Insteon HubV2 modem configuration schema.""" + + ws_client, mock_devices, _, _ = await async_mock_setup( + hass, + hass_ws_client, + config_data={**MOCK_USER_INPUT_HUB_V2, CONF_HUB_VERSION: 2}, + config_options={"dev_path": "/some/path"}, + ) + with ( + patch( + "homeassistant.components.insteon.api.config.async_connect", + new=mock_successful_connection, + ), + patch("homeassistant.components.insteon.api.config.devices", mock_devices), + patch("homeassistant.components.insteon.api.config.async_close"), + ): + await ws_client.send_json( + { + ID: 2, + TYPE: "insteon/config/update_modem_config", + "config": MOCK_USER_INPUT_HUB_V2, + } + ) + msg = await ws_client.receive_json() + result = msg["result"] + + assert result["status"] == "success" + + +async def test_update_modem_config_hub_v1( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test getting the Insteon HubV1 modem configuration schema.""" + + ws_client, mock_devices, _, _ = await async_mock_setup( + hass, + hass_ws_client, + config_data={**MOCK_USER_INPUT_HUB_V1, CONF_HUB_VERSION: 1}, + ) + with ( + patch( + "homeassistant.components.insteon.api.config.async_connect", + new=mock_successful_connection, + ), + patch("homeassistant.components.insteon.api.config.devices", mock_devices), + patch("homeassistant.components.insteon.api.config.async_close"), + ): + await ws_client.send_json( + { + ID: 2, + TYPE: "insteon/config/update_modem_config", + "config": MOCK_USER_INPUT_HUB_V1, + } + ) + msg = await ws_client.receive_json() + result = msg["result"] + + assert result["status"] == "success" + + +async def test_update_modem_config_bad( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test updating the Insteon modem configuration with bad connection information.""" + + ws_client, mock_devices, _, _ = await async_mock_setup( + hass, + hass_ws_client, + ) + with ( + patch( + "homeassistant.components.insteon.api.config.async_connect", + new=mock_failed_connection, + ), + patch("homeassistant.components.insteon.api.config.devices", mock_devices), + patch("homeassistant.components.insteon.api.config.async_close"), + ): + await ws_client.send_json( + { + ID: 2, + TYPE: "insteon/config/update_modem_config", + "config": MOCK_USER_INPUT_PLM, + } + ) + msg = await ws_client.receive_json() + result = msg["error"] + assert result["code"] == "connection_failed" + + +async def test_update_modem_config_bad_reconnect( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test updating the Insteon modem configuration with bad connection information so reconnect to old.""" + + ws_client, mock_devices, _, _ = await async_mock_setup( + hass, + hass_ws_client, + ) + with ( + patch( + "homeassistant.components.insteon.api.config.async_connect", + new=mock_failed_connection, + ), + patch("homeassistant.components.insteon.api.config.devices", mock_devices), + patch("homeassistant.components.insteon.api.config.async_close"), + ): + mock_devices.modem.protocol = MockProtocol() + await ws_client.send_json( + { + ID: 2, + TYPE: "insteon/config/update_modem_config", + "config": MOCK_USER_INPUT_PLM, + } + ) + msg = await ws_client.receive_json() + result = msg["error"] + assert result["code"] == "connection_failed" + + +async def test_add_device_override( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test adding a device configuration override.""" + + ws_client, _, _, _ = await async_mock_setup(hass, hass_ws_client) + override = { + "address": "99.99.99", + "cat": "0x01", + "subcat": "0x03", + } + await ws_client.send_json( + {ID: 2, TYPE: "insteon/config/device_override/add", "override": override} + ) + msg = await ws_client.receive_json() + assert msg["success"] + + config_entry = hass.config_entries.async_get_entry("abcde12345") + assert len(config_entry.options[CONF_OVERRIDE]) == 1 + assert config_entry.options[CONF_OVERRIDE][0]["address"] == "99.99.99" + + +async def test_add_device_override_duplicate( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test adding a duplicate device configuration override.""" + + override = { + "address": "99.99.99", + "cat": "0x01", + "subcat": "0x03", + } + + ws_client, _, _, _ = await async_mock_setup( + hass, hass_ws_client, config_options={CONF_OVERRIDE: [override]} + ) + await ws_client.send_json( + {ID: 2, TYPE: "insteon/config/device_override/add", "override": override} + ) + msg = await ws_client.receive_json() + assert msg["error"] + + +async def test_remove_device_override( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test removing a device configuration override.""" + + override = { + "address": "99.99.99", + "cat": "0x01", + "subcat": "0x03", + } + overrides = [ + override, + { + "address": "88.88.88", + "cat": "0x02", + "subcat": "0x05", + }, + ] + + ws_client, _, _, _ = await async_mock_setup( + hass, hass_ws_client, config_options={CONF_OVERRIDE: overrides} + ) + await ws_client.send_json( + { + ID: 2, + TYPE: "insteon/config/device_override/remove", + "device_address": "99.99.99", + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + + config_entry = hass.config_entries.async_get_entry("abcde12345") + assert len(config_entry.options[CONF_OVERRIDE]) == 1 + assert config_entry.options[CONF_OVERRIDE][0]["address"] == "88.88.88" + + +async def test_add_device_override_with_x10( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test adding a device configuration override when X10 configuration exists.""" + + x10_device = {"housecode": "a", "unitcode": 1, "platform": "switch"} + ws_client, _, _, _ = await async_mock_setup( + hass, hass_ws_client, config_options={CONF_X10: [x10_device]} + ) + override = { + "address": "99.99.99", + "cat": "0x01", + "subcat": "0x03", + } + await ws_client.send_json( + {ID: 2, TYPE: "insteon/config/device_override/add", "override": override} + ) + msg = await ws_client.receive_json() + assert msg["success"] + + config_entry = hass.config_entries.async_get_entry("abcde12345") + assert len(config_entry.options[CONF_X10]) == 1 + + +async def test_remove_device_override_with_x10( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test removing a device configuration override when X10 configuration exists.""" + + override = { + "address": "99.99.99", + "cat": "0x01", + "subcat": "0x03", + } + overrides = [ + override, + { + "address": "88.88.88", + "cat": "0x02", + "subcat": "0x05", + }, + ] + x10_device = {"housecode": "a", "unitcode": 1, "platform": "switch"} + + ws_client, _, _, _ = await async_mock_setup( + hass, + hass_ws_client, + config_options={CONF_OVERRIDE: overrides, CONF_X10: [x10_device]}, + ) + await ws_client.send_json( + { + ID: 2, + TYPE: "insteon/config/device_override/remove", + "device_address": "99.99.99", + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + + config_entry = hass.config_entries.async_get_entry("abcde12345") + assert len(config_entry.options[CONF_X10]) == 1 + + +async def test_remove_device_override_no_overrides( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test removing a device override when no overrides are configured.""" + + ws_client, _, _, _ = await async_mock_setup(hass, hass_ws_client) + await ws_client.send_json( + { + ID: 2, + TYPE: "insteon/config/device_override/remove", + "device_address": "99.99.99", + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + + config_entry = hass.config_entries.async_get_entry("abcde12345") + assert not config_entry.options.get(CONF_OVERRIDE) diff --git a/tests/components/insteon/test_api_device.py b/tests/components/insteon/test_api_device.py index f3c67d479d0..29d601eb3ef 100644 --- a/tests/components/insteon/test_api_device.py +++ b/tests/components/insteon/test_api_device.py @@ -18,48 +18,29 @@ from homeassistant.components.insteon.api.device import ( TYPE, async_device_name, ) -from homeassistant.components.insteon.const import DOMAIN, MULTIPLE +from homeassistant.components.insteon.const import ( + CONF_OVERRIDE, + CONF_X10, + DOMAIN, + MULTIPLE, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from .const import MOCK_USER_INPUT_PLM from .mock_devices import MockDevices +from .mock_setup import async_mock_setup from tests.common import MockConfigEntry from tests.typing import WebSocketGenerator -async def _async_setup(hass, hass_ws_client): - """Set up for tests.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - entry_id="abcde12345", - data=MOCK_USER_INPUT_PLM, - options={}, - ) - config_entry.add_to_hass(hass) - async_load_api(hass) - - ws_client = await hass_ws_client(hass) - devices = MockDevices() - await devices.async_load() - - dev_reg = dr.async_get(hass) - # Create device registry entry for mock node - ha_device = dev_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, - identifiers={(DOMAIN, "11.11.11")}, - name="Device 11.11.11", - ) - return ws_client, devices, ha_device, dev_reg - - -async def test_get_device_api( +async def test_get_config( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test getting an Insteon device.""" - ws_client, devices, ha_device, _ = await _async_setup(hass, hass_ws_client) + ws_client, devices, ha_device, _ = await async_mock_setup(hass, hass_ws_client) with patch.object(insteon.api.device, "devices", devices): await ws_client.send_json( {ID: 2, TYPE: "insteon/device/get", DEVICE_ID: ha_device.id} @@ -76,7 +57,7 @@ async def test_no_ha_device( ) -> None: """Test response when no HA device exists.""" - ws_client, devices, _, _ = await _async_setup(hass, hass_ws_client) + ws_client, devices, _, _ = await async_mock_setup(hass, hass_ws_client) with patch.object(insteon.api.device, "devices", devices): await ws_client.send_json( {ID: 2, TYPE: "insteon/device/get", DEVICE_ID: "not_a_device"} @@ -141,7 +122,7 @@ async def test_get_ha_device_name( ) -> None: """Test getting the HA device name from an Insteon address.""" - _, devices, _, device_reg = await _async_setup(hass, hass_ws_client) + _, devices, _, device_reg = await async_mock_setup(hass, hass_ws_client) with patch.object(insteon.api.device, "devices", devices): # Test a real HA and Insteon device @@ -164,7 +145,7 @@ async def test_add_device_api( ) -> None: """Test adding an Insteon device.""" - ws_client, devices, _, _ = await _async_setup(hass, hass_ws_client) + ws_client, devices, _, _ = await async_mock_setup(hass, hass_ws_client) with patch.object(insteon.api.device, "devices", devices): await ws_client.send_json({ID: 2, TYPE: "insteon/device/add", MULTIPLE: True}) @@ -194,7 +175,7 @@ async def test_cancel_add_device( ) -> None: """Test cancelling adding of a new device.""" - ws_client, devices, _, _ = await _async_setup(hass, hass_ws_client) + ws_client, devices, _, _ = await async_mock_setup(hass, hass_ws_client) with patch.object(insteon.api.aldb, "devices", devices): await ws_client.send_json( @@ -205,3 +186,127 @@ async def test_cancel_add_device( ) msg = await ws_client.receive_json() assert msg["success"] + + +async def test_add_x10_device( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test adding an X10 device.""" + + ws_client, _, _, _ = await async_mock_setup(hass, hass_ws_client) + x10_device = {"housecode": "a", "unitcode": 1, "platform": "switch"} + await ws_client.send_json( + {ID: 2, TYPE: "insteon/device/add_x10", "x10_device": x10_device} + ) + msg = await ws_client.receive_json() + assert msg["success"] + + config_entry = hass.config_entries.async_get_entry("abcde12345") + assert len(config_entry.options[CONF_X10]) == 1 + assert config_entry.options[CONF_X10][0]["housecode"] == "a" + assert config_entry.options[CONF_X10][0]["unitcode"] == 1 + assert config_entry.options[CONF_X10][0]["platform"] == "switch" + + +async def test_add_x10_device_duplicate( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test adding a duplicate X10 device.""" + + x10_device = {"housecode": "a", "unitcode": 1, "platform": "switch"} + + ws_client, _, _, _ = await async_mock_setup( + hass, hass_ws_client, config_options={CONF_X10: [x10_device]} + ) + await ws_client.send_json( + {ID: 2, TYPE: "insteon/device/add_x10", "x10_device": x10_device} + ) + msg = await ws_client.receive_json() + assert msg["error"] + assert msg["error"]["code"] == "duplicate" + + +async def test_remove_device( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test removing an Insteon device.""" + ws_client, _, _, _ = await async_mock_setup(hass, hass_ws_client) + await ws_client.send_json( + { + ID: 2, + TYPE: "insteon/device/remove", + "device_address": "11.22.33", + "remove_all_refs": True, + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + + +async def test_remove_x10_device( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test removing an X10 device.""" + ws_client, _, _, _ = await async_mock_setup(hass, hass_ws_client) + await ws_client.send_json( + { + ID: 2, + TYPE: "insteon/device/remove", + "device_address": "X10.A.01", + "remove_all_refs": True, + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + + +async def test_remove_one_x10_device( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test one X10 device without removing others.""" + x10_device = {"housecode": "a", "unitcode": 1, "platform": "light", "dim_steps": 22} + x10_devices = [ + x10_device, + {"housecode": "a", "unitcode": 2, "platform": "switch"}, + ] + ws_client, _, _, _ = await async_mock_setup( + hass, hass_ws_client, config_options={CONF_X10: x10_devices} + ) + await ws_client.send_json( + { + ID: 2, + TYPE: "insteon/device/remove", + "device_address": "X10.A.01", + "remove_all_refs": True, + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + config_entry = hass.config_entries.async_get_entry("abcde12345") + assert len(config_entry.options[CONF_X10]) == 1 + assert config_entry.options[CONF_X10][0]["housecode"] == "a" + assert config_entry.options[CONF_X10][0]["unitcode"] == 2 + + +async def test_remove_device_with_overload( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test removing an Insteon device that has a device overload.""" + overload = {"address": "99.99.99", "cat": 1, "subcat": 3} + overloads = {CONF_OVERRIDE: [overload]} + ws_client, _, _, _ = await async_mock_setup( + hass, hass_ws_client, config_options=overloads + ) + await ws_client.send_json( + { + ID: 2, + TYPE: "insteon/device/remove", + "device_address": "99.99.99", + "remove_all_refs": True, + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + + config_entry = hass.config_entries.async_get_entry("abcde12345") + assert not config_entry.options.get(CONF_OVERRIDE) diff --git a/tests/components/insteon/test_config_flow.py b/tests/components/insteon/test_config_flow.py index 7cc0eefc0b5..4d3fb815463 100644 --- a/tests/components/insteon/test_config_flow.py +++ b/tests/components/insteon/test_config_flow.py @@ -8,38 +8,14 @@ from voluptuous_serialize import convert from homeassistant import config_entries from homeassistant.components import dhcp, usb from homeassistant.components.insteon.config_flow import ( - STEP_ADD_OVERRIDE, - STEP_ADD_X10, - STEP_CHANGE_HUB_CONFIG, - STEP_CHANGE_PLM_CONFIG, STEP_HUB_V1, STEP_HUB_V2, STEP_PLM, STEP_PLM_MANUALLY, - STEP_REMOVE_OVERRIDE, - STEP_REMOVE_X10, -) -from homeassistant.components.insteon.const import ( - CONF_CAT, - CONF_DIM_STEPS, - CONF_HOUSECODE, - CONF_HUB_VERSION, - CONF_OVERRIDE, - CONF_SUBCAT, - CONF_UNITCODE, - CONF_X10, - DOMAIN, ) +from homeassistant.components.insteon.const import CONF_HUB_VERSION, DOMAIN from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import ( - CONF_ADDRESS, - CONF_DEVICE, - CONF_HOST, - CONF_PASSWORD, - CONF_PLATFORM, - CONF_PORT, - CONF_USERNAME, -) +from homeassistant.const import CONF_DEVICE, CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -52,11 +28,8 @@ from .const import ( PATCH_ASYNC_SETUP, PATCH_ASYNC_SETUP_ENTRY, PATCH_CONNECTION, - PATCH_CONNECTION_CLOSE, - PATCH_DEVICES, PATCH_USB_LIST, ) -from .mock_devices import MockDevices from tests.common import MockConfigEntry @@ -294,379 +267,6 @@ async def test_failed_connection_hub(hass: HomeAssistant) -> None: assert result2["errors"] == {"base": "cannot_connect"} -async def _options_init_form(hass, entry_id, step): - """Run the init options form.""" - with patch(PATCH_ASYNC_SETUP_ENTRY, return_value=True): - result = await hass.config_entries.options.async_init(entry_id) - - assert result["type"] is FlowResultType.MENU - assert result["step_id"] == "init" - - return await hass.config_entries.options.async_configure( - result["flow_id"], - {"next_step_id": step}, - ) - - -async def _options_form( - hass, flow_id, user_input, connection=mock_successful_connection -): - """Test an options form.""" - mock_devices = MockDevices(connected=True) - await mock_devices.async_load() - mock_devices.modem = mock_devices["AA.AA.AA"] - with ( - patch(PATCH_CONNECTION, new=connection), - patch(PATCH_ASYNC_SETUP_ENTRY, return_value=True) as mock_setup_entry, - patch(PATCH_DEVICES, mock_devices), - patch(PATCH_CONNECTION_CLOSE), - ): - result = await hass.config_entries.options.async_configure(flow_id, user_input) - return result, mock_setup_entry - - -async def test_options_change_hub_config(hass: HomeAssistant) -> None: - """Test changing Hub v2 config.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - entry_id="abcde12345", - data={**MOCK_USER_INPUT_HUB_V2, CONF_HUB_VERSION: 2}, - options={}, - ) - - config_entry.add_to_hass(hass) - result = await _options_init_form( - hass, config_entry.entry_id, STEP_CHANGE_HUB_CONFIG - ) - - user_input = { - CONF_HOST: "2.3.4.5", - CONF_PORT: 9999, - CONF_USERNAME: "new username", - CONF_PASSWORD: "new password", - } - result, _ = await _options_form(hass, result["flow_id"], user_input) - assert result["type"] is FlowResultType.CREATE_ENTRY - assert config_entry.options == {} - assert config_entry.data == {**user_input, CONF_HUB_VERSION: 2} - - -async def test_options_change_hub_bad_config(hass: HomeAssistant) -> None: - """Test changing Hub v2 with bad config.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - entry_id="abcde12345", - data={**MOCK_USER_INPUT_HUB_V2, CONF_HUB_VERSION: 2}, - options={}, - ) - - config_entry.add_to_hass(hass) - result = await _options_init_form( - hass, config_entry.entry_id, STEP_CHANGE_HUB_CONFIG - ) - - user_input = { - CONF_HOST: "2.3.4.5", - CONF_PORT: 9999, - CONF_USERNAME: "new username", - CONF_PASSWORD: "new password", - } - result, _ = await _options_form( - hass, result["flow_id"], user_input, mock_failed_connection - ) - - assert result["type"] is FlowResultType.FORM - assert result["errors"]["base"] == "cannot_connect" - - -async def test_options_change_plm_config(hass: HomeAssistant) -> None: - """Test changing PLM config.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - entry_id="abcde12345", - data=MOCK_USER_INPUT_PLM, - options={}, - ) - - config_entry.add_to_hass(hass) - result = await _options_init_form( - hass, config_entry.entry_id, STEP_CHANGE_PLM_CONFIG - ) - - user_input = {CONF_DEVICE: "/dev/ttyUSB0"} - result, _ = await _options_form(hass, result["flow_id"], user_input) - - assert result["type"] is FlowResultType.CREATE_ENTRY - assert config_entry.options == {} - assert config_entry.data == user_input - - -async def test_options_change_plm_bad_config(hass: HomeAssistant) -> None: - """Test changing PLM config.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - entry_id="abcde12345", - data=MOCK_USER_INPUT_PLM, - options={}, - ) - - config_entry.add_to_hass(hass) - result = await _options_init_form( - hass, config_entry.entry_id, STEP_CHANGE_PLM_CONFIG - ) - - user_input = {CONF_DEVICE: "/dev/ttyUSB0"} - result, _ = await _options_form( - hass, result["flow_id"], user_input, mock_failed_connection - ) - - assert result["type"] is FlowResultType.FORM - - assert result["type"] is FlowResultType.FORM - assert result["errors"]["base"] == "cannot_connect" - - -async def test_options_add_device_override(hass: HomeAssistant) -> None: - """Test adding a device override.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - entry_id="abcde12345", - data={**MOCK_USER_INPUT_HUB_V2, CONF_HUB_VERSION: 2}, - options={}, - ) - - config_entry.add_to_hass(hass) - result = await _options_init_form(hass, config_entry.entry_id, STEP_ADD_OVERRIDE) - - user_input = { - CONF_ADDRESS: "1a2b3c", - CONF_CAT: "0x04", - CONF_SUBCAT: "0xaa", - } - result, _ = await _options_form(hass, result["flow_id"], user_input) - - assert result["type"] is FlowResultType.CREATE_ENTRY - assert len(config_entry.options[CONF_OVERRIDE]) == 1 - assert config_entry.options[CONF_OVERRIDE][0][CONF_ADDRESS] == "1A.2B.3C" - assert config_entry.options[CONF_OVERRIDE][0][CONF_CAT] == 4 - assert config_entry.options[CONF_OVERRIDE][0][CONF_SUBCAT] == 170 - - result2 = await _options_init_form(hass, config_entry.entry_id, STEP_ADD_OVERRIDE) - - user_input = { - CONF_ADDRESS: "4d5e6f", - CONF_CAT: "05", - CONF_SUBCAT: "bb", - } - result3, _ = await _options_form(hass, result2["flow_id"], user_input) - - assert len(config_entry.options[CONF_OVERRIDE]) == 2 - assert config_entry.options[CONF_OVERRIDE][1][CONF_ADDRESS] == "4D.5E.6F" - assert config_entry.options[CONF_OVERRIDE][1][CONF_CAT] == 5 - assert config_entry.options[CONF_OVERRIDE][1][CONF_SUBCAT] == 187 - - # If result1 eq result2 the changes will not save - assert result["data"] != result3["data"] - - -async def test_options_remove_device_override(hass: HomeAssistant) -> None: - """Test removing a device override.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - entry_id="abcde12345", - data={**MOCK_USER_INPUT_HUB_V2, CONF_HUB_VERSION: 2}, - options={ - CONF_OVERRIDE: [ - {CONF_ADDRESS: "1A.2B.3C", CONF_CAT: 6, CONF_SUBCAT: 100}, - {CONF_ADDRESS: "4D.5E.6F", CONF_CAT: 7, CONF_SUBCAT: 200}, - ] - }, - ) - - config_entry.add_to_hass(hass) - result = await _options_init_form(hass, config_entry.entry_id, STEP_REMOVE_OVERRIDE) - - user_input = {CONF_ADDRESS: "1A.2B.3C"} - result, _ = await _options_form(hass, result["flow_id"], user_input) - - assert result["type"] is FlowResultType.CREATE_ENTRY - assert len(config_entry.options[CONF_OVERRIDE]) == 1 - - -async def test_options_remove_device_override_with_x10(hass: HomeAssistant) -> None: - """Test removing a device override when an X10 device is configured.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - entry_id="abcde12345", - data={**MOCK_USER_INPUT_HUB_V2, CONF_HUB_VERSION: 2}, - options={ - CONF_OVERRIDE: [ - {CONF_ADDRESS: "1A.2B.3C", CONF_CAT: 6, CONF_SUBCAT: 100}, - {CONF_ADDRESS: "4D.5E.6F", CONF_CAT: 7, CONF_SUBCAT: 200}, - ], - CONF_X10: [ - { - CONF_HOUSECODE: "d", - CONF_UNITCODE: 5, - CONF_PLATFORM: "light", - CONF_DIM_STEPS: 22, - } - ], - }, - ) - - config_entry.add_to_hass(hass) - result = await _options_init_form(hass, config_entry.entry_id, STEP_REMOVE_OVERRIDE) - - user_input = {CONF_ADDRESS: "1A.2B.3C"} - result, _ = await _options_form(hass, result["flow_id"], user_input) - - assert result["type"] is FlowResultType.CREATE_ENTRY - assert len(config_entry.options[CONF_OVERRIDE]) == 1 - assert len(config_entry.options[CONF_X10]) == 1 - - -async def test_options_add_x10_device(hass: HomeAssistant) -> None: - """Test adding an X10 device.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - entry_id="abcde12345", - data={**MOCK_USER_INPUT_HUB_V2, CONF_HUB_VERSION: 2}, - options={}, - ) - - config_entry.add_to_hass(hass) - result = await _options_init_form(hass, config_entry.entry_id, STEP_ADD_X10) - - user_input = { - CONF_HOUSECODE: "c", - CONF_UNITCODE: 12, - CONF_PLATFORM: "light", - CONF_DIM_STEPS: 18, - } - result2, _ = await _options_form(hass, result["flow_id"], user_input) - - assert result2["type"] is FlowResultType.CREATE_ENTRY - assert len(config_entry.options[CONF_X10]) == 1 - assert config_entry.options[CONF_X10][0][CONF_HOUSECODE] == "c" - assert config_entry.options[CONF_X10][0][CONF_UNITCODE] == 12 - assert config_entry.options[CONF_X10][0][CONF_PLATFORM] == "light" - assert config_entry.options[CONF_X10][0][CONF_DIM_STEPS] == 18 - - result = await _options_init_form(hass, config_entry.entry_id, STEP_ADD_X10) - user_input = { - CONF_HOUSECODE: "d", - CONF_UNITCODE: 10, - CONF_PLATFORM: "binary_sensor", - CONF_DIM_STEPS: 15, - } - result3, _ = await _options_form(hass, result["flow_id"], user_input) - - assert result3["type"] is FlowResultType.CREATE_ENTRY - assert len(config_entry.options[CONF_X10]) == 2 - assert config_entry.options[CONF_X10][1][CONF_HOUSECODE] == "d" - assert config_entry.options[CONF_X10][1][CONF_UNITCODE] == 10 - assert config_entry.options[CONF_X10][1][CONF_PLATFORM] == "binary_sensor" - assert config_entry.options[CONF_X10][1][CONF_DIM_STEPS] == 15 - - # If result2 eq result3 the changes will not save - assert result2["data"] != result3["data"] - - -async def test_options_remove_x10_device(hass: HomeAssistant) -> None: - """Test removing an X10 device.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - entry_id="abcde12345", - data={**MOCK_USER_INPUT_HUB_V2, CONF_HUB_VERSION: 2}, - options={ - CONF_X10: [ - { - CONF_HOUSECODE: "C", - CONF_UNITCODE: 4, - CONF_PLATFORM: "light", - CONF_DIM_STEPS: 18, - }, - { - CONF_HOUSECODE: "D", - CONF_UNITCODE: 10, - CONF_PLATFORM: "binary_sensor", - CONF_DIM_STEPS: 15, - }, - ] - }, - ) - - config_entry.add_to_hass(hass) - result = await _options_init_form(hass, config_entry.entry_id, STEP_REMOVE_X10) - - user_input = {CONF_DEVICE: "Housecode: C, Unitcode: 4"} - result, _ = await _options_form(hass, result["flow_id"], user_input) - - assert result["type"] is FlowResultType.CREATE_ENTRY - assert len(config_entry.options[CONF_X10]) == 1 - - -async def test_options_remove_x10_device_with_override(hass: HomeAssistant) -> None: - """Test removing an X10 device when a device override is configured.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - entry_id="abcde12345", - data={**MOCK_USER_INPUT_HUB_V2, CONF_HUB_VERSION: 2}, - options={ - CONF_X10: [ - { - CONF_HOUSECODE: "C", - CONF_UNITCODE: 4, - CONF_PLATFORM: "light", - CONF_DIM_STEPS: 18, - }, - { - CONF_HOUSECODE: "D", - CONF_UNITCODE: 10, - CONF_PLATFORM: "binary_sensor", - CONF_DIM_STEPS: 15, - }, - ], - CONF_OVERRIDE: [{CONF_ADDRESS: "1A.2B.3C", CONF_CAT: 1, CONF_SUBCAT: 18}], - }, - ) - - config_entry.add_to_hass(hass) - result = await _options_init_form(hass, config_entry.entry_id, STEP_REMOVE_X10) - - user_input = {CONF_DEVICE: "Housecode: C, Unitcode: 4"} - result, _ = await _options_form(hass, result["flow_id"], user_input) - - assert result["type"] is FlowResultType.CREATE_ENTRY - assert len(config_entry.options[CONF_X10]) == 1 - assert len(config_entry.options[CONF_OVERRIDE]) == 1 - - -async def test_options_override_bad_data(hass: HomeAssistant) -> None: - """Test for bad data in a device override.""" - - config_entry = MockConfigEntry( - domain=DOMAIN, - entry_id="abcde12345", - data={**MOCK_USER_INPUT_HUB_V2, CONF_HUB_VERSION: 2}, - options={}, - ) - - config_entry.add_to_hass(hass) - result = await _options_init_form(hass, config_entry.entry_id, STEP_ADD_OVERRIDE) - - user_input = { - CONF_ADDRESS: "zzzzzz", - CONF_CAT: "bad", - CONF_SUBCAT: "data", - } - result, _ = await _options_form(hass, result["flow_id"], user_input) - - assert result["type"] is FlowResultType.FORM - assert result["errors"] == {"base": "input_error"} - - async def test_discovery_via_usb(hass: HomeAssistant) -> None: """Test usb flow.""" discovery_info = usb.UsbServiceInfo( diff --git a/tests/components/insteon/test_init.py b/tests/components/insteon/test_init.py index a4e8da03345..c5524ff1919 100644 --- a/tests/components/insteon/test_init.py +++ b/tests/components/insteon/test_init.py @@ -1,6 +1,5 @@ """Test the init file for the Insteon component.""" -import asyncio from unittest.mock import patch import pytest @@ -11,7 +10,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from .const import MOCK_USER_INPUT_PLM, PATCH_CONNECTION +from .const import MOCK_USER_INPUT_PLM from .mock_devices import MockDevices from tests.common import MockConfigEntry @@ -70,22 +69,24 @@ async def test_setup_entry_failed_connection( async def test_import_frontend_dev_url(hass: HomeAssistant) -> None: """Test importing a dev_url config entry.""" - config = {} - config[DOMAIN] = {CONF_DEV_PATH: "/some/path"} + config_entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_USER_INPUT_PLM, options={CONF_DEV_PATH: "/some/path"} + ) + config_entry.add_to_hass(hass) with ( patch.object(insteon, "async_connect", new=mock_successful_connection), - patch.object(insteon, "close_insteon_connection"), + patch.object(insteon, "async_close") as mock_close, patch.object(insteon, "devices", new=MockDevices()), - patch( - PATCH_CONNECTION, - new=mock_successful_connection, - ), ): assert await async_setup_component( hass, insteon.DOMAIN, - config, + {}, ) await hass.async_block_till_done() - await asyncio.sleep(0.01) + assert hass.data[DOMAIN][CONF_DEV_PATH] == "/some/path" + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert insteon.devices.async_save.call_count == 1 + assert mock_close.called From a99ecb024eef0ded73a467f71af9d672f31c60ae Mon Sep 17 00:00:00 2001 From: brave0d <138725265+brave0d@users.noreply.github.com> Date: Tue, 16 Apr 2024 08:24:32 +0100 Subject: [PATCH 594/967] New BMW sensor for climate activity (#110287) * add sensor with climate activity status * Update strings.json * use icon translation and is_available for sensor * use enum with translations * Return None if value is UNKNOWN * fix getting the value: x.value * fix getting the value: x instead of x.value * Fix tests and pre-commit --------- Co-authored-by: Richard --- .../components/bmw_connected_drive/const.py | 7 +++ .../components/bmw_connected_drive/icons.json | 3 + .../components/bmw_connected_drive/sensor.py | 11 +++- .../bmw_connected_drive/strings.json | 9 +++ .../snapshots/test_sensor.ambr | 57 +++++++++++++++++++ 5 files changed, 86 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bmw_connected_drive/const.py b/homeassistant/components/bmw_connected_drive/const.py index 49990977f71..5374b52e684 100644 --- a/homeassistant/components/bmw_connected_drive/const.py +++ b/homeassistant/components/bmw_connected_drive/const.py @@ -28,3 +28,10 @@ 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/icons.json b/homeassistant/components/bmw_connected_drive/icons.json index a4eb37b369a..fc30b87ed3f 100644 --- a/homeassistant/components/bmw_connected_drive/icons.json +++ b/homeassistant/components/bmw_connected_drive/icons.json @@ -85,6 +85,9 @@ }, "remaining_fuel_percent": { "default": "mdi:gas-station" + }, + "climate_status": { + "default": "mdi:fan" } }, "switch": { diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index e1ed398cfec..d3366543c55 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -23,7 +23,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from . import BMWBaseEntity -from .const import DOMAIN, UNIT_MAP +from .const import CLIMATE_ACTIVITY_STATE, DOMAIN, UNIT_MAP from .coordinator import BMWDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -153,6 +153,15 @@ SENSOR_TYPES: list[BMWSensorEntityDescription] = [ state_class=SensorStateClass.MEASUREMENT, is_available=lambda v: v.is_lsc_enabled and v.has_combustion_drivetrain, ), + BMWSensorEntityDescription( + key="activity", + translation_key="climate_status", + 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, + ), ] diff --git a/homeassistant/components/bmw_connected_drive/strings.json b/homeassistant/components/bmw_connected_drive/strings.json index 69abd97ddfe..539c281a1a5 100644 --- a/homeassistant/components/bmw_connected_drive/strings.json +++ b/homeassistant/components/bmw_connected_drive/strings.json @@ -122,6 +122,15 @@ }, "remaining_fuel_percent": { "name": "Remaining fuel percent" + }, + "climate_status": { + "name": "Climate status", + "state": { + "cooling": "Cooling", + "heating": "Heating", + "inactive": "Inactive", + "standby": "Standby" + } } }, "switch": { diff --git a/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr b/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr index c9dd4e3ddb8..dcf68622fdc 100644 --- a/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr +++ b/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr @@ -96,6 +96,25 @@ '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', @@ -191,6 +210,25 @@ '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', @@ -261,6 +299,25 @@ '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', From f62fb76765513a5f1b0f96701fac29c1a35d636f Mon Sep 17 00:00:00 2001 From: Stephen Alderman Date: Tue, 16 Apr 2024 08:29:02 +0100 Subject: [PATCH 595/967] Add Config Flow to LG Netcast (#104913) * Add Config Flow to lg_netcast * Add YAML import to Lg Netcast ConfigFlow Deprecates YAML config support * Add LG Netcast Device triggers for turn_on action * Add myself to LG Netcast codeowners * Remove unnecessary user_input validation check. * Move netcast discovery logic to the backend * Use FlowResultType Enum for tests * Mock pylgnetcast.query_device_info instead of _send_to_tv * Refactor lg_netcast client discovery, simplify YAML import * Simplify CONF_NAME to use friendly name Fix: Use Friendly name for Name * Expose model to DeviceInfo * Add test for testing YAML import when not TV not online * Switch to entity_name for LGTVDevice * Add data_description to host field in user step * Wrap try only around _get_session_id * Send regular request for access_token to ensure it display on the TV * Stop displaying access token when flow is aborted * Remove config_flow only consts and minor fixups * Simplify media_player logic & raise new migration issue * Add async_unload_entry * Create issues when import config flow fails, and raise only a single yaml deprecation issue type * Remove single use trigger helpers * Bump issue deprecation breakage version * Lint --------- Co-authored-by: Erik Montnemery --- CODEOWNERS | 3 +- .../components/lg_netcast/__init__.py | 32 +++ .../components/lg_netcast/config_flow.py | 217 +++++++++++++++ homeassistant/components/lg_netcast/const.py | 6 + .../components/lg_netcast/device_trigger.py | 88 ++++++ .../components/lg_netcast/helpers.py | 59 ++++ .../components/lg_netcast/manifest.json | 7 +- .../components/lg_netcast/media_player.py | 92 +++++-- .../components/lg_netcast/strings.json | 46 ++++ .../components/lg_netcast/trigger.py | 49 ++++ .../lg_netcast/triggers/__init__.py | 1 + .../components/lg_netcast/triggers/turn_on.py | 115 ++++++++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 4 +- requirements_all.txt | 2 +- requirements_test_all.txt | 3 + tests/components/lg_netcast/__init__.py | 116 ++++++++ tests/components/lg_netcast/conftest.py | 11 + .../components/lg_netcast/test_config_flow.py | 252 ++++++++++++++++++ .../lg_netcast/test_device_trigger.py | 148 ++++++++++ tests/components/lg_netcast/test_trigger.py | 189 +++++++++++++ 21 files changed, 1411 insertions(+), 30 deletions(-) create mode 100644 homeassistant/components/lg_netcast/config_flow.py create mode 100644 homeassistant/components/lg_netcast/device_trigger.py create mode 100644 homeassistant/components/lg_netcast/helpers.py create mode 100644 homeassistant/components/lg_netcast/strings.json create mode 100644 homeassistant/components/lg_netcast/trigger.py create mode 100644 homeassistant/components/lg_netcast/triggers/__init__.py create mode 100644 homeassistant/components/lg_netcast/triggers/turn_on.py create mode 100644 tests/components/lg_netcast/__init__.py create mode 100644 tests/components/lg_netcast/conftest.py create mode 100644 tests/components/lg_netcast/test_config_flow.py create mode 100644 tests/components/lg_netcast/test_device_trigger.py create mode 100644 tests/components/lg_netcast/test_trigger.py diff --git a/CODEOWNERS b/CODEOWNERS index 39fa804314d..d93a8f6b9d3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -753,7 +753,8 @@ build.json @home-assistant/supervisor /tests/components/leaone/ @bdraco /homeassistant/components/led_ble/ @bdraco /tests/components/led_ble/ @bdraco -/homeassistant/components/lg_netcast/ @Drafteed +/homeassistant/components/lg_netcast/ @Drafteed @splinter98 +/tests/components/lg_netcast/ @Drafteed @splinter98 /homeassistant/components/lidarr/ @tkdrob /tests/components/lidarr/ @tkdrob /homeassistant/components/light/ @home-assistant/core diff --git a/homeassistant/components/lg_netcast/__init__.py b/homeassistant/components/lg_netcast/__init__.py index 232d7bd10b8..f6fb834ab11 100644 --- a/homeassistant/components/lg_netcast/__init__.py +++ b/homeassistant/components/lg_netcast/__init__.py @@ -1 +1,33 @@ """The lg_netcast component.""" + +from typing import Final + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_validation as cv + +from .const import DOMAIN + +PLATFORMS: Final[list[Platform]] = [Platform.MEDIA_PLAYER] + +CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Set up a config entry.""" + hass.data.setdefault(DOMAIN, {}) + + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) + + return True + + +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: + del hass.data[DOMAIN][entry.entry_id] + + return unload_ok diff --git a/homeassistant/components/lg_netcast/config_flow.py b/homeassistant/components/lg_netcast/config_flow.py new file mode 100644 index 00000000000..3c1d3d73e0f --- /dev/null +++ b/homeassistant/components/lg_netcast/config_flow.py @@ -0,0 +1,217 @@ +"""Config flow to configure the LG Netcast TV integration.""" + +from __future__ import annotations + +import contextlib +from datetime import datetime, timedelta +from typing import TYPE_CHECKING, Any + +from pylgnetcast import AccessTokenError, LgNetCastClient, SessionIdError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlowResult +from homeassistant.const import ( + CONF_ACCESS_TOKEN, + CONF_HOST, + CONF_ID, + CONF_MODEL, + CONF_NAME, +) +from homeassistant.core import CALLBACK_TYPE, DOMAIN as HOMEASSISTANT_DOMAIN, callback +from homeassistant.data_entry_flow import AbortFlow +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue +from homeassistant.util.network import is_host_valid + +from .const import DEFAULT_NAME, DOMAIN +from .helpers import LGNetCastDetailDiscoveryError, async_discover_netcast_details + +DISPLAY_ACCESS_TOKEN_INTERVAL = timedelta(seconds=1) + + +class LGNetCast(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for LG Netcast TV integration.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize config flow.""" + self.client: LgNetCastClient | None = None + self.device_config: dict[str, Any] = {} + self._discovered_devices: dict[str, Any] = {} + self._track_interval: CALLBACK_TYPE | None = None + + def create_client(self) -> None: + """Create LG Netcast client from config.""" + host = self.device_config[CONF_HOST] + access_token = self.device_config.get(CONF_ACCESS_TOKEN) + self.client = LgNetCastClient(host, access_token) + + 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: + host = user_input[CONF_HOST] + if is_host_valid(host): + self.device_config[CONF_HOST] = host + return await self.async_step_authorize() + + errors[CONF_HOST] = "invalid_host" + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({vol.Required(CONF_HOST): str}), + errors=errors, + ) + + async def async_step_import(self, config: dict[str, Any]) -> ConfigFlowResult: + """Import configuration from yaml.""" + self.device_config = { + CONF_HOST: config[CONF_HOST], + CONF_NAME: config[CONF_NAME], + } + + def _create_issue(): + async_create_issue( + self.hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.11.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "LG Netcast", + }, + ) + + try: + result: ConfigFlowResult = await self.async_step_authorize(config) + except AbortFlow as err: + if err.reason != "already_configured": + async_create_issue( + self.hass, + DOMAIN, + "deprecated_yaml_import_issue_{err.reason}", + breaks_in_ha_version="2024.11.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key=f"deprecated_yaml_import_issue_{err.reason}", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "LG Netcast", + "error_type": err.reason, + }, + ) + else: + _create_issue() + raise + + _create_issue() + + return result + + async def async_discover_client(self): + """Handle Discovery step.""" + self.create_client() + + if TYPE_CHECKING: + assert self.client is not None + + if self.device_config.get(CONF_ID): + return + + try: + details = await async_discover_netcast_details(self.hass, self.client) + except LGNetCastDetailDiscoveryError as err: + raise AbortFlow("cannot_connect") from err + + if (unique_id := details["uuid"]) is None: + raise AbortFlow("invalid_host") + + self.device_config[CONF_ID] = unique_id + self.device_config[CONF_MODEL] = details["model_name"] + + if CONF_NAME not in self.device_config: + self.device_config[CONF_NAME] = details["friendly_name"] or DEFAULT_NAME + + async def async_step_authorize( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle Authorize step.""" + errors: dict[str, str] = {} + self.async_stop_display_access_token() + + if user_input is not None and user_input.get(CONF_ACCESS_TOKEN) is not None: + self.device_config[CONF_ACCESS_TOKEN] = user_input[CONF_ACCESS_TOKEN] + + await self.async_discover_client() + assert self.client is not None + + await self.async_set_unique_id(self.device_config[CONF_ID]) + self._abort_if_unique_id_configured( + updates={CONF_HOST: self.device_config[CONF_HOST]} + ) + + try: + await self.hass.async_add_executor_job( + self.client._get_session_id # pylint: disable=protected-access + ) + except AccessTokenError: + if user_input is not None: + errors[CONF_ACCESS_TOKEN] = "invalid_access_token" + except SessionIdError: + errors["base"] = "cannot_connect" + else: + return await self.async_create_device() + + self._track_interval = async_track_time_interval( + self.hass, + self.async_display_access_token, + DISPLAY_ACCESS_TOKEN_INTERVAL, + cancel_on_shutdown=True, + ) + + return self.async_show_form( + step_id="authorize", + data_schema=vol.Schema( + { + vol.Optional(CONF_ACCESS_TOKEN): vol.All(str, vol.Length(max=6)), + } + ), + errors=errors, + ) + + async def async_display_access_token(self, _: datetime | None = None): + """Display access token on screen.""" + assert self.client is not None + with contextlib.suppress(AccessTokenError, SessionIdError): + await self.hass.async_add_executor_job( + self.client._get_session_id # pylint: disable=protected-access + ) + + @callback + def async_remove(self): + """Terminate Access token display if flow is removed.""" + self.async_stop_display_access_token() + + def async_stop_display_access_token(self): + """Stop Access token request if running.""" + if self._track_interval is not None: + self._track_interval() + self._track_interval = None + + async def async_create_device(self) -> ConfigFlowResult: + """Create LG Netcast TV Device from config.""" + assert self.client + + return self.async_create_entry( + title=self.device_config[CONF_NAME], data=self.device_config + ) diff --git a/homeassistant/components/lg_netcast/const.py b/homeassistant/components/lg_netcast/const.py index 0344ad6f177..aca01c9b870 100644 --- a/homeassistant/components/lg_netcast/const.py +++ b/homeassistant/components/lg_netcast/const.py @@ -1,3 +1,9 @@ """Constants for the lg_netcast component.""" +from typing import Final + +ATTR_MANUFACTURER: Final = "LG" + +DEFAULT_NAME: Final = "LG Netcast TV" + DOMAIN = "lg_netcast" diff --git a/homeassistant/components/lg_netcast/device_trigger.py b/homeassistant/components/lg_netcast/device_trigger.py new file mode 100644 index 00000000000..51c5ec53004 --- /dev/null +++ b/homeassistant/components/lg_netcast/device_trigger.py @@ -0,0 +1,88 @@ +"""Provides device triggers for LG Netcast.""" + +from __future__ import annotations + +from typing import Any + +import voluptuous as vol + +from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation.exceptions import ( + InvalidDeviceAutomationConfig, +) +from homeassistant.const import CONF_DEVICE_ID, CONF_PLATFORM, CONF_TYPE +from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo +from homeassistant.helpers.typing import ConfigType + +from . import trigger +from .const import DOMAIN +from .helpers import async_get_device_entry_by_device_id +from .triggers.turn_on import ( + PLATFORM_TYPE as TURN_ON_PLATFORM_TYPE, + async_get_turn_on_trigger, +) + +TRIGGER_TYPES = {TURN_ON_PLATFORM_TYPE} + +TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), + } +) + + +async def async_validate_trigger_config( + hass: HomeAssistant, config: ConfigType +) -> ConfigType: + """Validate config.""" + config = TRIGGER_SCHEMA(config) + + if config[CONF_TYPE] == TURN_ON_PLATFORM_TYPE: + device_id = config[CONF_DEVICE_ID] + + try: + device = async_get_device_entry_by_device_id(hass, device_id) + except ValueError as err: + raise InvalidDeviceAutomationConfig(err) from err + + if DOMAIN in hass.data: + for config_entry_id in device.config_entries: + if hass.data[DOMAIN].get(config_entry_id): + break + else: + raise InvalidDeviceAutomationConfig( + f"Device {device.id} is not from an existing {DOMAIN} config entry" + ) + + return config + + +async def async_get_triggers( + _hass: HomeAssistant, device_id: str +) -> list[dict[str, Any]]: + """List device triggers for LG Netcast devices.""" + return [async_get_turn_on_trigger(device_id)] + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: TriggerActionType, + trigger_info: TriggerInfo, +) -> CALLBACK_TYPE: + """Attach a trigger.""" + if (trigger_type := config[CONF_TYPE]) == TURN_ON_PLATFORM_TYPE: + trigger_config = { + CONF_PLATFORM: trigger_type, + CONF_DEVICE_ID: config[CONF_DEVICE_ID], + } + trigger_config = await trigger.async_validate_trigger_config( + hass, trigger_config + ) + return await trigger.async_attach_trigger( + hass, trigger_config, action, trigger_info + ) + + raise HomeAssistantError(f"Unhandled trigger type {trigger_type}") diff --git a/homeassistant/components/lg_netcast/helpers.py b/homeassistant/components/lg_netcast/helpers.py new file mode 100644 index 00000000000..7cfc0d50271 --- /dev/null +++ b/homeassistant/components/lg_netcast/helpers.py @@ -0,0 +1,59 @@ +"""Helper functions for LG Netcast TV.""" + +from typing import TypedDict +import xml.etree.ElementTree as ET + +from pylgnetcast import LgNetCastClient +from requests import RequestException + +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.device_registry import DeviceEntry + +from .const import DOMAIN + + +class LGNetCastDetailDiscoveryError(Exception): + """Unable to retrieve details from Netcast TV.""" + + +class NetcastDetails(TypedDict): + """Netcast TV Details.""" + + uuid: str + model_name: str + friendly_name: str + + +async def async_discover_netcast_details( + hass: HomeAssistant, client: LgNetCastClient +) -> NetcastDetails: + """Discover UUID and Model Name from Netcast Tv.""" + try: + resp = await hass.async_add_executor_job(client.query_device_info) + except RequestException as err: + raise LGNetCastDetailDiscoveryError( + f"Error in connecting to {client.url}" + ) from err + except ET.ParseError as err: + raise LGNetCastDetailDiscoveryError("Invalid XML") from err + + if resp is None: + raise LGNetCastDetailDiscoveryError("Empty response received") + + return resp + + +@callback +def async_get_device_entry_by_device_id( + hass: HomeAssistant, device_id: str +) -> DeviceEntry: + """Get Device Entry from Device Registry by device ID. + + Raises ValueError if device ID is invalid. + """ + device_reg = dr.async_get(hass) + if (device := device_reg.async_get(device_id)) is None: + raise ValueError(f"Device {device_id} is not a valid {DOMAIN} device.") + + return device diff --git a/homeassistant/components/lg_netcast/manifest.json b/homeassistant/components/lg_netcast/manifest.json index 8a63e064b41..cf91374feb7 100644 --- a/homeassistant/components/lg_netcast/manifest.json +++ b/homeassistant/components/lg_netcast/manifest.json @@ -1,9 +1,12 @@ { "domain": "lg_netcast", "name": "LG Netcast", - "codeowners": ["@Drafteed"], + "codeowners": ["@Drafteed", "@splinter98"], + "config_flow": true, + "dependencies": ["network"], "documentation": "https://www.home-assistant.io/integrations/lg_netcast", + "integration_type": "device", "iot_class": "local_polling", "loggers": ["pylgnetcast"], - "requirements": ["pylgnetcast==0.3.7"] + "requirements": ["pylgnetcast==0.3.9"] } diff --git a/homeassistant/components/lg_netcast/media_player.py b/homeassistant/components/lg_netcast/media_player.py index 9f6e88dc614..3fc07cab12b 100644 --- a/homeassistant/components/lg_netcast/media_player.py +++ b/homeassistant/components/lg_netcast/media_player.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import datetime -from typing import Any +from typing import TYPE_CHECKING, Any from pylgnetcast import LG_COMMAND, LgNetCastClient, LgNetCastError from requests import RequestException @@ -17,14 +17,19 @@ from homeassistant.components.media_player import ( MediaPlayerState, MediaType, ) -from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_NAME +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_MODEL, CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType +from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.script import Script +from homeassistant.helpers.trigger import PluggableAction from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import DOMAIN +from .const import ATTR_MANUFACTURER, DOMAIN +from .triggers.turn_on import async_get_turn_on_trigger DEFAULT_NAME = "LG TV Remote" @@ -54,23 +59,45 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform( +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up a LG Netcast Media Player from a config_entry.""" + + host = config_entry.data[CONF_HOST] + access_token = config_entry.data[CONF_ACCESS_TOKEN] + unique_id = config_entry.unique_id + name = config_entry.data.get(CONF_NAME, DEFAULT_NAME) + model = config_entry.data[CONF_MODEL] + + client = LgNetCastClient(host, access_token) + + hass.data[DOMAIN][config_entry.entry_id] = client + + async_add_entities([LgTVDevice(client, name, model, unique_id=unique_id)]) + + +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 LG TV platform.""" host = config.get(CONF_HOST) - access_token = config.get(CONF_ACCESS_TOKEN) - name = config[CONF_NAME] - on_action = config.get(CONF_ON_ACTION) - client = LgNetCastClient(host, access_token) - on_action_script = Script(hass, on_action, name, DOMAIN) if on_action else None + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config + ) - add_entities([LgTVDevice(client, name, on_action_script)], True) + if ( + result.get("type") == FlowResultType.ABORT + and result.get("reason") == "cannot_connect" + ): + raise PlatformNotReady(f"Connection error while connecting to {host}") class LgTVDevice(MediaPlayerEntity): @@ -79,19 +106,42 @@ class LgTVDevice(MediaPlayerEntity): _attr_assumed_state = True _attr_device_class = MediaPlayerDeviceClass.TV _attr_media_content_type = MediaType.CHANNEL + _attr_has_entity_name = True + _attr_name = None - def __init__(self, client, name, on_action_script): + def __init__(self, client, name, model, unique_id): """Initialize the LG TV device.""" self._client = client - self._name = name self._muted = False - self._on_action_script = on_action_script + self._turn_on = PluggableAction(self.async_write_ha_state) self._volume = 0 self._channel_id = None self._channel_name = "" self._program_name = "" self._sources = {} self._source_names = [] + self._attr_unique_id = unique_id + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + manufacturer=ATTR_MANUFACTURER, + name=name, + model=model, + ) + + async def async_added_to_hass(self) -> None: + """Connect and subscribe to dispatcher signals and state updates.""" + await super().async_added_to_hass() + + entry = self.registry_entry + + if TYPE_CHECKING: + assert entry is not None and entry.device_id is not None + + self.async_on_remove( + self._turn_on.async_register( + self.hass, async_get_turn_on_trigger(entry.device_id) + ) + ) def send_command(self, command): """Send remote control commands to the TV.""" @@ -151,11 +201,6 @@ class LgTVDevice(MediaPlayerEntity): self._volume = volume self._muted = muted - @property - def name(self): - """Return the name of the device.""" - return self._name - @property def is_volume_muted(self): """Boolean if volume is currently muted.""" @@ -194,7 +239,7 @@ class LgTVDevice(MediaPlayerEntity): @property def supported_features(self) -> MediaPlayerEntityFeature: """Flag media player features that are supported.""" - if self._on_action_script: + if self._turn_on: return SUPPORT_LGTV | MediaPlayerEntityFeature.TURN_ON return SUPPORT_LGTV @@ -209,10 +254,9 @@ class LgTVDevice(MediaPlayerEntity): """Turn off media player.""" self.send_command(LG_COMMAND.POWER) - def turn_on(self) -> None: + async def async_turn_on(self) -> None: """Turn on the media player.""" - if self._on_action_script: - self._on_action_script.run(context=self._context) + await self._turn_on.async_run(self.hass, self._context) def volume_up(self) -> None: """Volume up the media player.""" diff --git a/homeassistant/components/lg_netcast/strings.json b/homeassistant/components/lg_netcast/strings.json new file mode 100644 index 00000000000..77003f60f43 --- /dev/null +++ b/homeassistant/components/lg_netcast/strings.json @@ -0,0 +1,46 @@ +{ + "config": { + "step": { + "user": { + "description": "Ensure that your TV is turned on before trying to set it up.\nIf you leave the host empty, discovery will be used to find devices.", + "data": { + "host": "[%key:common::config_flow::data::host%]" + }, + "data_description": { + "host": "The hostname or IP address of the LG Netcast TV to control." + } + }, + "authorize": { + "title": "Authorize LG Netcast TV", + "description": "Enter the Pairing Key displayed on the TV", + "data": { + "access_token": "[%key:common::config_flow::data::access_token%]" + } + } + }, + "error": { + "invalid_host": "[%key:common::config_flow::error::invalid_host%]", + "invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + } + }, + "issues": { + "deprecated_yaml_import_issue_cannot_connect": { + "title": "The {integration_title} is not online for YAML migration to complete", + "description": "Migrating {integration_title} from YAML cannot complete until the TV is online.\n\nPlease turn on your TV for migration to complete." + }, + "deprecated_yaml_import_issue_invalid_host": { + "title": "The {integration_title} YAML configuration has an invalid host.", + "description": "Configuring {integration_title} using YAML is being removed but the device returned an invalid response.\n\nPlease check or manually remove the YAML configuration." + } + }, + "device_automation": { + "trigger_type": { + "lg_netcast.turn_on": "Device is requested to turn on" + } + } +} diff --git a/homeassistant/components/lg_netcast/trigger.py b/homeassistant/components/lg_netcast/trigger.py new file mode 100644 index 00000000000..8dfbe309e03 --- /dev/null +++ b/homeassistant/components/lg_netcast/trigger.py @@ -0,0 +1,49 @@ +"""LG Netcast TV trigger dispatcher.""" + +from __future__ import annotations + +from typing import cast + +from homeassistant.const import CONF_PLATFORM +from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.trigger import ( + TriggerActionType, + TriggerInfo, + TriggerProtocol, +) +from homeassistant.helpers.typing import ConfigType + +from .triggers import turn_on + +TRIGGERS = { + "turn_on": turn_on, +} + + +def _get_trigger_platform(config: ConfigType) -> TriggerProtocol: + """Return trigger platform.""" + platform_split = config[CONF_PLATFORM].split(".", maxsplit=1) + if len(platform_split) < 2 or platform_split[1] not in TRIGGERS: + raise ValueError( + f"Unknown LG Netcast TV trigger platform {config[CONF_PLATFORM]}" + ) + return cast(TriggerProtocol, TRIGGERS[platform_split[1]]) + + +async def async_validate_trigger_config( + hass: HomeAssistant, config: ConfigType +) -> ConfigType: + """Validate config.""" + platform = _get_trigger_platform(config) + return cast(ConfigType, platform.TRIGGER_SCHEMA(config)) + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: TriggerActionType, + trigger_info: TriggerInfo, +) -> CALLBACK_TYPE: + """Attach trigger of specified platform.""" + platform = _get_trigger_platform(config) + return await platform.async_attach_trigger(hass, config, action, trigger_info) diff --git a/homeassistant/components/lg_netcast/triggers/__init__.py b/homeassistant/components/lg_netcast/triggers/__init__.py new file mode 100644 index 00000000000..d352620118e --- /dev/null +++ b/homeassistant/components/lg_netcast/triggers/__init__.py @@ -0,0 +1 @@ +"""LG Netcast triggers.""" diff --git a/homeassistant/components/lg_netcast/triggers/turn_on.py b/homeassistant/components/lg_netcast/triggers/turn_on.py new file mode 100644 index 00000000000..118ed89797e --- /dev/null +++ b/homeassistant/components/lg_netcast/triggers/turn_on.py @@ -0,0 +1,115 @@ +"""LG Netcast TV device turn on trigger.""" + +import voluptuous as vol + +from homeassistant.const import ( + ATTR_DEVICE_ID, + ATTR_ENTITY_ID, + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_PLATFORM, + CONF_TYPE, +) +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.helpers import config_validation as cv, entity_registry as er +from homeassistant.helpers.trigger import ( + PluggableAction, + TriggerActionType, + TriggerInfo, +) +from homeassistant.helpers.typing import ConfigType + +from ..const import DOMAIN +from ..helpers import async_get_device_entry_by_device_id + +PLATFORM_TYPE = f"{DOMAIN}.{__name__.rsplit('.', maxsplit=1)[-1]}" + +TRIGGER_SCHEMA = vol.All( + cv.TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_PLATFORM): PLATFORM_TYPE, + vol.Optional(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + }, + ), + cv.has_at_least_one_key(ATTR_ENTITY_ID, ATTR_DEVICE_ID), +) + + +def async_get_turn_on_trigger(device_id: str) -> dict[str, str]: + """Return data for a turn on trigger.""" + + return { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_TYPE: PLATFORM_TYPE, + } + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: TriggerActionType, + trigger_info: TriggerInfo, + *, + platform_type: str = PLATFORM_TYPE, +) -> CALLBACK_TYPE | None: + """Attach a trigger.""" + device_ids = set() + if ATTR_DEVICE_ID in config: + device_ids.update(config.get(ATTR_DEVICE_ID, [])) + + if ATTR_ENTITY_ID in config: + ent_reg = er.async_get(hass) + + def _get_device_id_from_entity_id(entity_id): + entity_entry = ent_reg.async_get(entity_id) + + if ( + entity_entry is None + or entity_entry.device_id is None + or entity_entry.platform != DOMAIN + ): + raise ValueError(f"Entity {entity_id} is not a valid {DOMAIN} entity.") + + return entity_entry.device_id + + device_ids.update( + { + _get_device_id_from_entity_id(entity_id) + for entity_id in config.get(ATTR_ENTITY_ID, []) + } + ) + + trigger_data = trigger_info["trigger_data"] + + unsubs = [] + + for device_id in device_ids: + device = async_get_device_entry_by_device_id(hass, device_id) + device_name = device.name_by_user or device.name + + variables = { + **trigger_data, + CONF_PLATFORM: platform_type, + ATTR_DEVICE_ID: device_id, + "description": f"lg netcast turn on trigger for {device_name}", + } + + turn_on_trigger = async_get_turn_on_trigger(device_id) + + unsubs.append( + PluggableAction.async_attach_trigger( + hass, turn_on_trigger, action, {"trigger": variables} + ) + ) + + @callback + def async_remove() -> None: + """Remove state listeners async.""" + for unsub in unsubs: + unsub() + unsubs.clear() + + return async_remove diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 125f02df3b5..d1fe540c1b4 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -285,6 +285,7 @@ FLOWS = { "ld2410_ble", "leaone", "led_ble", + "lg_netcast", "lg_soundbar", "lidarr", "lifx", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 340be50978d..1b964ceae34 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -3177,8 +3177,8 @@ "name": "LG", "integrations": { "lg_netcast": { - "integration_type": "hub", - "config_flow": false, + "integration_type": "device", + "config_flow": true, "iot_class": "local_polling", "name": "LG Netcast" }, diff --git a/requirements_all.txt b/requirements_all.txt index 653e481d2fb..92c2533dc4d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1931,7 +1931,7 @@ pylast==5.1.0 pylaunches==1.4.0 # homeassistant.components.lg_netcast -pylgnetcast==0.3.7 +pylgnetcast==0.3.9 # homeassistant.components.forked_daapd pylibrespot-java==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0decf82fe0c..216edd0c5da 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1502,6 +1502,9 @@ pylast==5.1.0 # homeassistant.components.launch_library pylaunches==1.4.0 +# homeassistant.components.lg_netcast +pylgnetcast==0.3.9 + # homeassistant.components.forked_daapd pylibrespot-java==0.1.1 diff --git a/tests/components/lg_netcast/__init__.py b/tests/components/lg_netcast/__init__.py new file mode 100644 index 00000000000..ce3e09aeb65 --- /dev/null +++ b/tests/components/lg_netcast/__init__.py @@ -0,0 +1,116 @@ +"""Tests for LG Netcast TV.""" + +from unittest.mock import patch +from xml.etree import ElementTree + +from pylgnetcast import AccessTokenError, LgNetCastClient, SessionIdError +import requests + +from homeassistant.components.lg_netcast import DOMAIN +from homeassistant.components.media_player import DOMAIN as MP_DOMAIN +from homeassistant.const import ( + CONF_ACCESS_TOKEN, + CONF_HOST, + CONF_ID, + CONF_MODEL, + CONF_NAME, +) +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +FAIL_TO_BIND_IP = "1.2.3.4" + +IP_ADDRESS = "192.168.1.239" +DEVICE_TYPE = "TV" +MODEL_NAME = "MockLGModelName" +FRIENDLY_NAME = "LG Smart TV" +UNIQUE_ID = "1234" +ENTITY_ID = f"{MP_DOMAIN}.{MODEL_NAME.lower()}" + +FAKE_SESSION_ID = "987654321" +FAKE_PIN = "123456" + + +def _patched_lgnetcast_client( + *args, + session_error=False, + fail_connection: bool = True, + invalid_details: bool = False, + always_404: bool = False, + no_unique_id: bool = False, + **kwargs, +): + client = LgNetCastClient(*args, **kwargs) + + def _get_fake_session_id(): + if not client.access_token: + raise AccessTokenError("Fake Access Token Requested") + if session_error: + raise SessionIdError("Can not get session id from TV.") + return FAKE_SESSION_ID + + def _get_fake_query_device_info(): + if fail_connection: + raise requests.exceptions.ConnectTimeout("Mocked Failed Connection") + if always_404: + return None + if invalid_details: + raise ElementTree.ParseError("Mocked Parsed Error") + return { + "uuid": UNIQUE_ID if not no_unique_id else None, + "model_name": MODEL_NAME, + "friendly_name": FRIENDLY_NAME, + } + + client._get_session_id = _get_fake_session_id + client.query_device_info = _get_fake_query_device_info + + return client + + +def _patch_lg_netcast( + *, + session_error: bool = False, + fail_connection: bool = False, + invalid_details: bool = False, + always_404: bool = False, + no_unique_id: bool = False, +): + def _generate_fake_lgnetcast_client(*args, **kwargs): + return _patched_lgnetcast_client( + *args, + session_error=session_error, + fail_connection=fail_connection, + invalid_details=invalid_details, + always_404=always_404, + no_unique_id=no_unique_id, + **kwargs, + ) + + return patch( + "homeassistant.components.lg_netcast.config_flow.LgNetCastClient", + new=_generate_fake_lgnetcast_client, + ) + + +async def setup_lgnetcast(hass: HomeAssistant, unique_id: str = UNIQUE_ID): + """Initialize lg netcast and media_player for tests.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: IP_ADDRESS, + CONF_ACCESS_TOKEN: FAKE_PIN, + CONF_NAME: MODEL_NAME, + CONF_MODEL: MODEL_NAME, + CONF_ID: unique_id, + }, + title=MODEL_NAME, + unique_id=unique_id, + ) + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return config_entry diff --git a/tests/components/lg_netcast/conftest.py b/tests/components/lg_netcast/conftest.py new file mode 100644 index 00000000000..4faee2c6f06 --- /dev/null +++ b/tests/components/lg_netcast/conftest.py @@ -0,0 +1,11 @@ +"""Common fixtures and objects for the LG Netcast integration tests.""" + +import pytest + +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") diff --git a/tests/components/lg_netcast/test_config_flow.py b/tests/components/lg_netcast/test_config_flow.py new file mode 100644 index 00000000000..c159b8fb9d2 --- /dev/null +++ b/tests/components/lg_netcast/test_config_flow.py @@ -0,0 +1,252 @@ +"""Define tests for the LG Netcast config flow.""" + +from datetime import timedelta +from unittest.mock import DEFAULT, patch + +from homeassistant import data_entry_flow +from homeassistant.components.lg_netcast.const import DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.const import ( + CONF_ACCESS_TOKEN, + CONF_HOST, + CONF_ID, + CONF_MODEL, + CONF_NAME, +) +from homeassistant.core import HomeAssistant + +from . import ( + FAKE_PIN, + FRIENDLY_NAME, + IP_ADDRESS, + MODEL_NAME, + UNIQUE_ID, + _patch_lg_netcast, +) + +from tests.common import MockConfigEntry + + +async def test_show_form(hass: HomeAssistant) -> None: + """Test that the form is served with no input.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + + +async def test_user_invalid_host(hass: HomeAssistant) -> None: + """Test that errors are shown when the host is invalid.""" + with _patch_lg_netcast(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "invalid/host"} + ) + + assert result["errors"] == {CONF_HOST: "invalid_host"} + + +async def test_manual_host(hass: HomeAssistant) -> None: + """Test manual host configuration.""" + with _patch_lg_netcast(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: IP_ADDRESS} + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "authorize" + assert not result["errors"] + + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["step_id"] == "authorize" + assert result2["errors"] is not None + assert result2["errors"][CONF_ACCESS_TOKEN] == "invalid_access_token" + + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_ACCESS_TOKEN: FAKE_PIN} + ) + + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["title"] == FRIENDLY_NAME + assert result3["data"] == { + CONF_HOST: IP_ADDRESS, + CONF_ACCESS_TOKEN: FAKE_PIN, + CONF_NAME: FRIENDLY_NAME, + CONF_MODEL: MODEL_NAME, + CONF_ID: UNIQUE_ID, + } + + +async def test_manual_host_no_connection_during_authorize(hass: HomeAssistant) -> None: + """Test manual host configuration.""" + with _patch_lg_netcast(fail_connection=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: IP_ADDRESS} + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "cannot_connect" + + +async def test_manual_host_invalid_details_during_authorize( + hass: HomeAssistant, +) -> None: + """Test manual host configuration.""" + with _patch_lg_netcast(invalid_details=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: IP_ADDRESS} + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "cannot_connect" + + +async def test_manual_host_unsuccessful_details_response(hass: HomeAssistant) -> None: + """Test manual host configuration.""" + with _patch_lg_netcast(always_404=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: IP_ADDRESS} + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "cannot_connect" + + +async def test_manual_host_no_unique_id_response(hass: HomeAssistant) -> None: + """Test manual host configuration.""" + with _patch_lg_netcast(no_unique_id=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: IP_ADDRESS} + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "invalid_host" + + +async def test_invalid_session_id(hass: HomeAssistant) -> None: + """Test Invalid Session ID.""" + with _patch_lg_netcast(session_error=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: IP_ADDRESS} + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "authorize" + assert not result["errors"] + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_ACCESS_TOKEN: FAKE_PIN} + ) + + assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["step_id"] == "authorize" + assert result2["errors"] is not None + assert result2["errors"]["base"] == "cannot_connect" + + +async def test_import(hass: HomeAssistant) -> None: + """Test that the import works.""" + with _patch_lg_netcast(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_HOST: IP_ADDRESS, + CONF_ACCESS_TOKEN: FAKE_PIN, + CONF_NAME: MODEL_NAME, + }, + ) + + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["result"].unique_id == UNIQUE_ID + assert result["data"] == { + CONF_HOST: IP_ADDRESS, + CONF_ACCESS_TOKEN: FAKE_PIN, + CONF_NAME: MODEL_NAME, + CONF_MODEL: MODEL_NAME, + CONF_ID: UNIQUE_ID, + } + + +async def test_import_not_online(hass: HomeAssistant) -> None: + """Test that the import works.""" + with _patch_lg_netcast(fail_connection=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_HOST: IP_ADDRESS, + CONF_ACCESS_TOKEN: FAKE_PIN, + CONF_NAME: MODEL_NAME, + }, + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "cannot_connect" + + +async def test_import_duplicate_error(hass): + """Test that errors are shown when duplicates are added during import.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=UNIQUE_ID, + data={ + CONF_HOST: IP_ADDRESS, + CONF_ACCESS_TOKEN: FAKE_PIN, + CONF_NAME: MODEL_NAME, + CONF_ID: UNIQUE_ID, + }, + ) + config_entry.add_to_hass(hass) + + with _patch_lg_netcast(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_HOST: IP_ADDRESS, + CONF_ACCESS_TOKEN: FAKE_PIN, + CONF_NAME: MODEL_NAME, + CONF_ID: UNIQUE_ID, + }, + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_display_access_token_aborted(hass: HomeAssistant): + """Test Access token display is cancelled.""" + + def _async_track_time_interval( + hass: HomeAssistant, + action, + interval: timedelta, + *, + name=None, + cancel_on_shutdown=None, + ): + hass.async_create_task(action()) + return DEFAULT + + with ( + _patch_lg_netcast(session_error=True), + patch( + "homeassistant.components.lg_netcast.config_flow.async_track_time_interval" + ) as mock_interval, + ): + mock_interval.side_effect = _async_track_time_interval + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: IP_ADDRESS} + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "authorize" + assert not result["errors"] + + assert mock_interval.called + + hass.config_entries.flow.async_abort(result["flow_id"]) + assert mock_interval.return_value.called diff --git a/tests/components/lg_netcast/test_device_trigger.py b/tests/components/lg_netcast/test_device_trigger.py new file mode 100644 index 00000000000..05911acc41d --- /dev/null +++ b/tests/components/lg_netcast/test_device_trigger.py @@ -0,0 +1,148 @@ +"""The tests for LG NEtcast device triggers.""" + +import pytest + +from homeassistant.components import automation +from homeassistant.components.device_automation import DeviceAutomationType +from homeassistant.components.device_automation.exceptions import ( + InvalidDeviceAutomationConfig, +) +from homeassistant.components.lg_netcast import DOMAIN, device_trigger +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import device_registry as dr +from homeassistant.setup import async_setup_component + +from . import ENTITY_ID, UNIQUE_ID, setup_lgnetcast + +from tests.common import MockConfigEntry, async_get_device_automations + + +async def test_get_triggers( + hass: HomeAssistant, device_registry: dr.DeviceRegistry +) -> None: + """Test we get the expected triggers.""" + await setup_lgnetcast(hass) + + device = device_registry.async_get_device(identifiers={(DOMAIN, UNIQUE_ID)}) + assert device is not None + + turn_on_trigger = { + "platform": "device", + "domain": DOMAIN, + "type": "lg_netcast.turn_on", + "device_id": device.id, + "metadata": {}, + } + + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device.id + ) + assert turn_on_trigger in triggers + + +async def test_if_fires_on_turn_on_request( + hass: HomeAssistant, calls: list[ServiceCall], device_registry: dr.DeviceRegistry +) -> None: + """Test for turn_on triggers firing.""" + await setup_lgnetcast(hass) + + device = device_registry.async_get_device(identifiers={(DOMAIN, UNIQUE_ID)}) + assert device is not None + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": device.id, + "type": "lg_netcast.turn_on", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "{{ trigger.device_id }}", + "id": "{{ trigger.id }}", + }, + }, + }, + { + "trigger": { + "platform": "lg_netcast.turn_on", + "entity_id": ENTITY_ID, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ENTITY_ID, + "id": "{{ trigger.id }}", + }, + }, + }, + ], + }, + ) + + await hass.services.async_call( + "media_player", + "turn_on", + {"entity_id": ENTITY_ID}, + blocking=True, + ) + + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[0].data["some"] == device.id + assert calls[0].data["id"] == 0 + assert calls[1].data["some"] == ENTITY_ID + assert calls[1].data["id"] == 0 + + +async def test_failure_scenarios( + hass: HomeAssistant, device_registry: dr.DeviceRegistry +) -> None: + """Test failure scenarios.""" + await setup_lgnetcast(hass) + + # Test wrong trigger platform type + with pytest.raises(HomeAssistantError): + await device_trigger.async_attach_trigger( + hass, {"type": "wrong.type", "device_id": "invalid_device_id"}, None, {} + ) + + # Test invalid device id + with pytest.raises(HomeAssistantError): + await device_trigger.async_validate_trigger_config( + hass, + { + "platform": "device", + "domain": DOMAIN, + "type": "lg_netcast.turn_on", + "device_id": "invalid_device_id", + }, + ) + + entry = MockConfigEntry(domain="fake", state=ConfigEntryState.LOADED, data={}) + entry.add_to_hass(hass) + + device = device_registry.async_get_or_create( + config_entry_id=entry.entry_id, identifiers={("fake", "fake")} + ) + + config = { + "platform": "device", + "domain": DOMAIN, + "device_id": device.id, + "type": "lg_netcast.turn_on", + } + + # Test that device id from non lg_netcast domain raises exception + with pytest.raises(InvalidDeviceAutomationConfig): + await device_trigger.async_validate_trigger_config(hass, config) + + # Test that only valid triggers are attached diff --git a/tests/components/lg_netcast/test_trigger.py b/tests/components/lg_netcast/test_trigger.py new file mode 100644 index 00000000000..e75dac501c3 --- /dev/null +++ b/tests/components/lg_netcast/test_trigger.py @@ -0,0 +1,189 @@ +"""The tests for LG Netcast device triggers.""" + +from unittest.mock import patch + +import pytest + +from homeassistant.components import automation +from homeassistant.components.lg_netcast import DOMAIN +from homeassistant.const import SERVICE_RELOAD +from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import device_registry as dr +from homeassistant.setup import async_setup_component + +from . import ENTITY_ID, UNIQUE_ID, setup_lgnetcast + +from tests.common import MockEntity, MockEntityPlatform + + +async def test_lg_netcast_turn_on_trigger_device_id( + hass: HomeAssistant, calls: list[ServiceCall], device_registry: dr.DeviceRegistry +) -> None: + """Test for turn_on trigger by device_id firing.""" + await setup_lgnetcast(hass) + + device = device_registry.async_get_device(identifiers={(DOMAIN, UNIQUE_ID)}) + assert device, repr(device_registry.devices) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "lg_netcast.turn_on", + "device_id": device.id, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": device.id, + "id": "{{ trigger.id }}", + }, + }, + }, + ], + }, + ) + + await hass.services.async_call( + "media_player", + "turn_on", + {"entity_id": ENTITY_ID}, + blocking=True, + ) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].data["some"] == device.id + assert calls[0].data["id"] == 0 + + with patch("homeassistant.config.load_yaml_dict", return_value={}): + await hass.services.async_call(automation.DOMAIN, SERVICE_RELOAD, blocking=True) + + calls.clear() + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + "media_player", + "turn_on", + {"entity_id": ENTITY_ID}, + blocking=True, + ) + + await hass.async_block_till_done() + assert len(calls) == 0 + + +async def test_lg_netcast_turn_on_trigger_entity_id(hass: HomeAssistant, calls): + """Test for turn_on triggers by entity firing.""" + await setup_lgnetcast(hass) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "lg_netcast.turn_on", + "entity_id": ENTITY_ID, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ENTITY_ID, + "id": "{{ trigger.id }}", + }, + }, + }, + ], + }, + ) + + await hass.services.async_call( + "media_player", + "turn_on", + {"entity_id": ENTITY_ID}, + blocking=True, + ) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].data["some"] == ENTITY_ID + assert calls[0].data["id"] == 0 + + +async def test_wrong_trigger_platform_type( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test wrong trigger platform type.""" + await setup_lgnetcast(hass) + + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "lg_netcast.wrong_type", + "entity_id": ENTITY_ID, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ENTITY_ID, + "id": "{{ trigger.id }}", + }, + }, + }, + ], + }, + ) + + assert ( + "ValueError: Unknown LG Netcast TV trigger platform lg_netcast.wrong_type" + in caplog.text + ) + + +async def test_trigger_invalid_entity_id( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test turn on trigger using invalid entity_id.""" + await setup_lgnetcast(hass) + + platform = MockEntityPlatform(hass) + + invalid_entity = f"{DOMAIN}.invalid" + await platform.async_add_entities([MockEntity(name=invalid_entity)]) + + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "lg_netcast.turn_on", + "entity_id": invalid_entity, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ENTITY_ID, + "id": "{{ trigger.id }}", + }, + }, + } + ], + }, + ) + + assert ( + f"ValueError: Entity {invalid_entity} is not a valid lg_netcast entity" + in caplog.text + ) From 0dd8ffd1f541574ddf7835af89d63b934a1dc224 Mon Sep 17 00:00:00 2001 From: Thomas Kistler Date: Tue, 16 Apr 2024 00:46:15 -0700 Subject: [PATCH 596/967] Add a new "Ambient Weather Network" integration (#105779) * Adding a new "Ambient Weather Network" integration. * Rebase and update code coverage. * Addressed some reviewer comments. * Remove mnemonics and replace with station names. * Remove climate-utils * Remove support for virtual stations. * Rebase * Address feedback * Remove redundant errors * Reviewer feedback * Add icons.json * More icons * Reviewer feedback * Fix test * Make sensor tests more robust * Make coordinator more robust * Change update coordinator to raise UpdateFailed * Recover from no station found error * Dynamically set device name * Address feedback * Disable some sensors by default * Reviewer feedback * Change from hub to service * Rebase * Address reviewer feedback * Reviewer feedback * Manually rerun ruff on all files --- .strict-typing | 1 + CODEOWNERS | 2 + .../components/ambient_network/__init__.py | 35 + .../components/ambient_network/config_flow.py | 152 ++++ .../components/ambient_network/const.py | 16 + .../components/ambient_network/coordinator.py | 65 ++ .../components/ambient_network/entity.py | 50 + .../components/ambient_network/helper.py | 31 + .../components/ambient_network/icons.json | 21 + .../components/ambient_network/manifest.json | 11 + .../components/ambient_network/sensor.py | 315 +++++++ .../components/ambient_network/strings.json | 87 ++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + mypy.ini | 10 + requirements_all.txt | 1 + requirements_test_all.txt | 1 + tests/components/ambient_network/__init__.py | 1 + tests/components/ambient_network/conftest.py | 91 ++ .../fixtures/device_details_response_a.json | 34 + .../fixtures/device_details_response_b.json | 7 + .../fixtures/device_details_response_c.json | 33 + .../devices_by_location_response.json | 364 ++++++++ .../snapshots/test_sensor.ambr | 856 ++++++++++++++++++ .../ambient_network/test_config_flow.py | 85 ++ .../components/ambient_network/test_sensor.py | 123 +++ 26 files changed, 2399 insertions(+) create mode 100644 homeassistant/components/ambient_network/__init__.py create mode 100644 homeassistant/components/ambient_network/config_flow.py create mode 100644 homeassistant/components/ambient_network/const.py create mode 100644 homeassistant/components/ambient_network/coordinator.py create mode 100644 homeassistant/components/ambient_network/entity.py create mode 100644 homeassistant/components/ambient_network/helper.py create mode 100644 homeassistant/components/ambient_network/icons.json create mode 100644 homeassistant/components/ambient_network/manifest.json create mode 100644 homeassistant/components/ambient_network/sensor.py create mode 100644 homeassistant/components/ambient_network/strings.json create mode 100644 tests/components/ambient_network/__init__.py create mode 100644 tests/components/ambient_network/conftest.py create mode 100644 tests/components/ambient_network/fixtures/device_details_response_a.json create mode 100644 tests/components/ambient_network/fixtures/device_details_response_b.json create mode 100644 tests/components/ambient_network/fixtures/device_details_response_c.json create mode 100644 tests/components/ambient_network/fixtures/devices_by_location_response.json create mode 100644 tests/components/ambient_network/snapshots/test_sensor.ambr create mode 100644 tests/components/ambient_network/test_config_flow.py create mode 100644 tests/components/ambient_network/test_sensor.py diff --git a/.strict-typing b/.strict-typing index 63a867e9c50..5985938885f 100644 --- a/.strict-typing +++ b/.strict-typing @@ -66,6 +66,7 @@ homeassistant.components.alpha_vantage.* homeassistant.components.amazon_polly.* homeassistant.components.amberelectric.* homeassistant.components.ambiclimate.* +homeassistant.components.ambient_network.* homeassistant.components.ambient_station.* homeassistant.components.amcrest.* homeassistant.components.ampio.* diff --git a/CODEOWNERS b/CODEOWNERS index d93a8f6b9d3..83d5539a15c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -90,6 +90,8 @@ build.json @home-assistant/supervisor /tests/components/amberelectric/ @madpilot /homeassistant/components/ambiclimate/ @danielhiversen /tests/components/ambiclimate/ @danielhiversen +/homeassistant/components/ambient_network/ @thomaskistler +/tests/components/ambient_network/ @thomaskistler /homeassistant/components/ambient_station/ @bachya /tests/components/ambient_station/ @bachya /homeassistant/components/amcrest/ @flacjacket diff --git a/homeassistant/components/ambient_network/__init__.py b/homeassistant/components/ambient_network/__init__.py new file mode 100644 index 00000000000..b286fb7fbc9 --- /dev/null +++ b/homeassistant/components/ambient_network/__init__.py @@ -0,0 +1,35 @@ +"""The Ambient Weather Network integration.""" + +from __future__ import annotations + +from aioambient.open_api import OpenAPI + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .coordinator import AmbientNetworkDataUpdateCoordinator + +PLATFORMS: list[Platform] = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up the Ambient Weather Network from a config entry.""" + + api = OpenAPI() + coordinator = AmbientNetworkDataUpdateCoordinator(hass, api) + await coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = 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.""" + + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/ambient_network/config_flow.py b/homeassistant/components/ambient_network/config_flow.py new file mode 100644 index 00000000000..d29134db1c9 --- /dev/null +++ b/homeassistant/components/ambient_network/config_flow.py @@ -0,0 +1,152 @@ +"""Config flow for the Ambient Weather Network integration.""" + +from __future__ import annotations + +from typing import Any + +from aioambient import OpenAPI +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LOCATION, + CONF_LONGITUDE, + CONF_MAC, + CONF_RADIUS, + UnitOfLength, +) +from homeassistant.helpers.selector import ( + LocationSelector, + LocationSelectorConfig, + SelectOptionDict, + SelectSelector, + SelectSelectorConfig, +) +from homeassistant.util.unit_conversion import DistanceConverter + +from .const import API_STATION_INDOOR, API_STATION_INFO, API_STATION_MAC_ADDRESS, DOMAIN +from .helper import get_station_name + +CONF_USER = "user" +CONF_STATION = "station" + +# One mile +CONF_RADIUS_DEFAULT = 1609.34 + + +class AmbientNetworkConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle the config flow for the Ambient Weather Network integration.""" + + VERSION = 1 + + def __init__(self) -> None: + """Construct the config flow.""" + + self._longitude = 0.0 + self._latitude = 0.0 + self._radius = 0.0 + self._stations: dict[str, dict[str, Any]] = {} + + async def async_step_user( + self, + user_input: dict[str, Any] | None = None, + ) -> ConfigFlowResult: + """Handle the initial step to select the location.""" + + errors: dict[str, str] | None = None + if user_input: + self._latitude = user_input[CONF_LOCATION][CONF_LATITUDE] + self._longitude = user_input[CONF_LOCATION][CONF_LONGITUDE] + self._radius = user_input[CONF_LOCATION][CONF_RADIUS] + + client: OpenAPI = OpenAPI() + self._stations = { + x[API_STATION_MAC_ADDRESS]: x + for x in await client.get_devices_by_location( + self._latitude, + self._longitude, + radius=DistanceConverter.convert( + self._radius, + UnitOfLength.METERS, + UnitOfLength.MILES, + ), + ) + } + + # Filter out indoor stations + self._stations = dict( + filter( + lambda item: not item[1] + .get(API_STATION_INFO, {}) + .get(API_STATION_INDOOR, False), + self._stations.items(), + ) + ) + + if self._stations: + return await self.async_step_station() + + errors = {"base": "no_stations_found"} + + schema: vol.Schema = self.add_suggested_values_to_schema( + vol.Schema( + { + vol.Required( + CONF_LOCATION, + ): LocationSelector(LocationSelectorConfig(radius=True)), + } + ), + { + CONF_LOCATION: { + CONF_LATITUDE: self.hass.config.latitude, + CONF_LONGITUDE: self.hass.config.longitude, + CONF_RADIUS: CONF_RADIUS_DEFAULT, + } + if not errors + else { + CONF_LATITUDE: self._latitude, + CONF_LONGITUDE: self._longitude, + CONF_RADIUS: self._radius, + } + }, + ) + + return self.async_show_form( + step_id=CONF_USER, data_schema=schema, errors=errors if errors else {} + ) + + async def async_step_station( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the second step to select the station.""" + + if user_input: + mac_address = user_input[CONF_STATION] + await self.async_set_unique_id(mac_address) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=get_station_name(self._stations[mac_address]), + data={CONF_MAC: mac_address}, + ) + + options: list[SelectOptionDict] = [ + SelectOptionDict( + label=get_station_name(station), + value=mac_address, + ) + for mac_address, station in self._stations.items() + ] + + schema: vol.Schema = vol.Schema( + { + vol.Required(CONF_STATION): SelectSelector( + SelectSelectorConfig(options=options, multiple=False, sort=True), + ) + } + ) + + return self.async_show_form( + step_id=CONF_STATION, + data_schema=schema, + ) diff --git a/homeassistant/components/ambient_network/const.py b/homeassistant/components/ambient_network/const.py new file mode 100644 index 00000000000..402e5f81097 --- /dev/null +++ b/homeassistant/components/ambient_network/const.py @@ -0,0 +1,16 @@ +"""Constants for the Ambient Weather Network integration.""" + +import logging + +DOMAIN = "ambient_network" + +API_LAST_DATA = "lastData" +API_STATION_COORDS = "coords" +API_STATION_INDOOR = "indoor" +API_STATION_INFO = "info" +API_STATION_LOCATION = "location" +API_STATION_NAME = "name" +API_STATION_MAC_ADDRESS = "macAddress" +API_STATION_TYPE = "stationtype" + +LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/ambient_network/coordinator.py b/homeassistant/components/ambient_network/coordinator.py new file mode 100644 index 00000000000..f26ddd47b24 --- /dev/null +++ b/homeassistant/components/ambient_network/coordinator.py @@ -0,0 +1,65 @@ +"""DataUpdateCoordinator for the Ambient Weather Network integration.""" + +from __future__ import annotations + +from datetime import datetime, timedelta +from typing import Any, cast + +from aioambient import OpenAPI +from aioambient.errors import RequestError + +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 .const import API_LAST_DATA, DOMAIN, LOGGER +from .helper import get_station_name + +SCAN_INTERVAL = timedelta(minutes=5) + + +class AmbientNetworkDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): + """The Ambient Network Data Update Coordinator.""" + + config_entry: ConfigEntry + station_name: str + + def __init__(self, hass: HomeAssistant, api: OpenAPI) -> None: + """Initialize the coordinator.""" + super().__init__(hass, LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL) + self.api = api + + async def _async_update_data(self) -> dict[str, Any]: + """Fetch the latest data from the Ambient Network.""" + + try: + response = await self.api.get_device_details( + self.config_entry.data[CONF_MAC] + ) + except RequestError as ex: + raise UpdateFailed("Cannot connect to Ambient Network") from ex + + self.station_name = get_station_name(response) + + if (last_data := response.get(API_LAST_DATA)) is None: + raise UpdateFailed( + 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" + ) + + return cast(dict[str, Any], last_data) diff --git a/homeassistant/components/ambient_network/entity.py b/homeassistant/components/ambient_network/entity.py new file mode 100644 index 00000000000..ad0241ea3de --- /dev/null +++ b/homeassistant/components/ambient_network/entity.py @@ -0,0 +1,50 @@ +"""Base entity class for the Ambient Weather Network integration.""" + +from __future__ import annotations + +from abc import abstractmethod + +from homeassistant.core import callback +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import AmbientNetworkDataUpdateCoordinator + + +class AmbientNetworkEntity(CoordinatorEntity[AmbientNetworkDataUpdateCoordinator]): + """Entity class for Ambient network devices.""" + + _attr_attribution = "Data provided by ambientnetwork.net" + _attr_has_entity_name = True + + def __init__( + self, + coordinator: AmbientNetworkDataUpdateCoordinator, + description: EntityDescription, + mac_address: str, + ) -> None: + """Initialize the Ambient network entity.""" + + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"{mac_address}_{description.key}" + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + name=coordinator.station_name, + identifiers={(DOMAIN, mac_address)}, + manufacturer="Ambient Weather", + ) + self._update_attrs() + + @abstractmethod + def _update_attrs(self) -> None: + """Update state attributes.""" + + @callback + def _handle_coordinator_update(self) -> None: + """Get the latest data and updates the state.""" + + self._update_attrs() + super()._handle_coordinator_update() diff --git a/homeassistant/components/ambient_network/helper.py b/homeassistant/components/ambient_network/helper.py new file mode 100644 index 00000000000..fbde45ee756 --- /dev/null +++ b/homeassistant/components/ambient_network/helper.py @@ -0,0 +1,31 @@ +"""Helper class for the Ambient Weather Network integration.""" + +from __future__ import annotations + +from typing import Any + +from .const import ( + API_LAST_DATA, + API_STATION_COORDS, + API_STATION_INFO, + API_STATION_LOCATION, + API_STATION_NAME, + API_STATION_TYPE, +) + + +def get_station_name(station: dict[str, Any]) -> str: + """Pick a station name. + + Station names can be empty, in which case we construct the name from + the location and device type. + """ + if name := station.get(API_STATION_INFO, {}).get(API_STATION_NAME): + return str(name) + location = ( + station.get(API_STATION_INFO, {}) + .get(API_STATION_COORDS, {}) + .get(API_STATION_LOCATION) + ) + station_type = station.get(API_LAST_DATA, {}).get(API_STATION_TYPE) + return f"{location}{'' if location is None or station_type is None else ' '}{station_type}" diff --git a/homeassistant/components/ambient_network/icons.json b/homeassistant/components/ambient_network/icons.json new file mode 100644 index 00000000000..a7abebce187 --- /dev/null +++ b/homeassistant/components/ambient_network/icons.json @@ -0,0 +1,21 @@ +{ + "entity": { + "sensor": { + "last_rain": { + "default": "mdi:water" + }, + "lightning_strikes_per_day": { + "default": "mdi:lightning-bolt" + }, + "lightning_strikes_per_hour": { + "default": "mdi:lightning-bolt" + }, + "lightning_distance": { + "default": "mdi:lightning-bolt" + }, + "wind_direction": { + "default": "mdi:compass-outline" + } + } + } +} diff --git a/homeassistant/components/ambient_network/manifest.json b/homeassistant/components/ambient_network/manifest.json new file mode 100644 index 00000000000..553adb240b0 --- /dev/null +++ b/homeassistant/components/ambient_network/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "ambient_network", + "name": "Ambient Weather Network", + "codeowners": ["@thomaskistler"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/ambient_network", + "integration_type": "service", + "iot_class": "cloud_polling", + "loggers": ["aioambient"], + "requirements": ["aioambient==2024.01.0"] +} diff --git a/homeassistant/components/ambient_network/sensor.py b/homeassistant/components/ambient_network/sensor.py new file mode 100644 index 00000000000..c28b69229d8 --- /dev/null +++ b/homeassistant/components/ambient_network/sensor.py @@ -0,0 +1,315 @@ +"""Support for Ambient Weather Network sensors.""" + +from __future__ import annotations + +from datetime import datetime + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_MILLION, + CONF_MAC, + DEGREE, + PERCENTAGE, + UnitOfIrradiance, + UnitOfLength, + UnitOfPrecipitationDepth, + UnitOfPressure, + UnitOfSpeed, + UnitOfTemperature, + UnitOfVolumetricFlux, +) +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 .coordinator import AmbientNetworkDataUpdateCoordinator +from .entity import AmbientNetworkEntity + +TYPE_AQI_PM25 = "aqi_pm25" +TYPE_AQI_PM25_24H = "aqi_pm25_24h" +TYPE_BAROMABSIN = "baromabsin" +TYPE_BAROMRELIN = "baromrelin" +TYPE_CO2 = "co2" +TYPE_DAILYRAININ = "dailyrainin" +TYPE_DEWPOINT = "dewPoint" +TYPE_EVENTRAININ = "eventrainin" +TYPE_FEELSLIKE = "feelsLike" +TYPE_HOURLYRAININ = "hourlyrainin" +TYPE_HUMIDITY = "humidity" +TYPE_LASTRAIN = "lastRain" +TYPE_LIGHTNING_DISTANCE = "lightning_distance" +TYPE_LIGHTNING_PER_DAY = "lightning_day" +TYPE_LIGHTNING_PER_HOUR = "lightning_hour" +TYPE_MAXDAILYGUST = "maxdailygust" +TYPE_MONTHLYRAININ = "monthlyrainin" +TYPE_PM25 = "pm25" +TYPE_PM25_24H = "pm25_24h" +TYPE_SOLARRADIATION = "solarradiation" +TYPE_TEMPF = "tempf" +TYPE_UV = "uv" +TYPE_WEEKLYRAININ = "weeklyrainin" +TYPE_WINDDIR = "winddir" +TYPE_WINDGUSTMPH = "windgustmph" +TYPE_WINDSPEEDMPH = "windspeedmph" +TYPE_YEARLYRAININ = "yearlyrainin" + + +SENSOR_DESCRIPTIONS = ( + SensorEntityDescription( + key=TYPE_AQI_PM25, + translation_key="pm25_aqi", + device_class=SensorDeviceClass.AQI, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=0, + ), + SensorEntityDescription( + key=TYPE_AQI_PM25_24H, + translation_key="pm25_aqi_24h_average", + device_class=SensorDeviceClass.AQI, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=0, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key=TYPE_BAROMABSIN, + translation_key="absolute_pressure", + native_unit_of_measurement=UnitOfPressure.INHG, + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=2, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key=TYPE_BAROMRELIN, + translation_key="relative_pressure", + native_unit_of_measurement=UnitOfPressure.INHG, + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=2, + ), + SensorEntityDescription( + key=TYPE_CO2, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + device_class=SensorDeviceClass.CO2, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=2, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key=TYPE_DAILYRAININ, + translation_key="daily_rain", + native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, + device_class=SensorDeviceClass.PRECIPITATION, + state_class=SensorStateClass.TOTAL, + suggested_display_precision=2, + ), + SensorEntityDescription( + key=TYPE_DEWPOINT, + translation_key="dew_point", + native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + ), + SensorEntityDescription( + key=TYPE_FEELSLIKE, + translation_key="feels_like", + native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + ), + SensorEntityDescription( + key=TYPE_HOURLYRAININ, + translation_key="hourly_rain", + native_unit_of_measurement=UnitOfVolumetricFlux.INCHES_PER_HOUR, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.PRECIPITATION_INTENSITY, + suggested_display_precision=2, + ), + SensorEntityDescription( + key=TYPE_HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + ), + SensorEntityDescription( + key=TYPE_LASTRAIN, + translation_key="last_rain", + device_class=SensorDeviceClass.TIMESTAMP, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key=TYPE_LIGHTNING_PER_DAY, + translation_key="lightning_strikes_per_day", + native_unit_of_measurement="strikes", + state_class=SensorStateClass.TOTAL, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key=TYPE_LIGHTNING_PER_HOUR, + translation_key="lightning_strikes_per_hour", + native_unit_of_measurement="strikes/hour", + state_class=SensorStateClass.TOTAL, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key=TYPE_LIGHTNING_DISTANCE, + translation_key="lightning_distance", + native_unit_of_measurement=UnitOfLength.MILES, + device_class=SensorDeviceClass.DISTANCE, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=2, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key=TYPE_MAXDAILYGUST, + translation_key="max_daily_gust", + native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR, + device_class=SensorDeviceClass.WIND_SPEED, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + ), + SensorEntityDescription( + key=TYPE_MONTHLYRAININ, + translation_key="monthly_rain", + native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, + device_class=SensorDeviceClass.PRECIPITATION, + state_class=SensorStateClass.TOTAL, + suggested_display_precision=2, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key=TYPE_PM25_24H, + translation_key="pm25_24h_average", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.PM25, + suggested_display_precision=1, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key=TYPE_PM25, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.PM25, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key=TYPE_SOLARRADIATION, + native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER, + device_class=SensorDeviceClass.IRRADIANCE, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key=TYPE_TEMPF, + native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + ), + SensorEntityDescription( + key=TYPE_UV, + translation_key="uv_index", + native_unit_of_measurement="index", + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + ), + SensorEntityDescription( + key=TYPE_WEEKLYRAININ, + translation_key="weekly_rain", + native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, + device_class=SensorDeviceClass.PRECIPITATION, + state_class=SensorStateClass.TOTAL, + suggested_display_precision=2, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key=TYPE_WINDDIR, + translation_key="wind_direction", + native_unit_of_measurement=DEGREE, + suggested_display_precision=0, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key=TYPE_WINDGUSTMPH, + translation_key="wind_gust", + native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR, + device_class=SensorDeviceClass.WIND_SPEED, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + ), + SensorEntityDescription( + key=TYPE_WINDSPEEDMPH, + native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR, + device_class=SensorDeviceClass.WIND_SPEED, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + ), + SensorEntityDescription( + key=TYPE_YEARLYRAININ, + translation_key="yearly_rain", + native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, + device_class=SensorDeviceClass.PRECIPITATION, + state_class=SensorStateClass.TOTAL, + suggested_display_precision=2, + entity_registry_enabled_default=False, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Ambient Network sensor entities.""" + + coordinator: AmbientNetworkDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + if coordinator.config_entry is not None: + async_add_entities( + AmbientNetworkSensor( + coordinator, + description, + coordinator.config_entry.data[CONF_MAC], + ) + for description in SENSOR_DESCRIPTIONS + if coordinator.data.get(description.key) is not None + ) + + +class AmbientNetworkSensor(AmbientNetworkEntity, SensorEntity): + """A sensor implementation for an Ambient Weather Network sensor.""" + + def __init__( + self, + coordinator: AmbientNetworkDataUpdateCoordinator, + description: SensorEntityDescription, + 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. + if value is not None and self.device_class == SensorDeviceClass.TIMESTAMP: + value = datetime.fromtimestamp(value / 1000, tz=dt_util.DEFAULT_TIME_ZONE) + + self._attr_available = value is not None + self._attr_native_value = value diff --git a/homeassistant/components/ambient_network/strings.json b/homeassistant/components/ambient_network/strings.json new file mode 100644 index 00000000000..7d18c40d902 --- /dev/null +++ b/homeassistant/components/ambient_network/strings.json @@ -0,0 +1,87 @@ +{ + "config": { + "step": { + "user": { + "title": "Select region", + "description": "Choose the region you want to survey in order to locate Ambient personal weather stations." + }, + "station": { + "title": "Select station", + "description": "Select the weather station you want to add to Home Assistant.", + "data": { + "station": "Station" + } + } + }, + "error": { + "no_stations_found": "Did not find any stations in the selected region." + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "entity": { + "sensor": { + "pm25_24h_average": { + "name": "PM2.5 (24 hour average)" + }, + "pm25_aqi": { + "name": "PM2.5 AQI" + }, + "pm25_aqi_24h_average": { + "name": "PM2.5 AQI (24 hour average)" + }, + "absolute_pressure": { + "name": "Absolute pressure" + }, + "relative_pressure": { + "name": "Relative pressure" + }, + "daily_rain": { + "name": "Daily rain" + }, + "dew_point": { + "name": "Dew point" + }, + "feels_like": { + "name": "Feels like" + }, + "hourly_rain": { + "name": "Hourly rain" + }, + "last_rain": { + "name": "Last rain" + }, + "lightning_strikes_per_day": { + "name": "Lightning strikes per day" + }, + "lightning_strikes_per_hour": { + "name": "Lightning strikes per hour" + }, + "lightning_distance": { + "name": "Lightning distance" + }, + "max_daily_gust": { + "name": "Max daily gust" + }, + "monthly_rain": { + "name": "Monthly rain" + }, + "uv_index": { + "name": "UV index" + }, + "weekly_rain": { + "name": "Weekly rain" + }, + "wind_direction": { + "name": "Wind direction" + }, + "wind_gust": { + "name": "Wind gust" + }, + "yearly_rain": { + "name": "Yearly rain" + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index d1fe540c1b4..30d580ad1ea 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -42,6 +42,7 @@ FLOWS = { "alarmdecoder", "amberelectric", "ambiclimate", + "ambient_network", "ambient_station", "analytics_insights", "android_ip_webcam", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 1b964ceae34..fa2cec4d77a 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -244,6 +244,12 @@ "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", diff --git a/mypy.ini b/mypy.ini index 546ae52f972..216d43322a4 100644 --- a/mypy.ini +++ b/mypy.ini @@ -421,6 +421,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.ambient_network.*] +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.ambient_station.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 92c2533dc4d..64d67ada712 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -190,6 +190,7 @@ aioairzone-cloud==0.5.1 # homeassistant.components.airzone aioairzone==0.7.6 +# homeassistant.components.ambient_network # homeassistant.components.ambient_station aioambient==2024.01.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 216edd0c5da..d9fd0586fa7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -169,6 +169,7 @@ aioairzone-cloud==0.5.1 # homeassistant.components.airzone aioairzone==0.7.6 +# homeassistant.components.ambient_network # homeassistant.components.ambient_station aioambient==2024.01.0 diff --git a/tests/components/ambient_network/__init__.py b/tests/components/ambient_network/__init__.py new file mode 100644 index 00000000000..2971b77ddd8 --- /dev/null +++ b/tests/components/ambient_network/__init__.py @@ -0,0 +1 @@ +"""Tests for the Ambient Weather Network integration.""" diff --git a/tests/components/ambient_network/conftest.py b/tests/components/ambient_network/conftest.py new file mode 100644 index 00000000000..3afadbfa722 --- /dev/null +++ b/tests/components/ambient_network/conftest.py @@ -0,0 +1,91 @@ +"""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 homeassistant.components import ambient_network +from homeassistant.core import HomeAssistant + +from tests.common import ( + MockConfigEntry, + load_json_array_fixture, + load_json_object_fixture, +) + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.ambient_network.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture(name="devices_by_location", scope="package") +def devices_by_location_fixture() -> list[dict[str, Any]]: + """Return result of OpenAPI get_devices_by_location() call.""" + return load_json_array_fixture( + "devices_by_location_response.json", "ambient_network" + ) + + +def mock_device_details_callable(mac_address: str) -> dict[str, Any]: + """Return result of OpenAPI get_device_details() call.""" + return load_json_object_fixture( + f"device_details_response_{mac_address[0].lower()}.json", "ambient_network" + ) + + +@pytest.fixture(name="open_api") +def mock_open_api() -> OpenAPI: + """Mock OpenAPI object.""" + return Mock( + get_device_details=AsyncMock(side_effect=mock_device_details_callable), + ) + + +@pytest.fixture(name="aioambient") +async def mock_aioambient(open_api: OpenAPI): + """Mock aioambient library.""" + with ( + patch( + "homeassistant.components.ambient_network.config_flow.OpenAPI", + return_value=open_api, + ), + patch( + "homeassistant.components.ambient_network.OpenAPI", + return_value=open_api, + ), + ): + yield + + +@pytest.fixture(name="config_entry") +def config_entry_fixture(request) -> MockConfigEntry: + """Mock config entry.""" + return MockConfigEntry( + domain=ambient_network.DOMAIN, + title=f"Station {request.param[0]}", + data={"mac": request.param}, + ) + + +async def setup_platform( + expected_outcome: bool, + hass: HomeAssistant, + config_entry: MockConfigEntry, +): + """Load the Ambient Network integration with the provided OpenAPI and config entry.""" + + config_entry.add_to_hass(hass) + assert ( + await hass.config_entries.async_setup(config_entry.entry_id) == expected_outcome + ) + await hass.async_block_till_done() + + return diff --git a/tests/components/ambient_network/fixtures/device_details_response_a.json b/tests/components/ambient_network/fixtures/device_details_response_a.json new file mode 100644 index 00000000000..40491e2631c --- /dev/null +++ b/tests/components/ambient_network/fixtures/device_details_response_a.json @@ -0,0 +1,34 @@ +{ + "_id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "macAddress": "AA:AA:AA:AA:AA:AA", + "lastData": { + "stationtype": "AMBWeatherPro_V5.0.6", + "dateutc": 1699474320000, + "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", + "created_at": 1699474320914, + "dateutc5": 1699474200000, + "lastRain": 1698659100000, + "tz": "America/Chicago" + }, + "info": { + "name": "Station A" + } +} diff --git a/tests/components/ambient_network/fixtures/device_details_response_b.json b/tests/components/ambient_network/fixtures/device_details_response_b.json new file mode 100644 index 00000000000..8249f6f0c30 --- /dev/null +++ b/tests/components/ambient_network/fixtures/device_details_response_b.json @@ -0,0 +1,7 @@ +{ + "_id": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "macAddress": "BB:BB:BB:BB:BB:BB", + "info": { + "name": "Station B" + } +} diff --git a/tests/components/ambient_network/fixtures/device_details_response_c.json b/tests/components/ambient_network/fixtures/device_details_response_c.json new file mode 100644 index 00000000000..8e171f35374 --- /dev/null +++ b/tests/components/ambient_network/fixtures/device_details_response_c.json @@ -0,0 +1,33 @@ +{ + "_id": "cccccccccccccccccccccccccccccccc", + "macAddress": "CC:CC:CC:CC:CC:CC", + "lastData": { + "stationtype": "AMBWeatherPro_V5.0.6", + "dateutc": 1699474320000, + "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", + "dateutc5": 1699474200000, + "lastRain": 1698659100000, + "tz": "America/Chicago" + }, + "info": { + "name": "Station C" + } +} diff --git a/tests/components/ambient_network/fixtures/devices_by_location_response.json b/tests/components/ambient_network/fixtures/devices_by_location_response.json new file mode 100644 index 00000000000..848ba0a7b87 --- /dev/null +++ b/tests/components/ambient_network/fixtures/devices_by_location_response.json @@ -0,0 +1,364 @@ +[ + { + "_id": "aaaaaaaaaaaaaaaaaaaaaaaa", + "macAddress": "AA:AA:AA:AA:AA:AA", + "lastData": { + "stationtype": "AMBWeatherPro_V5.0.6", + "dateutc": 1699474320000, + "tempf": 82.9, + "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", + "created_at": 1699474320914, + "dateutc5": 1699474200000, + "lastRain": 1698659100000, + "tz": "America/Chicago" + }, + "info": { + "name": "Station A", + "coords": { + "geo": { + "coordinates": [-97.0, 32.0], + "type": "Point" + }, + "elevation": 237.0, + "location": "Location A", + "coords": { + "lon": -97.0, + "lat": 32.0 + } + }, + "indoor": false, + "slug": "aaaaaaaaaaaaaaaaaaaaaaaa" + }, + "tz": { + "name": "America/Chicago" + } + }, + { + "_id": "bbbbbbbbbbbbbbbbbbbbbbbb", + "macAddress": "BB:BB:BB:BB:BB:BB", + "lastData": { + "stationtype": "AMBWeatherV4.2.6", + "dateutc": 1700716980000, + "baromrelin": 29.342, + "baromabsin": 29.342, + "tempf": 35.8, + "humidity": 88, + "winddir": 237, + "winddir_avg10m": 221, + "windspeedmph": 0, + "windspdmph_avg10m": 0, + "windgustmph": 1.3, + "maxdailygust": 12.3, + "hourlyrainin": 0, + "eventrainin": 0, + "dailyrainin": 0, + "weeklyrainin": 0.024, + "monthlyrainin": 0.331, + "yearlyrainin": 12.382, + "solarradiation": 0, + "uv": 0, + "soilhum2": 0, + "type": "weather-data", + "created_at": 1700717004020, + "dateutc5": 1700716800000, + "lastRain": 1700445000000, + "discreets": { + "humidity1": [41, 42, 43] + }, + "tz": "America/Chicago" + }, + "info": { + "name": "Station B", + "coords": { + "coords": { + "lat": 32.0, + "lon": -97.0 + }, + "location": "Location B", + "elevation": 226.0, + "geo": { + "type": "Point", + "coordinates": [-97.0, 32.0] + } + }, + "indoor": false, + "slug": "bbbbbbbbbbbbbbbbbbbbbbbb" + }, + "tz": { + "name": "America/Chicago" + } + }, + { + "_id": "cccccccccccccccccccccccc", + "macAddress": "CC:CC:CC:CC:CC:CC", + "lastData": {}, + "info": { + "name": "Station C", + "coords": { + "geo": { + "coordinates": [-97.0, 32.0], + "type": "Point" + }, + "elevation": 242.0, + "location": "Location C", + "coords": { + "lon": -97.0, + "lat": 32.0 + } + }, + "indoor": false, + "slug": "cccccccccccccccccccccccc" + }, + "tz": { + "name": "America/Chicago" + } + }, + { + "_id": "dddddddddddddddddddddddd", + "macAddress": "DD:DD:DD:DD:DD:DD", + "lastData": { + "stationtype": "AMBWeatherPro_V5.1.3", + "dateutc": 1700716920000, + "tempf": 38.1, + "humidity": 85, + "windspeedmph": 0, + "windgustmph": 0, + "maxdailygust": 0, + "winddir": 89, + "uv": 0, + "solarradiation": 0, + "hourlyrainin": 0, + "eventrainin": 0, + "dailyrainin": 0, + "weeklyrainin": 0.028, + "monthlyrainin": 0.327, + "yearlyrainin": 12.76, + "totalrainin": 12.76, + "baromrelin": 29.731, + "baromabsin": 29.338, + "type": "weather-data", + "created_at": 1700716969446, + "dateutc5": 1700716800000, + "lastRain": 1700449500000, + "tz": "America/Chicago" + }, + "info": { + "name": "Station D", + "coords": { + "coords": { + "lat": 32.0, + "lon": -97.0 + }, + "address": "", + "location": "Location D", + "elevation": 221.0, + "address_components": [ + { + "long_name": "1234", + "short_name": "1234", + "types": ["street_number"] + }, + { + "long_name": "D Street", + "short_name": "D St.", + "types": ["route"] + }, + { + "long_name": "D Town", + "short_name": "D Town", + "types": ["locality", "political"] + }, + { + "long_name": "D County", + "short_name": "D County", + "types": ["administrative_area_level_2", "political"] + }, + { + "long_name": "Delaware", + "short_name": "DE", + "types": ["administrative_area_level_1", "political"] + }, + { + "long_name": "United States", + "short_name": "US", + "types": ["country", "political"] + }, + { + "long_name": "12345", + "short_name": "12345", + "types": ["postal_code"] + } + ], + "geo": { + "type": "Point", + "coordinates": [-97.0, 32.0] + } + }, + "indoor": false, + "slug": "dddddddddddddddddddddddd" + }, + "tz": { + "name": "America/Chicago" + } + }, + { + "_id": "eeeeeeeeeeeeeeeeeeeeeeee", + "macAddress": "EE:EE:EE:EE:EE:EE", + "lastData": { + "stationtype": "AMBWeatherV4.3.4", + "dateutc": 1700716920000, + "baromrelin": 29.238, + "baromabsin": 29.238, + "tempf": 45, + "humidity": 55, + "winddir": 98, + "winddir_avg10m": 185, + "windspeedmph": 1.1, + "windspdmph_avg10m": 1.3, + "windgustmph": 3.4, + "maxdailygust": 12.5, + "hourlyrainin": 0, + "eventrainin": 0, + "dailyrainin": 0, + "weeklyrainin": 0.059, + "monthlyrainin": 0.39, + "yearlyrainin": 31.268, + "lightning_day": 1, + "lightning_time": 1700700515000, + "lightning_distance": 8.7, + "batt_lightning": 0, + "solarradiation": 0, + "uv": 0, + "batt_co2": 1, + "type": "weather-data", + "created_at": 1700716954726, + "dateutc5": 1700716800000, + "lastRain": 1700445300000, + "lightnings": [ + [1700713320000, 0], + [1700713380000, 0], + [1700713440000, 0], + [1700713500000, 0], + [1700713560000, 0], + [1700713620000, 0], + [1700713680000, 0], + [1700713740000, 0], + [1700713800000, 0], + [1700713860000, 0], + [1700713920000, 0], + [1700713980000, 0], + [1700714040000, 0], + [1700714100000, 0], + [1700714160000, 0], + [1700714220000, 0], + [1700714280000, 0], + [1700714340000, 0], + [1700714400000, 0], + [1700714460000, 0], + [1700714520000, 0], + [1700714580000, 0], + [1700714640000, 0], + [1700714700000, 0], + [1700714760000, 0], + [1700714820000, 0], + [1700714880000, 0], + [1700714940000, 0], + [1700715000000, 0], + [1700715060000, 0], + [1700715120000, 0], + [1700715180000, 0], + [1700715240000, 0], + [1700715300000, 0], + [1700715360000, 0], + [1700715420000, 0], + [1700715480000, 0], + [1700715540000, 0], + [1700715600000, 0], + [1700715660000, 0], + [1700715720000, 0], + [1700715780000, 0], + [1700715840000, 0], + [1700715900000, 0], + [1700715960000, 0], + [1700716020000, 0], + [1700716080000, 0], + [1700716140000, 0], + [1700716200000, 0], + [1700716260000, 0], + [1700716320000, 0], + [1700716380000, 0], + [1700716440000, 0], + [1700716500000, 0], + [1700716560000, 0], + [1700716620000, 0], + [1700716680000, 0], + [1700716740000, 0], + [1700716800000, 0], + [1700716860000, 0], + [1700716920000, 0] + ], + "lightning_hour": 0, + "tz": "America/Chicago" + }, + "info": { + "name": "Station E", + "coords": { + "coords": { + "lat": 32.0, + "lon": -97.0 + }, + "location": "Location E", + "elevation": 236.0, + "geo": { + "type": "Point", + "coordinates": [-97.0, 32.0] + } + }, + "indoor": false, + "slug": "eeeeeeeeeeeeeeeeeeeeeeee" + }, + "tz": { + "name": "America/Chicago" + } + }, + { + "_id": "ffffffffffffffffffffffff", + "macAddress": "FF:FF:FF:FF:FF:FF", + "lastData": {}, + "info": { + "name": "", + "coords": { + "coords": { + "lat": 32.0, + "lon": -97.0 + }, + "location": "Location F", + "elevation": 242.0, + "geo": { + "type": "Point", + "coordinates": [-97.0, 32.0] + } + }, + "indoor": false, + "slug": "ffffffffffffffffffffffff" + }, + "tz": { + "name": "America/Chicago" + } + } +] diff --git a/tests/components/ambient_network/snapshots/test_sensor.ambr b/tests/components/ambient_network/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..377018c54be --- /dev/null +++ b/tests/components/ambient_network/snapshots/test_sensor.ambr @@ -0,0 +1,856 @@ +# serializer version: 1 +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_absolute_pressure-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.station_a_absolute_pressure', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + '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': 'AA:AA:AA:AA:AA:AA_baromabsin', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_absolute_pressure-state] + None +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_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_a_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': 'AA:AA:AA:AA:AA:AA_dailyrainin', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_daily_rain-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'precipitation', + 'friendly_name': 'Station A Daily rain', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_a_daily_rain', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_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_a_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': 'AA:AA:AA:AA:AA:AA_dewPoint', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_dew_point-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'temperature', + 'friendly_name': 'Station A Dew point', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_a_dew_point', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '27.7777777777778', + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_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_a_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': 'AA:AA:AA:AA:AA:AA_feelsLike', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_feels_like-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'temperature', + 'friendly_name': 'Station A Feels like', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_a_feels_like', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '29.4444444444444', + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_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_a_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': 'AA:AA:AA:AA:AA:AA_hourlyrainin', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_hourly_rain-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'precipitation_intensity', + 'friendly_name': 'Station A Hourly rain', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_a_hourly_rain', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_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_a_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': 'AA:AA:AA:AA:AA:AA_humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'humidity', + 'friendly_name': 'Station A Humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.station_a_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '60', + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_irradiance-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.station_a_irradiance', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Irradiance', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'AA:AA:AA:AA:AA:AA_solarradiation', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_irradiance-state] + None +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_last_rain-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.station_a_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': 'AA:AA:AA:AA:AA:AA_lastRain', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_last_rain-state] + None +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_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_a_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': 'AA:AA:AA:AA:AA:AA_maxdailygust', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_max_daily_gust-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'wind_speed', + 'friendly_name': 'Station A Max daily gust', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_a_max_daily_gust', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '36.72523008', + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_monthly_rain-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.station_a_monthly_rain', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + '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': 'AA:AA:AA:AA:AA:AA_monthlyrainin', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_monthly_rain-state] + None +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_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_a_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': 'AA:AA:AA:AA:AA:AA_baromrelin', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_relative_pressure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'pressure', + 'friendly_name': 'Station A Relative pressure', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_a_relative_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1001.89694313129', + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_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_a_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': 'AA:AA:AA:AA:AA:AA_tempf', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'temperature', + 'friendly_name': 'Station A Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_a_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '28.2777777777778', + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_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_a_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': 'AA:AA:AA:AA:AA:AA_uv', + 'unit_of_measurement': 'index', + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_uv_index-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'friendly_name': 'Station A UV index', + 'state_class': , + 'unit_of_measurement': 'index', + }), + 'context': , + 'entity_id': 'sensor.station_a_uv_index', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_weekly_rain-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.station_a_weekly_rain', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + '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': 'AA:AA:AA:AA:AA:AA_weeklyrainin', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_weekly_rain-state] + None +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_wind_direction-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.station_a_wind_direction', + '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': 'Wind direction', + 'platform': 'ambient_network', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_direction', + 'unique_id': 'AA:AA:AA:AA:AA:AA_winddir', + 'unit_of_measurement': '°', + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_wind_direction-state] + None +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_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_a_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': 'AA:AA:AA:AA:AA:AA_windgustmph', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_wind_gust-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'wind_speed', + 'friendly_name': 'Station A Wind gust', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_a_wind_gust', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '14.75768448', + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_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_a_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': 'AA:AA:AA:AA:AA:AA_windspeedmph', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_wind_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'wind_speed', + 'friendly_name': 'Station A Wind speed', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_a_wind_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '14.03347968', + }) +# --- diff --git a/tests/components/ambient_network/test_config_flow.py b/tests/components/ambient_network/test_config_flow.py new file mode 100644 index 00000000000..d9093de7234 --- /dev/null +++ b/tests/components/ambient_network/test_config_flow.py @@ -0,0 +1,85 @@ +"""Test the Ambient Weather Network config flow.""" + +from typing import Any +from unittest.mock import AsyncMock, patch + +from aioambient import OpenAPI +import pytest + +from homeassistant.components.ambient_network.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER, ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +pytestmark = pytest.mark.usefixtures("mock_setup_entry") + + +@pytest.mark.parametrize("config_entry", ["AA:AA:AA:AA:AA:AA"], indirect=True) +async def test_happy_path( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + open_api: OpenAPI, + aioambient: AsyncMock, + devices_by_location: list[dict[str, Any]], + config_entry: ConfigEntry, +) -> None: + """Test the happy path.""" + + setup_result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert setup_result["type"] == FlowResultType.FORM + assert setup_result["step_id"] == "user" + + with patch.object( + open_api, + "get_devices_by_location", + AsyncMock(return_value=devices_by_location), + ): + user_result = await hass.config_entries.flow.async_configure( + setup_result["flow_id"], + {"location": {"latitude": 10.0, "longitude": 20.0, "radius": 1.0}}, + ) + + assert user_result["type"] == FlowResultType.FORM + assert user_result["step_id"] == "station" + + stations_result = await hass.config_entries.flow.async_configure( + user_result["flow_id"], + { + "station": "AA:AA:AA:AA:AA:AA", + }, + ) + + assert stations_result["type"] == FlowResultType.CREATE_ENTRY + assert stations_result["title"] == config_entry.title + assert stations_result["data"] == config_entry.data + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_no_station_found( + hass: HomeAssistant, + aioambient: AsyncMock, + open_api: OpenAPI, +) -> None: + """Test that we abort when we cannot find a station in the area.""" + + setup_result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert setup_result["type"] == FlowResultType.FORM + assert setup_result["step_id"] == "user" + + with patch.object( + open_api, + "get_devices_by_location", + AsyncMock(return_value=[]), + ): + user_result = await hass.config_entries.flow.async_configure( + setup_result["flow_id"], + {"location": {"latitude": 10.0, "longitude": 20.0, "radius": 1.0}}, + ) + + assert user_result["type"] == FlowResultType.FORM + assert user_result["step_id"] == "user" + assert user_result["errors"] == {"base": "no_stations_found"} diff --git a/tests/components/ambient_network/test_sensor.py b/tests/components/ambient_network/test_sensor.py new file mode 100644 index 00000000000..b556c0c9c7c --- /dev/null +++ b/tests/components/ambient_network/test_sensor.py @@ -0,0 +1,123 @@ +"""Test Ambient Weather Network sensors.""" + +from datetime import datetime, timedelta +from unittest.mock import patch + +from aioambient import OpenAPI +from aioambient.errors import RequestError +from freezegun import freeze_time +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from .conftest import setup_platform + +from tests.common import async_fire_time_changed + + +@freeze_time("2023-11-08") +@pytest.mark.parametrize("config_entry", ["AA:AA:AA:AA:AA:AA"], indirect=True) +async def test_sensors( + hass: HomeAssistant, + open_api: OpenAPI, + aioambient, + config_entry, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test all sensors under normal operation.""" + await setup_platform(True, hass, config_entry) + + entity_entries = er.async_entries_for_config_entry( + entity_registry, config_entry.entry_id + ) + + assert entity_entries + for entity_entry in entity_entries: + assert hass.states.get(entity_entry.entity_id) == snapshot( + name=f"{entity_entry.entity_id}-state" + ) + assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry") + + +@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 +) -> None: + """Test that the sensors are not populated if the last data is absent.""" + await setup_platform(False, 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 + + +@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, + caplog, +) -> None: + """Test that we log errors properly.""" + + initial_datetime = datetime(year=2023, month=11, day=8) + with freeze_time(initial_datetime) as frozen_datetime: + # Normal state, sensor is available. + await setup_platform(True, hass, config_entry) + sensor = hass.states.get("sensor.station_a_relative_pressure") + assert sensor is not None + assert float(sensor.state) == pytest.approx(1001.89694313129) + + # Sensor becomes unavailable if the network is unavailable. Log message + # should only show up once. + for _ in range(5): + with patch.object( + open_api, "get_device_details", side_effect=RequestError() + ): + frozen_datetime.tick(timedelta(minutes=10)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + sensor = hass.states.get("sensor.station_a_relative_pressure") + assert sensor is not None + assert sensor.state == "unavailable" + assert caplog.text.count("Cannot connect to Ambient Network") == 1 + + # Network comes back. Sensor should start reporting again. Log message + # should only show up once. + for _ in range(5): + frozen_datetime.tick(timedelta(minutes=10)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + sensor = hass.states.get("sensor.station_a_relative_pressure") + assert sensor is not None + assert float(sensor.state) == pytest.approx(1001.89694313129) + assert caplog.text.count("Fetching ambient_network data recovered") == 1 From 093aee672cb103e30a7dedc5088d29c5f718cafa Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 16 Apr 2024 10:19:23 +0200 Subject: [PATCH 597/967] Fix ambient network test linting (#115691) --- tests/components/ambient_network/conftest.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/components/ambient_network/conftest.py b/tests/components/ambient_network/conftest.py index 3afadbfa722..ede44b5d92f 100644 --- a/tests/components/ambient_network/conftest.py +++ b/tests/components/ambient_network/conftest.py @@ -79,7 +79,7 @@ async def setup_platform( expected_outcome: bool, hass: HomeAssistant, config_entry: MockConfigEntry, -): +) -> None: """Load the Ambient Network integration with the provided OpenAPI and config entry.""" config_entry.add_to_hass(hass) @@ -87,5 +87,3 @@ async def setup_platform( await hass.config_entries.async_setup(config_entry.entry_id) == expected_outcome ) await hass.async_block_till_done() - - return From d1ed8d817c514630114e1849808365f4dad8151a Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 16 Apr 2024 10:50:51 +0200 Subject: [PATCH 598/967] Remove Adafruit-BBIO from commented requirements (#115689) --- script/gen_requirements_all.py | 1 - 1 file changed, 1 deletion(-) diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index d8fffac1a06..3f96e41a8ef 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -18,7 +18,6 @@ from homeassistant.util.yaml.loader import load_yaml from script.hassfest.model import Integration COMMENT_REQUIREMENTS = ( - "Adafruit-BBIO", "atenpdu", # depends on pysnmp which is not maintained at this time "avea", # depends on bluepy "avion", From 679752ceb8d96df9387191906bc0dcb89e5d912a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Apr 2024 12:33:10 +0200 Subject: [PATCH 599/967] Bump github/codeql-action from 3.24.10 to 3.25.0 (#115686) --- .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 475c0bd352f..9dba09557e3 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,11 +24,11 @@ jobs: uses: actions/checkout@v4.1.2 - name: Initialize CodeQL - uses: github/codeql-action/init@v3.24.10 + uses: github/codeql-action/init@v3.25.0 with: languages: python - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3.24.10 + uses: github/codeql-action/analyze@v3.25.0 with: category: "/language:python" From 7cd0fe3c5f1c4dad49f5c05ebd755c2b86d63fa4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 16 Apr 2024 15:58:57 +0200 Subject: [PATCH 600/967] Don't reload other automations when saving an automation (#80254) * Only reload modified automation * Correct check for existing automation * Add tests * Remove the new service, improve ReloadServiceHelper * Revert unneeded changes * Update tests * Address review comments * Improve test coverage * Address review comments * Tweak reloader code + add a targetted test * Apply suggestions from code review Co-authored-by: Martin Hjelmare * Explain the tests + add more variations * Fix copy-paste mistake in test * Rephrase explanation of expected test outcome --------- Co-authored-by: Martin Hjelmare --- .../components/automation/__init__.py | 56 +++- homeassistant/components/config/automation.py | 4 +- homeassistant/helpers/service.py | 53 +++- tests/components/automation/test_init.py | 253 +++++++++++++++++- tests/components/config/test_automation.py | 8 +- tests/helpers/test_service.py | 136 ++++++++++ 6 files changed, 484 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index afc8f9aba10..89a2817e236 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -331,17 +331,25 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await async_get_blueprints(hass).async_reset_cache() if (conf := await component.async_prepare_reload(skip_reset=True)) is None: return - await _async_process_config(hass, conf, component) + if automation_id := service_call.data.get(CONF_ID): + await _async_process_single_config(hass, conf, component, automation_id) + else: + await _async_process_config(hass, conf, component) hass.bus.async_fire(EVENT_AUTOMATION_RELOADED, context=service_call.context) - reload_helper = ReloadServiceHelper(reload_service_handler) + def reload_targets(service_call: ServiceCall) -> set[str | None]: + if automation_id := service_call.data.get(CONF_ID): + return {automation_id} + return {automation.unique_id for automation in component.entities} + + reload_helper = ReloadServiceHelper(reload_service_handler, reload_targets) async_register_admin_service( hass, DOMAIN, SERVICE_RELOAD, reload_helper.execute_service, - schema=vol.Schema({}), + schema=vol.Schema({vol.Optional(CONF_ID): str}), ) websocket_api.async_register_command(hass, websocket_config) @@ -859,6 +867,7 @@ class AutomationEntityConfig: async def _prepare_automation_config( hass: HomeAssistant, config: ConfigType, + wanted_automation_id: str | None, ) -> list[AutomationEntityConfig]: """Parse configuration and prepare automation entity configuration.""" automation_configs: list[AutomationEntityConfig] = [] @@ -866,6 +875,10 @@ async def _prepare_automation_config( conf: list[ConfigType] = config[DOMAIN] for list_no, config_block in enumerate(conf): + automation_id: str | None = config_block.get(CONF_ID) + if wanted_automation_id is not None and automation_id != wanted_automation_id: + continue + 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 @@ -1025,7 +1038,7 @@ async def _async_process_config( return automation_matches, config_matches - automation_configs = await _prepare_automation_config(hass, config) + automation_configs = await _prepare_automation_config(hass, config, None) automations: list[BaseAutomationEntity] = list(component.entities) # Find automations and configurations which have matches @@ -1049,6 +1062,41 @@ async def _async_process_config( await component.async_add_entities(entities) +def _automation_matches_config( + automation: BaseAutomationEntity | None, config: AutomationEntityConfig | None +) -> bool: + """Return False if an automation's config has been changed.""" + if not automation: + return False + if not config: + return False + name = _automation_name(config) + return automation.name == name and automation.raw_config == config.raw_config + + +async def _async_process_single_config( + hass: HomeAssistant, + config: dict[str, Any], + component: EntityComponent[BaseAutomationEntity], + automation_id: str, +) -> None: + """Process config and add a single automation.""" + + automation_configs = await _prepare_automation_config(hass, config, automation_id) + automation = next( + (x for x in component.entities if x.unique_id == automation_id), None + ) + automation_config = automation_configs[0] if automation_configs else None + + if _automation_matches_config(automation, automation_config): + return + + if automation: + await automation.async_remove() + entities = await _create_automation_entities(hass, automation_configs) + await component.async_add_entities(entities) + + async def _async_process_if( hass: HomeAssistant, name: str, config: dict[str, Any] ) -> IfAction | None: diff --git a/homeassistant/components/config/automation.py b/homeassistant/components/config/automation.py index a5a010c00a6..ccc36dc4430 100644 --- a/homeassistant/components/config/automation.py +++ b/homeassistant/components/config/automation.py @@ -26,7 +26,9 @@ def async_setup(hass: HomeAssistant) -> bool: async def hook(action: str, config_key: str) -> None: """post_write_hook for Config View that reloads automations.""" if action != ACTION_DELETE: - await hass.services.async_call(DOMAIN, SERVICE_RELOAD) + await hass.services.async_call( + DOMAIN, SERVICE_RELOAD, {CONF_ID: config_key} + ) return ent_reg = er.async_get(hass) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 3947bc9cbf8..66c9f7db3e6 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -77,6 +77,8 @@ _LOGGER = logging.getLogger(__name__) SERVICE_DESCRIPTION_CACHE = "service_description_cache" ALL_SERVICE_DESCRIPTIONS_CACHE = "all_service_descriptions_cache" +_T = TypeVar("_T") + @cache def _base_components() -> dict[str, ModuleType]: @@ -1154,40 +1156,67 @@ def verify_domain_control( class ReloadServiceHelper: - """Helper for reload services to minimize unnecessary reloads.""" + """Helper for reload services. - def __init__(self, service_func: Callable[[ServiceCall], Awaitable]) -> None: + The helper has the following purposes: + - Make sure reloads do not happen in parallel + - Avoid redundant reloads of the same target + """ + + def __init__( + self, + service_func: Callable[[ServiceCall], Awaitable], + reload_targets_func: Callable[[ServiceCall], set[_T]], + ) -> None: """Initialize ReloadServiceHelper.""" self._service_func = service_func self._service_running = False self._service_condition = asyncio.Condition() + self._pending_reload_targets: set[_T] = set() + self._reload_targets_func = reload_targets_func async def execute_service(self, service_call: ServiceCall) -> None: """Execute the service. - If a previous reload task if currently in progress, wait for it to finish first. + If a previous reload task is currently in progress, wait for it to finish first. Once the previous reload task has finished, one of the waiting tasks will be - assigned to execute the reload, the others will wait for the reload to finish. + assigned to execute the reload of the targets it is assigned to reload. The + other tasks will wait if they should reload the same target, otherwise they + will wait for the next round. """ do_reload = False + reload_targets = None async with self._service_condition: if self._service_running: - # A previous reload task is already in progress, wait for it to finish + # A previous reload task is already in progress, wait for it to finish, + # because that task may be reloading a stale version of the resource. await self._service_condition.wait() - async with self._service_condition: - if not self._service_running: - # This task will do the reload - self._service_running = True - do_reload = True - else: - # Another task will perform the reload, wait for it to finish + while True: + async with self._service_condition: + # Once we've passed this point, we assume the version of the resource is + # the one our task was assigned to reload, or a newer one. Regardless of + # which, our task is happy as long as the target is reloaded at least + # once. + if reload_targets is None: + reload_targets = self._reload_targets_func(service_call) + self._pending_reload_targets |= reload_targets + if not self._service_running: + # This task will do a reload + self._service_running = True + do_reload = True + break + # Another task will perform a reload, wait for it to finish await self._service_condition.wait() + # Check if the reload this task is waiting for has been completed + if reload_targets.isdisjoint(self._pending_reload_targets): + break if do_reload: # Reload, then notify other tasks await self._service_func(service_call) async with self._service_condition: self._service_running = False + self._pending_reload_targets -= reload_targets self._service_condition.notify_all() diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 5b3fc2a723e..61e6d0e4660 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -21,6 +21,7 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_NAME, + CONF_ID, EVENT_HOMEASSISTANT_STARTED, SERVICE_RELOAD, SERVICE_TOGGLE, @@ -692,7 +693,9 @@ async def test_reload_config_handles_load_fails(hass: HomeAssistant, calls) -> N assert len(calls) == 2 -@pytest.mark.parametrize("service", ["turn_off_stop", "turn_off_no_stop", "reload"]) +@pytest.mark.parametrize( + "service", ["turn_off_stop", "turn_off_no_stop", "reload", "reload_single"] +) async def test_automation_stops(hass: HomeAssistant, calls, service) -> None: """Test that turning off / reloading stops any running actions as appropriate.""" entity_id = "automation.hello" @@ -700,6 +703,7 @@ async def test_automation_stops(hass: HomeAssistant, calls, service) -> None: config = { automation.DOMAIN: { + "id": "sun", "alias": "hello", "trigger": {"platform": "event", "event_type": "test_event"}, "action": [ @@ -737,7 +741,7 @@ async def test_automation_stops(hass: HomeAssistant, calls, service) -> None: {ATTR_ENTITY_ID: entity_id, automation.CONF_STOP_ACTIONS: False}, blocking=True, ) - else: + elif service == "reload": config[automation.DOMAIN]["alias"] = "goodbye" with patch( "homeassistant.config.load_yaml_config_file", @@ -747,6 +751,19 @@ async def test_automation_stops(hass: HomeAssistant, calls, service) -> None: await hass.services.async_call( automation.DOMAIN, SERVICE_RELOAD, blocking=True ) + else: # service == "reload_single" + config[automation.DOMAIN]["alias"] = "goodbye" + with patch( + "homeassistant.config.load_yaml_config_file", + autospec=True, + return_value=config, + ): + await hass.services.async_call( + automation.DOMAIN, + SERVICE_RELOAD, + {CONF_ID: "sun"}, + blocking=True, + ) hass.states.async_set(test_entity, "goodbye") await hass.async_block_till_done() @@ -801,6 +818,238 @@ async def test_reload_unchanged_does_not_stop( assert len(calls) == 1 +async def test_reload_single_unchanged_does_not_stop( + hass: HomeAssistant, calls +) -> None: + """Test that reloading stops any running actions as appropriate.""" + test_entity = "test.entity" + + config = { + automation.DOMAIN: { + "id": "sun", + "alias": "hello", + "trigger": {"platform": "event", "event_type": "test_event"}, + "action": [ + {"event": "running"}, + {"wait_template": "{{ is_state('test.entity', 'goodbye') }}"}, + {"service": "test.automation"}, + ], + } + } + assert await async_setup_component(hass, automation.DOMAIN, config) + + running = asyncio.Event() + + @callback + def running_cb(event): + running.set() + + hass.bus.async_listen_once("running", running_cb) + hass.states.async_set(test_entity, "hello") + + hass.bus.async_fire("test_event") + await running.wait() + assert len(calls) == 0 + + with patch( + "homeassistant.config.load_yaml_config_file", + autospec=True, + return_value=config, + ): + await hass.services.async_call( + automation.DOMAIN, + SERVICE_RELOAD, + {CONF_ID: "sun"}, + blocking=True, + ) + + hass.states.async_set(test_entity, "goodbye") + await hass.async_block_till_done() + + assert len(calls) == 1 + + +async def test_reload_single_add_automation(hass: HomeAssistant, calls) -> None: + """Test that reloading a single automation.""" + config1 = {automation.DOMAIN: {}} + config2 = { + automation.DOMAIN: { + "id": "sun", + "alias": "hello", + "trigger": {"platform": "event", "event_type": "test_event"}, + "action": [{"service": "test.automation"}], + } + } + assert await async_setup_component(hass, automation.DOMAIN, config1) + + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 0 + + with patch( + "homeassistant.config.load_yaml_config_file", + autospec=True, + return_value=config2, + ): + await hass.services.async_call( + automation.DOMAIN, + SERVICE_RELOAD, + {CONF_ID: "sun"}, + blocking=True, + ) + + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 1 + + +async def test_reload_single_parallel_calls(hass: HomeAssistant, calls) -> None: + """Test reloading single automations in parallel.""" + config1 = {automation.DOMAIN: {}} + config2 = { + automation.DOMAIN: [ + { + "id": "sun", + "alias": "hello", + "trigger": {"platform": "event", "event_type": "test_event_sun"}, + "action": [{"service": "test.automation"}], + }, + { + "id": "moon", + "alias": "goodbye", + "trigger": {"platform": "event", "event_type": "test_event_moon"}, + "action": [{"service": "test.automation"}], + }, + { + "id": "mars", + "alias": "goodbye", + "trigger": {"platform": "event", "event_type": "test_event_mars"}, + "action": [{"service": "test.automation"}], + }, + { + "id": "venus", + "alias": "goodbye", + "trigger": {"platform": "event", "event_type": "test_event_venus"}, + "action": [{"service": "test.automation"}], + }, + ] + } + assert await async_setup_component(hass, automation.DOMAIN, config1) + + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 0 + + # Trigger multiple reload service calls, each automation is reloaded twice. + # This tests the logic in the `ReloadServiceHelper` which avoids redundant + # reloads of the same target automation. + with patch( + "homeassistant.config.load_yaml_config_file", + autospec=True, + return_value=config2, + ): + tasks = [ + hass.services.async_call( + automation.DOMAIN, + SERVICE_RELOAD, + {CONF_ID: "sun"}, + blocking=False, + ), + hass.services.async_call( + automation.DOMAIN, + SERVICE_RELOAD, + {CONF_ID: "moon"}, + blocking=False, + ), + hass.services.async_call( + automation.DOMAIN, + SERVICE_RELOAD, + {CONF_ID: "mars"}, + blocking=False, + ), + hass.services.async_call( + automation.DOMAIN, + SERVICE_RELOAD, + {CONF_ID: "venus"}, + blocking=False, + ), + hass.services.async_call( + automation.DOMAIN, + SERVICE_RELOAD, + {CONF_ID: "sun"}, + blocking=False, + ), + hass.services.async_call( + automation.DOMAIN, + SERVICE_RELOAD, + {CONF_ID: "moon"}, + blocking=False, + ), + hass.services.async_call( + automation.DOMAIN, + SERVICE_RELOAD, + {CONF_ID: "mars"}, + blocking=False, + ), + hass.services.async_call( + automation.DOMAIN, + SERVICE_RELOAD, + {CONF_ID: "venus"}, + blocking=False, + ), + ] + await asyncio.gather(*tasks) + await hass.async_block_till_done() + + # Sanity check to ensure all automations are correctly setup + hass.bus.async_fire("test_event_sun") + await hass.async_block_till_done() + assert len(calls) == 1 + hass.bus.async_fire("test_event_moon") + await hass.async_block_till_done() + assert len(calls) == 2 + hass.bus.async_fire("test_event_mars") + await hass.async_block_till_done() + assert len(calls) == 3 + hass.bus.async_fire("test_event_venus") + await hass.async_block_till_done() + assert len(calls) == 4 + + +async def test_reload_single_remove_automation(hass: HomeAssistant, calls) -> None: + """Test that reloading a single automation.""" + config1 = { + automation.DOMAIN: { + "id": "sun", + "alias": "hello", + "trigger": {"platform": "event", "event_type": "test_event"}, + "action": [{"service": "test.automation"}], + } + } + config2 = {automation.DOMAIN: {}} + assert await async_setup_component(hass, automation.DOMAIN, config1) + + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 1 + + with patch( + "homeassistant.config.load_yaml_config_file", + autospec=True, + return_value=config2, + ): + await hass.services.async_call( + automation.DOMAIN, + SERVICE_RELOAD, + {CONF_ID: "sun"}, + blocking=True, + ) + + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 1 + + async def test_reload_moved_automation_without_alias( hass: HomeAssistant, calls ) -> None: diff --git a/tests/components/config/test_automation.py b/tests/components/config/test_automation.py index 80f68b96fe1..b17face10d9 100644 --- a/tests/components/config/test_automation.py +++ b/tests/components/config/test_automation.py @@ -10,7 +10,7 @@ import pytest from homeassistant.bootstrap import async_setup_component from homeassistant.components import config from homeassistant.components.config import automation -from homeassistant.const import STATE_ON, STATE_UNAVAILABLE +from homeassistant.const import STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.util import yaml @@ -82,10 +82,8 @@ async def test_update_automation_config( ) await hass.async_block_till_done() assert sorted(hass.states.async_entity_ids("automation")) == [ - "automation.automation_0", "automation.automation_1", ] - assert hass.states.get("automation.automation_0").state == STATE_UNAVAILABLE assert hass.states.get("automation.automation_1").state == STATE_ON assert resp.status == HTTPStatus.OK @@ -260,10 +258,8 @@ async def test_update_remove_key_automation_config( ) await hass.async_block_till_done() assert sorted(hass.states.async_entity_ids("automation")) == [ - "automation.automation_0", "automation.automation_1", ] - assert hass.states.get("automation.automation_0").state == STATE_UNAVAILABLE assert hass.states.get("automation.automation_1").state == STATE_ON assert resp.status == HTTPStatus.OK @@ -305,10 +301,8 @@ async def test_bad_formatted_automations( ) await hass.async_block_till_done() assert sorted(hass.states.async_entity_ids("automation")) == [ - "automation.automation_0", "automation.automation_1", ] - assert hass.states.get("automation.automation_0").state == STATE_UNAVAILABLE assert hass.states.get("automation.automation_1").state == STATE_ON assert resp.status == HTTPStatus.OK diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index b5e71f4c9d8..e32768ee33e 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -1852,3 +1852,139 @@ async def test_async_extract_config_entry_ids(hass: HomeAssistant) -> None: ) assert await service.async_extract_config_entry_ids(hass, call) == {"abc"} + + +async def test_reload_service_helper(hass: HomeAssistant) -> None: + """Test the reload service helper.""" + + active_reload_calls = 0 + reloaded = [] + + async def reload_service_handler(service_call: ServiceCall) -> None: + """Remove all automations and load new ones from config.""" + nonlocal active_reload_calls + # Assert the reload helper prevents parallel reloads + assert not active_reload_calls + active_reload_calls += 1 + if not (target := service_call.data.get("target")): + reloaded.append("all") + else: + reloaded.append(target) + await asyncio.sleep(0.01) + active_reload_calls -= 1 + + def reload_targets(service_call: ServiceCall) -> set[str | None]: + if target_id := service_call.data.get("target"): + return {target_id} + return {"target1", "target2", "target3", "target4"} + + # Test redundant reload of single targets + reloader = service.ReloadServiceHelper(reload_service_handler, reload_targets) + tasks = [ + # This reload task will start executing first, (target1) + reloader.execute_service(ServiceCall("test", "test", {"target": "target1"})), + # These reload tasks will be deduplicated to (target2, target3, target4, target1) + # while the first task is reloaded, note that target1 can't be deduplicated + # because it's already being reloaded. + reloader.execute_service(ServiceCall("test", "test", {"target": "target2"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target3"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target4"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target1"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target2"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target3"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target4"})), + ] + await asyncio.gather(*tasks) + assert reloaded == unordered( + ["target1", "target2", "target3", "target4", "target1"] + ) + + # Test redundant reload of multiple targets + single target + reloaded.clear() + tasks = [ + # This reload task will start executing first, (target1) + reloader.execute_service(ServiceCall("test", "test", {"target": "target1"})), + # These reload tasks will be deduplicated to (target2, target3, target4, all) + # while the first task is reloaded. + reloader.execute_service(ServiceCall("test", "test", {"target": "target2"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target3"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target4"})), + reloader.execute_service(ServiceCall("test", "test")), + ] + await asyncio.gather(*tasks) + assert reloaded == unordered(["target1", "target2", "target3", "target4", "all"]) + + # Test redundant reload of multiple targets + single target + reloaded.clear() + tasks = [ + # This reload task will start executing first, (all) + reloader.execute_service(ServiceCall("test", "test")), + # These reload tasks will be deduplicated to (target1, target2, target3, target4) + # while the first task is reloaded. + reloader.execute_service(ServiceCall("test", "test", {"target": "target1"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target2"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target3"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target4"})), + ] + await asyncio.gather(*tasks) + assert reloaded == unordered(["all", "target1", "target2", "target3", "target4"]) + + # Test redundant reload of single targets + reloaded.clear() + tasks = [ + # This reload task will start executing first, (target1) + reloader.execute_service(ServiceCall("test", "test", {"target": "target1"})), + # These reload tasks will be deduplicated to (target2, target3, target4, target1) + # while the first task is reloaded, note that target1 can't be deduplicated + # because it's already being reloaded. + reloader.execute_service(ServiceCall("test", "test", {"target": "target2"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target3"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target4"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target1"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target2"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target3"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target4"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target1"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target2"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target3"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target4"})), + ] + await asyncio.gather(*tasks) + assert reloaded == unordered( + ["target1", "target2", "target3", "target4", "target1"] + ) + + # Test redundant reload of multiple targets + single target + reloaded.clear() + tasks = [ + # This reload task will start executing first, (target1) + reloader.execute_service(ServiceCall("test", "test", {"target": "target1"})), + # These reload tasks will be deduplicated to (target2, target3, target4, all) + # while the first task is reloaded. + reloader.execute_service(ServiceCall("test", "test", {"target": "target2"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target3"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target4"})), + reloader.execute_service(ServiceCall("test", "test")), + reloader.execute_service(ServiceCall("test", "test")), + ] + await asyncio.gather(*tasks) + assert reloaded == unordered(["target1", "target2", "target3", "target4", "all"]) + + # Test redundant reload of multiple targets + single target + reloaded.clear() + tasks = [ + # This reload task will start executing first, (all) + reloader.execute_service(ServiceCall("test", "test")), + # These reload tasks will be deduplicated to (target1, target2, target3, target4) + # while the first task is reloaded. + reloader.execute_service(ServiceCall("test", "test", {"target": "target1"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target2"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target3"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target4"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target1"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target2"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target3"})), + reloader.execute_service(ServiceCall("test", "test", {"target": "target4"})), + ] + await asyncio.gather(*tasks) + assert reloaded == unordered(["all", "target1", "target2", "target3", "target4"]) From e9894f8e91495ebe113f0145a3e78d7edb8a28c5 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 16 Apr 2024 16:13:03 +0200 Subject: [PATCH 601/967] Add extract media url service to media extractor (#100780) * Exclude manifest files from youtube media extraction * Add media_extractor service to extract media * Fix snapshot * Run ytdlp async * Add icon * Fix * Fix --- .../components/media_extractor/__init__.py | 71 +- .../components/media_extractor/const.py | 9 + .../components/media_extractor/icons.json | 3 +- .../components/media_extractor/services.yaml | 11 + .../components/media_extractor/strings.json | 14 + tests/components/media_extractor/__init__.py | 6 +- .../media_extractor/fixtures/no_formats.json | 87 + .../media_extractor/fixtures/soundcloud.json | 114 ++ .../media_extractor/fixtures/youtube_1.json | 1430 +++++++++++++++++ .../fixtures/youtube_empty_playlist.json | 49 + .../fixtures/youtube_playlist.json | 179 +++ .../media_extractor/snapshots/test_init.ambr | 20 + tests/components/media_extractor/test_init.py | 59 +- 13 files changed, 2045 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/media_extractor/const.py create mode 100644 tests/components/media_extractor/fixtures/no_formats.json create mode 100644 tests/components/media_extractor/fixtures/soundcloud.json create mode 100644 tests/components/media_extractor/fixtures/youtube_1.json create mode 100644 tests/components/media_extractor/fixtures/youtube_empty_playlist.json create mode 100644 tests/components/media_extractor/fixtures/youtube_playlist.json diff --git a/homeassistant/components/media_extractor/__init__.py b/homeassistant/components/media_extractor/__init__.py index 888265e8d3c..228a012a04f 100644 --- a/homeassistant/components/media_extractor/__init__.py +++ b/homeassistant/components/media_extractor/__init__.py @@ -17,18 +17,29 @@ from homeassistant.components.media_player import ( SERVICE_PLAY_MEDIA, ) from homeassistant.const import ATTR_ENTITY_ID -from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.core import ( + HomeAssistant, + ServiceCall, + ServiceResponse, + SupportsResponse, +) +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import ConfigType +from .const import ( + ATTR_FORMAT_QUERY, + ATTR_URL, + DEFAULT_STREAM_QUERY, + DOMAIN, + SERVICE_EXTRACT_MEDIA_URL, +) + _LOGGER = logging.getLogger(__name__) CONF_CUSTOMIZE_ENTITIES = "customize" CONF_DEFAULT_STREAM_QUERY = "default_query" -DEFAULT_STREAM_QUERY = "best" -DOMAIN = "media_extractor" - CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( @@ -47,10 +58,62 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the media extractor service.""" + async def extract_media_url(call: ServiceCall) -> ServiceResponse: + """Extract media url.""" + youtube_dl = YoutubeDL( + {"quiet": True, "logger": _LOGGER, "format": call.data[ATTR_FORMAT_QUERY]} + ) + + def extract_info() -> dict[str, Any]: + return cast( + dict[str, Any], + youtube_dl.extract_info( + call.data[ATTR_URL], download=False, process=False + ), + ) + + result = await hass.async_add_executor_job(extract_info) + if "entries" in result: + _LOGGER.warning("Playlists are not supported, looking for the first video") + entries = list(result["entries"]) + if entries: + selected_media = entries[0] + else: + raise HomeAssistantError("Playlist is empty") + else: + selected_media = result + if "formats" in selected_media: + if selected_media["extractor"] == "youtube": + url = get_best_stream_youtube(selected_media["formats"]) + else: + url = get_best_stream(selected_media["formats"]) + else: + url = cast(str, selected_media["url"]) + return {"url": url} + def play_media(call: ServiceCall) -> None: """Get stream URL and send it to the play_media service.""" MediaExtractor(hass, config[DOMAIN], call.data).extract_and_send() + default_format_query = config.get(DOMAIN, {}).get( + CONF_DEFAULT_STREAM_QUERY, DEFAULT_STREAM_QUERY + ) + + hass.services.async_register( + DOMAIN, + SERVICE_EXTRACT_MEDIA_URL, + extract_media_url, + schema=vol.Schema( + { + vol.Required(ATTR_URL): cv.string, + vol.Optional( + ATTR_FORMAT_QUERY, default=default_format_query + ): cv.string, + } + ), + supports_response=SupportsResponse.ONLY, + ) + hass.services.register( DOMAIN, SERVICE_PLAY_MEDIA, diff --git a/homeassistant/components/media_extractor/const.py b/homeassistant/components/media_extractor/const.py new file mode 100644 index 00000000000..009ab37602c --- /dev/null +++ b/homeassistant/components/media_extractor/const.py @@ -0,0 +1,9 @@ +"""Constants for media_extractor.""" + +DEFAULT_STREAM_QUERY = "best" +DOMAIN = "media_extractor" + +ATTR_URL = "url" +ATTR_FORMAT_QUERY = "format_query" + +SERVICE_EXTRACT_MEDIA_URL = "extract_media_url" diff --git a/homeassistant/components/media_extractor/icons.json b/homeassistant/components/media_extractor/icons.json index 71b65e7c4a6..7abc4410b19 100644 --- a/homeassistant/components/media_extractor/icons.json +++ b/homeassistant/components/media_extractor/icons.json @@ -1,5 +1,6 @@ { "services": { - "play_media": "mdi:play" + "play_media": "mdi:play", + "extract_media_url": "mdi:link" } } diff --git a/homeassistant/components/media_extractor/services.yaml b/homeassistant/components/media_extractor/services.yaml index 8af2d12d0e9..abfe52dc4f5 100644 --- a/homeassistant/components/media_extractor/services.yaml +++ b/homeassistant/components/media_extractor/services.yaml @@ -19,3 +19,14 @@ play_media: - "MUSIC" - "TVSHOW" - "VIDEO" +extract_media_url: + fields: + url: + required: true + example: "https://www.youtube.com/watch?v=dQw4w9WgXcQ" + selector: + text: + format_query: + example: "best" + selector: + text: diff --git a/homeassistant/components/media_extractor/strings.json b/homeassistant/components/media_extractor/strings.json index 0cdffd5d508..1af84b5b8c8 100644 --- a/homeassistant/components/media_extractor/strings.json +++ b/homeassistant/components/media_extractor/strings.json @@ -13,6 +13,20 @@ "description": "The type of the content to play. Must be one of MUSIC, TVSHOW, VIDEO, EPISODE, CHANNEL or PLAYLIST MUSIC." } } + }, + "extract_media_url": { + "name": "Get Media URL", + "description": "Extract media url from a service.", + "fields": { + "url": { + "name": "Media URL", + "description": "URL where the media can be found." + }, + "format_query": { + "name": "Format query", + "description": "Youtube-dl query to select the quality of the result." + } + } } } } diff --git a/tests/components/media_extractor/__init__.py b/tests/components/media_extractor/__init__.py index 7aac726501b..79130f1ea4b 100644 --- a/tests/components/media_extractor/__init__.py +++ b/tests/components/media_extractor/__init__.py @@ -36,9 +36,13 @@ class MockYoutubeDL: """Initialize mock object for YoutubeDL.""" self.params = params - def extract_info(self, url: str, *, process: bool = False) -> dict[str, Any]: + def extract_info( + self, url: str, *, download: bool = True, process: bool = False + ) -> dict[str, Any]: """Return info.""" self._fixture = _get_base_fixture(url) + if not download: + return load_json_object_fixture(f"media_extractor/{self._fixture}.json") return load_json_object_fixture(f"media_extractor/{self._fixture}_info.json") def process_ie_result( diff --git a/tests/components/media_extractor/fixtures/no_formats.json b/tests/components/media_extractor/fixtures/no_formats.json new file mode 100644 index 00000000000..aefb1525738 --- /dev/null +++ b/tests/components/media_extractor/fixtures/no_formats.json @@ -0,0 +1,87 @@ +{ + "id": "223644256", + "uploader": "BRUTTOBAND", + "uploader_id": "111488150", + "uploader_url": "https://soundcloud.com/bruttoband", + "timestamp": 1442140228, + "title": "BRUTTO - \u0420\u043e\u0434\u043d\u044b \u043a\u0440\u0430\u0439", + "description": "", + "thumbnails": [ + { + "id": "mini", + "url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-mini.jpg", + "width": 16, + "height": 16 + }, + { + "id": "tiny", + "url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-tiny.jpg", + "width": 20, + "height": 20 + }, + { + "id": "small", + "url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-small.jpg", + "width": 32, + "height": 32 + }, + { + "id": "badge", + "url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-badge.jpg", + "width": 47, + "height": 47 + }, + { + "id": "t67x67", + "url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-t67x67.jpg", + "width": 67, + "height": 67 + }, + { + "id": "large", + "url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-large.jpg", + "width": 100, + "height": 100 + }, + { + "id": "t300x300", + "url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-t300x300.jpg", + "width": 300, + "height": 300 + }, + { + "id": "crop", + "url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-crop.jpg", + "width": 400, + "height": 400 + }, + { + "id": "t500x500", + "url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-t500x500.jpg", + "width": 500, + "height": 500 + }, + { + "id": "original", + "url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-original.jpg", + "preference": 10 + } + ], + "duration": 229.089, + "webpage_url": "https://soundcloud.com/bruttoband/brutto-11", + "license": "all-rights-reserved", + "view_count": 291779, + "like_count": 3347, + "comment_count": 14, + "repost_count": 59, + "genre": "Brutto", + "url": "https://cf-media.sndcdn.com/50remGX1OqRY.128.mp3?Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiKjovL2NmLW1lZGlhLnNuZGNkbi5jb20vNTByZW1HWDFPcVJZLjEyOC5tcDMqIiwiQ29uZGl0aW9uIjp7IkRhdGVMZXNzVGhhbiI6eyJBV1M6RXBvY2hUaW1lIjoxNjk0Nzk4NTkzfX19XX0_&Signature=flALJvEBnzS0ZOOhf0-07Ap~NURw2Gn2OqkeKKTTMX5HRGJw9eXFay79tcC4GsMMXWUgWoCx-n3yelpyilE2MOEIufBNUbjqRfMSJaX5YhYxjQdoDYuiU~gqBzJyPw9pKzr6P8~5HNKL3Idr0CNhUzdV6FQLaUPKMMibq9ghV833mUmdyvdk1~GZBc8MOg9GrTdcigGgpPzd-vrIMICMvFzFnwBOeOotxX2Vfqf9~wVekBKGlvB9A~7TlZ71lv9Fl9u4m8rse9E-mByweVc1M784ehJV3~tRPjuF~FXXWKP8x0nGJmoq7RAnG7iFIt~fQFmsfOq2o~PG7dHMRPh7hw__&Key-Pair-Id=APKAI6TU7MMXM5DG6EPQ", + "ext": "mp3", + "original_url": "https://soundcloud.com/bruttoband/brutto-11", + "webpage_url_basename": "brutto-11", + "webpage_url_domain": "soundcloud.com", + "extractor": "soundcloud", + "extractor_key": "Soundcloud", + "heatmap": [], + "automatic_captions": {} +} diff --git a/tests/components/media_extractor/fixtures/soundcloud.json b/tests/components/media_extractor/fixtures/soundcloud.json new file mode 100644 index 00000000000..ee430e43982 --- /dev/null +++ b/tests/components/media_extractor/fixtures/soundcloud.json @@ -0,0 +1,114 @@ +{ + "id": "223644255", + "uploader": "BRUTTOBAND", + "uploader_id": "111488150", + "uploader_url": "https://soundcloud.com/bruttoband", + "timestamp": 1442140228, + "title": "BRUTTO - \u0420\u043e\u0434\u043d\u044b \u043a\u0440\u0430\u0439", + "description": "", + "thumbnails": [ + { + "id": "mini", + "url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-mini.jpg", + "width": 16, + "height": 16 + }, + { + "id": "tiny", + "url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-tiny.jpg", + "width": 20, + "height": 20 + }, + { + "id": "small", + "url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-small.jpg", + "width": 32, + "height": 32 + }, + { + "id": "badge", + "url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-badge.jpg", + "width": 47, + "height": 47 + }, + { + "id": "t67x67", + "url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-t67x67.jpg", + "width": 67, + "height": 67 + }, + { + "id": "large", + "url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-large.jpg", + "width": 100, + "height": 100 + }, + { + "id": "t300x300", + "url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-t300x300.jpg", + "width": 300, + "height": 300 + }, + { + "id": "crop", + "url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-crop.jpg", + "width": 400, + "height": 400 + }, + { + "id": "t500x500", + "url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-t500x500.jpg", + "width": 500, + "height": 500 + }, + { + "id": "original", + "url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-original.jpg", + "preference": 10 + } + ], + "duration": 229.089, + "webpage_url": "https://soundcloud.com/bruttoband/brutto-11", + "license": "all-rights-reserved", + "view_count": 291779, + "like_count": 3347, + "comment_count": 14, + "repost_count": 59, + "genre": "Brutto", + "formats": [ + { + "url": "https://cf-hls-media.sndcdn.com/playlist/50remGX1OqRY.128.mp3/playlist.m3u8?Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiKjovL2NmLWhscy1tZWRpYS5zbmRjZG4uY29tL3BsYXlsaXN0LzUwcmVtR1gxT3FSWS4xMjgubXAzL3BsYXlsaXN0Lm0zdTgqIiwiQ29uZGl0aW9uIjp7IkRhdGVMZXNzVGhhbiI6eyJBV1M6RXBvY2hUaW1lIjoxNjk1NTAzOTYzfX19XX0_&Signature=eNeIoSTgRZL89YBJYXpmRg0AVGk3M0gV4E4rYPYbFw6pTePHO4o8Mv6HwdK85FOMsaUHZvYgzc35uWPhAr1SUqqjnm--xwN8VUrDkCPgdv97Vrs9qJ9QElHKnlWhK2-BDs3Y7sDcAurA00L2uReB-vjI-4K65WBApYBTaUGnOACimoVAOWHmtigO0Ap5DxlEh7fqqwi88enEvVDE-98v5uX9FcV9lq9AfVwEtfqbPsjVJyh6WbWAB3PJDJElvV13RgKmzVvbFluLElYlDud9WMsHjztdWhdaRzGOj1AfcQcwkQbQlBRiAKMtqrRlzAAXnBfLvMF3DOvdYWeCwJeCXA__&Key-Pair-Id=APKAI6TU7MMXM5DG6EPQ", + "ext": "mp3", + "abr": 128, + "format_id": "hls_mp3_128", + "protocol": "m3u8_native", + "preference": null, + "vcodec": "none" + }, + { + "url": "https://cf-media.sndcdn.com/50remGX1OqRY.128.mp3?Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiKjovL2NmLW1lZGlhLnNuZGNkbi5jb20vNTByZW1HWDFPcVJZLjEyOC5tcDMqIiwiQ29uZGl0aW9uIjp7IkRhdGVMZXNzVGhhbiI6eyJBV1M6RXBvY2hUaW1lIjoxNjk1NTAzOTYzfX19XX0_&Signature=JAG~zJ~2NOWOgiuHLCSYWwdjUVuYWR2fBvmxPGSnLzMgX2xqu5~WfOk-gOyRUbHhnKnybUbP70cr6~t~Qx0KEU5mwIy2H0YhOXDHFX5RJVQlj1iCVuko-hAFJc7RtZuKTP5oCWOM-R2a6HfYN88YAIqgwWbGvTKin1CAgHaICeoM2p5O50n-kp05KgCw3RKcRutkYT-RVcWkmXtY4D4Jtw~LuBERDNyErseTHzmruDCkaYkVNlTcaIdgygQjgxVlgZiIRj-p0vRNO0qv5Bc0LfNMBzYm9fTAr86c~TzxyvQRhwHOPYp-DCXcs1K6i9x4BVvHWLOSHr0Dhd3X4fe5kw__&Key-Pair-Id=APKAI6TU7MMXM5DG6EPQ", + "ext": "mp3", + "abr": 128, + "format_id": "http_mp3_128", + "protocol": "http", + "preference": null, + "vcodec": "none" + }, + { + "url": "https://cf-hls-opus-media.sndcdn.com/playlist/50remGX1OqRY.64.opus/playlist.m3u8?Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiKjovL2NmLWhscy1vcHVzLW1lZGlhLnNuZGNkbi5jb20vcGxheWxpc3QvNTByZW1HWDFPcVJZLjY0Lm9wdXMvcGxheWxpc3QubTN1OCoiLCJDb25kaXRpb24iOnsiRGF0ZUxlc3NUaGFuIjp7IkFXUzpFcG9jaFRpbWUiOjE2OTU1MDM5NjR9fX1dfQ__&Signature=HqyeKoK4gx1Bd3jId5v1-9ltDY2SN7fzGp6M7tW3kWluv8Ns0SgANPKG3~Tzo8hUrQzAYVvwMbO2F75b6NBeBr4xx0SRCeKotZdArzBFT4Wtrz4HtEddLyPjp12vWYCDuOshd1sTjrvaBAd9TAFTZuwyal1OKpLMsqK0QN-KFH-5GsmLDZTPWcoVkMqC7XBmNWTq0G1mtVeP57TN~9T7qEYqRLFIDXdm2HKSPSR4BB2gjaZUK22pBUPSGVr-ziBSpNVHvNISy2QdObjS5zjuAe8bl-npQ3PlFatYECDI3Gc~wjCNIJPdTpCbHUsX36SSR4dnKlgW1nYGx~eED7dppA__&Key-Pair-Id=APKAI6TU7MMXM5DG6EPQ", + "ext": "opus", + "abr": 64, + "format_id": "hls_opus_64", + "protocol": "m3u8_native", + "preference": null, + "vcodec": "none" + } + ], + "original_url": "https://soundcloud.com/bruttoband/brutto-11", + "webpage_url_basename": "brutto-11", + "webpage_url_domain": "soundcloud.com", + "extractor": "soundcloud", + "extractor_key": "Soundcloud", + "heatmap": [], + "automatic_captions": {} +} diff --git a/tests/components/media_extractor/fixtures/youtube_1.json b/tests/components/media_extractor/fixtures/youtube_1.json new file mode 100644 index 00000000000..e1283274f63 --- /dev/null +++ b/tests/components/media_extractor/fixtures/youtube_1.json @@ -0,0 +1,1430 @@ +{ + "id": "dQw4w9WgXcQ", + "title": "Rick Astley - Never Gonna Give You Up (Official Music Video)", + "formats": [ + { + "asr": null, + "filesize": 80166145, + "format_id": "137", + "format_note": "1080p", + "source_preference": -1, + "fps": 25, + "audio_channels": null, + "height": 1080, + "quality": 9.0, + "has_drm": false, + "tbr": 3024.566, + "url": "https://rr3---sn-5hne6nzy.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZZamLdK-6dsP1vWlkAY&ip=45.93.75.130&id=o-AE88cX3U8S0nqUyaTcInhPRdfxWwxMu3bolA1C1aEj2f&itag=137&source=youtube&requiressl=yes&mh=7c&mm=31%2C26&mn=sn-5hne6nzy%2Csn-25ge7nz6&ms=au%2Conr&mv=m&mvi=3&pl=22&initcwndbps=1311250&vprv=1&svpuc=1&mime=video%2Fmp4&gir=yes&clen=80166145&dur=212.040&lmt=1694045208995966&mt=1695502401&fvip=1&keepalive=yes&fexp=24007246&c=IOS&txp=4535434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRgIhAJvqkzrOZjbqQLPANU-Q0Ti57XZCS5MLEZMrme2Vqqj2AiEAoU5oDVbWI-82LxhSDuTtTvpgKEspgfrw7aPzQ8Di40w%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAODiH5vGneSVUexV9_V6nZzlqakYIjA64qDFHDB-fYgKAiEA0gnzEhZwqeUUXjDukB4wIJXS3evZuLV7Dtp5cpZZXUA%3D", + "width": 1920, + "language": null, + "language_preference": -1, + "preference": null, + "ext": "mp4", + "vcodec": "avc1.640028", + "acodec": "none", + "dynamic_range": null, + "container": "mp4_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": null, + "filesize": 40874930, + "format_id": "248", + "format_note": "1080p", + "source_preference": -1, + "fps": 25, + "audio_channels": null, + "height": 1080, + "quality": 9.0, + "has_drm": false, + "tbr": 1542.159, + "url": "https://rr3---sn-5hne6nzy.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZZamLdK-6dsP1vWlkAY&ip=45.93.75.130&id=o-AE88cX3U8S0nqUyaTcInhPRdfxWwxMu3bolA1C1aEj2f&itag=248&source=youtube&requiressl=yes&mh=7c&mm=31%2C26&mn=sn-5hne6nzy%2Csn-25ge7nz6&ms=au%2Conr&mv=m&mvi=3&pl=22&initcwndbps=1311250&vprv=1&svpuc=1&mime=video%2Fwebm&gir=yes&clen=40874930&dur=212.040&lmt=1694044655610179&mt=1695502401&fvip=1&keepalive=yes&fexp=24007246&c=IOS&txp=4535434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIgM6ztX8BqXVEkyq4FTukRfb0mlWfDdll8wN_8iZvFoDMCIQDvRawrloLUqZWDjgf2ZZKkQPPX2NZQm5mUcIHjX04bWA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAODiH5vGneSVUexV9_V6nZzlqakYIjA64qDFHDB-fYgKAiEA0gnzEhZwqeUUXjDukB4wIJXS3evZuLV7Dtp5cpZZXUA%3D", + "width": 1920, + "language": null, + "language_preference": -1, + "preference": null, + "ext": "webm", + "vcodec": "vp09.00.40.08", + "acodec": "none", + "dynamic_range": null, + "container": "webm_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": null, + "filesize": 16780212, + "format_id": "136", + "format_note": "720p", + "source_preference": -1, + "fps": 25, + "audio_channels": null, + "height": 720, + "quality": 8.0, + "has_drm": false, + "tbr": 633.096, + "url": "https://rr3---sn-5hne6nzy.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZZamLdK-6dsP1vWlkAY&ip=45.93.75.130&id=o-AE88cX3U8S0nqUyaTcInhPRdfxWwxMu3bolA1C1aEj2f&itag=136&source=youtube&requiressl=yes&mh=7c&mm=31%2C26&mn=sn-5hne6nzy%2Csn-25ge7nz6&ms=au%2Conr&mv=m&mvi=3&pl=22&initcwndbps=1311250&vprv=1&svpuc=1&mime=video%2Fmp4&gir=yes&clen=16780212&dur=212.040&lmt=1694045071129751&mt=1695502401&fvip=1&keepalive=yes&fexp=24007246&c=IOS&txp=4535434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIgG69VUvtxkC7_DuWzobsIDSBoAq9K8NfzCDI1BRkqC4ICIQD-G-4SOmZuQKmSkka0p8USe-GX_RzmuxsNPZj89r-9WA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAODiH5vGneSVUexV9_V6nZzlqakYIjA64qDFHDB-fYgKAiEA0gnzEhZwqeUUXjDukB4wIJXS3evZuLV7Dtp5cpZZXUA%3D", + "width": 1280, + "language": null, + "language_preference": -1, + "preference": null, + "ext": "mp4", + "vcodec": "avc1.4D401F", + "acodec": "none", + "dynamic_range": null, + "container": "mp4_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": null, + "filesize": 15359727, + "format_id": "247", + "format_note": "720p", + "source_preference": -1, + "fps": 25, + "audio_channels": null, + "height": 720, + "quality": 8.0, + "has_drm": false, + "tbr": 579.502, + "url": "https://rr3---sn-5hne6nzy.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZZamLdK-6dsP1vWlkAY&ip=45.93.75.130&id=o-AE88cX3U8S0nqUyaTcInhPRdfxWwxMu3bolA1C1aEj2f&itag=247&source=youtube&requiressl=yes&mh=7c&mm=31%2C26&mn=sn-5hne6nzy%2Csn-25ge7nz6&ms=au%2Conr&mv=m&mvi=3&pl=22&initcwndbps=1311250&vprv=1&svpuc=1&mime=video%2Fwebm&gir=yes&clen=15359727&dur=212.040&lmt=1694043486219683&mt=1695502401&fvip=1&keepalive=yes&fexp=24007246&c=IOS&txp=4535434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIgclGPS3eSxjSMzpc-gOTA8Vsr4yfK6UCVyG5LUot-jMkCIQCdkrhr5s2GjZH5i8d_WciMXSN6kjqG9A6BCMzxqpeuRw%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAODiH5vGneSVUexV9_V6nZzlqakYIjA64qDFHDB-fYgKAiEA0gnzEhZwqeUUXjDukB4wIJXS3evZuLV7Dtp5cpZZXUA%3D", + "width": 1280, + "language": null, + "language_preference": -1, + "preference": null, + "ext": "webm", + "vcodec": "vp09.00.31.08", + "acodec": "none", + "dynamic_range": null, + "container": "webm_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": null, + "filesize": 8683274, + "format_id": "135", + "format_note": "480p", + "source_preference": -1, + "fps": 25, + "audio_channels": null, + "height": 480, + "quality": 7.0, + "has_drm": false, + "tbr": 327.608, + "url": "https://rr3---sn-5hne6nzy.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZZamLdK-6dsP1vWlkAY&ip=45.93.75.130&id=o-AE88cX3U8S0nqUyaTcInhPRdfxWwxMu3bolA1C1aEj2f&itag=135&source=youtube&requiressl=yes&mh=7c&mm=31%2C26&mn=sn-5hne6nzy%2Csn-25ge7nz6&ms=au%2Conr&mv=m&mvi=3&pl=22&initcwndbps=1311250&vprv=1&svpuc=1&mime=video%2Fmp4&gir=yes&clen=8683274&dur=212.040&lmt=1694045045723793&mt=1695502401&fvip=1&keepalive=yes&fexp=24007246&c=IOS&txp=4535434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRgIhANvnqVNT-trAFyACiWh8EllyhTzAuStHpLlDrTan7LxXAiEAy_Yajm6EEJUwcAVnEBRukcxc5-CB8UTY5BjB9oR1TeM%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAODiH5vGneSVUexV9_V6nZzlqakYIjA64qDFHDB-fYgKAiEA0gnzEhZwqeUUXjDukB4wIJXS3evZuLV7Dtp5cpZZXUA%3D", + "width": 854, + "language": null, + "language_preference": -1, + "preference": null, + "ext": "mp4", + "vcodec": "avc1.4D401E", + "acodec": "none", + "dynamic_range": null, + "container": "mp4_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": null, + "filesize": 10927666, + "format_id": "244", + "format_note": "480p", + "source_preference": -1, + "fps": 25, + "audio_channels": null, + "height": 480, + "quality": 7.0, + "has_drm": false, + "tbr": 412.286, + "url": "https://rr3---sn-5hne6nzy.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZZamLdK-6dsP1vWlkAY&ip=45.93.75.130&id=o-AE88cX3U8S0nqUyaTcInhPRdfxWwxMu3bolA1C1aEj2f&itag=244&source=youtube&requiressl=yes&mh=7c&mm=31%2C26&mn=sn-5hne6nzy%2Csn-25ge7nz6&ms=au%2Conr&mv=m&mvi=3&pl=22&initcwndbps=1311250&vprv=1&svpuc=1&mime=video%2Fwebm&gir=yes&clen=10927666&dur=212.040&lmt=1694043369037289&mt=1695502401&fvip=1&keepalive=yes&fexp=24007246&c=IOS&txp=4535434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIhAKBxnJIQLH7cWZFfZuMs5yhQ66jdt35KMdmqi5nmGIgnAiAzZ28nc8BNIKhKlhKBr5w6gWmvz-vm8E-PnNWigmhwgA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAODiH5vGneSVUexV9_V6nZzlqakYIjA64qDFHDB-fYgKAiEA0gnzEhZwqeUUXjDukB4wIJXS3evZuLV7Dtp5cpZZXUA%3D", + "width": 854, + "language": null, + "language_preference": -1, + "preference": null, + "ext": "webm", + "vcodec": "vp09.00.30.08", + "acodec": "none", + "dynamic_range": null, + "container": "webm_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": null, + "filesize": 5678772, + "format_id": "134", + "format_note": "360p", + "source_preference": -1, + "fps": 25, + "audio_channels": null, + "height": 360, + "quality": 6.0, + "has_drm": false, + "tbr": 214.252, + "url": "https://rr3---sn-5hne6nzy.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZZamLdK-6dsP1vWlkAY&ip=45.93.75.130&id=o-AE88cX3U8S0nqUyaTcInhPRdfxWwxMu3bolA1C1aEj2f&itag=134&source=youtube&requiressl=yes&mh=7c&mm=31%2C26&mn=sn-5hne6nzy%2Csn-25ge7nz6&ms=au%2Conr&mv=m&mvi=3&pl=22&initcwndbps=1311250&vprv=1&svpuc=1&mime=video%2Fmp4&gir=yes&clen=5678772&dur=212.040&lmt=1694045013473544&mt=1695502401&fvip=1&keepalive=yes&fexp=24007246&c=IOS&txp=4535434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRgIhAOHb4lBYlXkxFj1ZMXhNDcw1CQPWiB2c6Y6vOTGevdqJAiEAt644Dv84Eqzc6yfe1GG3sDMwYeLRUKA_KYHbSeJeKIo%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAODiH5vGneSVUexV9_V6nZzlqakYIjA64qDFHDB-fYgKAiEA0gnzEhZwqeUUXjDukB4wIJXS3evZuLV7Dtp5cpZZXUA%3D", + "width": 640, + "language": null, + "language_preference": -1, + "preference": null, + "ext": "mp4", + "vcodec": "avc1.4D401E", + "acodec": "none", + "dynamic_range": null, + "container": "mp4_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": null, + "filesize": 6902164, + "format_id": "243", + "format_note": "360p", + "source_preference": -1, + "fps": 25, + "audio_channels": null, + "height": 360, + "quality": 6.0, + "has_drm": false, + "tbr": 260.409, + "url": "https://rr3---sn-5hne6nzy.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZZamLdK-6dsP1vWlkAY&ip=45.93.75.130&id=o-AE88cX3U8S0nqUyaTcInhPRdfxWwxMu3bolA1C1aEj2f&itag=243&source=youtube&requiressl=yes&mh=7c&mm=31%2C26&mn=sn-5hne6nzy%2Csn-25ge7nz6&ms=au%2Conr&mv=m&mvi=3&pl=22&initcwndbps=1311250&vprv=1&svpuc=1&mime=video%2Fwebm&gir=yes&clen=6902164&dur=212.040&lmt=1694043349554753&mt=1695502401&fvip=1&keepalive=yes&fexp=24007246&c=IOS&txp=4535434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRgIhAM3xj4hJ22Ur3eTOxA0LselI9THQg1Qb2gryxihUmPFLAiEAuQYROAwdEs6XdFszg8SRgCgojRUr1y9VS3096aQXnjc%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAODiH5vGneSVUexV9_V6nZzlqakYIjA64qDFHDB-fYgKAiEA0gnzEhZwqeUUXjDukB4wIJXS3evZuLV7Dtp5cpZZXUA%3D", + "width": 640, + "language": null, + "language_preference": -1, + "preference": null, + "ext": "webm", + "vcodec": "vp09.00.21.08", + "acodec": "none", + "dynamic_range": null, + "container": "webm_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": null, + "filesize": 3019976, + "format_id": "133", + "format_note": "240p", + "source_preference": -1, + "fps": 25, + "audio_channels": null, + "height": 240, + "quality": 5.0, + "has_drm": false, + "tbr": 113.939, + "url": "https://rr3---sn-5hne6nzy.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZZamLdK-6dsP1vWlkAY&ip=45.93.75.130&id=o-AE88cX3U8S0nqUyaTcInhPRdfxWwxMu3bolA1C1aEj2f&itag=133&source=youtube&requiressl=yes&mh=7c&mm=31%2C26&mn=sn-5hne6nzy%2Csn-25ge7nz6&ms=au%2Conr&mv=m&mvi=3&pl=22&initcwndbps=1311250&vprv=1&svpuc=1&mime=video%2Fmp4&gir=yes&clen=3019976&dur=212.040&lmt=1694045014258984&mt=1695502401&fvip=1&keepalive=yes&fexp=24007246&c=IOS&txp=4535434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIgWmsCuyZEqz2NashfdLp92iJqaqRtA8bYJJhohjGFxzgCIQD3aQZ90zKGHu-JXiJZMViWuCb0UeZ-MesxOGi_gMWHxA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAODiH5vGneSVUexV9_V6nZzlqakYIjA64qDFHDB-fYgKAiEA0gnzEhZwqeUUXjDukB4wIJXS3evZuLV7Dtp5cpZZXUA%3D", + "width": 426, + "language": null, + "language_preference": -1, + "preference": null, + "ext": "mp4", + "vcodec": "avc1.4D4015", + "acodec": "none", + "dynamic_range": null, + "container": "mp4_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": null, + "filesize": 4021173, + "format_id": "242", + "format_note": "240p", + "source_preference": -1, + "fps": 25, + "audio_channels": null, + "height": 240, + "quality": 5.0, + "has_drm": false, + "tbr": 151.713, + "url": "https://rr3---sn-5hne6nzy.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZZamLdK-6dsP1vWlkAY&ip=45.93.75.130&id=o-AE88cX3U8S0nqUyaTcInhPRdfxWwxMu3bolA1C1aEj2f&itag=242&source=youtube&requiressl=yes&mh=7c&mm=31%2C26&mn=sn-5hne6nzy%2Csn-25ge7nz6&ms=au%2Conr&mv=m&mvi=3&pl=22&initcwndbps=1311250&vprv=1&svpuc=1&mime=video%2Fwebm&gir=yes&clen=4021173&dur=212.040&lmt=1694043379783601&mt=1695502401&fvip=1&keepalive=yes&fexp=24007246&c=IOS&txp=4535434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIgAVwANVM5s1vtgkPKId_2b9bw2d_Lhbvkvm2J2OJM-fUCIQDwcC5FLMxOF3g6nZq1vpf0d7dyKnp0plE1Niy3rZ6Cdg%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAODiH5vGneSVUexV9_V6nZzlqakYIjA64qDFHDB-fYgKAiEA0gnzEhZwqeUUXjDukB4wIJXS3evZuLV7Dtp5cpZZXUA%3D", + "width": 426, + "language": null, + "language_preference": -1, + "preference": null, + "ext": "webm", + "vcodec": "vp09.00.20.08", + "acodec": "none", + "dynamic_range": null, + "container": "webm_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": null, + "filesize": 1863601, + "format_id": "160", + "format_note": "144p", + "source_preference": -1, + "fps": 25, + "audio_channels": null, + "height": 144, + "quality": 0.0, + "has_drm": false, + "tbr": 70.311, + "url": "https://rr3---sn-5hne6nzy.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZZamLdK-6dsP1vWlkAY&ip=45.93.75.130&id=o-AE88cX3U8S0nqUyaTcInhPRdfxWwxMu3bolA1C1aEj2f&itag=160&source=youtube&requiressl=yes&mh=7c&mm=31%2C26&mn=sn-5hne6nzy%2Csn-25ge7nz6&ms=au%2Conr&mv=m&mvi=3&pl=22&initcwndbps=1311250&vprv=1&svpuc=1&mime=video%2Fmp4&gir=yes&clen=1863601&dur=212.040&lmt=1694045032286738&mt=1695502401&fvip=1&keepalive=yes&fexp=24007246&c=IOS&txp=4535434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIhAL_zAjcV7CL3hke-z49D6nQ7k5dCTVweXQdj4_cVHIc2AiB9bkIVgy7GYGFUGo36PYjnlN_8KNnyxiNhh0M76Fjjgw%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAODiH5vGneSVUexV9_V6nZzlqakYIjA64qDFHDB-fYgKAiEA0gnzEhZwqeUUXjDukB4wIJXS3evZuLV7Dtp5cpZZXUA%3D", + "width": 256, + "language": null, + "language_preference": -1, + "preference": null, + "ext": "mp4", + "vcodec": "avc1.4D400C", + "acodec": "none", + "dynamic_range": null, + "container": "mp4_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": null, + "filesize": 2404581, + "format_id": "278", + "format_note": "144p", + "source_preference": -1, + "fps": 25, + "audio_channels": null, + "height": 144, + "quality": 0.0, + "has_drm": false, + "tbr": 90.721, + "url": "https://rr3---sn-5hne6nzy.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZZamLdK-6dsP1vWlkAY&ip=45.93.75.130&id=o-AE88cX3U8S0nqUyaTcInhPRdfxWwxMu3bolA1C1aEj2f&itag=278&source=youtube&requiressl=yes&mh=7c&mm=31%2C26&mn=sn-5hne6nzy%2Csn-25ge7nz6&ms=au%2Conr&mv=m&mvi=3&pl=22&initcwndbps=1311250&vprv=1&svpuc=1&mime=video%2Fwebm&gir=yes&clen=2404581&dur=212.040&lmt=1694043382822868&mt=1695502401&fvip=1&keepalive=yes&fexp=24007246&c=IOS&txp=4535434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRgIhAOetHAZolpx87k21SDKePQP6gZHC3CWiQ_DtEQd1bDRvAiEA8GGA-2C5lIFuucuPqdnS4FZiGdKYgWUTlJ-9yQEnSR0%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAODiH5vGneSVUexV9_V6nZzlqakYIjA64qDFHDB-fYgKAiEA0gnzEhZwqeUUXjDukB4wIJXS3evZuLV7Dtp5cpZZXUA%3D", + "width": 256, + "language": null, + "language_preference": -1, + "preference": null, + "ext": "webm", + "vcodec": "vp09.00.11.08", + "acodec": "none", + "dynamic_range": null, + "container": "webm_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": 22050, + "filesize": 1294944, + "format_id": "139", + "format_note": "low", + "source_preference": -1, + "fps": null, + "audio_channels": 2, + "height": null, + "quality": 2.0, + "has_drm": false, + "tbr": 48.823, + "url": "https://rr3---sn-5hne6nzy.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZZamLdK-6dsP1vWlkAY&ip=45.93.75.130&id=o-AE88cX3U8S0nqUyaTcInhPRdfxWwxMu3bolA1C1aEj2f&itag=139&source=youtube&requiressl=yes&mh=7c&mm=31%2C26&mn=sn-5hne6nzy%2Csn-25ge7nz6&ms=au%2Conr&mv=m&mvi=3&pl=22&initcwndbps=1311250&vprv=1&svpuc=1&mime=audio%2Fmp4&gir=yes&clen=1294944&dur=212.183&lmt=1694042119353699&mt=1695502401&fvip=1&keepalive=yes&fexp=24007246&c=IOS&txp=4532434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIhAMKE8c3kivZrqSOCOcLzUCa1erqAaOj6K7SWFAcCJyCXAiBOFkaL_lvsXhZeLwyOUP98LBTGxUHEurO_IWZOeRCkAQ%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAODiH5vGneSVUexV9_V6nZzlqakYIjA64qDFHDB-fYgKAiEA0gnzEhZwqeUUXjDukB4wIJXS3evZuLV7Dtp5cpZZXUA%3D", + "width": null, + "language": "en", + "language_preference": -1, + "preference": null, + "ext": "m4a", + "vcodec": "none", + "acodec": "mp4a.40.5", + "dynamic_range": null, + "container": "m4a_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": 44100, + "filesize": 3433514, + "format_id": "140", + "format_note": "medium", + "source_preference": -1, + "fps": null, + "audio_channels": 2, + "height": null, + "quality": 3.0, + "has_drm": false, + "tbr": 129.51, + "url": "https://rr3---sn-5hne6nzy.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZZamLdK-6dsP1vWlkAY&ip=45.93.75.130&id=o-AE88cX3U8S0nqUyaTcInhPRdfxWwxMu3bolA1C1aEj2f&itag=140&source=youtube&requiressl=yes&mh=7c&mm=31%2C26&mn=sn-5hne6nzy%2Csn-25ge7nz6&ms=au%2Conr&mv=m&mvi=3&pl=22&initcwndbps=1311250&vprv=1&svpuc=1&mime=audio%2Fmp4&gir=yes&clen=3433514&dur=212.091&lmt=1694042124987733&mt=1695502401&fvip=1&keepalive=yes&fexp=24007246&c=IOS&txp=4532434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIhAKw2jXdoZhn8sjUu-1MSxfV1p0TzRyqjuooRwoQohtOwAiANfeuxDTlTpi_f9scAC0n01xOejhRLD0m-6pl3oo7wIQ%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAODiH5vGneSVUexV9_V6nZzlqakYIjA64qDFHDB-fYgKAiEA0gnzEhZwqeUUXjDukB4wIJXS3evZuLV7Dtp5cpZZXUA%3D", + "width": null, + "language": "en", + "language_preference": -1, + "preference": null, + "ext": "m4a", + "vcodec": "none", + "acodec": "mp4a.40.2", + "dynamic_range": null, + "container": "m4a_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": 22050, + "filesize": 2086732, + "format_id": "17", + "format_note": "144p", + "source_preference": -1, + "fps": 6, + "audio_channels": 1, + "height": 144, + "quality": 0.0, + "has_drm": false, + "tbr": 78.693, + "url": "https://rr2---sn-5hnekn7k.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZeSINMKn6dsPt6mr8Ac&ip=45.93.75.130&id=o-AA7xSUDqvzXPAn6m_Jd8d0jhyk4er7c5zjbyZCHyREaQ&itag=17&source=youtube&requiressl=yes&mh=7c&mm=31%2C29&mn=sn-5hnekn7k%2Csn-5hne6nzy&ms=au%2Crdu&mv=m&mvi=2&pl=22&initcwndbps=1311250&spc=UWF9f6JvGMIlJJFOWUL6L4bH2pl28C4&vprv=1&svpuc=1&mime=video%2F3gpp&gir=yes&clen=2086732&dur=212.137&lmt=1694042486266781&mt=1695502401&fvip=3&fexp=24007246&beids=24350018&c=ANDROID&txp=4532434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRgIhAIOMPqxbtJwYRIrAYmr0I9rEovBipWNTTg9AMju1ehECAiEA7vjnz-TCwh2zQQm4vmXW0nGpft4nX42Ql_hwHHCA-Yk%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhANJRDHqeYwWk4W8XXGlTNmYA3OOiuzK3PEWtVpK4NimlAiEAwK0nj8vGtcP0IJMZVBGh8SC5KRFzPfx_yJn41ZOUGyQ%3D", + "width": 176, + "language": "en", + "language_preference": -1, + "preference": -2, + "ext": "3gp", + "vcodec": "mp4v.20.3", + "acodec": "mp4a.40.2", + "dynamic_range": null, + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": 44100, + "filesize": null, + "format_id": "18", + "format_note": "360p", + "source_preference": -1, + "fps": 25, + "audio_channels": 2, + "height": 360, + "quality": 6.0, + "has_drm": false, + "tbr": 343.32, + "url": "https://rr2---sn-5hnekn7k.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZeSINMKn6dsPt6mr8Ac&ip=45.93.75.130&id=o-AA7xSUDqvzXPAn6m_Jd8d0jhyk4er7c5zjbyZCHyREaQ&itag=18&source=youtube&requiressl=yes&mh=7c&mm=31%2C29&mn=sn-5hnekn7k%2Csn-5hne6nzy&ms=au%2Crdu&mv=m&mvi=2&pl=22&initcwndbps=1311250&spc=UWF9f6JvGMIlJJFOWUL6L4bH2pl28C4&vprv=1&svpuc=1&mime=video%2Fmp4&cnr=14&ratebypass=yes&dur=212.091&lmt=1694045104514388&mt=1695502401&fvip=3&fexp=24007246&beids=24350018&c=ANDROID&txp=4538434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cmime%2Ccnr%2Cratebypass%2Cdur%2Clmt&sig=AOq0QJ8wRAIgFhTg7TavCE2HBWS9I3agqj3CG2RqrvxLJt6JgHtN4O4CIF-IDHhEPLlkGP2QtwJ19sSumUVPqVElVXjrM-qqYIae&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhANJRDHqeYwWk4W8XXGlTNmYA3OOiuzK3PEWtVpK4NimlAiEAwK0nj8vGtcP0IJMZVBGh8SC5KRFzPfx_yJn41ZOUGyQ%3D", + "width": 640, + "language": "en", + "language_preference": -1, + "preference": null, + "ext": "mp4", + "vcodec": "avc1.42001E", + "acodec": "mp4a.40.2", + "dynamic_range": null, + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": 44100, + "filesize": null, + "format_id": "22", + "format_note": "720p", + "source_preference": -5, + "fps": 25, + "audio_channels": 2, + "height": 720, + "quality": 8.0, + "has_drm": false, + "tbr": 762.182, + "url": "https://rr2---sn-5hnekn7k.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZeSINMKn6dsPt6mr8Ac&ip=45.93.75.130&id=o-AA7xSUDqvzXPAn6m_Jd8d0jhyk4er7c5zjbyZCHyREaQ&itag=22&source=youtube&requiressl=yes&mh=7c&mm=31%2C29&mn=sn-5hnekn7k%2Csn-5hne6nzy&ms=au%2Crdu&mv=m&mvi=2&pl=22&initcwndbps=1311250&spc=UWF9f6JvGMIlJJFOWUL6L4bH2pl28C4&vprv=1&svpuc=1&mime=video%2Fmp4&cnr=14&ratebypass=yes&dur=212.091&lmt=1694045086815467&mt=1695502401&fvip=3&fexp=24007246&beids=24350018&c=ANDROID&txp=4532434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cmime%2Ccnr%2Cratebypass%2Cdur%2Clmt&sig=AOq0QJ8wRAIgHVwajP0J2fiJ1ERoAonpxghXGzDmEXh3rvJ399UEMWECIFdBjiVUOk7QdiFBxQ4QqojJd8p_PfL25TV_8TBrp_Kb&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhANJRDHqeYwWk4W8XXGlTNmYA3OOiuzK3PEWtVpK4NimlAiEAwK0nj8vGtcP0IJMZVBGh8SC5KRFzPfx_yJn41ZOUGyQ%3D", + "width": 1280, + "language": "en", + "language_preference": -1, + "preference": null, + "ext": "mp4", + "vcodec": "avc1.64001F", + "acodec": "mp4a.40.2", + "dynamic_range": null, + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": null, + "filesize": 31265835, + "format_id": "399", + "format_note": "1080p", + "source_preference": -1, + "fps": 25, + "audio_channels": null, + "height": 1080, + "quality": 9.0, + "has_drm": false, + "tbr": 1179.62, + "url": "https://rr2---sn-5hnekn7k.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZeSINMKn6dsPt6mr8Ac&ip=45.93.75.130&id=o-AA7xSUDqvzXPAn6m_Jd8d0jhyk4er7c5zjbyZCHyREaQ&itag=399&source=youtube&requiressl=yes&mh=7c&mm=31%2C29&mn=sn-5hnekn7k%2Csn-5hne6nzy&ms=au%2Crdu&mv=m&mvi=2&pl=22&initcwndbps=1311250&spc=UWF9f6JvGMIlJJFOWUL6L4bH2pl28C4&vprv=1&svpuc=1&mime=video%2Fmp4&gir=yes&clen=31265835&dur=212.040&lmt=1694042163788395&mt=1695502401&fvip=3&keepalive=yes&fexp=24007246&beids=24350018&c=ANDROID&txp=4537434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRgIhAMbD0sObTPrf1j0GESI-SRztzhMi98xn1XBMfFsnMjLFAiEAnMCImljVChi4G_wjA9UE2EN9xQHJ7LhuEO9HeNlR334%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhANJRDHqeYwWk4W8XXGlTNmYA3OOiuzK3PEWtVpK4NimlAiEAwK0nj8vGtcP0IJMZVBGh8SC5KRFzPfx_yJn41ZOUGyQ%3D", + "width": 1920, + "language": null, + "language_preference": -1, + "preference": null, + "ext": "mp4", + "vcodec": "av01.0.08M.08", + "acodec": "none", + "dynamic_range": null, + "container": "mp4_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": null, + "filesize": 17466721, + "format_id": "398", + "format_note": "720p", + "source_preference": -1, + "fps": 25, + "audio_channels": null, + "height": 720, + "quality": 8.0, + "has_drm": false, + "tbr": 658.997, + "url": "https://rr2---sn-5hnekn7k.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZeSINMKn6dsPt6mr8Ac&ip=45.93.75.130&id=o-AA7xSUDqvzXPAn6m_Jd8d0jhyk4er7c5zjbyZCHyREaQ&itag=398&source=youtube&requiressl=yes&mh=7c&mm=31%2C29&mn=sn-5hnekn7k%2Csn-5hne6nzy&ms=au%2Crdu&mv=m&mvi=2&pl=22&initcwndbps=1311250&spc=UWF9f6JvGMIlJJFOWUL6L4bH2pl28C4&vprv=1&svpuc=1&mime=video%2Fmp4&gir=yes&clen=17466721&dur=212.040&lmt=1694042319819525&mt=1695502401&fvip=3&keepalive=yes&fexp=24007246&beids=24350018&c=ANDROID&txp=4537434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIgfJ1sWUmxG82ls65giBWKTwsH7UP4ItT0soOPZSEtKg4CIQC_GFYhkfiktXrWOoKWW2j50GkQX7dE7mfWzvjh-XnIXA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhANJRDHqeYwWk4W8XXGlTNmYA3OOiuzK3PEWtVpK4NimlAiEAwK0nj8vGtcP0IJMZVBGh8SC5KRFzPfx_yJn41ZOUGyQ%3D", + "width": 1280, + "language": null, + "language_preference": -1, + "preference": null, + "ext": "mp4", + "vcodec": "av01.0.05M.08", + "acodec": "none", + "dynamic_range": null, + "container": "mp4_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": null, + "filesize": 9434981, + "format_id": "397", + "format_note": "480p", + "source_preference": -1, + "fps": 25, + "audio_channels": null, + "height": 480, + "quality": 7.0, + "has_drm": false, + "tbr": 355.969, + "url": "https://rr2---sn-5hnekn7k.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZeSINMKn6dsPt6mr8Ac&ip=45.93.75.130&id=o-AA7xSUDqvzXPAn6m_Jd8d0jhyk4er7c5zjbyZCHyREaQ&itag=397&source=youtube&requiressl=yes&mh=7c&mm=31%2C29&mn=sn-5hnekn7k%2Csn-5hne6nzy&ms=au%2Crdu&mv=m&mvi=2&pl=22&initcwndbps=1311250&spc=UWF9f6JvGMIlJJFOWUL6L4bH2pl28C4&vprv=1&svpuc=1&mime=video%2Fmp4&gir=yes&clen=9434981&dur=212.040&lmt=1694042458043976&mt=1695502401&fvip=3&keepalive=yes&fexp=24007246&beids=24350018&c=ANDROID&txp=4537434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRAIgFFbe3pmUWAFqJCycmW7hGbeJuC8dfEax2p6v9J-y9GQCIB-2d1ss2yBL3yhesngue7dM5AsJqLNOMLnCD51o-1zW&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhANJRDHqeYwWk4W8XXGlTNmYA3OOiuzK3PEWtVpK4NimlAiEAwK0nj8vGtcP0IJMZVBGh8SC5KRFzPfx_yJn41ZOUGyQ%3D", + "width": 854, + "language": null, + "language_preference": -1, + "preference": null, + "ext": "mp4", + "vcodec": "av01.0.04M.08", + "acodec": "none", + "dynamic_range": null, + "container": "mp4_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": null, + "filesize": 5438397, + "format_id": "396", + "format_note": "360p", + "source_preference": -1, + "fps": 25, + "audio_channels": null, + "height": 360, + "quality": 6.0, + "has_drm": false, + "tbr": 205.183, + "url": "https://rr2---sn-5hnekn7k.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZeSINMKn6dsPt6mr8Ac&ip=45.93.75.130&id=o-AA7xSUDqvzXPAn6m_Jd8d0jhyk4er7c5zjbyZCHyREaQ&itag=396&source=youtube&requiressl=yes&mh=7c&mm=31%2C29&mn=sn-5hnekn7k%2Csn-5hne6nzy&ms=au%2Crdu&mv=m&mvi=2&pl=22&initcwndbps=1311250&spc=UWF9f6JvGMIlJJFOWUL6L4bH2pl28C4&vprv=1&svpuc=1&mime=video%2Fmp4&gir=yes&clen=5438397&dur=212.040&lmt=1694042190822892&mt=1695502401&fvip=3&keepalive=yes&fexp=24007246&beids=24350018&c=ANDROID&txp=4537434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRAIgNr6BZ03kG8p2KTAv7gOZ01pEcCWwnkWvjOdxmGn1X4ACICrqnbbGqLvm0jpEqXYOXMISHcPt7vQVzwohM84tfeYb&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhANJRDHqeYwWk4W8XXGlTNmYA3OOiuzK3PEWtVpK4NimlAiEAwK0nj8vGtcP0IJMZVBGh8SC5KRFzPfx_yJn41ZOUGyQ%3D", + "width": 640, + "language": null, + "language_preference": -1, + "preference": null, + "ext": "mp4", + "vcodec": "av01.0.01M.08", + "acodec": "none", + "dynamic_range": null, + "container": "mp4_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": null, + "filesize": 3024455, + "format_id": "395", + "format_note": "240p", + "source_preference": -1, + "fps": 25, + "audio_channels": null, + "height": 240, + "quality": 5.0, + "has_drm": false, + "tbr": 114.108, + "url": "https://rr2---sn-5hnekn7k.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZeSINMKn6dsPt6mr8Ac&ip=45.93.75.130&id=o-AA7xSUDqvzXPAn6m_Jd8d0jhyk4er7c5zjbyZCHyREaQ&itag=395&source=youtube&requiressl=yes&mh=7c&mm=31%2C29&mn=sn-5hnekn7k%2Csn-5hne6nzy&ms=au%2Crdu&mv=m&mvi=2&pl=22&initcwndbps=1311250&spc=UWF9f6JvGMIlJJFOWUL6L4bH2pl28C4&vprv=1&svpuc=1&mime=video%2Fmp4&gir=yes&clen=3024455&dur=212.040&lmt=1694042297309821&mt=1695502401&fvip=3&keepalive=yes&fexp=24007246&beids=24350018&c=ANDROID&txp=4537434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIhAM7Up-h9A3SMwIYMo6V5t4oM7BpkjnEIcO_s7BTR1hfzAiA72-vEcn4y21NtpQkzpTZI_BdjCeCUez43ohuzw4MJsA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhANJRDHqeYwWk4W8XXGlTNmYA3OOiuzK3PEWtVpK4NimlAiEAwK0nj8vGtcP0IJMZVBGh8SC5KRFzPfx_yJn41ZOUGyQ%3D", + "width": 426, + "language": null, + "language_preference": -1, + "preference": null, + "ext": "mp4", + "vcodec": "av01.0.00M.08", + "acodec": "none", + "dynamic_range": null, + "container": "mp4_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": null, + "filesize": 1416915, + "format_id": "394", + "format_note": "144p", + "source_preference": -1, + "fps": 25, + "audio_channels": null, + "height": 144, + "quality": 0.0, + "has_drm": false, + "tbr": 53.458, + "url": "https://rr2---sn-5hnekn7k.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZeSINMKn6dsPt6mr8Ac&ip=45.93.75.130&id=o-AA7xSUDqvzXPAn6m_Jd8d0jhyk4er7c5zjbyZCHyREaQ&itag=394&source=youtube&requiressl=yes&mh=7c&mm=31%2C29&mn=sn-5hnekn7k%2Csn-5hne6nzy&ms=au%2Crdu&mv=m&mvi=2&pl=22&initcwndbps=1311250&spc=UWF9f6JvGMIlJJFOWUL6L4bH2pl28C4&vprv=1&svpuc=1&mime=video%2Fmp4&gir=yes&clen=1416915&dur=212.040&lmt=1694042192787352&mt=1695502401&fvip=3&keepalive=yes&fexp=24007246&beids=24350018&c=ANDROID&txp=4537434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIgLYhy9j4feWSuTyTnJr2MF4xiEpALLDeez2_BwF__Qq8CIQDBTg9R-8YOcUtA4-R-Gu8A7o_66wGf69Vky62ZE-T0Zw%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhANJRDHqeYwWk4W8XXGlTNmYA3OOiuzK3PEWtVpK4NimlAiEAwK0nj8vGtcP0IJMZVBGh8SC5KRFzPfx_yJn41ZOUGyQ%3D", + "width": 256, + "language": null, + "language_preference": -1, + "preference": null, + "ext": "mp4", + "vcodec": "av01.0.00M.08", + "acodec": "none", + "dynamic_range": null, + "container": "mp4_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": null, + "filesize": 847252, + "format_id": "597", + "format_note": "144p", + "source_preference": -1, + "fps": 13, + "audio_channels": null, + "height": 144, + "quality": 0.0, + "has_drm": false, + "tbr": 31.959, + "url": "https://rr2---sn-5hnekn7k.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZeSINMKn6dsPt6mr8Ac&ip=45.93.75.130&id=o-AA7xSUDqvzXPAn6m_Jd8d0jhyk4er7c5zjbyZCHyREaQ&itag=597&source=youtube&requiressl=yes&mh=7c&mm=31%2C29&mn=sn-5hnekn7k%2Csn-5hne6nzy&ms=au%2Crdu&mv=m&mvi=2&pl=22&initcwndbps=1311250&spc=UWF9f6JvGMIlJJFOWUL6L4bH2pl28C4&vprv=1&svpuc=1&mime=video%2Fmp4&gir=yes&clen=847252&dur=212.080&lmt=1694042194934376&mt=1695502401&fvip=3&keepalive=yes&fexp=24007246&beids=24350018&c=ANDROID&txp=4532434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIgH6Tk52MhzNtfm-6d1XHdQfIh12aqbEohhH-ffBZP9z0CIQDJwPFA7eTz6LdcZaBlfnlogft7pgtrXHvm6DIHWCODUg%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhANJRDHqeYwWk4W8XXGlTNmYA3OOiuzK3PEWtVpK4NimlAiEAwK0nj8vGtcP0IJMZVBGh8SC5KRFzPfx_yJn41ZOUGyQ%3D", + "width": 256, + "language": null, + "language_preference": -1, + "preference": null, + "ext": "mp4", + "vcodec": "avc1.4d400b", + "acodec": "none", + "dynamic_range": null, + "container": "mp4_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": null, + "filesize": 643306, + "format_id": "598", + "format_note": "144p", + "source_preference": -1, + "fps": 13, + "audio_channels": null, + "height": 144, + "quality": 0.0, + "has_drm": false, + "tbr": 24.266, + "url": "https://rr2---sn-5hnekn7k.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZeSINMKn6dsPt6mr8Ac&ip=45.93.75.130&id=o-AA7xSUDqvzXPAn6m_Jd8d0jhyk4er7c5zjbyZCHyREaQ&itag=598&source=youtube&requiressl=yes&mh=7c&mm=31%2C29&mn=sn-5hnekn7k%2Csn-5hne6nzy&ms=au%2Crdu&mv=m&mvi=2&pl=22&initcwndbps=1311250&spc=UWF9f6JvGMIlJJFOWUL6L4bH2pl28C4&vprv=1&svpuc=1&mime=video%2Fwebm&gir=yes&clen=643306&dur=212.080&lmt=1694042224218554&mt=1695502401&fvip=3&keepalive=yes&fexp=24007246&beids=24350018&c=ANDROID&txp=4532434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIgHwZWhUJuuFMMAva_Katkrgk3FGNcBlHCwBVwV1jGz4ACIQCerrScqjke9mtPVPwYZraaCp4u7VkFz1hIzx-Fl_7HzQ%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhANJRDHqeYwWk4W8XXGlTNmYA3OOiuzK3PEWtVpK4NimlAiEAwK0nj8vGtcP0IJMZVBGh8SC5KRFzPfx_yJn41ZOUGyQ%3D", + "width": 256, + "language": null, + "language_preference": -1, + "preference": null, + "ext": "webm", + "vcodec": "vp9", + "acodec": "none", + "dynamic_range": null, + "container": "webm_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": 48000, + "filesize": 1232413, + "format_id": "249", + "format_note": "low", + "source_preference": -1, + "fps": null, + "audio_channels": 2, + "height": null, + "quality": 2.0, + "has_drm": false, + "tbr": 46.492, + "url": "https://rr2---sn-5hnekn7k.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZeSINMKn6dsPt6mr8Ac&ip=45.93.75.130&id=o-AA7xSUDqvzXPAn6m_Jd8d0jhyk4er7c5zjbyZCHyREaQ&itag=249&source=youtube&requiressl=yes&mh=7c&mm=31%2C29&mn=sn-5hnekn7k%2Csn-5hne6nzy&ms=au%2Crdu&mv=m&mvi=2&pl=22&initcwndbps=1311250&spc=UWF9f6JvGMIlJJFOWUL6L4bH2pl28C4&vprv=1&svpuc=1&mime=audio%2Fwebm&gir=yes&clen=1232413&dur=212.061&lmt=1694040798737498&mt=1695502401&fvip=3&keepalive=yes&fexp=24007246&beids=24350018&c=ANDROID&txp=4532434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIgD3BhwkfqYjk1FEud7AdjzD9RJImYWaebeN9Ip7HuX3ICIQDCzT7tMYmDyb_fz4TB4GPwroXqO55NV5h7Ao-IoPq0Kw%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhANJRDHqeYwWk4W8XXGlTNmYA3OOiuzK3PEWtVpK4NimlAiEAwK0nj8vGtcP0IJMZVBGh8SC5KRFzPfx_yJn41ZOUGyQ%3D", + "width": null, + "language": "en", + "language_preference": -1, + "preference": null, + "ext": "webm", + "vcodec": "none", + "acodec": "opus", + "dynamic_range": null, + "container": "webm_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": 48000, + "filesize": 1630086, + "format_id": "250", + "format_note": "low", + "source_preference": -1, + "fps": null, + "audio_channels": 2, + "height": null, + "quality": 2.0, + "has_drm": false, + "tbr": 61.494, + "url": "https://rr2---sn-5hnekn7k.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZeSINMKn6dsPt6mr8Ac&ip=45.93.75.130&id=o-AA7xSUDqvzXPAn6m_Jd8d0jhyk4er7c5zjbyZCHyREaQ&itag=250&source=youtube&requiressl=yes&mh=7c&mm=31%2C29&mn=sn-5hnekn7k%2Csn-5hne6nzy&ms=au%2Crdu&mv=m&mvi=2&pl=22&initcwndbps=1311250&spc=UWF9f6JvGMIlJJFOWUL6L4bH2pl28C4&vprv=1&svpuc=1&mime=audio%2Fwebm&gir=yes&clen=1630086&dur=212.061&lmt=1694040798724510&mt=1695502401&fvip=3&keepalive=yes&fexp=24007246&beids=24350018&c=ANDROID&txp=4532434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIgJ1nALE0kFCC4pg2mj0LpB_ZwivihtQo6ugYw-AzKJAsCIQD5q8PFpJtloWUmuK2A80NC7c2hr_9OUldFCXOCLyPN3Q%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhANJRDHqeYwWk4W8XXGlTNmYA3OOiuzK3PEWtVpK4NimlAiEAwK0nj8vGtcP0IJMZVBGh8SC5KRFzPfx_yJn41ZOUGyQ%3D", + "width": null, + "language": "en", + "language_preference": -1, + "preference": null, + "ext": "webm", + "vcodec": "none", + "acodec": "opus", + "dynamic_range": null, + "container": "webm_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": 48000, + "filesize": 3437753, + "format_id": "251", + "format_note": "medium", + "source_preference": -1, + "fps": null, + "audio_channels": 2, + "height": null, + "quality": 3.0, + "has_drm": false, + "tbr": 129.689, + "url": "https://rr2---sn-5hnekn7k.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZeSINMKn6dsPt6mr8Ac&ip=45.93.75.130&id=o-AA7xSUDqvzXPAn6m_Jd8d0jhyk4er7c5zjbyZCHyREaQ&itag=251&source=youtube&requiressl=yes&mh=7c&mm=31%2C29&mn=sn-5hnekn7k%2Csn-5hne6nzy&ms=au%2Crdu&mv=m&mvi=2&pl=22&initcwndbps=1311250&spc=UWF9f6JvGMIlJJFOWUL6L4bH2pl28C4&vprv=1&svpuc=1&mime=audio%2Fwebm&gir=yes&clen=3437753&dur=212.061&lmt=1694040798752663&mt=1695502401&fvip=3&keepalive=yes&fexp=24007246&beids=24350018&c=ANDROID&txp=4532434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIhAPSOofxlLab49bVcmvVP8wmIHVWvqDyOd11oJdP1RPFfAiAOCKp1VodP12z6FqdWxp_2xYcS2J949BbgFWqlfGkHjA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhANJRDHqeYwWk4W8XXGlTNmYA3OOiuzK3PEWtVpK4NimlAiEAwK0nj8vGtcP0IJMZVBGh8SC5KRFzPfx_yJn41ZOUGyQ%3D", + "width": null, + "language": "en", + "language_preference": -1, + "preference": null, + "ext": "webm", + "vcodec": "none", + "acodec": "opus", + "dynamic_range": null, + "container": "webm_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": 22050, + "filesize": 817805, + "format_id": "599", + "format_note": "ultralow", + "source_preference": -1, + "fps": null, + "audio_channels": 2, + "height": null, + "quality": 1.0, + "has_drm": false, + "tbr": 30.833, + "url": "https://rr2---sn-5hnekn7k.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZeSINMKn6dsPt6mr8Ac&ip=45.93.75.130&id=o-AA7xSUDqvzXPAn6m_Jd8d0jhyk4er7c5zjbyZCHyREaQ&itag=599&source=youtube&requiressl=yes&mh=7c&mm=31%2C29&mn=sn-5hnekn7k%2Csn-5hne6nzy&ms=au%2Crdu&mv=m&mvi=2&pl=22&initcwndbps=1311250&spc=UWF9f6JvGMIlJJFOWUL6L4bH2pl28C4&vprv=1&svpuc=1&mime=audio%2Fmp4&gir=yes&clen=817805&dur=212.183&lmt=1694040788792847&mt=1695502401&fvip=3&keepalive=yes&fexp=24007246&beids=24350018&c=ANDROID&txp=4532434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRgIhAKEpb371lM9F_wlPf3i5D6zreL34as0UmzOJxw5TXqlrAiEAjNU124xhGmkotlkRSCWdF15IBB-frezyqNQY1AV-l4M%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhANJRDHqeYwWk4W8XXGlTNmYA3OOiuzK3PEWtVpK4NimlAiEAwK0nj8vGtcP0IJMZVBGh8SC5KRFzPfx_yJn41ZOUGyQ%3D", + "width": null, + "language": "en", + "language_preference": -1, + "preference": null, + "ext": "m4a", + "vcodec": "none", + "acodec": "mp4a.40.5", + "dynamic_range": null, + "container": "m4a_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "asr": 48000, + "filesize": 832823, + "format_id": "600", + "format_note": "ultralow", + "source_preference": -1, + "fps": null, + "audio_channels": 2, + "height": null, + "quality": 1.0, + "has_drm": false, + "tbr": 31.418, + "url": "https://rr2---sn-5hnekn7k.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZeSINMKn6dsPt6mr8Ac&ip=45.93.75.130&id=o-AA7xSUDqvzXPAn6m_Jd8d0jhyk4er7c5zjbyZCHyREaQ&itag=600&source=youtube&requiressl=yes&mh=7c&mm=31%2C29&mn=sn-5hnekn7k%2Csn-5hne6nzy&ms=au%2Crdu&mv=m&mvi=2&pl=22&initcwndbps=1311250&spc=UWF9f6JvGMIlJJFOWUL6L4bH2pl28C4&vprv=1&svpuc=1&mime=audio%2Fwebm&gir=yes&clen=832823&dur=212.061&lmt=1694040798740210&mt=1695502401&fvip=3&keepalive=yes&fexp=24007246&beids=24350018&c=ANDROID&txp=4532434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIgRqdkURprOblK7GurvCvDxSunECdm96nzQwzwHuDvUKcCIQCmeMMeDP816FMH0GgpugoQhe4z4X6nDYoY3PtQhShtWA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhANJRDHqeYwWk4W8XXGlTNmYA3OOiuzK3PEWtVpK4NimlAiEAwK0nj8vGtcP0IJMZVBGh8SC5KRFzPfx_yJn41ZOUGyQ%3D", + "width": null, + "language": "en", + "language_preference": -1, + "preference": null, + "ext": "webm", + "vcodec": "none", + "acodec": "opus", + "dynamic_range": null, + "container": "webm_dash", + "downloader_options": { + "http_chunk_size": 10485760 + } + }, + { + "format_id": "233", + "format_note": "Default", + "format_index": null, + "url": "https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/itag/233/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/goi/133/sgoap/clen%3D1294944%3Bdur%3D212.183%3Bgir%3Dyes%3Bitag%3D139%3Blmt%3D1694042119353699/hls_chunk_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31,26/mn/sn-5hne6nzy,sn-25ge7nz6/ms/au,onr/mv/m/mvi/3/pl/22/initcwndbps/1311250/vprv/1/playlist_type/DVR/dover/13/txp/4532434/mt/1695502401/fvip/1/short_key/1/keepalive/yes/fexp/24007246/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,goi,sgoap,vprv,playlist_type/sig/AOq0QJ8wRgIhANECzBL2bCCpseaXL3qapc_gEQkpP-2eTqyPCspG44PXAiEA4J6i9mS_2vJB4TtIwHjg7-f-KCyiYSs-kL5dcEkToRg%3D/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AG3C_xAwRAIgUS67qZ67NkB4J8491hAMcCKs400sACGWUhNcrXWefx4CIB64Ny0g5mTEFnTryntu2vexyLjfXtNHmvEAYgosB_9w/playlist/index.m3u8", + "manifest_url": "https://manifest.googlevideo.com/api/manifest/hls_variant/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/source/youtube/requiressl/yes/playback_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31%2C26/mn/sn-5hne6nzy%2Csn-25ge7nz6/ms/au%2Conr/mv/m/mvi/3/pl/22/hfr/1/demuxed/1/tts_caps/1/maudio/1/initcwndbps/1311250/vprv/1/go/1/mt/1695502401/fvip/1/nvgoi/1/short_key/1/ncsapi/1/keepalive/yes/fexp/24007246/dover/13/itag/0/playlist_type/DVR/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cdemuxed%2Ctts_caps%2Cmaudio%2Cvprv%2Cgo%2Citag%2Cplaylist_type/sig/AOq0QJ8wRAIgVqAL59tazORSvoQGDDOnSykYFq5uZpRuPHMC9uFJi-8CIB35p6PNovYkmOKgp4hgRrNprxu-TvLYlHdP4MsxNJgt/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgYBcJvvjykFc2vD1MGR4R95tcg4uoXutvWjGr5CDO7XoCIQDKUqdUFDFde44FsyWMfj2qVOJp_GcX1UGoyQ847aMpVg%3D%3D/file/index.m3u8", + "language": "en", + "ext": "mp4", + "protocol": "m3u8_native", + "preference": null, + "quality": -1, + "has_drm": false, + "vcodec": "none", + "source_preference": -1 + }, + { + "format_id": "234", + "format_note": "Default", + "format_index": null, + "url": "https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/itag/234/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/goi/133/sgoap/clen%3D3433514%3Bdur%3D212.091%3Bgir%3Dyes%3Bitag%3D140%3Blmt%3D1694042124987733/hls_chunk_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31,26/mn/sn-5hne6nzy,sn-25ge7nz6/ms/au,onr/mv/m/mvi/3/pl/22/initcwndbps/1311250/vprv/1/playlist_type/DVR/dover/13/txp/4532434/mt/1695502401/fvip/1/short_key/1/keepalive/yes/fexp/24007246/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,goi,sgoap,vprv,playlist_type/sig/AOq0QJ8wRgIhAM_Q8sGDeFsxPWBEUPdlx6Mul7XMV1uCmH_5jdrqR06cAiEAqpNga8OlFdW_uqNwYxIL70Ki64lw0hKT700Z8dZPTtA%3D/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AG3C_xAwRQIgM8VqbzvtcXd5PhUv9cffclj4q86NQYiKEJw9CuwxlbYCIQDntLRgkHqq_HrUvzSiwWLph8lvrnAgSps0aAilpdDKfg%3D%3D/playlist/index.m3u8", + "manifest_url": "https://manifest.googlevideo.com/api/manifest/hls_variant/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/source/youtube/requiressl/yes/playback_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31%2C26/mn/sn-5hne6nzy%2Csn-25ge7nz6/ms/au%2Conr/mv/m/mvi/3/pl/22/hfr/1/demuxed/1/tts_caps/1/maudio/1/initcwndbps/1311250/vprv/1/go/1/mt/1695502401/fvip/1/nvgoi/1/short_key/1/ncsapi/1/keepalive/yes/fexp/24007246/dover/13/itag/0/playlist_type/DVR/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cdemuxed%2Ctts_caps%2Cmaudio%2Cvprv%2Cgo%2Citag%2Cplaylist_type/sig/AOq0QJ8wRAIgVqAL59tazORSvoQGDDOnSykYFq5uZpRuPHMC9uFJi-8CIB35p6PNovYkmOKgp4hgRrNprxu-TvLYlHdP4MsxNJgt/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgYBcJvvjykFc2vD1MGR4R95tcg4uoXutvWjGr5CDO7XoCIQDKUqdUFDFde44FsyWMfj2qVOJp_GcX1UGoyQ847aMpVg%3D%3D/file/index.m3u8", + "language": "en", + "ext": "mp4", + "protocol": "m3u8_native", + "preference": null, + "quality": -1, + "has_drm": false, + "vcodec": "none", + "source_preference": -1 + }, + { + "format_id": "229", + "format_index": null, + "url": "https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/itag/229/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/sgovp/clen%3D3019976%3Bdur%3D212.040%3Bgir%3Dyes%3Bitag%3D133%3Blmt%3D1694045014258984/hls_chunk_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31,26/mn/sn-5hne6nzy,sn-25ge7nz6/ms/au,onr/mv/m/mvi/3/pl/22/initcwndbps/1311250/vprv/1/playlist_type/DVR/dover/13/txp/4535434/mt/1695502401/fvip/1/short_key/1/keepalive/yes/fexp/24007246/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,sgovp,vprv,playlist_type/sig/AOq0QJ8wRQIgDXYLvO8hsKnxOFI4QK0KR6_bH3y963ahqlBfJqHHOBgCIQDlYY2nTWarn59nFcVOD35IVk5obSssFUeidm3u4n6FBw%3D%3D/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AG3C_xAwRQIhAKnlpIqSLjxTjoejuijzzpuB-Di1I2eiDdzTDDWNboffAiBbdWGc04XUqoe4imJg9kVp3fWTDOFXFVnhAXfqp3Qp6w%3D%3D/playlist/index.m3u8", + "manifest_url": "https://manifest.googlevideo.com/api/manifest/hls_variant/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/source/youtube/requiressl/yes/playback_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31%2C26/mn/sn-5hne6nzy%2Csn-25ge7nz6/ms/au%2Conr/mv/m/mvi/3/pl/22/hfr/1/demuxed/1/tts_caps/1/maudio/1/initcwndbps/1311250/vprv/1/go/1/mt/1695502401/fvip/1/nvgoi/1/short_key/1/ncsapi/1/keepalive/yes/fexp/24007246/dover/13/itag/0/playlist_type/DVR/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cdemuxed%2Ctts_caps%2Cmaudio%2Cvprv%2Cgo%2Citag%2Cplaylist_type/sig/AOq0QJ8wRAIgVqAL59tazORSvoQGDDOnSykYFq5uZpRuPHMC9uFJi-8CIB35p6PNovYkmOKgp4hgRrNprxu-TvLYlHdP4MsxNJgt/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgYBcJvvjykFc2vD1MGR4R95tcg4uoXutvWjGr5CDO7XoCIQDKUqdUFDFde44FsyWMfj2qVOJp_GcX1UGoyQ847aMpVg%3D%3D/file/index.m3u8", + "tbr": 225.675, + "ext": "mp4", + "fps": 25.0, + "protocol": "m3u8_native", + "preference": null, + "quality": 5, + "has_drm": false, + "width": 426, + "height": 240, + "vcodec": "avc1.4D4015", + "acodec": "none", + "dynamic_range": null, + "source_preference": -1 + }, + { + "format_id": "230", + "format_index": null, + "url": "https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/itag/230/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/sgovp/clen%3D5678772%3Bdur%3D212.040%3Bgir%3Dyes%3Bitag%3D134%3Blmt%3D1694045013473544/hls_chunk_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31,26/mn/sn-5hne6nzy,sn-25ge7nz6/ms/au,onr/mv/m/mvi/3/pl/22/initcwndbps/1311250/vprv/1/playlist_type/DVR/dover/13/txp/4535434/mt/1695502401/fvip/1/short_key/1/keepalive/yes/fexp/24007246/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,sgovp,vprv,playlist_type/sig/AOq0QJ8wRgIhAIQvJojwVvM5frF6Na5JJ5zkd1VdUfPLdqPW5Tht2eNkAiEA3437jLFLon8Rpbsp5krc66qddvGP4rj8sbwJd4rlHmU%3D/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AG3C_xAwRQIhAIL_XGGyZVNILzOXnviLoYEMYJfAngPM1eBZGgN6wEr-AiAInhTuU6hkWCkpZGr9dPeXYSfa3brLjZNivRNbpJB8QA%3D%3D/playlist/index.m3u8", + "manifest_url": "https://manifest.googlevideo.com/api/manifest/hls_variant/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/source/youtube/requiressl/yes/playback_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31%2C26/mn/sn-5hne6nzy%2Csn-25ge7nz6/ms/au%2Conr/mv/m/mvi/3/pl/22/hfr/1/demuxed/1/tts_caps/1/maudio/1/initcwndbps/1311250/vprv/1/go/1/mt/1695502401/fvip/1/nvgoi/1/short_key/1/ncsapi/1/keepalive/yes/fexp/24007246/dover/13/itag/0/playlist_type/DVR/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cdemuxed%2Ctts_caps%2Cmaudio%2Cvprv%2Cgo%2Citag%2Cplaylist_type/sig/AOq0QJ8wRAIgVqAL59tazORSvoQGDDOnSykYFq5uZpRuPHMC9uFJi-8CIB35p6PNovYkmOKgp4hgRrNprxu-TvLYlHdP4MsxNJgt/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgYBcJvvjykFc2vD1MGR4R95tcg4uoXutvWjGr5CDO7XoCIQDKUqdUFDFde44FsyWMfj2qVOJp_GcX1UGoyQ847aMpVg%3D%3D/file/index.m3u8", + "tbr": 478.155, + "ext": "mp4", + "fps": 25.0, + "protocol": "m3u8_native", + "preference": null, + "quality": 6, + "has_drm": false, + "width": 640, + "height": 360, + "vcodec": "avc1.4D401E", + "acodec": "none", + "dynamic_range": null, + "source_preference": -1 + }, + { + "format_id": "231", + "format_index": null, + "url": "https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/itag/231/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/sgovp/clen%3D8683274%3Bdur%3D212.040%3Bgir%3Dyes%3Bitag%3D135%3Blmt%3D1694045045723793/hls_chunk_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31,26/mn/sn-5hne6nzy,sn-25ge7nz6/ms/au,onr/mv/m/mvi/3/pl/22/initcwndbps/1311250/vprv/1/playlist_type/DVR/dover/13/txp/4535434/mt/1695502401/fvip/1/short_key/1/keepalive/yes/fexp/24007246/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,sgovp,vprv,playlist_type/sig/AOq0QJ8wRAIgdgnN5tIO26lbTUty02k4r8k9-oaX_7m4LsLXMRBE7n8CIEb2DJRXZ0dGH_ZDbtYAapQKCiCxqht8Bznh0LmS83cw/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AG3C_xAwRQIgfNxyHiNcbQeIRKSwnBRTymUYFDqdhNZqEdRJ5-dl_uICIQD6XyM3ReaIZkg4DsK6ys3VjdMroKJUBHPx4pZGYbqJ0Q%3D%3D/playlist/index.m3u8", + "manifest_url": "https://manifest.googlevideo.com/api/manifest/hls_variant/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/source/youtube/requiressl/yes/playback_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31%2C26/mn/sn-5hne6nzy%2Csn-25ge7nz6/ms/au%2Conr/mv/m/mvi/3/pl/22/hfr/1/demuxed/1/tts_caps/1/maudio/1/initcwndbps/1311250/vprv/1/go/1/mt/1695502401/fvip/1/nvgoi/1/short_key/1/ncsapi/1/keepalive/yes/fexp/24007246/dover/13/itag/0/playlist_type/DVR/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cdemuxed%2Ctts_caps%2Cmaudio%2Cvprv%2Cgo%2Citag%2Cplaylist_type/sig/AOq0QJ8wRAIgVqAL59tazORSvoQGDDOnSykYFq5uZpRuPHMC9uFJi-8CIB35p6PNovYkmOKgp4hgRrNprxu-TvLYlHdP4MsxNJgt/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgYBcJvvjykFc2vD1MGR4R95tcg4uoXutvWjGr5CDO7XoCIQDKUqdUFDFde44FsyWMfj2qVOJp_GcX1UGoyQ847aMpVg%3D%3D/file/index.m3u8", + "tbr": 660.067, + "ext": "mp4", + "fps": 25.0, + "protocol": "m3u8_native", + "preference": null, + "quality": 7, + "has_drm": false, + "width": 854, + "height": 480, + "vcodec": "avc1.4D401E", + "acodec": "none", + "dynamic_range": null, + "source_preference": -1 + }, + { + "format_id": "232", + "format_index": null, + "url": "https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/itag/232/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/sgovp/clen%3D16780212%3Bdur%3D212.040%3Bgir%3Dyes%3Bitag%3D136%3Blmt%3D1694045071129751/hls_chunk_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31,26/mn/sn-5hne6nzy,sn-25ge7nz6/ms/au,onr/mv/m/mvi/3/pl/22/initcwndbps/1311250/vprv/1/playlist_type/DVR/dover/13/txp/4535434/mt/1695502401/fvip/1/short_key/1/keepalive/yes/fexp/24007246/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,sgovp,vprv,playlist_type/sig/AOq0QJ8wRgIhAKklfVW5UAGwbMKG5dyOkjEW9RqlFeZXuS9RKazS7hgHAiEAluSFc2bFqy_0nb32n7mR-SOR0gCmAdFwl35gbDldf3w%3D/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AG3C_xAwRgIhAOqcypFYB4AyrOahc7gihR0-jqv-Gzc8JHdRtQEn3r9wAiEA2cqI-R7Cjr-UaDu-B9miweYpBXWzDeC8PoxK_0bkm5c%3D/playlist/index.m3u8", + "manifest_url": "https://manifest.googlevideo.com/api/manifest/hls_variant/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/source/youtube/requiressl/yes/playback_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31%2C26/mn/sn-5hne6nzy%2Csn-25ge7nz6/ms/au%2Conr/mv/m/mvi/3/pl/22/hfr/1/demuxed/1/tts_caps/1/maudio/1/initcwndbps/1311250/vprv/1/go/1/mt/1695502401/fvip/1/nvgoi/1/short_key/1/ncsapi/1/keepalive/yes/fexp/24007246/dover/13/itag/0/playlist_type/DVR/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cdemuxed%2Ctts_caps%2Cmaudio%2Cvprv%2Cgo%2Citag%2Cplaylist_type/sig/AOq0QJ8wRAIgVqAL59tazORSvoQGDDOnSykYFq5uZpRuPHMC9uFJi-8CIB35p6PNovYkmOKgp4hgRrNprxu-TvLYlHdP4MsxNJgt/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgYBcJvvjykFc2vD1MGR4R95tcg4uoXutvWjGr5CDO7XoCIQDKUqdUFDFde44FsyWMfj2qVOJp_GcX1UGoyQ847aMpVg%3D%3D/file/index.m3u8", + "tbr": 1130.986, + "ext": "mp4", + "fps": 25.0, + "protocol": "m3u8_native", + "preference": null, + "quality": 8, + "has_drm": false, + "width": 1280, + "height": 720, + "vcodec": "avc1.4D401F", + "acodec": "none", + "dynamic_range": null, + "source_preference": -1 + }, + { + "format_id": "269", + "format_index": null, + "url": "https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/itag/269/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/sgovp/clen%3D1863601%3Bdur%3D212.040%3Bgir%3Dyes%3Bitag%3D160%3Blmt%3D1694045032286738/hls_chunk_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31,26/mn/sn-5hne6nzy,sn-25ge7nz6/ms/au,onr/mv/m/mvi/3/pl/22/initcwndbps/1311250/vprv/1/playlist_type/DVR/dover/13/txp/4535434/mt/1695502401/fvip/1/short_key/1/keepalive/yes/fexp/24007246/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,sgovp,vprv,playlist_type/sig/AOq0QJ8wRQIhAOI4zFdpx6CuAJUVfQO8mSPKS-WskVy5lco9PRAL-TfDAiABtG5PX_rqg5Vr77L9IKeZgKU4Mbt-YLWmvxQos9prAg%3D%3D/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AG3C_xAwRQIhAJA2-IQoPA4HYhx5ehZh9_b91jlS-QLvYO8xOp8HXN2uAiAUYQXpYWShcC4WGSKU0_MMNwdKqeQgdYPtqIXTVTKZuA%3D%3D/playlist/index.m3u8", + "manifest_url": "https://manifest.googlevideo.com/api/manifest/hls_variant/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/source/youtube/requiressl/yes/playback_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31%2C26/mn/sn-5hne6nzy%2Csn-25ge7nz6/ms/au%2Conr/mv/m/mvi/3/pl/22/hfr/1/demuxed/1/tts_caps/1/maudio/1/initcwndbps/1311250/vprv/1/go/1/mt/1695502401/fvip/1/nvgoi/1/short_key/1/ncsapi/1/keepalive/yes/fexp/24007246/dover/13/itag/0/playlist_type/DVR/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cdemuxed%2Ctts_caps%2Cmaudio%2Cvprv%2Cgo%2Citag%2Cplaylist_type/sig/AOq0QJ8wRAIgVqAL59tazORSvoQGDDOnSykYFq5uZpRuPHMC9uFJi-8CIB35p6PNovYkmOKgp4hgRrNprxu-TvLYlHdP4MsxNJgt/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgYBcJvvjykFc2vD1MGR4R95tcg4uoXutvWjGr5CDO7XoCIQDKUqdUFDFde44FsyWMfj2qVOJp_GcX1UGoyQ847aMpVg%3D%3D/file/index.m3u8", + "tbr": 156.229, + "ext": "mp4", + "fps": 25.0, + "protocol": "m3u8_native", + "preference": null, + "quality": 0, + "has_drm": false, + "width": 256, + "height": 144, + "vcodec": "avc1.4D400C", + "acodec": "none", + "dynamic_range": null, + "source_preference": -1 + }, + { + "format_id": "270", + "format_index": null, + "url": "https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/itag/270/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/sgovp/clen%3D80166145%3Bdur%3D212.040%3Bgir%3Dyes%3Bitag%3D137%3Blmt%3D1694045208995966/hls_chunk_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31,26/mn/sn-5hne6nzy,sn-25ge7nz6/ms/au,onr/mv/m/mvi/3/pl/22/initcwndbps/1311250/vprv/1/playlist_type/DVR/dover/13/txp/4535434/mt/1695502401/fvip/1/short_key/1/keepalive/yes/fexp/24007246/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,sgovp,vprv,playlist_type/sig/AOq0QJ8wRAIgbzf0MRvdmSh-0Na1BR8xckB4DoMcL2nNJl3vDRew0AICIANhvJC9Q1hAqDjjLubtM7DoNaY-PtJpVlbfaL81F3l8/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AG3C_xAwRQIhAOya7lZyTdnNmoyCacVPgOcsyLDSKevmW3xFt_afVsWfAiB-hASkkk9GrfTuT-6adP2aXYrMXkiB-Y8nuX-wrWUmSg%3D%3D/playlist/index.m3u8", + "manifest_url": "https://manifest.googlevideo.com/api/manifest/hls_variant/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/source/youtube/requiressl/yes/playback_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31%2C26/mn/sn-5hne6nzy%2Csn-25ge7nz6/ms/au%2Conr/mv/m/mvi/3/pl/22/hfr/1/demuxed/1/tts_caps/1/maudio/1/initcwndbps/1311250/vprv/1/go/1/mt/1695502401/fvip/1/nvgoi/1/short_key/1/ncsapi/1/keepalive/yes/fexp/24007246/dover/13/itag/0/playlist_type/DVR/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cdemuxed%2Ctts_caps%2Cmaudio%2Cvprv%2Cgo%2Citag%2Cplaylist_type/sig/AOq0QJ8wRAIgVqAL59tazORSvoQGDDOnSykYFq5uZpRuPHMC9uFJi-8CIB35p6PNovYkmOKgp4hgRrNprxu-TvLYlHdP4MsxNJgt/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgYBcJvvjykFc2vD1MGR4R95tcg4uoXutvWjGr5CDO7XoCIQDKUqdUFDFde44FsyWMfj2qVOJp_GcX1UGoyQ847aMpVg%3D%3D/file/index.m3u8", + "tbr": 4901.412, + "ext": "mp4", + "fps": 25.0, + "protocol": "m3u8_native", + "preference": null, + "quality": 9, + "has_drm": false, + "width": 1920, + "height": 1080, + "vcodec": "avc1.640028", + "acodec": "none", + "dynamic_range": null, + "source_preference": -1 + }, + { + "format_id": "602", + "format_index": null, + "url": "https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/itag/602/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/wft/1/sgovp/clen%3D643306%3Bdur%3D212.080%3Bgir%3Dyes%3Bitag%3D598%3Blmt%3D1694042224218554/hls_chunk_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31,26/mn/sn-5hne6nzy,sn-25ge7nz6/ms/au,onr/mv/m/mvi/3/pl/22/initcwndbps/1311250/vprv/1/playlist_type/DVR/dover/13/txp/4532434/mt/1695502401/fvip/1/short_key/1/keepalive/yes/fexp/24007246/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,wft,sgovp,vprv,playlist_type/sig/AOq0QJ8wRQIhAJ5BHwzV7tAkb9pRDEsdmzFTJOrsuq-IZSmBaa8ZgWU4AiBoqSh-knrE3feDNHwFm_0fAM_qNFn3xvV98kmX_-pYPA%3D%3D/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AG3C_xAwRgIhAIQe5bcuBu9He-VtMYGRHkjZuDoUvmnuIbyjxf6sncbKAiEA9iegdULdUppfIh2N3Lz4Kt0PwtdV-c5G1gRDaO-U7t0%3D/playlist/index.m3u8", + "manifest_url": "https://manifest.googlevideo.com/api/manifest/hls_variant/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/source/youtube/requiressl/yes/playback_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31%2C26/mn/sn-5hne6nzy%2Csn-25ge7nz6/ms/au%2Conr/mv/m/mvi/3/pl/22/hfr/1/demuxed/1/tts_caps/1/maudio/1/initcwndbps/1311250/vprv/1/go/1/mt/1695502401/fvip/1/nvgoi/1/short_key/1/ncsapi/1/keepalive/yes/fexp/24007246/dover/13/itag/0/playlist_type/DVR/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cdemuxed%2Ctts_caps%2Cmaudio%2Cvprv%2Cgo%2Citag%2Cplaylist_type/sig/AOq0QJ8wRAIgVqAL59tazORSvoQGDDOnSykYFq5uZpRuPHMC9uFJi-8CIB35p6PNovYkmOKgp4hgRrNprxu-TvLYlHdP4MsxNJgt/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgYBcJvvjykFc2vD1MGR4R95tcg4uoXutvWjGr5CDO7XoCIQDKUqdUFDFde44FsyWMfj2qVOJp_GcX1UGoyQ847aMpVg%3D%3D/file/index.m3u8", + "tbr": 80.559, + "ext": "mp4", + "fps": 13.0, + "protocol": "m3u8_native", + "preference": null, + "quality": 0, + "has_drm": false, + "width": 256, + "height": 144, + "vcodec": "vp09.00.10.08", + "acodec": "none", + "dynamic_range": null, + "source_preference": -1 + }, + { + "format_id": "603", + "format_index": null, + "url": "https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/itag/603/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/wft/1/sgovp/clen%3D2404581%3Bdur%3D212.040%3Bgir%3Dyes%3Bitag%3D278%3Blmt%3D1694043382822868/hls_chunk_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31,26/mn/sn-5hne6nzy,sn-25ge7nz6/ms/au,onr/mv/m/mvi/3/pl/22/initcwndbps/1311250/vprv/1/playlist_type/DVR/dover/13/txp/4535434/mt/1695502401/fvip/1/short_key/1/keepalive/yes/fexp/24007246/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,wft,sgovp,vprv,playlist_type/sig/AOq0QJ8wRAIgW4kL5-Bqq0OV_FyB5Df0QcqkyUTYid2eN4BUzn8sp98CIFqLxBBSz7H3PaXJ4NycNae2P0--5ri0HHMItBr8PKIP/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AG3C_xAwRgIhAJ6-KuK2swgLKLyCSnGkgsoVy2VR9SuNpx6Crrz9mc9GAiEAqbUS5dWqCZkA7oSKAONrBYKbsjgiXwT1EV6Uxj8ToOU%3D/playlist/index.m3u8", + "manifest_url": "https://manifest.googlevideo.com/api/manifest/hls_variant/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/source/youtube/requiressl/yes/playback_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31%2C26/mn/sn-5hne6nzy%2Csn-25ge7nz6/ms/au%2Conr/mv/m/mvi/3/pl/22/hfr/1/demuxed/1/tts_caps/1/maudio/1/initcwndbps/1311250/vprv/1/go/1/mt/1695502401/fvip/1/nvgoi/1/short_key/1/ncsapi/1/keepalive/yes/fexp/24007246/dover/13/itag/0/playlist_type/DVR/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cdemuxed%2Ctts_caps%2Cmaudio%2Cvprv%2Cgo%2Citag%2Cplaylist_type/sig/AOq0QJ8wRAIgVqAL59tazORSvoQGDDOnSykYFq5uZpRuPHMC9uFJi-8CIB35p6PNovYkmOKgp4hgRrNprxu-TvLYlHdP4MsxNJgt/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgYBcJvvjykFc2vD1MGR4R95tcg4uoXutvWjGr5CDO7XoCIQDKUqdUFDFde44FsyWMfj2qVOJp_GcX1UGoyQ847aMpVg%3D%3D/file/index.m3u8", + "tbr": 153.593, + "ext": "mp4", + "fps": 25.0, + "protocol": "m3u8_native", + "preference": null, + "quality": 0, + "has_drm": false, + "width": 256, + "height": 144, + "vcodec": "vp09.00.11.08", + "acodec": "none", + "dynamic_range": null, + "source_preference": -1 + }, + { + "format_id": "604", + "format_index": null, + "url": "https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/itag/604/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/wft/1/sgovp/clen%3D4021173%3Bdur%3D212.040%3Bgir%3Dyes%3Bitag%3D242%3Blmt%3D1694043379783601/hls_chunk_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31,26/mn/sn-5hne6nzy,sn-25ge7nz6/ms/au,onr/mv/m/mvi/3/pl/22/initcwndbps/1311250/vprv/1/playlist_type/DVR/dover/13/txp/4535434/mt/1695502401/fvip/1/short_key/1/keepalive/yes/fexp/24007246/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,wft,sgovp,vprv,playlist_type/sig/AOq0QJ8wRQIhAIW_PHJodC3iY33S6s7ju5X9_6oByqQFda5vPWR8jwrgAiB_csQrznhta4iTLmj6Xzybwgfe5CRA6TFV1KbQ21QJZw%3D%3D/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AG3C_xAwRQIgHGms27SWPkQSOt2slvmBWboDwV_BrqW_RoRlpdqD5rACIQCVpBzzlQxE44nHEJ4hoYD2QUvIm732saxlZ2fLjfljJQ%3D%3D/playlist/index.m3u8", + "manifest_url": "https://manifest.googlevideo.com/api/manifest/hls_variant/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/source/youtube/requiressl/yes/playback_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31%2C26/mn/sn-5hne6nzy%2Csn-25ge7nz6/ms/au%2Conr/mv/m/mvi/3/pl/22/hfr/1/demuxed/1/tts_caps/1/maudio/1/initcwndbps/1311250/vprv/1/go/1/mt/1695502401/fvip/1/nvgoi/1/short_key/1/ncsapi/1/keepalive/yes/fexp/24007246/dover/13/itag/0/playlist_type/DVR/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cdemuxed%2Ctts_caps%2Cmaudio%2Cvprv%2Cgo%2Citag%2Cplaylist_type/sig/AOq0QJ8wRAIgVqAL59tazORSvoQGDDOnSykYFq5uZpRuPHMC9uFJi-8CIB35p6PNovYkmOKgp4hgRrNprxu-TvLYlHdP4MsxNJgt/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgYBcJvvjykFc2vD1MGR4R95tcg4uoXutvWjGr5CDO7XoCIQDKUqdUFDFde44FsyWMfj2qVOJp_GcX1UGoyQ847aMpVg%3D%3D/file/index.m3u8", + "tbr": 287.523, + "ext": "mp4", + "fps": 25.0, + "protocol": "m3u8_native", + "preference": null, + "quality": 5, + "has_drm": false, + "width": 426, + "height": 240, + "vcodec": "vp09.00.20.08", + "acodec": "none", + "dynamic_range": null, + "source_preference": -1 + }, + { + "format_id": "605", + "format_index": null, + "url": "https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/itag/605/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/wft/1/sgovp/clen%3D6902164%3Bdur%3D212.040%3Bgir%3Dyes%3Bitag%3D243%3Blmt%3D1694043349554753/hls_chunk_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31,26/mn/sn-5hne6nzy,sn-25ge7nz6/ms/au,onr/mv/m/mvi/3/pl/22/initcwndbps/1311250/vprv/1/playlist_type/DVR/dover/13/txp/4535434/mt/1695502401/fvip/1/short_key/1/keepalive/yes/fexp/24007246/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,wft,sgovp,vprv,playlist_type/sig/AOq0QJ8wRQIgJd3aB7R2t5a0XNGTqDIuYSimhFpK2hEvDD1-itRftKkCIQCqe4F0OhI5PSp0tSYEXlngrmJgfTGIuVZUMH8saPZlnQ%3D%3D/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AG3C_xAwRAIgQveRyDrh7DOwnJgI7dzbB3XLvqrPvKwutQI7ZjCtIs0CIHRxPzpMlfC9QmQMTu2SIGs7QP8bP1Nn65JxYGRecFCt/playlist/index.m3u8", + "manifest_url": "https://manifest.googlevideo.com/api/manifest/hls_variant/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/source/youtube/requiressl/yes/playback_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31%2C26/mn/sn-5hne6nzy%2Csn-25ge7nz6/ms/au%2Conr/mv/m/mvi/3/pl/22/hfr/1/demuxed/1/tts_caps/1/maudio/1/initcwndbps/1311250/vprv/1/go/1/mt/1695502401/fvip/1/nvgoi/1/short_key/1/ncsapi/1/keepalive/yes/fexp/24007246/dover/13/itag/0/playlist_type/DVR/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cdemuxed%2Ctts_caps%2Cmaudio%2Cvprv%2Cgo%2Citag%2Cplaylist_type/sig/AOq0QJ8wRAIgVqAL59tazORSvoQGDDOnSykYFq5uZpRuPHMC9uFJi-8CIB35p6PNovYkmOKgp4hgRrNprxu-TvLYlHdP4MsxNJgt/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgYBcJvvjykFc2vD1MGR4R95tcg4uoXutvWjGr5CDO7XoCIQDKUqdUFDFde44FsyWMfj2qVOJp_GcX1UGoyQ847aMpVg%3D%3D/file/index.m3u8", + "tbr": 566.25, + "ext": "mp4", + "fps": 25.0, + "protocol": "m3u8_native", + "preference": null, + "quality": 6, + "has_drm": false, + "width": 640, + "height": 360, + "vcodec": "vp09.00.21.08", + "acodec": "none", + "dynamic_range": null, + "source_preference": -1 + }, + { + "format_id": "606", + "format_index": null, + "url": "https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/itag/606/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/wft/1/sgovp/clen%3D10927666%3Bdur%3D212.040%3Bgir%3Dyes%3Bitag%3D244%3Blmt%3D1694043369037289/hls_chunk_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31,26/mn/sn-5hne6nzy,sn-25ge7nz6/ms/au,onr/mv/m/mvi/3/pl/22/initcwndbps/1311250/vprv/1/playlist_type/DVR/dover/13/txp/4535434/mt/1695502401/fvip/1/short_key/1/keepalive/yes/fexp/24007246/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,wft,sgovp,vprv,playlist_type/sig/AOq0QJ8wRQIhAIIZhHj_PxMIRxj2AvOoouUWwZPnPs3-autC7-_Qu1dnAiAtQJp9ZV1TVQXd02g1viHWghB6tKSD5_jcRHzLPHIAeA%3D%3D/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AG3C_xAwRQIgXJh3M-PAlfv0g5H_brRmCBl1Z0w0b5y9mqIdEsZSp-0CIQD4j4piciikRuQI3KX-HFizmq-dPxMc-aqVBFYw43-NRA%3D%3D/playlist/index.m3u8", + "manifest_url": "https://manifest.googlevideo.com/api/manifest/hls_variant/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/source/youtube/requiressl/yes/playback_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31%2C26/mn/sn-5hne6nzy%2Csn-25ge7nz6/ms/au%2Conr/mv/m/mvi/3/pl/22/hfr/1/demuxed/1/tts_caps/1/maudio/1/initcwndbps/1311250/vprv/1/go/1/mt/1695502401/fvip/1/nvgoi/1/short_key/1/ncsapi/1/keepalive/yes/fexp/24007246/dover/13/itag/0/playlist_type/DVR/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cdemuxed%2Ctts_caps%2Cmaudio%2Cvprv%2Cgo%2Citag%2Cplaylist_type/sig/AOq0QJ8wRAIgVqAL59tazORSvoQGDDOnSykYFq5uZpRuPHMC9uFJi-8CIB35p6PNovYkmOKgp4hgRrNprxu-TvLYlHdP4MsxNJgt/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgYBcJvvjykFc2vD1MGR4R95tcg4uoXutvWjGr5CDO7XoCIQDKUqdUFDFde44FsyWMfj2qVOJp_GcX1UGoyQ847aMpVg%3D%3D/file/index.m3u8", + "tbr": 733.359, + "ext": "mp4", + "fps": 25.0, + "protocol": "m3u8_native", + "preference": null, + "quality": 7, + "has_drm": false, + "width": 854, + "height": 480, + "vcodec": "vp09.00.30.08", + "acodec": "none", + "dynamic_range": null, + "source_preference": -1 + }, + { + "format_id": "609", + "format_index": null, + "url": "https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/itag/609/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/wft/1/sgovp/clen%3D15359727%3Bdur%3D212.040%3Bgir%3Dyes%3Bitag%3D247%3Blmt%3D1694043486219683/hls_chunk_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31,26/mn/sn-5hne6nzy,sn-25ge7nz6/ms/au,onr/mv/m/mvi/3/pl/22/initcwndbps/1311250/vprv/1/playlist_type/DVR/dover/13/txp/4535434/mt/1695502401/fvip/1/short_key/1/keepalive/yes/fexp/24007246/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,wft,sgovp,vprv,playlist_type/sig/AOq0QJ8wRQIhANJRh709Wuh23a9Wj2UUKFE9qjHRMscBHd3fQjuNjK5zAiB4gh40D5HmwOx3JuqptUi44o5EtkdzK0IQEunFmwOPiA%3D%3D/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AG3C_xAwRQIgRcLcLuy1kw90oGfrplalJdXQ4t9tjEQNH-bp7lGNsCICIQDLlCQYGjyHjnkZONzlaYidWV7-_stKKzzkhz3xEsOP4w%3D%3D/playlist/index.m3u8", + "manifest_url": "https://manifest.googlevideo.com/api/manifest/hls_variant/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/source/youtube/requiressl/yes/playback_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31%2C26/mn/sn-5hne6nzy%2Csn-25ge7nz6/ms/au%2Conr/mv/m/mvi/3/pl/22/hfr/1/demuxed/1/tts_caps/1/maudio/1/initcwndbps/1311250/vprv/1/go/1/mt/1695502401/fvip/1/nvgoi/1/short_key/1/ncsapi/1/keepalive/yes/fexp/24007246/dover/13/itag/0/playlist_type/DVR/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cdemuxed%2Ctts_caps%2Cmaudio%2Cvprv%2Cgo%2Citag%2Cplaylist_type/sig/AOq0QJ8wRAIgVqAL59tazORSvoQGDDOnSykYFq5uZpRuPHMC9uFJi-8CIB35p6PNovYkmOKgp4hgRrNprxu-TvLYlHdP4MsxNJgt/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgYBcJvvjykFc2vD1MGR4R95tcg4uoXutvWjGr5CDO7XoCIQDKUqdUFDFde44FsyWMfj2qVOJp_GcX1UGoyQ847aMpVg%3D%3D/file/index.m3u8", + "tbr": 1179.472, + "ext": "mp4", + "fps": 25.0, + "protocol": "m3u8_native", + "preference": null, + "quality": 8, + "has_drm": false, + "width": 1280, + "height": 720, + "vcodec": "vp09.00.31.08", + "acodec": "none", + "dynamic_range": null, + "source_preference": -1 + }, + { + "format_id": "614", + "format_index": null, + "url": "https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/itag/614/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/wft/1/sgovp/clen%3D40874930%3Bdur%3D212.040%3Bgir%3Dyes%3Bitag%3D248%3Blmt%3D1694044655610179/hls_chunk_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31,26/mn/sn-5hne6nzy,sn-25ge7nz6/ms/au,onr/mv/m/mvi/3/pl/22/initcwndbps/1311250/vprv/1/playlist_type/DVR/dover/13/txp/4535434/mt/1695502401/fvip/1/short_key/1/keepalive/yes/fexp/24007246/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,wft,sgovp,vprv,playlist_type/sig/AOq0QJ8wRQIga8rGnkxIK7d-77rdKN1yHtRP9NyUJGXfRyVba5rKVRoCIQD7mJ1LOowgdfuJQuXTvarIbd54VwB6hM5O05zpPdFJDQ%3D%3D/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AG3C_xAwRAIgGwgwIro32WyMRnb_Ccp6z_iH1YZLIwF2D8nnhQOoyJwCIGvOkZvz50XkJPrLReF_rHyHcsgE9PM_hcpudysB6YN9/playlist/index.m3u8", + "manifest_url": "https://manifest.googlevideo.com/api/manifest/hls_variant/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/source/youtube/requiressl/yes/playback_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31%2C26/mn/sn-5hne6nzy%2Csn-25ge7nz6/ms/au%2Conr/mv/m/mvi/3/pl/22/hfr/1/demuxed/1/tts_caps/1/maudio/1/initcwndbps/1311250/vprv/1/go/1/mt/1695502401/fvip/1/nvgoi/1/short_key/1/ncsapi/1/keepalive/yes/fexp/24007246/dover/13/itag/0/playlist_type/DVR/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cdemuxed%2Ctts_caps%2Cmaudio%2Cvprv%2Cgo%2Citag%2Cplaylist_type/sig/AOq0QJ8wRAIgVqAL59tazORSvoQGDDOnSykYFq5uZpRuPHMC9uFJi-8CIB35p6PNovYkmOKgp4hgRrNprxu-TvLYlHdP4MsxNJgt/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgYBcJvvjykFc2vD1MGR4R95tcg4uoXutvWjGr5CDO7XoCIQDKUqdUFDFde44FsyWMfj2qVOJp_GcX1UGoyQ847aMpVg%3D%3D/file/index.m3u8", + "tbr": 2831.123, + "ext": "mp4", + "fps": 25.0, + "protocol": "m3u8_native", + "preference": null, + "quality": 9, + "has_drm": false, + "width": 1920, + "height": 1080, + "vcodec": "vp09.00.40.08", + "acodec": "none", + "dynamic_range": null, + "source_preference": -1 + }, + { + "format_id": "616", + "format_index": null, + "url": "https://manifest.googlevideo.com/api/manifest/hls_playlist/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/itag/616/source/youtube/requiressl/yes/ratebypass/yes/pfa/1/wft/1/sgovp/clen%3D99471214%3Bdur%3D212.040%3Bgir%3Dyes%3Bitag%3D356%3Blmt%3D1694043438471036/hls_chunk_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31,26/mn/sn-5hne6nzy,sn-25ge7nz6/ms/au,onr/mv/m/mvi/3/pl/22/initcwndbps/1311250/vprv/1/playlist_type/DVR/dover/13/txp/4532434/mt/1695502401/fvip/1/short_key/1/keepalive/yes/fexp/24007246/sparams/expire,ei,ip,id,itag,source,requiressl,ratebypass,pfa,wft,sgovp,vprv,playlist_type/sig/AOq0QJ8wRQIhAPTG-DKQd4Rtv1dvvExqPNGfPU_wBRbsGSIYRqJ3UCDEAiBBYBgPR_gAJAiCr2eHvR3hu6uWUEUCvEN5pr5Dm2_5gA%3D%3D/lsparams/hls_chunk_host,mh,mm,mn,ms,mv,mvi,pl,initcwndbps/lsig/AG3C_xAwRQIgIzhyfPERJMLLzgSld4XG3lYTJKhsmpOrVD2v_siZfEgCIQCPOKf2Or4aqJhe--d_2Qh_ljI39BS6JH7x6BPXC7f_NA%3D%3D/playlist/index.m3u8", + "manifest_url": "https://manifest.googlevideo.com/api/manifest/hls_variant/expire/1695524274/ei/UlEPZZamLdK-6dsP1vWlkAY/ip/45.93.75.130/id/750c38c3d5a05dc4/source/youtube/requiressl/yes/playback_host/rr3---sn-5hne6nzy.googlevideo.com/mh/7c/mm/31%2C26/mn/sn-5hne6nzy%2Csn-25ge7nz6/ms/au%2Conr/mv/m/mvi/3/pl/22/hfr/1/demuxed/1/tts_caps/1/maudio/1/initcwndbps/1311250/vprv/1/go/1/mt/1695502401/fvip/1/nvgoi/1/short_key/1/ncsapi/1/keepalive/yes/fexp/24007246/dover/13/itag/0/playlist_type/DVR/sparams/expire%2Cei%2Cip%2Cid%2Csource%2Crequiressl%2Chfr%2Cdemuxed%2Ctts_caps%2Cmaudio%2Cvprv%2Cgo%2Citag%2Cplaylist_type/sig/AOq0QJ8wRAIgVqAL59tazORSvoQGDDOnSykYFq5uZpRuPHMC9uFJi-8CIB35p6PNovYkmOKgp4hgRrNprxu-TvLYlHdP4MsxNJgt/lsparams/playback_host%2Cmh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps/lsig/AG3C_xAwRQIgYBcJvvjykFc2vD1MGR4R95tcg4uoXutvWjGr5CDO7XoCIQDKUqdUFDFde44FsyWMfj2qVOJp_GcX1UGoyQ847aMpVg%3D%3D/file/index.m3u8", + "tbr": 5704.254, + "ext": "mp4", + "fps": 25.0, + "protocol": "m3u8_native", + "preference": null, + "quality": 9, + "has_drm": false, + "width": 1920, + "height": 1080, + "vcodec": "vp09.00.40.08", + "acodec": "none", + "dynamic_range": null, + "source_preference": 99, + "format_note": "Premium" + }, + { + "format_id": "sb0", + "format_note": "storyboard", + "ext": "mhtml", + "protocol": "mhtml", + "acodec": "none", + "vcodec": "none", + "url": "https://i.ytimg.com/sb/dQw4w9WgXcQ/storyboard3_L2/M$M.jpg?sqp=-oaymwGbA0g48quKqQOSA4gBAZUBAAAEQpgBMqABPKgBBLABELABDbABDLABELABFbABH7ABJrABLbABDrABDrABD7ABErABF7ABK7ABLLABKbABD7ABDrABELABFbABH7ABKrABMrABKbABD7ABEbABFLABGLABJrABPbABOLABLbABEbABFLABHrABKrABMbABS7ABR7ABNrABFbABHLABKbABLrABObABR7ABTbABP7ABJbABLrABN7ABPbABR7ABUrABUbABRbABM7ABQLABQrABQ7ABTLABRLABRrABQ7gBEbgBEbgBFbgBI7gBRLgBQ7gBQ7gBQ7gBEbgBE7gBFrgBL7gBQ7gBQ7gBQ7gBQ7gBFbgBFrgBKbgBQ7gBQ7gBQ7gBQ7gBQ7gBI7gBL7gBQ7gBQ7gBQ7gBQ7gBQ7gBQ7gBRLgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQqLzl_8DBgjuj9umBg==&sigh=rs$AOn4CLCHYrXWHttYu2902drAXv1Wg3kN4g", + "width": 160, + "height": 90, + "fps": 0.5094339622641509, + "rows": 5, + "columns": 5, + "fragments": [ + { + "url": "https://i.ytimg.com/sb/dQw4w9WgXcQ/storyboard3_L2/M0.jpg?sqp=-oaymwGbA0g48quKqQOSA4gBAZUBAAAEQpgBMqABPKgBBLABELABDbABDLABELABFbABH7ABJrABLbABDrABDrABD7ABErABF7ABK7ABLLABKbABD7ABDrABELABFbABH7ABKrABMrABKbABD7ABEbABFLABGLABJrABPbABOLABLbABEbABFLABHrABKrABMbABS7ABR7ABNrABFbABHLABKbABLrABObABR7ABTbABP7ABJbABLrABN7ABPbABR7ABUrABUbABRbABM7ABQLABQrABQ7ABTLABRLABRrABQ7gBEbgBEbgBFbgBI7gBRLgBQ7gBQ7gBQ7gBEbgBE7gBFrgBL7gBQ7gBQ7gBQ7gBQ7gBFbgBFrgBKbgBQ7gBQ7gBQ7gBQ7gBQ7gBI7gBL7gBQ7gBQ7gBQ7gBQ7gBQ7gBQ7gBRLgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQqLzl_8DBgjuj9umBg==&sigh=rs$AOn4CLCHYrXWHttYu2902drAXv1Wg3kN4g", + "duration": 49.07407407407407 + }, + { + "url": "https://i.ytimg.com/sb/dQw4w9WgXcQ/storyboard3_L2/M1.jpg?sqp=-oaymwGbA0g48quKqQOSA4gBAZUBAAAEQpgBMqABPKgBBLABELABDbABDLABELABFbABH7ABJrABLbABDrABDrABD7ABErABF7ABK7ABLLABKbABD7ABDrABELABFbABH7ABKrABMrABKbABD7ABEbABFLABGLABJrABPbABOLABLbABEbABFLABHrABKrABMbABS7ABR7ABNrABFbABHLABKbABLrABObABR7ABTbABP7ABJbABLrABN7ABPbABR7ABUrABUbABRbABM7ABQLABQrABQ7ABTLABRLABRrABQ7gBEbgBEbgBFbgBI7gBRLgBQ7gBQ7gBQ7gBEbgBE7gBFrgBL7gBQ7gBQ7gBQ7gBQ7gBFbgBFrgBKbgBQ7gBQ7gBQ7gBQ7gBQ7gBI7gBL7gBQ7gBQ7gBQ7gBQ7gBQ7gBQ7gBRLgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQqLzl_8DBgjuj9umBg==&sigh=rs$AOn4CLCHYrXWHttYu2902drAXv1Wg3kN4g", + "duration": 49.07407407407407 + }, + { + "url": "https://i.ytimg.com/sb/dQw4w9WgXcQ/storyboard3_L2/M2.jpg?sqp=-oaymwGbA0g48quKqQOSA4gBAZUBAAAEQpgBMqABPKgBBLABELABDbABDLABELABFbABH7ABJrABLbABDrABDrABD7ABErABF7ABK7ABLLABKbABD7ABDrABELABFbABH7ABKrABMrABKbABD7ABEbABFLABGLABJrABPbABOLABLbABEbABFLABHrABKrABMbABS7ABR7ABNrABFbABHLABKbABLrABObABR7ABTbABP7ABJbABLrABN7ABPbABR7ABUrABUbABRbABM7ABQLABQrABQ7ABTLABRLABRrABQ7gBEbgBEbgBFbgBI7gBRLgBQ7gBQ7gBQ7gBEbgBE7gBFrgBL7gBQ7gBQ7gBQ7gBQ7gBFbgBFrgBKbgBQ7gBQ7gBQ7gBQ7gBQ7gBI7gBL7gBQ7gBQ7gBQ7gBQ7gBQ7gBQ7gBRLgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQqLzl_8DBgjuj9umBg==&sigh=rs$AOn4CLCHYrXWHttYu2902drAXv1Wg3kN4g", + "duration": 49.07407407407407 + }, + { + "url": "https://i.ytimg.com/sb/dQw4w9WgXcQ/storyboard3_L2/M3.jpg?sqp=-oaymwGbA0g48quKqQOSA4gBAZUBAAAEQpgBMqABPKgBBLABELABDbABDLABELABFbABH7ABJrABLbABDrABDrABD7ABErABF7ABK7ABLLABKbABD7ABDrABELABFbABH7ABKrABMrABKbABD7ABEbABFLABGLABJrABPbABOLABLbABEbABFLABHrABKrABMbABS7ABR7ABNrABFbABHLABKbABLrABObABR7ABTbABP7ABJbABLrABN7ABPbABR7ABUrABUbABRbABM7ABQLABQrABQ7ABTLABRLABRrABQ7gBEbgBEbgBFbgBI7gBRLgBQ7gBQ7gBQ7gBEbgBE7gBFrgBL7gBQ7gBQ7gBQ7gBQ7gBFbgBFrgBKbgBQ7gBQ7gBQ7gBQ7gBQ7gBI7gBL7gBQ7gBQ7gBQ7gBQ7gBQ7gBQ7gBRLgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQqLzl_8DBgjuj9umBg==&sigh=rs$AOn4CLCHYrXWHttYu2902drAXv1Wg3kN4g", + "duration": 49.07407407407407 + }, + { + "url": "https://i.ytimg.com/sb/dQw4w9WgXcQ/storyboard3_L2/M4.jpg?sqp=-oaymwGbA0g48quKqQOSA4gBAZUBAAAEQpgBMqABPKgBBLABELABDbABDLABELABFbABH7ABJrABLbABDrABDrABD7ABErABF7ABK7ABLLABKbABD7ABDrABELABFbABH7ABKrABMrABKbABD7ABEbABFLABGLABJrABPbABOLABLbABEbABFLABHrABKrABMbABS7ABR7ABNrABFbABHLABKbABLrABObABR7ABTbABP7ABJbABLrABN7ABPbABR7ABUrABUbABRbABM7ABQLABQrABQ7ABTLABRLABRrABQ7gBEbgBEbgBFbgBI7gBRLgBQ7gBQ7gBQ7gBEbgBE7gBFrgBL7gBQ7gBQ7gBQ7gBQ7gBFbgBFrgBKbgBQ7gBQ7gBQ7gBQ7gBQ7gBI7gBL7gBQ7gBQ7gBQ7gBQ7gBQ7gBQ7gBRLgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQqLzl_8DBgjuj9umBg==&sigh=rs$AOn4CLCHYrXWHttYu2902drAXv1Wg3kN4g", + "duration": 15.703703703703724 + } + ] + }, + { + "format_id": "sb1", + "format_note": "storyboard", + "ext": "mhtml", + "protocol": "mhtml", + "acodec": "none", + "vcodec": "none", + "url": "https://i.ytimg.com/sb/dQw4w9WgXcQ/storyboard3_L1/M$M.jpg?sqp=-oaymwGbA0g48quKqQOSA4gBAZUBAAAEQpgBMqABPKgBBLABELABDbABDLABELABFbABH7ABJrABLbABDrABDrABD7ABErABF7ABK7ABLLABKbABD7ABDrABELABFbABH7ABKrABMrABKbABD7ABEbABFLABGLABJrABPbABOLABLbABEbABFLABHrABKrABMbABS7ABR7ABNrABFbABHLABKbABLrABObABR7ABTbABP7ABJbABLrABN7ABPbABR7ABUrABUbABRbABM7ABQLABQrABQ7ABTLABRLABRrABQ7gBEbgBEbgBFbgBI7gBRLgBQ7gBQ7gBQ7gBEbgBE7gBFrgBL7gBQ7gBQ7gBQ7gBQ7gBFbgBFrgBKbgBQ7gBQ7gBQ7gBQ7gBQ7gBI7gBL7gBQ7gBQ7gBQ7gBQ7gBQ7gBQ7gBRLgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQqLzl_8DBgjuj9umBg==&sigh=rs$AOn4CLCTXAc1RH5-ZIGm3FlRiYRUkzQXug", + "width": 80, + "height": 45, + "fps": 0.5094339622641509, + "rows": 10, + "columns": 10, + "fragments": [ + { + "url": "https://i.ytimg.com/sb/dQw4w9WgXcQ/storyboard3_L1/M0.jpg?sqp=-oaymwGbA0g48quKqQOSA4gBAZUBAAAEQpgBMqABPKgBBLABELABDbABDLABELABFbABH7ABJrABLbABDrABDrABD7ABErABF7ABK7ABLLABKbABD7ABDrABELABFbABH7ABKrABMrABKbABD7ABEbABFLABGLABJrABPbABOLABLbABEbABFLABHrABKrABMbABS7ABR7ABNrABFbABHLABKbABLrABObABR7ABTbABP7ABJbABLrABN7ABPbABR7ABUrABUbABRbABM7ABQLABQrABQ7ABTLABRLABRrABQ7gBEbgBEbgBFbgBI7gBRLgBQ7gBQ7gBQ7gBEbgBE7gBFrgBL7gBQ7gBQ7gBQ7gBQ7gBFbgBFrgBKbgBQ7gBQ7gBQ7gBQ7gBQ7gBI7gBL7gBQ7gBQ7gBQ7gBQ7gBQ7gBQ7gBRLgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQqLzl_8DBgjuj9umBg==&sigh=rs$AOn4CLCTXAc1RH5-ZIGm3FlRiYRUkzQXug", + "duration": 196.29629629629628 + }, + { + "url": "https://i.ytimg.com/sb/dQw4w9WgXcQ/storyboard3_L1/M1.jpg?sqp=-oaymwGbA0g48quKqQOSA4gBAZUBAAAEQpgBMqABPKgBBLABELABDbABDLABELABFbABH7ABJrABLbABDrABDrABD7ABErABF7ABK7ABLLABKbABD7ABDrABELABFbABH7ABKrABMrABKbABD7ABEbABFLABGLABJrABPbABOLABLbABEbABFLABHrABKrABMbABS7ABR7ABNrABFbABHLABKbABLrABObABR7ABTbABP7ABJbABLrABN7ABPbABR7ABUrABUbABRbABM7ABQLABQrABQ7ABTLABRLABRrABQ7gBEbgBEbgBFbgBI7gBRLgBQ7gBQ7gBQ7gBEbgBE7gBFrgBL7gBQ7gBQ7gBQ7gBQ7gBFbgBFrgBKbgBQ7gBQ7gBQ7gBQ7gBQ7gBI7gBL7gBQ7gBQ7gBQ7gBQ7gBQ7gBQ7gBRLgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQqLzl_8DBgjuj9umBg==&sigh=rs$AOn4CLCTXAc1RH5-ZIGm3FlRiYRUkzQXug", + "duration": 15.703703703703724 + } + ] + }, + { + "format_id": "sb2", + "format_note": "storyboard", + "ext": "mhtml", + "protocol": "mhtml", + "acodec": "none", + "vcodec": "none", + "url": "https://i.ytimg.com/sb/dQw4w9WgXcQ/storyboard3_L0/default.jpg?sqp=-oaymwGbA0g48quKqQOSA4gBAZUBAAAEQpgBMqABPKgBBLABELABDbABDLABELABFbABH7ABJrABLbABDrABDrABD7ABErABF7ABK7ABLLABKbABD7ABDrABELABFbABH7ABKrABMrABKbABD7ABEbABFLABGLABJrABPbABOLABLbABEbABFLABHrABKrABMbABS7ABR7ABNrABFbABHLABKbABLrABObABR7ABTbABP7ABJbABLrABN7ABPbABR7ABUrABUbABRbABM7ABQLABQrABQ7ABTLABRLABRrABQ7gBEbgBEbgBFbgBI7gBRLgBQ7gBQ7gBQ7gBEbgBE7gBFrgBL7gBQ7gBQ7gBQ7gBQ7gBFbgBFrgBKbgBQ7gBQ7gBQ7gBQ7gBQ7gBI7gBL7gBQ7gBQ7gBQ7gBQ7gBQ7gBQ7gBRLgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQqLzl_8DBgjuj9umBg==&sigh=rs$AOn4CLCSw4ypjBBVyVfNU-jl-4aLZArqkA", + "width": 48, + "height": 27, + "fps": 0.4716981132075472, + "rows": 10, + "columns": 10, + "fragments": [ + { + "url": "https://i.ytimg.com/sb/dQw4w9WgXcQ/storyboard3_L0/default.jpg?sqp=-oaymwGbA0g48quKqQOSA4gBAZUBAAAEQpgBMqABPKgBBLABELABDbABDLABELABFbABH7ABJrABLbABDrABDrABD7ABErABF7ABK7ABLLABKbABD7ABDrABELABFbABH7ABKrABMrABKbABD7ABEbABFLABGLABJrABPbABOLABLbABEbABFLABHrABKrABMbABS7ABR7ABNrABFbABHLABKbABLrABObABR7ABTbABP7ABJbABLrABN7ABPbABR7ABUrABUbABRbABM7ABQLABQrABQ7ABTLABRLABRrABQ7gBEbgBEbgBFbgBI7gBRLgBQ7gBQ7gBQ7gBEbgBE7gBFrgBL7gBQ7gBQ7gBQ7gBQ7gBFbgBFrgBKbgBQ7gBQ7gBQ7gBQ7gBQ7gBI7gBL7gBQ7gBQ7gBQ7gBQ7gBQ7gBQ7gBRLgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQqLzl_8DBgjuj9umBg==&sigh=rs$AOn4CLCSw4ypjBBVyVfNU-jl-4aLZArqkA", + "duration": 212.0 + } + ] + } + ], + "thumbnails": [ + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/mqdefault.jpg", + "height": 180, + "width": 320, + "preference": -11 + }, + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg", + "height": 360, + "width": 480, + "preference": -7 + }, + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/sddefault.jpg", + "height": 480, + "width": 640, + "preference": -5 + }, + { + "url": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/default.webp", + "height": 90, + "width": 120, + "preference": -12 + }, + { + "url": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/mqdefault.webp", + "height": 180, + "width": 320, + "preference": -10 + }, + { + "url": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/hqdefault.webp", + "height": 360, + "width": 480, + "preference": -6 + }, + { + "url": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/sddefault.webp", + "height": 480, + "width": 640, + "preference": -4 + }, + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLDd2KtelLHaNSXrI9_5K-NvTscKNw", + "height": 94, + "width": 168, + "preference": -7 + }, + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg?sqp=-oaymwEbCMQBEG5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBUpEOOWUXWkNyijQuZ4UPzp2BE-w", + "height": 110, + "width": 196, + "preference": -7 + }, + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg?sqp=-oaymwEcCPYBEIoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBCyhr8AqpJ1SxKVU6SyK5ODJ_IpA", + "height": 138, + "width": 246, + "preference": -7 + }, + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLB_p0PncTtkrhaNDZtntrE3gKkoYw", + "height": 188, + "width": 336, + "preference": -7 + }, + { + "url": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/maxresdefault.webp", + "height": 1080, + "width": 1920, + "preference": 0 + }, + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg", + "height": 720, + "width": 1280, + "preference": -1 + }, + { + "url": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/hq720.webp", + "preference": -2 + }, + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/hq720.jpg", + "preference": -3 + }, + { + "url": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/0.webp", + "preference": -8 + }, + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/0.jpg", + "preference": -9 + }, + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/default.jpg", + "preference": -13 + }, + { + "url": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/sd1.webp", + "preference": -14 + }, + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/sd1.jpg", + "preference": -15 + }, + { + "url": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/sd2.webp", + "preference": -16 + }, + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/sd2.jpg", + "preference": -17 + }, + { + "url": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/sd3.webp", + "preference": -18 + }, + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/sd3.jpg", + "preference": -19 + }, + { + "url": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/hq1.webp", + "preference": -20 + }, + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/hq1.jpg", + "preference": -21 + }, + { + "url": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/hq2.webp", + "preference": -22 + }, + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/hq2.jpg", + "preference": -23 + }, + { + "url": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/hq3.webp", + "preference": -24 + }, + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/hq3.jpg", + "preference": -25 + }, + { + "url": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/mq1.webp", + "preference": -26 + }, + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/mq1.jpg", + "preference": -27 + }, + { + "url": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/mq2.webp", + "preference": -28 + }, + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/mq2.jpg", + "preference": -29 + }, + { + "url": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/mq3.webp", + "preference": -30 + }, + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/mq3.jpg", + "preference": -31 + }, + { + "url": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/1.webp", + "preference": -32 + }, + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/1.jpg", + "preference": -33 + }, + { + "url": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/2.webp", + "preference": -34 + }, + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/2.jpg", + "preference": -35 + }, + { + "url": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/3.webp", + "preference": -36 + }, + { + "url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/3.jpg", + "preference": -37 + } + ], + "thumbnail": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/maxresdefault.webp", + "description": "The official video for \u201cNever Gonna Give You Up\u201d by Rick Astley\n\n\u2018Hold Me In Your Arms\u2019 \u2013 deluxe blue vinyl, 2CD and digital deluxe out 12th May 2023 Pre-order here \u2013 https://rick-astley.lnk.to/HMIYA2023ID\n\n\u201cNever Gonna Give You Up\u201d was a global smash on its release in July 1987, topping the charts in 25 countries including Rick\u2019s native UK and the US Billboard Hot 100. It also won the Brit Award for Best single in 1988. Stock Aitken and Waterman wrote and produced the track which was the lead-off single and lead track from Rick\u2019s debut LP \u201cWhenever You Need Somebody\u201d. The album was itself a UK number one and would go on to sell over 15 million copies worldwide.\n\nThe legendary video was directed by Simon West \u2013 who later went on to make Hollywood blockbusters such as Con Air, Lara Croft \u2013 Tomb Raider and The Expendables 2. The video passed the 1bn YouTube views milestone on 28 July 2021.\n\nSubscribe to the official Rick Astley YouTube channel: https://RickAstley.lnk.to/YTSubID\n\nFollow Rick Astley:\nFacebook: https://RickAstley.lnk.to/FBFollowID \nTwitter: https://RickAstley.lnk.to/TwitterID \nInstagram: https://RickAstley.lnk.to/InstagramID \nWebsite: https://RickAstley.lnk.to/storeID \nTikTok: https://RickAstley.lnk.to/TikTokID\n\nListen to Rick Astley:\nSpotify: https://RickAstley.lnk.to/SpotifyID \nApple Music: https://RickAstley.lnk.to/AppleMusicID \nAmazon Music: https://RickAstley.lnk.to/AmazonMusicID \nDeezer: https://RickAstley.lnk.to/DeezerID \n\nLyrics:\nWe\u2019re no strangers to love\nYou know the rules and so do I\nA full commitment\u2019s what I\u2019m thinking of\nYou wouldn\u2019t get this from any other guy\n\nI just wanna tell you how I\u2019m feeling\nGotta make you understand\n\nNever gonna give you up\nNever gonna let you down\nNever gonna run around and desert you\nNever gonna make you cry\nNever gonna say goodbye\nNever gonna tell a lie and hurt you\n\nWe\u2019ve known each other for so long\nYour heart\u2019s been aching but you\u2019re too shy to say it\nInside we both know what\u2019s been going on\nWe know the game and we\u2019re gonna play it\n\nAnd if you ask me how I\u2019m feeling\nDon\u2019t tell me you\u2019re too blind to see\n\nNever gonna give you up\nNever gonna let you down\nNever gonna run around and desert you\nNever gonna make you cry\nNever gonna say goodbye\nNever gonna tell a lie and hurt you\n\n#RickAstley #NeverGonnaGiveYouUp #WheneverYouNeedSomebody #OfficialMusicVideo", + "channel_id": "UCuAXFkgsw1L7xaCfnd5JJOw", + "channel_url": "https://www.youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw", + "duration": 212, + "view_count": 1450567564, + "average_rating": null, + "age_limit": 0, + "webpage_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", + "categories": ["Music"], + "tags": [ + "rick astley", + "Never Gonna Give You Up", + "nggyu", + "never gonna give you up lyrics", + "rick rolled", + "Rick Roll", + "rick astley official", + "rickrolled", + "Fortnite song", + "Fortnite event", + "Fortnite dance", + "fortnite never gonna give you up", + "rick roll", + "rickrolling", + "rick rolling", + "never gonna give you up", + "80s music", + "rick astley new", + "animated video", + "rickroll", + "meme songs", + "never gonna give u up lyrics", + "Rick Astley 2022", + "never gonna let you down", + "animated", + "rick rolls 2022", + "never gonna give you up karaoke" + ], + "playable_in_embed": true, + "live_status": "not_live", + "release_timestamp": null, + "_format_sort_fields": [ + "quality", + "res", + "fps", + "hdr:12", + "source", + "vcodec:vp9.2", + "channels", + "acodec", + "lang", + "proto" + ], + "automatic_captions": {}, + "subtitles": {}, + "comment_count": 2300000, + "chapters": null, + "heatmap": [], + "like_count": 16869622, + "channel": "Rick Astley", + "channel_follower_count": 3890000, + "channel_is_verified": true, + "uploader": "Rick Astley", + "uploader_id": "@RickAstleyYT", + "uploader_url": "https://www.youtube.com/@RickAstleyYT", + "upload_date": "20091025", + "availability": "public", + "__post_extractor": null, + "original_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", + "webpage_url_basename": "watch", + "webpage_url_domain": "youtube.com", + "extractor": "youtube", + "extractor_key": "Youtube" +} diff --git a/tests/components/media_extractor/fixtures/youtube_empty_playlist.json b/tests/components/media_extractor/fixtures/youtube_empty_playlist.json new file mode 100644 index 00000000000..37f22693528 --- /dev/null +++ b/tests/components/media_extractor/fixtures/youtube_empty_playlist.json @@ -0,0 +1,49 @@ +{ + "id": "PLZ4DbyIWUwCq4V8bIEa8jm2ozHZVuREJO", + "title": "Very important videos", + "availability": "public", + "channel_follower_count": null, + "description": "Not original", + "tags": [], + "thumbnails": [ + { + "url": "https://i.ytimg.com/vi/q6EoRBvdVPQ/hqdefault.jpg?sqp=-oaymwEwCKgBEF5IWvKriqkDIwgBFQAAiEIYAfABAfgB3gOAAugCigIMCAAQARg8IGUoPzAP&rs=AOn4CLDBCH5IQ0obogxXhAzIH8pE0d7r1Q", + "height": 94, + "width": 168 + }, + { + "url": "https://i.ytimg.com/vi/q6EoRBvdVPQ/hqdefault.jpg?sqp=-oaymwEwCMQBEG5IWvKriqkDIwgBFQAAiEIYAfABAfgB3gOAAugCigIMCAAQARg8IGUoPzAP&rs=AOn4CLAybhgn-CoPMjBE-0VfBDqvy0jyOQ", + "height": 110, + "width": 196 + }, + { + "url": "https://i.ytimg.com/vi/q6EoRBvdVPQ/hqdefault.jpg?sqp=-oaymwExCPYBEIoBSFryq4qpAyMIARUAAIhCGAHwAQH4Ad4DgALoAooCDAgAEAEYPCBlKD8wDw==&rs=AOn4CLDutIdjr5zTE9G78eWf83-mGXYnUA", + "height": 138, + "width": 246 + }, + { + "url": "https://i.ytimg.com/vi/q6EoRBvdVPQ/hqdefault.jpg?sqp=-oaymwExCNACELwBSFryq4qpAyMIARUAAIhCGAHwAQH4Ad4DgALoAooCDAgAEAEYPCBlKD8wDw==&rs=AOn4CLD2884fHuvAv8ysHA48LD3uArB6bA", + "height": 188, + "width": 336 + } + ], + "modified_date": "20230813", + "view_count": 5770834, + "playlist_count": 3, + "channel": "ZulTarx", + "channel_id": "UChOLuQpsxxmJiJUeSU2tSTw", + "uploader_id": "@Armand314", + "uploader": "ZulTarx", + "channel_url": "https://www.youtube.com/channel/UChOLuQpsxxmJiJUeSU2tSTw", + "uploader_url": "https://www.youtube.com/@Armand314", + "_type": "playlist", + "entries": [], + "extractor_key": "YoutubeTab", + "extractor": "youtube:tab", + "webpage_url": "https://www.youtube.com/playlist?list=PLZ4DbyIWUwCq4V8bIEa8jm2ozHZVuREJO", + "original_url": "https://www.youtube.com/playlist?list=PLZ4DbyIWUwCq4V8bIEa8jm2ozHZVuREJO", + "webpage_url_basename": "playlist", + "webpage_url_domain": "youtube.com", + "heatmap": [], + "automatic_captions": {} +} diff --git a/tests/components/media_extractor/fixtures/youtube_playlist.json b/tests/components/media_extractor/fixtures/youtube_playlist.json new file mode 100644 index 00000000000..053b243a1be --- /dev/null +++ b/tests/components/media_extractor/fixtures/youtube_playlist.json @@ -0,0 +1,179 @@ +{ + "id": "PLZ4DbyIWUwCq4V8bIEa8jm2ozHZVuREJP", + "title": "Very important videos", + "availability": "public", + "channel_follower_count": null, + "description": "Not original", + "tags": [], + "thumbnails": [ + { + "url": "https://i.ytimg.com/vi/q6EoRBvdVPQ/hqdefault.jpg?sqp=-oaymwEwCKgBEF5IWvKriqkDIwgBFQAAiEIYAfABAfgB3gOAAugCigIMCAAQARg8IGUoPzAP&rs=AOn4CLDBCH5IQ0obogxXhAzIH8pE0d7r1Q", + "height": 94, + "width": 168 + }, + { + "url": "https://i.ytimg.com/vi/q6EoRBvdVPQ/hqdefault.jpg?sqp=-oaymwEwCMQBEG5IWvKriqkDIwgBFQAAiEIYAfABAfgB3gOAAugCigIMCAAQARg8IGUoPzAP&rs=AOn4CLAybhgn-CoPMjBE-0VfBDqvy0jyOQ", + "height": 110, + "width": 196 + }, + { + "url": "https://i.ytimg.com/vi/q6EoRBvdVPQ/hqdefault.jpg?sqp=-oaymwExCPYBEIoBSFryq4qpAyMIARUAAIhCGAHwAQH4Ad4DgALoAooCDAgAEAEYPCBlKD8wDw==&rs=AOn4CLDutIdjr5zTE9G78eWf83-mGXYnUA", + "height": 138, + "width": 246 + }, + { + "url": "https://i.ytimg.com/vi/q6EoRBvdVPQ/hqdefault.jpg?sqp=-oaymwExCNACELwBSFryq4qpAyMIARUAAIhCGAHwAQH4Ad4DgALoAooCDAgAEAEYPCBlKD8wDw==&rs=AOn4CLD2884fHuvAv8ysHA48LD3uArB6bA", + "height": 188, + "width": 336 + } + ], + "modified_date": "20230813", + "view_count": 5770834, + "playlist_count": 3, + "channel": "ZulTarx", + "channel_id": "UChOLuQpsxxmJiJUeSU2tSTw", + "uploader_id": "@Armand314", + "uploader": "ZulTarx", + "channel_url": "https://www.youtube.com/channel/UChOLuQpsxxmJiJUeSU2tSTw", + "uploader_url": "https://www.youtube.com/@Armand314", + "_type": "playlist", + "entries": [ + { + "_type": "url", + "ie_key": "Youtube", + "id": "q6EoRBvdVPQ", + "url": "https://www.youtube.com/watch?v=q6EoRBvdVPQ", + "title": "Yee", + "description": null, + "duration": 10, + "channel_id": "UC-fD_qwTEQQ1L-MUWx_mNvg", + "channel": "revergo", + "channel_url": "https://www.youtube.com/channel/UC-fD_qwTEQQ1L-MUWx_mNvg", + "uploader": "revergo", + "uploader_id": "@revergo", + "uploader_url": "https://www.youtube.com/@revergo", + "thumbnails": [ + { + "url": "https://i.ytimg.com/vi/q6EoRBvdVPQ/hqdefault.jpg?sqp=-oaymwE1CKgBEF5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AHeA4AC6AKKAgwIABABGDwgZSg_MA8=&rs=AOn4CLAJYg16HMBdEsv9lYBJyNqA5G3anQ", + "height": 94, + "width": 168 + }, + { + "url": "https://i.ytimg.com/vi/q6EoRBvdVPQ/hqdefault.jpg?sqp=-oaymwE1CMQBEG5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AHeA4AC6AKKAgwIABABGDwgZSg_MA8=&rs=AOn4CLAgCNP9UuQas-D59hHHM-RqkUvA6g", + "height": 110, + "width": 196 + }, + { + "url": "https://i.ytimg.com/vi/q6EoRBvdVPQ/hqdefault.jpg?sqp=-oaymwE2CPYBEIoBSFXyq4qpAygIARUAAIhCGAFwAcABBvABAfgB3gOAAugCigIMCAAQARg8IGUoPzAP&rs=AOn4CLCTWaY5897XxhcpRyVtGQQNuMHfTg", + "height": 138, + "width": 246 + }, + { + "url": "https://i.ytimg.com/vi/q6EoRBvdVPQ/hqdefault.jpg?sqp=-oaymwE2CNACELwBSFXyq4qpAygIARUAAIhCGAFwAcABBvABAfgB3gOAAugCigIMCAAQARg8IGUoPzAP&rs=AOn4CLCeS6NC75yTYvyP4DsehZ3oXNuxMQ", + "height": 188, + "width": 336 + } + ], + "timestamp": null, + "release_timestamp": null, + "availability": null, + "view_count": 96000000, + "live_status": null, + "channel_is_verified": null + }, + { + "_type": "url", + "ie_key": "Youtube", + "id": "8YWl7tDGUPA", + "url": "https://www.youtube.com/watch?v=8YWl7tDGUPA", + "title": "color red", + "description": null, + "duration": 17, + "channel_id": "UCbYMTn6xKV0IKshL4pRCV3g", + "channel": "Alex Jimenez", + "channel_url": "https://www.youtube.com/channel/UCbYMTn6xKV0IKshL4pRCV3g", + "uploader": "Alex Jimenez", + "uploader_id": "@alexjimenez1237", + "uploader_url": "https://www.youtube.com/@alexjimenez1237", + "thumbnails": [ + { + "url": "https://i.ytimg.com/vi/8YWl7tDGUPA/hqdefault.jpg?sqp=-oaymwE1CKgBEF5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AG2BIACwAKKAgwIABABGGUgXShUMA8=&rs=AOn4CLBqzngIx-4i_HFvqloetUfeN8yrYw", + "height": 94, + "width": 168 + }, + { + "url": "https://i.ytimg.com/vi/8YWl7tDGUPA/hqdefault.jpg?sqp=-oaymwE1CMQBEG5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AG2BIACwAKKAgwIABABGGUgXShUMA8=&rs=AOn4CLB7mWPQmdL2QBLxTHhrgbFj2jFaCg", + "height": 110, + "width": 196 + }, + { + "url": "https://i.ytimg.com/vi/8YWl7tDGUPA/hqdefault.jpg?sqp=-oaymwE2CPYBEIoBSFXyq4qpAygIARUAAIhCGAFwAcABBvABAfgBtgSAAsACigIMCAAQARhlIF0oVDAP&rs=AOn4CLA9YAIO3g_DnClsuc5LjMQn4O9ZQQ", + "height": 138, + "width": 246 + }, + { + "url": "https://i.ytimg.com/vi/8YWl7tDGUPA/hqdefault.jpg?sqp=-oaymwE2CNACELwBSFXyq4qpAygIARUAAIhCGAFwAcABBvABAfgBtgSAAsACigIMCAAQARhlIF0oVDAP&rs=AOn4CLDPHY6aG08hlTJMlc-LJt9ywtpWEg", + "height": 188, + "width": 336 + } + ], + "timestamp": null, + "release_timestamp": null, + "availability": null, + "view_count": 30000000, + "live_status": null, + "channel_is_verified": null + }, + { + "_type": "url", + "ie_key": "Youtube", + "id": "6bnanI9jXps", + "url": "https://www.youtube.com/watch?v=6bnanI9jXps", + "title": "Terrible Mall Commercial", + "description": null, + "duration": 31, + "channel_id": "UCLmnB20wsih9F5N0o5K0tig", + "channel": "quantim", + "channel_url": "https://www.youtube.com/channel/UCLmnB20wsih9F5N0o5K0tig", + "uploader": "quantim", + "uploader_id": "@Potatoflesh", + "uploader_url": "https://www.youtube.com/@Potatoflesh", + "thumbnails": [ + { + "url": "https://i.ytimg.com/vi/6bnanI9jXps/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAsyI0ZJA9STG8vlSdRkKk55ls5Dg", + "height": 94, + "width": 168 + }, + { + "url": "https://i.ytimg.com/vi/6bnanI9jXps/hqdefault.jpg?sqp=-oaymwEbCMQBEG5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLD2bZ9S8AB4UGsZlx_8TjBoL72enA", + "height": 110, + "width": 196 + }, + { + "url": "https://i.ytimg.com/vi/6bnanI9jXps/hqdefault.jpg?sqp=-oaymwEcCPYBEIoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLCKNlgvl_7lKoFq8vyDYZRtTs4woA", + "height": 138, + "width": 246 + }, + { + "url": "https://i.ytimg.com/vi/6bnanI9jXps/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBeZv8F8IyICmKD9qjo9pTMJmM8ug", + "height": 188, + "width": 336 + } + ], + "timestamp": null, + "release_timestamp": null, + "availability": null, + "view_count": 26000000, + "live_status": null, + "channel_is_verified": null + } + ], + "extractor_key": "YoutubeTab", + "extractor": "youtube:tab", + "webpage_url": "https://www.youtube.com/playlist?list=PLZ4DbyIWUwCq4V8bIEa8jm2ozHZVuREJP", + "original_url": "https://www.youtube.com/playlist?list=PLZ4DbyIWUwCq4V8bIEa8jm2ozHZVuREJP", + "webpage_url_basename": "playlist", + "webpage_url_domain": "youtube.com", + "heatmap": [], + "automatic_captions": {} +} diff --git a/tests/components/media_extractor/snapshots/test_init.ambr b/tests/components/media_extractor/snapshots/test_init.ambr index d70c370b60c..ed56f40af73 100644 --- a/tests/components/media_extractor/snapshots/test_init.ambr +++ b/tests/components/media_extractor/snapshots/test_init.ambr @@ -1,4 +1,24 @@ # serializer version: 1 +# name: test_extract_media_service[https://soundcloud.com/bruttoband/brutto-11] + dict({ + 'url': 'https://cf-hls-opus-media.sndcdn.com/playlist/50remGX1OqRY.64.opus/playlist.m3u8?Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiKjovL2NmLWhscy1vcHVzLW1lZGlhLnNuZGNkbi5jb20vcGxheWxpc3QvNTByZW1HWDFPcVJZLjY0Lm9wdXMvcGxheWxpc3QubTN1OCoiLCJDb25kaXRpb24iOnsiRGF0ZUxlc3NUaGFuIjp7IkFXUzpFcG9jaFRpbWUiOjE2OTU1MDM5NjR9fX1dfQ__&Signature=HqyeKoK4gx1Bd3jId5v1-9ltDY2SN7fzGp6M7tW3kWluv8Ns0SgANPKG3~Tzo8hUrQzAYVvwMbO2F75b6NBeBr4xx0SRCeKotZdArzBFT4Wtrz4HtEddLyPjp12vWYCDuOshd1sTjrvaBAd9TAFTZuwyal1OKpLMsqK0QN-KFH-5GsmLDZTPWcoVkMqC7XBmNWTq0G1mtVeP57TN~9T7qEYqRLFIDXdm2HKSPSR4BB2gjaZUK22pBUPSGVr-ziBSpNVHvNISy2QdObjS5zjuAe8bl-npQ3PlFatYECDI3Gc~wjCNIJPdTpCbHUsX36SSR4dnKlgW1nYGx~eED7dppA__&Key-Pair-Id=APKAI6TU7MMXM5DG6EPQ', + }) +# --- +# name: test_extract_media_service[https://test.com/abc] + dict({ + 'url': 'https://cf-media.sndcdn.com/50remGX1OqRY.128.mp3?Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiKjovL2NmLW1lZGlhLnNuZGNkbi5jb20vNTByZW1HWDFPcVJZLjEyOC5tcDMqIiwiQ29uZGl0aW9uIjp7IkRhdGVMZXNzVGhhbiI6eyJBV1M6RXBvY2hUaW1lIjoxNjk0Nzk4NTkzfX19XX0_&Signature=flALJvEBnzS0ZOOhf0-07Ap~NURw2Gn2OqkeKKTTMX5HRGJw9eXFay79tcC4GsMMXWUgWoCx-n3yelpyilE2MOEIufBNUbjqRfMSJaX5YhYxjQdoDYuiU~gqBzJyPw9pKzr6P8~5HNKL3Idr0CNhUzdV6FQLaUPKMMibq9ghV833mUmdyvdk1~GZBc8MOg9GrTdcigGgpPzd-vrIMICMvFzFnwBOeOotxX2Vfqf9~wVekBKGlvB9A~7TlZ71lv9Fl9u4m8rse9E-mByweVc1M784ehJV3~tRPjuF~FXXWKP8x0nGJmoq7RAnG7iFIt~fQFmsfOq2o~PG7dHMRPh7hw__&Key-Pair-Id=APKAI6TU7MMXM5DG6EPQ', + }) +# --- +# name: test_extract_media_service[https://www.youtube.com/playlist?list=PLZ4DbyIWUwCq4V8bIEa8jm2ozHZVuREJP] + dict({ + 'url': 'https://www.youtube.com/watch?v=q6EoRBvdVPQ', + }) +# --- +# name: test_extract_media_service[https://www.youtube.com/watch?v=dQw4w9WgXcQ] + dict({ + 'url': 'https://rr2---sn-5hnekn7k.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZeSINMKn6dsPt6mr8Ac&ip=45.93.75.130&id=o-AA7xSUDqvzXPAn6m_Jd8d0jhyk4er7c5zjbyZCHyREaQ&itag=22&source=youtube&requiressl=yes&mh=7c&mm=31%2C29&mn=sn-5hnekn7k%2Csn-5hne6nzy&ms=au%2Crdu&mv=m&mvi=2&pl=22&initcwndbps=1311250&spc=UWF9f6JvGMIlJJFOWUL6L4bH2pl28C4&vprv=1&svpuc=1&mime=video%2Fmp4&cnr=14&ratebypass=yes&dur=212.091&lmt=1694045086815467&mt=1695502401&fvip=3&fexp=24007246&beids=24350018&c=ANDROID&txp=4532434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cmime%2Ccnr%2Cratebypass%2Cdur%2Clmt&sig=AOq0QJ8wRAIgHVwajP0J2fiJ1ERoAonpxghXGzDmEXh3rvJ399UEMWECIFdBjiVUOk7QdiFBxQ4QqojJd8p_PfL25TV_8TBrp_Kb&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhANJRDHqeYwWk4W8XXGlTNmYA3OOiuzK3PEWtVpK4NimlAiEAwK0nj8vGtcP0IJMZVBGh8SC5KRFzPfx_yJn41ZOUGyQ%3D', + }) +# --- # name: test_no_target_entity ReadOnlyDict({ 'device_id': list([ diff --git a/tests/components/media_extractor/test_init.py b/tests/components/media_extractor/test_init.py index e47f0ae1470..388ea3be1fd 100644 --- a/tests/components/media_extractor/test_init.py +++ b/tests/components/media_extractor/test_init.py @@ -9,9 +9,14 @@ import pytest from syrupy import SnapshotAssertion from yt_dlp import DownloadError -from homeassistant.components.media_extractor import DOMAIN +from homeassistant.components.media_extractor.const import ( + ATTR_URL, + DOMAIN, + SERVICE_EXTRACT_MEDIA_URL, +) from homeassistant.components.media_player import SERVICE_PLAY_MEDIA from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component from tests.common import load_json_object_fixture @@ -30,6 +35,58 @@ async def test_play_media_service_is_registered(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert hass.services.has_service(DOMAIN, SERVICE_PLAY_MEDIA) + assert hass.services.has_service(DOMAIN, SERVICE_EXTRACT_MEDIA_URL) + + +@pytest.mark.parametrize( + "url", + [ + YOUTUBE_VIDEO, + SOUNDCLOUD_TRACK, + NO_FORMATS_RESPONSE, + YOUTUBE_PLAYLIST, + ], +) +async def test_extract_media_service( + hass: HomeAssistant, + mock_youtube_dl: MockYoutubeDL, + snapshot: SnapshotAssertion, + empty_media_extractor_config: dict[str, Any], + url: str, +) -> None: + """Test play media service is registered.""" + await async_setup_component(hass, DOMAIN, empty_media_extractor_config) + await hass.async_block_till_done() + + assert ( + await hass.services.async_call( + DOMAIN, + SERVICE_EXTRACT_MEDIA_URL, + {ATTR_URL: url}, + blocking=True, + return_response=True, + ) + == snapshot + ) + + +async def test_extracting_playlist_no_entries( + hass: HomeAssistant, + mock_youtube_dl: MockYoutubeDL, + empty_media_extractor_config: dict[str, Any], +) -> None: + """Test extracting a playlist without entries.""" + + await async_setup_component(hass, DOMAIN, empty_media_extractor_config) + await hass.async_block_till_done() + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + DOMAIN, + SERVICE_EXTRACT_MEDIA_URL, + {ATTR_URL: YOUTUBE_EMPTY_PLAYLIST}, + blocking=True, + return_response=True, + ) @pytest.mark.parametrize( From 18ac9a7ba5d26a49785066e8fd581659a045208c Mon Sep 17 00:00:00 2001 From: myMartek Date: Tue, 16 Apr 2024 16:16:32 +0200 Subject: [PATCH 602/967] Add select hold to AppleTVs remote entity as possible command (#105764) * Fixed home hold and added select hold * Fixed home hold and added select hold * Removed select_hold for now * Fixed wrong import block sorting * Fixed unit tests for AppleTV * Added select hold command to AppleTV integration * Removed home_hold and added hold_secs option for remote commands * Added DEFAULT_HOLD_SECS --------- Co-authored-by: Erik Montnemery --- homeassistant/components/apple_tv/remote.py | 13 +++++++-- tests/components/apple_tv/test_remote.py | 32 ++++++++++++++------- 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/apple_tv/remote.py b/homeassistant/components/apple_tv/remote.py index 822a9c3306a..aed2c0ae3f0 100644 --- a/homeassistant/components/apple_tv/remote.py +++ b/homeassistant/components/apple_tv/remote.py @@ -5,10 +5,14 @@ from collections.abc import Iterable import logging from typing import Any +from pyatv.const import InputAction + from homeassistant.components.remote import ( ATTR_DELAY_SECS, + ATTR_HOLD_SECS, ATTR_NUM_REPEATS, DEFAULT_DELAY_SECS, + DEFAULT_HOLD_SECS, RemoteEntity, ) from homeassistant.config_entries import ConfigEntry @@ -29,7 +33,6 @@ COMMAND_TO_ATTRIBUTE = { "turn_off": ("power", "turn_off"), "volume_up": ("audio", "volume_up"), "volume_down": ("audio", "volume_down"), - "home_hold": ("remote_control", "home"), } @@ -66,6 +69,7 @@ class AppleTVRemote(AppleTVEntity, RemoteEntity): """Send a command to one device.""" num_repeats = kwargs[ATTR_NUM_REPEATS] delay = kwargs.get(ATTR_DELAY_SECS, DEFAULT_DELAY_SECS) + hold_secs = kwargs.get(ATTR_HOLD_SECS, DEFAULT_HOLD_SECS) if not self.atv: _LOGGER.error("Unable to send commands, not connected to %s", self.name) @@ -84,5 +88,10 @@ class AppleTVRemote(AppleTVEntity, RemoteEntity): raise ValueError("Command not found. Exiting sequence") _LOGGER.info("Sending command %s", single_command) - await attr_value() + + if hold_secs >= 1: + await attr_value(action=InputAction.Hold) + else: + await attr_value() + await asyncio.sleep(delay) diff --git a/tests/components/apple_tv/test_remote.py b/tests/components/apple_tv/test_remote.py index f831518d75a..bc8a0e6a2dd 100644 --- a/tests/components/apple_tv/test_remote.py +++ b/tests/components/apple_tv/test_remote.py @@ -5,25 +5,37 @@ from unittest.mock import AsyncMock import pytest from homeassistant.components.apple_tv.remote import AppleTVRemote -from homeassistant.components.remote import ATTR_DELAY_SECS, ATTR_NUM_REPEATS +from homeassistant.components.remote import ( + ATTR_DELAY_SECS, + ATTR_HOLD_SECS, + ATTR_NUM_REPEATS, +) @pytest.mark.parametrize( - ("command", "method"), + ("command", "method", "hold_secs"), [ - ("up", "remote_control.up"), - ("wakeup", "power.turn_on"), - ("volume_up", "audio.volume_up"), - ("home_hold", "remote_control.home"), + ("up", "remote_control.up", 0.0), + ("wakeup", "power.turn_on", 0.0), + ("volume_up", "audio.volume_up", 0.0), + ("home", "remote_control.home", 1.0), + ("select", "remote_control.select", 1.0), ], - ids=["up", "wakeup", "volume_up", "home_hold"], + ids=["up", "wakeup", "volume_up", "home", "select"], ) -async def test_send_command(command: str, method: str) -> None: +async def test_send_command(command: str, method: str, hold_secs: float) -> None: """Test "send_command" method.""" remote = AppleTVRemote("test", "test", None) remote.atv = AsyncMock() await remote.async_send_command( - [command], **{ATTR_NUM_REPEATS: 1, ATTR_DELAY_SECS: 0} + [command], + **{ATTR_NUM_REPEATS: 1, ATTR_DELAY_SECS: 0, ATTR_HOLD_SECS: hold_secs}, ) assert len(remote.atv.method_calls) == 1 - assert str(remote.atv.method_calls[0]) == f"call.{method}()" + if hold_secs >= 1: + assert ( + str(remote.atv.method_calls[0]) + == f"call.{method}(action=)" + ) + else: + assert str(remote.atv.method_calls[0]) == f"call.{method}()" From 63c9aef71dd110e3588701314725372087147282 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Tue, 16 Apr 2024 16:28:25 +0200 Subject: [PATCH 603/967] Correct spelling of "Wi-Fi" in devolo_home_network (#106167) * Use Wi-Fi in devolo_home_network * Recreate snapshots --------- Co-authored-by: Erik Montnemery --- .../devolo_home_network/strings.json | 8 +- .../snapshots/test_button.ambr | 172 ------------------ .../snapshots/test_image.ambr | 4 +- .../snapshots/test_sensor.ambr | 24 +-- .../snapshots/test_switch.ambr | 94 +--------- .../devolo_home_network/test_image.py | 6 +- .../devolo_home_network/test_sensor.py | 10 +- .../devolo_home_network/test_switch.py | 8 +- 8 files changed, 35 insertions(+), 291 deletions(-) diff --git a/homeassistant/components/devolo_home_network/strings.json b/homeassistant/components/devolo_home_network/strings.json index 9d86b127d77..97348c5c43c 100644 --- a/homeassistant/components/devolo_home_network/strings.json +++ b/homeassistant/components/devolo_home_network/strings.json @@ -50,7 +50,7 @@ }, "image": { "image_guest_wifi": { - "name": "Guest Wifi credentials as QR code" + "name": "Guest Wi-Fi credentials as QR code" } }, "sensor": { @@ -58,10 +58,10 @@ "name": "Connected PLC devices" }, "connected_wifi_clients": { - "name": "Connected Wifi clients" + "name": "Connected Wi-Fi clients" }, "neighboring_wifi_networks": { - "name": "Neighboring Wifi networks" + "name": "Neighboring Wi-Fi networks" }, "plc_rx_rate": { "name": "PLC downlink PHY rate" @@ -72,7 +72,7 @@ }, "switch": { "switch_guest_wifi": { - "name": "Enable guest Wifi" + "name": "Enable guest Wi-Fi" }, "switch_leds": { "name": "Enable LEDs" diff --git a/tests/components/devolo_home_network/snapshots/test_button.ambr b/tests/components/devolo_home_network/snapshots/test_button.ambr index 3e8e4ae2bb3..126ac4e7cdb 100644 --- a/tests/components/devolo_home_network/snapshots/test_button.ambr +++ b/tests/components/devolo_home_network/snapshots/test_button.ambr @@ -1,47 +1,4 @@ # serializer version: 1 -# name: test_button[identify_device_with_a_blinking_led-async_identify_device_start] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Mock Title Identify device with a blinking LED', - 'icon': 'mdi:led-on', - }), - 'context': , - 'entity_id': 'button.mock_title_identify_device_with_a_blinking_led', - 'last_changed': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_button[identify_device_with_a_blinking_led-async_identify_device_start].1 - 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.mock_title_identify_device_with_a_blinking_led', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:led-on', - 'original_name': 'Identify device with a blinking LED', - 'platform': 'devolo_home_network', - 'supported_features': 0, - 'translation_key': 'identify', - 'unique_id': '1234567890_identify', - 'unit_of_measurement': None, - }) -# --- # name: test_button[identify_device_with_a_blinking_led-plcnet-async_identify_device_start] StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -89,49 +46,6 @@ 'unit_of_measurement': None, }) # --- -# name: test_button[restart_device-async_restart] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'restart', - 'friendly_name': 'Mock Title Restart device', - }), - 'context': , - 'entity_id': 'button.mock_title_restart_device', - 'last_changed': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_button[restart_device-async_restart].1 - 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.mock_title_restart_device', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Restart device', - 'platform': 'devolo_home_network', - 'supported_features': 0, - 'translation_key': 'restart', - 'unique_id': '1234567890_restart', - 'unit_of_measurement': None, - }) -# --- # name: test_button[restart_device-device-async_restart] StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -179,49 +93,6 @@ 'unit_of_measurement': None, }) # --- -# name: test_button[start_plc_pairing-async_pair_device] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Mock Title Start PLC pairing', - 'icon': 'mdi:plus-network-outline', - }), - 'context': , - 'entity_id': 'button.mock_title_start_plc_pairing', - 'last_changed': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_button[start_plc_pairing-async_pair_device].1 - 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.mock_title_start_plc_pairing', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:plus-network-outline', - 'original_name': 'Start PLC pairing', - 'platform': 'devolo_home_network', - 'supported_features': 0, - 'translation_key': 'pairing', - 'unique_id': '1234567890_pairing', - 'unit_of_measurement': None, - }) -# --- # name: test_button[start_plc_pairing-plcnet-async_pair_device] StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -268,49 +139,6 @@ 'unit_of_measurement': None, }) # --- -# name: test_button[start_wps-async_start_wps] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Mock Title Start WPS', - 'icon': 'mdi:wifi-plus', - }), - 'context': , - 'entity_id': 'button.mock_title_start_wps', - 'last_changed': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_button[start_wps-async_start_wps].1 - 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.mock_title_start_wps', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:wifi-plus', - 'original_name': 'Start WPS', - 'platform': 'devolo_home_network', - 'supported_features': 0, - 'translation_key': 'start_wps', - 'unique_id': '1234567890_start_wps', - 'unit_of_measurement': None, - }) -# --- # name: test_button[start_wps-device-async_start_wps] StateSnapshot({ 'attributes': ReadOnlyDict({ diff --git a/tests/components/devolo_home_network/snapshots/test_image.ambr b/tests/components/devolo_home_network/snapshots/test_image.ambr index ad8ccf43c55..b3924a508cf 100644 --- a/tests/components/devolo_home_network/snapshots/test_image.ambr +++ b/tests/components/devolo_home_network/snapshots/test_image.ambr @@ -11,7 +11,7 @@ 'disabled_by': None, 'domain': 'image', 'entity_category': , - 'entity_id': 'image.mock_title_guest_wifi_credentials_as_qr_code', + 'entity_id': 'image.mock_title_guest_wi_fi_credentials_as_qr_code', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -23,7 +23,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Guest Wifi credentials as QR code', + 'original_name': 'Guest Wi-Fi credentials as QR code', 'platform': 'devolo_home_network', 'previous_unique_id': None, 'supported_features': 0, diff --git a/tests/components/devolo_home_network/snapshots/test_sensor.ambr b/tests/components/devolo_home_network/snapshots/test_sensor.ambr index fc173da8294..d985ac35495 100644 --- a/tests/components/devolo_home_network/snapshots/test_sensor.ambr +++ b/tests/components/devolo_home_network/snapshots/test_sensor.ambr @@ -45,21 +45,21 @@ 'unit_of_measurement': None, }) # --- -# name: test_sensor[connected_wifi_clients-async_get_wifi_connected_station-interval0] +# name: test_sensor[connected_wi_fi_clients-async_get_wifi_connected_station-interval0] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Mock Title Connected Wifi clients', + 'friendly_name': 'Mock Title Connected Wi-Fi clients', 'state_class': , }), 'context': , - 'entity_id': 'sensor.mock_title_connected_wifi_clients', + 'entity_id': 'sensor.mock_title_connected_wi_fi_clients', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '1', }) # --- -# name: test_sensor[connected_wifi_clients-async_get_wifi_connected_station-interval0].1 +# name: test_sensor[connected_wi_fi_clients-async_get_wifi_connected_station-interval0].1 EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -73,7 +73,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.mock_title_connected_wifi_clients', + 'entity_id': 'sensor.mock_title_connected_wi_fi_clients', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -85,7 +85,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Connected Wifi clients', + 'original_name': 'Connected Wi-Fi clients', 'platform': 'devolo_home_network', 'previous_unique_id': None, 'supported_features': 0, @@ -94,20 +94,20 @@ 'unit_of_measurement': None, }) # --- -# name: test_sensor[neighboring_wifi_networks-async_get_wifi_neighbor_access_points-interval1] +# name: test_sensor[neighboring_wi_fi_networks-async_get_wifi_neighbor_access_points-interval1] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Mock Title Neighboring Wifi networks', + 'friendly_name': 'Mock Title Neighboring Wi-Fi networks', }), 'context': , - 'entity_id': 'sensor.mock_title_neighboring_wifi_networks', + 'entity_id': 'sensor.mock_title_neighboring_wi_fi_networks', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '1', }) # --- -# name: test_sensor[neighboring_wifi_networks-async_get_wifi_neighbor_access_points-interval1].1 +# name: test_sensor[neighboring_wi_fi_networks-async_get_wifi_neighbor_access_points-interval1].1 EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -119,7 +119,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.mock_title_neighboring_wifi_networks', + 'entity_id': 'sensor.mock_title_neighboring_wi_fi_networks', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -131,7 +131,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Neighboring Wifi networks', + 'original_name': 'Neighboring Wi-Fi networks', 'platform': 'devolo_home_network', 'previous_unique_id': None, 'supported_features': 0, diff --git a/tests/components/devolo_home_network/snapshots/test_switch.ambr b/tests/components/devolo_home_network/snapshots/test_switch.ambr index 09b56efc784..a2df5d2579f 100644 --- a/tests/components/devolo_home_network/snapshots/test_switch.ambr +++ b/tests/components/devolo_home_network/snapshots/test_switch.ambr @@ -1,97 +1,11 @@ # serializer version: 1 -# name: test_switches[enable_guest_wifi-async_get_wifi_guest_access-async_set_wifi_guest_access-interval0] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Mock Title Enable guest Wifi', - 'icon': 'mdi:wifi', - }), - 'context': , - 'entity_id': 'switch.mock_title_enable_guest_wifi', - 'last_changed': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_switches[enable_guest_wifi-async_get_wifi_guest_access-async_set_wifi_guest_access-interval0].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.mock_title_enable_guest_wifi', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:wifi', - 'original_name': 'Enable guest Wifi', - 'platform': 'devolo_home_network', - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '1234567890_switch_guest_wifi', - 'unit_of_measurement': None, - }) -# --- -# name: test_switches[enable_leds-async_get_led_setting-async_set_led_setting-interval1] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Mock Title Enable LEDs', - 'icon': 'mdi:led-off', - }), - 'context': , - 'entity_id': 'switch.mock_title_enable_leds', - 'last_changed': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_switches[enable_leds-async_get_led_setting-async_set_led_setting-interval1].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.mock_title_enable_leds', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:led-off', - 'original_name': 'Enable LEDs', - 'platform': 'devolo_home_network', - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '1234567890_switch_leds', - 'unit_of_measurement': None, - }) -# --- # name: test_update_enable_guest_wifi StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Mock Title Enable guest Wifi', + 'friendly_name': 'Mock Title Enable guest Wi-Fi', }), 'context': , - 'entity_id': 'switch.mock_title_enable_guest_wifi', + 'entity_id': 'switch.mock_title_enable_guest_wi_fi', 'last_changed': , 'last_reported': , 'last_updated': , @@ -110,7 +24,7 @@ 'disabled_by': None, 'domain': 'switch', 'entity_category': None, - 'entity_id': 'switch.mock_title_enable_guest_wifi', + 'entity_id': 'switch.mock_title_enable_guest_wi_fi', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -122,7 +36,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Enable guest Wifi', + 'original_name': 'Enable guest Wi-Fi', 'platform': 'devolo_home_network', 'previous_unique_id': None, 'supported_features': 0, diff --git a/tests/components/devolo_home_network/test_image.py b/tests/components/devolo_home_network/test_image.py index 0ca3936e1ac..80efc4fcc09 100644 --- a/tests/components/devolo_home_network/test_image.py +++ b/tests/components/devolo_home_network/test_image.py @@ -32,7 +32,7 @@ async def test_image_setup(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert ( - hass.states.get(f"{DOMAIN}.{device_name}_guest_wifi_credentials_as_qr_code") + hass.states.get(f"{DOMAIN}.{device_name}_guest_wi_fi_credentials_as_qr_code") is not None ) @@ -51,13 +51,13 @@ async def test_guest_wifi_qr( """Test showing a QR code of the guest wifi credentials.""" entry = configure_integration(hass) device_name = entry.title.replace(" ", "_").lower() - state_key = f"{DOMAIN}.{device_name}_guest_wifi_credentials_as_qr_code" + state_key = f"{DOMAIN}.{device_name}_guest_wi_fi_credentials_as_qr_code" await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() state = hass.states.get(state_key) - assert state.name == "Mock Title Guest Wifi credentials as QR code" + assert state.name == "Mock Title Guest Wi-Fi credentials as QR code" assert state.state == dt_util.utcnow().isoformat() assert entity_registry.async_get(state_key) == snapshot diff --git a/tests/components/devolo_home_network/test_sensor.py b/tests/components/devolo_home_network/test_sensor.py index 5b5e05a40d1..efcbaa803df 100644 --- a/tests/components/devolo_home_network/test_sensor.py +++ b/tests/components/devolo_home_network/test_sensor.py @@ -32,9 +32,11 @@ async def test_sensor_setup(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert hass.states.get(f"{DOMAIN}.{device_name}_connected_wifi_clients") is not None + assert ( + hass.states.get(f"{DOMAIN}.{device_name}_connected_wi_fi_clients") is not None + ) assert hass.states.get(f"{DOMAIN}.{device_name}_connected_plc_devices") is None - assert hass.states.get(f"{DOMAIN}.{device_name}_neighboring_wifi_networks") is None + assert hass.states.get(f"{DOMAIN}.{device_name}_neighboring_wi_fi_networks") is None assert ( hass.states.get( f"{DOMAIN}.{device_name}_plc_downlink_phy_rate_{PLCNET.devices[1].user_device_name}" @@ -67,12 +69,12 @@ async def test_sensor_setup(hass: HomeAssistant) -> None: ("name", "get_method", "interval"), [ ( - "connected_wifi_clients", + "connected_wi_fi_clients", "async_get_wifi_connected_station", SHORT_UPDATE_INTERVAL, ), ( - "neighboring_wifi_networks", + "neighboring_wi_fi_networks", "async_get_wifi_neighbor_access_points", LONG_UPDATE_INTERVAL, ), diff --git a/tests/components/devolo_home_network/test_switch.py b/tests/components/devolo_home_network/test_switch.py index 0fe5bea5c52..b96697dc9cc 100644 --- a/tests/components/devolo_home_network/test_switch.py +++ b/tests/components/devolo_home_network/test_switch.py @@ -41,7 +41,7 @@ async def test_switch_setup(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert hass.states.get(f"{PLATFORM}.{device_name}_enable_guest_wifi") is not None + assert hass.states.get(f"{PLATFORM}.{device_name}_enable_guest_wi_fi") is not None assert hass.states.get(f"{PLATFORM}.{device_name}_enable_leds") is not None await hass.config_entries.async_unload(entry.entry_id) @@ -82,7 +82,7 @@ async def test_update_enable_guest_wifi( """Test state change of a enable_guest_wifi switch device.""" entry = configure_integration(hass) device_name = entry.title.replace(" ", "_").lower() - state_key = f"{PLATFORM}.{device_name}_enable_guest_wifi" + state_key = f"{PLATFORM}.{device_name}_enable_guest_wi_fi" await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -247,7 +247,7 @@ async def test_update_enable_leds( @pytest.mark.parametrize( ("name", "get_method", "update_interval"), [ - ("enable_guest_wifi", "async_get_wifi_guest_access", SHORT_UPDATE_INTERVAL), + ("enable_guest_wi_fi", "async_get_wifi_guest_access", SHORT_UPDATE_INTERVAL), ("enable_leds", "async_get_led_setting", SHORT_UPDATE_INTERVAL), ], ) @@ -284,7 +284,7 @@ async def test_device_failure( @pytest.mark.parametrize( ("name", "set_method"), [ - ("enable_guest_wifi", "async_set_wifi_guest_access"), + ("enable_guest_wi_fi", "async_set_wifi_guest_access"), ("enable_leds", "async_set_led_setting"), ], ) From 135fe26704c2fce94a85d3266919bd762e394028 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 16 Apr 2024 10:13:47 -0500 Subject: [PATCH 604/967] Bump httpcore to 1.0.5 (#115672) Fixes missing handling of EndOfStream errors --- 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 f3ee84392a7..b6f814c9f58 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -109,7 +109,7 @@ regex==2021.8.28 # requirements so we can directly link HA versions to these library versions. anyio==4.3.0 h11==0.14.0 -httpcore==1.0.4 +httpcore==1.0.5 # Ensure we have a hyperframe version that works in Python 3.10 # 5.2.0 fixed a collections abc deprecation diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 3f96e41a8ef..94147e3932b 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -99,7 +99,7 @@ regex==2021.8.28 # requirements so we can directly link HA versions to these library versions. anyio==4.3.0 h11==0.14.0 -httpcore==1.0.4 +httpcore==1.0.5 # Ensure we have a hyperframe version that works in Python 3.10 # 5.2.0 fixed a collections abc deprecation From 586d27320ed9e1506c8cba164f5e70a383d4ef86 Mon Sep 17 00:00:00 2001 From: BestPig Date: Tue, 16 Apr 2024 17:45:48 +0200 Subject: [PATCH 605/967] Add Sound Mode selection in soundpal components (#106589) --- .../components/songpal/media_player.py | 70 +++++++++++++++++++ tests/components/songpal/__init__.py | 18 +++++ tests/components/songpal/test_media_player.py | 22 +++++- 3 files changed, 109 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/songpal/media_player.py b/homeassistant/components/songpal/media_player.py index 33dc65d5eaa..d3ce934ec51 100644 --- a/homeassistant/components/songpal/media_player.py +++ b/homeassistant/components/songpal/media_player.py @@ -11,9 +11,11 @@ from songpal import ( ContentChange, Device, PowerChange, + SettingChange, SongpalException, VolumeChange, ) +from songpal.containers import Setting import voluptuous as vol from homeassistant.components.media_player import ( @@ -99,6 +101,7 @@ class SongpalEntity(MediaPlayerEntity): | MediaPlayerEntityFeature.VOLUME_STEP | MediaPlayerEntityFeature.VOLUME_MUTE | MediaPlayerEntityFeature.SELECT_SOURCE + | MediaPlayerEntityFeature.SELECT_SOUND_MODE | MediaPlayerEntityFeature.TURN_ON | MediaPlayerEntityFeature.TURN_OFF ) @@ -124,6 +127,8 @@ class SongpalEntity(MediaPlayerEntity): self._active_source = None self._sources = {} + self._active_sound_mode = None + self._sound_modes = {} async def async_added_to_hass(self) -> None: """Run when entity is added to hass.""" @@ -133,6 +138,28 @@ class SongpalEntity(MediaPlayerEntity): """Run when entity will be removed from hass.""" await self._dev.stop_listen_notifications() + async def _get_sound_modes_info(self): + """Get available sound modes and the active one.""" + settings = await self._dev.get_sound_settings("soundField") + if isinstance(settings, Setting): + settings = [settings] + + sound_modes = {} + active_sound_mode = None + for setting in settings: + cur = setting.currentValue + for opt in setting.candidate: + if not opt.isAvailable: + continue + if opt.value == cur: + active_sound_mode = opt.value + sound_modes[opt.value] = opt + + _LOGGER.debug("Got sound modes: %s", sound_modes) + _LOGGER.debug("Active sound mode: %s", active_sound_mode) + + return active_sound_mode, sound_modes + async def async_activate_websocket(self): """Activate websocket for listening if wanted.""" _LOGGER.info("Activating websocket connection") @@ -152,6 +179,16 @@ class SongpalEntity(MediaPlayerEntity): else: _LOGGER.debug("Got non-handled content change: %s", content) + async def _setting_changed(setting: SettingChange): + _LOGGER.debug("Setting changed: %s", setting) + + if setting.target == "soundField": + self._active_sound_mode = setting.currentValue + _LOGGER.debug("New active sound mode: %s", self._active_sound_mode) + self.async_write_ha_state() + else: + _LOGGER.debug("Got non-handled setting change: %s", setting) + async def _power_changed(power: PowerChange): _LOGGER.debug("Power changed: %s", power) self._state = power.status @@ -192,6 +229,7 @@ class SongpalEntity(MediaPlayerEntity): self._dev.on_notification(VolumeChange, _volume_changed) self._dev.on_notification(ContentChange, _source_changed) self._dev.on_notification(PowerChange, _power_changed) + self._dev.on_notification(SettingChange, _setting_changed) self._dev.on_notification(ConnectChange, _try_reconnect) async def handle_stop(event): @@ -271,6 +309,11 @@ class SongpalEntity(MediaPlayerEntity): _LOGGER.debug("Active source: %s", self._active_source) + ( + self._active_sound_mode, + self._sound_modes, + ) = await self._get_sound_modes_info() + self._attr_available = True except SongpalException as ex: @@ -291,6 +334,27 @@ class SongpalEntity(MediaPlayerEntity): """Return list of available sources.""" return [src.title for src in self._sources.values()] + async def async_select_sound_mode(self, sound_mode: str) -> None: + """Select sound mode.""" + for mode in self._sound_modes.values(): + if mode.title == sound_mode: + await self._dev.set_sound_settings("soundField", mode.value) + return + + _LOGGER.error("Unable to find sound mode: %s", sound_mode) + + @property + def sound_mode_list(self) -> list[str] | None: + """Return list of available sound modes. + + When active mode is None it means that sound mode is unavailable on the sound bar. + Can be due to incompatible sound bar or the sound bar is in a mode that does not + support sound mode changes. + """ + if not self._active_sound_mode: + return None + return [sound_mode.title for sound_mode in self._sound_modes.values()] + @property def state(self) -> MediaPlayerState: """Return current state.""" @@ -304,6 +368,12 @@ class SongpalEntity(MediaPlayerEntity): # Avoid a KeyError when _active_source is not (yet) populated return getattr(self._active_source, "title", None) + @property + def sound_mode(self) -> str | None: + """Return currently active sound_mode.""" + active_sound_mode = self._sound_modes.get(self._active_sound_mode) + return active_sound_mode.title if active_sound_mode else None + @property def volume_level(self): """Return volume level.""" diff --git a/tests/components/songpal/__init__.py b/tests/components/songpal/__init__.py index 6ebc2ec5ef4..ab585c5a6d5 100644 --- a/tests/components/songpal/__init__.py +++ b/tests/components/songpal/__init__.py @@ -85,6 +85,24 @@ def _create_mocked_device(throw_exception=False, wired_mac=MAC, wireless_mac=Non input2.active = True type(mocked_device).get_inputs = AsyncMock(return_value=[input1, input2]) + sound_mode1 = MagicMock() + sound_mode1.title = "Sound Mode 1" + sound_mode1.value = "sound_mode1" + sound_mode1.isAvailable = True + sound_mode2 = MagicMock() + sound_mode2.title = "Sound Mode 2" + sound_mode2.value = "sound_mode2" + sound_mode2.isAvailable = True + sound_mode3 = MagicMock() + sound_mode3.title = "Sound Mode 3" + sound_mode3.value = "sound_mode3" + sound_mode3.isAvailable = False + + soundField = MagicMock() + soundField.currentValue = "sound_mode2" + soundField.candidate = [sound_mode1, sound_mode2, sound_mode3] + type(mocked_device).get_sound_settings = AsyncMock(return_value=[soundField]) + type(mocked_device).set_power = AsyncMock() type(mocked_device).set_sound_settings = AsyncMock() type(mocked_device).listen_notifications = AsyncMock() diff --git a/tests/components/songpal/test_media_player.py b/tests/components/songpal/test_media_player.py index 4b1abf8709e..88443bf58b9 100644 --- a/tests/components/songpal/test_media_player.py +++ b/tests/components/songpal/test_media_player.py @@ -12,6 +12,7 @@ from songpal import ( SongpalException, VolumeChange, ) +from songpal.notification import SettingChange from homeassistant.components import media_player, songpal from homeassistant.components.media_player import MediaPlayerEntityFeature @@ -47,6 +48,7 @@ SUPPORT_SONGPAL = ( | MediaPlayerEntityFeature.VOLUME_STEP | MediaPlayerEntityFeature.VOLUME_MUTE | MediaPlayerEntityFeature.SELECT_SOURCE + | MediaPlayerEntityFeature.SELECT_SOUND_MODE | MediaPlayerEntityFeature.TURN_ON | MediaPlayerEntityFeature.TURN_OFF ) @@ -138,6 +140,8 @@ async def test_state(hass: HomeAssistant) -> None: assert attributes["is_volume_muted"] is False assert attributes["source_list"] == ["title1", "title2"] assert attributes["source"] == "title2" + assert attributes["sound_mode_list"] == ["Sound Mode 1", "Sound Mode 2"] + assert attributes["sound_mode"] == "Sound Mode 2" assert attributes["supported_features"] == SUPPORT_SONGPAL device_registry = dr.async_get(hass) @@ -171,6 +175,8 @@ async def test_state_wireless(hass: HomeAssistant) -> None: assert attributes["is_volume_muted"] is False assert attributes["source_list"] == ["title1", "title2"] assert attributes["source"] == "title2" + assert attributes["sound_mode_list"] == ["Sound Mode 1", "Sound Mode 2"] + assert attributes["sound_mode"] == "Sound Mode 2" assert attributes["supported_features"] == SUPPORT_SONGPAL device_registry = dr.async_get(hass) @@ -206,6 +212,8 @@ async def test_state_both(hass: HomeAssistant) -> None: assert attributes["is_volume_muted"] is False assert attributes["source_list"] == ["title1", "title2"] assert attributes["source"] == "title2" + assert attributes["sound_mode_list"] == ["Sound Mode 1", "Sound Mode 2"] + assert attributes["sound_mode"] == "Sound Mode 2" assert attributes["supported_features"] == SUPPORT_SONGPAL device_registry = dr.async_get(hass) @@ -303,6 +311,9 @@ async def test_services(hass: HomeAssistant) -> None: mocked_device2.set_sound_settings.assert_called_once_with("name", "value") mocked_device3.set_sound_settings.assert_called_once_with("name", "value") + await _call(hass, media_player.SERVICE_SELECT_SOUND_MODE, sound_mode="Sound Mode 1") + mocked_device.set_sound_settings.assert_called_with("soundField", "sound_mode1") + async def test_websocket_events(hass: HomeAssistant) -> None: """Test websocket events.""" @@ -315,7 +326,7 @@ async def test_websocket_events(hass: HomeAssistant) -> None: await hass.async_block_till_done() mocked_device.listen_notifications.assert_called_once() - assert mocked_device.on_notification.call_count == 4 + assert mocked_device.on_notification.call_count == 5 notification_callbacks = mocked_device.notification_callbacks @@ -336,6 +347,15 @@ async def test_websocket_events(hass: HomeAssistant) -> None: await notification_callbacks[ContentChange](content_change) assert _get_attributes(hass)["source"] == "title1" + sound_mode_change = MagicMock() + sound_mode_change.target = "soundField" + sound_mode_change.currentValue = "sound_mode1" + await notification_callbacks[SettingChange](sound_mode_change) + assert _get_attributes(hass)["sound_mode"] == "Sound Mode 1" + sound_mode_change.currentValue = "sound_mode2" + await notification_callbacks[SettingChange](sound_mode_change) + assert _get_attributes(hass)["sound_mode"] == "Sound Mode 2" + power_change = MagicMock() power_change.status = False await notification_callbacks[PowerChange](power_change) From 249a92d3211b459b773bd63138b176bf5a84db01 Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Tue, 16 Apr 2024 11:54:49 -0400 Subject: [PATCH 606/967] Unsupported if wrong image used on virtualization (#113882) * Unsupported if wrong image used on virtualization * Language tweaks Co-authored-by: Stefan Agner --------- Co-authored-by: Stefan Agner --- homeassistant/components/hassio/strings.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/hassio/strings.json b/homeassistant/components/hassio/strings.json index 63c1da4bfd8..fe026be6633 100644 --- a/homeassistant/components/hassio/strings.json +++ b/homeassistant/components/hassio/strings.json @@ -187,6 +187,10 @@ "unsupported_systemd_resolved": { "title": "Unsupported system - Systemd-Resolved issues", "description": "System is unsupported because Systemd Resolved is missing, inactive or misconfigured. Use the link to learn more and how to fix this." + }, + "unsupported_virtualization_image": { + "title": "Unsupported system - Incorrect OS image for virtualization", + "description": "System is unsupported because the Home Assistant OS image in use is not intended for use in a virtualized environment. Use the link to learn more and how to fix this." } }, "entity": { From 4a89e18b7e6ff8a7e06c6b023368b684a8ce81f0 Mon Sep 17 00:00:00 2001 From: Matthew Hallonbacka <79469789+Mallonbacka@users.noreply.github.com> Date: Tue, 16 Apr 2024 22:34:09 +0300 Subject: [PATCH 607/967] Fix check for missing parts on incoming SMS (#105068) * Fix check for missing parts on incoming SMS * Add tests for get_and_delete_all_sms function * Fix CI issues * Install libgammu-dev in CI * Bust the venv cache * Include python-gammu in requirements-all.txt * Adjust install of dependencies --------- Co-authored-by: Erik --- .github/workflows/ci.yaml | 13 ++- CODEOWNERS | 1 + homeassistant/components/sms/gateway.py | 29 +++-- requirements_test_all.txt | 3 + tests/components/sms/__init__.py | 1 + tests/components/sms/const.py | 143 ++++++++++++++++++++++++ tests/components/sms/test_gateway.py | 52 +++++++++ 7 files changed, 222 insertions(+), 20 deletions(-) create mode 100644 tests/components/sms/__init__.py create mode 100644 tests/components/sms/const.py create mode 100644 tests/components/sms/test_gateway.py diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d619fd8c7dc..c96c6b5e5f2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -33,7 +33,7 @@ on: type: boolean env: - CACHE_VERSION: 5 + CACHE_VERSION: 7 UV_CACHE_VERSION: 1 MYPY_CACHE_VERSION: 8 HA_SHORT_VERSION: "2024.5" @@ -484,6 +484,7 @@ jobs: libavfilter-dev \ libavformat-dev \ libavutil-dev \ + libgammu-dev \ libswresample-dev \ libswscale-dev \ libudev-dev @@ -496,6 +497,7 @@ jobs: pip install "$(grep '^uv' < requirements_test.txt)" uv pip install -U "pip>=21.3.1" setuptools wheel uv pip install -r requirements_all.txt + uv pip install "$(grep 'python-gammu' < requirements_all.txt | sed -e 's|# python-gammu|python-gammu|g')" uv pip install -r requirements_test.txt uv pip install -e . --config-settings editable_mode=compat @@ -688,7 +690,8 @@ jobs: sudo apt-get update sudo apt-get -y install \ bluez \ - ffmpeg + ffmpeg \ + libgammu-dev - name: Check out code from GitHub uses: actions/checkout@v4.1.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} @@ -747,7 +750,8 @@ jobs: sudo apt-get update sudo apt-get -y install \ bluez \ - ffmpeg + ffmpeg \ + libgammu-dev - name: Check out code from GitHub uses: actions/checkout@v4.1.2 - name: Set up Python ${{ matrix.python-version }} @@ -1124,7 +1128,8 @@ jobs: sudo apt-get update sudo apt-get -y install \ bluez \ - ffmpeg + ffmpeg \ + libgammu-dev - name: Check out code from GitHub uses: actions/checkout@v4.1.2 - name: Set up Python ${{ matrix.python-version }} diff --git a/CODEOWNERS b/CODEOWNERS index 83d5539a15c..56d42e5a3f3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1272,6 +1272,7 @@ build.json @home-assistant/supervisor /homeassistant/components/smhi/ @gjohansson-ST /tests/components/smhi/ @gjohansson-ST /homeassistant/components/sms/ @ocalvo +/tests/components/sms/ @ocalvo /homeassistant/components/snapcast/ @luar123 /tests/components/snapcast/ @luar123 /homeassistant/components/snmp/ @nmaggioni diff --git a/homeassistant/components/sms/gateway.py b/homeassistant/components/sms/gateway.py index e0cbf78dba4..1ed1f66570f 100644 --- a/homeassistant/components/sms/gateway.py +++ b/homeassistant/components/sms/gateway.py @@ -92,7 +92,6 @@ class Gateway: start = True entries = [] all_parts = -1 - all_parts_arrived = False _LOGGER.debug("Start remaining:%i", start_remaining) try: @@ -101,33 +100,31 @@ class Gateway: entry = state_machine.GetNextSMS(Folder=0, Start=True) all_parts = entry[0]["UDH"]["AllParts"] part_number = entry[0]["UDH"]["PartNumber"] - is_single_part = all_parts == 0 - is_multi_part = 0 <= all_parts < start_remaining + part_is_missing = all_parts > start_remaining _LOGGER.debug("All parts:%i", all_parts) _LOGGER.debug("Part Number:%i", part_number) _LOGGER.debug("Remaining:%i", remaining) - all_parts_arrived = is_multi_part or is_single_part - _LOGGER.debug("Start all_parts_arrived:%s", all_parts_arrived) + _LOGGER.debug("Start is_part_missing:%s", part_is_missing) start = False else: entry = state_machine.GetNextSMS( Folder=0, Location=entry[0]["Location"] ) - if all_parts_arrived or force: - remaining = remaining - 1 - entries.append(entry) - - # delete retrieved sms - _LOGGER.debug("Deleting message") - try: - state_machine.DeleteSMS(Folder=0, Location=entry[0]["Location"]) - except gammu.ERR_MEMORY_NOT_AVAILABLE: - _LOGGER.error("Error deleting SMS, memory not available") - else: + if part_is_missing and not force: _LOGGER.debug("Not all parts have arrived") break + remaining = remaining - 1 + entries.append(entry) + + # delete retrieved sms + _LOGGER.debug("Deleting message") + try: + state_machine.DeleteSMS(Folder=0, Location=entry[0]["Location"]) + except gammu.ERR_MEMORY_NOT_AVAILABLE: + _LOGGER.error("Error deleting SMS, memory not available") + except gammu.ERR_EMPTY: # error is raised if memory is empty (this induces wrong reported # memory status) diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d9fd0586fa7..fd3bad4398b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1723,6 +1723,9 @@ python-ecobee-api==0.2.17 # homeassistant.components.fully_kiosk python-fullykiosk==0.0.12 +# homeassistant.components.sms +# python-gammu==3.2.4 + # homeassistant.components.analytics_insights python-homeassistant-analytics==0.6.0 diff --git a/tests/components/sms/__init__.py b/tests/components/sms/__init__.py new file mode 100644 index 00000000000..09b4b0941fb --- /dev/null +++ b/tests/components/sms/__init__.py @@ -0,0 +1 @@ +"""Tests for SMS integration.""" diff --git a/tests/components/sms/const.py b/tests/components/sms/const.py new file mode 100644 index 00000000000..ae875e6d58e --- /dev/null +++ b/tests/components/sms/const.py @@ -0,0 +1,143 @@ +"""Constants for tests of the SMS component.""" + +import datetime + +SMS_STATUS_SINGLE = { + "SIMUnRead": 0, + "SIMUsed": 1, + "SIMSize": 30, + "PhoneUnRead": 0, + "PhoneUsed": 0, + "PhoneSize": 50, + "TemplatesUsed": 0, +} + +NEXT_SMS_SINGLE = [ + { + "SMSC": { + "Location": 0, + "Name": "", + "Format": "Text", + "Validity": "NA", + "Number": "+358444111111", + "DefaultNumber": "", + }, + "UDH": { + "Type": "NoUDH", + "Text": b"", + "ID8bit": 0, + "ID16bit": 0, + "PartNumber": -1, + "AllParts": 0, + }, + "Folder": 1, + "InboxFolder": 1, + "Memory": "SM", + "Location": 1, + "Name": "", + "Number": "+358444222222", + "Text": "Short message", + "Type": "Deliver", + "Coding": "Default_No_Compression", + "DateTime": datetime.datetime(2024, 3, 23, 20, 15, 37), + "SMSCDateTime": datetime.datetime(2024, 3, 23, 20, 15, 41), + "DeliveryStatus": 0, + "ReplyViaSameSMSC": 0, + "State": "UnRead", + "Class": -1, + "MessageReference": 0, + "ReplaceMessage": 0, + "RejectDuplicates": 0, + "Length": 7, + } +] + +SMS_STATUS_MULTIPLE = { + "SIMUnRead": 0, + "SIMUsed": 2, + "SIMSize": 30, + "PhoneUnRead": 0, + "PhoneUsed": 0, + "PhoneSize": 50, + "TemplatesUsed": 0, +} + +NEXT_SMS_MULTIPLE_1 = [ + { + "SMSC": { + "Location": 0, + "Name": "", + "Format": "Text", + "Validity": "NA", + "Number": "+358444111111", + "DefaultNumber": "", + }, + "UDH": { + "Type": "ConcatenatedMessages", + "Text": b"\x05\x00\x03\x00\x02\x01", + "ID8bit": 0, + "ID16bit": -1, + "PartNumber": 1, + "AllParts": 2, + }, + "Folder": 1, + "InboxFolder": 1, + "Memory": "SM", + "Location": 1, + "Name": "", + "Number": "+358444222222", + "Text": "Longer test again: 01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123", + "Type": "Deliver", + "Coding": "Default_No_Compression", + "DateTime": datetime.datetime(2024, 3, 25, 19, 53, 56), + "SMSCDateTime": datetime.datetime(2024, 3, 25, 19, 54, 6), + "DeliveryStatus": 0, + "ReplyViaSameSMSC": 0, + "State": "UnRead", + "Class": -1, + "MessageReference": 0, + "ReplaceMessage": 0, + "RejectDuplicates": 0, + "Length": 153, + } +] + +NEXT_SMS_MULTIPLE_2 = [ + { + "SMSC": { + "Location": 0, + "Name": "", + "Format": "Text", + "Validity": "NA", + "Number": "+358444111111", + "DefaultNumber": "", + }, + "UDH": { + "Type": "ConcatenatedMessages", + "Text": b"\x05\x00\x03\x00\x02\x02", + "ID8bit": 0, + "ID16bit": -1, + "PartNumber": 2, + "AllParts": 2, + }, + "Folder": 1, + "InboxFolder": 1, + "Memory": "SM", + "Location": 2, + "Name": "", + "Number": "+358444222222", + "Text": "4567890123456789012345678901", + "Type": "Deliver", + "Coding": "Default_No_Compression", + "DateTime": datetime.datetime(2024, 3, 25, 19, 53, 56), + "SMSCDateTime": datetime.datetime(2024, 3, 25, 19, 54, 7), + "DeliveryStatus": 0, + "ReplyViaSameSMSC": 0, + "State": "UnRead", + "Class": -1, + "MessageReference": 0, + "ReplaceMessage": 0, + "RejectDuplicates": 0, + "Length": 28, + } +] diff --git a/tests/components/sms/test_gateway.py b/tests/components/sms/test_gateway.py new file mode 100644 index 00000000000..132ba9bc1f3 --- /dev/null +++ b/tests/components/sms/test_gateway.py @@ -0,0 +1,52 @@ +"""Test the SMS Gateway.""" + +from unittest.mock import MagicMock + +from homeassistant.components.sms.gateway import Gateway +from homeassistant.core import HomeAssistant + +from .const import ( + NEXT_SMS_MULTIPLE_1, + NEXT_SMS_MULTIPLE_2, + NEXT_SMS_SINGLE, + SMS_STATUS_MULTIPLE, + SMS_STATUS_SINGLE, +) + + +async def test_get_and_delete_all_sms_single_message(hass: HomeAssistant) -> None: + """Test that a single message produces a list of entries containing the single message.""" + + # Mock the Gammu state_machine + state_machine = MagicMock() + state_machine.GetSMSStatus = MagicMock(return_value=SMS_STATUS_SINGLE) + state_machine.GetNextSMS = MagicMock(return_value=NEXT_SMS_SINGLE) + state_machine.DeleteSMS = MagicMock() + + response = Gateway({"Connection": None}, hass).get_and_delete_all_sms(state_machine) + + # Assert the length of the list + assert len(response) == 1 + assert len(response[0]) == 1 + + # Assert the content of the message + assert response[0][0]["Text"] == "Short message" + + +async def test_get_and_delete_all_sms_two_part_message(hass: HomeAssistant) -> None: + """Test that a two-part message produces a list of entries containing one combined message.""" + + state_machine = MagicMock() + state_machine.GetSMSStatus = MagicMock(return_value=SMS_STATUS_MULTIPLE) + state_machine.GetNextSMS = MagicMock( + side_effect=iter([NEXT_SMS_MULTIPLE_1, NEXT_SMS_MULTIPLE_2]) + ) + state_machine.DeleteSMS = MagicMock() + + response = Gateway({"Connection": None}, hass).get_and_delete_all_sms(state_machine) + + assert len(response) == 1 + assert len(response[0]) == 2 + + assert response[0][0]["Text"] == NEXT_SMS_MULTIPLE_1[0]["Text"] + assert response[0][1]["Text"] == NEXT_SMS_MULTIPLE_2[0]["Text"] From 81036967f04b47e75cf861aa821515e2de6e812f Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 16 Apr 2024 22:35:55 +0200 Subject: [PATCH 608/967] Correct unit for total usage in rfxtrx (#115719) --- homeassistant/components/rfxtrx/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index f421b6da7ef..46a3f021122 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -149,7 +149,7 @@ SENSOR_TYPES = ( translation_key="total_energy_usage", device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, ), RfxtrxSensorEntityDescription( key="Voltage", From e7076ac83f80c249309d0a5f245bcbc7a15f42c1 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 17 Apr 2024 00:00:16 +0200 Subject: [PATCH 609/967] Use separate data coordinators for AccuWeather observation and forecast (#115628) * Remove forecast option * Update strings * Use separate DataUpdateCoordinator for observation and forecast * Fix tests * Remove unneeded variable * Separate data coordinator classes * Use list comprehension * Separate coordinator clasess to add type annotations * Test the availability of the forecast sensor entity * Add DataUpdateCoordinator types * Use snapshot for test_sensor() --------- Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- .../components/accuweather/__init__.py | 121 +- .../components/accuweather/config_flow.py | 24 +- homeassistant/components/accuweather/const.py | 5 +- .../components/accuweather/coordinator.py | 124 + .../components/accuweather/diagnostics.py | 8 +- .../components/accuweather/sensor.py | 192 +- .../components/accuweather/strings.json | 12 +- .../components/accuweather/weather.py | 90 +- tests/components/accuweather/__init__.py | 9 +- .../snapshots/test_diagnostics.ambr | 4 +- .../accuweather/snapshots/test_sensor.ambr | 6436 +++++++++++++++++ .../accuweather/test_config_flow.py | 53 +- .../accuweather/test_diagnostics.py | 7 - tests/components/accuweather/test_init.py | 35 +- tests/components/accuweather/test_sensor.py | 618 +- tests/components/accuweather/test_weather.py | 27 +- 16 files changed, 6913 insertions(+), 852 deletions(-) create mode 100644 homeassistant/components/accuweather/coordinator.py create mode 100644 tests/components/accuweather/snapshots/test_sensor.ambr diff --git a/homeassistant/components/accuweather/__init__.py b/homeassistant/components/accuweather/__init__.py index 26e0c1331be..d52ef5e0ec6 100644 --- a/homeassistant/components/accuweather/__init__.py +++ b/homeassistant/components/accuweather/__init__.py @@ -2,14 +2,10 @@ from __future__ import annotations -from asyncio import timeout -from datetime import timedelta +from dataclasses import dataclass import logging -from typing import Any -from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError -from aiohttp import ClientSession -from aiohttp.client_exceptions import ClientConnectorError +from accuweather import AccuWeather from homeassistant.components.sensor import DOMAIN as SENSOR_PLATFORM from homeassistant.config_entries import ConfigEntry @@ -17,43 +13,70 @@ from homeassistant.const import CONF_API_KEY, CONF_NAME, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import ATTR_FORECAST, CONF_FORECAST, DOMAIN, MANUFACTURER +from .const import DOMAIN, UPDATE_INTERVAL_DAILY_FORECAST, UPDATE_INTERVAL_OBSERVATION +from .coordinator import ( + AccuWeatherDailyForecastDataUpdateCoordinator, + AccuWeatherObservationDataUpdateCoordinator, +) _LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.SENSOR, Platform.WEATHER] +@dataclass +class AccuWeatherData: + """Data for AccuWeather integration.""" + + coordinator_observation: AccuWeatherObservationDataUpdateCoordinator + coordinator_daily_forecast: AccuWeatherDailyForecastDataUpdateCoordinator + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up AccuWeather as config entry.""" api_key: str = entry.data[CONF_API_KEY] name: str = entry.data[CONF_NAME] - assert entry.unique_id is not None - location_key = entry.unique_id - forecast: bool = entry.options.get(CONF_FORECAST, False) - _LOGGER.debug("Using location_key: %s, get forecast: %s", location_key, forecast) + location_key = entry.unique_id + + _LOGGER.debug("Using location_key: %s", location_key) websession = async_get_clientsession(hass) + accuweather = AccuWeather(api_key, websession, location_key=location_key) - coordinator = AccuWeatherDataUpdateCoordinator( - hass, websession, api_key, location_key, forecast, name + coordinator_observation = AccuWeatherObservationDataUpdateCoordinator( + hass, + accuweather, + name, + "observation", + UPDATE_INTERVAL_OBSERVATION, ) - await coordinator.async_config_entry_first_refresh() + + coordinator_daily_forecast = AccuWeatherDailyForecastDataUpdateCoordinator( + hass, + accuweather, + name, + "daily forecast", + UPDATE_INTERVAL_DAILY_FORECAST, + ) + + await coordinator_observation.async_config_entry_first_refresh() + await coordinator_daily_forecast.async_config_entry_first_refresh() entry.async_on_unload(entry.add_update_listener(update_listener)) - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = AccuWeatherData( + coordinator_observation=coordinator_observation, + coordinator_daily_forecast=coordinator_daily_forecast, + ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # Remove ozone sensors from registry if they exist ent_reg = er.async_get(hass) for day in range(5): - unique_id = f"{coordinator.location_key}-ozone-{day}" + unique_id = f"{location_key}-ozone-{day}" if entity_id := ent_reg.async_get_entity_id(SENSOR_PLATFORM, DOMAIN, unique_id): _LOGGER.debug("Removing ozone sensor entity %s", entity_id) ent_reg.async_remove(entity_id) @@ -74,65 +97,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Update listener.""" await hass.config_entries.async_reload(entry.entry_id) - - -class AccuWeatherDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): # pylint: disable=hass-enforce-coordinator-module - """Class to manage fetching AccuWeather data API.""" - - def __init__( - self, - hass: HomeAssistant, - session: ClientSession, - api_key: str, - location_key: str, - forecast: bool, - name: str, - ) -> None: - """Initialize.""" - self.location_key = location_key - self.forecast = forecast - self.accuweather = AccuWeather(api_key, session, location_key=location_key) - self.device_info = DeviceInfo( - entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, location_key)}, - manufacturer=MANUFACTURER, - name=name, - # You don't need to provide specific details for the URL, - # so passing in _ characters is fine if the location key - # is correct - configuration_url=( - "http://accuweather.com/en/" - f"_/_/{location_key}/" - f"weather-forecast/{location_key}/" - ), - ) - - # Enabling the forecast download increases the number of requests per data - # update, we use 40 minutes for current condition only and 80 minutes for - # current condition and forecast as update interval to not exceed allowed number - # of requests. We have 50 requests allowed per day, so we use 36 and leave 14 as - # a reserve for restarting HA. - update_interval = timedelta(minutes=40) - if self.forecast: - update_interval *= 2 - _LOGGER.debug("Data will be update every %s", update_interval) - - super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) - - async def _async_update_data(self) -> dict[str, Any]: - """Update data via library.""" - forecast: list[dict[str, Any]] = [] - try: - async with timeout(10): - current = await self.accuweather.async_get_current_conditions() - if self.forecast: - forecast = await self.accuweather.async_get_daily_forecast() - except ( - ApiError, - ClientConnectorError, - InvalidApiKeyError, - RequestsExceededError, - ) as error: - raise UpdateFailed(error) from error - _LOGGER.debug("Requests remaining: %d", self.accuweather.requests_remaining) - return {**current, ATTR_FORECAST: forecast} diff --git a/homeassistant/components/accuweather/config_flow.py b/homeassistant/components/accuweather/config_flow.py index af7560d963a..71f7de89528 100644 --- a/homeassistant/components/accuweather/config_flow.py +++ b/homeassistant/components/accuweather/config_flow.py @@ -10,26 +10,12 @@ from aiohttp import ClientError from aiohttp.client_exceptions import ClientConnectorError 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_LATITUDE, CONF_LONGITUDE, CONF_NAME -from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.schema_config_entry_flow import ( - SchemaFlowFormStep, - SchemaOptionsFlowHandler, -) -from .const import CONF_FORECAST, DOMAIN - -OPTIONS_SCHEMA = vol.Schema( - { - vol.Optional(CONF_FORECAST, default=False): bool, - } -) -OPTIONS_FLOW = { - "init": SchemaFlowFormStep(OPTIONS_SCHEMA), -} +from .const import DOMAIN class AccuWeatherFlowHandler(ConfigFlow, domain=DOMAIN): @@ -87,9 +73,3 @@ class AccuWeatherFlowHandler(ConfigFlow, domain=DOMAIN): ), errors=errors, ) - - @staticmethod - @callback - def async_get_options_flow(config_entry: ConfigEntry) -> SchemaOptionsFlowHandler: - """Options callback for AccuWeather.""" - return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW) diff --git a/homeassistant/components/accuweather/const.py b/homeassistant/components/accuweather/const.py index 31925172d1c..1bbf5a36187 100644 --- a/homeassistant/components/accuweather/const.py +++ b/homeassistant/components/accuweather/const.py @@ -2,6 +2,7 @@ from __future__ import annotations +from datetime import timedelta from typing import Final from homeassistant.components.weather import ( @@ -27,10 +28,8 @@ ATTR_CATEGORY: Final = "Category" ATTR_DIRECTION: Final = "Direction" ATTR_ENGLISH: Final = "English" ATTR_LEVEL: Final = "level" -ATTR_FORECAST: Final = "forecast" ATTR_SPEED: Final = "Speed" ATTR_VALUE: Final = "Value" -CONF_FORECAST: Final = "forecast" DOMAIN: Final = "accuweather" MANUFACTURER: Final = "AccuWeather, Inc." MAX_FORECAST_DAYS: Final = 4 @@ -56,3 +55,5 @@ CONDITION_MAP = { for cond_ha, cond_codes in CONDITION_CLASSES.items() for cond_code in cond_codes } +UPDATE_INTERVAL_OBSERVATION = timedelta(minutes=40) +UPDATE_INTERVAL_DAILY_FORECAST = timedelta(hours=6) diff --git a/homeassistant/components/accuweather/coordinator.py b/homeassistant/components/accuweather/coordinator.py new file mode 100644 index 00000000000..26fadd6806c --- /dev/null +++ b/homeassistant/components/accuweather/coordinator.py @@ -0,0 +1,124 @@ +"""The AccuWeather coordinator.""" + +from asyncio import timeout +from datetime import timedelta +import logging +from typing import TYPE_CHECKING, Any + +from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError +from aiohttp.client_exceptions import ClientConnectorError + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.update_coordinator import ( + DataUpdateCoordinator, + TimestampDataUpdateCoordinator, + UpdateFailed, +) + +from .const import DOMAIN, MANUFACTURER + +EXCEPTIONS = (ApiError, ClientConnectorError, InvalidApiKeyError, RequestsExceededError) + +_LOGGER = logging.getLogger(__name__) + + +class AccuWeatherObservationDataUpdateCoordinator( + DataUpdateCoordinator[dict[str, Any]] +): + """Class to manage fetching AccuWeather data API.""" + + def __init__( + self, + hass: HomeAssistant, + accuweather: AccuWeather, + name: str, + coordinator_type: str, + update_interval: timedelta, + ) -> None: + """Initialize.""" + self.accuweather = accuweather + self.location_key = accuweather.location_key + + if TYPE_CHECKING: + assert self.location_key is not None + + self.device_info = _get_device_info(self.location_key, name) + + super().__init__( + hass, + _LOGGER, + name=f"{name} ({coordinator_type})", + update_interval=update_interval, + ) + + async def _async_update_data(self) -> dict[str, Any]: + """Update data via library.""" + try: + async with timeout(10): + result = await self.accuweather.async_get_current_conditions() + except EXCEPTIONS as error: + raise UpdateFailed(error) from error + + _LOGGER.debug("Requests remaining: %d", self.accuweather.requests_remaining) + + return result + + +class AccuWeatherDailyForecastDataUpdateCoordinator( + TimestampDataUpdateCoordinator[list[dict[str, Any]]] +): + """Class to manage fetching AccuWeather data API.""" + + def __init__( + self, + hass: HomeAssistant, + accuweather: AccuWeather, + name: str, + coordinator_type: str, + update_interval: timedelta, + ) -> None: + """Initialize.""" + self.accuweather = accuweather + self.location_key = accuweather.location_key + + if TYPE_CHECKING: + assert self.location_key is not None + + self.device_info = _get_device_info(self.location_key, name) + + super().__init__( + hass, + _LOGGER, + name=f"{name} ({coordinator_type})", + update_interval=update_interval, + ) + + async def _async_update_data(self) -> list[dict[str, Any]]: + """Update data via library.""" + try: + async with timeout(10): + result = await self.accuweather.async_get_daily_forecast() + except EXCEPTIONS as error: + raise UpdateFailed(error) from error + + _LOGGER.debug("Requests remaining: %d", self.accuweather.requests_remaining) + + return result + + +def _get_device_info(location_key: str, name: str) -> DeviceInfo: + """Get device info.""" + return DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, location_key)}, + manufacturer=MANUFACTURER, + name=name, + # You don't need to provide specific details for the URL, + # so passing in _ characters is fine if the location key + # is correct + configuration_url=( + "http://accuweather.com/en/" + f"_/_/{location_key}/weather-forecast/{location_key}/" + ), + ) diff --git a/homeassistant/components/accuweather/diagnostics.py b/homeassistant/components/accuweather/diagnostics.py index c4f04b209cf..810638a1e49 100644 --- a/homeassistant/components/accuweather/diagnostics.py +++ b/homeassistant/components/accuweather/diagnostics.py @@ -9,7 +9,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant -from . import AccuWeatherDataUpdateCoordinator +from . import AccuWeatherData from .const import DOMAIN TO_REDACT = {CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE} @@ -19,11 +19,9 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - coordinator: AccuWeatherDataUpdateCoordinator = hass.data[DOMAIN][ - config_entry.entry_id - ] + accuweather_data: AccuWeatherData = hass.data[DOMAIN][config_entry.entry_id] return { "config_entry_data": async_redact_data(dict(config_entry.data), TO_REDACT), - "coordinator_data": coordinator.data, + "observation_data": accuweather_data.coordinator_observation.data, } diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py index 521dfdfbead..95274297828 100644 --- a/homeassistant/components/accuweather/sensor.py +++ b/homeassistant/components/accuweather/sensor.py @@ -28,13 +28,12 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import AccuWeatherDataUpdateCoordinator +from . import AccuWeatherData from .const import ( API_METRIC, ATTR_CATEGORY, ATTR_DIRECTION, ATTR_ENGLISH, - ATTR_FORECAST, ATTR_LEVEL, ATTR_SPEED, ATTR_VALUE, @@ -42,6 +41,10 @@ from .const import ( DOMAIN, MAX_FORECAST_DAYS, ) +from .coordinator import ( + AccuWeatherDailyForecastDataUpdateCoordinator, + AccuWeatherObservationDataUpdateCoordinator, +) PARALLEL_UPDATES = 1 @@ -52,12 +55,18 @@ class AccuWeatherSensorDescription(SensorEntityDescription): value_fn: Callable[[dict[str, Any]], str | int | float | None] attr_fn: Callable[[dict[str, Any]], dict[str, Any]] = lambda _: {} - day: int | None = None -FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( +@dataclass(frozen=True, kw_only=True) +class AccuWeatherForecastSensorDescription(AccuWeatherSensorDescription): + """Class describing AccuWeather sensor entities.""" + + day: int + + +FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = ( *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="AirQuality", icon="mdi:air-filter", value_fn=lambda data: cast(str, data[ATTR_CATEGORY]), @@ -69,7 +78,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( for day in range(MAX_FORECAST_DAYS + 1) ), *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="CloudCoverDay", icon="mdi:weather-cloudy", entity_registry_enabled_default=False, @@ -81,7 +90,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( for day in range(MAX_FORECAST_DAYS + 1) ), *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="CloudCoverNight", icon="mdi:weather-cloudy", entity_registry_enabled_default=False, @@ -93,7 +102,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( for day in range(MAX_FORECAST_DAYS + 1) ), *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="Grass", icon="mdi:grass", entity_registry_enabled_default=False, @@ -106,7 +115,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( for day in range(MAX_FORECAST_DAYS + 1) ), *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="HoursOfSun", icon="mdi:weather-partly-cloudy", native_unit_of_measurement=UnitOfTime.HOURS, @@ -117,7 +126,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( for day in range(MAX_FORECAST_DAYS + 1) ), *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="LongPhraseDay", value_fn=lambda data: cast(str, data), translation_key=f"condition_day_{day}d", @@ -126,7 +135,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( for day in range(MAX_FORECAST_DAYS + 1) ), *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="LongPhraseNight", value_fn=lambda data: cast(str, data), translation_key=f"condition_night_{day}d", @@ -135,7 +144,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( for day in range(MAX_FORECAST_DAYS + 1) ), *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="Mold", icon="mdi:blur", entity_registry_enabled_default=False, @@ -148,7 +157,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( for day in range(MAX_FORECAST_DAYS + 1) ), *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="Ragweed", icon="mdi:sprout", native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -161,7 +170,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( for day in range(MAX_FORECAST_DAYS + 1) ), *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="RealFeelTemperatureMax", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, @@ -172,7 +181,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( for day in range(MAX_FORECAST_DAYS + 1) ), *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="RealFeelTemperatureMin", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, @@ -183,7 +192,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( for day in range(MAX_FORECAST_DAYS + 1) ), *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="RealFeelTemperatureShadeMax", device_class=SensorDeviceClass.TEMPERATURE, entity_registry_enabled_default=False, @@ -195,7 +204,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( for day in range(MAX_FORECAST_DAYS + 1) ), *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="RealFeelTemperatureShadeMin", device_class=SensorDeviceClass.TEMPERATURE, entity_registry_enabled_default=False, @@ -207,7 +216,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( for day in range(MAX_FORECAST_DAYS + 1) ), *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="SolarIrradianceDay", icon="mdi:weather-sunny", entity_registry_enabled_default=False, @@ -219,7 +228,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( for day in range(MAX_FORECAST_DAYS + 1) ), *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="SolarIrradianceNight", icon="mdi:weather-sunny", entity_registry_enabled_default=False, @@ -231,7 +240,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( for day in range(MAX_FORECAST_DAYS + 1) ), *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="ThunderstormProbabilityDay", icon="mdi:weather-lightning", native_unit_of_measurement=PERCENTAGE, @@ -242,7 +251,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( for day in range(MAX_FORECAST_DAYS + 1) ), *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="ThunderstormProbabilityNight", icon="mdi:weather-lightning", native_unit_of_measurement=PERCENTAGE, @@ -253,7 +262,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( for day in range(MAX_FORECAST_DAYS + 1) ), *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="Tree", icon="mdi:tree-outline", native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -266,7 +275,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( for day in range(MAX_FORECAST_DAYS + 1) ), *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="UVIndex", icon="mdi:weather-sunny", native_unit_of_measurement=UV_INDEX, @@ -278,7 +287,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( for day in range(MAX_FORECAST_DAYS + 1) ), *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="WindGustDay", device_class=SensorDeviceClass.WIND_SPEED, entity_registry_enabled_default=False, @@ -291,7 +300,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( for day in range(MAX_FORECAST_DAYS + 1) ), *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="WindGustNight", device_class=SensorDeviceClass.WIND_SPEED, entity_registry_enabled_default=False, @@ -304,7 +313,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( for day in range(MAX_FORECAST_DAYS + 1) ), *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="WindDay", device_class=SensorDeviceClass.WIND_SPEED, native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, @@ -316,7 +325,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( for day in range(MAX_FORECAST_DAYS + 1) ), *( - AccuWeatherSensorDescription( + AccuWeatherForecastSensorDescription( key="WindNight", device_class=SensorDeviceClass.WIND_SPEED, native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, @@ -453,25 +462,33 @@ async def async_setup_entry( ) -> None: """Add AccuWeather entities from a config_entry.""" - coordinator: AccuWeatherDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + accuweather_data: AccuWeatherData = hass.data[DOMAIN][entry.entry_id] - sensors = [ - AccuWeatherSensor(coordinator, description) for description in SENSOR_TYPES + observation_coordinator: AccuWeatherObservationDataUpdateCoordinator = ( + accuweather_data.coordinator_observation + ) + forecast_daily_coordinator: AccuWeatherDailyForecastDataUpdateCoordinator = ( + accuweather_data.coordinator_daily_forecast + ) + + sensors: list[AccuWeatherSensor | AccuWeatherForecastSensor] = [ + AccuWeatherSensor(observation_coordinator, description) + for description in SENSOR_TYPES ] - if coordinator.forecast: - for description in FORECAST_SENSOR_TYPES: - # Some air quality/allergy sensors are only available for certain - # locations. - if description.key not in coordinator.data[ATTR_FORECAST][description.day]: - continue - sensors.append(AccuWeatherSensor(coordinator, description)) + sensors.extend( + [ + AccuWeatherForecastSensor(forecast_daily_coordinator, description) + for description in FORECAST_SENSOR_TYPES + if description.key in forecast_daily_coordinator.data[description.day] + ] + ) async_add_entities(sensors) class AccuWeatherSensor( - CoordinatorEntity[AccuWeatherDataUpdateCoordinator], SensorEntity + CoordinatorEntity[AccuWeatherObservationDataUpdateCoordinator], SensorEntity ): """Define an AccuWeather entity.""" @@ -481,22 +498,15 @@ class AccuWeatherSensor( def __init__( self, - coordinator: AccuWeatherDataUpdateCoordinator, + coordinator: AccuWeatherObservationDataUpdateCoordinator, description: AccuWeatherSensorDescription, ) -> None: """Initialize.""" super().__init__(coordinator) - self.forecast_day = description.day + self.entity_description = description - self._sensor_data = _get_sensor_data( - coordinator.data, description.key, self.forecast_day - ) - if self.forecast_day is not None: - self._attr_unique_id = f"{coordinator.location_key}-{description.key}-{self.forecast_day}".lower() - else: - self._attr_unique_id = ( - f"{coordinator.location_key}-{description.key}".lower() - ) + self._sensor_data = self._get_sensor_data(coordinator.data, description.key) + self._attr_unique_id = f"{coordinator.location_key}-{description.key}".lower() self._attr_device_info = coordinator.device_info @property @@ -507,30 +517,78 @@ class AccuWeatherSensor( @property def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" - if self.forecast_day is not None: - return self.entity_description.attr_fn(self._sensor_data) - return self.entity_description.attr_fn(self.coordinator.data) @callback def _handle_coordinator_update(self) -> None: """Handle data update.""" - self._sensor_data = _get_sensor_data( + self._sensor_data = self._get_sensor_data( + self.coordinator.data, self.entity_description.key + ) + self.async_write_ha_state() + + @staticmethod + def _get_sensor_data( + sensors: dict[str, Any], + kind: str, + ) -> Any: + """Get sensor data.""" + if kind == "Precipitation": + return sensors["PrecipitationSummary"]["PastHour"] + + return sensors[kind] + + +class AccuWeatherForecastSensor( + CoordinatorEntity[AccuWeatherDailyForecastDataUpdateCoordinator], SensorEntity +): + """Define an AccuWeather entity.""" + + _attr_attribution = ATTRIBUTION + _attr_has_entity_name = True + entity_description: AccuWeatherForecastSensorDescription + + def __init__( + self, + coordinator: AccuWeatherDailyForecastDataUpdateCoordinator, + description: AccuWeatherForecastSensorDescription, + ) -> 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 + ) + self._attr_unique_id = ( + f"{coordinator.location_key}-{description.key}-{self.forecast_day}".lower() + ) + self._attr_device_info = coordinator.device_info + + @property + def native_value(self) -> str | int | float | None: + """Return the state.""" + return self.entity_description.value_fn(self._sensor_data) + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the state attributes.""" + return self.entity_description.attr_fn(self._sensor_data) + + @callback + def _handle_coordinator_update(self) -> None: + """Handle data update.""" + self._sensor_data = self._get_sensor_data( self.coordinator.data, self.entity_description.key, self.forecast_day ) self.async_write_ha_state() - -def _get_sensor_data( - sensors: dict[str, Any], - kind: str, - forecast_day: int | None = None, -) -> Any: - """Get sensor data.""" - if forecast_day is not None: - return sensors[ATTR_FORECAST][forecast_day][kind] - - if kind == "Precipitation": - return sensors["PrecipitationSummary"]["PastHour"] - - return sensors[kind] + @staticmethod + def _get_sensor_data( + sensors: list[dict[str, Any]], + kind: str, + forecast_day: int, + ) -> Any: + """Get sensor data.""" + return sensors[forecast_day][kind] diff --git a/homeassistant/components/accuweather/strings.json b/homeassistant/components/accuweather/strings.json index 718f2da6a75..9d8fce865fd 100644 --- a/homeassistant/components/accuweather/strings.json +++ b/homeassistant/components/accuweather/strings.json @@ -11,7 +11,7 @@ } }, "create_entry": { - "default": "Some sensors are not enabled by default. You can enable them in the entity registry after the integration configuration.\nWeather forecast is not enabled by default. You can enable it in the integration options." + "default": "Some sensors are not enabled by default. You can enable them in the entity registry after the integration configuration." }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", @@ -790,16 +790,6 @@ } } }, - "options": { - "step": { - "init": { - "description": "Due to the limitations of the free version of the AccuWeather API key, when you enable weather forecast, data updates will be performed every 80 minutes instead of every 40 minutes.", - "data": { - "forecast": "Weather forecast" - } - } - } - }, "system_health": { "info": { "can_reach_server": "Reach AccuWeather server", diff --git a/homeassistant/components/accuweather/weather.py b/homeassistant/components/accuweather/weather.py index 1f2e606f6ea..4d248a06ac3 100644 --- a/homeassistant/components/accuweather/weather.py +++ b/homeassistant/components/accuweather/weather.py @@ -17,8 +17,8 @@ from homeassistant.components.weather import ( ATTR_FORECAST_TIME, ATTR_FORECAST_UV_INDEX, ATTR_FORECAST_WIND_BEARING, + CoordinatorWeatherEntity, Forecast, - SingleCoordinatorWeatherEntity, WeatherEntityFeature, ) from homeassistant.config_entries import ConfigEntry @@ -31,19 +31,23 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import TimestampDataUpdateCoordinator from homeassistant.util.dt import utc_from_timestamp -from . import AccuWeatherDataUpdateCoordinator +from . import AccuWeatherData from .const import ( API_METRIC, ATTR_DIRECTION, - ATTR_FORECAST, ATTR_SPEED, ATTR_VALUE, ATTRIBUTION, CONDITION_MAP, DOMAIN, ) +from .coordinator import ( + AccuWeatherDailyForecastDataUpdateCoordinator, + AccuWeatherObservationDataUpdateCoordinator, +) PARALLEL_UPDATES = 1 @@ -52,106 +56,134 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Add a AccuWeather weather entity from a config_entry.""" + accuweather_data: AccuWeatherData = hass.data[DOMAIN][entry.entry_id] - coordinator: AccuWeatherDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - - async_add_entities([AccuWeatherEntity(coordinator)]) + async_add_entities([AccuWeatherEntity(accuweather_data)]) class AccuWeatherEntity( - SingleCoordinatorWeatherEntity[AccuWeatherDataUpdateCoordinator] + CoordinatorWeatherEntity[ + AccuWeatherObservationDataUpdateCoordinator, + AccuWeatherDailyForecastDataUpdateCoordinator, + TimestampDataUpdateCoordinator, + TimestampDataUpdateCoordinator, + ] ): """Define an AccuWeather entity.""" _attr_has_entity_name = True _attr_name = None - def __init__(self, coordinator: AccuWeatherDataUpdateCoordinator) -> None: + def __init__(self, accuweather_data: AccuWeatherData) -> None: """Initialize.""" - super().__init__(coordinator) + super().__init__( + observation_coordinator=accuweather_data.coordinator_observation, + daily_coordinator=accuweather_data.coordinator_daily_forecast, + ) + self._attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS self._attr_native_pressure_unit = UnitOfPressure.HPA self._attr_native_temperature_unit = UnitOfTemperature.CELSIUS self._attr_native_visibility_unit = UnitOfLength.KILOMETERS self._attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR - self._attr_unique_id = coordinator.location_key + self._attr_unique_id = accuweather_data.coordinator_observation.location_key self._attr_attribution = ATTRIBUTION - self._attr_device_info = coordinator.device_info - if self.coordinator.forecast: - self._attr_supported_features = WeatherEntityFeature.FORECAST_DAILY + self._attr_device_info = accuweather_data.coordinator_observation.device_info + self._attr_supported_features = WeatherEntityFeature.FORECAST_DAILY + + self.observation_coordinator = accuweather_data.coordinator_observation + self.daily_coordinator = accuweather_data.coordinator_daily_forecast @property def condition(self) -> str | None: """Return the current condition.""" - return CONDITION_MAP.get(self.coordinator.data["WeatherIcon"]) + return CONDITION_MAP.get(self.observation_coordinator.data["WeatherIcon"]) @property def cloud_coverage(self) -> float: """Return the Cloud coverage in %.""" - return cast(float, self.coordinator.data["CloudCover"]) + return cast(float, self.observation_coordinator.data["CloudCover"]) @property def native_apparent_temperature(self) -> float: """Return the apparent temperature.""" return cast( - float, self.coordinator.data["ApparentTemperature"][API_METRIC][ATTR_VALUE] + float, + self.observation_coordinator.data["ApparentTemperature"][API_METRIC][ + ATTR_VALUE + ], ) @property def native_temperature(self) -> float: """Return the temperature.""" - return cast(float, self.coordinator.data["Temperature"][API_METRIC][ATTR_VALUE]) + return cast( + float, + self.observation_coordinator.data["Temperature"][API_METRIC][ATTR_VALUE], + ) @property def native_pressure(self) -> float: """Return the pressure.""" - return cast(float, self.coordinator.data["Pressure"][API_METRIC][ATTR_VALUE]) + return cast( + float, self.observation_coordinator.data["Pressure"][API_METRIC][ATTR_VALUE] + ) @property def native_dew_point(self) -> float: """Return the dew point.""" - return cast(float, self.coordinator.data["DewPoint"][API_METRIC][ATTR_VALUE]) + return cast( + float, self.observation_coordinator.data["DewPoint"][API_METRIC][ATTR_VALUE] + ) @property def humidity(self) -> int: """Return the humidity.""" - return cast(int, self.coordinator.data["RelativeHumidity"]) + return cast(int, self.observation_coordinator.data["RelativeHumidity"]) @property def native_wind_gust_speed(self) -> float: """Return the wind gust speed.""" return cast( - float, self.coordinator.data["WindGust"][ATTR_SPEED][API_METRIC][ATTR_VALUE] + float, + self.observation_coordinator.data["WindGust"][ATTR_SPEED][API_METRIC][ + ATTR_VALUE + ], ) @property def native_wind_speed(self) -> float: """Return the wind speed.""" return cast( - float, self.coordinator.data["Wind"][ATTR_SPEED][API_METRIC][ATTR_VALUE] + float, + self.observation_coordinator.data["Wind"][ATTR_SPEED][API_METRIC][ + ATTR_VALUE + ], ) @property def wind_bearing(self) -> int: """Return the wind bearing.""" - return cast(int, self.coordinator.data["Wind"][ATTR_DIRECTION]["Degrees"]) + return cast( + int, self.observation_coordinator.data["Wind"][ATTR_DIRECTION]["Degrees"] + ) @property def native_visibility(self) -> float: """Return the visibility.""" - return cast(float, self.coordinator.data["Visibility"][API_METRIC][ATTR_VALUE]) + return cast( + float, + self.observation_coordinator.data["Visibility"][API_METRIC][ATTR_VALUE], + ) @property def uv_index(self) -> float: """Return the UV index.""" - return cast(float, self.coordinator.data["UVIndex"]) + return cast(float, self.observation_coordinator.data["UVIndex"]) @callback def _async_forecast_daily(self) -> list[Forecast] | None: """Return the daily forecast in native units.""" - if not self.coordinator.forecast: - return None - # remap keys from library to keys understood by the weather component return [ { ATTR_FORECAST_TIME: utc_from_timestamp(item["EpochDate"]).isoformat(), @@ -175,5 +207,5 @@ class AccuWeatherEntity( ATTR_FORECAST_WIND_BEARING: item["WindDay"][ATTR_DIRECTION]["Degrees"], ATTR_FORECAST_CONDITION: CONDITION_MAP.get(item["IconDay"]), } - for item in self.coordinator.data[ATTR_FORECAST] + for item in self.daily_coordinator.data ] diff --git a/tests/components/accuweather/__init__.py b/tests/components/accuweather/__init__.py index afaa5bbef25..a08b894ebb4 100644 --- a/tests/components/accuweather/__init__.py +++ b/tests/components/accuweather/__init__.py @@ -11,14 +11,8 @@ from tests.common import ( ) -async def init_integration( - hass, forecast=False, unsupported_icon=False -) -> MockConfigEntry: +async def init_integration(hass, unsupported_icon=False) -> MockConfigEntry: """Set up the AccuWeather integration in Home Assistant.""" - options = {} - if forecast: - options["forecast"] = True - entry = MockConfigEntry( domain=DOMAIN, title="Home", @@ -29,7 +23,6 @@ async def init_integration( "longitude": 122.12, "name": "Home", }, - options=options, ) current = load_json_object_fixture("accuweather/current_conditions_data.json") diff --git a/tests/components/accuweather/snapshots/test_diagnostics.ambr b/tests/components/accuweather/snapshots/test_diagnostics.ambr index b3c0c1de752..7477602f3a4 100644 --- a/tests/components/accuweather/snapshots/test_diagnostics.ambr +++ b/tests/components/accuweather/snapshots/test_diagnostics.ambr @@ -7,7 +7,7 @@ 'longitude': '**REDACTED**', 'name': 'Home', }), - 'coordinator_data': dict({ + 'observation_data': dict({ 'ApparentTemperature': dict({ 'Imperial': dict({ 'Unit': 'F', @@ -297,8 +297,6 @@ }), }), }), - 'forecast': list([ - ]), }), }) # --- diff --git a/tests/components/accuweather/snapshots/test_sensor.ambr b/tests/components/accuweather/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..42783f375b0 --- /dev/null +++ b/tests/components/accuweather/snapshots/test_sensor.ambr @@ -0,0 +1,6436 @@ +# serializer version: 1 +# name: test_sensor[sensor.home_air_quality_day_1-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_1', + '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 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'air_quality_1d', + 'unique_id': '0123456-airquality-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.home_air_quality_day_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'enum', + 'friendly_name': 'Home Air quality day 1', + 'icon': 'mdi:air-filter', + 'options': list([ + 'good', + 'hazardous', + 'high', + 'low', + 'moderate', + 'unhealthy', + ]), + }), + 'context': , + 'entity_id': 'sensor.home_air_quality_day_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'good', + }) +# --- +# name: test_sensor[sensor.home_air_quality_day_2-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_2', + '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 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'air_quality_2d', + 'unique_id': '0123456-airquality-2', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.home_air_quality_day_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'enum', + 'friendly_name': 'Home Air quality day 2', + 'icon': 'mdi:air-filter', + 'options': list([ + 'good', + 'hazardous', + 'high', + 'low', + 'moderate', + 'unhealthy', + ]), + }), + 'context': , + 'entity_id': 'sensor.home_air_quality_day_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'good', + }) +# --- +# name: test_sensor[sensor.home_air_quality_day_3-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_3', + '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 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'air_quality_3d', + 'unique_id': '0123456-airquality-3', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.home_air_quality_day_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'enum', + 'friendly_name': 'Home Air quality day 3', + 'icon': 'mdi:air-filter', + 'options': list([ + 'good', + 'hazardous', + 'high', + 'low', + 'moderate', + 'unhealthy', + ]), + }), + 'context': , + 'entity_id': 'sensor.home_air_quality_day_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'good', + }) +# --- +# name: test_sensor[sensor.home_air_quality_day_4-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_4', + '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 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'air_quality_4d', + 'unique_id': '0123456-airquality-4', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.home_air_quality_day_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'enum', + 'friendly_name': 'Home Air quality day 4', + 'icon': 'mdi:air-filter', + 'options': list([ + 'good', + 'hazardous', + 'high', + 'low', + 'moderate', + 'unhealthy', + ]), + }), + 'context': , + 'entity_id': 'sensor.home_air_quality_day_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + '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({ + }), + '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.home_apparent_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': 'Apparent temperature', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'apparent_temperature', + 'unique_id': '0123456-apparenttemperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_apparent_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home Apparent temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_apparent_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '22.8', + }) +# --- +# name: test_sensor[sensor.home_cloud_ceiling-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.home_cloud_ceiling', + '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': 'mdi:weather-fog', + 'original_name': 'Cloud ceiling', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cloud_ceiling', + 'unique_id': '0123456-ceiling', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_cloud_ceiling-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'distance', + 'friendly_name': 'Home Cloud ceiling', + 'icon': 'mdi:weather-fog', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_cloud_ceiling', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3200.0', + }) +# --- +# name: test_sensor[sensor.home_cloud_cover-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.home_cloud_cover', + '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', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cloud_cover', + 'unique_id': '0123456-cloudcover', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.home_cloud_cover-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Cloud cover', + 'icon': 'mdi:weather-cloudy', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.home_cloud_cover', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10', + }) +# --- +# name: test_sensor[sensor.home_cloud_cover_day_1-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_1', + '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 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cloud_cover_day_1d', + 'unique_id': '0123456-cloudcoverday-1', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.home_cloud_cover_day_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Cloud cover day 1', + 'icon': 'mdi:weather-cloudy', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.home_cloud_cover_day_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '52', + }) +# --- +# name: test_sensor[sensor.home_cloud_cover_day_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.home_cloud_cover_day_2', + '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 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cloud_cover_day_2d', + 'unique_id': '0123456-cloudcoverday-2', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.home_cloud_cover_day_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Cloud cover day 2', + 'icon': 'mdi:weather-cloudy', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.home_cloud_cover_day_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '65', + }) +# --- +# name: test_sensor[sensor.home_cloud_cover_day_3-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_3', + '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 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cloud_cover_day_3d', + 'unique_id': '0123456-cloudcoverday-3', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.home_cloud_cover_day_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Cloud cover day 3', + 'icon': 'mdi:weather-cloudy', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.home_cloud_cover_day_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '45', + }) +# --- +# name: test_sensor[sensor.home_cloud_cover_day_4-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_4', + '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 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cloud_cover_day_4d', + 'unique_id': '0123456-cloudcoverday-4', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.home_cloud_cover_day_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Cloud cover day 4', + 'icon': 'mdi:weather-cloudy', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.home_cloud_cover_day_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '50', + }) +# --- +# name: test_sensor[sensor.home_cloud_cover_night_1-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_1', + '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 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cloud_cover_night_1d', + 'unique_id': '0123456-cloudcovernight-1', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.home_cloud_cover_night_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Cloud cover night 1', + 'icon': 'mdi:weather-cloudy', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.home_cloud_cover_night_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '63', + }) +# --- +# name: test_sensor[sensor.home_cloud_cover_night_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.home_cloud_cover_night_2', + '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 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cloud_cover_night_2d', + 'unique_id': '0123456-cloudcovernight-2', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.home_cloud_cover_night_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Cloud cover night 2', + 'icon': 'mdi:weather-cloudy', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.home_cloud_cover_night_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '53', + }) +# --- +# name: test_sensor[sensor.home_cloud_cover_night_3-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_3', + '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 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cloud_cover_night_3d', + 'unique_id': '0123456-cloudcovernight-3', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.home_cloud_cover_night_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Cloud cover night 3', + 'icon': 'mdi:weather-cloudy', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.home_cloud_cover_night_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '27', + }) +# --- +# name: test_sensor[sensor.home_cloud_cover_night_4-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_4', + '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 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cloud_cover_night_4d', + 'unique_id': '0123456-cloudcovernight-4', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.home_cloud_cover_night_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Cloud cover night 4', + 'icon': 'mdi:weather-cloudy', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.home_cloud_cover_night_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '13', + }) +# --- +# name: test_sensor[sensor.home_cloud_cover_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_cloud_cover_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-cloudy', + 'original_name': 'Cloud cover today', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cloud_cover_day_0d', + 'unique_id': '0123456-cloudcoverday-0', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.home_cloud_cover_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Cloud cover today', + 'icon': 'mdi:weather-cloudy', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.home_cloud_cover_today', + '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', + }) +# --- +# name: test_sensor[sensor.home_condition_day_1-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_day_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': 'Condition day 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'condition_day_1d', + 'unique_id': '0123456-longphraseday-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.home_condition_day_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Condition day 1', + }), + 'context': , + 'entity_id': 'sensor.home_condition_day_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Clouds and sun', + }) +# --- +# name: test_sensor[sensor.home_condition_day_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.home_condition_day_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': 'Condition day 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'condition_day_2d', + 'unique_id': '0123456-longphraseday-2', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.home_condition_day_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Condition day 2', + }), + 'context': , + 'entity_id': 'sensor.home_condition_day_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Very warm with a blend of sun and clouds', + }) +# --- +# name: test_sensor[sensor.home_condition_day_3-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_day_3', + '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 day 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'condition_day_3d', + 'unique_id': '0123456-longphraseday-3', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.home_condition_day_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Condition day 3', + }), + 'context': , + 'entity_id': 'sensor.home_condition_day_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Cooler with partial sunshine', + }) +# --- +# name: test_sensor[sensor.home_condition_day_4-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_day_4', + '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 day 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'condition_day_4d', + 'unique_id': '0123456-longphraseday-4', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.home_condition_day_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Condition day 4', + }), + 'context': , + 'entity_id': 'sensor.home_condition_day_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Intervals of clouds and sunshine', + }) +# --- +# name: test_sensor[sensor.home_condition_night_1-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_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': 'Condition night 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'condition_night_1d', + 'unique_id': '0123456-longphrasenight-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.home_condition_night_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Condition night 1', + }), + 'context': , + 'entity_id': 'sensor.home_condition_night_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Partly cloudy', + }) +# --- +# name: test_sensor[sensor.home_condition_night_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.home_condition_night_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': 'Condition night 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'condition_night_2d', + 'unique_id': '0123456-longphrasenight-2', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.home_condition_night_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Condition night 2', + }), + 'context': , + 'entity_id': 'sensor.home_condition_night_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Partly cloudy', + }) +# --- +# name: test_sensor[sensor.home_condition_night_3-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_3', + '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 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'condition_night_3d', + 'unique_id': '0123456-longphrasenight-3', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.home_condition_night_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Condition night 3', + }), + 'context': , + 'entity_id': 'sensor.home_condition_night_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Mainly clear', + }) +# --- +# name: test_sensor[sensor.home_condition_night_4-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_4', + '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 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'condition_night_4d', + 'unique_id': '0123456-longphrasenight-4', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.home_condition_night_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Condition night 4', + }), + 'context': , + 'entity_id': 'sensor.home_condition_night_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + '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({ + }), + '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.home_dew_point', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Dew point', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dew_point', + 'unique_id': '0123456-dewpoint', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_dew_point-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home Dew point', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_dew_point', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '16.2', + }) +# --- +# name: test_sensor[sensor.home_grass_pollen_day_1-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_1', + '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 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'grass_pollen_1d', + 'unique_id': '0123456-grass-1', + 'unit_of_measurement': 'p/m³', + }) +# --- +# name: test_sensor[sensor.home_grass_pollen_day_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Grass pollen day 1', + 'icon': 'mdi:grass', + 'level': 'low', + 'unit_of_measurement': 'p/m³', + }), + 'context': , + 'entity_id': 'sensor.home_grass_pollen_day_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.home_grass_pollen_day_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.home_grass_pollen_day_2', + '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 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'grass_pollen_2d', + 'unique_id': '0123456-grass-2', + 'unit_of_measurement': 'p/m³', + }) +# --- +# name: test_sensor[sensor.home_grass_pollen_day_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Grass pollen day 2', + 'icon': 'mdi:grass', + 'level': 'low', + 'unit_of_measurement': 'p/m³', + }), + 'context': , + 'entity_id': 'sensor.home_grass_pollen_day_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.home_grass_pollen_day_3-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_3', + '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 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'grass_pollen_3d', + 'unique_id': '0123456-grass-3', + 'unit_of_measurement': 'p/m³', + }) +# --- +# name: test_sensor[sensor.home_grass_pollen_day_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Grass pollen day 3', + 'icon': 'mdi:grass', + 'level': 'low', + 'unit_of_measurement': 'p/m³', + }), + 'context': , + 'entity_id': 'sensor.home_grass_pollen_day_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.home_grass_pollen_day_4-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_4', + '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 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'grass_pollen_4d', + 'unique_id': '0123456-grass-4', + 'unit_of_measurement': 'p/m³', + }) +# --- +# name: test_sensor[sensor.home_grass_pollen_day_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Grass pollen day 4', + 'icon': 'mdi:grass', + 'level': 'low', + 'unit_of_measurement': 'p/m³', + }), + 'context': , + 'entity_id': 'sensor.home_grass_pollen_day_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.home_grass_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_grass_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:grass', + 'original_name': 'Grass pollen today', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'grass_pollen_0d', + 'unique_id': '0123456-grass-0', + 'unit_of_measurement': 'p/m³', + }) +# --- +# name: test_sensor[sensor.home_grass_pollen_today-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³', + }), + 'context': , + 'entity_id': 'sensor.home_grass_pollen_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.home_hours_of_sun_day_1-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_hours_of_sun_day_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-partly-cloudy', + 'original_name': 'Hours of sun day 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'hours_of_sun_1d', + 'unique_id': '0123456-hoursofsun-1', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_hours_of_sun_day_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Hours of sun day 1', + 'icon': 'mdi:weather-partly-cloudy', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_hours_of_sun_day_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.4', + }) +# --- +# name: test_sensor[sensor.home_hours_of_sun_day_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.home_hours_of_sun_day_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-partly-cloudy', + 'original_name': 'Hours of sun day 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'hours_of_sun_2d', + 'unique_id': '0123456-hoursofsun-2', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_hours_of_sun_day_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Hours of sun day 2', + 'icon': 'mdi:weather-partly-cloudy', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_hours_of_sun_day_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.7', + }) +# --- +# name: test_sensor[sensor.home_hours_of_sun_day_3-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_hours_of_sun_day_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-partly-cloudy', + 'original_name': 'Hours of sun day 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'hours_of_sun_3d', + 'unique_id': '0123456-hoursofsun-3', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_hours_of_sun_day_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Hours of sun day 3', + 'icon': 'mdi:weather-partly-cloudy', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_hours_of_sun_day_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '9.4', + }) +# --- +# name: test_sensor[sensor.home_hours_of_sun_day_4-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_hours_of_sun_day_4', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-partly-cloudy', + 'original_name': 'Hours of sun day 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'hours_of_sun_4d', + 'unique_id': '0123456-hoursofsun-4', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_hours_of_sun_day_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Hours of sun day 4', + 'icon': 'mdi:weather-partly-cloudy', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_hours_of_sun_day_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '9.2', + }) +# --- +# name: test_sensor[sensor.home_hours_of_sun_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_hours_of_sun_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-partly-cloudy', + 'original_name': 'Hours of sun today', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'hours_of_sun_0d', + 'unique_id': '0123456-hoursofsun-0', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_hours_of_sun_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Hours of sun today', + 'icon': 'mdi:weather-partly-cloudy', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_hours_of_sun_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.2', + }) +# --- +# name: test_sensor[sensor.home_mold_pollen_day_1-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_day_1', + '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 day 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'mold_pollen_1d', + 'unique_id': '0123456-mold-1', + 'unit_of_measurement': 'p/m³', + }) +# --- +# name: test_sensor[sensor.home_mold_pollen_day_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Mold pollen day 1', + 'icon': 'mdi:blur', + 'level': 'low', + 'unit_of_measurement': 'p/m³', + }), + 'context': , + 'entity_id': 'sensor.home_mold_pollen_day_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.home_mold_pollen_day_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.home_mold_pollen_day_2', + '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 day 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'mold_pollen_2d', + 'unique_id': '0123456-mold-2', + 'unit_of_measurement': 'p/m³', + }) +# --- +# name: test_sensor[sensor.home_mold_pollen_day_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Mold pollen day 2', + 'icon': 'mdi:blur', + 'level': 'low', + 'unit_of_measurement': 'p/m³', + }), + 'context': , + 'entity_id': 'sensor.home_mold_pollen_day_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.home_mold_pollen_day_3-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_day_3', + '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 day 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'mold_pollen_3d', + 'unique_id': '0123456-mold-3', + 'unit_of_measurement': 'p/m³', + }) +# --- +# name: test_sensor[sensor.home_mold_pollen_day_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Mold pollen day 3', + 'icon': 'mdi:blur', + 'level': 'low', + 'unit_of_measurement': 'p/m³', + }), + 'context': , + 'entity_id': 'sensor.home_mold_pollen_day_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.home_mold_pollen_day_4-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_day_4', + '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 day 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'mold_pollen_4d', + 'unique_id': '0123456-mold-4', + 'unit_of_measurement': 'p/m³', + }) +# --- +# name: test_sensor[sensor.home_mold_pollen_day_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Mold pollen day 4', + 'icon': 'mdi:blur', + 'level': 'low', + 'unit_of_measurement': 'p/m³', + }), + 'context': , + 'entity_id': 'sensor.home_mold_pollen_day_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + '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({ + }), + '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.home_precipitation', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Precipitation', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'precipitation', + 'unique_id': '0123456-precipitation', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_precipitation-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'precipitation_intensity', + 'friendly_name': 'Home Precipitation', + 'state_class': , + 'type': None, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_precipitation', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensor[sensor.home_pressure_tendency-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'falling', + 'rising', + 'steady', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_pressure_tendency', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:gauge', + 'original_name': 'Pressure tendency', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pressure_tendency', + 'unique_id': '0123456-pressuretendency', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.home_pressure_tendency-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'enum', + 'friendly_name': 'Home Pressure tendency', + 'icon': 'mdi:gauge', + 'options': list([ + 'falling', + 'rising', + 'steady', + ]), + }), + 'context': , + 'entity_id': 'sensor.home_pressure_tendency', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'falling', + }) +# --- +# name: test_sensor[sensor.home_ragweed_pollen_day_1-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_1', + '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 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ragweed_pollen_1d', + 'unique_id': '0123456-ragweed-1', + 'unit_of_measurement': 'p/m³', + }) +# --- +# name: test_sensor[sensor.home_ragweed_pollen_day_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Ragweed pollen day 1', + 'icon': 'mdi:sprout', + 'level': 'low', + 'unit_of_measurement': 'p/m³', + }), + 'context': , + 'entity_id': 'sensor.home_ragweed_pollen_day_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.home_ragweed_pollen_day_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.home_ragweed_pollen_day_2', + '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 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ragweed_pollen_2d', + 'unique_id': '0123456-ragweed-2', + 'unit_of_measurement': 'p/m³', + }) +# --- +# name: test_sensor[sensor.home_ragweed_pollen_day_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Ragweed pollen day 2', + 'icon': 'mdi:sprout', + 'level': 'low', + 'unit_of_measurement': 'p/m³', + }), + 'context': , + 'entity_id': 'sensor.home_ragweed_pollen_day_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.home_ragweed_pollen_day_3-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_3', + '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 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ragweed_pollen_3d', + 'unique_id': '0123456-ragweed-3', + 'unit_of_measurement': 'p/m³', + }) +# --- +# name: test_sensor[sensor.home_ragweed_pollen_day_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Ragweed pollen day 3', + 'icon': 'mdi:sprout', + 'level': 'low', + 'unit_of_measurement': 'p/m³', + }), + 'context': , + 'entity_id': 'sensor.home_ragweed_pollen_day_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.home_ragweed_pollen_day_4-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_4', + '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 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ragweed_pollen_4d', + 'unique_id': '0123456-ragweed-4', + 'unit_of_measurement': 'p/m³', + }) +# --- +# name: test_sensor[sensor.home_ragweed_pollen_day_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Ragweed pollen day 4', + 'icon': 'mdi:sprout', + 'level': 'low', + 'unit_of_measurement': 'p/m³', + }), + 'context': , + 'entity_id': 'sensor.home_ragweed_pollen_day_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + '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({ + }), + '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.home_realfeel_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': 'RealFeel temperature', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'realfeel_temperature', + 'unique_id': '0123456-realfeeltemperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home RealFeel temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_realfeel_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '25.1', + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_max_day_1-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_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': 'RealFeel temperature max day 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'realfeel_temperature_max_1d', + 'unique_id': '0123456-realfeeltemperaturemax-1', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_max_day_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home RealFeel temperature max day 1', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_realfeel_temperature_max_day_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '28.9', + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_max_day_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.home_realfeel_temperature_max_day_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': 'RealFeel temperature max day 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'realfeel_temperature_max_2d', + 'unique_id': '0123456-realfeeltemperaturemax-2', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_max_day_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home RealFeel temperature max day 2', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_realfeel_temperature_max_day_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '31.6', + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_max_day_3-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_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': 'RealFeel temperature max day 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'realfeel_temperature_max_3d', + 'unique_id': '0123456-realfeeltemperaturemax-3', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_max_day_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home RealFeel temperature max day 3', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_realfeel_temperature_max_day_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '26.5', + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_max_day_4-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_4', + '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 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'realfeel_temperature_max_4d', + 'unique_id': '0123456-realfeeltemperaturemax-4', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_max_day_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home RealFeel temperature max day 4', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_realfeel_temperature_max_day_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '22.2', + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_max_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_max_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 max today', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'realfeel_temperature_max_0d', + 'unique_id': '0123456-realfeeltemperaturemax-0', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_max_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home RealFeel temperature max today', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_realfeel_temperature_max_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '29.8', + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_min_day_1-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_day_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': 'RealFeel temperature min day 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'realfeel_temperature_min_1d', + 'unique_id': '0123456-realfeeltemperaturemin-1', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_min_day_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home RealFeel temperature min day 1', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_realfeel_temperature_min_day_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '15.8', + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_min_day_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.home_realfeel_temperature_min_day_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': 'RealFeel temperature min day 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'realfeel_temperature_min_2d', + 'unique_id': '0123456-realfeeltemperaturemin-2', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_min_day_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home RealFeel temperature min day 2', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_realfeel_temperature_min_day_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '16.7', + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_min_day_3-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_day_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': 'RealFeel temperature min day 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'realfeel_temperature_min_3d', + 'unique_id': '0123456-realfeeltemperaturemin-3', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_min_day_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home RealFeel temperature min day 3', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_realfeel_temperature_min_day_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10.1', + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_min_day_4-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_day_4', + '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 day 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'realfeel_temperature_min_4d', + 'unique_id': '0123456-realfeeltemperaturemin-4', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_min_day_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home RealFeel temperature min day 4', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_realfeel_temperature_min_day_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + '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({ + }), + '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.home_realfeel_temperature_shade', + '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', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'realfeel_temperature_shade', + 'unique_id': '0123456-realfeeltemperatureshade', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_shade-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home RealFeel temperature shade', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_realfeel_temperature_shade', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '21.1', + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_shade_max_day_1-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_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': 'RealFeel temperature shade max day 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'realfeel_temperature_shade_max_1d', + 'unique_id': '0123456-realfeeltemperatureshademax-1', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_shade_max_day_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home RealFeel temperature shade max day 1', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_realfeel_temperature_shade_max_day_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '25.0', + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_shade_max_day_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.home_realfeel_temperature_shade_max_day_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': 'RealFeel temperature shade max day 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'realfeel_temperature_shade_max_2d', + 'unique_id': '0123456-realfeeltemperatureshademax-2', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_shade_max_day_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home RealFeel temperature shade max day 2', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_realfeel_temperature_shade_max_day_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '30.0', + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_shade_max_day_3-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_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': 'RealFeel temperature shade max day 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'realfeel_temperature_shade_max_3d', + 'unique_id': '0123456-realfeeltemperatureshademax-3', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_shade_max_day_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home RealFeel temperature shade max day 3', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_realfeel_temperature_shade_max_day_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '22.5', + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_shade_max_day_4-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_4', + '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 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'realfeel_temperature_shade_max_4d', + 'unique_id': '0123456-realfeeltemperatureshademax-4', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_shade_max_day_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home RealFeel temperature shade max day 4', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_realfeel_temperature_shade_max_day_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '19.5', + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_shade_max_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_shade_max_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 shade max today', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'realfeel_temperature_shade_max_0d', + 'unique_id': '0123456-realfeeltemperatureshademax-0', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_shade_max_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home RealFeel temperature shade max today', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_realfeel_temperature_shade_max_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '28.0', + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_shade_min_day_1-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_min_day_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': 'RealFeel temperature shade min day 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'realfeel_temperature_shade_min_1d', + 'unique_id': '0123456-realfeeltemperatureshademin-1', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_shade_min_day_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home RealFeel temperature shade min day 1', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_realfeel_temperature_shade_min_day_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '15.8', + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_shade_min_day_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.home_realfeel_temperature_shade_min_day_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': 'RealFeel temperature shade min day 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'realfeel_temperature_shade_min_2d', + 'unique_id': '0123456-realfeeltemperatureshademin-2', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_shade_min_day_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home RealFeel temperature shade min day 2', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_realfeel_temperature_shade_min_day_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '16.7', + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_shade_min_day_3-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_min_day_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': 'RealFeel temperature shade min day 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'realfeel_temperature_shade_min_3d', + 'unique_id': '0123456-realfeeltemperatureshademin-3', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_shade_min_day_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home RealFeel temperature shade min day 3', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_realfeel_temperature_shade_min_day_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10.1', + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_shade_min_day_4-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_min_day_4', + '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 min day 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'realfeel_temperature_shade_min_4d', + 'unique_id': '0123456-realfeeltemperatureshademin-4', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_shade_min_day_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home RealFeel temperature shade min day 4', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_realfeel_temperature_shade_min_day_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '11.3', + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_shade_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_shade_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 shade min today', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'realfeel_temperature_shade_min_0d', + 'unique_id': '0123456-realfeeltemperatureshademin-0', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_realfeel_temperature_shade_min_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home RealFeel temperature shade min today', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_realfeel_temperature_shade_min_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '15.1', + }) +# --- +# name: test_sensor[sensor.home_solar_irradiance_day_1-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_day_1', + '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 day 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'solar_irradiance_day_1d', + 'unique_id': '0123456-solarirradianceday-1', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_solar_irradiance_day_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Solar irradiance day 1', + 'icon': 'mdi:weather-sunny', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_solar_irradiance_day_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7447.1', + }) +# --- +# name: test_sensor[sensor.home_solar_irradiance_day_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.home_solar_irradiance_day_2', + '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 day 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'solar_irradiance_day_2d', + 'unique_id': '0123456-solarirradianceday-2', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_solar_irradiance_day_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Solar irradiance day 2', + 'icon': 'mdi:weather-sunny', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_solar_irradiance_day_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7447.1', + }) +# --- +# name: test_sensor[sensor.home_solar_irradiance_day_3-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_day_3', + '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 day 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'solar_irradiance_day_3d', + 'unique_id': '0123456-solarirradianceday-3', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_solar_irradiance_day_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Solar irradiance day 3', + 'icon': 'mdi:weather-sunny', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_solar_irradiance_day_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7447.1', + }) +# --- +# name: test_sensor[sensor.home_solar_irradiance_day_4-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_day_4', + '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 day 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'solar_irradiance_day_4d', + 'unique_id': '0123456-solarirradianceday-4', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_solar_irradiance_day_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Solar irradiance day 4', + 'icon': 'mdi:weather-sunny', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_solar_irradiance_day_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7447.1', + }) +# --- +# name: test_sensor[sensor.home_solar_irradiance_night_1-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_1', + '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 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'solar_irradiance_night_1d', + 'unique_id': '0123456-solarirradiancenight-1', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_solar_irradiance_night_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Solar irradiance night 1', + 'icon': 'mdi:weather-sunny', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_solar_irradiance_night_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '271.6', + }) +# --- +# name: test_sensor[sensor.home_solar_irradiance_night_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.home_solar_irradiance_night_2', + '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 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'solar_irradiance_night_2d', + 'unique_id': '0123456-solarirradiancenight-2', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_solar_irradiance_night_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Solar irradiance night 2', + 'icon': 'mdi:weather-sunny', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_solar_irradiance_night_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '271.6', + }) +# --- +# name: test_sensor[sensor.home_solar_irradiance_night_3-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_3', + '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 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'solar_irradiance_night_3d', + 'unique_id': '0123456-solarirradiancenight-3', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_solar_irradiance_night_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Solar irradiance night 3', + 'icon': 'mdi:weather-sunny', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_solar_irradiance_night_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7447.1', + }) +# --- +# name: test_sensor[sensor.home_solar_irradiance_night_4-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_4', + '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 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'solar_irradiance_night_4d', + 'unique_id': '0123456-solarirradiancenight-4', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_solar_irradiance_night_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Solar irradiance night 4', + 'icon': 'mdi:weather-sunny', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_solar_irradiance_night_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '276.1', + }) +# --- +# name: test_sensor[sensor.home_solar_irradiance_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_solar_irradiance_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': 'Solar irradiance today', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'solar_irradiance_day_0d', + 'unique_id': '0123456-solarirradianceday-0', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_solar_irradiance_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Solar irradiance today', + 'icon': 'mdi:weather-sunny', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_solar_irradiance_today', + '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', + }) +# --- +# name: test_sensor[sensor.home_thunderstorm_probability_day_1-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_day_1', + '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 day 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'thunderstorm_probability_day_1d', + 'unique_id': '0123456-thunderstormprobabilityday-1', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.home_thunderstorm_probability_day_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Thunderstorm probability day 1', + 'icon': 'mdi:weather-lightning', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.home_thunderstorm_probability_day_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '24', + }) +# --- +# name: test_sensor[sensor.home_thunderstorm_probability_day_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.home_thunderstorm_probability_day_2', + '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 day 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'thunderstorm_probability_day_2d', + 'unique_id': '0123456-thunderstormprobabilityday-2', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.home_thunderstorm_probability_day_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Thunderstorm probability day 2', + 'icon': 'mdi:weather-lightning', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.home_thunderstorm_probability_day_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4', + }) +# --- +# name: test_sensor[sensor.home_thunderstorm_probability_day_3-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_day_3', + '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 day 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'thunderstorm_probability_day_3d', + 'unique_id': '0123456-thunderstormprobabilityday-3', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.home_thunderstorm_probability_day_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Thunderstorm probability day 3', + 'icon': 'mdi:weather-lightning', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.home_thunderstorm_probability_day_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.home_thunderstorm_probability_day_4-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_day_4', + '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 day 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'thunderstorm_probability_day_4d', + 'unique_id': '0123456-thunderstormprobabilityday-4', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.home_thunderstorm_probability_day_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Thunderstorm probability day 4', + 'icon': 'mdi:weather-lightning', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.home_thunderstorm_probability_day_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.home_thunderstorm_probability_night_1-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_1', + '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 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'thunderstorm_probability_night_1d', + 'unique_id': '0123456-thunderstormprobabilitynight-1', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.home_thunderstorm_probability_night_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Thunderstorm probability night 1', + 'icon': 'mdi:weather-lightning', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.home_thunderstorm_probability_night_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.home_thunderstorm_probability_night_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.home_thunderstorm_probability_night_2', + '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 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'thunderstorm_probability_night_2d', + 'unique_id': '0123456-thunderstormprobabilitynight-2', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.home_thunderstorm_probability_night_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Thunderstorm probability night 2', + 'icon': 'mdi:weather-lightning', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.home_thunderstorm_probability_night_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '24', + }) +# --- +# name: test_sensor[sensor.home_thunderstorm_probability_night_3-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_3', + '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 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'thunderstorm_probability_night_3d', + 'unique_id': '0123456-thunderstormprobabilitynight-3', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.home_thunderstorm_probability_night_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Thunderstorm probability night 3', + 'icon': 'mdi:weather-lightning', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.home_thunderstorm_probability_night_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.home_thunderstorm_probability_night_4-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_4', + '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 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'thunderstorm_probability_night_4d', + 'unique_id': '0123456-thunderstormprobabilitynight-4', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.home_thunderstorm_probability_night_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Thunderstorm probability night 4', + 'icon': 'mdi:weather-lightning', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.home_thunderstorm_probability_night_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.home_thunderstorm_probability_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_thunderstorm_probability_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-lightning', + 'original_name': 'Thunderstorm probability today', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'thunderstorm_probability_day_0d', + 'unique_id': '0123456-thunderstormprobabilityday-0', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.home_thunderstorm_probability_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'friendly_name': 'Home Thunderstorm probability today', + 'icon': 'mdi:weather-lightning', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.home_thunderstorm_probability_today', + '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', + }) +# --- +# name: test_sensor[sensor.home_tree_pollen_day_1-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_day_1', + '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 day 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tree_pollen_1d', + 'unique_id': '0123456-tree-1', + 'unit_of_measurement': 'p/m³', + }) +# --- +# name: test_sensor[sensor.home_tree_pollen_day_1-state] + StateSnapshot({ + '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³', + }), + 'context': , + 'entity_id': 'sensor.home_tree_pollen_day_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.home_tree_pollen_day_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.home_tree_pollen_day_2', + '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 day 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tree_pollen_2d', + 'unique_id': '0123456-tree-2', + 'unit_of_measurement': 'p/m³', + }) +# --- +# name: test_sensor[sensor.home_tree_pollen_day_2-state] + StateSnapshot({ + '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³', + }), + 'context': , + 'entity_id': 'sensor.home_tree_pollen_day_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.home_tree_pollen_day_3-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_day_3', + '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 day 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tree_pollen_3d', + 'unique_id': '0123456-tree-3', + 'unit_of_measurement': 'p/m³', + }) +# --- +# name: test_sensor[sensor.home_tree_pollen_day_3-state] + StateSnapshot({ + '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³', + }), + 'context': , + 'entity_id': 'sensor.home_tree_pollen_day_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.home_tree_pollen_day_4-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_day_4', + '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 day 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tree_pollen_4d', + 'unique_id': '0123456-tree-4', + 'unit_of_measurement': 'p/m³', + }) +# --- +# name: test_sensor[sensor.home_tree_pollen_day_4-state] + StateSnapshot({ + '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³', + }), + 'context': , + 'entity_id': 'sensor.home_tree_pollen_day_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + '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({ + }), + '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.home_uv_index', + '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', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'uv_index', + 'unique_id': '0123456-uvindex', + 'unit_of_measurement': 'UV index', + }) +# --- +# name: test_sensor[sensor.home_uv_index-state] + StateSnapshot({ + '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', + }), + 'context': , + 'entity_id': 'sensor.home_uv_index', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6', + }) +# --- +# name: test_sensor[sensor.home_uv_index_day_1-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_1', + '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 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'uv_index_1d', + 'unique_id': '0123456-uvindex-1', + 'unit_of_measurement': 'UV index', + }) +# --- +# name: test_sensor[sensor.home_uv_index_day_1-state] + StateSnapshot({ + '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', + }), + 'context': , + 'entity_id': 'sensor.home_uv_index_day_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7', + }) +# --- +# name: test_sensor[sensor.home_uv_index_day_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.home_uv_index_day_2', + '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 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'uv_index_2d', + 'unique_id': '0123456-uvindex-2', + 'unit_of_measurement': 'UV index', + }) +# --- +# name: test_sensor[sensor.home_uv_index_day_2-state] + StateSnapshot({ + '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', + }), + 'context': , + 'entity_id': 'sensor.home_uv_index_day_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7', + }) +# --- +# name: test_sensor[sensor.home_uv_index_day_3-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_3', + '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 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'uv_index_3d', + 'unique_id': '0123456-uvindex-3', + 'unit_of_measurement': 'UV index', + }) +# --- +# name: test_sensor[sensor.home_uv_index_day_3-state] + StateSnapshot({ + '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', + }), + 'context': , + 'entity_id': 'sensor.home_uv_index_day_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6', + }) +# --- +# name: test_sensor[sensor.home_uv_index_day_4-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_4', + '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 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'uv_index_4d', + 'unique_id': '0123456-uvindex-4', + 'unit_of_measurement': 'UV index', + }) +# --- +# name: test_sensor[sensor.home_uv_index_day_4-state] + StateSnapshot({ + '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', + }), + 'context': , + 'entity_id': 'sensor.home_uv_index_day_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + '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({ + }), + '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.home_wet_bulb_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': 'Wet bulb temperature', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wet_bulb_temperature', + 'unique_id': '0123456-wetbulbtemperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wet_bulb_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home Wet bulb temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wet_bulb_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '18.6', + }) +# --- +# name: test_sensor[sensor.home_wind_chill_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.home_wind_chill_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': 'Wind chill temperature', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_chill_temperature', + 'unique_id': '0123456-windchilltemperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_chill_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'temperature', + 'friendly_name': 'Home Wind chill temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_chill_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '22.8', + }) +# --- +# name: test_sensor[sensor.home_wind_gust_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.home_wind_gust_speed', + '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', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_gust_speed', + 'unique_id': '0123456-windgust', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_gust_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'wind_speed', + 'friendly_name': 'Home Wind gust speed', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_gust_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.3', + }) +# --- +# name: test_sensor[sensor.home_wind_gust_speed_day_1-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_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': 'Wind gust speed day 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_gust_speed_day_1d', + 'unique_id': '0123456-windgustday-1', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_gust_speed_day_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'wind_speed', + 'direction': 'NW', + 'friendly_name': 'Home Wind gust speed day 1', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_gust_speed_day_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '14.8', + }) +# --- +# name: test_sensor[sensor.home_wind_gust_speed_day_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.home_wind_gust_speed_day_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': 'Wind gust speed day 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_gust_speed_day_2d', + 'unique_id': '0123456-windgustday-2', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_gust_speed_day_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'wind_speed', + 'direction': 'SSW', + 'friendly_name': 'Home Wind gust speed day 2', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_gust_speed_day_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '24.1', + }) +# --- +# name: test_sensor[sensor.home_wind_gust_speed_day_3-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_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': 'Wind gust speed day 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_gust_speed_day_3d', + 'unique_id': '0123456-windgustday-3', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_gust_speed_day_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'wind_speed', + 'direction': 'W', + 'friendly_name': 'Home Wind gust speed day 3', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_gust_speed_day_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '24.1', + }) +# --- +# name: test_sensor[sensor.home_wind_gust_speed_day_4-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_4', + '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 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_gust_speed_day_4d', + 'unique_id': '0123456-windgustday-4', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_gust_speed_day_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'wind_speed', + 'direction': 'W', + 'friendly_name': 'Home Wind gust speed day 4', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_gust_speed_day_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '27.8', + }) +# --- +# name: test_sensor[sensor.home_wind_gust_speed_night_1-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_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': 'Wind gust speed night 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_gust_speed_night_1d', + 'unique_id': '0123456-windgustnight-1', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_gust_speed_night_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'wind_speed', + 'direction': 'S', + 'friendly_name': 'Home Wind gust speed night 1', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_gust_speed_night_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '14.8', + }) +# --- +# name: test_sensor[sensor.home_wind_gust_speed_night_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.home_wind_gust_speed_night_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': 'Wind gust speed night 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_gust_speed_night_2d', + 'unique_id': '0123456-windgustnight-2', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_gust_speed_night_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'wind_speed', + 'direction': 'W', + 'friendly_name': 'Home Wind gust speed night 2', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_gust_speed_night_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '22.2', + }) +# --- +# name: test_sensor[sensor.home_wind_gust_speed_night_3-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_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': 'Wind gust speed night 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_gust_speed_night_3d', + 'unique_id': '0123456-windgustnight-3', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_gust_speed_night_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'wind_speed', + 'direction': 'W', + 'friendly_name': 'Home Wind gust speed night 3', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_gust_speed_night_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '18.5', + }) +# --- +# name: test_sensor[sensor.home_wind_gust_speed_night_4-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_4', + '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 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_gust_speed_night_4d', + 'unique_id': '0123456-windgustnight-4', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_gust_speed_night_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'wind_speed', + 'direction': 'W', + 'friendly_name': 'Home Wind gust speed night 4', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_gust_speed_night_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + '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({ + }), + '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.home_wind_speed', + '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', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_speed', + 'unique_id': '0123456-wind', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'wind_speed', + 'friendly_name': 'Home Wind speed', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '14.5', + }) +# --- +# name: test_sensor[sensor.home_wind_speed_day_1-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_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': 'Wind speed day 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_speed_day_1d', + 'unique_id': '0123456-windday-1', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_speed_day_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'wind_speed', + 'direction': 'WNW', + 'friendly_name': 'Home Wind speed day 1', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_speed_day_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '9.3', + }) +# --- +# name: test_sensor[sensor.home_wind_speed_day_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.home_wind_speed_day_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': 'Wind speed day 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_speed_day_2d', + 'unique_id': '0123456-windday-2', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_speed_day_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'wind_speed', + 'direction': 'SSW', + 'friendly_name': 'Home Wind speed day 2', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_speed_day_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '16.7', + }) +# --- +# name: test_sensor[sensor.home_wind_speed_day_3-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_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': 'Wind speed day 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_speed_day_3d', + 'unique_id': '0123456-windday-3', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_speed_day_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'wind_speed', + 'direction': 'WNW', + 'friendly_name': 'Home Wind speed day 3', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_speed_day_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '13.0', + }) +# --- +# name: test_sensor[sensor.home_wind_speed_day_4-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_4', + '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 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_speed_day_4d', + 'unique_id': '0123456-windday-4', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_speed_day_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'wind_speed', + 'direction': 'W', + 'friendly_name': 'Home Wind speed day 4', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_speed_day_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '18.5', + }) +# --- +# name: test_sensor[sensor.home_wind_speed_night_1-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_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': 'Wind speed night 1', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_speed_night_1d', + 'unique_id': '0123456-windnight-1', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_speed_night_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'wind_speed', + 'direction': 'SSE', + 'friendly_name': 'Home Wind speed night 1', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_speed_night_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.4', + }) +# --- +# name: test_sensor[sensor.home_wind_speed_night_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.home_wind_speed_night_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': 'Wind speed night 2', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_speed_night_2d', + 'unique_id': '0123456-windnight-2', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_speed_night_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'wind_speed', + 'direction': 'W', + 'friendly_name': 'Home Wind speed night 2', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_speed_night_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '9.3', + }) +# --- +# name: test_sensor[sensor.home_wind_speed_night_3-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_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': 'Wind speed night 3', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_speed_night_3d', + 'unique_id': '0123456-windnight-3', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_speed_night_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'wind_speed', + 'direction': 'W', + 'friendly_name': 'Home Wind speed night 3', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_speed_night_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '11.1', + }) +# --- +# name: test_sensor[sensor.home_wind_speed_night_4-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_4', + '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 4', + 'platform': 'accuweather', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wind_speed_night_4d', + 'unique_id': '0123456-windnight-4', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.home_wind_speed_night_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by AccuWeather', + 'device_class': 'wind_speed', + 'direction': 'W', + 'friendly_name': 'Home Wind speed night 4', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.home_wind_speed_night_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + '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', + }) +# --- diff --git a/tests/components/accuweather/test_config_flow.py b/tests/components/accuweather/test_config_flow.py index bc75ef17309..07b126e0856 100644 --- a/tests/components/accuweather/test_config_flow.py +++ b/tests/components/accuweather/test_config_flow.py @@ -1,10 +1,10 @@ """Define tests for the AccuWeather config flow.""" -from unittest.mock import PropertyMock, patch +from unittest.mock import patch from accuweather import ApiError, InvalidApiKeyError, RequestsExceededError -from homeassistant.components.accuweather.const import CONF_FORECAST, DOMAIN +from homeassistant.components.accuweather.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant @@ -140,52 +140,3 @@ async def test_create_entry(hass: HomeAssistant) -> None: assert result["data"][CONF_LATITUDE] == 55.55 assert result["data"][CONF_LONGITUDE] == 122.12 assert result["data"][CONF_API_KEY] == "32-character-string-1234567890qw" - - -async def test_options_flow(hass: HomeAssistant) -> None: - """Test config flow options.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - unique_id="123456", - data=VALID_CONFIG, - ) - config_entry.add_to_hass(hass) - - with ( - patch( - "homeassistant.components.accuweather.AccuWeather._async_get_data", - return_value=load_json_object_fixture("accuweather/location_data.json"), - ), - patch( - "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", - return_value=load_json_object_fixture( - "accuweather/current_conditions_data.json" - ), - ), - patch( - "homeassistant.components.accuweather.AccuWeather.async_get_daily_forecast" - ), - patch( - "homeassistant.components.accuweather.AccuWeather.requests_remaining", - new_callable=PropertyMock, - return_value=10, - ), - ): - 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"] == "init" - - result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={CONF_FORECAST: True} - ) - - assert result["type"] is FlowResultType.CREATE_ENTRY - assert config_entry.options == {CONF_FORECAST: True} - - await hass.async_block_till_done() - assert await hass.config_entries.async_unload(config_entry.entry_id) - await hass.async_block_till_done() diff --git a/tests/components/accuweather/test_diagnostics.py b/tests/components/accuweather/test_diagnostics.py index ab77fc337d0..593cde0f0a3 100644 --- a/tests/components/accuweather/test_diagnostics.py +++ b/tests/components/accuweather/test_diagnostics.py @@ -6,7 +6,6 @@ from homeassistant.core import HomeAssistant from . import init_integration -from tests.common import load_json_object_fixture from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator @@ -19,12 +18,6 @@ async def test_entry_diagnostics( """Test config entry diagnostics.""" entry = await init_integration(hass) - coordinator_data = load_json_object_fixture( - "current_conditions_data.json", "accuweather" - ) - - coordinator_data["forecast"] = [] - result = await get_diagnostics_for_config_entry(hass, hass_client, entry) assert result == snapshot diff --git a/tests/components/accuweather/test_init.py b/tests/components/accuweather/test_init.py index bb5b67e7918..08ad4a66dec 100644 --- a/tests/components/accuweather/test_init.py +++ b/tests/components/accuweather/test_init.py @@ -1,11 +1,14 @@ """Test init of AccuWeather integration.""" -from datetime import timedelta from unittest.mock import patch from accuweather import ApiError -from homeassistant.components.accuweather.const import DOMAIN +from homeassistant.components.accuweather.const import ( + DOMAIN, + UPDATE_INTERVAL_DAILY_FORECAST, + UPDATE_INTERVAL_OBSERVATION, +) from homeassistant.components.sensor import DOMAIN as SENSOR_PLATFORM from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE @@ -76,30 +79,8 @@ async def test_update_interval(hass: HomeAssistant) -> None: assert entry.state is ConfigEntryState.LOADED - current = load_json_object_fixture("accuweather/current_conditions_data.json") - future = utcnow() + timedelta(minutes=40) - - with patch( - "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", - return_value=current, - ) as mock_current: - assert mock_current.call_count == 0 - - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - - assert mock_current.call_count == 1 - - -async def test_update_interval_forecast(hass: HomeAssistant) -> None: - """Test correct update interval when forecast is True.""" - entry = await init_integration(hass, forecast=True) - - assert entry.state is ConfigEntryState.LOADED - current = load_json_object_fixture("accuweather/current_conditions_data.json") forecast = load_json_array_fixture("accuweather/forecast_data.json") - future = utcnow() + timedelta(minutes=80) with ( patch( @@ -114,10 +95,14 @@ async def test_update_interval_forecast(hass: HomeAssistant) -> None: assert mock_current.call_count == 0 assert mock_forecast.call_count == 0 - async_fire_time_changed(hass, future) + async_fire_time_changed(hass, utcnow() + UPDATE_INTERVAL_OBSERVATION) await hass.async_block_till_done() assert mock_current.call_count == 1 + + async_fire_time_changed(hass, utcnow() + UPDATE_INTERVAL_DAILY_FORECAST) + await hass.async_block_till_done() + assert mock_forecast.call_count == 1 diff --git a/tests/components/accuweather/test_sensor.py b/tests/components/accuweather/test_sensor.py index 8e6e01a4578..e79e49db96d 100644 --- a/tests/components/accuweather/test_sensor.py +++ b/tests/components/accuweather/test_sensor.py @@ -3,29 +3,20 @@ from datetime import timedelta from unittest.mock import PropertyMock, patch -from homeassistant.components.accuweather.const import ATTRIBUTION -from homeassistant.components.sensor import ( - ATTR_OPTIONS, - ATTR_STATE_CLASS, - SensorDeviceClass, - SensorStateClass, -) +from accuweather import ApiError, InvalidApiKeyError, RequestsExceededError +from aiohttp.client_exceptions import ClientConnectorError +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.components.accuweather.const import UPDATE_INTERVAL_DAILY_FORECAST from homeassistant.const import ( - ATTR_ATTRIBUTION, - ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, - ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, - CONCENTRATION_PARTS_PER_CUBIC_METER, - PERCENTAGE, STATE_UNAVAILABLE, - UV_INDEX, - UnitOfIrradiance, + Platform, UnitOfLength, UnitOfSpeed, UnitOfTemperature, - UnitOfTime, - UnitOfVolumetricFlux, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -42,517 +33,23 @@ from tests.common import ( ) -async def test_sensor_without_forecast( +async def test_sensor( hass: HomeAssistant, entity_registry_enabled_by_default: None, entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, ) -> None: - """Test states of the sensor without forecast.""" - await init_integration(hass) + """Test states of the sensor.""" + with patch("homeassistant.components.accuweather.PLATFORMS", [Platform.SENSOR]): + entry = await init_integration(hass) - state = hass.states.get("sensor.home_cloud_ceiling") - assert state - assert state.state == "3200.0" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_ICON) == "mdi:weather-fog" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfLength.METERS - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DISTANCE + entity_entries = er.async_entries_for_config_entry(entity_registry, entry.entry_id) - entry = entity_registry.async_get("sensor.home_cloud_ceiling") - assert entry - assert entry.unique_id == "0123456-ceiling" - assert entry.options["sensor"] == {"suggested_display_precision": 0} - - state = hass.states.get("sensor.home_precipitation") - assert state - assert state.state == "0.0" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR - ) - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get("type") is None - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_DEVICE_CLASS) - == SensorDeviceClass.PRECIPITATION_INTENSITY - ) - - entry = entity_registry.async_get("sensor.home_precipitation") - assert entry - assert entry.unique_id == "0123456-precipitation" - - state = hass.states.get("sensor.home_pressure_tendency") - assert state - assert state.state == "falling" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_ICON) == "mdi:gauge" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENUM - assert state.attributes.get(ATTR_STATE_CLASS) is None - assert state.attributes.get(ATTR_OPTIONS) == ["falling", "rising", "steady"] - - entry = entity_registry.async_get("sensor.home_pressure_tendency") - assert entry - assert entry.unique_id == "0123456-pressuretendency" - assert entry.translation_key == "pressure_tendency" - - state = hass.states.get("sensor.home_realfeel_temperature") - assert state - assert state.state == "25.1" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.home_realfeel_temperature") - assert entry - assert entry.unique_id == "0123456-realfeeltemperature" - - state = hass.states.get("sensor.home_uv_index") - assert state - assert state.state == "6" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UV_INDEX - assert state.attributes.get("level") == "High" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.home_uv_index") - assert entry - assert entry.unique_id == "0123456-uvindex" - - state = hass.states.get("sensor.home_apparent_temperature") - assert state - assert state.state == "22.8" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.home_apparent_temperature") - assert entry - assert entry.unique_id == "0123456-apparenttemperature" - - state = hass.states.get("sensor.home_cloud_cover") - assert state - assert state.state == "10" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - assert state.attributes.get(ATTR_ICON) == "mdi:weather-cloudy" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.home_cloud_cover") - assert entry - assert entry.unique_id == "0123456-cloudcover" - - state = hass.states.get("sensor.home_dew_point") - assert state - assert state.state == "16.2" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.home_dew_point") - assert entry - assert entry.unique_id == "0123456-dewpoint" - - state = hass.states.get("sensor.home_realfeel_temperature_shade") - assert state - assert state.state == "21.1" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.home_realfeel_temperature_shade") - assert entry - assert entry.unique_id == "0123456-realfeeltemperatureshade" - - state = hass.states.get("sensor.home_wet_bulb_temperature") - assert state - assert state.state == "18.6" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.home_wet_bulb_temperature") - assert entry - assert entry.unique_id == "0123456-wetbulbtemperature" - - state = hass.states.get("sensor.home_wind_chill_temperature") - assert state - assert state.state == "22.8" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.home_wind_chill_temperature") - assert entry - assert entry.unique_id == "0123456-windchilltemperature" - - state = hass.states.get("sensor.home_wind_gust_speed") - assert state - assert state.state == "20.3" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == UnitOfSpeed.KILOMETERS_PER_HOUR - ) - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WIND_SPEED - - entry = entity_registry.async_get("sensor.home_wind_gust_speed") - assert entry - assert entry.unique_id == "0123456-windgust" - - state = hass.states.get("sensor.home_wind_speed") - assert state - assert state.state == "14.5" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == UnitOfSpeed.KILOMETERS_PER_HOUR - ) - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WIND_SPEED - - entry = entity_registry.async_get("sensor.home_wind_speed") - assert entry - assert entry.unique_id == "0123456-wind" - - -async def test_sensor_with_forecast( - hass: HomeAssistant, - entity_registry_enabled_by_default: None, - entity_registry: er.EntityRegistry, -) -> None: - """Test states of the sensor with forecast.""" - await init_integration(hass, forecast=True) - - state = hass.states.get("sensor.home_hours_of_sun_today") - assert state - assert state.state == "7.2" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_ICON) == "mdi:weather-partly-cloudy" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTime.HOURS - assert state.attributes.get(ATTR_STATE_CLASS) is None - - entry = entity_registry.async_get("sensor.home_hours_of_sun_today") - assert entry - assert entry.unique_id == "0123456-hoursofsun-0" - - state = hass.states.get("sensor.home_realfeel_temperature_max_today") - assert state - assert state.state == "29.8" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE - assert state.attributes.get(ATTR_STATE_CLASS) is None - - entry = entity_registry.async_get("sensor.home_realfeel_temperature_max_today") - assert entry - - state = hass.states.get("sensor.home_realfeel_temperature_min_today") - assert state - assert state.state == "15.1" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE - assert state.attributes.get(ATTR_STATE_CLASS) is None - - entry = entity_registry.async_get("sensor.home_realfeel_temperature_min_today") - assert entry - assert entry.unique_id == "0123456-realfeeltemperaturemin-0" - - state = hass.states.get("sensor.home_thunderstorm_probability_today") - assert state - assert state.state == "40" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_ICON) == "mdi:weather-lightning" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - assert state.attributes.get(ATTR_STATE_CLASS) is None - - entry = entity_registry.async_get("sensor.home_thunderstorm_probability_today") - assert entry - assert entry.unique_id == "0123456-thunderstormprobabilityday-0" - - state = hass.states.get("sensor.home_thunderstorm_probability_tonight") - assert state - assert state.state == "40" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_ICON) == "mdi:weather-lightning" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - assert state.attributes.get(ATTR_STATE_CLASS) is None - - entry = entity_registry.async_get("sensor.home_thunderstorm_probability_tonight") - assert entry - assert entry.unique_id == "0123456-thunderstormprobabilitynight-0" - - state = hass.states.get("sensor.home_uv_index_today") - assert state - assert state.state == "5" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_ICON) == "mdi:weather-sunny" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UV_INDEX - assert state.attributes.get("level") == "moderate" - assert state.attributes.get(ATTR_STATE_CLASS) is None - - entry = entity_registry.async_get("sensor.home_uv_index_today") - assert entry - assert entry.unique_id == "0123456-uvindex-0" - - state = hass.states.get("sensor.home_air_quality_today") - assert state - assert state.state == "good" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_ICON) == "mdi:air-filter" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENUM - assert state.attributes.get(ATTR_OPTIONS) == [ - "good", - "hazardous", - "high", - "low", - "moderate", - "unhealthy", - ] - - state = hass.states.get("sensor.home_cloud_cover_today") - assert state - assert state.state == "58" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - assert state.attributes.get(ATTR_ICON) == "mdi:weather-cloudy" - assert state.attributes.get(ATTR_STATE_CLASS) is None - - entry = entity_registry.async_get("sensor.home_cloud_cover_today") - assert entry - assert entry.unique_id == "0123456-cloudcoverday-0" - - state = hass.states.get("sensor.home_cloud_cover_tonight") - assert state - assert state.state == "65" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - assert state.attributes.get(ATTR_ICON) == "mdi:weather-cloudy" - assert state.attributes.get(ATTR_STATE_CLASS) is None - - entry = entity_registry.async_get("sensor.home_cloud_cover_tonight") - assert entry - - state = hass.states.get("sensor.home_grass_pollen_today") - assert state - assert state.state == "0" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_PARTS_PER_CUBIC_METER - ) - assert state.attributes.get("level") == "low" - assert state.attributes.get(ATTR_ICON) == "mdi:grass" - assert state.attributes.get(ATTR_STATE_CLASS) is None - - entry = entity_registry.async_get("sensor.home_grass_pollen_today") - assert entry - assert entry.unique_id == "0123456-grass-0" - - state = hass.states.get("sensor.home_mold_pollen_today") - assert state - assert state.state == "0" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_PARTS_PER_CUBIC_METER - ) - assert state.attributes.get("level") == "low" - assert state.attributes.get(ATTR_ICON) == "mdi:blur" - - entry = entity_registry.async_get("sensor.home_mold_pollen_today") - assert entry - assert entry.unique_id == "0123456-mold-0" - - state = hass.states.get("sensor.home_ragweed_pollen_today") - assert state - assert state.state == "0" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_PARTS_PER_CUBIC_METER - ) - assert state.attributes.get("level") == "low" - assert state.attributes.get(ATTR_ICON) == "mdi:sprout" - - entry = entity_registry.async_get("sensor.home_ragweed_pollen_today") - assert entry - assert entry.unique_id == "0123456-ragweed-0" - - state = hass.states.get("sensor.home_realfeel_temperature_shade_max_today") - assert state - assert state.state == "28.0" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE - assert state.attributes.get(ATTR_STATE_CLASS) is None - - entry = entity_registry.async_get( - "sensor.home_realfeel_temperature_shade_max_today" - ) - assert entry - assert entry.unique_id == "0123456-realfeeltemperatureshademax-0" - - state = hass.states.get("sensor.home_realfeel_temperature_shade_min_today") - assert state - assert state.state == "15.1" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE - - entry = entity_registry.async_get( - "sensor.home_realfeel_temperature_shade_min_today" - ) - assert entry - assert entry.unique_id == "0123456-realfeeltemperatureshademin-0" - - state = hass.states.get("sensor.home_tree_pollen_today") - assert state - assert state.state == "0" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_PARTS_PER_CUBIC_METER - ) - assert state.attributes.get("level") == "low" - assert state.attributes.get(ATTR_ICON) == "mdi:tree-outline" - assert state.attributes.get(ATTR_STATE_CLASS) is None - - entry = entity_registry.async_get("sensor.home_tree_pollen_today") - assert entry - assert entry.unique_id == "0123456-tree-0" - - state = hass.states.get("sensor.home_wind_speed_today") - assert state - assert state.state == "13.0" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == UnitOfSpeed.KILOMETERS_PER_HOUR - ) - assert state.attributes.get("direction") == "SSE" - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WIND_SPEED - - entry = entity_registry.async_get("sensor.home_wind_speed_today") - assert entry - assert entry.unique_id == "0123456-windday-0" - - state = hass.states.get("sensor.home_wind_speed_tonight") - assert state - assert state.state == "7.4" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == UnitOfSpeed.KILOMETERS_PER_HOUR - ) - assert state.attributes.get("direction") == "WNW" - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_STATE_CLASS) is None - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WIND_SPEED - - entry = entity_registry.async_get("sensor.home_wind_speed_tonight") - assert entry - assert entry.unique_id == "0123456-windnight-0" - - state = hass.states.get("sensor.home_wind_gust_speed_today") - assert state - assert state.state == "29.6" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == UnitOfSpeed.KILOMETERS_PER_HOUR - ) - assert state.attributes.get("direction") == "S" - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_STATE_CLASS) is None - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WIND_SPEED - - entry = entity_registry.async_get("sensor.home_wind_gust_speed_today") - assert entry - assert entry.unique_id == "0123456-windgustday-0" - - state = hass.states.get("sensor.home_wind_gust_speed_tonight") - assert state - assert state.state == "18.5" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == UnitOfSpeed.KILOMETERS_PER_HOUR - ) - assert state.attributes.get("direction") == "WSW" - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_STATE_CLASS) is None - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WIND_SPEED - - entry = entity_registry.async_get("sensor.home_wind_gust_speed_tonight") - assert entry - assert entry.unique_id == "0123456-windgustnight-0" - - entry = entity_registry.async_get("sensor.home_air_quality_today") - assert entry - assert entry.unique_id == "0123456-airquality-0" - - state = hass.states.get("sensor.home_solar_irradiance_today") - assert state - assert state.state == "7447.1" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_ICON) == "mdi:weather-sunny" - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == UnitOfIrradiance.WATTS_PER_SQUARE_METER - ) - - entry = entity_registry.async_get("sensor.home_solar_irradiance_today") - assert entry - assert entry.unique_id == "0123456-solarirradianceday-0" - - state = hass.states.get("sensor.home_solar_irradiance_tonight") - assert state - assert state.state == "271.6" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_ICON) == "mdi:weather-sunny" - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == UnitOfIrradiance.WATTS_PER_SQUARE_METER - ) - - entry = entity_registry.async_get("sensor.home_solar_irradiance_tonight") - assert entry - assert entry.unique_id == "0123456-solarirradiancenight-0" - - state = hass.states.get("sensor.home_condition_today") - assert state - assert ( - state.state - == "Clouds and sunshine with a couple of showers and a thunderstorm around late this afternoon" - ) - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - - entry = entity_registry.async_get("sensor.home_condition_today") - assert entry - assert entry.unique_id == "0123456-longphraseday-0" - - state = hass.states.get("sensor.home_condition_tonight") - assert state - assert state.state == "Partly cloudy" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - - entry = entity_registry.async_get("sensor.home_condition_tonight") - assert entry - assert entry.unique_id == "0123456-longphrasenight-0" + assert entity_entries + for entity_entry in entity_entries: + assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry") + assert (state := hass.states.get(entity_entry.entity_id)) + assert state == snapshot(name=f"{entity_entry.entity_id}-state") async def test_availability(hass: HomeAssistant) -> None: @@ -599,24 +96,88 @@ async def test_availability(hass: HomeAssistant) -> None: assert state.state == "3200.0" +@pytest.mark.parametrize( + "exception", + [ + ApiError, + ConnectionError, + ClientConnectorError, + InvalidApiKeyError, + RequestsExceededError, + ], +) +async def test_availability_forecast(hass: HomeAssistant, exception: Exception) -> None: + """Ensure that we mark the entities unavailable correctly when service is offline.""" + current = load_json_object_fixture("accuweather/current_conditions_data.json") + forecast = load_json_array_fixture("accuweather/forecast_data.json") + entity_id = "sensor.home_hours_of_sun_day_2" + + await init_integration(hass) + + state = hass.states.get(entity_id) + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "5.7" + + with ( + patch( + "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", + return_value=current, + ), + patch( + "homeassistant.components.accuweather.AccuWeather.async_get_daily_forecast", + side_effect=exception, + ), + patch( + "homeassistant.components.accuweather.AccuWeather.requests_remaining", + new_callable=PropertyMock, + return_value=10, + ), + ): + async_fire_time_changed(hass, utcnow() + UPDATE_INTERVAL_DAILY_FORECAST) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_UNAVAILABLE + + with ( + patch( + "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", + return_value=current, + ), + patch( + "homeassistant.components.accuweather.AccuWeather.async_get_daily_forecast", + return_value=forecast, + ), + patch( + "homeassistant.components.accuweather.AccuWeather.requests_remaining", + new_callable=PropertyMock, + return_value=10, + ), + ): + async_fire_time_changed(hass, utcnow() + UPDATE_INTERVAL_DAILY_FORECAST * 2) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "5.7" + + async def test_manual_update_entity(hass: HomeAssistant) -> None: """Test manual update entity via service homeassistant/update_entity.""" - await init_integration(hass, forecast=True) + await init_integration(hass) await async_setup_component(hass, "homeassistant", {}) current = load_json_object_fixture("accuweather/current_conditions_data.json") - forecast = load_json_array_fixture("accuweather/forecast_data.json") with ( patch( "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", return_value=current, ) as mock_current, - patch( - "homeassistant.components.accuweather.AccuWeather.async_get_daily_forecast", - return_value=forecast, - ) as mock_forecast, patch( "homeassistant.components.accuweather.AccuWeather.requests_remaining", new_callable=PropertyMock, @@ -629,8 +190,7 @@ async def test_manual_update_entity(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: ["sensor.home_cloud_ceiling"]}, blocking=True, ) - assert mock_current.call_count == 1 - assert mock_forecast.call_count == 1 + assert mock_current.call_count == 1 async def test_sensor_imperial_units(hass: HomeAssistant) -> None: diff --git a/tests/components/accuweather/test_weather.py b/tests/components/accuweather/test_weather.py index 0b9d3e28fb2..b3237ca2958 100644 --- a/tests/components/accuweather/test_weather.py +++ b/tests/components/accuweather/test_weather.py @@ -7,7 +7,10 @@ from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.accuweather.const import ATTRIBUTION +from homeassistant.components.accuweather.const import ( + ATTRIBUTION, + UPDATE_INTERVAL_DAILY_FORECAST, +) from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, ATTR_WEATHER_APPARENT_TEMPERATURE, @@ -24,6 +27,7 @@ from homeassistant.components.weather import ( DOMAIN as WEATHER_DOMAIN, LEGACY_SERVICE_GET_FORECAST, SERVICE_GET_FORECASTS, + WeatherEntityFeature, ) from homeassistant.const import ( ATTR_ATTRIBUTION, @@ -65,7 +69,10 @@ async def test_weather(hass: HomeAssistant, entity_registry: er.EntityRegistry) assert state.attributes.get(ATTR_WEATHER_WIND_GUST_SPEED) == 20.3 assert state.attributes.get(ATTR_WEATHER_UV_INDEX) == 6 assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert ATTR_SUPPORTED_FEATURES not in state.attributes + assert ( + state.attributes.get(ATTR_SUPPORTED_FEATURES) + is WeatherEntityFeature.FORECAST_DAILY + ) entry = entity_registry.async_get("weather.home") assert entry @@ -118,22 +125,17 @@ async def test_availability(hass: HomeAssistant) -> None: async def test_manual_update_entity(hass: HomeAssistant) -> None: """Test manual update entity via service homeassistant/update_entity.""" - await init_integration(hass, forecast=True) + await init_integration(hass) await async_setup_component(hass, "homeassistant", {}) current = load_json_object_fixture("accuweather/current_conditions_data.json") - forecast = load_json_array_fixture("accuweather/forecast_data.json") with ( patch( "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", return_value=current, ) as mock_current, - patch( - "homeassistant.components.accuweather.AccuWeather.async_get_daily_forecast", - return_value=forecast, - ) as mock_forecast, patch( "homeassistant.components.accuweather.AccuWeather.requests_remaining", new_callable=PropertyMock, @@ -147,12 +149,11 @@ async def test_manual_update_entity(hass: HomeAssistant) -> None: blocking=True, ) assert mock_current.call_count == 1 - assert mock_forecast.call_count == 1 async def test_unsupported_condition_icon_data(hass: HomeAssistant) -> None: """Test with unsupported condition icon data.""" - await init_integration(hass, forecast=True, unsupported_icon=True) + await init_integration(hass, unsupported_icon=True) state = hass.states.get("weather.home") assert state.attributes.get(ATTR_FORECAST_CONDITION) is None @@ -171,7 +172,7 @@ async def test_forecast_service( service: str, ) -> None: """Test multiple forecast.""" - await init_integration(hass, forecast=True) + await init_integration(hass) response = await hass.services.async_call( WEATHER_DOMAIN, @@ -195,7 +196,7 @@ async def test_forecast_subscription( """Test multiple forecast.""" client = await hass_ws_client(hass) - await init_integration(hass, forecast=True) + await init_integration(hass) await client.send_json_auto_id( { @@ -235,7 +236,7 @@ async def test_forecast_subscription( return_value=10, ), ): - freezer.tick(timedelta(minutes=80) + timedelta(seconds=1)) + freezer.tick(UPDATE_INTERVAL_DAILY_FORECAST + timedelta(seconds=1)) await hass.async_block_till_done() msg = await client.receive_json() From 6dcfe861fdc1907cdd88909d4a88ab2de4029586 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 16 Apr 2024 18:34:57 -0500 Subject: [PATCH 610/967] Bump habluetooth to 2.5.2 (#115721) --- .../components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/bluetooth/test_scanner.py | 233 +++++++++--------- 5 files changed, 121 insertions(+), 120 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 58009216464..5939a03cefc 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -20,6 +20,6 @@ "bluetooth-auto-recovery==1.4.0", "bluetooth-data-tools==1.19.0", "dbus-fast==2.21.1", - "habluetooth==2.4.2" + "habluetooth==2.5.2" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b6f814c9f58..92522a69e53 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -28,7 +28,7 @@ dbus-fast==2.21.1 fnv-hash-fast==0.5.0 ha-av==10.1.1 ha-ffmpeg==3.2.0 -habluetooth==2.4.2 +habluetooth==2.5.2 hass-nabucasa==0.78.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 diff --git a/requirements_all.txt b/requirements_all.txt index 64d67ada712..c4e22294747 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1029,7 +1029,7 @@ ha-philipsjs==3.1.1 habitipy==0.2.0 # homeassistant.components.bluetooth -habluetooth==2.4.2 +habluetooth==2.5.2 # homeassistant.components.cloud hass-nabucasa==0.78.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fd3bad4398b..be395c42054 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -843,7 +843,7 @@ ha-philipsjs==3.1.1 habitipy==0.2.0 # homeassistant.components.bluetooth -habluetooth==2.4.2 +habluetooth==2.5.2 # homeassistant.components.cloud hass-nabucasa==0.78.0 diff --git a/tests/components/bluetooth/test_scanner.py b/tests/components/bluetooth/test_scanner.py index 523364e0dfd..5658aea523b 100644 --- a/tests/components/bluetooth/test_scanner.py +++ b/tests/components/bluetooth/test_scanner.py @@ -22,7 +22,6 @@ from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util from . import ( - _get_manager, async_setup_with_one_adapter, generate_advertisement_data, generate_ble_device, @@ -183,7 +182,7 @@ async def test_adapter_needs_reset_at_start( with ( patch( "habluetooth.scanner.OriginalBleakScanner.start", - side_effect=[BleakError(error), None], + side_effect=[BleakError(error), BleakError(error), None], ), patch( "habluetooth.util.recover_adapter", return_value=True @@ -239,46 +238,47 @@ async def test_recovery_from_dbus_restart( assert called_start == 1 - start_time_monotonic = time.monotonic() - mock_discovered = [MagicMock()] + start_time_monotonic = time.monotonic() + mock_discovered = [MagicMock()] - # Ensure we don't restart the scanner if we don't need to - with patch_bluetooth_time( - start_time_monotonic + 10, - ): - async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) - await hass.async_block_till_done() + # Ensure we don't restart the scanner if we don't need to + with patch_bluetooth_time( + start_time_monotonic + 10, + ): + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() - assert called_start == 1 + assert called_start == 1 - # Fire a callback to reset the timer - with patch_bluetooth_time( - start_time_monotonic, - ): - _callback( - generate_ble_device("44:44:33:11:23:42", "any_name"), - generate_advertisement_data(local_name="any_name"), - ) + # Fire a callback to reset the timer + with patch_bluetooth_time( + start_time_monotonic, + ): + _callback( + generate_ble_device("44:44:33:11:23:42", "any_name"), + generate_advertisement_data(local_name="any_name"), + ) - # Ensure we don't restart the scanner if we don't need to - with patch_bluetooth_time( - start_time_monotonic + 20, - ): - async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) - await hass.async_block_till_done() + # Ensure we don't restart the scanner if we don't need to + with patch_bluetooth_time( + start_time_monotonic + 20, + ): + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() - assert called_start == 1 + assert called_start == 1 - # We hit the timer, so we restart the scanner - with patch_bluetooth_time( - start_time_monotonic + SCANNER_WATCHDOG_TIMEOUT + 20, - ): - async_fire_time_changed( - hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL + timedelta(seconds=20) - ) - await hass.async_block_till_done() + # We hit the timer, so we restart the scanner + with patch_bluetooth_time( + start_time_monotonic + SCANNER_WATCHDOG_TIMEOUT + 20, + ): + async_fire_time_changed( + hass, + dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL + timedelta(seconds=20), + ) + await hass.async_block_till_done() - assert called_start == 2 + assert called_start == 2 async def test_adapter_recovery(hass: HomeAssistant, one_adapter: None) -> None: @@ -327,43 +327,42 @@ async def test_adapter_recovery(hass: HomeAssistant, one_adapter: None) -> None: assert called_start == 1 - scanner = _get_manager() - mock_discovered = [MagicMock()] + mock_discovered = [MagicMock()] - # Ensure we don't restart the scanner if we don't need to - with patch_bluetooth_time( - start_time_monotonic + 10, - ): - async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) - await hass.async_block_till_done() + # Ensure we don't restart the scanner if we don't need to + with patch_bluetooth_time( + start_time_monotonic + 10, + ): + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() - assert called_start == 1 + assert called_start == 1 - # Ensure we don't restart the scanner if we don't need to - with patch_bluetooth_time( - start_time_monotonic + 20, - ): - async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) - await hass.async_block_till_done() + # Ensure we don't restart the scanner if we don't need to + with patch_bluetooth_time( + start_time_monotonic + 20, + ): + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() - assert called_start == 1 + assert called_start == 1 - # We hit the timer with no detections, so we reset the adapter and restart the scanner - with ( - patch_bluetooth_time( - start_time_monotonic - + SCANNER_WATCHDOG_TIMEOUT - + SCANNER_WATCHDOG_INTERVAL.total_seconds(), - ), - patch( - "habluetooth.util.recover_adapter", return_value=True - ) as mock_recover_adapter, - ): - async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) - await hass.async_block_till_done() + # We hit the timer with no detections, so we reset the adapter and restart the scanner + with ( + patch_bluetooth_time( + start_time_monotonic + + SCANNER_WATCHDOG_TIMEOUT + + SCANNER_WATCHDOG_INTERVAL.total_seconds(), + ), + patch( + "habluetooth.util.recover_adapter", return_value=True + ) as mock_recover_adapter, + ): + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() - assert len(mock_recover_adapter.mock_calls) == 1 - assert called_start == 2 + assert len(mock_recover_adapter.mock_calls) == 1 + assert called_start == 2 async def test_adapter_scanner_fails_to_start_first_time( @@ -418,61 +417,61 @@ async def test_adapter_scanner_fails_to_start_first_time( assert called_start == 1 - scanner = _get_manager() - mock_discovered = [MagicMock()] + mock_discovered = [MagicMock()] - # Ensure we don't restart the scanner if we don't need to - with patch_bluetooth_time( - start_time_monotonic + 10, - ): - async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) - await hass.async_block_till_done() + # Ensure we don't restart the scanner if we don't need to + with patch_bluetooth_time( + start_time_monotonic + 10, + ): + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() - assert called_start == 1 + assert called_start == 1 - # Ensure we don't restart the scanner if we don't need to - with patch_bluetooth_time( - start_time_monotonic + 20, - ): - async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) - await hass.async_block_till_done() + # Ensure we don't restart the scanner if we don't need to + with patch_bluetooth_time( + start_time_monotonic + 20, + ): + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() - assert called_start == 1 + assert called_start == 1 - # We hit the timer with no detections, so we reset the adapter and restart the scanner - with ( - patch_bluetooth_time( - start_time_monotonic - + SCANNER_WATCHDOG_TIMEOUT - + SCANNER_WATCHDOG_INTERVAL.total_seconds(), - ), - patch( - "habluetooth.util.recover_adapter", return_value=True - ) as mock_recover_adapter, - ): - async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) - await hass.async_block_till_done() + # We hit the timer with no detections, so we reset the adapter and restart the scanner + with ( + patch_bluetooth_time( + start_time_monotonic + + SCANNER_WATCHDOG_TIMEOUT + + SCANNER_WATCHDOG_INTERVAL.total_seconds(), + ), + patch( + "habluetooth.util.recover_adapter", return_value=True + ) as mock_recover_adapter, + ): + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() - assert len(mock_recover_adapter.mock_calls) == 1 - assert called_start == 3 + assert len(mock_recover_adapter.mock_calls) == 1 + assert called_start == 4 - # We hit the timer again the previous start call failed, make sure - # we try again - with ( - patch_bluetooth_time( - start_time_monotonic - + SCANNER_WATCHDOG_TIMEOUT - + SCANNER_WATCHDOG_INTERVAL.total_seconds(), - ), - patch( - "habluetooth.util.recover_adapter", return_value=True - ) as mock_recover_adapter, - ): - async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) - await hass.async_block_till_done() + now_monotonic = time.monotonic() + # We hit the timer again the previous start call failed, make sure + # we try again + with ( + patch_bluetooth_time( + now_monotonic + + SCANNER_WATCHDOG_TIMEOUT * 2 + + SCANNER_WATCHDOG_INTERVAL.total_seconds(), + ), + patch( + "habluetooth.util.recover_adapter", return_value=True + ) as mock_recover_adapter, + ): + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() - assert len(mock_recover_adapter.mock_calls) == 1 - assert called_start == 4 + assert len(mock_recover_adapter.mock_calls) == 1 + assert called_start == 5 async def test_adapter_fails_to_start_and_takes_a_bit_to_init( @@ -497,9 +496,11 @@ async def test_adapter_fails_to_start_and_takes_a_bit_to_init( nonlocal called_start called_start += 1 if called_start == 1: - raise BleakError("org.bluez.Error.InProgress") - if called_start == 2: raise BleakError("org.freedesktop.DBus.Error.UnknownObject") + if called_start == 2: + raise BleakError("org.bluez.Error.InProgress") + if called_start == 3: + raise BleakError("org.bluez.Error.InProgress") async def stop(self, *args, **kwargs): """Mock Start.""" @@ -538,7 +539,7 @@ async def test_adapter_fails_to_start_and_takes_a_bit_to_init( ): await async_setup_with_one_adapter(hass) - assert called_start == 3 + assert called_start == 4 assert len(mock_recover_adapter.mock_calls) == 1 assert "Waiting for adapter to initialize" in caplog.text From 94a66fa64896a19ca657b8da9cc238ddcbb6cda6 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 17 Apr 2024 15:39:38 +1200 Subject: [PATCH 611/967] Bump aioesphomeapi to 24.1.0 (#115729) --- 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 4d5636a6f26..e700dddbb96 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -15,7 +15,7 @@ "iot_class": "local_push", "loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"], "requirements": [ - "aioesphomeapi==24.0.0", + "aioesphomeapi==24.1.0", "esphome-dashboard-api==1.2.3", "bleak-esphome==1.0.0" ], diff --git a/requirements_all.txt b/requirements_all.txt index c4e22294747..5c1df9b1844 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.0.0 +aioesphomeapi==24.1.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index be395c42054..0342a10d69c 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.0.0 +aioesphomeapi==24.1.0 # homeassistant.components.flo aioflo==2021.11.0 From 537b00db9a519f633edfd33c3622dcb090b8f6dd Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 17 Apr 2024 08:50:33 +0200 Subject: [PATCH 612/967] Fix stale comment in wheels.yml (#115736) --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 9f127acb57d..0148e476892 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -172,7 +172,7 @@ jobs: - name: Split requirements all run: | - # We split requirements all into two different files. + # We split requirements all into multiple files. # This is to prevent the build from running out of memory when # resolving packages on 32-bits systems (like armhf, armv7). From 20ff101015c2910c4ba66c066de14efc1da726ec Mon Sep 17 00:00:00 2001 From: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> Date: Wed, 17 Apr 2024 09:06:47 +0200 Subject: [PATCH 613/967] Multiple data disks detected: tweak strings (#115713) * Multiple data disks: tweak strings * Fix typos * Update homeassistant/components/hassio/strings.json Co-authored-by: Martin Hjelmare --------- Co-authored-by: Martin Hjelmare --- homeassistant/components/hassio/strings.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/hassio/strings.json b/homeassistant/components/hassio/strings.json index fe026be6633..6abf9ca6334 100644 --- a/homeassistant/components/hassio/strings.json +++ b/homeassistant/components/hassio/strings.json @@ -52,14 +52,14 @@ "fix_flow": { "step": { "fix_menu": { - "description": "`{reference}` is a filesystem with the name hassos-data and is not the active data disk. This can cause Home Assistant to choose the wrong data disk at system reboot.\n\nUse the 'Rename' option to rename the filesystem to prevent this. Use the 'Adopt' option to make that your data disk and rename the existing one. Alternatively you can move the data disk to the drive (overwriting its contents) or remove the drive from the system.", + "description": "At `{reference}`, we detected another active data disk (containing a file system `hassos-data` from another Home Assistant installation).\n\nYou need to decide what to do with it. Otherwise Home Assistant might choose the wrong data disk at system reboot.\n\nIf you don't want to use this data disk, unplug it from your system. If you leave it plugged in, choose one of the following options:", "menu_options": { - "system_rename_data_disk": "Rename", - "system_adopt_data_disk": "Adopt" + "system_rename_data_disk": "Mark as inactive data disk (rename file system)", + "system_adopt_data_disk": "Use the detected data disk instead of the current system" } }, "system_adopt_data_disk": { - "description": "This fix will initiate a system reboot which will make Home Assistant and all the Add-ons inaccessible for a brief period. After the reboot `{reference}` will be the data disk of Home Assistant and your existing data disk will be renamed and ignored." + "description": "Select submit to make `{reference}` the active data disk. The one and only.\n\nYou won't have access anymore to the current Home Assistant data (will be marked as inactive data disk). After reboot, your system will be in the state of the Home Assistant data on `{reference}`." } }, "abort": { From 34c3e523b4334e4b1079e90177d17e8cd1a7e0f9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 17 Apr 2024 02:10:06 -0500 Subject: [PATCH 614/967] Bump aiohttp to 3.9.5 (#115727) changelog: https://github.com/aio-libs/aiohttp/compare/v3.9.4...v3.9.5 --- 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 92522a69e53..2921a845244 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -5,7 +5,7 @@ aiodiscover==2.0.0 aiodns==3.2.0 aiohttp-fast-url-dispatcher==0.3.0 aiohttp-zlib-ng==0.3.1 -aiohttp==3.9.4 +aiohttp==3.9.5 aiohttp_cors==0.7.0 aiohttp_session==2.12.0 astral==2.2 diff --git a/pyproject.toml b/pyproject.toml index 3db19fe6851..8e521fc35a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ requires-python = ">=3.12.0" dependencies = [ "aiodns==3.2.0", - "aiohttp==3.9.4", + "aiohttp==3.9.5", "aiohttp_cors==0.7.0", "aiohttp_session==2.12.0", "aiohttp-fast-url-dispatcher==0.3.0", diff --git a/requirements.txt b/requirements.txt index 3c2a453b762..440e71d2286 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ # Home Assistant Core aiodns==3.2.0 -aiohttp==3.9.4 +aiohttp==3.9.5 aiohttp_cors==0.7.0 aiohttp_session==2.12.0 aiohttp-fast-url-dispatcher==0.3.0 From dae56222e90868ce3514012cee9a08ba6b649580 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 17 Apr 2024 02:13:55 -0500 Subject: [PATCH 615/967] Bump orjson to 3.10.1 (#115728) changelog: https://github.com/ijl/orjson/compare/3.9.15...3.10.1 --- 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 2921a845244..318738558e8 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -39,7 +39,7 @@ ifaddr==0.2.0 Jinja2==3.1.3 lru-dict==1.3.0 mutagen==1.47.0 -orjson==3.9.15 +orjson==3.10.1 packaging>=23.1 paho-mqtt==1.6.1 Pillow==10.3.0 diff --git a/pyproject.toml b/pyproject.toml index 8e521fc35a5..2216295d00d 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.9.15", + "orjson==3.10.1", "packaging>=23.1", "pip>=21.3.1", "psutil-home-assistant==0.0.1", diff --git a/requirements.txt b/requirements.txt index 440e71d2286..650a9bf7554 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.9.15 +orjson==3.10.1 packaging>=23.1 pip>=21.3.1 psutil-home-assistant==0.0.1 From ba4731ecb419a17a4d6742d27ea172e38b7686d0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 17 Apr 2024 11:54:53 +0200 Subject: [PATCH 616/967] Remove stale packages from uncommenting when building wheels (#115700) --- .github/workflows/wheels.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 0148e476892..3636906c305 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -142,11 +142,9 @@ jobs: run: | requirement_files="requirements_all.txt requirements_diff.txt" for requirement_file in ${requirement_files}; do - sed -i "s|# fritzconnection|fritzconnection|g" ${requirement_file} sed -i "s|# pyuserinput|pyuserinput|g" ${requirement_file} sed -i "s|# evdev|evdev|g" ${requirement_file} sed -i "s|# pycups|pycups|g" ${requirement_file} - sed -i "s|# homekit|homekit|g" ${requirement_file} sed -i "s|# decora-wifi|decora-wifi|g" ${requirement_file} sed -i "s|# python-gammu|python-gammu|g" ${requirement_file} From a3c767da2d407246e7df7adeae2b614f46a0dae2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 17 Apr 2024 12:03:06 +0200 Subject: [PATCH 617/967] Correct normalize_package_name (#115750) --- script/gen_requirements_all.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 94147e3932b..b6a37df9012 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -261,8 +261,8 @@ def normalize_package_name(requirement: str) -> str: if not match: return "" - # pipdeptree needs lowercase and dash instead of underscore as separator - return match.group(1).lower().replace("_", "-") + # pipdeptree needs lowercase and dash instead of underscore or period as separator + return match.group(1).lower().replace("_", "-").replace(".", "-") def comment_requirement(req: str) -> bool: From ff1ac1a5446801845e2637d5651fcb284528d7d0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 17 Apr 2024 12:12:51 +0200 Subject: [PATCH 618/967] Remove useless any in gen_requirements_all.comment_requirement (#115751) --- script/gen_requirements_all.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index b6a37df9012..7fc0907e756 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -267,9 +267,7 @@ def normalize_package_name(requirement: str) -> str: def comment_requirement(req: str) -> bool: """Comment out requirement. Some don't install on all systems.""" - return any( - normalize_package_name(req) == ign for ign in COMMENT_REQUIREMENTS_NORMALIZED - ) + return normalize_package_name(req) in COMMENT_REQUIREMENTS_NORMALIZED def gather_modules() -> dict[str, list[str]] | None: From fee1f2833d0ac0f436ff8be8898152eec52003b2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 17 Apr 2024 12:27:48 +0200 Subject: [PATCH 619/967] Fix hassfest requirements check (#115744) * Fix hassfest requirements check * Add electrasmart to ignore list --- script/hassfest/requirements.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py index 18d560f840f..aba7e5819d2 100644 --- a/script/hassfest/requirements.py +++ b/script/hassfest/requirements.py @@ -32,6 +32,7 @@ IGNORE_VIOLATIONS = { # Still has standard library requirements. "acmeda", "blink", + "electrasmart", "ezviz", "hdmi_cec", "juicenet", @@ -126,7 +127,7 @@ def validate_requirements(integration: Integration) -> None: f"Failed to normalize package name from requirement {req}", ) return - if (package == ign for ign in IGNORE_PACKAGES): + if package in IGNORE_PACKAGES: continue integration_requirements.add(req) integration_packages.add(package) @@ -150,7 +151,7 @@ def validate_requirements(integration: Integration) -> None: # Check for requirements incompatible with standard library. for req in all_integration_requirements: - if req in sys.stlib_module_names: + if req in sys.stdlib_module_names: integration.add_error( "requirements", f"Package {req} is not compatible with the Python standard library", From cb16465539eedd02c75558617b3a4fa37fd42175 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 17 Apr 2024 06:23:20 -0500 Subject: [PATCH 620/967] Keep track of top level components (#115586) * Keep track of top level components Currently we have to do a set comp for icons, translations, and integration platforms every time to split the top level components from the platforms. Keep track of the top level components in a seperate set so avoid having to do the setcomp every time. * remove impossible paths * remove unused code * preen * preen * fix * coverage and fixes * Update homeassistant/core.py * Update homeassistant/core.py * Update tests/test_core.py --- homeassistant/core.py | 44 +++++++++++++++- homeassistant/helpers/icon.py | 50 +++++++------------ homeassistant/helpers/integration_platform.py | 2 +- homeassistant/helpers/translation.py | 11 ++-- tests/helpers/test_icon.py | 4 +- tests/test_core.py | 17 +++++++ 6 files changed, 83 insertions(+), 45 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index d957953b609..69227f793a1 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -2670,6 +2670,41 @@ class ServiceRegistry: return await self._hass.async_add_executor_job(target, service_call) +class _ComponentSet(set[str]): + """Set of loaded components. + + This set contains both top level components and platforms. + + Examples: + `light`, `switch`, `hue`, `mjpeg.camera`, `universal.media_player`, + `homeassistant.scene` + + The top level components set only contains the top level components. + + """ + + def __init__(self, top_level_components: set[str]) -> None: + """Initialize the component set.""" + self._top_level_components = top_level_components + + def add(self, component: str) -> None: + """Add a component to the store.""" + if "." not in component: + self._top_level_components.add(component) + return super().add(component) + + def remove(self, component: str) -> None: + """Remove a component from the store.""" + if "." in component: + raise ValueError("_ComponentSet does not support removing sub-components") + self._top_level_components.remove(component) + return super().remove(component) + + def discard(self, component: str) -> None: + """Remove a component from the store.""" + raise NotImplementedError("_ComponentSet does not support discard, use remove") + + class Config: """Configuration settings for Home Assistant.""" @@ -2702,8 +2737,13 @@ class Config: # List of packages to skip when installing requirements on startup self.skip_pip_packages: list[str] = [] - # List of loaded components - self.components: set[str] = set() + # Set of loaded top level components + # This set is updated by _ComponentSet + # and should not be modified directly + self.top_level_components: set[str] = set() + + # Set of loaded components + self.components: _ComponentSet = _ComponentSet(self.top_level_components) # API (HTTP) server configuration self.api: ApiConfig | None = None diff --git a/homeassistant/helpers/icon.py b/homeassistant/helpers/icon.py index 973c93674b1..db90d38744a 100644 --- a/homeassistant/helpers/icon.py +++ b/homeassistant/helpers/icon.py @@ -6,6 +6,7 @@ import asyncio from collections.abc import Iterable from functools import lru_cache import logging +import pathlib from typing import Any from homeassistant.core import HomeAssistant, callback @@ -20,23 +21,17 @@ _LOGGER = logging.getLogger(__name__) @callback -def _component_icons_path(component: str, integration: Integration) -> str | None: +def _component_icons_path(integration: Integration) -> pathlib.Path: """Return the icons json file location for a component. Ex: components/hue/icons.json - If component is just a single file, will return None. """ - domain = component.rpartition(".")[-1] - - # If it's a component that is just one file, we don't support icons - # Example custom_components/my_component.py - if integration.file_path.name != domain: - return None - - return str(integration.file_path / "icons.json") + return integration.file_path / "icons.json" -def _load_icons_files(icons_files: dict[str, str]) -> dict[str, dict[str, Any]]: +def _load_icons_files( + icons_files: dict[str, pathlib.Path], +) -> dict[str, dict[str, Any]]: """Load and parse icons.json files.""" return { component: load_json_object(icons_file) @@ -53,19 +48,15 @@ async def _async_get_component_icons( icons: dict[str, Any] = {} # Determine files to load - files_to_load = {} - for loaded in components: - domain = loaded.rpartition(".")[-1] - if (path := _component_icons_path(loaded, integrations[domain])) is None: - icons[loaded] = {} - else: - files_to_load[loaded] = path + files_to_load = { + comp: _component_icons_path(integrations[comp]) for comp in components + } # Load files - if files_to_load and ( - load_icons_job := hass.async_add_executor_job(_load_icons_files, files_to_load) - ): - icons |= await load_icons_job + if files_to_load: + icons.update( + await hass.async_add_executor_job(_load_icons_files, files_to_load) + ) return icons @@ -108,8 +99,7 @@ class _IconsCache: _LOGGER.debug("Cache miss for: %s", components) integrations: dict[str, Integration] = {} - domains = {loaded.rpartition(".")[-1] for loaded in components} - ints_or_excs = await async_get_integrations(self._hass, domains) + ints_or_excs = await async_get_integrations(self._hass, components) for domain, int_or_exc in ints_or_excs.items(): if isinstance(int_or_exc, Exception): raise int_or_exc @@ -127,11 +117,9 @@ class _IconsCache: icons: dict[str, dict[str, Any]], ) -> None: """Extract resources into the cache.""" - categories: set[str] = set() - - for resource in icons.values(): - categories.update(resource) - + categories = { + category for component in icons.values() for category in component + } for category in categories: self._cache.setdefault(category, {}).update( build_resources(icons, components, category) @@ -151,9 +139,7 @@ async def async_get_icons( if integrations: components = set(integrations) else: - components = { - component for component in hass.config.components if "." not in component - } + components = hass.config.top_level_components if ICON_CACHE in hass.data: cache: _IconsCache = hass.data[ICON_CACHE] diff --git a/homeassistant/helpers/integration_platform.py b/homeassistant/helpers/integration_platform.py index 70846156702..be525b384e0 100644 --- a/homeassistant/helpers/integration_platform.py +++ b/homeassistant/helpers/integration_platform.py @@ -174,7 +174,7 @@ async def async_process_integration_platforms( integration_platforms = hass.data[DATA_INTEGRATION_PLATFORMS] async_register_preload_platform(hass, platform_name) - top_level_components = {comp for comp in hass.config.components if "." not in comp} + top_level_components = hass.config.top_level_components.copy() process_job = HassJob( catch_log_exception( process_platform, diff --git a/homeassistant/helpers/translation.py b/homeassistant/helpers/translation.py index 1fc2c3d075b..5ec3af2d382 100644 --- a/homeassistant/helpers/translation.py +++ b/homeassistant/helpers/translation.py @@ -212,8 +212,7 @@ class _TranslationCache: languages = [LOCALE_EN] if language == LOCALE_EN else [LOCALE_EN, language] integrations: dict[str, Integration] = {} - domains = {loaded.partition(".")[0] for loaded in components} - ints_or_excs = await async_get_integrations(self.hass, domains) + ints_or_excs = await async_get_integrations(self.hass, components) for domain, int_or_exc in ints_or_excs.items(): if isinstance(int_or_exc, Exception): _LOGGER.warning( @@ -345,7 +344,7 @@ async def async_get_translations( elif integrations is not None: components = set(integrations) else: - components = {comp for comp in hass.config.components if "." not in comp} + components = hass.config.top_level_components return await _async_get_translations_cache(hass).async_fetch( language, category, components @@ -364,11 +363,7 @@ def async_get_cached_translations( If integration is specified, return translations for it. Otherwise, default to all loaded integrations. """ - if integration is not None: - components = {integration} - else: - components = {comp for comp in hass.config.components if "." not in comp} - + components = {integration} if integration else hass.config.top_level_components return _async_get_translations_cache(hass).get_cached( language, category, components ) diff --git a/tests/helpers/test_icon.py b/tests/helpers/test_icon.py index e986a07d7d5..5ad5071266b 100644 --- a/tests/helpers/test_icon.py +++ b/tests/helpers/test_icon.py @@ -106,8 +106,8 @@ async def test_get_icons(hass: HomeAssistant) -> None: # Ensure icons file for platform isn't loaded, as that isn't supported icons = await icon.async_get_icons(hass, "entity") assert icons == {} - icons = await icon.async_get_icons(hass, "entity", ["test.switch"]) - assert icons == {} + with pytest.raises(ValueError, match="test.switch"): + await icon.async_get_icons(hass, "entity", ["test.switch"]) # Load up an custom integration hass.config.components.add("test_package") diff --git a/tests/test_core.py b/tests/test_core.py index 58738e3e52a..caed1433082 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -3411,3 +3411,20 @@ async def test_async_listen_with_run_immediately_deprecated( f"Detected code that calls `{method}` with run_immediately, which is " "deprecated and will be removed in Home Assistant 2025.5." ) in caplog.text + + +async def test_top_level_components(hass: HomeAssistant) -> None: + """Test top level components are updated when components change.""" + hass.config.components.add("homeassistant") + assert hass.config.components == {"homeassistant"} + assert hass.config.top_level_components == {"homeassistant"} + hass.config.components.add("homeassistant.scene") + assert hass.config.components == {"homeassistant", "homeassistant.scene"} + assert hass.config.top_level_components == {"homeassistant"} + hass.config.components.remove("homeassistant") + assert hass.config.components == {"homeassistant.scene"} + assert hass.config.top_level_components == set() + with pytest.raises(ValueError): + hass.config.components.remove("homeassistant.scene") + with pytest.raises(NotImplementedError): + hass.config.components.discard("homeassistant") From 5c018f6ffceefd02f0f42779c2348e802a7e0541 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 17 Apr 2024 14:10:26 +0200 Subject: [PATCH 621/967] Improve standard library violation check in hassfest (#115752) * Improve standard library violation check in hassfest * Improve prints * Improve error message --- script/hassfest/requirements.py | 45 ++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py index aba7e5819d2..ee63bf07f90 100644 --- a/script/hassfest/requirements.py +++ b/script/hassfest/requirements.py @@ -28,16 +28,9 @@ PACKAGE_REGEX = re.compile( PIP_REGEX = re.compile(r"^(--.+\s)?([-_\.\w\d]+.*(?:==|>=|<=|~=|!=|<|>|===)?.*$)") PIP_VERSION_RANGE_SEPARATOR = re.compile(r"^(==|>=|<=|~=|!=|<|>|===)?(.*)$") -IGNORE_VIOLATIONS = { - # Still has standard library requirements. - "acmeda", - "blink", +IGNORE_STANDARD_LIBRARY_VIOLATIONS = { + # Integrations which have standard library requirements. "electrasmart", - "ezviz", - "hdmi_cec", - "juicenet", - "lupusec", - "rainbird", "slide", "suez_water", } @@ -113,10 +106,6 @@ def validate_requirements(integration: Integration) -> None: if not validate_requirements_format(integration): return - # Some integrations have not been fixed yet so are allowed to have violations. - if integration.domain in IGNORE_VIOLATIONS: - return - integration_requirements = set() integration_packages = set() for req in integration.requirements: @@ -150,12 +139,34 @@ def validate_requirements(integration: Integration) -> None: return # Check for requirements incompatible with standard library. + standard_library_violations = set() for req in all_integration_requirements: if req in sys.stdlib_module_names: - integration.add_error( - "requirements", - f"Package {req} is not compatible with the Python standard library", - ) + standard_library_violations.add(req) + + if ( + standard_library_violations + and integration.domain not in IGNORE_STANDARD_LIBRARY_VIOLATIONS + ): + integration.add_error( + "requirements", + ( + f"Package {req} has dependencies {standard_library_violations} which " + "are not compatible with the Python standard library" + ), + ) + elif ( + not standard_library_violations + and integration.domain in IGNORE_STANDARD_LIBRARY_VIOLATIONS + ): + integration.add_error( + "requirements", + ( + f"Integration {integration.domain} no longer has requirements which are" + " incompatible with the Python standard library, remove it from " + "IGNORE_STANDARD_LIBRARY_VIOLATIONS" + ), + ) @cache From 92aae4d368e7a5953206bbcf7837bc0c35d548b4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 17 Apr 2024 14:23:47 +0200 Subject: [PATCH 622/967] Bump renault-api to 0.2.2 (#115738) --- homeassistant/components/renault/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/renault/manifest.json b/homeassistant/components/renault/manifest.json index 98e1c8b1e7c..9891c838950 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.1"] + "requirements": ["renault-api==0.2.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5c1df9b1844..2f8d3e43780 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2427,7 +2427,7 @@ refoss-ha==1.2.0 regenmaschine==2024.03.0 # homeassistant.components.renault -renault-api==0.2.1 +renault-api==0.2.2 # homeassistant.components.renson renson-endura-delta==1.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0342a10d69c..c70de76d37e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1882,7 +1882,7 @@ refoss-ha==1.2.0 regenmaschine==2024.03.0 # homeassistant.components.renault -renault-api==0.2.1 +renault-api==0.2.2 # homeassistant.components.renson renson-endura-delta==1.7.1 From 864c80fa55d2ad4f3e3c82bc85009ff5ae3958a7 Mon Sep 17 00:00:00 2001 From: Tomasz Date: Wed, 17 Apr 2024 14:24:34 +0200 Subject: [PATCH 623/967] Add Sanix integration (#106785) * Add Sanix integration * Add Sanix integration * Add sanix pypi package * Add Sanix integration * Add Sanix integration * Add Sanix integration * Add Sanix integration * Add Sanix integration * Add Sanix integration * Add Sanix integration * Add Sanix integration * Add Sanix integration * Add Sanix integration * Add Sanix integration * Add Sanix integration * Add Sanix integration * Fix ruff * Fix * Fix --------- Co-authored-by: Joostlek --- CODEOWNERS | 2 + homeassistant/components/sanix/__init__.py | 37 ++++++ homeassistant/components/sanix/config_flow.py | 60 +++++++++ homeassistant/components/sanix/const.py | 8 ++ homeassistant/components/sanix/coordinator.py | 36 +++++ homeassistant/components/sanix/icons.json | 9 ++ homeassistant/components/sanix/manifest.json | 9 ++ homeassistant/components/sanix/sensor.py | 125 ++++++++++++++++++ homeassistant/components/sanix/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/sanix/__init__.py | 13 ++ tests/components/sanix/conftest.py | 52 ++++++++ .../sanix/fixtures/get_measurements.json | 10 ++ tests/components/sanix/test_config_flow.py | 112 ++++++++++++++++ tests/components/sanix/test_init.py | 27 ++++ 18 files changed, 549 insertions(+) create mode 100644 homeassistant/components/sanix/__init__.py create mode 100644 homeassistant/components/sanix/config_flow.py create mode 100644 homeassistant/components/sanix/const.py create mode 100644 homeassistant/components/sanix/coordinator.py create mode 100644 homeassistant/components/sanix/icons.json create mode 100644 homeassistant/components/sanix/manifest.json create mode 100644 homeassistant/components/sanix/sensor.py create mode 100644 homeassistant/components/sanix/strings.json create mode 100644 tests/components/sanix/__init__.py create mode 100644 tests/components/sanix/conftest.py create mode 100644 tests/components/sanix/fixtures/get_measurements.json create mode 100644 tests/components/sanix/test_config_flow.py create mode 100644 tests/components/sanix/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index 56d42e5a3f3..a4224025acc 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1186,6 +1186,8 @@ build.json @home-assistant/supervisor /homeassistant/components/saj/ @fredericvl /homeassistant/components/samsungtv/ @chemelli74 @epenet /tests/components/samsungtv/ @chemelli74 @epenet +/homeassistant/components/sanix/ @tomaszsluszniak +/tests/components/sanix/ @tomaszsluszniak /homeassistant/components/scene/ @home-assistant/core /tests/components/scene/ @home-assistant/core /homeassistant/components/schedule/ @home-assistant/core diff --git a/homeassistant/components/sanix/__init__.py b/homeassistant/components/sanix/__init__.py new file mode 100644 index 00000000000..c8c5567eedc --- /dev/null +++ b/homeassistant/components/sanix/__init__.py @@ -0,0 +1,37 @@ +"""The Sanix integration.""" + +from sanix import Sanix + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_TOKEN, Platform +from homeassistant.core import HomeAssistant + +from .const import CONF_SERIAL_NUMBER, DOMAIN +from .coordinator import SanixCoordinator + +PLATFORMS: list[Platform] = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Sanix from a config entry.""" + + serial_no = entry.data[CONF_SERIAL_NUMBER] + token = entry.data[CONF_TOKEN] + + sanix_api = Sanix(serial_no, token) + coordinator = SanixCoordinator(hass, sanix_api) + + await coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = 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.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/sanix/config_flow.py b/homeassistant/components/sanix/config_flow.py new file mode 100644 index 00000000000..57aa5a5293a --- /dev/null +++ b/homeassistant/components/sanix/config_flow.py @@ -0,0 +1,60 @@ +"""Config flow for Sanix integration.""" + +import logging +from typing import Any + +from sanix import Sanix +from sanix.exceptions import SanixException, SanixInvalidAuthException +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_TOKEN + +from .const import CONF_SERIAL_NUMBER, DOMAIN, MANUFACTURER + +_LOGGER = logging.getLogger(__name__) + + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_SERIAL_NUMBER): str, + vol.Required(CONF_TOKEN): str, + } +) + + +class SanixConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Sanix.""" + + 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: + await self.async_set_unique_id(user_input[CONF_SERIAL_NUMBER]) + self._abort_if_unique_id_configured() + + sanix_api = Sanix(user_input[CONF_SERIAL_NUMBER], user_input[CONF_TOKEN]) + + try: + await self.hass.async_add_executor_job(sanix_api.fetch_data) + except SanixInvalidAuthException: + errors["base"] = "invalid_auth" + except SanixException: + errors["base"] = "unknown" + else: + return self.async_create_entry( + title=MANUFACTURER, + data=user_input, + ) + + return self.async_show_form( + step_id="user", + description_placeholders={"dashboard_url": "https://sanix.bitcomplex.pl/"}, + data_schema=STEP_USER_DATA_SCHEMA, + errors=errors, + ) diff --git a/homeassistant/components/sanix/const.py b/homeassistant/components/sanix/const.py new file mode 100644 index 00000000000..22ab33823d6 --- /dev/null +++ b/homeassistant/components/sanix/const.py @@ -0,0 +1,8 @@ +"""Constants for the Sanix integration.""" + +CONF_SERIAL_NUMBER = "serial_number" + +DOMAIN = "sanix" +MANUFACTURER = "Sanix" + +SANIX_API_HOST = "https://sanix.bitcomplex.pl" diff --git a/homeassistant/components/sanix/coordinator.py b/homeassistant/components/sanix/coordinator.py new file mode 100644 index 00000000000..d6362337a38 --- /dev/null +++ b/homeassistant/components/sanix/coordinator.py @@ -0,0 +1,36 @@ +"""Sanix Coordinator.""" + +from datetime import timedelta +import logging + +from sanix import Sanix +from sanix.exceptions import SanixException +from sanix.models import Measurement + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import MANUFACTURER + +_LOGGER = logging.getLogger(__name__) + + +class SanixCoordinator(DataUpdateCoordinator[Measurement]): + """Sanix coordinator.""" + + config_entry: ConfigEntry + + def __init__(self, hass: HomeAssistant, sanix_api: Sanix) -> None: + """Initialize coordinator.""" + super().__init__( + hass, _LOGGER, name=MANUFACTURER, update_interval=timedelta(hours=1) + ) + self._sanix_api = sanix_api + + async def _async_update_data(self) -> Measurement: + """Fetch data from API endpoint.""" + try: + return await self.hass.async_add_executor_job(self._sanix_api.fetch_data) + except SanixException as err: + raise UpdateFailed("Error while communicating with the API") from err diff --git a/homeassistant/components/sanix/icons.json b/homeassistant/components/sanix/icons.json new file mode 100644 index 00000000000..2b49cf8ea20 --- /dev/null +++ b/homeassistant/components/sanix/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "sensor": { + "fill_perc": { + "default": "mdi:water-percent" + } + } + } +} diff --git a/homeassistant/components/sanix/manifest.json b/homeassistant/components/sanix/manifest.json new file mode 100644 index 00000000000..4e1c6d56add --- /dev/null +++ b/homeassistant/components/sanix/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "sanix", + "name": "Sanix", + "codeowners": ["@tomaszsluszniak"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/sanix", + "iot_class": "cloud_polling", + "requirements": ["sanix==1.0.5"] +} diff --git a/homeassistant/components/sanix/sensor.py b/homeassistant/components/sanix/sensor.py new file mode 100644 index 00000000000..e780c6f2df0 --- /dev/null +++ b/homeassistant/components/sanix/sensor.py @@ -0,0 +1,125 @@ +"""Platform for Sanix integration.""" + +from collections.abc import Callable +from dataclasses import dataclass +from datetime import date, datetime + +from sanix.const import ( + ATTR_API_BATTERY, + ATTR_API_DEVICE_NO, + ATTR_API_DISTANCE, + ATTR_API_FILL_PERC, + ATTR_API_SERVICE_DATE, + ATTR_API_SSID, +) +from sanix.models import Measurement + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE, UnitOfLength +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 .const import DOMAIN, MANUFACTURER +from .coordinator import SanixCoordinator + + +@dataclass(frozen=True, kw_only=True) +class SanixSensorEntityDescription(SensorEntityDescription): + """Class describing Sanix Sensor entities.""" + + native_value_fn: Callable[[Measurement], int | datetime | date | str] + + +SENSOR_TYPES: tuple[SanixSensorEntityDescription, ...] = ( + SanixSensorEntityDescription( + key=ATTR_API_BATTERY, + translation_key=ATTR_API_BATTERY, + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, + native_value_fn=lambda data: data.battery, + ), + SanixSensorEntityDescription( + key=ATTR_API_DISTANCE, + translation_key=ATTR_API_DISTANCE, + native_unit_of_measurement=UnitOfLength.CENTIMETERS, + device_class=SensorDeviceClass.DISTANCE, + state_class=SensorStateClass.MEASUREMENT, + native_value_fn=lambda data: data.distance, + ), + SanixSensorEntityDescription( + key=ATTR_API_SERVICE_DATE, + translation_key=ATTR_API_SERVICE_DATE, + device_class=SensorDeviceClass.DATE, + native_value_fn=lambda data: data.service_date, + ), + SanixSensorEntityDescription( + key=ATTR_API_FILL_PERC, + translation_key=ATTR_API_FILL_PERC, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + native_value_fn=lambda data: data.fill_perc, + ), + SanixSensorEntityDescription( + key=ATTR_API_SSID, + translation_key=ATTR_API_SSID, + entity_registry_enabled_default=False, + native_value_fn=lambda data: data.ssid, + ), + SanixSensorEntityDescription( + key=ATTR_API_DEVICE_NO, + translation_key=ATTR_API_DEVICE_NO, + entity_registry_enabled_default=False, + native_value_fn=lambda data: data.device_no, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Sanix Sensor entities based on a config entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + SanixSensorEntity(coordinator, description) for description in SENSOR_TYPES + ) + + +class SanixSensorEntity(CoordinatorEntity[SanixCoordinator], SensorEntity): + """Sanix Sensor entity.""" + + _attr_has_entity_name = True + entity_description: SanixSensorEntityDescription + + def __init__( + self, + coordinator: SanixCoordinator, + description: SanixSensorEntityDescription, + ) -> None: + """Pass coordinator to CoordinatorEntity.""" + super().__init__(coordinator) + serial_no = str(coordinator.config_entry.unique_id) + + self._attr_unique_id = f"{serial_no}-{description.key}" + self.entity_description = description + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, serial_no)}, + entry_type=DeviceEntryType.SERVICE, + manufacturer=MANUFACTURER, + serial_number=serial_no, + ) + + @property + def native_value(self) -> int | datetime | date | str: + """Return the state of the sensor.""" + return self.entity_description.native_value_fn(self.coordinator.data) diff --git a/homeassistant/components/sanix/strings.json b/homeassistant/components/sanix/strings.json new file mode 100644 index 00000000000..6bff11e36af --- /dev/null +++ b/homeassistant/components/sanix/strings.json @@ -0,0 +1,36 @@ +{ + "config": { + "step": { + "user": { + "description": "To get the Serial number and the Token you just have to sign in to the [Sanix Dashboard]({dashboard_url}) and open the Help -> System version page.", + "data": { + "serial_number": "Serial number", + "token": "[%key:common::config_flow::data::access_token%]" + } + } + }, + "error": { + "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": { + "service_date": { + "name": "Service date" + }, + "fill_perc": { + "name": "Filled" + }, + "device_no": { + "name": "Device number" + }, + "ssid": { + "name": "SSID" + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 30d580ad1ea..fd87c965db5 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -457,6 +457,7 @@ FLOWS = { "rympro", "sabnzbd", "samsungtv", + "sanix", "schlage", "scrape", "screenlogic", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index fa2cec4d77a..d10cb3fdb80 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -5180,6 +5180,12 @@ } } }, + "sanix": { + "name": "Sanix", + "integration_type": "hub", + "config_flow": true, + "iot_class": "cloud_polling" + }, "satel_integra": { "name": "Satel Integra", "integration_type": "hub", diff --git a/requirements_all.txt b/requirements_all.txt index 2f8d3e43780..74b458920e8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2492,6 +2492,9 @@ samsungctl[websocket]==0.7.1 # homeassistant.components.samsungtv samsungtvws[async,encrypted]==2.6.0 +# homeassistant.components.sanix +sanix==1.0.5 + # homeassistant.components.satel_integra satel-integra==0.3.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c70de76d37e..ea115d4b29d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1929,6 +1929,9 @@ samsungctl[websocket]==0.7.1 # homeassistant.components.samsungtv samsungtvws[async,encrypted]==2.6.0 +# homeassistant.components.sanix +sanix==1.0.5 + # homeassistant.components.screenlogic screenlogicpy==0.10.0 diff --git a/tests/components/sanix/__init__.py b/tests/components/sanix/__init__.py new file mode 100644 index 00000000000..ef1a9c63fbe --- /dev/null +++ b/tests/components/sanix/__init__.py @@ -0,0 +1,13 @@ +"""Tests for Sanix.""" + +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) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/sanix/conftest.py b/tests/components/sanix/conftest.py new file mode 100644 index 00000000000..297416a6290 --- /dev/null +++ b/tests/components/sanix/conftest.py @@ -0,0 +1,52 @@ +"""Sanix tests configuration.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest +from sanix.models import Measurement + +from homeassistant.components.sanix.const import CONF_SERIAL_NUMBER, DOMAIN +from homeassistant.const import CONF_TOKEN + +from tests.common import MockConfigEntry, load_json_object_fixture + + +@pytest.fixture +def mock_sanix(): + """Build a fixture for the Sanix API that connects successfully and returns measurements.""" + fixture = load_json_object_fixture("sanix/get_measurements.json") + mock_sanix_api = MagicMock() + with ( + patch( + "homeassistant.components.sanix.config_flow.Sanix", + return_value=mock_sanix_api, + ) as mock_sanix_api, + patch( + "homeassistant.components.sanix.Sanix", + return_value=mock_sanix_api, + ), + ): + mock_sanix_api.return_value.fetch_data.return_value = Measurement(**fixture) + yield mock_sanix_api + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Mock a config entry.""" + return MockConfigEntry( + domain=DOMAIN, + title="Sanix", + unique_id="1810088", + data={CONF_SERIAL_NUMBER: "1234", CONF_TOKEN: "abcd"}, + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.sanix.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + yield mock_setup_entry diff --git a/tests/components/sanix/fixtures/get_measurements.json b/tests/components/sanix/fixtures/get_measurements.json new file mode 100644 index 00000000000..de6f4c41311 --- /dev/null +++ b/tests/components/sanix/fixtures/get_measurements.json @@ -0,0 +1,10 @@ +{ + "device_no": "SANIX-1810088", + "status": "1", + "time": "30.12.2023 03:10:21", + "ssid": "Wifi", + "battery": "100", + "distance": "109", + "fill_perc": 32, + "service_date": "15.06.2024" +} diff --git a/tests/components/sanix/test_config_flow.py b/tests/components/sanix/test_config_flow.py new file mode 100644 index 00000000000..abd91ee306c --- /dev/null +++ b/tests/components/sanix/test_config_flow.py @@ -0,0 +1,112 @@ +"""Define tests for the Sanix config flow.""" + +from unittest.mock import MagicMock + +import pytest +from sanix.exceptions import SanixException, SanixInvalidAuthException + +from homeassistant.components.sanix.const import ( + CONF_SERIAL_NUMBER, + DOMAIN, + MANUFACTURER, +) +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_TOKEN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + +CONFIG = {CONF_SERIAL_NUMBER: "1810088", CONF_TOKEN: "75868dcf8ea4c64e2063f6c4e70132d2"} + + +async def test_create_entry( + hass: HomeAssistant, mock_sanix: MagicMock, mock_setup_entry +) -> None: + """Test that the user step works.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + CONFIG, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == MANUFACTURER + assert result["data"] == { + CONF_SERIAL_NUMBER: "1810088", + CONF_TOKEN: "75868dcf8ea4c64e2063f6c4e70132d2", + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + ("exception", "error"), + [ + (SanixInvalidAuthException("Invalid auth"), "invalid_auth"), + (SanixException("Something went wrong"), "unknown"), + ], +) +async def test_form_exceptions( + hass: HomeAssistant, + exception: Exception, + error: str, + mock_sanix: MagicMock, + mock_setup_entry, +) -> None: + """Test Form exceptions.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + mock_sanix.return_value.fetch_data.side_effect = exception + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + CONFIG, + ) + + mock_sanix.return_value.fetch_data.side_effect = None + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + CONFIG, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Sanix" + assert result["data"] == { + CONF_SERIAL_NUMBER: "1810088", + CONF_TOKEN: "75868dcf8ea4c64e2063f6c4e70132d2", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_duplicate_error( + hass: HomeAssistant, mock_sanix: MagicMock, mock_config_entry: MockConfigEntry +) -> None: + """Test that errors are shown when duplicates are added.""" + + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + CONFIG, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/sanix/test_init.py b/tests/components/sanix/test_init.py new file mode 100644 index 00000000000..57e4920da11 --- /dev/null +++ b/tests/components/sanix/test_init.py @@ -0,0 +1,27 @@ +"""Test the Home Assistant analytics init module.""" + +from __future__ import annotations + +from unittest.mock import AsyncMock + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.components.sanix import setup_integration + + +async def test_load_unload_entry( + hass: HomeAssistant, + mock_sanix: 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_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED From 3f62267a482fa800c5f556b2b8dbcb53a96ef591 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 17 Apr 2024 14:29:49 +0200 Subject: [PATCH 624/967] Fix flaky qld_bushfire test (#115757) --- tests/components/qld_bushfire/test_geo_location.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/qld_bushfire/test_geo_location.py b/tests/components/qld_bushfire/test_geo_location.py index 522c5fabe90..20659182726 100644 --- a/tests/components/qld_bushfire/test_geo_location.py +++ b/tests/components/qld_bushfire/test_geo_location.py @@ -169,7 +169,7 @@ async def test_setup(hass: HomeAssistant, freezer: FrozenDateTimeFactory) -> Non [mock_entry_1, mock_entry_4, mock_entry_3], ) async_fire_time_changed(hass, utcnow + SCAN_INTERVAL) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) all_states = hass.states.async_all() assert len(all_states) == 3 From e45583b83b9d434370a23a830e1ba72a4997e26d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 17 Apr 2024 15:00:10 +0200 Subject: [PATCH 625/967] Fix homeworks import flow (#115761) --- .../components/homeworks/config_flow.py | 10 +----- .../components/homeworks/test_config_flow.py | 32 +------------------ 2 files changed, 2 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/homeworks/config_flow.py b/homeassistant/components/homeworks/config_flow.py index b2fe4e0e022..e54bbc61141 100644 --- a/homeassistant/components/homeworks/config_flow.py +++ b/homeassistant/components/homeworks/config_flow.py @@ -565,15 +565,7 @@ class HomeworksConfigFlowHandler(ConfigFlow, domain=DOMAIN): CONF_KEYPADS: [ { CONF_ADDR: keypad[CONF_ADDR], - CONF_BUTTONS: [ - { - CONF_LED: button[CONF_LED], - CONF_NAME: button[CONF_NAME], - CONF_NUMBER: button[CONF_NUMBER], - CONF_RELEASE_DELAY: button[CONF_RELEASE_DELAY], - } - for button in keypad[CONF_BUTTONS] - ], + CONF_BUTTONS: [], CONF_NAME: keypad[CONF_NAME], } for keypad in config[CONF_KEYPADS] diff --git a/tests/components/homeworks/test_config_flow.py b/tests/components/homeworks/test_config_flow.py index 53128c4cd65..6a5ae68e6ab 100644 --- a/tests/components/homeworks/test_config_flow.py +++ b/tests/components/homeworks/test_config_flow.py @@ -9,7 +9,6 @@ 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_BUTTONS, CONF_DIMMERS, CONF_INDEX, CONF_KEYPADS, @@ -161,26 +160,6 @@ async def test_import_flow( { CONF_ADDR: "[02:08:02:01]", CONF_NAME: "Foyer Keypad", - CONF_BUTTONS: [ - { - CONF_NAME: "Morning", - CONF_NUMBER: 1, - CONF_LED: True, - CONF_RELEASE_DELAY: None, - }, - { - CONF_NAME: "Relax", - CONF_NUMBER: 2, - CONF_LED: True, - CONF_RELEASE_DELAY: None, - }, - { - CONF_NAME: "Dim up", - CONF_NUMBER: 3, - CONF_LED: False, - CONF_RELEASE_DELAY: 0.2, - }, - ], } ], }, @@ -207,16 +186,7 @@ async def test_import_flow( "keypads": [ { "addr": "[02:08:02:01]", - "buttons": [ - { - "led": True, - "name": "Morning", - "number": 1, - "release_delay": None, - }, - {"led": True, "name": "Relax", "number": 2, "release_delay": None}, - {"led": False, "name": "Dim up", "number": 3, "release_delay": 0.2}, - ], + "buttons": [], "name": "Foyer Keypad", } ], From 764a0f29cc3f557d1365da08f6e6c24ffbd65b91 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 17 Apr 2024 15:04:37 +0200 Subject: [PATCH 626/967] Allow [##:##:##] type keypad address in homeworks (#115762) Allow [##:##:##] type keypad address --- homeassistant/components/homeworks/config_flow.py | 2 +- tests/components/homeworks/test_config_flow.py | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homeworks/config_flow.py b/homeassistant/components/homeworks/config_flow.py index e54bbc61141..b9515c306d6 100644 --- a/homeassistant/components/homeworks/config_flow.py +++ b/homeassistant/components/homeworks/config_flow.py @@ -93,7 +93,7 @@ BUTTON_EDIT = { } -validate_addr = cv.matches_regex(r"\[\d\d:\d\d:\d\d:\d\d\]") +validate_addr = cv.matches_regex(r"\[(?:\d\d:)?\d\d:\d\d:\d\d\]") async def validate_add_controller( diff --git a/tests/components/homeworks/test_config_flow.py b/tests/components/homeworks/test_config_flow.py index 6a5ae68e6ab..d00b5a13150 100644 --- a/tests/components/homeworks/test_config_flow.py +++ b/tests/components/homeworks/test_config_flow.py @@ -544,8 +544,12 @@ async def test_options_add_remove_light_flow( ) +@pytest.mark.parametrize("keypad_address", ["[02:08:03:01]", "[02:08:03]"]) async def test_options_add_remove_keypad_flow( - hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_homeworks: MagicMock + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_homeworks: MagicMock, + keypad_address: str, ) -> None: """Test options flow to add and remove a keypad.""" mock_config_entry.add_to_hass(hass) @@ -566,7 +570,7 @@ async def test_options_add_remove_keypad_flow( result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ - CONF_ADDR: "[02:08:03:01]", + CONF_ADDR: keypad_address, CONF_NAME: "Hall Keypad", }, ) @@ -592,7 +596,7 @@ async def test_options_add_remove_keypad_flow( ], "name": "Foyer Keypad", }, - {"addr": "[02:08:03:01]", "buttons": [], "name": "Hall Keypad"}, + {"addr": keypad_address, "buttons": [], "name": "Hall Keypad"}, ], "port": 1234, } @@ -612,7 +616,7 @@ async def test_options_add_remove_keypad_flow( assert result["step_id"] == "remove_keypad" assert result["data_schema"].schema["index"].options == { "0": "Foyer Keypad ([02:08:02:01])", - "1": "Hall Keypad ([02:08:03:01])", + "1": f"Hall Keypad ({keypad_address})", } result = await hass.config_entries.options.async_configure( @@ -625,7 +629,7 @@ async def test_options_add_remove_keypad_flow( {"addr": "[02:08:01:01]", "name": "Foyer Sconces", "rate": 1.0}, ], "host": "192.168.0.1", - "keypads": [{"addr": "[02:08:03:01]", "buttons": [], "name": "Hall Keypad"}], + "keypads": [{"addr": keypad_address, "buttons": [], "name": "Hall Keypad"}], "port": 1234, } await hass.async_block_till_done() From be0926b7b8cb08415832d81e6559a43f062c85a7 Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Wed, 17 Apr 2024 15:21:54 +0200 Subject: [PATCH 627/967] Add config flow to enigma2 (#106348) * add config flow to enigma2 * Apply suggestions from code review Co-authored-by: Joost Lekkerkerker * fix suggested change * use parametrize for config flow tests * Restore PLATFORM_SCHEMA and add create_issue to async_setup_platform * fix docstring * remove name, refactor config flow * bump dependency * remove name, add verify_ssl, use async_create_clientsession * use translation key, change integration type to device * Bump openwebifpy to 4.2.1 * cleanup, remove CONF_NAME from entity, add async_set_unique_id * clear unneeded constants, fix tests * fix tests * move _attr_translation_key out of init * update test requirement * Address review comments * address review comments * clear strings.json * Review coments --------- Co-authored-by: Joost Lekkerkerker --- CODEOWNERS | 1 + homeassistant/components/enigma2/__init__.py | 47 ++++++ .../components/enigma2/config_flow.py | 158 ++++++++++++++++++ homeassistant/components/enigma2/const.py | 1 + .../components/enigma2/manifest.json | 2 + .../components/enigma2/media_player.py | 80 ++++----- homeassistant/components/enigma2/strings.json | 30 ++++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 4 +- requirements_test_all.txt | 3 + tests/components/enigma2/__init__.py | 1 + tests/components/enigma2/conftest.py | 90 ++++++++++ tests/components/enigma2/test_config_flow.py | 149 +++++++++++++++++ tests/components/enigma2/test_init.py | 38 +++++ 14 files changed, 565 insertions(+), 40 deletions(-) create mode 100644 homeassistant/components/enigma2/config_flow.py create mode 100644 homeassistant/components/enigma2/strings.json create mode 100644 tests/components/enigma2/__init__.py create mode 100644 tests/components/enigma2/conftest.py create mode 100644 tests/components/enigma2/test_config_flow.py create mode 100644 tests/components/enigma2/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index a4224025acc..b2de3031cf8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -389,6 +389,7 @@ build.json @home-assistant/supervisor /homeassistant/components/energyzero/ @klaasnicolaas /tests/components/energyzero/ @klaasnicolaas /homeassistant/components/enigma2/ @autinerd +/tests/components/enigma2/ @autinerd /homeassistant/components/enocean/ @bdurrer /tests/components/enocean/ @bdurrer /homeassistant/components/enphase_envoy/ @bdraco @cgarwood @dgomes @joostlek @catsmanac diff --git a/homeassistant/components/enigma2/__init__.py b/homeassistant/components/enigma2/__init__.py index 11cd4d9a804..241ca7444fb 100644 --- a/homeassistant/components/enigma2/__init__.py +++ b/homeassistant/components/enigma2/__init__.py @@ -1 +1,48 @@ """Support for Enigma2 devices.""" + +from openwebif.api import OpenWebIfDevice +from yarl import URL + +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.helpers.aiohttp_client import async_create_clientsession + +from .const import DOMAIN + +PLATFORMS = [Platform.MEDIA_PLAYER] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Enigma2 from a config entry.""" + base_url = URL.build( + scheme="http" if not entry.data[CONF_SSL] else "https", + host=entry.data[CONF_HOST], + port=entry.data[CONF_PORT], + user=entry.data.get(CONF_USERNAME), + password=entry.data.get(CONF_PASSWORD), + ) + + session = async_create_clientsession( + hass, verify_ssl=entry.data[CONF_VERIFY_SSL], base_url=base_url + ) + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = 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 diff --git a/homeassistant/components/enigma2/config_flow.py b/homeassistant/components/enigma2/config_flow.py new file mode 100644 index 00000000000..c144f2b7dae --- /dev/null +++ b/homeassistant/components/enigma2/config_flow.py @@ -0,0 +1,158 @@ +"""Config flow for Enigma2.""" + +from typing import Any + +from aiohttp.client_exceptions import ClientError +from openwebif.api import OpenWebIfDevice +from openwebif.error import InvalidAuthError +import voluptuous as vol +from yarl import URL + +from homeassistant.components.homeassistant import DOMAIN as HOMEASSISTANT_DOMAIN +from homeassistant.config_entries import SOURCE_USER, ConfigFlow, ConfigFlowResult +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) +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 .const import ( + CONF_DEEP_STANDBY, + CONF_SOURCE_BOUQUET, + CONF_USE_CHANNEL_ICON, + DEFAULT_PORT, + DEFAULT_SSL, + DEFAULT_VERIFY_SSL, + DOMAIN, +) + +CONFIG_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): selector.TextSelector(), + vol.Required(CONF_PORT, default=DEFAULT_PORT): vol.All( + selector.NumberSelector( + selector.NumberSelectorConfig( + min=1, max=65535, mode=selector.NumberSelectorMode.BOX + ) + ), + vol.Coerce(int), + ), + vol.Optional(CONF_USERNAME): selector.TextSelector(), + vol.Optional(CONF_PASSWORD): selector.TextSelector( + selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD) + ), + vol.Required(CONF_SSL, default=DEFAULT_SSL): selector.BooleanSelector(), + vol.Required( + CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL + ): selector.BooleanSelector(), + } +) + + +class Enigma2ConfigFlowHandler(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Enigma2.""" + + DATA_KEYS = ( + CONF_HOST, + CONF_PORT, + CONF_USERNAME, + CONF_PASSWORD, + CONF_SSL, + CONF_VERIFY_SSL, + ) + OPTIONS_KEYS = (CONF_DEEP_STANDBY, CONF_SOURCE_BOUQUET, CONF_USE_CHANNEL_ICON) + + def __init__(self) -> None: + """Initialize the config flow.""" + super().__init__() + self.errors: dict[str, str] = {} + self._data: dict[str, Any] = {} + self._options: dict[str, Any] = {} + + async def validate_user_input(self, user_input: dict[str, Any]) -> dict[str, Any]: + """Validate user input.""" + + self.errors = {} + + self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) + + base_url = URL.build( + scheme="http" if not user_input[CONF_SSL] else "https", + host=user_input[CONF_HOST], + port=user_input[CONF_PORT], + user=user_input.get(CONF_USERNAME), + password=user_input.get(CONF_PASSWORD), + ) + + session = async_create_clientsession( + self.hass, verify_ssl=user_input[CONF_VERIFY_SSL], base_url=base_url + ) + + try: + about = await OpenWebIfDevice(session).get_about() + except InvalidAuthError: + self.errors["base"] = "invalid_auth" + except ClientError: + self.errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + self.errors["base"] = "unknown" + else: + await self.async_set_unique_id(about["info"]["ifaces"][0]["mac"]) + self._abort_if_unique_id_configured() + + return user_input + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the user step.""" + if user_input is None: + return self.async_show_form(step_id=SOURCE_USER, data_schema=CONFIG_SCHEMA) + + data = await self.validate_user_input(user_input) + if "base" in self.errors: + return self.async_show_form( + step_id=SOURCE_USER, data_schema=CONFIG_SCHEMA, errors=self.errors + ) + return self.async_create_entry( + data=data, title=data[CONF_HOST], options=self._options + ) + + async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult: + """Validate import.""" + if CONF_PORT not in user_input: + user_input[CONF_PORT] = DEFAULT_PORT + if CONF_SSL not in user_input: + user_input[CONF_SSL] = DEFAULT_SSL + user_input[CONF_VERIFY_SSL] = DEFAULT_VERIFY_SSL + + async_create_issue( + self.hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.11.0", + is_fixable=False, + is_persistent=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Enigma2", + }, + ) + + self._data = { + key: user_input[key] for key in user_input if key in self.DATA_KEYS + } + self._options = { + key: user_input[key] for key in user_input if key in self.OPTIONS_KEYS + } + + return await self.async_step_user(self._data) diff --git a/homeassistant/components/enigma2/const.py b/homeassistant/components/enigma2/const.py index 277efad50eb..d7508fee64e 100644 --- a/homeassistant/components/enigma2/const.py +++ b/homeassistant/components/enigma2/const.py @@ -16,3 +16,4 @@ DEFAULT_PASSWORD = "dreambox" DEFAULT_DEEP_STANDBY = False DEFAULT_SOURCE_BOUQUET = "" DEFAULT_MAC_ADDRESS = "" +DEFAULT_VERIFY_SSL = False diff --git a/homeassistant/components/enigma2/manifest.json b/homeassistant/components/enigma2/manifest.json index 0de4adc13b8..ef08314e541 100644 --- a/homeassistant/components/enigma2/manifest.json +++ b/homeassistant/components/enigma2/manifest.json @@ -2,7 +2,9 @@ "domain": "enigma2", "name": "Enigma2 (OpenWebif)", "codeowners": ["@autinerd"], + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/enigma2", + "integration_type": "device", "iot_class": "local_polling", "loggers": ["openwebif"], "requirements": ["openwebifpy==4.2.4"] diff --git a/homeassistant/components/enigma2/media_player.py b/homeassistant/components/enigma2/media_player.py index afe8a426c72..037d82cd6c0 100644 --- a/homeassistant/components/enigma2/media_player.py +++ b/homeassistant/components/enigma2/media_player.py @@ -9,7 +9,6 @@ from aiohttp.client_exceptions import ClientConnectorError, ServerDisconnectedEr from openwebif.api import OpenWebIfDevice from openwebif.enums import PowerState, RemoteControlCodes, SetVolumeOption import voluptuous as vol -from yarl import URL from homeassistant.components.media_player import ( MediaPlayerEntity, @@ -17,6 +16,7 @@ from homeassistant.components.media_player import ( MediaPlayerState, MediaType, ) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -26,10 +26,9 @@ from homeassistant.const import ( CONF_USERNAME, ) 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.config_validation import PLATFORM_SCHEMA +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -47,6 +46,7 @@ from .const import ( DEFAULT_SSL, DEFAULT_USE_CHANNEL_ICON, DEFAULT_USERNAME, + DOMAIN, ) ATTR_MEDIA_CURRENTLY_RECORDING = "media_currently_recording" @@ -81,49 +81,44 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up of an enigma2 media player.""" - if discovery_info: - # Discovery gives us the streaming service port (8001) - # which is not useful as OpenWebif never runs on that port. - # So use the default port instead. - config[CONF_PORT] = DEFAULT_PORT - config[CONF_NAME] = discovery_info["hostname"] - config[CONF_HOST] = discovery_info["host"] - config[CONF_USERNAME] = DEFAULT_USERNAME - config[CONF_PASSWORD] = DEFAULT_PASSWORD - config[CONF_SSL] = DEFAULT_SSL - config[CONF_USE_CHANNEL_ICON] = DEFAULT_USE_CHANNEL_ICON - config[CONF_MAC_ADDRESS] = DEFAULT_MAC_ADDRESS - config[CONF_DEEP_STANDBY] = DEFAULT_DEEP_STANDBY - config[CONF_SOURCE_BOUQUET] = DEFAULT_SOURCE_BOUQUET - base_url = URL.build( - scheme="https" if config[CONF_SSL] else "http", - host=config[CONF_HOST], - port=config.get(CONF_PORT), - user=config.get(CONF_USERNAME), - password=config.get(CONF_PASSWORD), + entry_data = { + CONF_HOST: config[CONF_HOST], + CONF_PORT: config[CONF_PORT], + CONF_USERNAME: config[CONF_USERNAME], + CONF_PASSWORD: config[CONF_PASSWORD], + CONF_SSL: config[CONF_SSL], + CONF_USE_CHANNEL_ICON: config[CONF_USE_CHANNEL_ICON], + CONF_DEEP_STANDBY: config[CONF_DEEP_STANDBY], + CONF_SOURCE_BOUQUET: config[CONF_SOURCE_BOUQUET], + } + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=entry_data + ) ) - session = async_create_clientsession(hass, verify_ssl=False, base_url=base_url) - device = OpenWebIfDevice( - host=session, - turn_off_to_deep=config.get(CONF_DEEP_STANDBY, False), - source_bouquet=config.get(CONF_SOURCE_BOUQUET), - ) +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Enigma2 media player platform.""" - try: - about = await device.get_about() - except ClientConnectorError as err: - raise PlatformNotReady from err - - async_add_entities([Enigma2Device(config[CONF_NAME], device, about)]) + device: OpenWebIfDevice = hass.data[DOMAIN][entry.entry_id] + about = await device.get_about() + device.mac_address = about["info"]["ifaces"][0]["mac"] + entity = Enigma2Device(entry, device, about) + async_add_entities([entity]) class Enigma2Device(MediaPlayerEntity): """Representation of an Enigma2 box.""" _attr_has_entity_name = True + _attr_name = None _attr_media_content_type = MediaType.TVSHOW _attr_supported_features = ( @@ -139,14 +134,23 @@ class Enigma2Device(MediaPlayerEntity): | MediaPlayerEntityFeature.SELECT_SOURCE ) - def __init__(self, name: str, device: OpenWebIfDevice, about: dict) -> None: + def __init__( + self, entry: ConfigEntry, device: OpenWebIfDevice, about: dict + ) -> None: """Initialize the Enigma2 device.""" self._device: OpenWebIfDevice = device - self._device.mac_address = about["info"]["ifaces"][0]["mac"] + self._entry = entry - self._attr_name = name self._attr_unique_id = device.mac_address + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, device.mac_address)}, + manufacturer=about["info"]["brand"], + model=about["info"]["model"], + configuration_url=device.base, + name=entry.data[CONF_HOST], + ) + async def async_turn_off(self) -> None: """Turn off media player.""" if self._device.turn_off_to_deep: diff --git a/homeassistant/components/enigma2/strings.json b/homeassistant/components/enigma2/strings.json new file mode 100644 index 00000000000..888c6d59387 --- /dev/null +++ b/homeassistant/components/enigma2/strings.json @@ -0,0 +1,30 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "user": { + "description": "Please enter the connection details of your device.", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]", + "ssl": "[%key:common::config_flow::data::ssl%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "name": "[%key:common::config_flow::data::name%]", + "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" + } + } + }, + "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%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index fd87c965db5..c02d8a2987e 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -148,6 +148,7 @@ FLOWS = { "emulated_roku", "energenie_power_sockets", "energyzero", + "enigma2", "enocean", "enphase_envoy", "environment_canada", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index d10cb3fdb80..2b1e5b4fb91 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1604,8 +1604,8 @@ }, "enigma2": { "name": "Enigma2 (OpenWebif)", - "integration_type": "hub", - "config_flow": false, + "integration_type": "device", + "config_flow": true, "iot_class": "local_polling" }, "enmax": { diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ea115d4b29d..19aae180e4f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1173,6 +1173,9 @@ openerz-api==0.3.0 # homeassistant.components.openhome openhomedevice==2.2.0 +# homeassistant.components.enigma2 +openwebifpy==4.2.4 + # homeassistant.components.opower opower==0.4.3 diff --git a/tests/components/enigma2/__init__.py b/tests/components/enigma2/__init__.py new file mode 100644 index 00000000000..15580d55b17 --- /dev/null +++ b/tests/components/enigma2/__init__.py @@ -0,0 +1 @@ +"""Tests for the Enigma2 integration.""" diff --git a/tests/components/enigma2/conftest.py b/tests/components/enigma2/conftest.py new file mode 100644 index 00000000000..9bbbda895bd --- /dev/null +++ b/tests/components/enigma2/conftest.py @@ -0,0 +1,90 @@ +"""Test the Enigma2 config flow.""" + +from homeassistant.components.enigma2.const import ( + CONF_DEEP_STANDBY, + CONF_MAC_ADDRESS, + CONF_SOURCE_BOUQUET, + CONF_USE_CHANNEL_ICON, + DEFAULT_DEEP_STANDBY, + DEFAULT_PORT, + DEFAULT_SSL, + DEFAULT_VERIFY_SSL, +) +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) + +MAC_ADDRESS = "12:34:56:78:90:ab" + +TEST_REQUIRED = { + CONF_HOST: "1.1.1.1", + CONF_PORT: DEFAULT_PORT, + CONF_SSL: DEFAULT_SSL, + CONF_VERIFY_SSL: DEFAULT_VERIFY_SSL, +} + +TEST_FULL = { + CONF_HOST: "1.1.1.1", + CONF_PORT: DEFAULT_PORT, + CONF_SSL: DEFAULT_SSL, + CONF_USERNAME: "root", + CONF_PASSWORD: "password", + CONF_VERIFY_SSL: DEFAULT_VERIFY_SSL, +} + +TEST_IMPORT_FULL = { + CONF_HOST: "1.1.1.1", + CONF_PORT: DEFAULT_PORT, + CONF_SSL: DEFAULT_SSL, + CONF_USERNAME: "root", + CONF_PASSWORD: "password", + CONF_NAME: "My Player", + CONF_DEEP_STANDBY: DEFAULT_DEEP_STANDBY, + CONF_SOURCE_BOUQUET: "Favourites", + CONF_MAC_ADDRESS: MAC_ADDRESS, + CONF_USE_CHANNEL_ICON: False, +} + +TEST_IMPORT_REQUIRED = {CONF_HOST: "1.1.1.1"} + +EXPECTED_OPTIONS = { + CONF_DEEP_STANDBY: DEFAULT_DEEP_STANDBY, + CONF_SOURCE_BOUQUET: "Favourites", + CONF_USE_CHANNEL_ICON: False, +} + + +class MockDevice: + """A mock Enigma2 device.""" + + mac_address: str | None = "12:34:56:78:90:ab" + _base = "http://1.1.1.1" + + async def _call_api(self, url: str) -> dict: + if url.endswith("/api/about"): + return { + "info": { + "ifaces": [ + { + "mac": self.mac_address, + } + ] + } + } + + def get_version(self): + """Return the version.""" + return None + + async def get_about(self) -> dict: + """Get mock about endpoint.""" + return await self._call_api("/api/about") + + async def close(self): + """Mock close.""" diff --git a/tests/components/enigma2/test_config_flow.py b/tests/components/enigma2/test_config_flow.py new file mode 100644 index 00000000000..dcd249ad943 --- /dev/null +++ b/tests/components/enigma2/test_config_flow.py @@ -0,0 +1,149 @@ +"""Test the Enigma2 config flow.""" + +from typing import Any +from unittest.mock import patch + +from aiohttp.client_exceptions import ClientError +from openwebif.error import InvalidAuthError +import pytest + +from homeassistant import config_entries +from homeassistant.components.enigma2.const import DOMAIN +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from .conftest import ( + EXPECTED_OPTIONS, + TEST_FULL, + TEST_IMPORT_FULL, + TEST_IMPORT_REQUIRED, + TEST_REQUIRED, + MockDevice, +) + + +@pytest.fixture +async def user_flow(hass: HomeAssistant) -> str: + """Return a user-initiated flow after filling in host info.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] is None + return result["flow_id"] + + +@pytest.mark.parametrize( + ("test_config"), + [(TEST_FULL), (TEST_REQUIRED)], +) +async def test_form_user( + hass: HomeAssistant, user_flow: str, test_config: dict[str, Any] +): + """Test a successful user initiated flow.""" + with ( + patch( + "openwebif.api.OpenWebIfDevice.__new__", + return_value=MockDevice(), + ), + patch( + "homeassistant.components.enigma2.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): + result = await hass.config_entries.flow.async_configure(user_flow, test_config) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == test_config[CONF_HOST] + assert result["data"] == test_config + + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + ("exception", "error_type"), + [ + (InvalidAuthError, "invalid_auth"), + (ClientError, "cannot_connect"), + (Exception, "unknown"), + ], +) +async def test_form_user_errors( + hass: HomeAssistant, user_flow, exception: Exception, error_type: str +) -> None: + """Test we handle errors.""" + with patch( + "homeassistant.components.enigma2.config_flow.OpenWebIfDevice.__new__", + side_effect=exception, + ): + result = await hass.config_entries.flow.async_configure(user_flow, TEST_FULL) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == config_entries.SOURCE_USER + assert result["errors"] == {"base": error_type} + + +@pytest.mark.parametrize( + ("test_config", "expected_data", "expected_options"), + [ + (TEST_IMPORT_FULL, TEST_FULL, EXPECTED_OPTIONS), + (TEST_IMPORT_REQUIRED, TEST_REQUIRED, {}), + ], +) +async def test_form_import( + hass: HomeAssistant, + test_config: dict[str, Any], + expected_data: dict[str, Any], + expected_options: dict[str, Any], +) -> None: + """Test we get the form with import source.""" + with ( + patch( + "homeassistant.components.enigma2.config_flow.OpenWebIfDevice.__new__", + return_value=MockDevice(), + ), + patch( + "homeassistant.components.enigma2.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=test_config, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == test_config[CONF_HOST] + assert result["data"] == expected_data + assert result["options"] == expected_options + + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + ("exception", "error_type"), + [ + (InvalidAuthError, "invalid_auth"), + (ClientError, "cannot_connect"), + (Exception, "unknown"), + ], +) +async def test_form_import_errors( + hass: HomeAssistant, exception: Exception, error_type: str +) -> None: + """Test we handle errors on import.""" + with patch( + "homeassistant.components.enigma2.config_flow.OpenWebIfDevice.__new__", + side_effect=exception, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=TEST_IMPORT_FULL, + ) + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": error_type} diff --git a/tests/components/enigma2/test_init.py b/tests/components/enigma2/test_init.py new file mode 100644 index 00000000000..93a130eef54 --- /dev/null +++ b/tests/components/enigma2/test_init.py @@ -0,0 +1,38 @@ +"""Test the Enigma2 integration init.""" + +from unittest.mock import patch + +from homeassistant.components.enigma2.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from .conftest import TEST_REQUIRED, MockDevice + +from tests.common import MockConfigEntry + + +async def test_unload_entry(hass: HomeAssistant) -> None: + """Test successful unload of entry.""" + with ( + patch( + "homeassistant.components.enigma2.OpenWebIfDevice.__new__", + return_value=MockDevice(), + ), + patch( + "homeassistant.components.enigma2.media_player.async_setup_entry", + return_value=True, + ), + ): + entry = MockConfigEntry(domain=DOMAIN, data=TEST_REQUIRED, title="name") + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert entry.state is ConfigEntryState.LOADED + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.NOT_LOADED + assert not hass.data.get(DOMAIN) From 9ca24ab2a209d419ef2c8577aea22dc98086e014 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 17 Apr 2024 08:45:40 -0500 Subject: [PATCH 628/967] Avoid linear search to remove labels and floors from area registry (#115675) * Avoid linear search to remove labels and floors from area registry * simplify --- homeassistant/helpers/area_registry.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py index 2734ab5e2e5..b39fee9c185 100644 --- a/homeassistant/helpers/area_registry.py +++ b/homeassistant/helpers/area_registry.py @@ -385,9 +385,8 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]): def _handle_floor_registry_update(event: fr.EventFloorRegistryUpdated) -> None: """Update areas that are associated with a floor that has been removed.""" floor_id = event.data["floor_id"] - for area_id, area in self.areas.items(): - if floor_id == area.floor_id: - self.async_update(area_id, floor_id=None) + for area in self.areas.get_areas_for_floor(floor_id): + self.async_update(area.id, floor_id=None) self.hass.bus.async_listen( event_type=fr.EVENT_FLOOR_REGISTRY_UPDATED, @@ -399,11 +398,8 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]): def _handle_label_registry_update(event: lr.EventLabelRegistryUpdated) -> None: """Update areas that have a label that has been removed.""" label_id = event.data["label_id"] - for area_id, area in self.areas.items(): - if label_id in area.labels: - labels = area.labels.copy() - labels.remove(label_id) - self.async_update(area_id, labels=labels) + for area in self.areas.get_areas_for_label(label_id): + self.async_update(area.id, labels=area.labels - {label_id}) self.hass.bus.async_listen( event_type=lr.EVENT_LABEL_REGISTRY_UPDATED, From 8cf14c92681d92fe658d45a09278de4bbb0ad210 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 17 Apr 2024 08:46:59 -0500 Subject: [PATCH 629/967] Avoid linear search to clear labels and areas in the device registry (#115676) --- homeassistant/helpers/device_registry.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 0270c8dc456..4cc9a29d46a 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -1053,18 +1053,14 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): @callback def async_clear_area_id(self, area_id: str) -> None: """Clear area id from registry entries.""" - for dev_id, device in self.devices.items(): - if area_id == device.area_id: - self.async_update_device(dev_id, area_id=None) + for device in self.devices.get_devices_for_area_id(area_id): + self.async_update_device(device.id, area_id=None) @callback def async_clear_label_id(self, label_id: str) -> None: """Clear label from registry entries.""" - for device_id, entry in self.devices.items(): - if label_id in entry.labels: - labels = entry.labels.copy() - labels.remove(label_id) - self.async_update_device(device_id, labels=labels) + for device in self.devices.get_devices_for_label(label_id): + self.async_update_device(device.id, labels=device.labels - {label_id}) @callback From 45f025480e2e5b237f77b61510a6048a8ece8ca1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 17 Apr 2024 08:47:56 -0500 Subject: [PATCH 630/967] Avoid linear search to remove a label from the entity registry (#115674) * Avoid linear search to remove a label from the entity registry * simplify --- homeassistant/helpers/entity_registry.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 3a26505c7da..4e77df49ea6 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -1329,11 +1329,8 @@ class EntityRegistry(BaseRegistry): @callback def async_clear_label_id(self, label_id: str) -> None: """Clear label from registry entries.""" - for entity_id, entry in self.entities.items(): - if label_id in entry.labels: - labels = entry.labels.copy() - labels.remove(label_id) - self.async_update_entity(entity_id, labels=labels) + for entry in self.entities.get_entries_for_label(label_id): + self.async_update_entity(entry.entity_id, labels=entry.labels - {label_id}) @callback def async_clear_config_entry(self, config_entry_id: str) -> None: From bd2efffb4a95fa1b99675a24246c489edb5e91e2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 17 Apr 2024 08:50:39 -0500 Subject: [PATCH 631/967] Reduce duplicate code in the device registry (#115677) --- homeassistant/helpers/device_registry.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 4cc9a29d46a..3a9d047810b 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -1235,21 +1235,21 @@ def async_setup_cleanup(hass: HomeAssistant, dev_reg: DeviceRegistry) -> None: return True - if hass.is_running: + def _async_listen_for_cleanup() -> None: + """Listen for entity registry changes.""" hass.bus.async_listen( entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, _async_entity_registry_changed, event_filter=entity_registry_changed_filter, ) + + if hass.is_running: + _async_listen_for_cleanup() return async def startup_clean(event: Event) -> None: """Clean up on startup.""" - hass.bus.async_listen( - entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, - _async_entity_registry_changed, - event_filter=entity_registry_changed_filter, - ) + _async_listen_for_cleanup() await debounced_cleanup.async_call() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, startup_clean) From f1ebe9d20a9135c0cbf08fd7e557c4c641ef20fd Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Wed, 17 Apr 2024 09:51:29 -0400 Subject: [PATCH 632/967] Add repairs to hassio manifest (#115486) * Add repairs to hassio manifest * Remove unnecessary fixture --- homeassistant/components/hassio/manifest.json | 2 +- tests/components/hassio/test_repairs.py | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/homeassistant/components/hassio/manifest.json b/homeassistant/components/hassio/manifest.json index 70fc024c005..b32e5ebcd53 100644 --- a/homeassistant/components/hassio/manifest.json +++ b/homeassistant/components/hassio/manifest.json @@ -2,7 +2,7 @@ "domain": "hassio", "name": "Home Assistant Supervisor", "codeowners": ["@home-assistant/supervisor"], - "dependencies": ["http"], + "dependencies": ["http", "repairs"], "documentation": "https://www.home-assistant.io/integrations/hassio", "iot_class": "local_polling", "quality_scale": "internal" diff --git a/tests/components/hassio/test_repairs.py b/tests/components/hassio/test_repairs.py index 2dffba74fef..33d266eb24b 100644 --- a/tests/components/hassio/test_repairs.py +++ b/tests/components/hassio/test_repairs.py @@ -6,7 +6,6 @@ from unittest.mock import patch import pytest -from homeassistant.components.repairs import DOMAIN as REPAIRS_DOMAIN from homeassistant.core import HomeAssistant import homeassistant.helpers.issue_registry as ir from homeassistant.setup import async_setup_component @@ -18,12 +17,6 @@ from tests.test_util.aiohttp import AiohttpClientMocker from tests.typing import ClientSessionGenerator -@pytest.fixture(autouse=True) -async def setup_repairs(hass: HomeAssistant): - """Set up the repairs integration.""" - assert await async_setup_component(hass, REPAIRS_DOMAIN, {REPAIRS_DOMAIN: {}}) - - @pytest.fixture(autouse=True) async def fixture_supervisor_environ(): """Mock os environ for supervisor.""" From dfec91d2741253dab5221cbfca4f9f03b280ce20 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 17 Apr 2024 16:11:42 +0200 Subject: [PATCH 633/967] Remove obsolete translation keys in Sanix (#115764) --- homeassistant/components/sanix/sensor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/sanix/sensor.py b/homeassistant/components/sanix/sensor.py index e780c6f2df0..39a1c593433 100644 --- a/homeassistant/components/sanix/sensor.py +++ b/homeassistant/components/sanix/sensor.py @@ -41,7 +41,6 @@ class SanixSensorEntityDescription(SensorEntityDescription): SENSOR_TYPES: tuple[SanixSensorEntityDescription, ...] = ( SanixSensorEntityDescription( key=ATTR_API_BATTERY, - translation_key=ATTR_API_BATTERY, native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, @@ -49,7 +48,6 @@ SENSOR_TYPES: tuple[SanixSensorEntityDescription, ...] = ( ), SanixSensorEntityDescription( key=ATTR_API_DISTANCE, - translation_key=ATTR_API_DISTANCE, native_unit_of_measurement=UnitOfLength.CENTIMETERS, device_class=SensorDeviceClass.DISTANCE, state_class=SensorStateClass.MEASUREMENT, From 08b565701cad8d1aaf62bb1b29657603f7330c45 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 17 Apr 2024 16:37:49 +0200 Subject: [PATCH 634/967] Include hash of requirements.txt in venv cache key (#115759) --- .github/workflows/ci.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c96c6b5e5f2..a5bafa0c52d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -95,6 +95,7 @@ jobs: run: >- echo "key=venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements_test.txt', 'requirements_test_pre_commit.txt') }}-${{ + hashFiles('requirements.txt') }}-${{ hashFiles('requirements_all.txt') }}-${{ hashFiles('homeassistant/package_constraints.txt') }}" >> $GITHUB_OUTPUT - name: Generate partial pre-commit restore key From 7a673043012f30a2ce695fcf43a9bb54743184e1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 17 Apr 2024 09:41:42 -0500 Subject: [PATCH 635/967] Bump habluetooth to 2.6.0 (#115724) --- homeassistant/components/bluetooth/__init__.py | 10 ++++------ homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 8 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 3273080d88b..35fbeb2f3b3 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -315,16 +315,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: mode = BluetoothScanningMode.PASSIVE if passive else BluetoothScanningMode.ACTIVE manager: HomeAssistantBluetoothManager = hass.data[DATA_MANAGER] scanner = HaScanner(mode, adapter, address) + scanner.async_setup() try: - scanner.async_setup() - except RuntimeError as err: + await scanner.async_start() + except (RuntimeError, ScannerStartError) as err: raise ConfigEntryNotReady( f"{adapter_human_name(adapter, address)}: {err}" ) from err - try: - await scanner.async_start() - except ScannerStartError as err: - raise ConfigEntryNotReady from err adapters = await manager.async_get_bluetooth_adapters() details = adapters[adapter] slots: int = details.get(ADAPTER_CONNECTION_SLOTS) or DEFAULT_CONNECTION_SLOTS @@ -332,6 +329,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await async_update_device(hass, entry, adapter, details) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = scanner entry.async_on_unload(entry.add_update_listener(async_update_listener)) + entry.async_on_unload(scanner.async_stop) return True diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 5939a03cefc..471e327ee9d 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -20,6 +20,6 @@ "bluetooth-auto-recovery==1.4.0", "bluetooth-data-tools==1.19.0", "dbus-fast==2.21.1", - "habluetooth==2.5.2" + "habluetooth==2.6.0" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 318738558e8..dca7c82a885 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -28,7 +28,7 @@ dbus-fast==2.21.1 fnv-hash-fast==0.5.0 ha-av==10.1.1 ha-ffmpeg==3.2.0 -habluetooth==2.5.2 +habluetooth==2.6.0 hass-nabucasa==0.78.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 diff --git a/requirements_all.txt b/requirements_all.txt index 74b458920e8..ab9f24284e3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1029,7 +1029,7 @@ ha-philipsjs==3.1.1 habitipy==0.2.0 # homeassistant.components.bluetooth -habluetooth==2.5.2 +habluetooth==2.6.0 # homeassistant.components.cloud hass-nabucasa==0.78.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 19aae180e4f..ab9b1e94b88 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -843,7 +843,7 @@ ha-philipsjs==3.1.1 habitipy==0.2.0 # homeassistant.components.bluetooth -habluetooth==2.5.2 +habluetooth==2.6.0 # homeassistant.components.cloud hass-nabucasa==0.78.0 From f62a3a7176af5cab107a56aa0736ec1e584ad13c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 17 Apr 2024 09:42:23 -0500 Subject: [PATCH 636/967] Simplify config_entries entity registry filter (#115678) --- homeassistant/config_entries.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index a3f31ff8715..bf576b517d3 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -2609,14 +2609,12 @@ def _handle_entry_updated_filter( Only handle changes to "disabled_by". If "disabled_by" was CONFIG_ENTRY, reload is not needed. """ - if ( + return not ( event_data["action"] != "update" or "disabled_by" not in event_data["changes"] or event_data["changes"]["disabled_by"] is entity_registry.RegistryEntryDisabler.CONFIG_ENTRY - ): - return False - return True + ) async def support_entry_unload(hass: HomeAssistant, domain: str) -> bool: From 9bae6d694d4672a2ce061d21755afc98796c4694 Mon Sep 17 00:00:00 2001 From: Christopher Fenner <9592452+CFenner@users.noreply.github.com> Date: Wed, 17 Apr 2024 17:08:23 +0200 Subject: [PATCH 637/967] Add secondary temperature sensor for DHW in ViCare (#106612) * add temp2 sensor * Update strings.json --- homeassistant/components/vicare/number.py | 41 +++++++++++++++++--- homeassistant/components/vicare/strings.json | 3 ++ 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/vicare/number.py b/homeassistant/components/vicare/number.py index f92241ceace..c0564170274 100644 --- a/homeassistant/components/vicare/number.py +++ b/homeassistant/components/vicare/number.py @@ -49,6 +49,23 @@ class ViCareNumberEntityDescription(NumberEntityDescription, ViCareRequiredKeysM stepping_getter: Callable[[PyViCareDevice], float | None] | None = None +DEVICE_ENTITY_DESCRIPTIONS: tuple[ViCareNumberEntityDescription, ...] = ( + ViCareNumberEntityDescription( + key="dhw_secondary_temperature", + translation_key="dhw_secondary_temperature", + entity_category=EntityCategory.CONFIG, + device_class=NumberDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + value_getter=lambda api: api.getDomesticHotWaterConfiguredTemperature2(), + value_setter=lambda api, value: api.setDomesticHotWaterTemperature2(value), + # no getters for min, max, stepping exposed yet, using static values + native_min_value=10, + native_max_value=60, + native_step=1, + ), +) + + CIRCUIT_ENTITY_DESCRIPTIONS: tuple[ViCareNumberEntityDescription, ...] = ( ViCareNumberEntityDescription( key="heating curve shift", @@ -216,18 +233,32 @@ def _build_entities( ) -> list[ViCareNumber]: """Create ViCare number entities for a device.""" - return [ + entities: list[ViCareNumber] = [ ViCareNumber( - circuit, + device.api, device.config, description, ) for device in device_list - for circuit in get_circuits(device.api) - for description in CIRCUIT_ENTITY_DESCRIPTIONS - if is_supported(description.key, description, circuit) + for description in DEVICE_ENTITY_DESCRIPTIONS + if is_supported(description.key, description, device.api) ] + entities.extend( + [ + ViCareNumber( + circuit, + device.config, + description, + ) + for device in device_list + for circuit in get_circuits(device.api) + for description in CIRCUIT_ENTITY_DESCRIPTIONS + if is_supported(description.key, description, circuit) + ] + ) + return entities + async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/vicare/strings.json b/homeassistant/components/vicare/strings.json index 5a69cae4d29..f81d01b71cf 100644 --- a/homeassistant/components/vicare/strings.json +++ b/homeassistant/components/vicare/strings.json @@ -89,6 +89,9 @@ }, "comfort_heating_temperature": { "name": "[%key:component::vicare::entity::number::comfort_temperature::name%]" + }, + "dhw_secondary_temperature": { + "name": "DHW secondary temperature" } }, "sensor": { From 7e9b5b112817ad4b4b1220c2cec97a2896d6e6dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Wed, 17 Apr 2024 17:11:29 +0200 Subject: [PATCH 638/967] Allow selecting Air Quality mode for Airzone Cloud (#106769) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * airzone_cloud: add Air Quality mode select Signed-off-by: Álvaro Fernández Rojas * trigger CI * trigger CI * Update select.py Co-authored-by: Erik Montnemery * airzone_cloud: select: remove AirzoneSelectDescriptionMixin usage * airzone_cloud: select: rename AIR_QUALITY_DICT --------- Signed-off-by: Álvaro Fernández Rojas Co-authored-by: Erik Montnemery --- .../components/airzone_cloud/__init__.py | 1 + .../components/airzone_cloud/select.py | 124 ++++++++++++++++++ .../components/airzone_cloud/strings.json | 10 ++ tests/components/airzone_cloud/test_select.py | 61 +++++++++ 4 files changed, 196 insertions(+) create mode 100644 homeassistant/components/airzone_cloud/select.py create mode 100644 tests/components/airzone_cloud/test_select.py diff --git a/homeassistant/components/airzone_cloud/__init__.py b/homeassistant/components/airzone_cloud/__init__.py index c6908b191d7..e53c01e0f81 100644 --- a/homeassistant/components/airzone_cloud/__init__.py +++ b/homeassistant/components/airzone_cloud/__init__.py @@ -16,6 +16,7 @@ from .coordinator import AirzoneUpdateCoordinator PLATFORMS: list[Platform] = [ Platform.BINARY_SENSOR, Platform.CLIMATE, + Platform.SELECT, Platform.SENSOR, Platform.WATER_HEATER, ] diff --git a/homeassistant/components/airzone_cloud/select.py b/homeassistant/components/airzone_cloud/select.py new file mode 100644 index 00000000000..c5c9f664503 --- /dev/null +++ b/homeassistant/components/airzone_cloud/select.py @@ -0,0 +1,124 @@ +"""Support for the Airzone Cloud select.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Final + +from aioairzone_cloud.common import AirQualityMode +from aioairzone_cloud.const import ( + API_AQ_MODE_CONF, + API_VALUE, + AZD_AQ_MODE_CONF, + AZD_ZONES, +) + +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 .coordinator import AirzoneUpdateCoordinator +from .entity import AirzoneEntity, AirzoneZoneEntity + + +@dataclass(frozen=True, kw_only=True) +class AirzoneSelectDescription(SelectEntityDescription): + """Class to describe an Airzone select entity.""" + + api_param: str + options_dict: dict[str, str] + + +AIR_QUALITY_MAP: Final[dict[str, str]] = { + "off": AirQualityMode.OFF, + "on": AirQualityMode.ON, + "auto": AirQualityMode.AUTO, +} + + +ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = ( + AirzoneSelectDescription( + api_param=API_AQ_MODE_CONF, + entity_category=EntityCategory.CONFIG, + key=AZD_AQ_MODE_CONF, + options=list(AIR_QUALITY_MAP), + options_dict=AIR_QUALITY_MAP, + translation_key="air_quality", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Add Airzone Cloud select from a config_entry.""" + coordinator: AirzoneUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + # Zones + async_add_entities( + AirzoneZoneSelect( + coordinator, + description, + zone_id, + zone_data, + ) + for description in ZONE_SELECT_TYPES + for zone_id, zone_data in coordinator.data.get(AZD_ZONES, {}).items() + if description.key in zone_data + ) + + +class AirzoneBaseSelect(AirzoneEntity, SelectEntity): + """Define an Airzone Cloud select.""" + + entity_description: AirzoneSelectDescription + values_dict: dict[str, str] + + @callback + def _handle_coordinator_update(self) -> None: + """Update attributes when the coordinator updates.""" + self._async_update_attrs() + super()._handle_coordinator_update() + + def _get_current_option(self) -> str | None: + """Get current selected option.""" + value = self.get_airzone_value(self.entity_description.key) + return self.values_dict.get(value) + + @callback + def _async_update_attrs(self) -> None: + """Update select attributes.""" + self._attr_current_option = self._get_current_option() + + +class AirzoneZoneSelect(AirzoneZoneEntity, AirzoneBaseSelect): + """Define an Airzone Cloud Zone select.""" + + def __init__( + self, + coordinator: AirzoneUpdateCoordinator, + description: AirzoneSelectDescription, + zone_id: str, + zone_data: dict[str, Any], + ) -> None: + """Initialize.""" + super().__init__(coordinator, zone_id, zone_data) + + self._attr_unique_id = f"{zone_id}_{description.key}" + self.entity_description = description + self.values_dict = {v: k for k, v in description.options_dict.items()} + + self._async_update_attrs() + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + param = self.entity_description.api_param + value = self.entity_description.options_dict[option] + params: dict[str, Any] = {} + params[param] = { + API_VALUE: value, + } + await self._async_update_params(params) diff --git a/homeassistant/components/airzone_cloud/strings.json b/homeassistant/components/airzone_cloud/strings.json index fe7c38c8374..fe9455aa69e 100644 --- a/homeassistant/components/airzone_cloud/strings.json +++ b/homeassistant/components/airzone_cloud/strings.json @@ -21,6 +21,16 @@ "air_quality_active": { "name": "Air Quality active" } + }, + "select": { + "air_quality": { + "name": "Air Quality mode", + "state": { + "off": "Off", + "on": "On", + "auto": "Auto" + } + } } } } diff --git a/tests/components/airzone_cloud/test_select.py b/tests/components/airzone_cloud/test_select.py new file mode 100644 index 00000000000..1375b052050 --- /dev/null +++ b/tests/components/airzone_cloud/test_select.py @@ -0,0 +1,61 @@ +"""The select tests for the Airzone Cloud platform.""" + +from unittest.mock import patch + +import pytest + +from homeassistant.components.select import DOMAIN as SELECT_DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION, SERVICE_SELECT_OPTION +from homeassistant.core import HomeAssistant +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: + """Test creation of selects.""" + + await async_init_integration(hass) + + # Zones + state = hass.states.get("select.dormitorio_air_quality_mode") + assert state.state == "auto" + + state = hass.states.get("select.salon_air_quality_mode") + assert state.state == "auto" + + +async def test_airzone_select_air_quality_mode(hass: HomeAssistant) -> None: + """Test select Air Quality mode.""" + + await async_init_integration(hass) + + with pytest.raises(ServiceValidationError): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.dormitorio_air_quality_mode", + ATTR_OPTION: "Invalid", + }, + blocking=True, + ) + + with patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_patch_device", + return_value=None, + ): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.dormitorio_air_quality_mode", + ATTR_OPTION: "off", + }, + blocking=True, + ) + + state = hass.states.get("select.dormitorio_air_quality_mode") + assert state.state == "off" From e4280b2c0013cf0098bbf493f57c9052247442c7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 17 Apr 2024 17:57:34 +0200 Subject: [PATCH 639/967] Use aiohttp-zlib-ng[isal] (#115767) --- .github/workflows/builder.yml | 9 --------- .github/workflows/wheels.yml | 5 ----- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 5 files changed, 3 insertions(+), 17 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 217093793d1..f02a8bacce8 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -174,15 +174,6 @@ jobs: sed -i "s|pyezviz|# pyezviz|g" requirements_all.txt sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt - - name: Adjustments for 64-bit - if: matrix.arch == 'amd64' || matrix.arch == 'aarch64' - run: | - # Some speedups are only available on 64-bit, and since - # we build 32bit images on 64bit hosts, we only enable - # the speed ups on 64bit since the wheels for 32bit - # are not available. - sed -i "s|aiohttp-zlib-ng|aiohttp-zlib-ng\[isal\]|g" requirements_all.txt - - name: Download translations uses: actions/download-artifact@v4.1.4 with: diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 3636906c305..7102df0ae4d 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -161,11 +161,6 @@ jobs: sed -i "s|pykrakenapi|# pykrakenapi|g" ${requirement_file} fi - # Some speedups are only for 64-bit - if [ "${{ matrix.arch }}" = "amd64" ] || [ "${{ matrix.arch }}" = "aarch64" ]; then - sed -i "s|aiohttp-zlib-ng|aiohttp-zlib-ng\[isal\]|g" ${requirement_file} - fi - done - name: Split requirements all diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index dca7c82a885..6dce47b734d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodhcpwatcher==1.0.0 aiodiscover==2.0.0 aiodns==3.2.0 aiohttp-fast-url-dispatcher==0.3.0 -aiohttp-zlib-ng==0.3.1 +aiohttp-zlib-ng[isal]==0.3.1 aiohttp==3.9.5 aiohttp_cors==0.7.0 aiohttp_session==2.12.0 diff --git a/pyproject.toml b/pyproject.toml index 2216295d00d..90466aa7290 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ "aiohttp_cors==0.7.0", "aiohttp_session==2.12.0", "aiohttp-fast-url-dispatcher==0.3.0", - "aiohttp-zlib-ng==0.3.1", + "aiohttp-zlib-ng[isal]==0.3.1", "astral==2.2", "async-interrupt==1.1.1", "attrs==23.2.0", diff --git a/requirements.txt b/requirements.txt index 650a9bf7554..980bf84eb26 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ aiohttp==3.9.5 aiohttp_cors==0.7.0 aiohttp_session==2.12.0 aiohttp-fast-url-dispatcher==0.3.0 -aiohttp-zlib-ng==0.3.1 +aiohttp-zlib-ng[isal]==0.3.1 astral==2.2 async-interrupt==1.1.1 attrs==23.2.0 From dcd61ac0867c6bb485a7596035378b41b1f5f48c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A5le=20Stor=C3=B8=20Hauknes?= Date: Wed, 17 Apr 2024 18:47:29 +0200 Subject: [PATCH 640/967] Fix unrecoverable error when fetching airthings_ble data (#115699) --- homeassistant/components/airthings_ble/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/airthings_ble/__init__.py b/homeassistant/components/airthings_ble/__init__.py index e8a2d492ae2..39617a8a019 100644 --- a/homeassistant/components/airthings_ble/__init__.py +++ b/homeassistant/components/airthings_ble/__init__.py @@ -44,10 +44,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def _async_update_method() -> AirthingsDevice: """Get data from Airthings BLE.""" - ble_device = bluetooth.async_ble_device_from_address(hass, address) - try: - data = await airthings.update_device(ble_device) # type: ignore[arg-type] + data = await airthings.update_device(ble_device) except Exception as err: raise UpdateFailed(f"Unable to fetch data: {err}") from err From 3202743b6c8495365be182f628e4c5a7b8e4a9c6 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 17 Apr 2024 19:10:09 +0200 Subject: [PATCH 641/967] Cleanup modbus test mocks (#115412) --- tests/components/modbus/conftest.py | 119 +++++++----------- tests/components/modbus/test_binary_sensor.py | 4 +- tests/components/modbus/test_climate.py | 28 ++--- tests/components/modbus/test_cover.py | 18 +-- tests/components/modbus/test_fan.py | 5 +- tests/components/modbus/test_init.py | 33 ++--- tests/components/modbus/test_light.py | 11 +- tests/components/modbus/test_sensor.py | 6 +- tests/components/modbus/test_switch.py | 9 +- 9 files changed, 96 insertions(+), 137 deletions(-) diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index 62cf12958d3..1253a856bbf 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -47,12 +47,36 @@ class ReadResult: return False +@pytest.fixture(name="check_config_loaded") +def check_config_loaded_fixture(): + """Set default for check_config_loaded.""" + return True + + +@pytest.fixture(name="register_words") +def register_words_fixture(): + """Set default for register_words.""" + return [0x00, 0x00] + + +@pytest.fixture(name="config_addon") +def config_addon_fixture(): + """Add extra configuration items.""" + return None + + +@pytest.fixture(name="do_exception") +def do_exception_fixture(): + """Remove side_effect to pymodbus calls.""" + return False + + @pytest.fixture(name="mock_pymodbus") -def mock_pymodbus_fixture(): +def mock_pymodbus_fixture(do_exception, register_words): """Mock pymodbus.""" mock_pb = mock.AsyncMock() mock_pb.close = mock.MagicMock() - read_result = ReadResult([]) + read_result = ReadResult(register_words if register_words else []) mock_pb.read_coils.return_value = read_result mock_pb.read_discrete_inputs.return_value = read_result mock_pb.read_input_registers.return_value = read_result @@ -61,6 +85,16 @@ def mock_pymodbus_fixture(): mock_pb.write_registers.return_value = read_result mock_pb.write_coil.return_value = read_result mock_pb.write_coils.return_value = read_result + if do_exception: + exc = ModbusException("mocked pymodbus exception") + mock_pb.read_coils.side_effect = exc + mock_pb.read_discrete_inputs.side_effect = exc + mock_pb.read_input_registers.side_effect = exc + mock_pb.read_holding_registers.side_effect = exc + mock_pb.write_register.side_effect = exc + mock_pb.write_registers.side_effect = exc + mock_pb.write_coil.side_effect = exc + mock_pb.write_coils.side_effect = exc with ( mock.patch( "homeassistant.components.modbus.modbus.AsyncModbusTcpClient", @@ -81,33 +115,9 @@ def mock_pymodbus_fixture(): yield mock_pb -@pytest.fixture(name="check_config_loaded") -def check_config_loaded_fixture(): - """Set default for check_config_loaded.""" - return True - - -@pytest.fixture(name="register_words") -def register_words_fixture(): - """Set default for register_words.""" - return [0x00, 0x00] - - -@pytest.fixture(name="config_addon") -def config_addon_fixture(): - """Add entra configuration items.""" - return None - - -@pytest.fixture(name="do_exception") -def do_exception_fixture(): - """Remove side_effect to pymodbus calls.""" - return False - - @pytest.fixture(name="mock_modbus") async def mock_modbus_fixture( - hass, caplog, register_words, check_config_loaded, config_addon, do_config + hass, caplog, check_config_loaded, config_addon, do_config, mock_pymodbus ): """Load integration modbus using mocked pymodbus.""" conf = copy.deepcopy(do_config) @@ -132,57 +142,23 @@ async def mock_modbus_fixture( } ] } - mock_pb = mock.AsyncMock() - mock_pb.close = mock.MagicMock() + now = dt_util.utcnow() with mock.patch( - "homeassistant.components.modbus.modbus.AsyncModbusTcpClient", - return_value=mock_pb, + "homeassistant.helpers.event.dt_util.utcnow", + return_value=now, autospec=True, ): - now = dt_util.utcnow() - with mock.patch( - "homeassistant.helpers.event.dt_util.utcnow", - return_value=now, - autospec=True, - ): - result = await async_setup_component(hass, DOMAIN, config) - assert result or not check_config_loaded - await hass.async_block_till_done() - yield mock_pb - - -@pytest.fixture(name="mock_pymodbus_exception") -async def mock_pymodbus_exception_fixture(hass, do_exception, mock_modbus): - """Trigger update call with time_changed event.""" - if do_exception: - exc = ModbusException("fail read_coils") - mock_modbus.read_coils.side_effect = exc - mock_modbus.read_discrete_inputs.side_effect = exc - mock_modbus.read_input_registers.side_effect = exc - mock_modbus.read_holding_registers.side_effect = exc - - -@pytest.fixture(name="mock_pymodbus_return") -async def mock_pymodbus_return_fixture(hass, register_words, mock_modbus): - """Trigger update call with time_changed event.""" - read_result = ReadResult(register_words if register_words else []) - mock_modbus.read_coils.return_value = read_result - mock_modbus.read_discrete_inputs.return_value = read_result - mock_modbus.read_input_registers.return_value = read_result - mock_modbus.read_holding_registers.return_value = read_result - mock_modbus.write_register.return_value = read_result - mock_modbus.write_registers.return_value = read_result - mock_modbus.write_coil.return_value = read_result - mock_modbus.write_coils.return_value = read_result - return mock_modbus + result = await async_setup_component(hass, DOMAIN, config) + assert result or not check_config_loaded + await hass.async_block_till_done() + return mock_pymodbus @pytest.fixture(name="mock_do_cycle") async def mock_do_cycle_fixture( hass: HomeAssistant, freezer: FrozenDateTimeFactory, - mock_pymodbus_exception, - mock_pymodbus_return, + mock_modbus, ) -> FrozenDateTimeFactory: """Trigger update call with time_changed event.""" freezer.tick(timedelta(seconds=1)) @@ -207,11 +183,12 @@ async def mock_test_state_fixture(hass, request): return request.param -@pytest.fixture(name="mock_ha") -async def mock_ha_fixture(hass, mock_pymodbus_return): +@pytest.fixture(name="mock_modbus_ha") +async def mock_modbus_ha_fixture(hass, mock_modbus): """Load homeassistant to allow service calls.""" assert await async_setup_component(hass, "homeassistant", {}) await hass.async_block_till_done() + return mock_modbus @pytest.fixture(name="caplog_setup_text") diff --git a/tests/components/modbus/test_binary_sensor.py b/tests/components/modbus/test_binary_sensor.py index 567618de3c6..7ae933998cf 100644 --- a/tests/components/modbus/test_binary_sensor.py +++ b/tests/components/modbus/test_binary_sensor.py @@ -207,7 +207,7 @@ async def test_all_binary_sensor(hass: HomeAssistant, expected, mock_do_cycle) - ], ) async def test_service_binary_sensor_update( - hass: HomeAssistant, mock_modbus, mock_ha + hass: HomeAssistant, mock_modbus_ha ) -> None: """Run test for service homeassistant.update_entity.""" @@ -217,7 +217,7 @@ async def test_service_binary_sensor_update( await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_OFF - mock_modbus.read_coils.return_value = ReadResult([0x01]) + mock_modbus_ha.read_coils.return_value = ReadResult([0x01]) await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) diff --git a/tests/components/modbus/test_climate.py b/tests/components/modbus/test_climate.py index 093dee67895..94778cdcbd2 100644 --- a/tests/components/modbus/test_climate.py +++ b/tests/components/modbus/test_climate.py @@ -496,10 +496,10 @@ async def test_temperature_error(hass: HomeAssistant, expected, mock_do_cycle) - ], ) async def test_service_climate_update( - hass: HomeAssistant, mock_modbus, mock_ha, result, register_words + hass: HomeAssistant, mock_modbus_ha, result, register_words ) -> None: """Run test for service homeassistant.update_entity.""" - mock_modbus.read_holding_registers.return_value = ReadResult(register_words) + mock_modbus_ha.read_holding_registers.return_value = ReadResult(register_words) await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) @@ -611,10 +611,10 @@ async def test_service_climate_update( ], ) async def test_service_climate_fan_update( - hass: HomeAssistant, mock_modbus, mock_ha, result, register_words + hass: HomeAssistant, mock_modbus_ha, result, register_words ) -> None: """Run test for service homeassistant.update_entity.""" - mock_modbus.read_holding_registers.return_value = ReadResult(register_words) + mock_modbus_ha.read_holding_registers.return_value = ReadResult(register_words) await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) @@ -751,10 +751,10 @@ async def test_service_climate_fan_update( ], ) async def test_service_climate_swing_update( - hass: HomeAssistant, mock_modbus, mock_ha, result, register_words + hass: HomeAssistant, mock_modbus_ha, result, register_words ) -> None: """Run test for service homeassistant.update_entity.""" - mock_modbus.read_holding_registers.return_value = ReadResult(register_words) + mock_modbus_ha.read_holding_registers.return_value = ReadResult(register_words) await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) @@ -844,10 +844,10 @@ async def test_service_climate_swing_update( ], ) async def test_service_climate_set_temperature( - hass: HomeAssistant, temperature, result, mock_modbus, mock_ha + hass: HomeAssistant, temperature, result, mock_modbus_ha ) -> None: """Test set_temperature.""" - mock_modbus.read_holding_registers.return_value = ReadResult(result) + mock_modbus_ha.read_holding_registers.return_value = ReadResult(result) await hass.services.async_call( CLIMATE_DOMAIN, "set_temperature", @@ -954,10 +954,10 @@ async def test_service_climate_set_temperature( ], ) async def test_service_set_hvac_mode( - hass: HomeAssistant, hvac_mode, result, mock_modbus, mock_ha + hass: HomeAssistant, hvac_mode, result, mock_modbus_ha ) -> None: """Test set HVAC mode.""" - mock_modbus.read_holding_registers.return_value = ReadResult(result) + mock_modbus_ha.read_holding_registers.return_value = ReadResult(result) await hass.services.async_call( CLIMATE_DOMAIN, @@ -1018,10 +1018,10 @@ async def test_service_set_hvac_mode( ], ) async def test_service_set_fan_mode( - hass: HomeAssistant, fan_mode, result, mock_modbus, mock_ha + hass: HomeAssistant, fan_mode, result, mock_modbus_ha ) -> None: """Test set Fan mode.""" - mock_modbus.read_holding_registers.return_value = ReadResult(result) + mock_modbus_ha.read_holding_registers.return_value = ReadResult(result) await hass.services.async_call( CLIMATE_DOMAIN, "set_fan_mode", @@ -1081,10 +1081,10 @@ async def test_service_set_fan_mode( ], ) async def test_service_set_swing_mode( - hass: HomeAssistant, swing_mode, result, mock_modbus, mock_ha + hass: HomeAssistant, swing_mode, result, mock_modbus_ha ) -> None: """Test set Swing mode.""" - mock_modbus.read_holding_registers.return_value = ReadResult(result) + mock_modbus_ha.read_holding_registers.return_value = ReadResult(result) await hass.services.async_call( CLIMATE_DOMAIN, "set_swing_mode", diff --git a/tests/components/modbus/test_cover.py b/tests/components/modbus/test_cover.py index fa9e617d96d..0860b3136ba 100644 --- a/tests/components/modbus/test_cover.py +++ b/tests/components/modbus/test_cover.py @@ -182,13 +182,13 @@ async def test_register_cover(hass: HomeAssistant, expected, mock_do_cycle) -> N }, ], ) -async def test_service_cover_update(hass: HomeAssistant, mock_modbus, mock_ha) -> None: +async def test_service_cover_update(hass: HomeAssistant, mock_modbus_ha) -> None: """Run test for service homeassistant.update_entity.""" await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) assert hass.states.get(ENTITY_ID).state == STATE_CLOSED - mock_modbus.read_holding_registers.return_value = ReadResult([0x01]) + mock_modbus_ha.read_holding_registers.return_value = ReadResult([0x01]) await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) @@ -255,30 +255,30 @@ async def test_restore_state_cover( }, ], ) -async def test_service_cover_move(hass: HomeAssistant, mock_modbus, mock_ha) -> None: +async def test_service_cover_move(hass: HomeAssistant, mock_modbus_ha) -> None: """Run test for service homeassistant.update_entity.""" - mock_modbus.read_holding_registers.return_value = ReadResult([0x01]) + mock_modbus_ha.read_holding_registers.return_value = ReadResult([0x01]) await hass.services.async_call( "cover", "open_cover", {"entity_id": ENTITY_ID}, blocking=True ) assert hass.states.get(ENTITY_ID).state == STATE_OPEN - mock_modbus.read_holding_registers.return_value = ReadResult([0x00]) + mock_modbus_ha.read_holding_registers.return_value = ReadResult([0x00]) await hass.services.async_call( "cover", "close_cover", {"entity_id": ENTITY_ID}, blocking=True ) assert hass.states.get(ENTITY_ID).state == STATE_CLOSED - await mock_modbus.reset() - mock_modbus.read_holding_registers.side_effect = ModbusException("fail write_") + await mock_modbus_ha.reset() + mock_modbus_ha.read_holding_registers.side_effect = ModbusException("fail write_") await hass.services.async_call( "cover", "close_cover", {"entity_id": ENTITY_ID}, blocking=True ) - assert mock_modbus.read_holding_registers.called + assert mock_modbus_ha.read_holding_registers.called assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE - mock_modbus.read_coils.side_effect = ModbusException("fail write_") + mock_modbus_ha.read_coils.side_effect = ModbusException("fail write_") await hass.services.async_call( "cover", "close_cover", {"entity_id": ENTITY_ID2}, blocking=True ) diff --git a/tests/components/modbus/test_fan.py b/tests/components/modbus/test_fan.py index 9719de3601b..d52b9dc309a 100644 --- a/tests/components/modbus/test_fan.py +++ b/tests/components/modbus/test_fan.py @@ -262,7 +262,6 @@ async def test_fan_service_turn( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_modbus, - mock_pymodbus_return, ) -> None: """Run test for service turn_on/turn_off.""" @@ -323,13 +322,13 @@ async def test_fan_service_turn( }, ], ) -async def test_service_fan_update(hass: HomeAssistant, mock_modbus, mock_ha) -> None: +async def test_service_fan_update(hass: HomeAssistant, mock_modbus_ha) -> None: """Run test for service homeassistant.update_entity.""" await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) assert hass.states.get(ENTITY_ID).state == STATE_OFF - mock_modbus.read_coils.return_value = ReadResult([0x01]) + mock_modbus_ha.read_coils.return_value = ReadResult([0x01]) await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 1219a04fb0c..82c65576f02 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -1366,7 +1366,6 @@ async def mock_modbus_read_pymodbus_fixture( do_type, do_scan_interval, do_return, - do_exception, caplog, mock_pymodbus, freezer: FrozenDateTimeFactory, @@ -1374,10 +1373,6 @@ async def mock_modbus_read_pymodbus_fixture( """Load integration modbus using mocked pymodbus.""" caplog.clear() caplog.set_level(logging.ERROR) - mock_pymodbus.read_coils.side_effect = do_exception - mock_pymodbus.read_discrete_inputs.side_effect = do_exception - mock_pymodbus.read_input_registers.side_effect = do_exception - mock_pymodbus.read_holding_registers.side_effect = do_exception mock_pymodbus.read_coils.return_value = do_return mock_pymodbus.read_discrete_inputs.return_value = do_return mock_pymodbus.read_input_registers.return_value = do_return @@ -1646,7 +1641,7 @@ async def test_shutdown( ], ) async def test_stop_restart( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_pymodbus_return + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_modbus ) -> None: """Run test for service stop.""" @@ -1657,7 +1652,7 @@ async def test_stop_restart( await hass.async_block_till_done() assert hass.states.get(entity_id).state == "17" - mock_pymodbus_return.reset_mock() + mock_modbus.reset_mock() caplog.clear() data = { ATTR_HUB: TEST_MODBUS_NAME, @@ -1665,23 +1660,23 @@ async def test_stop_restart( await hass.services.async_call(DOMAIN, SERVICE_STOP, data, blocking=True) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - assert mock_pymodbus_return.close.called + assert mock_modbus.close.called assert f"modbus {TEST_MODBUS_NAME} communication closed" in caplog.text - mock_pymodbus_return.reset_mock() + mock_modbus.reset_mock() caplog.clear() await hass.services.async_call(DOMAIN, SERVICE_RESTART, data, blocking=True) await hass.async_block_till_done() - assert not mock_pymodbus_return.close.called - assert mock_pymodbus_return.connect.called + assert not mock_modbus.close.called + assert mock_modbus.connect.called assert f"modbus {TEST_MODBUS_NAME} communication open" in caplog.text - mock_pymodbus_return.reset_mock() + mock_modbus.reset_mock() caplog.clear() await hass.services.async_call(DOMAIN, SERVICE_RESTART, data, blocking=True) await hass.async_block_till_done() - assert mock_pymodbus_return.close.called - assert mock_pymodbus_return.connect.called + assert mock_modbus.close.called + assert mock_modbus.connect.called assert f"modbus {TEST_MODBUS_NAME} communication closed" in caplog.text assert f"modbus {TEST_MODBUS_NAME} communication open" in caplog.text @@ -1711,7 +1706,7 @@ async def test_write_no_client(hass: HomeAssistant, mock_modbus) -> None: async def test_integration_reload( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - mock_pymodbus_return, + mock_modbus, freezer: FrozenDateTimeFactory, ) -> None: """Run test for integration reload.""" @@ -1732,7 +1727,7 @@ async def test_integration_reload( @pytest.mark.parametrize("do_config", [{}]) async def test_integration_reload_failed( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_pymodbus_return + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_modbus ) -> None: """Run test for integration connect failure on reload.""" caplog.set_level(logging.INFO) @@ -1741,9 +1736,7 @@ async def test_integration_reload_failed( yaml_path = get_fixture_path("configuration.yaml", "modbus") with ( mock.patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path), - mock.patch.object( - mock_pymodbus_return, "connect", side_effect=ModbusException("error") - ), + mock.patch.object(mock_modbus, "connect", side_effect=ModbusException("error")), ): await hass.services.async_call(DOMAIN, SERVICE_RELOAD, blocking=True) await hass.async_block_till_done() @@ -1754,7 +1747,7 @@ async def test_integration_reload_failed( @pytest.mark.parametrize("do_config", [{}]) async def test_integration_setup_failed( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_pymodbus_return + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_modbus ) -> None: """Run test for integration setup on reload.""" with mock.patch.object( diff --git a/tests/components/modbus/test_light.py b/tests/components/modbus/test_light.py index e5e1b56d77b..e74da085180 100644 --- a/tests/components/modbus/test_light.py +++ b/tests/components/modbus/test_light.py @@ -262,7 +262,6 @@ async def test_light_service_turn( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_modbus, - mock_pymodbus_return, ) -> None: """Run test for service turn_on/turn_off.""" @@ -300,12 +299,6 @@ async def test_light_service_turn( ) await hass.async_block_till_done() assert hass.states.get(ENTITY_ID2).state == STATE_UNAVAILABLE - mock_modbus.write_coil.side_effect = ModbusException("fail write_") - await hass.services.async_call( - "light", "turn_off", service_data={"entity_id": ENTITY_ID} - ) - await hass.async_block_till_done() - assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE @pytest.mark.parametrize( @@ -323,13 +316,13 @@ async def test_light_service_turn( }, ], ) -async def test_service_light_update(hass: HomeAssistant, mock_modbus, mock_ha) -> None: +async def test_service_light_update(hass: HomeAssistant, mock_modbus_ha) -> None: """Run test for service homeassistant.update_entity.""" await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) assert hass.states.get(ENTITY_ID).state == STATE_OFF - mock_modbus.read_coils.return_value = ReadResult([0x01]) + mock_modbus_ha.read_coils.return_value = ReadResult([0x01]) await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index 524acc0dabb..71cb64cc1b6 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -1391,14 +1391,14 @@ async def test_restore_state_sensor( }, ], ) -async def test_service_sensor_update(hass: HomeAssistant, mock_modbus, mock_ha) -> None: +async def test_service_sensor_update(hass: HomeAssistant, mock_modbus_ha) -> None: """Run test for service homeassistant.update_entity.""" - mock_modbus.read_input_registers.return_value = ReadResult([27]) + mock_modbus_ha.read_input_registers.return_value = ReadResult([27]) await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) assert hass.states.get(ENTITY_ID).state == "27" - mock_modbus.read_input_registers.return_value = ReadResult([32]) + mock_modbus_ha.read_input_registers.return_value = ReadResult([32]) await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) diff --git a/tests/components/modbus/test_switch.py b/tests/components/modbus/test_switch.py index 4eb0a5b3a18..bdb95c667c7 100644 --- a/tests/components/modbus/test_switch.py +++ b/tests/components/modbus/test_switch.py @@ -277,7 +277,6 @@ async def test_switch_service_turn( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_modbus, - mock_pymodbus_return, ) -> None: """Run test for service turn_on/turn_off.""" assert MODBUS_DOMAIN in hass.config.components @@ -337,13 +336,13 @@ async def test_switch_service_turn( }, ], ) -async def test_service_switch_update(hass: HomeAssistant, mock_modbus, mock_ha) -> None: +async def test_service_switch_update(hass: HomeAssistant, mock_modbus_ha) -> None: """Run test for service homeassistant.update_entity.""" await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) assert hass.states.get(ENTITY_ID).state == STATE_OFF - mock_modbus.read_coils.return_value = ReadResult([0x01]) + mock_modbus_ha.read_coils.return_value = ReadResult([0x01]) await hass.services.async_call( "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) @@ -368,9 +367,7 @@ async def test_service_switch_update(hass: HomeAssistant, mock_modbus, mock_ha) }, ], ) -async def test_delay_switch( - hass: HomeAssistant, mock_modbus, mock_pymodbus_return -) -> None: +async def test_delay_switch(hass: HomeAssistant, mock_modbus) -> None: """Run test for switch verify delay.""" mock_modbus.read_holding_registers.return_value = ReadResult([0x01]) now = dt_util.utcnow() From d4b62adfdcbe5bda75d0c8eb3d59487ed92a7ef5 Mon Sep 17 00:00:00 2001 From: Xander Date: Wed, 17 Apr 2024 18:38:12 +0100 Subject: [PATCH 642/967] Guard negative values for IPP states (#107446) * Guard negative values for IPP states * ruff format * Update sensor.py --------- Co-authored-by: Chris Talkington Co-authored-by: Erik Montnemery --- homeassistant/components/ipp/sensor.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ipp/sensor.py b/homeassistant/components/ipp/sensor.py index 1aad6ae6b21..8d3b97d0ca5 100644 --- a/homeassistant/components/ipp/sensor.py +++ b/homeassistant/components/ipp/sensor.py @@ -120,7 +120,10 @@ async def async_setup_entry( ATTR_MARKER_TYPE: marker.marker_type, }, ), - value_fn=_get_marker_value_fn(index, lambda marker: marker.level), + value_fn=_get_marker_value_fn( + index, + lambda marker: marker.level if marker.level >= 0 else None, + ), ), ) ) From 0a78e9d4aa6c3082afa2136549073643e44c8102 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 17 Apr 2024 12:46:15 -0500 Subject: [PATCH 643/967] Replace aiohttp-zlib-ng[isal] with aiohttp-isal (#115777) * Replace aiohttp-zlib-ng[isal] with aiohttp-isal The extra was causing wheel builds to fail Since isal works on all of our supported platforms we can always use it and drop the need for zlib-ng https://github.com/home-assistant/core/actions/runs/8725019072 https://github.com/bdraco/aiohttp-isal * typo --- homeassistant/components/http/__init__.py | 4 ++-- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 3e5f7333cbc..f9532b90ce6 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -21,7 +21,7 @@ from aiohttp.typedefs import JSONDecoder, StrOrURL from aiohttp.web_exceptions import HTTPMovedPermanently, HTTPRedirection from aiohttp.web_protocol import RequestHandler from aiohttp_fast_url_dispatcher import FastUrlDispatcher, attach_fast_url_dispatcher -from aiohttp_zlib_ng import enable_zlib_ng +from aiohttp_isal import enable_isal from cryptography import x509 from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import rsa @@ -202,7 +202,7 @@ class ApiConfig: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the HTTP API and debug interface.""" - enable_zlib_ng() + enable_isal() conf: ConfData | None = config.get(DOMAIN) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6dce47b734d..bd16f3c6147 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodhcpwatcher==1.0.0 aiodiscover==2.0.0 aiodns==3.2.0 aiohttp-fast-url-dispatcher==0.3.0 -aiohttp-zlib-ng[isal]==0.3.1 +aiohttp-isal==0.2.0 aiohttp==3.9.5 aiohttp_cors==0.7.0 aiohttp_session==2.12.0 diff --git a/pyproject.toml b/pyproject.toml index 90466aa7290..4b3b15f7bde 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ "aiohttp_cors==0.7.0", "aiohttp_session==2.12.0", "aiohttp-fast-url-dispatcher==0.3.0", - "aiohttp-zlib-ng[isal]==0.3.1", + "aiohttp-isal==0.2.0", "astral==2.2", "async-interrupt==1.1.1", "attrs==23.2.0", diff --git a/requirements.txt b/requirements.txt index 980bf84eb26..34ee8237921 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ aiohttp==3.9.5 aiohttp_cors==0.7.0 aiohttp_session==2.12.0 aiohttp-fast-url-dispatcher==0.3.0 -aiohttp-zlib-ng[isal]==0.3.1 +aiohttp-isal==0.2.0 astral==2.2 async-interrupt==1.1.1 attrs==23.2.0 From 8275512130d6a62faf35e61fea3f1bd84b9f8b33 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 17 Apr 2024 20:07:11 +0200 Subject: [PATCH 644/967] Add mqtt notify platform (#115653) * Add mqtt notify platform * Stale docstring --- .../components/mqtt/config_integration.py | 1 + homeassistant/components/mqtt/const.py | 1 + homeassistant/components/mqtt/discovery.py | 1 + homeassistant/components/mqtt/notify.py | 95 ++++ tests/components/mqtt/test_notify.py | 474 ++++++++++++++++++ 5 files changed, 572 insertions(+) create mode 100644 homeassistant/components/mqtt/notify.py create mode 100644 tests/components/mqtt/test_notify.py diff --git a/homeassistant/components/mqtt/config_integration.py b/homeassistant/components/mqtt/config_integration.py index 2500923ca9b..7244a41e975 100644 --- a/homeassistant/components/mqtt/config_integration.py +++ b/homeassistant/components/mqtt/config_integration.py @@ -45,6 +45,7 @@ CONFIG_SCHEMA_BASE = vol.Schema( Platform.LAWN_MOWER.value: vol.All(cv.ensure_list, [dict]), Platform.LIGHT.value: vol.All(cv.ensure_list, [dict]), Platform.LOCK.value: vol.All(cv.ensure_list, [dict]), + Platform.NOTIFY.value: vol.All(cv.ensure_list, [dict]), Platform.NUMBER.value: vol.All(cv.ensure_list, [dict]), Platform.SCENE.value: vol.All(cv.ensure_list, [dict]), Platform.SELECT.value: vol.All(cv.ensure_list, [dict]), diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 82320cd2f11..7eca266edfa 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -157,6 +157,7 @@ RELOADABLE_PLATFORMS = [ Platform.LIGHT, Platform.LAWN_MOWER, Platform.LOCK, + Platform.NOTIFY, Platform.NUMBER, Platform.SCENE, Platform.SELECT, diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 43f4f8cfd46..e330cd9b44b 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -66,6 +66,7 @@ SUPPORTED_COMPONENTS = { "lawn_mower", "light", "lock", + "notify", "number", "scene", "siren", diff --git a/homeassistant/components/mqtt/notify.py b/homeassistant/components/mqtt/notify.py new file mode 100644 index 00000000000..b7a17f07f7f --- /dev/null +++ b/homeassistant/components/mqtt/notify.py @@ -0,0 +1,95 @@ +"""Support for MQTT notify.""" + +from __future__ import annotations + +import voluptuous as vol + +from homeassistant.components import notify +from homeassistant.components.notify import NotifyEntity +from homeassistant.config_entries import ConfigEntry +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 + +from .config import DEFAULT_RETAIN, MQTT_BASE_SCHEMA +from .const import ( + CONF_COMMAND_TEMPLATE, + CONF_COMMAND_TOPIC, + CONF_ENCODING, + CONF_QOS, + CONF_RETAIN, +) +from .mixins import ( + MQTT_ENTITY_COMMON_SCHEMA, + MqttEntity, + async_setup_entity_entry_helper, +) +from .models import MqttCommandTemplate +from .util import valid_publish_topic + +DEFAULT_NAME = "MQTT notify" + +PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend( + { + vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, + vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), + vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, + } +).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) + +DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up MQTT notify through YAML and through MQTT discovery.""" + await async_setup_entity_entry_helper( + hass, + config_entry, + MqttNotify, + notify.DOMAIN, + async_add_entities, + DISCOVERY_SCHEMA, + PLATFORM_SCHEMA_MODERN, + ) + + +class MqttNotify(MqttEntity, NotifyEntity): + """Representation of a notification entity service that can send messages using MQTT.""" + + _default_name = DEFAULT_NAME + _entity_id_format = notify.ENTITY_ID_FORMAT + + @staticmethod + def config_schema() -> vol.Schema: + """Return the config schema.""" + return DISCOVERY_SCHEMA + + def _setup_from_config(self, config: ConfigType) -> None: + """(Re)Setup the entity.""" + self._command_template = MqttCommandTemplate( + config.get(CONF_COMMAND_TEMPLATE), entity=self + ).async_render + + def _prepare_subscribe_topics(self) -> None: + """(Re)Subscribe to topics.""" + + async def _subscribe_topics(self) -> None: + """(Re)Subscribe to topics.""" + + async def async_send_message(self, message: str) -> None: + """Send a message.""" + payload = self._command_template(message) + await self.async_publish( + self._config[CONF_COMMAND_TOPIC], + payload, + self._config[CONF_QOS], + self._config[CONF_RETAIN], + self._config[CONF_ENCODING], + ) diff --git a/tests/components/mqtt/test_notify.py b/tests/components/mqtt/test_notify.py new file mode 100644 index 00000000000..bc833b79eb0 --- /dev/null +++ b/tests/components/mqtt/test_notify.py @@ -0,0 +1,474 @@ +"""The tests for the MQTT notify platform.""" + +import copy +from typing import Any +from unittest.mock import patch + +import pytest + +from homeassistant.components import mqtt, notify +from homeassistant.components.notify import ATTR_MESSAGE +from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, STATE_UNKNOWN +from homeassistant.core import HomeAssistant + +from .test_common import ( + help_test_availability_when_connection_lost, + help_test_availability_without_topic, + help_test_custom_availability_payload, + help_test_default_availability_payload, + help_test_discovery_broken, + help_test_discovery_removal, + help_test_discovery_update, + help_test_discovery_update_attr, + help_test_discovery_update_unchanged, + help_test_entity_debug_info_message, + help_test_entity_device_info_remove, + help_test_entity_device_info_update, + help_test_entity_device_info_with_connection, + help_test_entity_device_info_with_identifier, + help_test_entity_id_update_discovery_update, + help_test_publishing_with_custom_encoding, + help_test_reloadable, + help_test_setting_attribute_via_mqtt_json_message, + help_test_setting_attribute_with_template, + help_test_setting_blocked_attribute_via_mqtt_json_message, + help_test_unique_id, + help_test_unload_config_entry_with_platform, + help_test_update_with_json_attrs_bad_json, + help_test_update_with_json_attrs_not_dict, +) + +from tests.typing import MqttMockHAClientGenerator, MqttMockPahoClient + +DEFAULT_CONFIG = { + mqtt.DOMAIN: {notify.DOMAIN: {"name": "test", "command_topic": "test-topic"}} +} + + +@pytest.mark.freeze_time("2021-11-08 13:31:44+00:00") +@pytest.mark.parametrize( + "hass_config", + [ + { + mqtt.DOMAIN: { + notify.DOMAIN: { + "command_topic": "command-topic", + "name": "test", + "object_id": "test_notify", + "qos": "2", + } + } + } + ], +) +async def test_sending_mqtt_commands( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test the sending MQTT commands.""" + mqtt_mock = await mqtt_mock_entry() + + state = hass.states.get("notify.test_notify") + assert state.state == STATE_UNKNOWN + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "test" + + await hass.services.async_call( + notify.DOMAIN, + notify.SERVICE_SEND_MESSAGE, + {ATTR_MESSAGE: "Beer message", ATTR_ENTITY_ID: "notify.test_notify"}, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with( + "command-topic", "Beer message", 2, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("notify.test_notify") + assert state.state == "2021-11-08T13:31:44+00:00" + + +@pytest.mark.parametrize( + "hass_config", + [ + { + mqtt.DOMAIN: { + notify.DOMAIN: { + "command_topic": "command-topic", + "command_template": '{ "{{ entity_id }}": "{{ value }}" }', + "name": "test", + } + } + } + ], +) +async def test_command_template( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test the sending of MQTT commands through a command template.""" + mqtt_mock = await mqtt_mock_entry() + + state = hass.states.get("notify.test") + assert state.state == STATE_UNKNOWN + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "test" + + await hass.services.async_call( + notify.DOMAIN, + notify.SERVICE_SEND_MESSAGE, + {ATTR_MESSAGE: "Beer message", ATTR_ENTITY_ID: "notify.test"}, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with( + "command-topic", '{ "notify.test": "Beer message" }', 0, False + ) + mqtt_mock.async_publish.reset_mock() + + +@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) +async def test_availability_when_connection_lost( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test availability after MQTT disconnection.""" + await help_test_availability_when_connection_lost( + hass, mqtt_mock_entry, notify.DOMAIN + ) + + +@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) +async def test_availability_without_topic( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test availability without defined availability topic.""" + await help_test_availability_without_topic( + hass, mqtt_mock_entry, notify.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_default_availability_payload( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test availability by default payload with defined topic.""" + await help_test_default_availability_payload( + hass, mqtt_mock_entry, notify.DOMAIN, DEFAULT_CONFIG, True + ) + + +async def test_custom_availability_payload( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test availability by custom payload with defined topic.""" + await help_test_custom_availability_payload( + hass, + mqtt_mock_entry, + notify.DOMAIN, + DEFAULT_CONFIG, + True, + ) + + +async def test_setting_attribute_via_mqtt_json_message( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry, notify.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry, notify.DOMAIN, DEFAULT_CONFIG, None + ) + + +async def test_setting_attribute_with_template( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_attribute_with_template( + hass, mqtt_mock_entry, notify.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_update_with_json_attrs_not_dict( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test attributes get extracted from a JSON result.""" + await help_test_update_with_json_attrs_not_dict( + hass, + mqtt_mock_entry, + caplog, + notify.DOMAIN, + DEFAULT_CONFIG, + ) + + +async def test_update_with_json_attrs_bad_json( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test attributes get extracted from a JSON result.""" + await help_test_update_with_json_attrs_bad_json( + hass, + mqtt_mock_entry, + caplog, + notify.DOMAIN, + DEFAULT_CONFIG, + ) + + +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, + notify.DOMAIN, + DEFAULT_CONFIG, + ) + + +@pytest.mark.parametrize( + "hass_config", + [ + { + mqtt.DOMAIN: { + notify.DOMAIN: [ + { + "name": "Test 1", + "command_topic": "command-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "name": "Test 2", + "command_topic": "command-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } + } + ], +) +async def test_unique_id( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test unique id option only creates one notify entity per unique_id.""" + await help_test_unique_id(hass, mqtt_mock_entry, notify.DOMAIN) + + +async def test_discovery_removal_notify( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> 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 + ) + + +async def test_discovery_update_notify( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test update of discovered notify.""" + config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][notify.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][notify.DOMAIN]) + config1["name"] = "Beer" + config2["name"] = "Milk" + + await help_test_discovery_update( + hass, + mqtt_mock_entry, + caplog, + notify.DOMAIN, + config1, + config2, + ) + + +async def test_discovery_update_unchanged_notify( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test update of discovered notify.""" + data1 = ( + '{ "name": "Beer",' + ' "state_topic": "test_topic",' + ' "command_topic": "test_topic" }' + ) + with patch( + "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, + ) + + +@pytest.mark.no_fail_on_log_exception +async def test_discovery_broken( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> 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 + ) + + +async def test_entity_device_info_with_connection( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test MQTT notify device registry integration.""" + await help_test_entity_device_info_with_connection( + hass, mqtt_mock_entry, notify.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_device_info_with_identifier( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test MQTT notify device registry integration.""" + await help_test_entity_device_info_with_identifier( + hass, mqtt_mock_entry, notify.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_device_info_update( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test device registry update.""" + await help_test_entity_device_info_update( + hass, mqtt_mock_entry, notify.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_device_info_remove( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test device registry remove.""" + await help_test_entity_device_info_remove( + hass, mqtt_mock_entry, notify.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_id_update_discovery_update( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test MQTT discovery update when entity_id is updated.""" + await help_test_entity_id_update_discovery_update( + hass, mqtt_mock_entry, notify.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_debug_info_message( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test MQTT debug info.""" + await help_test_entity_debug_info_message( + hass, + mqtt_mock_entry, + notify.DOMAIN, + DEFAULT_CONFIG, + notify.SERVICE_SEND_MESSAGE, + command_topic="test-topic", + command_payload="Milk", + state_topic=None, + service_parameters={"message": "Milk"}, + ) + + +@pytest.mark.parametrize( + ("service", "topic", "parameters", "payload", "template"), + [ + ( + notify.SERVICE_SEND_MESSAGE, + "command_topic", + {"message": "Beer test"}, + "Beer test", + "command_template", + ), + ], +) +async def test_publishing_with_custom_encoding( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, + service: str, + topic: str, + parameters: dict[str, Any], + payload: str, + template: str | None, +) -> None: + """Test publishing MQTT payload with different encoding.""" + domain = notify.DOMAIN + config = DEFAULT_CONFIG + + await help_test_publishing_with_custom_encoding( + hass, + mqtt_mock_entry, + caplog, + domain, + config, + service, + topic, + parameters, + payload, + template, + ) + + +async def test_reloadable( + hass: HomeAssistant, + mqtt_client_mock: MqttMockPahoClient, +) -> None: + """Test reloading the MQTT platform.""" + domain = notify.DOMAIN + config = DEFAULT_CONFIG + await help_test_reloadable(hass, mqtt_client_mock, domain, config) + + +@pytest.mark.parametrize( + "hass_config", + [DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}], + ids=["platform_key", "listed"], +) +async def test_setup_manual_entity_from_yaml( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test setup manual configured MQTT entity.""" + await mqtt_mock_entry() + platform = notify.DOMAIN + assert hass.states.get(f"{platform}.test") + + +async def test_unload_entry( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, +) -> None: + """Test unloading the config entry.""" + domain = notify.DOMAIN + config = DEFAULT_CONFIG + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry, domain, config + ) From cdc49328be0520258ddd314f10e63d0ef69a8d9c Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Wed, 17 Apr 2024 21:08:10 +0200 Subject: [PATCH 645/967] Address late reviews for the enigma2 config flow (#115768) * Address late reviews for the enigma2 config flow * fix tests * review comments * test for issues * Apply suggestions from code review Co-authored-by: Joost Lekkerkerker --------- Co-authored-by: Joost Lekkerkerker --- .../components/enigma2/config_flow.py | 67 ++++++++++--------- homeassistant/components/enigma2/strings.json | 15 ++++- tests/components/enigma2/test_config_flow.py | 25 +++++-- 3 files changed, 72 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/enigma2/config_flow.py b/homeassistant/components/enigma2/config_flow.py index c144f2b7dae..ac57bd9d0fa 100644 --- a/homeassistant/components/enigma2/config_flow.py +++ b/homeassistant/components/enigma2/config_flow.py @@ -8,7 +8,6 @@ from openwebif.error import InvalidAuthError import voluptuous as vol from yarl import URL -from homeassistant.components.homeassistant import DOMAIN as HOMEASSISTANT_DOMAIN from homeassistant.config_entries import SOURCE_USER, ConfigFlow, ConfigFlowResult from homeassistant.const import ( CONF_HOST, @@ -18,6 +17,7 @@ from homeassistant.const import ( CONF_USERNAME, CONF_VERIFY_SSL, ) +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN from homeassistant.helpers import selector from homeassistant.helpers.aiohttp_client import async_create_clientsession from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue @@ -68,17 +68,12 @@ class Enigma2ConfigFlowHandler(ConfigFlow, domain=DOMAIN): ) OPTIONS_KEYS = (CONF_DEEP_STANDBY, CONF_SOURCE_BOUQUET, CONF_USE_CHANNEL_ICON) - def __init__(self) -> None: - """Initialize the config flow.""" - super().__init__() - self.errors: dict[str, str] = {} - self._data: dict[str, Any] = {} - self._options: dict[str, Any] = {} - - async def validate_user_input(self, user_input: dict[str, Any]) -> dict[str, Any]: + async def validate_user_input( + self, user_input: dict[str, Any] + ) -> dict[str, str] | None: """Validate user input.""" - self.errors = {} + errors = None self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) @@ -97,16 +92,16 @@ class Enigma2ConfigFlowHandler(ConfigFlow, domain=DOMAIN): try: about = await OpenWebIfDevice(session).get_about() except InvalidAuthError: - self.errors["base"] = "invalid_auth" + errors = {"base": "invalid_auth"} except ClientError: - self.errors["base"] = "cannot_connect" + errors = {"base": "cannot_connect"} except Exception: # pylint: disable=broad-except - self.errors["base"] = "unknown" + errors = {"base": "unknown"} else: await self.async_set_unique_id(about["info"]["ifaces"][0]["mac"]) self._abort_if_unique_id_configured() - return user_input + return errors async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -115,23 +110,41 @@ class Enigma2ConfigFlowHandler(ConfigFlow, domain=DOMAIN): if user_input is None: return self.async_show_form(step_id=SOURCE_USER, data_schema=CONFIG_SCHEMA) - data = await self.validate_user_input(user_input) - if "base" in self.errors: + if errors := await self.validate_user_input(user_input): return self.async_show_form( - step_id=SOURCE_USER, data_schema=CONFIG_SCHEMA, errors=self.errors + step_id=SOURCE_USER, data_schema=CONFIG_SCHEMA, errors=errors ) - return self.async_create_entry( - data=data, title=data[CONF_HOST], options=self._options - ) + return self.async_create_entry(data=user_input, title=user_input[CONF_HOST]) async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult: - """Validate import.""" + """Handle the import step.""" if CONF_PORT not in user_input: user_input[CONF_PORT] = DEFAULT_PORT if CONF_SSL not in user_input: user_input[CONF_SSL] = DEFAULT_SSL user_input[CONF_VERIFY_SSL] = DEFAULT_VERIFY_SSL + data = {key: user_input[key] for key in user_input if key in self.DATA_KEYS} + options = { + key: user_input[key] for key in user_input if key in self.OPTIONS_KEYS + } + + if errors := await self.validate_user_input(user_input): + async_create_issue( + self.hass, + DOMAIN, + f"deprecated_yaml_{DOMAIN}_import_issue_{errors["base"]}", + breaks_in_ha_version="2024.11.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key=f"deprecated_yaml_import_issue_{errors["base"]}", + translation_placeholders={ + "url": "/config/integrations/dashboard/add?domain=enigma2" + }, + ) + return self.async_abort(reason=errors["base"]) + async_create_issue( self.hass, HOMEASSISTANT_DOMAIN, @@ -147,12 +160,6 @@ class Enigma2ConfigFlowHandler(ConfigFlow, domain=DOMAIN): "integration_title": "Enigma2", }, ) - - self._data = { - key: user_input[key] for key in user_input if key in self.DATA_KEYS - } - self._options = { - key: user_input[key] for key in user_input if key in self.OPTIONS_KEYS - } - - return await self.async_step_user(self._data) + return self.async_create_entry( + data=data, title=data[CONF_HOST], options=options + ) diff --git a/homeassistant/components/enigma2/strings.json b/homeassistant/components/enigma2/strings.json index 888c6d59387..ddeb59ea6d5 100644 --- a/homeassistant/components/enigma2/strings.json +++ b/homeassistant/components/enigma2/strings.json @@ -10,7 +10,6 @@ "ssl": "[%key:common::config_flow::data::ssl%]", "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]", - "name": "[%key:common::config_flow::data::name%]", "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" } } @@ -26,5 +25,19 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "unknown": "[%key:common::config_flow::error::unknown%]" } + }, + "issues": { + "deprecated_yaml_import_issue_unknown": { + "title": "The Enigma2 YAML configuration import failed", + "description": "Configuring Enigma2 using YAML is being removed but there was an error importing your YAML configuration.\n\nEnsure connection to the device works, the authentication details are correct and restart Home Assistant to try again or remove the Enigma2 YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." + }, + "deprecated_yaml_import_issue_invalid_auth": { + "title": "The Enigma2 YAML configuration import failed", + "description": "Configuring Enigma2 using YAML is being removed but there was an error importing your YAML configuration.\n\nEnsure the authentication details are correct and restart Home Assistant to try again or remove the Enigma2 YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." + }, + "deprecated_yaml_import_issue_cannot_connect": { + "title": "The Enigma2 YAML configuration import failed", + "description": "Configuring Enigma2 using YAML is being removed but there was an error importing your YAML configuration.\n\nEnsure connection to the device works and restart Home Assistant to try again or remove the Enigma2 YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." + } } } diff --git a/tests/components/enigma2/test_config_flow.py b/tests/components/enigma2/test_config_flow.py index dcd249ad943..dfca569276d 100644 --- a/tests/components/enigma2/test_config_flow.py +++ b/tests/components/enigma2/test_config_flow.py @@ -10,8 +10,9 @@ import pytest from homeassistant import config_entries from homeassistant.components.enigma2.const import DOMAIN from homeassistant.const import CONF_HOST -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.issue_registry import IssueRegistry from .conftest import ( EXPECTED_OPTIONS, @@ -96,6 +97,7 @@ async def test_form_import( test_config: dict[str, Any], expected_data: dict[str, Any], expected_options: dict[str, Any], + issue_registry: IssueRegistry, ) -> None: """Test we get the form with import source.""" with ( @@ -115,6 +117,12 @@ async def test_form_import( ) await hass.async_block_till_done() + issue = issue_registry.async_get_issue( + HOMEASSISTANT_DOMAIN, f"deprecated_yaml_{DOMAIN}" + ) + + assert issue + assert issue.issue_domain == DOMAIN assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == test_config[CONF_HOST] assert result["data"] == expected_data @@ -132,7 +140,10 @@ async def test_form_import( ], ) async def test_form_import_errors( - hass: HomeAssistant, exception: Exception, error_type: str + hass: HomeAssistant, + exception: Exception, + error_type: str, + issue_registry: IssueRegistry, ) -> None: """Test we handle errors on import.""" with patch( @@ -145,5 +156,11 @@ async def test_form_import_errors( data=TEST_IMPORT_FULL, ) - assert result["type"] == FlowResultType.FORM - assert result["errors"] == {"base": error_type} + issue = issue_registry.async_get_issue( + DOMAIN, f"deprecated_yaml_{DOMAIN}_import_issue_{error_type}" + ) + + assert issue + assert issue.issue_domain == DOMAIN + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == error_type From 07a46f17d0e70ed963e823031fbd5dc8b436c911 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 17 Apr 2024 21:20:12 +0200 Subject: [PATCH 646/967] Add sanix sensor tests (#115763) Add sanix tests --- tests/components/sanix/conftest.py | 36 ++- .../sanix/snapshots/test_sensor.ambr | 292 ++++++++++++++++++ tests/components/sanix/test_init.py | 2 +- tests/components/sanix/test_sensor.py | 39 +++ 4 files changed, 362 insertions(+), 7 deletions(-) create mode 100644 tests/components/sanix/snapshots/test_sensor.ambr create mode 100644 tests/components/sanix/test_sensor.py diff --git a/tests/components/sanix/conftest.py b/tests/components/sanix/conftest.py index 297416a6290..d1f4424b166 100644 --- a/tests/components/sanix/conftest.py +++ b/tests/components/sanix/conftest.py @@ -1,9 +1,21 @@ """Sanix tests configuration.""" from collections.abc import Generator -from unittest.mock import AsyncMock, MagicMock, patch +from datetime import datetime +from unittest.mock import AsyncMock, patch +from zoneinfo import ZoneInfo import pytest +from sanix import ( + ATTR_API_BATTERY, + ATTR_API_DEVICE_NO, + ATTR_API_DISTANCE, + ATTR_API_FILL_PERC, + ATTR_API_SERVICE_DATE, + ATTR_API_SSID, + ATTR_API_STATUS, + ATTR_API_TIME, +) from sanix.models import Measurement from homeassistant.components.sanix.const import CONF_SERIAL_NUMBER, DOMAIN @@ -15,19 +27,31 @@ from tests.common import MockConfigEntry, load_json_object_fixture @pytest.fixture def mock_sanix(): """Build a fixture for the Sanix API that connects successfully and returns measurements.""" - fixture = load_json_object_fixture("sanix/get_measurements.json") - mock_sanix_api = MagicMock() + fixture = load_json_object_fixture("get_measurements.json", DOMAIN) with ( patch( "homeassistant.components.sanix.config_flow.Sanix", - return_value=mock_sanix_api, + autospec=True, ) as mock_sanix_api, patch( "homeassistant.components.sanix.Sanix", - return_value=mock_sanix_api, + new=mock_sanix_api, ), ): - mock_sanix_api.return_value.fetch_data.return_value = Measurement(**fixture) + mock_sanix_api.return_value.fetch_data.return_value = Measurement( + battery=fixture[ATTR_API_BATTERY], + device_no=fixture[ATTR_API_DEVICE_NO], + distance=fixture[ATTR_API_DISTANCE], + fill_perc=fixture[ATTR_API_FILL_PERC], + service_date=datetime.strptime( + fixture[ATTR_API_SERVICE_DATE], "%d.%m.%Y" + ).date(), + ssid=fixture[ATTR_API_SSID], + status=fixture[ATTR_API_STATUS], + time=datetime.strptime(fixture[ATTR_API_TIME], "%d.%m.%Y %H:%M:%S").replace( + tzinfo=ZoneInfo("Europe/Warsaw") + ), + ) yield mock_sanix_api diff --git a/tests/components/sanix/snapshots/test_sensor.ambr b/tests/components/sanix/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..792d2a3be64 --- /dev/null +++ b/tests/components/sanix/snapshots/test_sensor.ambr @@ -0,0 +1,292 @@ +# serializer version: 1 +# name: test_all_entities[sensor.sanix_battery-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.sanix_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': 'sanix', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'battery', + 'unique_id': '1810088-battery', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[sensor.sanix_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Sanix Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.sanix_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_all_entities[sensor.sanix_device_number-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.sanix_device_number', + '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': 'Device number', + 'platform': 'sanix', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'device_no', + 'unique_id': '1810088-device_no', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.sanix_device_number-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Sanix Device number', + }), + 'context': , + 'entity_id': 'sensor.sanix_device_number', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'SANIX-1810088', + }) +# --- +# name: test_all_entities[sensor.sanix_distance-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.sanix_distance', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Distance', + 'platform': 'sanix', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'distance', + 'unique_id': '1810088-distance', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sanix_distance-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'distance', + 'friendly_name': 'Sanix Distance', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sanix_distance', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '109', + }) +# --- +# name: test_all_entities[sensor.sanix_filled-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.sanix_filled', + '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': 'Filled', + 'platform': 'sanix', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'fill_perc', + 'unique_id': '1810088-fill_perc', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[sensor.sanix_filled-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Sanix Filled', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.sanix_filled', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '32', + }) +# --- +# name: test_all_entities[sensor.sanix_service_date-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.sanix_service_date', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Service date', + 'platform': 'sanix', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'service_date', + 'unique_id': '1810088-service_date', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.sanix_service_date-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'date', + 'friendly_name': 'Sanix Service date', + }), + 'context': , + 'entity_id': 'sensor.sanix_service_date', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-06-15', + }) +# --- +# name: test_all_entities[sensor.sanix_ssid-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.sanix_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': 'sanix', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ssid', + 'unique_id': '1810088-ssid', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.sanix_ssid-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Sanix SSID', + }), + 'context': , + 'entity_id': 'sensor.sanix_ssid', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Wifi', + }) +# --- diff --git a/tests/components/sanix/test_init.py b/tests/components/sanix/test_init.py index 57e4920da11..467737628fe 100644 --- a/tests/components/sanix/test_init.py +++ b/tests/components/sanix/test_init.py @@ -1,4 +1,4 @@ -"""Test the Home Assistant analytics init module.""" +"""Test the Sanix init module.""" from __future__ import annotations diff --git a/tests/components/sanix/test_sensor.py b/tests/components/sanix/test_sensor.py new file mode 100644 index 00000000000..d9729ca3c25 --- /dev/null +++ b/tests/components/sanix/test_sensor.py @@ -0,0 +1,39 @@ +"""Test the Sanix sensor module.""" + +from unittest.mock import AsyncMock, patch + +import pytest +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 + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_sanix: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch( + "homeassistant.components.sanix.PLATFORMS", + [Platform.SENSOR], + ): + await setup_integration(hass, mock_config_entry) + entity_entries = er.async_entries_for_config_entry( + entity_registry, mock_config_entry.entry_id + ) + + assert entity_entries + for entity_entry in entity_entries: + assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry") + assert (state := hass.states.get(entity_entry.entity_id)) + assert state == snapshot(name=f"{entity_entry.entity_id}-state") From 0aa7946208ee665a52aff814032f50ef32a33307 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 17 Apr 2024 12:23:58 -0700 Subject: [PATCH 647/967] Bump google-nest-sdm to 3.0.4 (#115731) --- 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 89244642207..354066e2d87 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==3.0.3"] + "requirements": ["google-nest-sdm==3.0.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index ab9f24284e3..a9bc97d9158 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -962,7 +962,7 @@ google-cloud-texttospeech==2.12.3 google-generativeai==0.3.1 # homeassistant.components.nest -google-nest-sdm==3.0.3 +google-nest-sdm==3.0.4 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ab9b1e94b88..6c40bd21e84 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -788,7 +788,7 @@ google-cloud-pubsub==2.13.11 google-generativeai==0.3.1 # homeassistant.components.nest -google-nest-sdm==3.0.3 +google-nest-sdm==3.0.4 # homeassistant.components.google_travel_time googlemaps==2.5.1 From 11931cdb5669c0e1b59c0541c92847358427663c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 17 Apr 2024 14:28:09 -0500 Subject: [PATCH 648/967] Simplify labels and areas template calls (#115673) The labels and areas are already exposed on the object --- homeassistant/helpers/template.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index d344a473494..1f0742e896d 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1453,8 +1453,7 @@ def floor_areas(hass: HomeAssistant, floor_id_or_name: str) -> Iterable[str]: def areas(hass: HomeAssistant) -> Iterable[str | None]: """Return all areas.""" - area_reg = area_registry.async_get(hass) - return [area.id for area in area_reg.async_list_areas()] + return list(area_registry.async_get(hass).areas) def area_id(hass: HomeAssistant, lookup_value: str) -> str | None: @@ -1580,7 +1579,7 @@ def labels(hass: HomeAssistant, lookup_value: Any = None) -> Iterable[str | None """Return all labels, or those from a area ID, device ID, or entity ID.""" label_reg = label_registry.async_get(hass) if lookup_value is None: - return [label.label_id for label in label_reg.async_list_labels()] + return list(label_reg.labels) ent_reg = entity_registry.async_get(hass) From 7188d62340ea790f898ba7612c10e89f217312d1 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Wed, 17 Apr 2024 16:37:38 -0300 Subject: [PATCH 649/967] Bump Broadlink to 0.19.0 (#115742) Co-authored-by: J. Nick Koston --- homeassistant/components/broadlink/const.py | 2 ++ homeassistant/components/broadlink/manifest.json | 2 +- homeassistant/components/broadlink/remote.py | 2 +- homeassistant/components/broadlink/switch.py | 2 +- homeassistant/components/broadlink/updater.py | 11 +++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 18 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/broadlink/const.py b/homeassistant/components/broadlink/const.py index 91d4358a077..41c4964c2b3 100644 --- a/homeassistant/components/broadlink/const.py +++ b/homeassistant/components/broadlink/const.py @@ -9,6 +9,7 @@ DOMAINS_AND_TYPES = { Platform.REMOTE: {"RM4MINI", "RM4PRO", "RMMINI", "RMMINIB", "RMPRO"}, Platform.SENSOR: { "A1", + "MP1S", "RM4MINI", "RM4PRO", "RMPRO", @@ -20,6 +21,7 @@ DOMAINS_AND_TYPES = { Platform.SWITCH: { "BG1", "MP1", + "MP1S", "RM4MINI", "RM4PRO", "RMMINI", diff --git a/homeassistant/components/broadlink/manifest.json b/homeassistant/components/broadlink/manifest.json index 7fd925a2ff4..bf5dfb16584 100644 --- a/homeassistant/components/broadlink/manifest.json +++ b/homeassistant/components/broadlink/manifest.json @@ -38,5 +38,5 @@ "documentation": "https://www.home-assistant.io/integrations/broadlink", "iot_class": "local_polling", "loggers": ["broadlink"], - "requirements": ["broadlink==0.18.3"] + "requirements": ["broadlink==0.19.0"] } diff --git a/homeassistant/components/broadlink/remote.py b/homeassistant/components/broadlink/remote.py index f8d903c51eb..55368e5ff59 100644 --- a/homeassistant/components/broadlink/remote.py +++ b/homeassistant/components/broadlink/remote.py @@ -373,7 +373,7 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity): start_time = dt_util.utcnow() while (dt_util.utcnow() - start_time) < LEARNING_TIMEOUT: await asyncio.sleep(1) - found = await device.async_request(device.api.check_frequency) + found = await device.async_request(device.api.check_frequency)[0] if found: break else: diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py index f61e726b1d5..9cf7e3391fa 100644 --- a/homeassistant/components/broadlink/switch.py +++ b/homeassistant/components/broadlink/switch.py @@ -129,7 +129,7 @@ async def async_setup_entry( elif device.api.type == "BG1": switches.extend(BroadlinkBG1Slot(device, slot) for slot in range(1, 3)) - elif device.api.type == "MP1": + elif device.api.type in {"MP1", "MP1S"}: switches.extend(BroadlinkMP1Slot(device, slot) for slot in range(1, 5)) async_add_entities(switches) diff --git a/homeassistant/components/broadlink/updater.py b/homeassistant/components/broadlink/updater.py index 20b241b0d89..f678af0105f 100644 --- a/homeassistant/components/broadlink/updater.py +++ b/homeassistant/components/broadlink/updater.py @@ -21,6 +21,7 @@ def get_update_manager(device): "LB1": BroadlinkLB1UpdateManager, "LB2": BroadlinkLB1UpdateManager, "MP1": BroadlinkMP1UpdateManager, + "MP1S": BroadlinkMP1SUpdateManager, "RM4MINI": BroadlinkRMUpdateManager, "RM4PRO": BroadlinkRMUpdateManager, "RMMINI": BroadlinkRMUpdateManager, @@ -112,6 +113,16 @@ class BroadlinkMP1UpdateManager(BroadlinkUpdateManager): return await self.device.async_request(self.device.api.check_power) +class BroadlinkMP1SUpdateManager(BroadlinkUpdateManager): + """Manages updates for Broadlink MP1 devices.""" + + async def async_fetch_data(self): + """Fetch data from the device.""" + power = await self.device.async_request(self.device.api.check_power) + sensors = await self.device.async_request(self.device.api.get_state) + return {**power, **sensors} + + class BroadlinkRMUpdateManager(BroadlinkUpdateManager): """Manages updates for Broadlink remotes.""" diff --git a/requirements_all.txt b/requirements_all.txt index a9bc97d9158..66c36c7ac18 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -601,7 +601,7 @@ boto3==1.34.51 bring-api==0.5.7 # homeassistant.components.broadlink -broadlink==0.18.3 +broadlink==0.19.0 # homeassistant.components.brother brother==4.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6c40bd21e84..5bac0527854 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -512,7 +512,7 @@ boschshcpy==0.2.91 bring-api==0.5.7 # homeassistant.components.broadlink -broadlink==0.18.3 +broadlink==0.19.0 # homeassistant.components.brother brother==4.1.0 From b829f1030bd67ca9f83d2b6a464e11cc53a1d95f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 17 Apr 2024 14:59:16 -0500 Subject: [PATCH 650/967] Migrate snooze config flow to use eager_start (#115658) --- homeassistant/components/snooz/config_flow.py | 2 +- tests/components/snooz/test_config_flow.py | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/snooz/config_flow.py b/homeassistant/components/snooz/config_flow.py index 9992a68ef69..3962a44d8b9 100644 --- a/homeassistant/components/snooz/config_flow.py +++ b/homeassistant/components/snooz/config_flow.py @@ -132,7 +132,7 @@ class SnoozConfigFlow(ConfigFlow, domain=DOMAIN): """Wait for device to enter pairing mode.""" if not self._pairing_task: self._pairing_task = self.hass.async_create_task( - self._async_wait_for_pairing_mode(), eager_start=False + self._async_wait_for_pairing_mode() ) if not self._pairing_task.done(): diff --git a/tests/components/snooz/test_config_flow.py b/tests/components/snooz/test_config_flow.py index 209bd50512a..4ed4d6184a7 100644 --- a/tests/components/snooz/test_config_flow.py +++ b/tests/components/snooz/test_config_flow.py @@ -2,7 +2,7 @@ from __future__ import annotations -from asyncio import Event +from asyncio import Event, sleep from unittest.mock import patch from homeassistant import config_entries @@ -298,9 +298,16 @@ async def _test_pairs( async def _test_pairs_timeout( hass: HomeAssistant, flow_id: str, user_input: dict | None = None ) -> str: + async def _async_process_advertisements( + _hass, _callback, _matcher, _mode, _timeout + ): + """Simulate a timeout waiting for pairing mode.""" + await sleep(0) + raise TimeoutError + with patch( "homeassistant.components.snooz.config_flow.async_process_advertisements", - side_effect=TimeoutError(), + _async_process_advertisements, ): result = await hass.config_entries.flow.async_configure( flow_id, user_input=user_input or {} From 98ed6e7fe573189e628cf0c35719185541eba60e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 17 Apr 2024 17:47:40 -0500 Subject: [PATCH 651/967] Bump habluetooth to 2.7.0 (#115783) --- .../components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/bluetooth/test_diagnostics.py | 32 +++++++++++++++++++ 5 files changed, 36 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 471e327ee9d..d8dca1da607 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -20,6 +20,6 @@ "bluetooth-auto-recovery==1.4.0", "bluetooth-data-tools==1.19.0", "dbus-fast==2.21.1", - "habluetooth==2.6.0" + "habluetooth==2.7.0" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index bd16f3c6147..6fe0d55ae6d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -28,7 +28,7 @@ dbus-fast==2.21.1 fnv-hash-fast==0.5.0 ha-av==10.1.1 ha-ffmpeg==3.2.0 -habluetooth==2.6.0 +habluetooth==2.7.0 hass-nabucasa==0.78.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 diff --git a/requirements_all.txt b/requirements_all.txt index 66c36c7ac18..7801db05dff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1029,7 +1029,7 @@ ha-philipsjs==3.1.1 habitipy==0.2.0 # homeassistant.components.bluetooth -habluetooth==2.6.0 +habluetooth==2.7.0 # homeassistant.components.cloud hass-nabucasa==0.78.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5bac0527854..5e52c069edb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -843,7 +843,7 @@ ha-philipsjs==3.1.1 habitipy==0.2.0 # homeassistant.components.bluetooth -habluetooth==2.6.0 +habluetooth==2.7.0 # homeassistant.components.cloud hass-nabucasa==0.78.0 diff --git a/tests/components/bluetooth/test_diagnostics.py b/tests/components/bluetooth/test_diagnostics.py index 3d29080d56c..c67bd583b1e 100644 --- a/tests/components/bluetooth/test_diagnostics.py +++ b/tests/components/bluetooth/test_diagnostics.py @@ -176,6 +176,14 @@ async def test_diagnostics( "source": "00:00:00:00:00:01", "start_time": ANY, "type": "HaScanner", + "current_mode": { + "__type": "", + "repr": "", + }, + "requested_mode": { + "__type": "", + "repr": "", + }, }, { "adapter": "hci1", @@ -203,6 +211,14 @@ async def test_diagnostics( "source": "00:00:00:00:00:02", "start_time": ANY, "type": "FakeHaScanner", + "current_mode": { + "__type": "", + "repr": "", + }, + "requested_mode": { + "__type": "", + "repr": "", + }, }, ], "slot_manager": { @@ -376,6 +392,14 @@ async def test_diagnostics_macos( "source": "Core Bluetooth", "start_time": ANY, "type": "FakeHaScanner", + "current_mode": { + "__type": "", + "repr": "", + }, + "requested_mode": { + "__type": "", + "repr": "", + }, } ], "slot_manager": { @@ -543,6 +567,14 @@ async def test_diagnostics_remote_adapter( "source": "00:00:00:00:00:01", "start_time": ANY, "type": "HaScanner", + "current_mode": { + "__type": "", + "repr": "", + }, + "requested_mode": { + "__type": "", + "repr": "", + }, }, { "connectable": True, From 8fb551430d68979fca872498cd952f7c28fb417f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 17 Apr 2024 20:38:17 -0500 Subject: [PATCH 652/967] Bump bluetooth-auto-recovery to 1.4.1 (#115792) --- 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 d8dca1da607..f7d27e84a17 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -17,7 +17,7 @@ "bleak==0.21.1", "bleak-retry-connector==3.5.0", "bluetooth-adapters==0.18.0", - "bluetooth-auto-recovery==1.4.0", + "bluetooth-auto-recovery==1.4.1", "bluetooth-data-tools==1.19.0", "dbus-fast==2.21.1", "habluetooth==2.7.0" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6fe0d55ae6d..7782fba1713 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -18,7 +18,7 @@ bcrypt==4.1.2 bleak-retry-connector==3.5.0 bleak==0.21.1 bluetooth-adapters==0.18.0 -bluetooth-auto-recovery==1.4.0 +bluetooth-auto-recovery==1.4.1 bluetooth-data-tools==1.19.0 cached_ipaddress==0.3.0 certifi>=2021.5.30 diff --git a/requirements_all.txt b/requirements_all.txt index 7801db05dff..2ac66b2b34c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -579,7 +579,7 @@ bluemaestro-ble==0.2.3 bluetooth-adapters==0.18.0 # homeassistant.components.bluetooth -bluetooth-auto-recovery==1.4.0 +bluetooth-auto-recovery==1.4.1 # homeassistant.components.bluetooth # homeassistant.components.ld2410_ble diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5e52c069edb..b7a4a59a18e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -494,7 +494,7 @@ bluemaestro-ble==0.2.3 bluetooth-adapters==0.18.0 # homeassistant.components.bluetooth -bluetooth-auto-recovery==1.4.0 +bluetooth-auto-recovery==1.4.1 # homeassistant.components.bluetooth # homeassistant.components.ld2410_ble From 14515b77bb79b245c177ea66a76dfa7f2020995e Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 17 Apr 2024 20:47:15 -0500 Subject: [PATCH 653/967] Add valve entity support for ESPHome (#115341) Co-authored-by: J. Nick Koston Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .../components/esphome/entry_data.py | 2 + homeassistant/components/esphome/valve.py | 103 +++++++++ tests/components/esphome/test_valve.py | 196 ++++++++++++++++++ 3 files changed, 301 insertions(+) create mode 100644 homeassistant/components/esphome/valve.py create mode 100644 tests/components/esphome/test_valve.py diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 005963db872..52dc1f17ad6 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -36,6 +36,7 @@ from aioesphomeapi import ( TextSensorInfo, TimeInfo, UserService, + ValveInfo, build_unique_id, ) from aioesphomeapi.model import ButtonInfo @@ -78,6 +79,7 @@ INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], Platform] = { TextInfo: Platform.TEXT, TextSensorInfo: Platform.SENSOR, TimeInfo: Platform.TIME, + ValveInfo: Platform.VALVE, } diff --git a/homeassistant/components/esphome/valve.py b/homeassistant/components/esphome/valve.py new file mode 100644 index 00000000000..5798d38803f --- /dev/null +++ b/homeassistant/components/esphome/valve.py @@ -0,0 +1,103 @@ +"""Support for ESPHome valves.""" + +from __future__ import annotations + +from typing import Any + +from aioesphomeapi import EntityInfo, ValveInfo, ValveOperation, ValveState + +from homeassistant.components.valve import ( + ValveDeviceClass, + 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 + +from .entity import ( + EsphomeEntity, + convert_api_error_ha_error, + esphome_state_property, + platform_async_setup_entry, +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, 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): + """A valve implementation for ESPHome.""" + + @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 + flags = ValveEntityFeature.OPEN | ValveEntityFeature.CLOSE + if static_info.supports_stop: + flags |= ValveEntityFeature.STOP + if static_info.supports_position: + flags |= ValveEntityFeature.SET_POSITION + self._attr_supported_features = flags + self._attr_device_class = try_parse_enum( + ValveDeviceClass, static_info.device_class + ) + self._attr_assumed_state = static_info.assumed_state + self._attr_reports_position = static_info.supports_position + + @property + @esphome_state_property + def is_closed(self) -> bool: + """Return if the valve is closed or not.""" + return self._state.position == 0.0 + + @property + @esphome_state_property + def is_opening(self) -> bool: + """Return if the valve is opening or not.""" + return self._state.current_operation is ValveOperation.IS_OPENING + + @property + @esphome_state_property + def is_closing(self) -> bool: + """Return if the valve is closing or not.""" + return self._state.current_operation is ValveOperation.IS_CLOSING + + @property + @esphome_state_property + def current_valve_position(self) -> int | None: + """Return current position of valve. 0 is closed, 100 is open.""" + return round(self._state.position * 100.0) + + @convert_api_error_ha_error + async def async_open_valve(self, **kwargs: Any) -> None: + """Open the valve.""" + self._client.valve_command(key=self._key, position=1.0) + + @convert_api_error_ha_error + async def async_close_valve(self, **kwargs: Any) -> None: + """Close valve.""" + self._client.valve_command(key=self._key, position=0.0) + + @convert_api_error_ha_error + async def async_stop_valve(self, **kwargs: Any) -> None: + """Stop the valve.""" + self._client.valve_command(key=self._key, stop=True) + + @convert_api_error_ha_error + 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) diff --git a/tests/components/esphome/test_valve.py b/tests/components/esphome/test_valve.py new file mode 100644 index 00000000000..5ba7bcbe187 --- /dev/null +++ b/tests/components/esphome/test_valve.py @@ -0,0 +1,196 @@ +"""Test ESPHome valves.""" + +from collections.abc import Awaitable, Callable +from unittest.mock import call + +from aioesphomeapi import ( + APIClient, + EntityInfo, + EntityState, + UserService, + ValveInfo, + ValveOperation, + ValveState, +) + +from homeassistant.components.valve import ( + ATTR_CURRENT_POSITION, + ATTR_POSITION, + DOMAIN as VALVE_DOMAIN, + SERVICE_CLOSE_VALVE, + SERVICE_OPEN_VALVE, + SERVICE_SET_VALVE_POSITION, + SERVICE_STOP_VALVE, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, +) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant + +from .conftest import MockESPHomeDevice + + +async def test_valve_entity( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], +) -> None: + """Test a generic valve entity.""" + entity_info = [ + ValveInfo( + object_id="myvalve", + key=1, + name="my valve", + unique_id="my_valve", + supports_position=True, + supports_stop=True, + ) + ] + states = [ + ValveState( + key=1, + position=0.5, + current_operation=ValveOperation.IS_OPENING, + ) + ] + user_service = [] + mock_device = await mock_esphome_device( + mock_client=mock_client, + entity_info=entity_info, + user_service=user_service, + states=states, + ) + state = hass.states.get("valve.test_myvalve") + assert state is not None + assert state.state == STATE_OPENING + assert state.attributes[ATTR_CURRENT_POSITION] == 50 + + await hass.services.async_call( + VALVE_DOMAIN, + SERVICE_CLOSE_VALVE, + {ATTR_ENTITY_ID: "valve.test_myvalve"}, + blocking=True, + ) + mock_client.valve_command.assert_has_calls([call(key=1, position=0.0)]) + mock_client.valve_command.reset_mock() + + await hass.services.async_call( + VALVE_DOMAIN, + SERVICE_OPEN_VALVE, + {ATTR_ENTITY_ID: "valve.test_myvalve"}, + blocking=True, + ) + mock_client.valve_command.assert_has_calls([call(key=1, position=1.0)]) + mock_client.valve_command.reset_mock() + + await hass.services.async_call( + VALVE_DOMAIN, + SERVICE_SET_VALVE_POSITION, + {ATTR_ENTITY_ID: "valve.test_myvalve", ATTR_POSITION: 50}, + blocking=True, + ) + mock_client.valve_command.assert_has_calls([call(key=1, position=0.5)]) + mock_client.valve_command.reset_mock() + + await hass.services.async_call( + VALVE_DOMAIN, + SERVICE_STOP_VALVE, + {ATTR_ENTITY_ID: "valve.test_myvalve"}, + blocking=True, + ) + mock_client.valve_command.assert_has_calls([call(key=1, stop=True)]) + mock_client.valve_command.reset_mock() + + mock_device.set_state( + ValveState(key=1, position=0.0, current_operation=ValveOperation.IDLE) + ) + await hass.async_block_till_done() + state = hass.states.get("valve.test_myvalve") + assert state is not None + assert state.state == STATE_CLOSED + + mock_device.set_state( + ValveState(key=1, position=0.5, current_operation=ValveOperation.IS_CLOSING) + ) + await hass.async_block_till_done() + state = hass.states.get("valve.test_myvalve") + assert state is not None + assert state.state == STATE_CLOSING + + mock_device.set_state( + ValveState(key=1, position=1.0, current_operation=ValveOperation.IDLE) + ) + await hass.async_block_till_done() + state = hass.states.get("valve.test_myvalve") + assert state is not None + assert state.state == STATE_OPEN + + +async def test_valve_entity_without_position( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], +) -> None: + """Test a generic valve entity without position or stop.""" + entity_info = [ + ValveInfo( + object_id="myvalve", + key=1, + name="my valve", + unique_id="my_valve", + supports_position=False, + supports_stop=False, + ) + ] + states = [ + ValveState( + key=1, + position=0.5, + current_operation=ValveOperation.IS_OPENING, + ) + ] + user_service = [] + mock_device = await mock_esphome_device( + mock_client=mock_client, + entity_info=entity_info, + user_service=user_service, + states=states, + ) + state = hass.states.get("valve.test_myvalve") + assert state is not None + assert state.state == STATE_OPENING + assert ATTR_CURRENT_POSITION not in state.attributes + + await hass.services.async_call( + VALVE_DOMAIN, + SERVICE_CLOSE_VALVE, + {ATTR_ENTITY_ID: "valve.test_myvalve"}, + blocking=True, + ) + mock_client.valve_command.assert_has_calls([call(key=1, position=0.0)]) + mock_client.valve_command.reset_mock() + + await hass.services.async_call( + VALVE_DOMAIN, + SERVICE_OPEN_VALVE, + {ATTR_ENTITY_ID: "valve.test_myvalve"}, + blocking=True, + ) + mock_client.valve_command.assert_has_calls([call(key=1, position=1.0)]) + mock_client.valve_command.reset_mock() + + mock_device.set_state( + ValveState(key=1, position=0.0, current_operation=ValveOperation.IDLE) + ) + await hass.async_block_till_done() + state = hass.states.get("valve.test_myvalve") + assert state is not None + assert state.state == STATE_CLOSED From 0398a481c38cc1b19b1bff0148e8f9bef39dd493 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 18 Apr 2024 02:25:03 -0500 Subject: [PATCH 654/967] Fix failing sanix tests (#115793) Fixing failing sanix tests Regenerate snapshot to match what actually happens. There is no translation keys for these two --- tests/components/sanix/snapshots/test_sensor.ambr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/sanix/snapshots/test_sensor.ambr b/tests/components/sanix/snapshots/test_sensor.ambr index 792d2a3be64..84c97ce68b1 100644 --- a/tests/components/sanix/snapshots/test_sensor.ambr +++ b/tests/components/sanix/snapshots/test_sensor.ambr @@ -29,7 +29,7 @@ 'platform': 'sanix', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'battery', + 'translation_key': None, 'unique_id': '1810088-battery', 'unit_of_measurement': '%', }) @@ -126,7 +126,7 @@ 'platform': 'sanix', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'distance', + 'translation_key': None, 'unique_id': '1810088-distance', 'unit_of_measurement': , }) From 4374ec767d5b5f3581901db812bba525cd35f28f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Apr 2024 10:14:51 +0200 Subject: [PATCH 655/967] Bump github/codeql-action from 3.25.0 to 3.25.1 (#115796) 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 9dba09557e3..2b9a2af127f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,11 +24,11 @@ jobs: uses: actions/checkout@v4.1.2 - name: Initialize CodeQL - uses: github/codeql-action/init@v3.25.0 + uses: github/codeql-action/init@v3.25.1 with: languages: python - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3.25.0 + uses: github/codeql-action/analyze@v3.25.1 with: category: "/language:python" From a47c76fc40d2207e54767a03575acfbf0c7ad67c Mon Sep 17 00:00:00 2001 From: Krzysztof Kwitt <120908425+krzysztof-kwitt@users.noreply.github.com> Date: Thu, 18 Apr 2024 13:18:14 +0200 Subject: [PATCH 656/967] Bump connect-box to 0.3.1 (#107852) --- homeassistant/components/upc_connect/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/upc_connect/manifest.json b/homeassistant/components/upc_connect/manifest.json index 79ed768282a..02b852ec3a6 100644 --- a/homeassistant/components/upc_connect/manifest.json +++ b/homeassistant/components/upc_connect/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/upc_connect", "iot_class": "local_polling", "loggers": ["connect_box"], - "requirements": ["connect-box==0.2.8"] + "requirements": ["connect-box==0.3.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2ac66b2b34c..edb4c8919d9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ colorthief==0.2.1 concord232==0.15 # homeassistant.components.upc_connect -connect-box==0.2.8 +connect-box==0.3.1 # homeassistant.components.xiaomi_miio construct==2.10.68 From aee620be9f54fe70e8327bbe6f48e70dbd775e96 Mon Sep 17 00:00:00 2001 From: William Easton Date: Thu, 18 Apr 2024 06:38:52 -0500 Subject: [PATCH 657/967] Ambient Weather: Check for key existence before checking value (#115776) --- homeassistant/components/ambient_station/entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ambient_station/entity.py b/homeassistant/components/ambient_station/entity.py index a1a81d97c3f..24dfab438d8 100644 --- a/homeassistant/components/ambient_station/entity.py +++ b/homeassistant/components/ambient_station/entity.py @@ -49,7 +49,7 @@ class AmbientWeatherEntity(Entity): last_data = self._ambient.stations[self._mac_address][ATTR_LAST_DATA] key = self.entity_description.key available_key = TYPE_SOLARRADIATION if key == TYPE_SOLARRADIATION_LX else key - self._attr_available = last_data[available_key] is not None + self._attr_available = last_data.get(available_key) is not None self.update_from_latest_data() self.async_write_ha_state() From 47f0d5ed1f39b7dfb742496d362a20b2a4ce1e95 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 18 Apr 2024 13:41:34 +0200 Subject: [PATCH 658/967] Add script to compare alexa locales with upstream (#114247) * Add script to compare alexa locales with upstream * Use a function in script * Add test base * Complete output assertion * Add type annotation * Add note to docstring * Update script call example Co-authored-by: Jan Bouwhuis --------- Co-authored-by: Jan Bouwhuis --- .../components/alexa/capabilities.py | 4 + script/alexa_locales.py | 67 ++ .../non_packaged_scripts/alexa_locales.txt | 650 ++++++++++++++++++ tests/non_packaged_scripts/__init__.py | 1 + .../snapshots/test_alexa_locales.ambr | 62 ++ .../test_alexa_locales.py | 29 + 6 files changed, 813 insertions(+) create mode 100644 script/alexa_locales.py create mode 100644 tests/fixtures/non_packaged_scripts/alexa_locales.txt create mode 100644 tests/non_packaged_scripts/__init__.py create mode 100644 tests/non_packaged_scripts/snapshots/test_alexa_locales.ambr create mode 100644 tests/non_packaged_scripts/test_alexa_locales.py diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index bc9b482109f..df32220895d 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -300,6 +300,10 @@ class Alexa(AlexaCapability): The API suggests you should explicitly include this interface. https://developer.amazon.com/docs/device-apis/alexa-interface.html + + To compare current supported locales in Home Assistant + with Alexa supported locales, run the following script: + python -m script.alexa_locales """ supported_locales = { diff --git a/script/alexa_locales.py b/script/alexa_locales.py new file mode 100644 index 00000000000..84bdac4133a --- /dev/null +++ b/script/alexa_locales.py @@ -0,0 +1,67 @@ +"""Check if upstream Alexa locales are subset of the core Alexa supported locales.""" + +from pprint import pprint +import re + +from bs4 import BeautifulSoup +import requests + +from homeassistant.components.alexa import capabilities + +SITE = ( + "https://developer.amazon.com/en-GB/docs/alexa/device-apis/list-of-interfaces.html" +) + + +def run_script() -> None: + """Run the script.""" + response = requests.get(SITE, timeout=10) + soup = BeautifulSoup(response.text, "html.parser") + + table = soup.find("table") + table_body = table.find_all("tbody")[-1] + rows = table_body.find_all("tr") + data = [[ele.text.strip() for ele in row.find_all("td") if ele] for row in rows] + upstream_locales_raw = {row[0]: row[3] for row in data} + language_pattern = re.compile(r"^[a-z]{2}-[A-Z]{2}$") + upstream_locales = { + upstream_interface: { + name + for word in upstream_locale.split(" ") + if (name := word.strip(",")) and language_pattern.match(name) is not None + } + for upstream_interface, upstream_locale in upstream_locales_raw.items() + if upstream_interface.count(".") == 1 # Skip sub-interfaces + } + + interfaces_missing = {} + interfaces_nok = {} + interfaces_ok = {} + + for upstream_interface, upstream_locale in upstream_locales.items(): + core_interface_name = upstream_interface.replace(".", "") + core_interface = getattr(capabilities, core_interface_name, None) + + if core_interface is None: + interfaces_missing[upstream_interface] = upstream_locale + continue + + core_locale = core_interface.supported_locales + + if not upstream_locale.issubset(core_locale): + interfaces_nok[core_interface_name] = core_locale + else: + interfaces_ok[core_interface_name] = core_locale + + print("Missing interfaces:") + pprint(list(interfaces_missing)) + print("\n") + print("Interfaces where upstream locales are not subsets of the core locales:") + pprint(list(interfaces_nok)) + print("\n") + print("Interfaces checked ok:") + pprint(list(interfaces_ok)) + + +if __name__ == "__main__": + run_script() diff --git a/tests/fixtures/non_packaged_scripts/alexa_locales.txt b/tests/fixtures/non_packaged_scripts/alexa_locales.txt new file mode 100644 index 00000000000..beb9c8dbc7e --- /dev/null +++ b/tests/fixtures/non_packaged_scripts/alexa_locales.txt @@ -0,0 +1,650 @@ +

List of Alexa Interfaces and Supported Languages

+ + +
+ + + + + +

Implement the Alexa interfaces to build automotive skills, music, radio, and podcast skills, smart home skills, and video skills. Alexa interfaces use the pre-built voice interaction model.

+ +

You can use these interfaces with Alexa Voice Service (AVS) Built-in and Alexa Connect Kit (ACK) enabled devices, also. For more details, see Smart Home Development Options.

+ +

Alexa interfaces

+ +

The following table shows the interfaces that you can implement in your Alexa skills. Follow the link to each interface for full details, including the supported capabilities and example customer utterances.

+ + + + +
Interface + Version + Primary skill type + Supported languages + +
+

Alexa.ApplicationStateReporter

+
+

1.0

+
+

AVS

+
+

ar-SA, de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.Audio.PlayQueue

+
+

1.0

+
+

Music, Radio, Podcast

+
+

en-US, es-US

+ +
+

Alexa.AuthorizationController

+
+

1.0

+
+

Automotive

+
+

en-CA, en-US, es-MX, es-US, fr-CA

+ +
+

Alexa.AutomationManagement

+
+

1.0

+
+

Smart Home Energy

+
+

en-US

+ +
+

Alexa.Automotive.VehicleData

+
+

1.0

+
+

Automotive

+
+

en-CA, en-US, es-MX, es-US, fr-CA

+ +
+

Alexa.BrightnessController

+
+

3

+
+

Smart Home

+
+

ar-SA, de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX,es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.Camera.LiveViewController

+
+

1.7

+
+

AVS

+
+

en-US

+ +
+

Alexa.CameraStreamController

+
+

3

+
+

Smart Home Security

+
+

ar-SA, de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.ChannelController

+
+

3

+
+

Smart Home Entertainment,
+Video

+
+

ar-SA, de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.ColorController

+
+

3

+
+

Smart Home

+
+

de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.ColorTemperatureController

+
+

3

+
+

Smart Home

+
+

de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.Commissionable

+
+

1.0

+
+

Smart Home

+
+

ar-SA, de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.ConsentManagement.ConsentRequiredReporter

+
+

1.0

+
+

Smart Home

+
+

ja-JP

+ +
+

Alexa.ContactSensor

+
+

3

+
+

Smart Home Security

+
+

de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.Cooking

+
+

3

+
+

Smart Home Cooking

+
+

en-US

+ +
+

Alexa.Cooking.FoodTemperatureController

+
+

3

+
+

Smart Home Cooking

+
+

en-US

+ +
+

Alexa.Cooking.FoodTemperatureSensor

+
+

3

+
+

Smart Home Cooking

+
+

en-US

+ +
+

Alexa.Cooking.PresetController

+
+

3

+
+

Smart Home Cooking

+
+

en-US

+ +
+

Alexa.Cooking.TemperatureController

+
+

3

+
+

Smart Home Cooking

+
+

en-US

+ +
+

Alexa.Cooking.TemperatureSensor

+
+

3

+
+

Smart Home Cooking

+
+

en-US

+ +
+

Alexa.Cooking.TimeController

+
+

3

+
+

Smart Home Cooking

+
+

en-US

+ +
+

Alexa.DataController

+
+

1.0

+
+

Smart Home

+
+

en-US

+ +
+

Alexa.DeviceUsage.Estimation

+
+

1.0

+
+

Smart Home Energy

+
+

en-US

+ +
+

Alexa.DeviceUsage.Meter

+
+

1.0

+
+

Smart Home Energy

+
+

en-US

+ +
+

Alexa.DoorbellEventSource

+
+

3

+
+

Smart Home Security

+
+

ar-SA, de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.EndpointHealth

+
+

3.1

+
+

Smart Home

+
+

ar-SA, de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.EqualizerController

+
+

3

+
+

Smart Home Entertainment

+
+

de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.InputController

+
+

3

+
+

Smart Home Entertainment,
+Video, AVS

+
+

ar-SA, de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.InventoryLevelSensor

+
+

3

+
+

Smart Home

+
+

de-DE, en-CA, en-GB, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, it-IT, ja-JP

+ +
+

Alexa.InventoryLevelUsageSensor

+
+

3

+
+

Smart Home

+
+

de-DE, en-CA, en-GB, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, it-IT, ja-JP

+ +
+

Alexa.InventoryUsageSensor

+
+

3

+
+

Smart Home

+
+

de-DE, en-CA, en-GB, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, it-IT, ja-JP

+ +
+

Alexa.KeypadController

+
+

3

+
+

Video

+
+

ar-SA, de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.Launcher

+
+

3

+
+

Video

+
+

ar-SA, de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.LockController

+
+

3

+
+

Smart Home Security

+
+

ar-SA, de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.Media.Playback

+
+

1.0

+
+

Music, Radio, Podcast

+
+

en-US, es-US

+ +
+

Alexa.Media.PlayQueue

+
+

1.0

+
+

Music, Radio, Podcast

+
+

en-US, es-US

+ +
+

Alexa.Media.Search

+
+

1.0

+
+

Music, Radio, Podcast

+
+

en-US, es-US

+ +
+

Alexa.ModeController

+
+

3

+
+

Smart Home, AVS

+
+

de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.MotionSensor

+
+

3

+
+

Smart Home Security

+
+

de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.PercentageController

+
+

3

+
+

Smart Home

+
+

de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.PlaybackController

+
+

3

+
+

Smart Home Entertainment,
+Video

+
+

ar-SA, de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.PlaybackStateReporter

+
+

3

+
+

Smart Home Entertainment,
+Video

+
+

ar-SA, de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.PowerController

+
+

3

+
+

Smart Home,
+Video, AVS

+
+

ar-SA, de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.PowerLevelController

+
+

3

+
+

Smart Home

+
+

de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, fr-CA, fr-FR, it-IT, ja-JP

+ +
+

Alexa.ProactiveNotificationSource

+
+

3.0

+
+

Smart Home

+
+

Notifications for device state: de-DE, en-CA, en-GB, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, it-IT
+Notifications for cooking: en-US

+ +
+

Alexa.RangeController

+
+

3

+
+

Smart Home, AVS

+
+

de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.RecordController

+
+

3

+
+

Video

+
+

ar-SA, de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.RemoteVideoPlayer

+
+

3.1

+
+

Video

+
+

ar-SA, de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.RTCSessionController

+
+

3

+
+

Smart Home Security

+
+

ar-SA, de-DE, en-AU, en-CA, en-GB, en-US, es-ES, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.SceneController

+
+

3

+
+

Smart Home

+
+

de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.SecurityPanelController

+
+

3

+
+

Smart Home Security

+
+

de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, it-IT, ja-JP, pt-BR

+ +
+

Alexa.SecurityPanelController.Alert

+
+

1.1

+
+

Smart Home Security

+
+

de-DE, en-CA, en-GB, en-US, es-US, fr-CA, fr-FR

+ +
+

Alexa.SeekController

+
+

3

+
+

Video

+
+

ar-SA, de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.SimpleEventSource

+
+

1.0

+
+

Smart Home

+
+

en-US

+ +
+

Alexa.SmartVision.ObjectDetectionSensor

+
+

1.0

+
+

Smart Home Security

+
+

en-US

+ +
+

Alexa.Speaker

+
+

3

+
+

Smart Home Entertainment,
+Video

+
+

de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, it-IT, ja-JP

+ +
+

Alexa.StepSpeaker

+
+

3

+
+

Smart Home Entertainment,
+Video

+
+

de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, it-IT

+ +
+

Alexa.TemperatureSensor

+
+

3

+
+

Smart Home

+
+

ar-SA, de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.ThermostatController

+
+

3.1

+
+

Smart Home

+
+

ar-SA, de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.ThermostatController.Configuration

+
+

3

+
+

Smart Home

+
+

en-US

+ +
+

Alexa.ThermostatController.HVAC.Components

+
+

1.0

+
+

Smart Home Energy

+
+

en-US

+ +
+

Alexa.ThermostatController.Schedule

+
+

3.2

+
+

Smart Home

+
+

en-US

+ +
+

Alexa.TimeHoldController

+
+

3

+
+

Smart Home Cooking

+
+

en-US

+ +
+

Alexa.ToggleController

+
+

3

+
+

Smart Home, AVS

+
+

de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.UIController

+
+

3.0

+
+

Video

+
+

en-US

+ +
+

Alexa.UserPreference

+
+

2.0

+
+

Music, Radio, Podcast

+
+

en-US, es-US

+ +
+

Alexa.VideoRecorder

+
+

3

+
+

Video

+
+

ar-SA, de-DE, en-AU, en-CA, en-GB, en-IN, en-US, es-ES, es-MX, es-US, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, pt-BR

+ +
+

Alexa.WakeOnLANController

+
+

3

+
+

Smart Home Entertainment

+
+

de-DE, en-AU, en-IN, en-US, es-ES, it-IT, ja-JP

+ +
+ + + + diff --git a/tests/non_packaged_scripts/__init__.py b/tests/non_packaged_scripts/__init__.py new file mode 100644 index 00000000000..852c52a8293 --- /dev/null +++ b/tests/non_packaged_scripts/__init__.py @@ -0,0 +1 @@ +"""Tests for the non-packaged scripts in the script directory.""" diff --git a/tests/non_packaged_scripts/snapshots/test_alexa_locales.ambr b/tests/non_packaged_scripts/snapshots/test_alexa_locales.ambr new file mode 100644 index 00000000000..bad47eedf53 --- /dev/null +++ b/tests/non_packaged_scripts/snapshots/test_alexa_locales.ambr @@ -0,0 +1,62 @@ +# serializer version: 1 +# name: test_alexa_locales + ''' + Missing interfaces: + ['Alexa.ApplicationStateReporter', + 'Alexa.AuthorizationController', + 'Alexa.AutomationManagement', + 'Alexa.Commissionable', + 'Alexa.Cooking', + 'Alexa.DataController', + 'Alexa.InventoryLevelSensor', + 'Alexa.InventoryLevelUsageSensor', + 'Alexa.InventoryUsageSensor', + 'Alexa.KeypadController', + 'Alexa.Launcher', + 'Alexa.PercentageController', + 'Alexa.ProactiveNotificationSource', + 'Alexa.RecordController', + 'Alexa.RemoteVideoPlayer', + 'Alexa.RTCSessionController', + 'Alexa.SimpleEventSource', + 'Alexa.UIController', + 'Alexa.UserPreference', + 'Alexa.VideoRecorder', + 'Alexa.WakeOnLANController'] + + + Interfaces where upstream locales are not subsets of the core locales: + [] + + + Interfaces checked ok: + ['AlexaBrightnessController', + 'AlexaCameraStreamController', + 'AlexaChannelController', + 'AlexaColorController', + 'AlexaColorTemperatureController', + 'AlexaContactSensor', + 'AlexaDoorbellEventSource', + 'AlexaEndpointHealth', + 'AlexaEqualizerController', + 'AlexaInputController', + 'AlexaLockController', + 'AlexaModeController', + 'AlexaMotionSensor', + 'AlexaPlaybackController', + 'AlexaPlaybackStateReporter', + 'AlexaPowerController', + 'AlexaPowerLevelController', + 'AlexaRangeController', + 'AlexaSceneController', + 'AlexaSecurityPanelController', + 'AlexaSeekController', + 'AlexaSpeaker', + 'AlexaStepSpeaker', + 'AlexaTemperatureSensor', + 'AlexaThermostatController', + 'AlexaTimeHoldController', + 'AlexaToggleController'] + + ''' +# --- diff --git a/tests/non_packaged_scripts/test_alexa_locales.py b/tests/non_packaged_scripts/test_alexa_locales.py new file mode 100644 index 00000000000..ea139f7de8e --- /dev/null +++ b/tests/non_packaged_scripts/test_alexa_locales.py @@ -0,0 +1,29 @@ +"""Test the alexa_locales script.""" + +from pathlib import Path + +import pytest +import requests_mock +from syrupy import SnapshotAssertion + +from script.alexa_locales import SITE, run_script + + +def test_alexa_locales( + capsys: pytest.CaptureFixture[str], + requests_mock: requests_mock.Mocker, + snapshot: SnapshotAssertion, +) -> None: + """Test alexa_locales script.""" + fixture_file = ( + Path(__file__).parent.parent / "fixtures/non_packaged_scripts/alexa_locales.txt" + ) + requests_mock.get( + SITE, + text=fixture_file.read_text(encoding="utf-8"), + ) + + run_script() + + captured = capsys.readouterr() + assert captured.out == snapshot From 28da10ad0d03a633c4b0aec60f31cda6b8382340 Mon Sep 17 00:00:00 2001 From: mkmer Date: Thu, 18 Apr 2024 07:53:32 -0400 Subject: [PATCH 659/967] Handle connection error in honeywell (#108168) * Handle connection error * Catch connection error * Add tests * Add translation strings * Clean up overlapping exceptions * ServiceValidationError * HomeAssistant Error translations --------- Co-authored-by: Erik Montnemery --- homeassistant/components/honeywell/climate.py | 77 ++++++++++++++----- .../components/honeywell/strings.json | 33 ++++++++ tests/components/honeywell/test_climate.py | 61 +++++++++++---- 3 files changed, 140 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index bd32ee0a23d..ff63d66230d 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -34,7 +34,7 @@ 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.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import device_registry as dr, issue_registry as ir from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -361,15 +361,18 @@ class HoneywellUSThermostat(ClimateEntity): if mode in ["heat", "emheat"]: await self._device.set_setpoint_heat(temperature) - except UnexpectedResponse as err: + except (AscConnectionError, UnexpectedResponse) as err: raise HomeAssistantError( - "Honeywell set temperature failed: Invalid Response" + translation_domain=DOMAIN, + translation_key="temp_failed", ) from err except SomeComfortError as err: _LOGGER.error("Invalid temperature %.1f: %s", temperature, err) - raise ValueError( - f"Honeywell set temperature failed: invalid temperature {temperature}." + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="temp_failed_value", + translation_placeholders={"temp": temperature}, ) from err async def async_set_temperature(self, **kwargs: Any) -> None: @@ -382,30 +385,41 @@ class HoneywellUSThermostat(ClimateEntity): if temperature := kwargs.get(ATTR_TARGET_TEMP_LOW): await self._device.set_setpoint_heat(temperature) - except UnexpectedResponse as err: + except (AscConnectionError, UnexpectedResponse) as err: raise HomeAssistantError( - "Honeywell set temperature failed: Invalid Response" + translation_domain=DOMAIN, + translation_key="temp_failed", ) from err except SomeComfortError as err: _LOGGER.error("Invalid temperature %.1f: %s", temperature, err) - raise ValueError( - f"Honeywell set temperature failed: invalid temperature: {temperature}." + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="temp_failed_value", + translation_placeholders={"temp": str(temperature)}, ) from err async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" try: await self._device.set_fan_mode(self._fan_mode_map[fan_mode]) + except SomeComfortError as err: - raise HomeAssistantError("Honeywell could not set fan mode.") from err + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="fan_mode_failed", + ) from err async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target hvac mode.""" try: await self._device.set_system_mode(self._hvac_mode_map[hvac_mode]) + except SomeComfortError as err: - raise HomeAssistantError("Honeywell could not set system mode.") from err + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="sys_mode_failed", + ) from err async def _turn_away_mode_on(self) -> None: """Turn away on. @@ -425,6 +439,12 @@ class HoneywellUSThermostat(ClimateEntity): if mode in HEATING_MODES: await self._device.set_hold_heat(True, self._heat_away_temp) + except (AscConnectionError, UnexpectedResponse) as err: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="away_mode_failed", + ) from err + except SomeComfortError as err: _LOGGER.error( "Temperature out of range. Mode: %s, Heat Temperature: %.1f, Cool Temperature: %.1f", @@ -432,8 +452,14 @@ class HoneywellUSThermostat(ClimateEntity): self._heat_away_temp, self._cool_away_temp, ) - raise ValueError( - f"Honeywell set temperature failed: temperature out of range. Mode: {mode}, Heat Temperuature: {self._heat_away_temp}, Cool Temperature: {self._cool_away_temp}." + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="temp_failed_range", + translation_placeholders={ + "heat": str(self._heat_away_temp), + "cool": str(self._cool_away_temp), + "mode": mode, + }, ) from err async def _turn_hold_mode_on(self) -> None: @@ -452,11 +478,16 @@ class HoneywellUSThermostat(ClimateEntity): except SomeComfortError as err: _LOGGER.error("Couldn't set permanent hold") raise HomeAssistantError( - "Honeywell couldn't set permanent hold." + translation_domain=DOMAIN, + translation_key="set_hold_failed", ) from err else: _LOGGER.error("Invalid system mode returned: %s", mode) - raise HomeAssistantError(f"Honeywell invalid system mode returned {mode}.") + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="set_mode_failed", + translation_placeholders={"mode": mode}, + ) async def _turn_away_mode_off(self) -> None: """Turn away/hold off.""" @@ -465,9 +496,13 @@ class HoneywellUSThermostat(ClimateEntity): # Disabling all hold modes await self._device.set_hold_cool(False) await self._device.set_hold_heat(False) + except SomeComfortError as err: _LOGGER.error("Can not stop hold mode") - raise HomeAssistantError("Honeywell could not stop hold mode") from err + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="stop_hold_failed", + ) from err async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" @@ -493,9 +528,11 @@ class HoneywellUSThermostat(ClimateEntity): ) try: await self._device.set_system_mode("emheat") + except SomeComfortError as err: raise HomeAssistantError( - "Honeywell could not set system mode to aux heat." + translation_domain=DOMAIN, + translation_key="set_aux_failed", ) from err async def async_turn_aux_heat_off(self) -> None: @@ -517,8 +554,12 @@ class HoneywellUSThermostat(ClimateEntity): await self.async_set_hvac_mode(HVACMode.HEAT) else: await self.async_set_hvac_mode(HVACMode.OFF) + except HomeAssistantError as err: - raise HomeAssistantError("Honeywell could turn off aux heat mode.") from err + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="disable_aux_failed", + ) from err async def async_update(self) -> None: """Get the latest state from the service.""" diff --git a/homeassistant/components/honeywell/strings.json b/homeassistant/components/honeywell/strings.json index 7506a7fda7c..d3bc1924e28 100644 --- a/homeassistant/components/honeywell/strings.json +++ b/homeassistant/components/honeywell/strings.json @@ -61,6 +61,39 @@ } }, "exceptions": { + "temp_failed": { + "message": "Honeywell set temperature failed" + }, + "sys_mode_failed": { + "message": "Honeywell could not set system mode" + }, + "fan_mode_failed": { + "message": "Honeywell could not set fan mode" + }, + "away_mode_failed": { + "message": "Honeywell set away mode failed" + }, + "temp_failed_value": { + "message": "Honeywell set temperature failed: invalid temperature {temperature}" + }, + "temp_failed_range": { + "message": "Honeywell set temperature failed: temperature out of range. Mode: {mode}, Heat Temperuature: {heat}, Cool Temperature: {cool}" + }, + "set_hold_failed": { + "message": "Honeywell could not set permanent hold" + }, + "set_mode_failed": { + "message": "Honeywell invalid system mode returned {mode}" + }, + "stop_hold_failed": { + "message": "Honeywell could not stop hold mode" + }, + "set_aux_failed": { + "message": "Honeywell could not set system mode to aux heat" + }, + "disable_aux_failed": { + "message": "Honeywell could turn off aux heat mode" + }, "switch_failed_off": { "message": "Honeywell could turn off emergency heat mode." }, diff --git a/tests/components/honeywell/test_climate.py b/tests/components/honeywell/test_climate.py index 751ba8aa288..d09444808d8 100644 --- a/tests/components/honeywell/test_climate.py +++ b/tests/components/honeywell/test_climate.py @@ -38,7 +38,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import entity_registry as er from homeassistant.util.dt import utcnow @@ -205,6 +205,16 @@ async def test_mode_service_calls( ) device.set_system_mode.assert_called_once_with("auto") + device.set_system_mode.reset_mock() + device.set_system_mode.side_effect = aiosomecomfort.UnexpectedResponse + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_HVAC_MODE: HVACMode.HEAT_COOL}, + blocking=True, + ) + async def test_auxheat_service_calls( hass: HomeAssistant, device: MagicMock, config_entry: MagicMock @@ -300,6 +310,15 @@ async def test_fan_modes_service_calls( blocking=True, ) + device.set_fan_mode.side_effect = aiosomecomfort.UnexpectedResponse + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_FAN_MODE: FAN_DIFFUSE}, + blocking=True, + ) + async def test_service_calls_off_mode( hass: HomeAssistant, @@ -344,7 +363,7 @@ async def test_service_calls_off_mode( device.set_setpoint_heat.side_effect = aiosomecomfort.SomeComfortError caplog.clear() - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, @@ -431,6 +450,12 @@ async def test_service_calls_off_mode( device.set_hold_heat.assert_called_once_with(False) device.set_hold_cool.assert_called_once_with(False) + device.set_hold_heat.reset_mock() + device.set_hold_cool.reset_mock() + + device.set_setpoint_cool.reset_mock() + device.set_setpoint_heat.reset_mock() + reset_mock(device) device.raw_ui_data["StatusHeat"] = 2 @@ -506,7 +531,7 @@ async def test_service_calls_cool_mode( device.set_setpoint_cool.reset_mock() device.set_setpoint_cool.side_effect = aiosomecomfort.SomeComfortError - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, @@ -538,7 +563,7 @@ async def test_service_calls_cool_mode( device.set_hold_cool.side_effect = aiosomecomfort.SomeComfortError caplog.clear() - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, @@ -570,7 +595,7 @@ async def test_service_calls_cool_mode( device.hold_heat = True device.hold_cool = True - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, @@ -709,7 +734,7 @@ async def test_service_calls_heat_mode( device.set_hold_heat.reset_mock() device.set_hold_heat.side_effect = aiosomecomfort.SomeComfortError - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, @@ -747,7 +772,7 @@ async def test_service_calls_heat_mode( device.set_setpoint_heat.reset_mock() device.set_setpoint_heat.side_effect = aiosomecomfort.SomeComfortError - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, @@ -780,7 +805,7 @@ async def test_service_calls_heat_mode( device.hold_heat = True device.hold_cool = True - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, @@ -811,7 +836,7 @@ async def test_service_calls_heat_mode( reset_mock(device) - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, @@ -828,7 +853,7 @@ async def test_service_calls_heat_mode( device.set_hold_heat.side_effect = aiosomecomfort.SomeComfortError - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, @@ -841,6 +866,16 @@ async def test_service_calls_heat_mode( device.set_setpoint_cool.assert_not_called() assert "Temperature out of range" in caplog.text + device.set_hold_heat.side_effect = aiosomecomfort.UnexpectedResponse + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_AWAY}, + blocking=True, + ) + reset_mock(device) caplog.clear() with pytest.raises(HomeAssistantError): @@ -951,7 +986,7 @@ async def test_service_calls_auto_mode( device.set_hold_cool.side_effect = aiosomecomfort.SomeComfortError device.set_hold_heat.side_effect = aiosomecomfort.SomeComfortError - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, @@ -966,7 +1001,7 @@ async def test_service_calls_auto_mode( device.set_setpoint_heat.side_effect = aiosomecomfort.SomeComfortError device.set_setpoint_cool.side_effect = aiosomecomfort.SomeComfortError - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, @@ -1021,7 +1056,7 @@ async def test_service_calls_auto_mode( device.set_setpoint_heat.side_effect = None device.set_setpoint_cool.side_effect = None - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, From 844ff30a606866eb28093ae6a0da901824f3729f Mon Sep 17 00:00:00 2001 From: Luca Angemi Date: Thu, 18 Apr 2024 14:06:51 +0200 Subject: [PATCH 660/967] Add state class to mobile_app restore entity (#115798) add state class --- homeassistant/components/mobile_app/entity.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/mobile_app/entity.py b/homeassistant/components/mobile_app/entity.py index 1cac62ce964..f1f7b592621 100644 --- a/homeassistant/components/mobile_app/entity.py +++ b/homeassistant/components/mobile_app/entity.py @@ -17,6 +17,7 @@ from .const import ( ATTR_SENSOR_ENTITY_CATEGORY, ATTR_SENSOR_ICON, ATTR_SENSOR_STATE, + ATTR_SENSOR_STATE_CLASS, SIGNAL_SENSOR_UPDATE, ) from .helpers import device_info @@ -44,6 +45,7 @@ class MobileAppEntity(RestoreEntity): """Update the entity from the config.""" config = self._config self._attr_device_class = config.get(ATTR_SENSOR_DEVICE_CLASS) + self._attr_state_class = config.get(ATTR_SENSOR_STATE_CLASS) self._attr_extra_state_attributes = config[ATTR_SENSOR_ATTRIBUTES] self._attr_icon = config[ATTR_SENSOR_ICON] self._attr_entity_category = config.get(ATTR_SENSOR_ENTITY_CATEGORY) From d5e5a1630361e7d4e6faddb6bce111c4620721ad Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Thu, 18 Apr 2024 14:08:23 +0200 Subject: [PATCH 661/967] Add diagnostics platform to DSMR Reader (#115805) * Add diagnostics platform * Feedback --------- Co-authored-by: Joost Lekkerkerker --- .../components/dsmr_reader/diagnostics.py | 27 +++++++++++++ .../snapshots/test_diagnostics.ambr | 23 +++++++++++ .../dsmr_reader/test_diagnostics.py | 39 +++++++++++++++++++ 3 files changed, 89 insertions(+) create mode 100644 homeassistant/components/dsmr_reader/diagnostics.py create mode 100644 tests/components/dsmr_reader/snapshots/test_diagnostics.ambr create mode 100644 tests/components/dsmr_reader/test_diagnostics.py diff --git a/homeassistant/components/dsmr_reader/diagnostics.py b/homeassistant/components/dsmr_reader/diagnostics.py new file mode 100644 index 00000000000..554d90cc5dd --- /dev/null +++ b/homeassistant/components/dsmr_reader/diagnostics.py @@ -0,0 +1,27 @@ +"""Diagnostics support for DSMR Reader.""" + +from __future__ import annotations + +from typing import Any + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + + +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} + + return { + "entry": entry.as_dict(), + "entities": entity_states, + } diff --git a/tests/components/dsmr_reader/snapshots/test_diagnostics.ambr b/tests/components/dsmr_reader/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..c6bc616ffd3 --- /dev/null +++ b/tests/components/dsmr_reader/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': 'dsmr_reader', + 'entry_id': 'TEST_ENTRY_ID', + 'minor_version': 1, + 'options': dict({ + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': 'dsmr_reader', + 'unique_id': 'UNIQUE_TEST_ID', + 'version': 1, + }), + }) +# --- diff --git a/tests/components/dsmr_reader/test_diagnostics.py b/tests/components/dsmr_reader/test_diagnostics.py new file mode 100644 index 00000000000..553efd0b38b --- /dev/null +++ b/tests/components/dsmr_reader/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.dsmr_reader.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.dsmr_reader.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 3299bc5ddc9132efed74606126672f4f4a67a2df Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 18 Apr 2024 14:36:03 +0200 Subject: [PATCH 662/967] Translate service validation errors (#115024) * Move service validation error message to translation cache * Fix test * Revert unrelated change * Address review comments * Improve error message --------- Co-authored-by: J. Nick Koston --- .../components/homeassistant/strings.json | 12 ++++++++ homeassistant/core.py | 30 ++++++++++++++----- homeassistant/exceptions.py | 2 +- .../components/websocket_api/test_commands.py | 2 +- tests/test_core.py | 16 +++++++--- 5 files changed, 49 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/homeassistant/strings.json b/homeassistant/components/homeassistant/strings.json index 37604c0e18e..d46a2e50bfd 100644 --- a/homeassistant/components/homeassistant/strings.json +++ b/homeassistant/components/homeassistant/strings.json @@ -191,6 +191,18 @@ }, "service_not_found": { "message": "Service {domain}.{service} not found." + }, + "service_does_not_supports_reponse": { + "message": "A service which does not return responses can't be called with {return_response}." + }, + "service_lacks_response_request": { + "message": "The service call requires responses and must be called with {return_response}." + }, + "service_reponse_invalid": { + "message": "Failed to process the returned service response data, expected a dictionary, but got {response_data_type}." + }, + "service_should_be_blocking": { + "message": "A non blocking service call with argument {non_blocking_argument} can't be used together with argument {return_response}." } } } diff --git a/homeassistant/core.py b/homeassistant/core.py index 69227f793a1..01536f8ffdb 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -86,6 +86,7 @@ from .exceptions import ( InvalidStateError, MaxLengthExceeded, ServiceNotFound, + ServiceValidationError, Unauthorized, ) from .helpers.deprecation import ( @@ -2571,16 +2572,27 @@ class ServiceRegistry: if return_response: if not blocking: - raise ValueError( - "Invalid argument return_response=True when blocking=False" + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="service_should_be_blocking", + translation_placeholders={ + "return_response": "return_response=True", + "non_blocking_argument": "blocking=False", + }, ) if handler.supports_response is SupportsResponse.NONE: - raise ValueError( - "Invalid argument return_response=True when handler does not support responses" + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="service_does_not_supports_reponse", + translation_placeholders={ + "return_response": "return_response=True" + }, ) elif handler.supports_response is SupportsResponse.ONLY: - raise ValueError( - "Service call requires responses but caller did not ask for responses" + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="service_lacks_response_request", + translation_placeholders={"return_response": "return_response=True"}, ) if target: @@ -2628,7 +2640,11 @@ class ServiceRegistry: return None if not isinstance(response_data, dict): raise HomeAssistantError( - f"Service response data expected a dictionary, was {type(response_data)}" + translation_domain=DOMAIN, + translation_key="service_reponse_invalid", + translation_placeholders={ + "response_data_type": str(type(response_data)) + }, ) return response_data diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index 1eb964d82b1..044a41aab7a 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -255,7 +255,7 @@ class UnknownUser(Unauthorized): """When call is made with user ID that doesn't exist.""" -class ServiceNotFound(HomeAssistantError): +class ServiceNotFound(ServiceValidationError): """Raised when a service is not found.""" def __init__(self, domain: str, service: str) -> None: diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 2bd76accfdd..655d8adf1ea 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -204,7 +204,7 @@ async def test_return_response_error(hass: HomeAssistant, websocket_client) -> N assert msg["id"] == 8 assert msg["type"] == const.TYPE_RESULT assert not msg["success"] - assert msg["error"]["code"] == "unknown_error" + assert msg["error"]["code"] == "service_validation_error" @pytest.mark.parametrize("command", ["call_service", "call_service_action"]) diff --git a/tests/test_core.py b/tests/test_core.py index caed1433082..5d687d89833 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -55,6 +55,7 @@ from homeassistant.exceptions import ( InvalidStateError, MaxLengthExceeded, ServiceNotFound, + ServiceValidationError, ) from homeassistant.helpers.json import json_dumps from homeassistant.setup import async_setup_component @@ -1791,8 +1792,9 @@ async def test_services_call_return_response_requires_blocking( hass: HomeAssistant, ) -> None: """Test that non-blocking service calls cannot ask for response data.""" + await async_setup_component(hass, "homeassistant", {}) async_mock_service(hass, "test_domain", "test_service") - with pytest.raises(ValueError, match="when blocking=False"): + with pytest.raises(ServiceValidationError, match="blocking=False") as exc: await hass.services.async_call( "test_domain", "test_service", @@ -1800,6 +1802,10 @@ async def test_services_call_return_response_requires_blocking( blocking=False, return_response=True, ) + assert ( + str(exc.value) + == "A non blocking service call with argument blocking=False can't be used together with argument return_response=True" + ) @pytest.mark.parametrize( @@ -1816,6 +1822,7 @@ async def test_serviceregistry_return_response_invalid( hass: HomeAssistant, response_data: Any, expected_error: str ) -> None: """Test service call response data must be json serializable objects.""" + await async_setup_component(hass, "homeassistant", {}) def service_handler(call: ServiceCall) -> ServiceResponse: """Service handler coroutine.""" @@ -1842,8 +1849,8 @@ async def test_serviceregistry_return_response_invalid( @pytest.mark.parametrize( ("supports_response", "return_response", "expected_error"), [ - (SupportsResponse.NONE, True, "not support responses"), - (SupportsResponse.ONLY, False, "caller did not ask for responses"), + (SupportsResponse.NONE, True, "does not return responses"), + (SupportsResponse.ONLY, False, "call requires responses"), ], ) async def test_serviceregistry_return_response_arguments( @@ -1853,6 +1860,7 @@ async def test_serviceregistry_return_response_arguments( expected_error: str, ) -> None: """Test service call response data invalid arguments.""" + await async_setup_component(hass, "homeassistant", {}) hass.services.async_register( "test_domain", @@ -1861,7 +1869,7 @@ async def test_serviceregistry_return_response_arguments( supports_response=supports_response, ) - with pytest.raises(ValueError, match=expected_error): + with pytest.raises(ServiceValidationError, match=expected_error): await hass.services.async_call( "test_domain", "test_service", From 8ba1340c2e287fca28861a41a14c6f45ad198a9a Mon Sep 17 00:00:00 2001 From: vexofp Date: Thu, 18 Apr 2024 08:58:16 -0400 Subject: [PATCH 663/967] Clarify cover toggle logic; prevent opening when already open (#107920) Co-authored-by: Erik Montnemery --- homeassistant/components/cover/__init__.py | 21 ++++++++++++++++++--- tests/components/cover/test_init.py | 9 +++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index 71e89797c05..5c7139d6290 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -480,15 +480,30 @@ class CoverEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def _get_toggle_function( self, fns: dict[str, Callable[_P, _R]] ) -> Callable[_P, _R]: + # If we are opening or closing and we support stopping, then we should stop if self.supported_features & CoverEntityFeature.STOP and ( self.is_closing or self.is_opening ): return fns["stop"] - if self.is_closed: + + # If we are fully closed or in the process of closing, then we should open + if self.is_closed or self.is_closing: return fns["open"] - if self._cover_is_last_toggle_direction_open: + + # If we are fully open or in the process of opening, then we should close + if self.current_cover_position == 100 or self.is_opening: return fns["close"] - return fns["open"] + + # We are any of: + # * fully open but do not report `current_cover_position` + # * stopped partially open + # * either opening or closing, but do not report them + # If we previously reported opening/closing, we should move in the opposite direction. + # Otherwise, we must assume we are (partially) open and should always close. + # Note: _cover_is_last_toggle_direction_open will always remain True if we never report opening/closing. + return ( + fns["close"] if self._cover_is_last_toggle_direction_open else fns["open"] + ) # These can be removed if no deprecated constant are in this module anymore diff --git a/tests/components/cover/test_init.py b/tests/components/cover/test_init.py index 0052093298e..5ccd948cc6b 100644 --- a/tests/components/cover/test_init.py +++ b/tests/components/cover/test_init.py @@ -108,6 +108,15 @@ async def test_services( await call_service(hass, SERVICE_TOGGLE, ent6) assert is_opening(hass, ent6) + # After the unusual state transition: closing -> fully open, toggle should close + set_state(ent5, STATE_OPEN) + await call_service(hass, SERVICE_TOGGLE, ent5) # Start closing + assert is_closing(hass, ent5) + set_state(ent5, STATE_OPEN) # Unusual state transition from closing -> fully open + set_cover_position(ent5, 100) + await call_service(hass, SERVICE_TOGGLE, ent5) # Should close, not open + assert is_closing(hass, ent5) + def call_service(hass, service, ent): """Call any service on entity.""" From ceaf8f240249baf9d1d4c842998850581da8c2af Mon Sep 17 00:00:00 2001 From: Lukasz Szmit <2490317+ptashek@users.noreply.github.com> Date: Thu, 18 Apr 2024 14:22:58 +0100 Subject: [PATCH 664/967] Add support for payload_template in rest component (#107464) * Add support for payload_template in rest component * Update homeassistant/components/rest/schema.py * Update homeassistant/components/rest/data.py --------- Co-authored-by: Erik Montnemery --- homeassistant/components/rest/__init__.py | 23 +++++++-- homeassistant/components/rest/const.py | 2 + homeassistant/components/rest/data.py | 4 ++ homeassistant/components/rest/schema.py | 4 +- tests/components/rest/test_init.py | 59 +++++++++++++++++++++++ 5 files changed, 86 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/rest/__init__.py b/homeassistant/components/rest/__init__.py index 1c33b4592df..b7cdee2e039 100644 --- a/homeassistant/components/rest/__init__.py +++ b/homeassistant/components/rest/__init__.py @@ -45,6 +45,7 @@ from homeassistant.util.async_ import create_eager_task from .const import ( CONF_ENCODING, + CONF_PAYLOAD_TEMPLATE, CONF_SSL_CIPHER_LIST, COORDINATOR, DEFAULT_SSL_CIPHER_LIST, @@ -108,8 +109,11 @@ async def _async_process_config(hass: HomeAssistant, config: ConfigType) -> bool for rest_idx, conf in enumerate(rest_config): scan_interval: timedelta = conf.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) resource_template: template.Template | None = conf.get(CONF_RESOURCE_TEMPLATE) + payload_template: template.Template | None = conf.get(CONF_PAYLOAD_TEMPLATE) rest = create_rest_data_from_config(hass, conf) - coordinator = _rest_coordinator(hass, rest, resource_template, scan_interval) + coordinator = _rest_coordinator( + hass, rest, resource_template, payload_template, scan_interval + ) refresh_coroutines.append(coordinator.async_refresh()) hass.data[DOMAIN][REST_DATA].append({REST: rest, COORDINATOR: coordinator}) @@ -156,16 +160,20 @@ def _rest_coordinator( hass: HomeAssistant, rest: RestData, resource_template: template.Template | None, + payload_template: template.Template | None, update_interval: timedelta, ) -> DataUpdateCoordinator[None]: """Wrap a DataUpdateCoordinator around the rest object.""" - if resource_template: + if resource_template or payload_template: - async def _async_refresh_with_resource_template() -> None: - rest.set_url(resource_template.async_render(parse_result=False)) + async def _async_refresh_with_templates() -> None: + if resource_template: + rest.set_url(resource_template.async_render(parse_result=False)) + if payload_template: + rest.set_payload(payload_template.async_render(parse_result=False)) await rest.async_update() - update_method = _async_refresh_with_resource_template + update_method = _async_refresh_with_templates else: update_method = rest.async_update @@ -184,6 +192,7 @@ def create_rest_data_from_config(hass: HomeAssistant, config: ConfigType) -> Res resource_template: template.Template | None = config.get(CONF_RESOURCE_TEMPLATE) method: str = config[CONF_METHOD] payload: str | None = config.get(CONF_PAYLOAD) + payload_template: template.Template | None = config.get(CONF_PAYLOAD_TEMPLATE) verify_ssl: bool = config[CONF_VERIFY_SSL] ssl_cipher_list: str = config.get(CONF_SSL_CIPHER_LIST, DEFAULT_SSL_CIPHER_LIST) username: str | None = config.get(CONF_USERNAME) @@ -196,6 +205,10 @@ def create_rest_data_from_config(hass: HomeAssistant, config: ConfigType) -> Res resource_template.hass = hass resource = resource_template.async_render(parse_result=False) + if payload_template is not None: + payload_template.hass = hass + payload = payload_template.async_render(parse_result=False) + if not resource: raise HomeAssistantError("Resource not set for RestData") diff --git a/homeassistant/components/rest/const.py b/homeassistant/components/rest/const.py index 8fb08f766fa..d10b3f3f74e 100644 --- a/homeassistant/components/rest/const.py +++ b/homeassistant/components/rest/const.py @@ -33,3 +33,5 @@ XML_MIME_TYPES = ( "application/xml", "text/xml", ) + +CONF_PAYLOAD_TEMPLATE = "payload_template" diff --git a/homeassistant/components/rest/data.py b/homeassistant/components/rest/data.py index 06be7a4f6ff..4c9667e7651 100644 --- a/homeassistant/components/rest/data.py +++ b/homeassistant/components/rest/data.py @@ -56,6 +56,10 @@ class RestData: self.last_exception: Exception | None = None self.headers: httpx.Headers | None = None + def set_payload(self, payload: str) -> None: + """Set request data.""" + self._request_data = payload + @property def url(self) -> str: """Get url.""" diff --git a/homeassistant/components/rest/schema.py b/homeassistant/components/rest/schema.py index d6011a43efd..f7fd8a36113 100644 --- a/homeassistant/components/rest/schema.py +++ b/homeassistant/components/rest/schema.py @@ -38,6 +38,7 @@ from .const import ( CONF_ENCODING, CONF_JSON_ATTRS, CONF_JSON_ATTRS_PATH, + CONF_PAYLOAD_TEMPLATE, CONF_SSL_CIPHER_LIST, DEFAULT_ENCODING, DEFAULT_FORCE_UPDATE, @@ -60,7 +61,8 @@ RESOURCE_SCHEMA = { vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): vol.In(METHODS), vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(CONF_PAYLOAD): cv.string, + vol.Exclusive(CONF_PAYLOAD, CONF_PAYLOAD): cv.string, + vol.Exclusive(CONF_PAYLOAD_TEMPLATE, CONF_PAYLOAD): cv.template, vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, vol.Optional( CONF_SSL_CIPHER_LIST, diff --git a/tests/components/rest/test_init.py b/tests/components/rest/test_init.py index 38a1661a831..0fda89cc329 100644 --- a/tests/components/rest/test_init.py +++ b/tests/components/rest/test_init.py @@ -475,3 +475,62 @@ async def test_config_schema_via_packages(hass: HomeAssistant) -> None: assert len(config["rest"]) == 2 assert config["rest"][0]["resource"] == "http://url1" assert config["rest"][1]["resource"] == "http://url2" + + +@respx.mock +async def test_setup_minimum_payload_template(hass: HomeAssistant) -> None: + """Test setup with minimum configuration (payload_template).""" + + respx.post("http://localhost", json={"data": "value"}).respond( + status_code=HTTPStatus.OK, + json={ + "sensor1": "1", + "sensor2": "2", + "binary_sensor1": "on", + "binary_sensor2": "off", + }, + ) + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: [ + { + "resource": "http://localhost", + "payload_template": '{% set payload = {"data": "value"} %}{{ payload | to_json }}', + "method": "POST", + "verify_ssl": "false", + "timeout": 30, + "sensor": [ + { + "unit_of_measurement": UnitOfInformation.MEGABYTES, + "name": "sensor1", + "value_template": "{{ value_json.sensor1 }}", + }, + { + "unit_of_measurement": UnitOfInformation.MEGABYTES, + "name": "sensor2", + "value_template": "{{ value_json.sensor2 }}", + }, + ], + "binary_sensor": [ + { + "name": "binary_sensor1", + "value_template": "{{ value_json.binary_sensor1 }}", + }, + { + "name": "binary_sensor2", + "value_template": "{{ value_json.binary_sensor2 }}", + }, + ], + } + ] + }, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 4 + + assert hass.states.get("sensor.sensor1").state == "1" + assert hass.states.get("sensor.sensor2").state == "2" + assert hass.states.get("binary_sensor.binary_sensor1").state == "on" + assert hass.states.get("binary_sensor.binary_sensor2").state == "off" From 90575bc496fa82d7bf6a52c8b31041b6b5caa14a Mon Sep 17 00:00:00 2001 From: Marcin Wielgoszewski Date: Thu, 18 Apr 2024 09:37:11 -0400 Subject: [PATCH 665/967] Add hvac_action attribute to iAqualink Thermostat climate entities (#107803) * Update climate.py * Reorder if/else statements per @dcmeglio's suggestion * Don't infer state, actually read it from the underlying device * HVACAction has a HEATING state, not ON * Update homeassistant/components/iaqualink/climate.py --------- Co-authored-by: Erik Montnemery --- homeassistant/components/iaqualink/climate.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/homeassistant/components/iaqualink/climate.py b/homeassistant/components/iaqualink/climate.py index 29576e9fc10..868b5a32c67 100644 --- a/homeassistant/components/iaqualink/climate.py +++ b/homeassistant/components/iaqualink/climate.py @@ -6,11 +6,13 @@ import logging from typing import Any from iaqualink.device import AqualinkThermostat +from iaqualink.systems.iaqua.device import AqualinkState from homeassistant.components.climate import ( DOMAIN as CLIMATE_DOMAIN, ClimateEntity, ClimateEntityFeature, + HVACAction, HVACMode, ) from homeassistant.config_entries import ConfigEntry @@ -82,6 +84,16 @@ class HassAqualinkThermostat(AqualinkEntity, ClimateEntity): else: _LOGGER.warning("Unknown operation mode: %s", hvac_mode) + @property + def hvac_action(self) -> HVACAction: + """Return the current HVAC action.""" + state = AqualinkState(self.dev._heater.state) + if state == AqualinkState.ON: + return HVACAction.HEATING + if state == AqualinkState.ENABLED: + return HVACAction.IDLE + return HVACAction.OFF + @property def target_temperature(self) -> float: """Return the current target temperature.""" From 74afed3b6d4267f92445029ca6ebde17c77793e6 Mon Sep 17 00:00:00 2001 From: Arjan van Balken Date: Thu, 18 Apr 2024 15:52:03 +0200 Subject: [PATCH 666/967] Bump arris-tg2492lg to 2.2.0 (#107905) Bumps arris-tg2492lg from 1.2.1 to 2.2.0 --- .../arris_tg2492lg/device_tracker.py | 29 +++++++++++++------ .../components/arris_tg2492lg/manifest.json | 4 ++- requirements_all.txt | 2 +- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/arris_tg2492lg/device_tracker.py b/homeassistant/components/arris_tg2492lg/device_tracker.py index 4f674a13c0e..3975109e07a 100644 --- a/homeassistant/components/arris_tg2492lg/device_tracker.py +++ b/homeassistant/components/arris_tg2492lg/device_tracker.py @@ -2,6 +2,7 @@ from __future__ import annotations +from aiohttp.client_exceptions import ClientResponseError from arris_tg2492lg import ConnectBox, Device import voluptuous as vol @@ -12,6 +13,7 @@ from homeassistant.components.device_tracker import ( ) from homeassistant.const import CONF_HOST, CONF_PASSWORD from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType @@ -25,12 +27,21 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( ) -def get_scanner(hass: HomeAssistant, config: ConfigType) -> ArrisDeviceScanner: - """Return the Arris device scanner.""" +async def async_get_scanner( + hass: HomeAssistant, config: ConfigType +) -> ArrisDeviceScanner | None: + """Return the Arris device scanner if successful.""" conf = config[DOMAIN] url = f"http://{conf[CONF_HOST]}" - connect_box = ConnectBox(url, conf[CONF_PASSWORD]) - return ArrisDeviceScanner(connect_box) + websession = async_get_clientsession(hass) + connect_box = ConnectBox(websession, url, conf[CONF_PASSWORD]) + + try: + await connect_box.async_login() + + return ArrisDeviceScanner(connect_box) + except ClientResponseError: + return None class ArrisDeviceScanner(DeviceScanner): @@ -41,22 +52,22 @@ class ArrisDeviceScanner(DeviceScanner): self.connect_box = connect_box self.last_results: list[Device] = [] - def scan_devices(self) -> list[str]: + async def async_scan_devices(self) -> list[str]: """Scan for new devices and return a list with found device IDs.""" - self._update_info() + await self._async_update_info() return [device.mac for device in self.last_results if device.mac] - def get_device_name(self, device: str) -> str | None: + async def async_get_device_name(self, device: str) -> str | None: """Return the name of the given device or None if we don't know.""" return next( (result.hostname for result in self.last_results if result.mac == device), None, ) - def _update_info(self) -> None: + async def _async_update_info(self) -> None: """Ensure the information from the Arris TG2492LG router is up to date.""" - result = self.connect_box.get_connected_devices() + result = await self.connect_box.async_get_connected_devices() last_results: list[Device] = [] mac_addresses: set[str | None] = set() diff --git a/homeassistant/components/arris_tg2492lg/manifest.json b/homeassistant/components/arris_tg2492lg/manifest.json index 0134ea9077d..fa7673b4276 100644 --- a/homeassistant/components/arris_tg2492lg/manifest.json +++ b/homeassistant/components/arris_tg2492lg/manifest.json @@ -2,8 +2,10 @@ "domain": "arris_tg2492lg", "name": "Arris TG2492LG", "codeowners": ["@vanbalken"], + "dependencies": [], "documentation": "https://www.home-assistant.io/integrations/arris_tg2492lg", + "integration_type": "hub", "iot_class": "local_polling", "loggers": ["arris_tg2492lg"], - "requirements": ["arris-tg2492lg==1.2.1"] + "requirements": ["arris-tg2492lg==2.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index edb4c8919d9..0e11345c278 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -464,7 +464,7 @@ aranet4==2.3.3 arcam-fmj==1.4.0 # homeassistant.components.arris_tg2492lg -arris-tg2492lg==1.2.1 +arris-tg2492lg==2.2.0 # homeassistant.components.ampio asmog==0.0.6 From fbdef7f5cdf7aead17df24a34b0dde1f8fb7e54e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 18 Apr 2024 09:37:20 -0500 Subject: [PATCH 667/967] Bump habluetooth to 2.8.0 (#115789) * Bump habluetooth to 2.8.0 Adds support for recovering some adapters that fail to initialize due to kernel races * bump lib * tweak --- homeassistant/components/bluetooth/__init__.py | 13 ++----------- homeassistant/components/bluetooth/diagnostics.py | 2 +- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 7 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 35fbeb2f3b3..35d4b625942 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -53,7 +53,6 @@ from homeassistant.loader import async_get_bluetooth from . import models, passive_update_processor from .api import ( - _get_manager, async_address_present, async_ble_device_from_address, async_discovered_service_info, @@ -130,13 +129,6 @@ _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) -async def _async_get_adapter_from_address( - hass: HomeAssistant, address: str -) -> str | None: - """Get an adapter by the address.""" - return await _get_manager(hass).async_get_adapter_from_address(address) - - async def _async_start_adapter_discovery( hass: HomeAssistant, manager: HomeAssistantBluetoothManager, @@ -303,17 +295,16 @@ async def async_update_device( async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry for a bluetooth scanner.""" + manager: HomeAssistantBluetoothManager = hass.data[DATA_MANAGER] address = entry.unique_id assert address is not None - adapter = await _async_get_adapter_from_address(hass, address) + adapter = await manager.async_get_adapter_from_address_or_recover(address) if adapter is None: raise ConfigEntryNotReady( f"Bluetooth adapter {adapter} with address {address} not found" ) - passive = entry.options.get(CONF_PASSIVE) mode = BluetoothScanningMode.PASSIVE if passive else BluetoothScanningMode.ACTIVE - manager: HomeAssistantBluetoothManager = hass.data[DATA_MANAGER] scanner = HaScanner(mode, adapter, address) scanner.async_setup() try: diff --git a/homeassistant/components/bluetooth/diagnostics.py b/homeassistant/components/bluetooth/diagnostics.py index a45500265cf..1c9c9a56b2e 100644 --- a/homeassistant/components/bluetooth/diagnostics.py +++ b/homeassistant/components/bluetooth/diagnostics.py @@ -10,7 +10,7 @@ from bluetooth_adapters import get_dbus_managed_objects from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from . import _get_manager +from .api import _get_manager async def async_get_config_entry_diagnostics( diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index f7d27e84a17..b41c344bdf2 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -20,6 +20,6 @@ "bluetooth-auto-recovery==1.4.1", "bluetooth-data-tools==1.19.0", "dbus-fast==2.21.1", - "habluetooth==2.7.0" + "habluetooth==2.8.0" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7782fba1713..7f134b1a93d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -28,7 +28,7 @@ dbus-fast==2.21.1 fnv-hash-fast==0.5.0 ha-av==10.1.1 ha-ffmpeg==3.2.0 -habluetooth==2.7.0 +habluetooth==2.8.0 hass-nabucasa==0.78.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 diff --git a/requirements_all.txt b/requirements_all.txt index 0e11345c278..18c4d6a0076 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1029,7 +1029,7 @@ ha-philipsjs==3.1.1 habitipy==0.2.0 # homeassistant.components.bluetooth -habluetooth==2.7.0 +habluetooth==2.8.0 # homeassistant.components.cloud hass-nabucasa==0.78.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b7a4a59a18e..aeb38c28aa1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -843,7 +843,7 @@ ha-philipsjs==3.1.1 habitipy==0.2.0 # homeassistant.components.bluetooth -habluetooth==2.7.0 +habluetooth==2.8.0 # homeassistant.components.cloud hass-nabucasa==0.78.0 From 53c48537d720e048e300dad7a04ee17dbb3028e7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 18 Apr 2024 09:39:32 -0500 Subject: [PATCH 668/967] Add bluetooth adapter model and manufacturer to config flow (#115780) * Show bluetooth adapter model and manufacturer in config flow If there are multiple adapters, it could be a bit difficult to figure out which one is which * Show bluetooth adapter model and manufacturer in config flow If there are multiple adapters, it could be a bit difficult to figure out which one is which * reorder * reorder * names * remove * fix incomplete mocking * more missing mocks --- .../components/airthings_ble/strings.json | 2 +- homeassistant/components/aranet/strings.json | 2 +- .../components/bluemaestro/strings.json | 2 +- .../components/bluetooth/config_flow.py | 52 +++++++++++++++---- .../components/bluetooth/strings.json | 4 +- homeassistant/components/bthome/strings.json | 2 +- .../components/dormakaba_dkey/strings.json | 2 +- .../components/eufylife_ble/strings.json | 2 +- .../components/govee_ble/strings.json | 2 +- .../components/improv_ble/strings.json | 2 +- homeassistant/components/inkbird/strings.json | 2 +- homeassistant/components/kegtron/strings.json | 2 +- homeassistant/components/leaone/strings.json | 2 +- .../components/medcom_ble/strings.json | 2 +- homeassistant/components/moat/strings.json | 2 +- homeassistant/components/mopeka/strings.json | 2 +- homeassistant/components/oralb/strings.json | 2 +- .../private_ble_device/strings.json | 2 +- .../components/qingping/strings.json | 2 +- .../components/rapt_ble/strings.json | 2 +- .../components/ruuvitag_ble/strings.json | 2 +- .../components/sensirion_ble/strings.json | 2 +- .../components/sensorpro/strings.json | 2 +- .../components/sensorpush/strings.json | 2 +- homeassistant/components/snooz/strings.json | 2 +- .../components/thermobeacon/strings.json | 2 +- .../components/thermopro/strings.json | 2 +- .../components/tilt_ble/strings.json | 2 +- .../components/xiaomi_ble/strings.json | 2 +- .../components/bluetooth/test_config_flow.py | 34 +++++++++--- tests/components/bluetooth/test_init.py | 4 ++ 31 files changed, 102 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/airthings_ble/strings.json b/homeassistant/components/airthings_ble/strings.json index 6f17b9a317e..4b38923384a 100644 --- a/homeassistant/components/airthings_ble/strings.json +++ b/homeassistant/components/airthings_ble/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/aranet/strings.json b/homeassistant/components/aranet/strings.json index 918cfc1d384..ac8d1907770 100644 --- a/homeassistant/components/aranet/strings.json +++ b/homeassistant/components/aranet/strings.json @@ -11,7 +11,7 @@ "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" } }, - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "error": { "unknown": "[%key:common::config_flow::error::unknown%]" }, diff --git a/homeassistant/components/bluemaestro/strings.json b/homeassistant/components/bluemaestro/strings.json index 9dc500980a6..8f84456d3a7 100644 --- a/homeassistant/components/bluemaestro/strings.json +++ b/homeassistant/components/bluemaestro/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/bluetooth/config_flow.py b/homeassistant/components/bluetooth/config_flow.py index 2b5980fbcd6..6802bdc37c0 100644 --- a/homeassistant/components/bluetooth/config_flow.py +++ b/homeassistant/components/bluetooth/config_flow.py @@ -6,8 +6,10 @@ from typing import Any, cast from bluetooth_adapters import ( ADAPTER_ADDRESS, + ADAPTER_MANUFACTURER, AdapterDetails, adapter_human_name, + adapter_model, adapter_unique_name, get_adapters, ) @@ -35,6 +37,22 @@ OPTIONS_FLOW = { } +def adapter_display_info(adapter: str, details: AdapterDetails) -> str: + """Return the adapter display info.""" + name = adapter_human_name(adapter, details[ADAPTER_ADDRESS]) + model = adapter_model(details) + manufacturer = details[ADAPTER_MANUFACTURER] or "Unknown" + return f"{name} {manufacturer} {model}" + + +def adapter_title(adapter: str, details: AdapterDetails) -> str: + """Return the adapter title.""" + unique_name = adapter_unique_name(adapter, details[ADAPTER_ADDRESS]) + model = adapter_model(details) + manufacturer = details[ADAPTER_MANUFACTURER] or "Unknown" + return f"{manufacturer} {model} ({unique_name})" + + class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): """Config flow for Bluetooth.""" @@ -45,6 +63,7 @@ class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): self._adapter: str | None = None self._details: AdapterDetails | None = None self._adapters: dict[str, AdapterDetails] = {} + self._placeholders: dict[str, str] = {} async def async_step_integration_discovery( self, discovery_info: DiscoveryInfoType @@ -54,11 +73,23 @@ class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): self._details = cast(AdapterDetails, discovery_info[CONF_DETAILS]) await self.async_set_unique_id(self._details[ADAPTER_ADDRESS]) self._abort_if_unique_id_configured() - self.context["title_placeholders"] = { - "name": adapter_human_name(self._adapter, self._details[ADAPTER_ADDRESS]) - } + details = self._details + self._async_set_adapter_info(self._adapter, details) return await self.async_step_single_adapter() + @callback + def _async_set_adapter_info(self, adapter: str, details: AdapterDetails) -> None: + """Set the adapter info.""" + name = adapter_human_name(adapter, details[ADAPTER_ADDRESS]) + model = adapter_model(details) + manufacturer = details[ADAPTER_MANUFACTURER] + self._placeholders = { + "name": name, + "model": model, + "manufacturer": manufacturer or "Unknown", + } + self.context["title_placeholders"] = self._placeholders + async def async_step_single_adapter( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: @@ -67,6 +98,7 @@ class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): details = self._details assert adapter is not None assert details is not None + assert self._placeholders is not None address = details[ADAPTER_ADDRESS] @@ -74,12 +106,12 @@ class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(address, raise_on_progress=False) self._abort_if_unique_id_configured() return self.async_create_entry( - title=adapter_unique_name(adapter, address), data={} + title=adapter_title(adapter, details), data={} ) return self.async_show_form( step_id="single_adapter", - description_placeholders={"name": adapter_human_name(adapter, address)}, + description_placeholders=self._placeholders, ) async def async_step_multiple_adapters( @@ -89,11 +121,12 @@ class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): if user_input is not None: assert self._adapters is not None adapter = user_input[CONF_ADAPTER] - address = self._adapters[adapter][ADAPTER_ADDRESS] + details = self._adapters[adapter] + address = details[ADAPTER_ADDRESS] await self.async_set_unique_id(address, raise_on_progress=False) self._abort_if_unique_id_configured() return self.async_create_entry( - title=adapter_unique_name(adapter, address), data={} + title=adapter_title(adapter, details), data={} ) configured_addresses = self._async_current_ids() @@ -116,6 +149,7 @@ class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): if len(unconfigured_adapters) == 1: self._adapter = list(self._adapters)[0] self._details = self._adapters[self._adapter] + self._async_set_adapter_info(self._adapter, self._details) return await self.async_step_single_adapter() return self.async_show_form( @@ -124,8 +158,8 @@ class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): { vol.Required(CONF_ADAPTER): vol.In( { - adapter: adapter_human_name( - adapter, self._adapters[adapter][ADAPTER_ADDRESS] + adapter: adapter_display_info( + adapter, self._adapters[adapter] ) for adapter in sorted(unconfigured_adapters) } diff --git a/homeassistant/components/bluetooth/strings.json b/homeassistant/components/bluetooth/strings.json index 4b168126251..c28bd3cc65e 100644 --- a/homeassistant/components/bluetooth/strings.json +++ b/homeassistant/components/bluetooth/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "{name}", + "flow_title": "{name} {manufacturer} {model}", "step": { "user": { "description": "Choose a device to set up", @@ -18,7 +18,7 @@ } }, "single_adapter": { - "description": "Do you want to set up the Bluetooth adapter {name}?" + "description": "Do you want to set up the Bluetooth adapter {name} {manufacturer} {model}?" } }, "abort": { diff --git a/homeassistant/components/bthome/strings.json b/homeassistant/components/bthome/strings.json index 50c5c7bada6..c64028229b3 100644 --- a/homeassistant/components/bthome/strings.json +++ b/homeassistant/components/bthome/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/dormakaba_dkey/strings.json b/homeassistant/components/dormakaba_dkey/strings.json index 480f021b126..1fdc7cb359f 100644 --- a/homeassistant/components/dormakaba_dkey/strings.json +++ b/homeassistant/components/dormakaba_dkey/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/eufylife_ble/strings.json b/homeassistant/components/eufylife_ble/strings.json index aaeeeb85f67..72f0e7b5973 100644 --- a/homeassistant/components/eufylife_ble/strings.json +++ b/homeassistant/components/eufylife_ble/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/govee_ble/strings.json b/homeassistant/components/govee_ble/strings.json index 4003debbbeb..4e12a84b653 100644 --- a/homeassistant/components/govee_ble/strings.json +++ b/homeassistant/components/govee_ble/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/improv_ble/strings.json b/homeassistant/components/improv_ble/strings.json index b5713910134..be157b8070d 100644 --- a/homeassistant/components/improv_ble/strings.json +++ b/homeassistant/components/improv_ble/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/inkbird/strings.json b/homeassistant/components/inkbird/strings.json index 4003debbbeb..4e12a84b653 100644 --- a/homeassistant/components/inkbird/strings.json +++ b/homeassistant/components/inkbird/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/kegtron/strings.json b/homeassistant/components/kegtron/strings.json index d1d544c2381..16a80220a20 100644 --- a/homeassistant/components/kegtron/strings.json +++ b/homeassistant/components/kegtron/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/leaone/strings.json b/homeassistant/components/leaone/strings.json index 6391c754dec..bb684941147 100644 --- a/homeassistant/components/leaone/strings.json +++ b/homeassistant/components/leaone/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/medcom_ble/strings.json b/homeassistant/components/medcom_ble/strings.json index 56cfb5a1dd7..4f2b29b7269 100644 --- a/homeassistant/components/medcom_ble/strings.json +++ b/homeassistant/components/medcom_ble/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/moat/strings.json b/homeassistant/components/moat/strings.json index 4003debbbeb..4e12a84b653 100644 --- a/homeassistant/components/moat/strings.json +++ b/homeassistant/components/moat/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/mopeka/strings.json b/homeassistant/components/mopeka/strings.json index d1d544c2381..16a80220a20 100644 --- a/homeassistant/components/mopeka/strings.json +++ b/homeassistant/components/mopeka/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/oralb/strings.json b/homeassistant/components/oralb/strings.json index f60fd56a9a4..775bbedac74 100644 --- a/homeassistant/components/oralb/strings.json +++ b/homeassistant/components/oralb/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/private_ble_device/strings.json b/homeassistant/components/private_ble_device/strings.json index 9e20a9476ec..c35775a4843 100644 --- a/homeassistant/components/private_ble_device/strings.json +++ b/homeassistant/components/private_ble_device/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "What is the IRK (Identity Resolving Key) of the BLE device you want to track?", diff --git a/homeassistant/components/qingping/strings.json b/homeassistant/components/qingping/strings.json index d1d544c2381..16a80220a20 100644 --- a/homeassistant/components/qingping/strings.json +++ b/homeassistant/components/qingping/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/rapt_ble/strings.json b/homeassistant/components/rapt_ble/strings.json index 4003debbbeb..4e12a84b653 100644 --- a/homeassistant/components/rapt_ble/strings.json +++ b/homeassistant/components/rapt_ble/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/ruuvitag_ble/strings.json b/homeassistant/components/ruuvitag_ble/strings.json index d1d544c2381..16a80220a20 100644 --- a/homeassistant/components/ruuvitag_ble/strings.json +++ b/homeassistant/components/ruuvitag_ble/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/sensirion_ble/strings.json b/homeassistant/components/sensirion_ble/strings.json index d1d544c2381..16a80220a20 100644 --- a/homeassistant/components/sensirion_ble/strings.json +++ b/homeassistant/components/sensirion_ble/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/sensorpro/strings.json b/homeassistant/components/sensorpro/strings.json index d1d544c2381..16a80220a20 100644 --- a/homeassistant/components/sensorpro/strings.json +++ b/homeassistant/components/sensorpro/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/sensorpush/strings.json b/homeassistant/components/sensorpush/strings.json index 4003debbbeb..4e12a84b653 100644 --- a/homeassistant/components/sensorpush/strings.json +++ b/homeassistant/components/sensorpush/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/snooz/strings.json b/homeassistant/components/snooz/strings.json index b38e105260c..5a31cea6cac 100644 --- a/homeassistant/components/snooz/strings.json +++ b/homeassistant/components/snooz/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/thermobeacon/strings.json b/homeassistant/components/thermobeacon/strings.json index d1d544c2381..16a80220a20 100644 --- a/homeassistant/components/thermobeacon/strings.json +++ b/homeassistant/components/thermobeacon/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/thermopro/strings.json b/homeassistant/components/thermopro/strings.json index 4003debbbeb..4e12a84b653 100644 --- a/homeassistant/components/thermopro/strings.json +++ b/homeassistant/components/thermopro/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/tilt_ble/strings.json b/homeassistant/components/tilt_ble/strings.json index 4003debbbeb..4e12a84b653 100644 --- a/homeassistant/components/tilt_ble/strings.json +++ b/homeassistant/components/tilt_ble/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/homeassistant/components/xiaomi_ble/strings.json b/homeassistant/components/xiaomi_ble/strings.json index 2f2b705ff60..8ee8bac3fea 100644 --- a/homeassistant/components/xiaomi_ble/strings.json +++ b/homeassistant/components/xiaomi_ble/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "flow_title": "{name}", "step": { "user": { "description": "[%key:component::bluetooth::config::step::user::description%]", diff --git a/tests/components/bluetooth/test_config_flow.py b/tests/components/bluetooth/test_config_flow.py index 89243223129..f9bbbcd2d0e 100644 --- a/tests/components/bluetooth/test_config_flow.py +++ b/tests/components/bluetooth/test_config_flow.py @@ -65,7 +65,7 @@ async def test_async_step_user_macos(hass: HomeAssistant, macos_adapter: None) - result["flow_id"], user_input={} ) assert result2["type"] is FlowResultType.CREATE_ENTRY - assert result2["title"] == "Core Bluetooth" + assert result2["title"] == "Apple Unknown MacOS Model (Core Bluetooth)" assert result2["data"] == {} assert len(mock_setup_entry.mock_calls) == 1 @@ -81,6 +81,11 @@ async def test_async_step_user_linux_one_adapter( ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "single_adapter" + assert result["description_placeholders"] == { + "name": "hci0 (00:00:00:00:00:01)", + "model": "Bluetooth Adapter 5.0 (cc01:aa01)", + "manufacturer": "ACME", + } with ( patch("homeassistant.components.bluetooth.async_setup", return_value=True), patch( @@ -91,7 +96,9 @@ async def test_async_step_user_linux_one_adapter( result["flow_id"], user_input={} ) assert result2["type"] is FlowResultType.CREATE_ENTRY - assert result2["title"] == "00:00:00:00:00:01" + assert ( + result2["title"] == "ACME Bluetooth Adapter 5.0 (cc01:aa01) (00:00:00:00:00:01)" + ) assert result2["data"] == {} assert len(mock_setup_entry.mock_calls) == 1 @@ -107,6 +114,10 @@ async def test_async_step_user_linux_two_adapters( ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "multiple_adapters" + assert result["data_schema"].schema["adapter"].container == { + "hci0": "hci0 (00:00:00:00:00:01) ACME Bluetooth Adapter 5.0 (cc01:aa01)", + "hci1": "hci1 (00:00:00:00:00:02) ACME Bluetooth Adapter 5.0 (cc01:aa01)", + } with ( patch("homeassistant.components.bluetooth.async_setup", return_value=True), patch( @@ -117,7 +128,9 @@ async def test_async_step_user_linux_two_adapters( result["flow_id"], user_input={CONF_ADAPTER: "hci1"} ) assert result2["type"] is FlowResultType.CREATE_ENTRY - assert result2["title"] == "00:00:00:00:00:02" + assert ( + result2["title"] == "ACME Bluetooth Adapter 5.0 (cc01:aa01) (00:00:00:00:00:02)" + ) assert result2["data"] == {} assert len(mock_setup_entry.mock_calls) == 1 @@ -153,6 +166,11 @@ async def test_async_step_integration_discovery(hass: HomeAssistant) -> None: data={CONF_ADAPTER: "hci0", CONF_DETAILS: details}, ) assert result["type"] is FlowResultType.FORM + assert result["description_placeholders"] == { + "name": "hci0 (00:00:00:00:00:01)", + "model": "Unknown", + "manufacturer": "ACME", + } assert result["step_id"] == "single_adapter" with ( patch("homeassistant.components.bluetooth.async_setup", return_value=True), @@ -164,7 +182,7 @@ async def test_async_step_integration_discovery(hass: HomeAssistant) -> None: result["flow_id"], user_input={} ) assert result2["type"] is FlowResultType.CREATE_ENTRY - assert result2["title"] == "00:00:00:00:00:01" + assert result2["title"] == "ACME Unknown (00:00:00:00:00:01)" assert result2["data"] == {} assert len(mock_setup_entry.mock_calls) == 1 @@ -196,7 +214,7 @@ async def test_async_step_integration_discovery_during_onboarding_one_adapter( data={CONF_ADAPTER: "hci0", CONF_DETAILS: details}, ) assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "00:00:00:00:00:01" + assert result["title"] == "ACME Unknown (00:00:00:00:00:01)" assert result["data"] == {} assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_onboarding.mock_calls) == 1 @@ -240,11 +258,11 @@ async def test_async_step_integration_discovery_during_onboarding_two_adapters( data={CONF_ADAPTER: "hci1", CONF_DETAILS: details2}, ) assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "00:00:00:00:00:01" + assert result["title"] == "ACME Unknown (00:00:00:00:00:01)" assert result["data"] == {} assert result2["type"] is FlowResultType.CREATE_ENTRY - assert result2["title"] == "00:00:00:00:00:02" + assert result2["title"] == "ACME Unknown (00:00:00:00:00:02)" assert result2["data"] == {} assert len(mock_setup_entry.mock_calls) == 2 @@ -278,7 +296,7 @@ async def test_async_step_integration_discovery_during_onboarding( data={CONF_ADAPTER: "Core Bluetooth", CONF_DETAILS: details}, ) assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "Core Bluetooth" + assert result["title"] == "ACME Unknown (Core Bluetooth)" assert result["data"] == {} assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_onboarding.mock_calls) == 1 diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 65962ac8f21..e68ccc94d19 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -3015,12 +3015,14 @@ async def test_discover_new_usb_adapters( "hw_version": "usb:v1D6Bp0246d053F", "passive_scan": False, "sw_version": "homeassistant", + "manufacturer": "ACME", }, "hci1": { "address": "00:00:00:00:00:02", "hw_version": "usb:v1D6Bp0246d053F", "passive_scan": False, "sw_version": "homeassistant", + "manufacturer": "ACME", }, }, ), @@ -3088,12 +3090,14 @@ async def test_discover_new_usb_adapters_with_firmware_fallback_delay( "hw_version": "usb:v1D6Bp0246d053F", "passive_scan": False, "sw_version": "homeassistant", + "manufacturer": "ACME", }, "hci1": { "address": "00:00:00:00:00:02", "hw_version": "usb:v1D6Bp0246d053F", "passive_scan": False, "sw_version": "homeassistant", + "manufacturer": "ACME", }, }, ), From ea8d4d0dcae8b042544341645170ba7ab940d431 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 18 Apr 2024 09:39:58 -0500 Subject: [PATCH 669/967] Add reauth support to oncue (#115667) * Add reauth support to oncue * review comments * reauth on update failure * coverage --- homeassistant/components/oncue/__init__.py | 18 ++-- homeassistant/components/oncue/config_flow.py | 82 +++++++++++++++---- homeassistant/components/oncue/strings.json | 9 +- tests/components/oncue/__init__.py | 20 ++++- tests/components/oncue/test_config_flow.py | 56 ++++++++++++- tests/components/oncue/test_init.py | 29 ++++++- 6 files changed, 185 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/oncue/__init__.py b/homeassistant/components/oncue/__init__.py index b3d59f50321..f960b1a8b81 100644 --- a/homeassistant/components/oncue/__init__.py +++ b/homeassistant/components/oncue/__init__.py @@ -5,12 +5,12 @@ from __future__ import annotations from datetime import timedelta import logging -from aiooncue import LoginFailedException, Oncue +from aiooncue import LoginFailedException, Oncue, OncueDevice from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -29,17 +29,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await client.async_login() except CONNECTION_EXCEPTIONS as ex: - raise ConfigEntryNotReady(ex) from ex + raise ConfigEntryNotReady from ex except LoginFailedException as ex: - _LOGGER.error("Failed to login to oncue service: %s", ex) - return False + raise ConfigEntryAuthFailed from ex + + async def _async_update() -> dict[str, OncueDevice]: + """Fetch data from Oncue.""" + try: + return await client.async_fetch_all() + except LoginFailedException as ex: + raise ConfigEntryAuthFailed from ex coordinator = DataUpdateCoordinator( hass, _LOGGER, name=f"Oncue {entry.data[CONF_USERNAME]}", update_interval=timedelta(minutes=10), - update_method=client.async_fetch_all, + update_method=_async_update, always_update=False, ) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/oncue/config_flow.py b/homeassistant/components/oncue/config_flow.py index ba672dcc588..e423ba08105 100644 --- a/homeassistant/components/oncue/config_flow.py +++ b/homeassistant/components/oncue/config_flow.py @@ -2,13 +2,14 @@ from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any from aiooncue import LoginFailedException, Oncue import voluptuous as vol -from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -22,30 +23,26 @@ class OncueConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 + def __init__(self) -> None: + """Initialize the oncue config flow.""" + self.reauth_entry: ConfigEntry | None = None + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle the initial step.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: - try: - await Oncue( - user_input[CONF_USERNAME], - user_input[CONF_PASSWORD], - async_get_clientsession(self.hass), - ).async_login() - except CONNECTION_EXCEPTIONS: - errors["base"] = "cannot_connect" - except LoginFailedException: - errors["base"] = "invalid_auth" - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" - else: + if not (errors := await self._async_validate_or_error(user_input)): normalized_username = user_input[CONF_USERNAME].lower() await self.async_set_unique_id(normalized_username) - self._abort_if_unique_id_configured() + self._abort_if_unique_id_configured( + updates={ + CONF_USERNAME: user_input[CONF_USERNAME], + CONF_PASSWORD: user_input[CONF_PASSWORD], + } + ) return self.async_create_entry( title=normalized_username, data=user_input ) @@ -60,3 +57,54 @@ class OncueConfigFlow(ConfigFlow, domain=DOMAIN): ), errors=errors, ) + + async def _async_validate_or_error(self, config: dict[str, Any]) -> dict[str, str]: + """Validate the user input.""" + errors: dict[str, str] = {} + try: + await Oncue( + config[CONF_USERNAME], + config[CONF_PASSWORD], + async_get_clientsession(self.hass), + ).async_login() + except CONNECTION_EXCEPTIONS: + errors["base"] = "cannot_connect" + except LoginFailedException: + errors[CONF_PASSWORD] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + return errors + + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: + """Handle reauth.""" + entry_id = self.context["entry_id"] + self.reauth_entry = self.hass.config_entries.async_get_entry(entry_id) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle reauth input.""" + errors: dict[str, str] = {} + existing_entry = self.reauth_entry + assert existing_entry + existing_data = existing_entry.data + description_placeholders: dict[str, str] = { + CONF_USERNAME: existing_data[CONF_USERNAME] + } + if user_input is not None: + new_config = {**existing_data, CONF_PASSWORD: user_input[CONF_PASSWORD]} + if not (errors := await self._async_validate_or_error(new_config)): + return self.async_update_reload_and_abort( + existing_entry, data=new_config + ) + + return self.async_show_form( + description_placeholders=description_placeholders, + step_id="reauth_confirm", + data_schema=vol.Schema({vol.Required(CONF_PASSWORD): str}), + errors=errors, + ) diff --git a/homeassistant/components/oncue/strings.json b/homeassistant/components/oncue/strings.json index f7a539fe0e6..ce7561962a2 100644 --- a/homeassistant/components/oncue/strings.json +++ b/homeassistant/components/oncue/strings.json @@ -6,6 +6,12 @@ "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]" } + }, + "reauth_confirm": { + "description": "Re-authenticate Oncue account {username}", + "data": { + "password": "[%key:common::config_flow::data::password%]" + } } }, "error": { @@ -14,7 +20,8 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } } } diff --git a/tests/components/oncue/__init__.py b/tests/components/oncue/__init__.py index df1452b176e..d88774307c0 100644 --- a/tests/components/oncue/__init__.py +++ b/tests/components/oncue/__init__.py @@ -3,7 +3,7 @@ from contextlib import contextmanager from unittest.mock import patch -from aiooncue import OncueDevice, OncueSensor +from aiooncue import LoginFailedException, OncueDevice, OncueSensor MOCK_ASYNC_FETCH_ALL = { "123456": OncueDevice( @@ -861,3 +861,21 @@ def _patch_login_and_data_unavailable_device(): yield return _patcher() + + +def _patch_login_and_data_auth_failure(): + @contextmanager + def _patcher(): + with ( + patch( + "homeassistant.components.oncue.Oncue.async_login", + side_effect=LoginFailedException, + ), + patch( + "homeassistant.components.oncue.Oncue.async_fetch_all", + side_effect=LoginFailedException, + ), + ): + yield + + return _patcher() diff --git a/tests/components/oncue/test_config_flow.py b/tests/components/oncue/test_config_flow.py index 2f327dec052..3907242e26c 100644 --- a/tests/components/oncue/test_config_flow.py +++ b/tests/components/oncue/test_config_flow.py @@ -6,6 +6,7 @@ from aiooncue import LoginFailedException from homeassistant import config_entries from homeassistant.components.oncue.const import DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -42,7 +43,7 @@ async def test_form(hass: HomeAssistant) -> None: "username": "TEST-username", "password": "test-password", } - assert len(mock_setup_entry.mock_calls) == 1 + assert mock_setup_entry.call_count == 1 async def test_form_invalid_auth(hass: HomeAssistant) -> None: @@ -64,7 +65,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: ) assert result2["type"] is FlowResultType.FORM - assert result2["errors"] == {"base": "invalid_auth"} + assert result2["errors"] == {CONF_PASSWORD: "invalid_auth"} async def test_form_cannot_connect(hass: HomeAssistant) -> None: @@ -138,3 +139,54 @@ async def test_already_configured(hass: HomeAssistant) -> None: assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" + + +async def test_reauth(hass: HomeAssistant) -> None: + """Test reauth flow.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_USERNAME: "any", + CONF_PASSWORD: "old", + }, + ) + config_entry.add_to_hass(hass) + config_entry.async_start_reauth(hass) + await hass.async_block_till_done() + flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN) + assert len(flows) == 1 + flow = flows[0] + + with patch( + "homeassistant.components.oncue.config_flow.Oncue.async_login", + side_effect=LoginFailedException, + ): + result2 = await hass.config_entries.flow.async_configure( + flow["flow_id"], + { + CONF_PASSWORD: "test-password", + }, + ) + + assert result2["type"] is FlowResultType.FORM + assert result2["errors"] == {"password": "invalid_auth"} + + with ( + patch("homeassistant.components.oncue.config_flow.Oncue.async_login"), + patch( + "homeassistant.components.oncue.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): + result2 = await hass.config_entries.flow.async_configure( + flow["flow_id"], + { + CONF_PASSWORD: "test-password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] is FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" + assert config_entry.data[CONF_PASSWORD] == "test-password" + assert mock_setup_entry.call_count == 1 diff --git a/tests/components/oncue/test_init.py b/tests/components/oncue/test_init.py index 2da3e04e4c3..cf93b51dee1 100644 --- a/tests/components/oncue/test_init.py +++ b/tests/components/oncue/test_init.py @@ -2,6 +2,7 @@ from __future__ import annotations +from datetime import timedelta from unittest.mock import patch from aiooncue import LoginFailedException @@ -12,10 +13,11 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util -from . import _patch_login_and_data +from . import _patch_login_and_data, _patch_login_and_data_auth_failure -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed async def test_config_entry_reload(hass: HomeAssistant) -> None: @@ -67,3 +69,26 @@ async def test_config_entry_retry_later(hass: HomeAssistant) -> None: await async_setup_component(hass, oncue.DOMAIN, {oncue.DOMAIN: {}}) await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_late_auth_failure(hass: HomeAssistant) -> None: + """Test auth fails after already setup.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_USERNAME: "any", CONF_PASSWORD: "any"}, + unique_id="any", + ) + config_entry.add_to_hass(hass) + with _patch_login_and_data(): + await async_setup_component(hass, oncue.DOMAIN, {oncue.DOMAIN: {}}) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED + + with _patch_login_and_data_auth_failure(): + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10)) + await hass.async_block_till_done() + + flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN) + assert len(flows) == 1 + flow = flows[0] + assert flow["context"]["source"] == "reauth" From 588c260dc5d3b532064eb63edb685f2b6f564620 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 18 Apr 2024 09:41:08 -0500 Subject: [PATCH 670/967] Skip processing websocket_api schema if it has no arguments (#115618) * Skip processing websocket_api schema if has no arguments About 40% of the websocket commands on first connection have no arguments. We can skip processing the schema for these cases * cover * fixes * allow extra * Revert "allow extra" This reverts commit 85d9ec36b30aa2aedecd8571c7ed734d0b0a9b05. * match behavior --- .../components/websocket_api/connection.py | 16 +++-- .../components/websocket_api/decorators.py | 10 ++- .../websocket_api/test_decorators.py | 68 +++++++++++++++++++ 3 files changed, 86 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py index 63b4418a19d..3c0743601dd 100644 --- a/homeassistant/components/websocket_api/connection.py +++ b/homeassistant/components/websocket_api/connection.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Callable, Hashable from contextvars import ContextVar -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Literal from aiohttp import web import voluptuous as vol @@ -65,9 +65,9 @@ class ActiveConnection: self.last_id = 0 self.can_coalesce = False self.supported_features: dict[str, float] = {} - self.handlers: dict[str, tuple[MessageHandler, vol.Schema]] = self.hass.data[ - const.DOMAIN - ] + self.handlers: dict[str, tuple[MessageHandler, vol.Schema | Literal[False]]] = ( + self.hass.data[const.DOMAIN] + ) self.binary_handlers: list[BinaryHandler | None] = [] current_connection.set(self) @@ -185,6 +185,7 @@ class ActiveConnection: or ( not (cur_id := msg.get("id")) or type(cur_id) is not int # noqa: E721 + or cur_id < 0 or not (type_ := msg.get("type")) or type(type_) is not str # noqa: E721 ) @@ -220,7 +221,12 @@ class ActiveConnection: handler, schema = handler_schema try: - handler(self.hass, self, schema(msg)) + if schema is False: + if len(msg) > 2: + raise vol.Invalid("extra keys not allowed") + handler(self.hass, self, msg) + else: + handler(self.hass, self, schema(msg)) except Exception as err: # pylint: disable=broad-except self.async_handle_exception(msg, err) diff --git a/homeassistant/components/websocket_api/decorators.py b/homeassistant/components/websocket_api/decorators.py index 51643752a0f..0ed8be30139 100644 --- a/homeassistant/components/websocket_api/decorators.py +++ b/homeassistant/components/websocket_api/decorators.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Callable from functools import wraps -from typing import Any +from typing import TYPE_CHECKING, Any import voluptuous as vol @@ -137,7 +137,7 @@ def websocket_command( The schema must be either a dictionary where the keys are voluptuous markers, or a voluptuous.All schema where the first item is a voluptuous Mapping schema. """ - if isinstance(schema, dict): + if is_dict := isinstance(schema, dict): command = schema["type"] else: command = schema.validators[0].schema["type"] @@ -145,9 +145,13 @@ def websocket_command( def decorate(func: const.WebSocketCommandHandler) -> const.WebSocketCommandHandler: """Decorate ws command function.""" # pylint: disable=protected-access - if isinstance(schema, dict): + if is_dict and len(schema) == 1: # type only empty schema + func._ws_schema = False # type: ignore[attr-defined] + elif is_dict: func._ws_schema = messages.BASE_COMMAND_MESSAGE_SCHEMA.extend(schema) # type: ignore[attr-defined] else: + if TYPE_CHECKING: + assert not isinstance(schema, dict) extended_schema = vol.All( schema.validators[0].extend( messages.BASE_COMMAND_MESSAGE_SCHEMA.schema diff --git a/tests/components/websocket_api/test_decorators.py b/tests/components/websocket_api/test_decorators.py index 3e9c13a8b15..0ade5329190 100644 --- a/tests/components/websocket_api/test_decorators.py +++ b/tests/components/websocket_api/test_decorators.py @@ -1,5 +1,7 @@ """Test decorators.""" +import voluptuous as vol + from homeassistant.components import http, websocket_api from homeassistant.core import HomeAssistant @@ -31,9 +33,16 @@ async def test_async_response_request_context( def get_request(hass, connection, msg): handle_request(http.current_request.get(), connection, msg) + @websocket_api.websocket_command( + {"type": "test-get-request-with-arg", vol.Required("arg"): str} + ) + def get_with_arg_request(hass, connection, msg): + handle_request(http.current_request.get(), connection, msg) + websocket_api.async_register_command(hass, executor_get_request) websocket_api.async_register_command(hass, async_get_request) websocket_api.async_register_command(hass, get_request) + websocket_api.async_register_command(hass, get_with_arg_request) await websocket_client.send_json( { @@ -71,6 +80,65 @@ async def test_async_response_request_context( assert not msg["success"] assert msg["error"]["code"] == "not_found" + await websocket_client.send_json( + { + "id": 8, + "type": "test-get-request-with-arg", + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 8 + assert not msg["success"] + assert msg["error"]["code"] == "invalid_format" + assert ( + msg["error"]["message"] == "required key not provided @ data['arg']. Got None" + ) + + await websocket_client.send_json( + { + "id": 9, + "type": "test-get-request-with-arg", + "arg": "dog", + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 9 + assert msg["success"] + assert msg["result"] == "/api/websocket" + + await websocket_client.send_json( + { + "id": -1, + "type": "test-get-request-with-arg", + "arg": "dog", + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == -1 + assert not msg["success"] + assert msg["error"]["code"] == "invalid_format" + assert msg["error"]["message"] == "Message incorrectly formatted." + + await websocket_client.send_json( + { + "id": 10, + "type": "test-get-request", + "not_valid": "dog", + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 10 + assert not msg["success"] + assert msg["error"]["code"] == "invalid_format" + assert msg["error"]["message"] == ( + "extra keys not allowed. " + "Got {'id': 10, 'type': 'test-get-request', 'not_valid': 'dog'}" + ) + async def test_supervisor_only(hass: HomeAssistant, websocket_client) -> None: """Test that only the Supervisor can make requests.""" From 80d6cdad676c88f88c9cde260c3deb92273d4781 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 18 Apr 2024 09:42:44 -0500 Subject: [PATCH 671/967] Small cleanups to translation loading (#115583) - Add missing typing - Convert a update loop to a set comp - Save some indent --- homeassistant/helpers/translation.py | 37 ++++++++++++++-------------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/homeassistant/helpers/translation.py b/homeassistant/helpers/translation.py index 5ec3af2d382..377826b7edb 100644 --- a/homeassistant/helpers/translation.py +++ b/homeassistant/helpers/translation.py @@ -30,9 +30,11 @@ TRANSLATION_FLATTEN_CACHE = "translation_flatten_cache" LOCALE_EN = "en" -def recursive_flatten(prefix: Any, data: dict[str, Any]) -> dict[str, Any]: +def recursive_flatten( + prefix: str, data: dict[str, dict[str, Any] | str] +) -> dict[str, str]: """Return a flattened representation of dict data.""" - output = {} + output: dict[str, str] = {} for key, value in data.items(): if isinstance(value, dict): output.update(recursive_flatten(f"{prefix}{key}.", value)) @@ -250,9 +252,9 @@ class _TranslationCache: def _validate_placeholders( self, language: str, - updated_resources: dict[str, Any], - cached_resources: dict[str, Any] | None = None, - ) -> dict[str, Any]: + updated_resources: dict[str, str], + cached_resources: dict[str, str] | None = None, + ) -> dict[str, str]: """Validate if updated resources have same placeholders as cached resources.""" if cached_resources is None: return updated_resources @@ -301,9 +303,11 @@ class _TranslationCache: """Extract resources into the cache.""" resource: dict[str, Any] | str cached = self.cache.setdefault(language, {}) - categories: set[str] = set() - for resource in translation_strings.values(): - categories.update(resource) + categories = { + category + for component in translation_strings.values() + for category in component + } for category in categories: new_resources = build_resources(translation_strings, components, category) @@ -312,17 +316,14 @@ class _TranslationCache: for component, resource in new_resources.items(): component_cache = category_cache.setdefault(component, {}) - if isinstance(resource, dict): - resources_flatten = recursive_flatten( - f"component.{component}.{category}.", - resource, - ) - resources_flatten = self._validate_placeholders( - language, resources_flatten, component_cache - ) - component_cache.update(resources_flatten) - else: + if not isinstance(resource, dict): component_cache[f"component.{component}.{category}"] = resource + continue + + prefix = f"component.{component}.{category}." + flat = recursive_flatten(prefix, resource) + flat = self._validate_placeholders(language, flat, component_cache) + component_cache.update(flat) @bind_hass From b18f1ac265460c83da1c394d519143aefbfea12d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 18 Apr 2024 09:45:06 -0500 Subject: [PATCH 672/967] Migrate device_sun_light_trigger to use async_track_state_change_event (#115555) * Migrate device_sun_light_trigger to use async_track_state_change_event async_track_state_change is legacy and will eventually be deprecated after all core usage is removed. There are only two places left * coverage --- .../device_sun_light_trigger/__init__.py | 31 ++++++++++++------- .../device_sun_light_trigger/test_init.py | 17 ++++++++-- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/device_sun_light_trigger/__init__.py b/homeassistant/components/device_sun_light_trigger/__init__.py index 861a634eda7..6781b9afaf7 100644 --- a/homeassistant/components/device_sun_light_trigger/__init__.py +++ b/homeassistant/components/device_sun_light_trigger/__init__.py @@ -1,6 +1,7 @@ """Support to turn on lights based on the states.""" from datetime import timedelta +from functools import partial import logging import voluptuous as vol @@ -27,11 +28,11 @@ from homeassistant.const import ( SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import ( async_track_point_in_utc_time, - async_track_state_change, + async_track_state_change_event, ) from homeassistant.helpers.sun import get_astral_event_next, is_up from homeassistant.helpers.typing import ConfigType @@ -195,8 +196,20 @@ async def activate_automation( # noqa: C901 schedule_light_turn_on(None) @callback - def check_light_on_dev_state_change(entity, old_state, new_state): + def check_light_on_dev_state_change( + from_state: str, to_state: str, event: Event[EventStateChangedData] + ) -> None: """Handle tracked device state changes.""" + event_data = event.data + if ( + (old_state := event_data["old_state"]) is None + or (new_state := event_data["new_state"]) is None + or old_state.state != from_state + or new_state.state != to_state + ): + return + + entity = event_data["entity_id"] lights_are_on = any_light_on() light_needed = not (lights_are_on or is_up(hass)) @@ -237,12 +250,10 @@ async def activate_automation( # noqa: C901 # will all the following then, break. break - async_track_state_change( + async_track_state_change_event( hass, device_entity_ids, - check_light_on_dev_state_change, - STATE_NOT_HOME, - STATE_HOME, + partial(check_light_on_dev_state_change, STATE_NOT_HOME, STATE_HOME), ) if disable_turn_off: @@ -266,12 +277,10 @@ async def activate_automation( # noqa: C901 ) ) - async_track_state_change( + async_track_state_change_event( hass, device_entity_ids, - turn_off_lights_when_all_leave, - STATE_HOME, - STATE_NOT_HOME, + partial(turn_off_lights_when_all_leave, STATE_HOME, STATE_NOT_HOME), ) return diff --git a/tests/components/device_sun_light_trigger/test_init.py b/tests/components/device_sun_light_trigger/test_init.py index b373bd4401f..5f44593aabe 100644 --- a/tests/components/device_sun_light_trigger/test_init.py +++ b/tests/components/device_sun_light_trigger/test_init.py @@ -22,6 +22,7 @@ from homeassistant.const import ( STATE_NOT_HOME, STATE_OFF, STATE_ON, + STATE_UNKNOWN, ) from homeassistant.core import CoreState, HomeAssistant from homeassistant.setup import async_setup_component @@ -150,10 +151,22 @@ async def test_lights_turn_on_when_coming_home_after_sun_set( hass, device_sun_light_trigger.DOMAIN, {device_sun_light_trigger.DOMAIN: {}} ) - hass.states.async_set(f"{DOMAIN}.device_2", STATE_HOME) - + hass.states.async_set(f"{DOMAIN}.device_2", STATE_UNKNOWN) await hass.async_block_till_done() + assert all( + hass.states.get(ent_id).state == STATE_OFF + for ent_id in hass.states.async_entity_ids("light") + ) + hass.states.async_set(f"{DOMAIN}.device_2", STATE_NOT_HOME) + await hass.async_block_till_done() + assert all( + hass.states.get(ent_id).state == STATE_OFF + for ent_id in hass.states.async_entity_ids("light") + ) + + hass.states.async_set(f"{DOMAIN}.device_2", STATE_HOME) + await hass.async_block_till_done() assert all( hass.states.get(ent_id).state == light.STATE_ON for ent_id in hass.states.async_entity_ids("light") From d48bd9b016584ba1eeb17e0e81fae6114acf121e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 18 Apr 2024 10:45:14 -0500 Subject: [PATCH 673/967] Deprecate async_track_state_change in favor of async_track_state_change_event (#115558) --- homeassistant/helpers/event.py | 9 +++++++++ tests/helpers/test_event.py | 15 +++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 648a118f175..7fae0976686 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -38,6 +38,7 @@ from homeassistant.util import dt as dt_util from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.event_type import EventType +from . import frame from .device_registry import ( EVENT_DEVICE_REGISTRY_UPDATED, EventDeviceRegistryUpdatedData, @@ -203,8 +204,16 @@ def async_track_state_change( being None, async_track_state_change_event should be used instead as it is slightly faster. + This function is deprecated and will be removed in Home Assistant 2025.5. + Must be run within the event loop. """ + frame.report( + "calls `async_track_state_change` instead of `async_track_state_change_event`" + " which is deprecated and will be removed in Home Assistant 2025.5", + error_if_core=False, + ) + if from_state is not None: match_from_state = process_state_match(from_state) if to_state is not None: diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index a4235d1ee2c..07228abcc2c 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -4804,3 +4804,18 @@ async def test_async_track_device_registry_updated_event_with_a_callback_that_th unsub2() assert event_data[0] == {"action": "create", "device_id": device_id} + + +async def test_track_state_change_deprecated( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test track_state_change is deprecated.""" + async_track_state_change( + hass, "light.Bowl", lambda entity_id, old_state, new_state: None, "on", "off" + ) + + assert ( + "Detected code that calls `async_track_state_change` instead " + "of `async_track_state_change_event` which is deprecated and " + "will be removed in Home Assistant 2025.5. Please report this issue." + ) in caplog.text From 3a461c32ac9f9e4466af48d43e98bfc44d454d3a Mon Sep 17 00:00:00 2001 From: Brian Rogers Date: Thu, 18 Apr 2024 14:26:09 -0400 Subject: [PATCH 674/967] Add battery binary sensor to Rachio hose timer (#115810) Co-authored-by: J. Nick Koston --- .../components/rachio/binary_sensor.py | 34 ++++++++++++++++++- homeassistant/components/rachio/const.py | 1 + homeassistant/components/rachio/entity.py | 1 + homeassistant/components/rachio/switch.py | 2 -- 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rachio/binary_sensor.py b/homeassistant/components/rachio/binary_sensor.py index eb7a84867ab..e6248b2c93b 100644 --- a/homeassistant/components/rachio/binary_sensor.py +++ b/homeassistant/components/rachio/binary_sensor.py @@ -2,6 +2,7 @@ from abc import abstractmethod import logging +from typing import Any from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, @@ -15,16 +16,21 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( DOMAIN as DOMAIN_RACHIO, + KEY_BATTERY_STATUS, KEY_DEVICE_ID, + KEY_LOW, KEY_RAIN_SENSOR_TRIPPED, + KEY_REPORTED_STATE, + KEY_STATE, KEY_STATUS, KEY_SUBTYPE, SIGNAL_RACHIO_CONTROLLER_UPDATE, SIGNAL_RACHIO_RAIN_SENSOR_UPDATE, STATUS_ONLINE, ) +from .coordinator import RachioUpdateCoordinator from .device import RachioPerson -from .entity import RachioDevice +from .entity import RachioDevice, RachioHoseTimerEntity from .webhooks import ( SUBTYPE_COLD_REBOOT, SUBTYPE_OFFLINE, @@ -52,6 +58,11 @@ def _create_entities(hass: HomeAssistant, config_entry: ConfigEntry) -> list[Ent for controller in person.controllers: entities.append(RachioControllerOnlineBinarySensor(controller)) entities.append(RachioRainSensor(controller)) + entities.extend( + RachioHoseTimerBattery(valve, base_station.coordinator) + for base_station in person.base_stations + for valve in base_station.coordinator.data.values() + ) return entities @@ -140,3 +151,24 @@ class RachioRainSensor(RachioControllerBinarySensor): self._async_handle_any_update, ) ) + + +class RachioHoseTimerBattery(RachioHoseTimerEntity, BinarySensorEntity): + """Represents a battery sensor for a smart hose timer.""" + + _attr_device_class = BinarySensorDeviceClass.BATTERY + + def __init__( + self, data: dict[str, Any], coordinator: RachioUpdateCoordinator + ) -> None: + """Initialize a smart hose timer battery sensor.""" + super().__init__(data, coordinator) + self._attr_unique_id = f"{self.id}-battery" + + @callback + def _update_attr(self) -> None: + """Handle updated coordinator data.""" + data = self.coordinator.data[self.id] + + self._static_attrs = data[KEY_STATE][KEY_REPORTED_STATE] + self._attr_is_on = self._static_attrs[KEY_BATTERY_STATUS] == KEY_LOW diff --git a/homeassistant/components/rachio/const.py b/homeassistant/components/rachio/const.py index 22c92be2b74..b9b16c0cd87 100644 --- a/homeassistant/components/rachio/const.py +++ b/homeassistant/components/rachio/const.py @@ -57,6 +57,7 @@ KEY_CONNECTED = "connected" KEY_CURRENT_STATUS = "lastWateringAction" KEY_DETECT_FLOW = "detectFlow" KEY_BATTERY_STATUS = "batteryStatus" +KEY_LOW = "LOW" KEY_REASON = "reason" KEY_DEFAULT_RUNTIME = "defaultRuntimeSeconds" KEY_DURATION_SECONDS = "durationSeconds" diff --git a/homeassistant/components/rachio/entity.py b/homeassistant/components/rachio/entity.py index 27564f1caca..056abe9145b 100644 --- a/homeassistant/components/rachio/entity.py +++ b/homeassistant/components/rachio/entity.py @@ -70,6 +70,7 @@ class RachioHoseTimerEntity(CoordinatorEntity[RachioUpdateCoordinator]): manufacturer=DEFAULT_NAME, configuration_url="https://app.rach.io", ) + self._update_attr() @property def available(self) -> bool: diff --git a/homeassistant/components/rachio/switch.py b/homeassistant/components/rachio/switch.py index 0f696baad3a..1a8dbe42904 100644 --- a/homeassistant/components/rachio/switch.py +++ b/homeassistant/components/rachio/switch.py @@ -548,8 +548,6 @@ class RachioValve(RachioHoseTimerEntity, SwitchEntity): self._person = person self._base = base self._attr_unique_id = f"{self.id}-valve" - self._static_attrs = data[KEY_STATE][KEY_REPORTED_STATE] - self._attr_is_on = KEY_CURRENT_STATUS in self._static_attrs def turn_on(self, **kwargs: Any) -> None: """Turn on this valve.""" From 5702ab30596c6b43ae460d39bd626fedd3f4f006 Mon Sep 17 00:00:00 2001 From: Or Evron <20145882+orevron@users.noreply.github.com> Date: Thu, 18 Apr 2024 21:48:37 +0300 Subject: [PATCH 675/967] Add zhimi.fan.za3 to xiaomi_miio workaround unable to discover device (#108310) * add zhimi.fan.za3 to workaround fix unable to discover issue * Update __init__.py --------- Co-authored-by: Erik Montnemery --- homeassistant/components/xiaomi_miio/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index 35ee017286f..bea8d9b402f 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -299,6 +299,7 @@ async def async_create_miio_device_and_coordinator( # List of models requiring specific lazy_discover setting LAZY_DISCOVER_FOR_MODEL = { + "zhimi.fan.za3": True, "zhimi.fan.za5": True, "zhimi.airpurifier.za1": True, } From 05c37648c416a451eee6f07ecd138dee4d2984b4 Mon Sep 17 00:00:00 2001 From: Ryan Mattson Date: Thu, 18 Apr 2024 13:50:11 -0500 Subject: [PATCH 676/967] Add support for room sensor accessories assigned to a Honeywell (Lyric) Thermostat (#104343) * Add support for room sensor accessories. - Update coordinator to refresh and grab information about room sensor accessories assigned to a thermostat - Add sensor entities for room humidity and room temperature - Add devices to the registry for each room accessory - "via_device" these entities through the assigned thermostat. * fixed pre-commit issues. * PR suggestions - update docstring to reflect ownership by thermostat - fixed potential issue where a sensor would not be added if its temperature value was 0 * fix bad github merge * asyicio.gather futures for updating theromstat room stats --- homeassistant/components/lyric/__init__.py | 47 +++++++++++- homeassistant/components/lyric/sensor.py | 80 ++++++++++++++++++++- homeassistant/components/lyric/strings.json | 6 ++ 3 files changed, 131 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lyric/__init__.py b/homeassistant/components/lyric/__init__.py index 84ef3a2b7db..349e4f871a3 100644 --- a/homeassistant/components/lyric/__init__.py +++ b/homeassistant/components/lyric/__init__.py @@ -12,6 +12,7 @@ from aiolyric import Lyric from aiolyric.exceptions import LyricAuthenticationException, LyricException from aiolyric.objects.device import LyricDevice from aiolyric.objects.location import LyricLocation +from aiolyric.objects.priority import LyricAccessories, LyricRoom from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -77,6 +78,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: async with asyncio.timeout(60): await lyric.get_locations() + await asyncio.gather( + *( + lyric.get_thermostat_rooms(location.locationID, device.deviceID) + for location in lyric.locations + for device in location.devices + if device.deviceClass == "Thermostat" + ) + ) + except LyricAuthenticationException as exception: # Attempt to refresh the token before failing. # Honeywell appear to have issues keeping tokens saved. @@ -159,8 +169,43 @@ class LyricDeviceEntity(LyricEntity): def device_info(self) -> DeviceInfo: """Return device information about this Honeywell Lyric instance.""" return DeviceInfo( + identifiers={(dr.CONNECTION_NETWORK_MAC, self._mac_id)}, connections={(dr.CONNECTION_NETWORK_MAC, self._mac_id)}, manufacturer="Honeywell", model=self.device.deviceModel, - name=self.device.name, + name=f"{self.device.name} Thermostat", + ) + + +class LyricAccessoryEntity(LyricDeviceEntity): + """Defines a Honeywell Lyric accessory entity, a sub-device of a thermostat.""" + + def __init__( + self, + coordinator: DataUpdateCoordinator[Lyric], + location: LyricLocation, + device: LyricDevice, + room: LyricRoom, + accessory: LyricAccessories, + key: str, + ) -> None: + """Initialize the Honeywell Lyric accessory entity.""" + super().__init__(coordinator, location, device, key) + self._room = room + self._accessory = accessory + + @property + def device_info(self) -> DeviceInfo: + """Return device information about this Honeywell Lyric instance.""" + return DeviceInfo( + identifiers={ + ( + f"{dr.CONNECTION_NETWORK_MAC}_room_accessory", + f"{self._mac_id}_room{self._room.id}_accessory{self._accessory.id}", + ) + }, + manufacturer="Honeywell", + model="RCHTSENSOR", + name=f"{self._room.roomName} Sensor", + via_device=(dr.CONNECTION_NETWORK_MAC, self._mac_id), ) diff --git a/homeassistant/components/lyric/sensor.py b/homeassistant/components/lyric/sensor.py index 276336e02cc..64f60fa6611 100644 --- a/homeassistant/components/lyric/sensor.py +++ b/homeassistant/components/lyric/sensor.py @@ -9,6 +9,7 @@ from datetime import datetime, timedelta from aiolyric import Lyric from aiolyric.objects.device import LyricDevice from aiolyric.objects.location import LyricLocation +from aiolyric.objects.priority import LyricAccessories, LyricRoom from homeassistant.components.sensor import ( SensorDeviceClass, @@ -24,7 +25,7 @@ from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util import dt as dt_util -from . import LyricDeviceEntity +from . import LyricAccessoryEntity, LyricDeviceEntity from .const import ( DOMAIN, PRESET_HOLD_UNTIL, @@ -50,6 +51,14 @@ class LyricSensorEntityDescription(SensorEntityDescription): suitable_fn: Callable[[LyricDevice], bool] +@dataclass(frozen=True, kw_only=True) +class LyricSensorAccessoryEntityDescription(SensorEntityDescription): + """Class describing Honeywell Lyric room sensor entities.""" + + value_fn: Callable[[LyricRoom, LyricAccessories], StateType | datetime] + suitable_fn: Callable[[LyricRoom, LyricAccessories], bool] + + DEVICE_SENSORS: list[LyricSensorEntityDescription] = [ LyricSensorEntityDescription( key="indoor_temperature", @@ -109,6 +118,26 @@ DEVICE_SENSORS: list[LyricSensorEntityDescription] = [ ), ] +ACCESSORY_SENSORS: list[LyricSensorAccessoryEntityDescription] = [ + LyricSensorAccessoryEntityDescription( + key="room_temperature", + translation_key="room_temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda _, accessory: accessory.temperature, + suitable_fn=lambda _, accessory: accessory.type == "IndoorAirSensor", + ), + LyricSensorAccessoryEntityDescription( + key="room_humidity", + translation_key="room_humidity", + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, + value_fn=lambda room, _: room.roomAvgHumidity, + suitable_fn=lambda _, accessory: accessory.type == "IndoorAirSensor", + ), +] + def get_setpoint_status(status: str, time: str) -> str | None: """Get status of the setpoint.""" @@ -147,6 +176,18 @@ async def async_setup_entry( if device_sensor.suitable_fn(device) ) + async_add_entities( + LyricAccessorySensor( + coordinator, accessory_sensor, location, device, room, accessory + ) + for location in coordinator.data.locations + for device in location.devices + for room in coordinator.data.rooms_dict.get(device.macID, {}).values() + for accessory in room.accessories + for accessory_sensor in ACCESSORY_SENSORS + if accessory_sensor.suitable_fn(room, accessory) + ) + class LyricSensor(LyricDeviceEntity, SensorEntity): """Define a Honeywell Lyric sensor.""" @@ -178,3 +219,40 @@ class LyricSensor(LyricDeviceEntity, SensorEntity): def native_value(self) -> StateType | datetime: """Return the state.""" return self.entity_description.value_fn(self.device) + + +class LyricAccessorySensor(LyricAccessoryEntity, SensorEntity): + """Define a Honeywell Lyric sensor.""" + + entity_description: LyricSensorAccessoryEntityDescription + + def __init__( + self, + coordinator: DataUpdateCoordinator[Lyric], + description: LyricSensorAccessoryEntityDescription, + location: LyricLocation, + parentDevice: LyricDevice, + room: LyricRoom, + accessory: LyricAccessories, + ) -> None: + """Initialize.""" + super().__init__( + coordinator, + location, + parentDevice, + room, + accessory, + f"{parentDevice.macID}_room{room.id}_acc{accessory.id}_{description.key}", + ) + self.room = room + self.entity_description = description + if description.device_class == SensorDeviceClass.TEMPERATURE: + if parentDevice.units == "Fahrenheit": + self._attr_native_unit_of_measurement = UnitOfTemperature.FAHRENHEIT + else: + self._attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS + + @property + def native_value(self) -> StateType | datetime: + """Return the state.""" + return self.entity_description.value_fn(self._room, self._accessory) diff --git a/homeassistant/components/lyric/strings.json b/homeassistant/components/lyric/strings.json index 68bb6292f9e..739ad7fad68 100644 --- a/homeassistant/components/lyric/strings.json +++ b/homeassistant/components/lyric/strings.json @@ -41,6 +41,12 @@ }, "setpoint_status": { "name": "Setpoint status" + }, + "room_temperature": { + "name": "Room temperature" + }, + "room_humidity": { + "name": "Room humidity" } } }, From 1d6ae01baab69957e1bf7f26fc00564a4de9df9c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 18 Apr 2024 15:06:53 -0500 Subject: [PATCH 677/967] Handle Bluetooth adapters in a crashed state (#115790) * Skip bluetooth discovery for Bluetooth adapters in a crashed state * fixes * fixes * adjust * coverage * coverage * fix race --- .../components/bluetooth/__init__.py | 19 ++++++++- .../components/bluetooth/config_flow.py | 7 ++++ tests/components/bluetooth/conftest.py | 39 +++++++++++++++++++ .../components/bluetooth/test_config_flow.py | 16 ++++++++ tests/components/bluetooth/test_init.py | 23 +++++++++++ 5 files changed, 103 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 35d4b625942..560fb0663a8 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -196,6 +196,17 @@ async def _async_start_adapter_discovery( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the bluetooth integration.""" + if platform.system() == "Linux": + # Remove any config entries that are using the default address + # that were created from discovering adapters in a crashed state + # + # DEFAULT_ADDRESS is perfectly valid on MacOS but on + # Linux it means the adapter is not yet configured + # or crashed + for entry in list(hass.config_entries.async_entries(DOMAIN)): + if entry.unique_id == DEFAULT_ADDRESS: + await hass.config_entries.async_remove(entry.entry_id) + bluetooth_adapters = get_adapters() bluetooth_storage = BluetoothStorage(hass) slot_manager = BleakSlotManager() @@ -257,13 +268,19 @@ async def async_discover_adapters( adapters: dict[str, AdapterDetails], ) -> None: """Discover adapters and start flows.""" - if platform.system() == "Windows": + system = platform.system() + if system == "Windows": # We currently do not have a good way to detect if a bluetooth device is # available on Windows. We will just assume that it is not unless they # actively add it. return for adapter, details in adapters.items(): + if system == "Linux" and details[ADAPTER_ADDRESS] == DEFAULT_ADDRESS: + # DEFAULT_ADDRESS is perfectly valid on MacOS but on + # Linux it means the adapter is not yet configured + # or crashed so we should not try to start a flow for it. + continue discovery_flow.async_create_flow( hass, DOMAIN, diff --git a/homeassistant/components/bluetooth/config_flow.py b/homeassistant/components/bluetooth/config_flow.py index 6802bdc37c0..87038d48151 100644 --- a/homeassistant/components/bluetooth/config_flow.py +++ b/homeassistant/components/bluetooth/config_flow.py @@ -2,11 +2,13 @@ from __future__ import annotations +import platform from typing import Any, cast from bluetooth_adapters import ( ADAPTER_ADDRESS, ADAPTER_MANUFACTURER, + DEFAULT_ADDRESS, AdapterDetails, adapter_human_name, adapter_model, @@ -133,10 +135,15 @@ class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): bluetooth_adapters = get_adapters() await bluetooth_adapters.refresh() self._adapters = bluetooth_adapters.adapters + system = platform.system() unconfigured_adapters = [ adapter for adapter, details in self._adapters.items() if details[ADAPTER_ADDRESS] not in configured_addresses + # DEFAULT_ADDRESS is perfectly valid on MacOS but on + # Linux it means the adapter is not yet configured + # or crashed + and not (system == "Linux" and details[ADAPTER_ADDRESS] == DEFAULT_ADDRESS) ] if not unconfigured_adapters: ignored_adapters = len( diff --git a/tests/components/bluetooth/conftest.py b/tests/components/bluetooth/conftest.py index c1e040ccd49..d4056c1e38e 100644 --- a/tests/components/bluetooth/conftest.py +++ b/tests/components/bluetooth/conftest.py @@ -219,6 +219,45 @@ def two_adapters_fixture(): yield +@pytest.fixture(name="crashed_adapter") +def crashed_adapter_fixture(): + """Fixture that mocks one crashed adapter on Linux.""" + with ( + patch( + "homeassistant.components.bluetooth.platform.system", + return_value="Linux", + ), + patch( + "habluetooth.scanner.platform.system", + return_value="Linux", + ), + patch( + "bluetooth_adapters.systems.platform.system", + return_value="Linux", + ), + patch("habluetooth.scanner.SYSTEM", "Linux"), + patch( + "bluetooth_adapters.systems.linux.LinuxAdapters.refresh", + ), + patch( + "bluetooth_adapters.systems.linux.LinuxAdapters.adapters", + { + "hci0": { + "address": "00:00:00:00:00:00", + "hw_version": "usb:v1D6Bp0246d053F", + "passive_scan": True, + "sw_version": "homeassistant", + "manufacturer": None, + "product": None, + "product_id": None, + "vendor_id": None, + }, + }, + ), + ): + yield + + @pytest.fixture(name="one_adapter_old_bluez") def one_adapter_old_bluez(): """Fixture that mocks two adapters on Linux.""" diff --git a/tests/components/bluetooth/test_config_flow.py b/tests/components/bluetooth/test_config_flow.py index f9bbbcd2d0e..d044be76e6d 100644 --- a/tests/components/bluetooth/test_config_flow.py +++ b/tests/components/bluetooth/test_config_flow.py @@ -32,6 +32,9 @@ async def test_options_flow_disabled_not_setup( domain=DOMAIN, data={}, options={}, unique_id=DEFAULT_ADDRESS ) entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + ws_client = await hass_ws_client(hass) await ws_client.send_json( @@ -103,6 +106,19 @@ async def test_async_step_user_linux_one_adapter( assert len(mock_setup_entry.mock_calls) == 1 +async def test_async_step_user_linux_crashed_adapter( + hass: HomeAssistant, crashed_adapter: None +) -> None: + """Test setting up manually with one crashed adapter on Linux.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={}, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "no_adapters" + + async def test_async_step_user_linux_two_adapters( hass: HomeAssistant, two_adapters: None ) -> None: diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index e68ccc94d19..82fa0341966 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -2807,6 +2807,19 @@ async def test_can_unsetup_bluetooth_single_adapter_macos( await hass.async_block_till_done() +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) + entry.add_to_hass(hass) + await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() + assert not hass.config_entries.async_entries(bluetooth.DOMAIN) + + async def test_can_unsetup_bluetooth_single_adapter_linux( hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, @@ -2889,6 +2902,16 @@ async def test_auto_detect_bluetooth_adapters_linux_multiple( assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 2 +async def test_auto_detect_bluetooth_adapters_skips_crashed( + hass: HomeAssistant, crashed_adapter: None +) -> None: + """Test we skip crashed adapters on linux.""" + assert await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() + assert not hass.config_entries.async_entries(bluetooth.DOMAIN) + assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 0 + + async def test_auto_detect_bluetooth_adapters_linux_none_found( hass: HomeAssistant, ) -> None: From 814b7a4447d4af5155068525401e931d7051e3bd Mon Sep 17 00:00:00 2001 From: Matrix Date: Fri, 19 Apr 2024 09:14:14 +0800 Subject: [PATCH 678/967] Bump yolink-api to 0.4.3 (#115794) --- homeassistant/components/yolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yolink/manifest.json b/homeassistant/components/yolink/manifest.json index cd6759b5864..b7bd1d4784f 100644 --- a/homeassistant/components/yolink/manifest.json +++ b/homeassistant/components/yolink/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["auth", "application_credentials"], "documentation": "https://www.home-assistant.io/integrations/yolink", "iot_class": "cloud_push", - "requirements": ["yolink-api==0.4.2"] + "requirements": ["yolink-api==0.4.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 18c4d6a0076..a2ddb9cdb02 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2911,7 +2911,7 @@ yeelight==0.7.14 yeelightsunflower==0.0.10 # homeassistant.components.yolink -yolink-api==0.4.2 +yolink-api==0.4.3 # homeassistant.components.youless youless-api==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aeb38c28aa1..820dccee669 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2261,7 +2261,7 @@ yalexs==3.0.1 yeelight==0.7.14 # homeassistant.components.yolink -yolink-api==0.4.2 +yolink-api==0.4.3 # homeassistant.components.youless youless-api==1.0.1 From e62ae90d810235568cab80b1298428b5bcad94be Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 19 Apr 2024 05:24:10 +0200 Subject: [PATCH 679/967] Bump `accuweather` to version 3.0.0 (#115820) Bump accuweather to version 3.0.0 Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/accuweather/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/accuweather/manifest.json b/homeassistant/components/accuweather/manifest.json index fa651d98efd..24a8180eef8 100644 --- a/homeassistant/components/accuweather/manifest.json +++ b/homeassistant/components/accuweather/manifest.json @@ -8,6 +8,6 @@ "iot_class": "cloud_polling", "loggers": ["accuweather"], "quality_scale": "platinum", - "requirements": ["accuweather==2.1.1"], + "requirements": ["accuweather==3.0.0"], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index a2ddb9cdb02..89084e920ad 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -140,7 +140,7 @@ TwitterAPI==2.7.12 WSDiscovery==2.0.0 # homeassistant.components.accuweather -accuweather==2.1.1 +accuweather==3.0.0 # homeassistant.components.adax adax==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 820dccee669..61a1b02f151 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -119,7 +119,7 @@ Tami4EdgeAPI==2.1 WSDiscovery==2.0.0 # homeassistant.components.accuweather -accuweather==2.1.1 +accuweather==3.0.0 # homeassistant.components.adax adax==0.4.0 From 6b6324f48ec045c4a6b451a757ec2d2f0fb89244 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 19 Apr 2024 06:36:43 +0200 Subject: [PATCH 680/967] Bump aiounifi to v75 (#115819) --- 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 05dc2189908..305400a4b9d 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==74"], + "requirements": ["aiounifi==75"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 89084e920ad..c33bcedc61d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -383,7 +383,7 @@ aiotankerkoenig==0.4.1 aiotractive==0.5.6 # homeassistant.components.unifi -aiounifi==74 +aiounifi==75 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 61a1b02f151..28be0f25568 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -356,7 +356,7 @@ aiotankerkoenig==0.4.1 aiotractive==0.5.6 # homeassistant.components.unifi -aiounifi==74 +aiounifi==75 # homeassistant.components.vlc_telnet aiovlc==0.1.0 From 4cce75177a6abca12bf0603678b8fb3d940ef97a Mon Sep 17 00:00:00 2001 From: Joe Neuman Date: Thu, 18 Apr 2024 22:56:37 -0700 Subject: [PATCH 681/967] Add get_torrents service to qBittorrent integration (#106501) * Upgrade QBittorrent integration to show torrents This brings the QBittorrent integration to be more in line with the Transmission integration. It updates how the integration is written, along with adding sensors for Active Torrents, Inactive Torrents, Paused Torrents, Total Torrents, Seeding Torrents, Started Torrents. * Remove unused stuff * Correct name in comments * Make get torrents a service with a response * Add new sensors * remove service * Add service with response to get torrents list This adds a service with a response to be able to get the list of torrents within qBittorrent * update * update from rebase * Update strings.json * Update helpers.py * Update to satisfy lint * add func comment * fix lint issues * another update attempt * Fix helpers * Remove unneccesary part in services.yaml and add translations * Fix return * Add tests * Fix test * Improve tests * Fix issue from rebase * Add icon for get_torrents service * Make get torrents a service with a response * remove service * Add service with response to get torrents list This adds a service with a response to be able to get the list of torrents within qBittorrent * Update to satisfy lint * Handle multiple installed integrations * fix lint issue * Set return types for helper methods * Create the service method in async_setup * Add CONFIG_SCHEMA * Add get_all_torrents service * fix lint issues * Add return types and ServiceValidationError(s) * Fix naming * Update translations * Fix tests --- .../components/qbittorrent/__init__.py | 90 ++++++++++++++- homeassistant/components/qbittorrent/const.py | 7 ++ .../components/qbittorrent/coordinator.py | 27 ++++- .../components/qbittorrent/helpers.py | 48 ++++++++ .../components/qbittorrent/icons.json | 4 + .../components/qbittorrent/services.yaml | 35 ++++++ .../components/qbittorrent/strings.json | 37 ++++++ tests/components/qbittorrent/test_helpers.py | 108 ++++++++++++++++++ 8 files changed, 349 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/qbittorrent/services.yaml create mode 100644 tests/components/qbittorrent/test_helpers.py diff --git a/homeassistant/components/qbittorrent/__init__.py b/homeassistant/components/qbittorrent/__init__.py index 7b1a38b7e31..84f080c4d49 100644 --- a/homeassistant/components/qbittorrent/__init__.py +++ b/homeassistant/components/qbittorrent/__init__.py @@ -1,29 +1,111 @@ """The qbittorrent component.""" import logging +from typing import Any from qbittorrent.client import LoginRequired from requests.exceptions import RequestException from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + ATTR_DEVICE_ID, CONF_PASSWORD, CONF_URL, CONF_USERNAME, CONF_VERIFY_SSL, Platform, ) -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse +from homeassistant.exceptions import ConfigEntryNotReady, ServiceValidationError +from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN +from .const import ( + DOMAIN, + SERVICE_GET_ALL_TORRENTS, + SERVICE_GET_TORRENTS, + STATE_ATTR_ALL_TORRENTS, + STATE_ATTR_TORRENTS, + TORRENT_FILTER, +) from .coordinator import QBittorrentDataCoordinator -from .helpers import setup_client +from .helpers import format_torrents, setup_client _LOGGER = logging.getLogger(__name__) +CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) + PLATFORMS = [Platform.SENSOR] +CONF_ENTRY = "entry" + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up qBittorrent services.""" + + async def handle_get_torrents(service_call: ServiceCall) -> dict[str, Any] | None: + device_registry = dr.async_get(hass) + device_entry = device_registry.async_get(service_call.data[ATTR_DEVICE_ID]) + + if device_entry is None: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_device", + translation_placeholders={ + "device_id": service_call.data[ATTR_DEVICE_ID] + }, + ) + + entry_id = None + + for key, value in device_entry.identifiers: + if key == DOMAIN: + entry_id = value + break + else: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_entry_id", + translation_placeholders={"device_id": entry_id or ""}, + ) + + coordinator: QBittorrentDataCoordinator = hass.data[DOMAIN][entry_id] + items = await coordinator.get_torrents(service_call.data[TORRENT_FILTER]) + info = format_torrents(items) + return { + STATE_ATTR_TORRENTS: info, + } + + hass.services.async_register( + DOMAIN, + SERVICE_GET_TORRENTS, + handle_get_torrents, + supports_response=SupportsResponse.ONLY, + ) + + async def handle_get_all_torrents( + service_call: ServiceCall, + ) -> dict[str, Any] | None: + torrents = {} + + for key, value in hass.data[DOMAIN].items(): + coordinator: QBittorrentDataCoordinator = value + items = await coordinator.get_torrents(service_call.data[TORRENT_FILTER]) + torrents[key] = format_torrents(items) + + return { + STATE_ATTR_ALL_TORRENTS: torrents, + } + + hass.services.async_register( + DOMAIN, + SERVICE_GET_ALL_TORRENTS, + handle_get_all_torrents, + supports_response=SupportsResponse.ONLY, + ) + + return True + async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up qBittorrent from a config entry.""" diff --git a/homeassistant/components/qbittorrent/const.py b/homeassistant/components/qbittorrent/const.py index d8fe2c012a3..73e29d06f40 100644 --- a/homeassistant/components/qbittorrent/const.py +++ b/homeassistant/components/qbittorrent/const.py @@ -7,6 +7,13 @@ DOMAIN: Final = "qbittorrent" DEFAULT_NAME = "qBittorrent" DEFAULT_URL = "http://127.0.0.1:8080" +STATE_ATTR_TORRENTS = "torrents" +STATE_ATTR_ALL_TORRENTS = "all_torrents" + STATE_UP_DOWN = "up_down" STATE_SEEDING = "seeding" STATE_DOWNLOADING = "downloading" + +SERVICE_GET_TORRENTS = "get_torrents" +SERVICE_GET_ALL_TORRENTS = "get_all_torrents" +TORRENT_FILTER = "torrent_filter" diff --git a/homeassistant/components/qbittorrent/coordinator.py b/homeassistant/components/qbittorrent/coordinator.py index 32ce4cf9711..850bcf15ca2 100644 --- a/homeassistant/components/qbittorrent/coordinator.py +++ b/homeassistant/components/qbittorrent/coordinator.py @@ -10,7 +10,7 @@ from qbittorrent import Client from qbittorrent.client import LoginRequired from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryError +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN @@ -19,11 +19,18 @@ _LOGGER = logging.getLogger(__name__) class QBittorrentDataCoordinator(DataUpdateCoordinator[dict[str, Any]]): - """Coordinator for updating QBittorrent data.""" + """Coordinator for updating qBittorrent data.""" def __init__(self, hass: HomeAssistant, client: Client) -> None: """Initialize coordinator.""" self.client = client + # self.main_data: dict[str, int] = {} + self.total_torrents: dict[str, int] = {} + self.active_torrents: dict[str, int] = {} + self.inactive_torrents: dict[str, int] = {} + self.paused_torrents: dict[str, int] = {} + self.seeding_torrents: dict[str, int] = {} + self.started_torrents: dict[str, int] = {} super().__init__( hass, @@ -33,7 +40,21 @@ class QBittorrentDataCoordinator(DataUpdateCoordinator[dict[str, Any]]): ) async def _async_update_data(self) -> dict[str, Any]: + """Async method to update QBittorrent data.""" try: return await self.hass.async_add_executor_job(self.client.sync_main_data) except LoginRequired as exc: - raise ConfigEntryError("Invalid authentication") from 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: + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="login_error" + ) from exc + + return torrents diff --git a/homeassistant/components/qbittorrent/helpers.py b/homeassistant/components/qbittorrent/helpers.py index b9c29675473..bbe53765f8b 100644 --- a/homeassistant/components/qbittorrent/helpers.py +++ b/homeassistant/components/qbittorrent/helpers.py @@ -1,5 +1,8 @@ """Helper functions for qBittorrent.""" +from datetime import UTC, datetime +from typing import Any + from qbittorrent.client import Client @@ -10,3 +13,48 @@ def setup_client(url: str, username: str, password: str, verify_ssl: bool) -> Cl # Get an arbitrary attribute to test if connection succeeds client.get_alternative_speed_status() return client + + +def seconds_to_hhmmss(seconds) -> str: + """Convert seconds to HH:MM:SS format.""" + if seconds == 8640000: + return "None" + + minutes, seconds = divmod(seconds, 60) + hours, minutes = divmod(minutes, 60) + return f"{int(hours):02}:{int(minutes):02}:{int(seconds):02}" + + +def format_unix_timestamp(timestamp) -> str: + """Format a UNIX timestamp to a human-readable date.""" + dt_object = datetime.fromtimestamp(timestamp, tz=UTC) + return dt_object.isoformat() + + +def format_progress(torrent) -> str: + """Format the progress of a torrent.""" + progress = torrent["progress"] + progress = float(progress) * 100 + return f"{progress:.2f}" + + +def format_torrents(torrents: list[dict[str, Any]]) -> dict[str, dict[str, Any]]: + """Format a list of torrents.""" + value = {} + for torrent in torrents: + value[torrent["name"]] = format_torrent(torrent) + + return value + + +def format_torrent(torrent) -> dict[str, Any]: + """Format a single torrent.""" + value = {} + value["id"] = torrent["hash"] + value["added_date"] = format_unix_timestamp(torrent["added_on"]) + value["percent_done"] = format_progress(torrent) + value["status"] = torrent["state"] + value["eta"] = seconds_to_hhmmss(torrent["eta"]) + value["ratio"] = "{:.2f}".format(float(torrent["ratio"])) + + return value diff --git a/homeassistant/components/qbittorrent/icons.json b/homeassistant/components/qbittorrent/icons.json index bb458c751e1..68fc1020dae 100644 --- a/homeassistant/components/qbittorrent/icons.json +++ b/homeassistant/components/qbittorrent/icons.json @@ -8,5 +8,9 @@ "default": "mdi:cloud-upload" } } + }, + "services": { + "get_torrents": "mdi:file-arrow-up-down-outline", + "get_all_torrents": "mdi:file-arrow-up-down-outline" } } diff --git a/homeassistant/components/qbittorrent/services.yaml b/homeassistant/components/qbittorrent/services.yaml new file mode 100644 index 00000000000..f7fc6b95f64 --- /dev/null +++ b/homeassistant/components/qbittorrent/services.yaml @@ -0,0 +1,35 @@ +get_torrents: + fields: + device_id: + required: true + selector: + device: + integration: qbittorrent + torrent_filter: + required: true + example: "all" + default: "all" + selector: + select: + options: + - "active" + - "inactive" + - "paused" + - "all" + - "seeding" + - "started" +get_all_torrents: + fields: + torrent_filter: + required: true + example: "all" + default: "all" + selector: + select: + options: + - "active" + - "inactive" + - "paused" + - "all" + - "seeding" + - "started" diff --git a/homeassistant/components/qbittorrent/strings.json b/homeassistant/components/qbittorrent/strings.json index 8b20a3354dd..5376e929429 100644 --- a/homeassistant/components/qbittorrent/strings.json +++ b/homeassistant/components/qbittorrent/strings.json @@ -48,5 +48,42 @@ "name": "All torrents" } } + }, + "services": { + "get_torrents": { + "name": "Get torrents", + "description": "Gets a list of current torrents", + "fields": { + "device_id": { + "name": "[%key:common::config_flow::data::device%]", + "description": "Which service to grab the list from" + }, + "torrent_filter": { + "name": "Torrent filter", + "description": "What kind of torrents you want to return, such as All or Active." + } + } + }, + "get_all_torrents": { + "name": "Get all torrents", + "description": "Gets a list of current torrents from all instances of qBittorrent", + "fields": { + "torrent_filter": { + "name": "Torrent filter", + "description": "What kind of torrents you want to return, such as All or Active." + } + } + } + }, + "exceptions": { + "invalid_device": { + "message": "No device with id {device_id} was found" + }, + "invalid_entry_id": { + "message": "No entry with id {device_id} was found" + }, + "login_error": { + "message": "A login error occured. Please check you username and password." + } } } diff --git a/tests/components/qbittorrent/test_helpers.py b/tests/components/qbittorrent/test_helpers.py new file mode 100644 index 00000000000..b308cd33aec --- /dev/null +++ b/tests/components/qbittorrent/test_helpers.py @@ -0,0 +1,108 @@ +"""Test the qBittorrent helpers.""" + +from homeassistant.components.qbittorrent.helpers import ( + format_progress, + format_torrent, + format_torrents, + format_unix_timestamp, + seconds_to_hhmmss, +) +from homeassistant.core import HomeAssistant + + +async def test_seconds_to_hhmmss( + hass: HomeAssistant, +) -> None: + """Test the seconds_to_hhmmss function.""" + assert seconds_to_hhmmss(8640000) == "None" + assert seconds_to_hhmmss(3661) == "01:01:01" + + +async def test_format_unix_timestamp( + hass: HomeAssistant, +) -> None: + """Test the format_unix_timestamp function.""" + assert format_unix_timestamp(1640995200) == "2022-01-01T00:00:00+00:00" + + +async def test_format_progress( + hass: HomeAssistant, +) -> None: + """Test the format_progress function.""" + assert format_progress({"progress": 0.5}) == "50.00" + + +async def test_format_torrents( + hass: HomeAssistant, +) -> None: + """Test the format_torrents function.""" + torrents_data = [ + { + "name": "torrent1", + "hash": "hash1", + "added_on": 1640995200, + "progress": 0.5, + "state": "paused", + "eta": 86400, + "ratio": 1.0, + }, + { + "name": "torrent2", + "hash": "hash1", + "added_on": 1640995200, + "progress": 0.5, + "state": "paused", + "eta": 86400, + "ratio": 1.0, + }, + ] + + expected_result = { + "torrent1": { + "id": "hash1", + "added_date": "2022-01-01T00:00:00+00:00", + "percent_done": "50.00", + "status": "paused", + "eta": "24:00:00", + "ratio": "1.00", + }, + "torrent2": { + "id": "hash1", + "added_date": "2022-01-01T00:00:00+00:00", + "percent_done": "50.00", + "status": "paused", + "eta": "24:00:00", + "ratio": "1.00", + }, + } + + result = format_torrents(torrents_data) + + assert result == expected_result + + +async def test_format_torrent( + hass: HomeAssistant, +) -> None: + """Test the format_torrent function.""" + torrent_data = { + "hash": "hash1", + "added_on": 1640995200, + "progress": 0.5, + "state": "paused", + "eta": 86400, + "ratio": 1.0, + } + + expected_result = { + "id": "hash1", + "added_date": "2022-01-01T00:00:00+00:00", + "percent_done": "50.00", + "status": "paused", + "eta": "24:00:00", + "ratio": "1.00", + } + + result = format_torrent(torrent_data) + + assert result == expected_result From ed4f00279e87d0dd171489664d8fce05f80acab1 Mon Sep 17 00:00:00 2001 From: hahn-th <15319212+hahn-th@users.noreply.github.com> Date: Fri, 19 Apr 2024 08:09:48 +0200 Subject: [PATCH 682/967] Show default profiles in homematic cloud climate entity (#107348) * Default names for visible profiles * Increase number of devices in test * remove unnecessary check * Add testcase and split another into two * Add type annotations and docstring * Remove code which not belongs to the PR * Add myself to codeowners --- CODEOWNERS | 2 + .../components/homematicip_cloud/climate.py | 39 +- .../homematicip_cloud/manifest.json | 2 +- .../fixtures/homematicip_cloud.json | 344 ++++++++++++++++++ .../homematicip_cloud/test_climate.py | 89 ++++- .../homematicip_cloud/test_device.py | 2 +- 6 files changed, 462 insertions(+), 16 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index b2de3031cf8..98f52070ed1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -599,6 +599,8 @@ build.json @home-assistant/supervisor /tests/components/homekit_controller/ @Jc2k @bdraco /homeassistant/components/homematic/ @pvizeli /tests/components/homematic/ @pvizeli +/homeassistant/components/homematicip_cloud/ @hahn-th +/tests/components/homematicip_cloud/ @hahn-th /homeassistant/components/homewizard/ @DCSBL /tests/components/homewizard/ @DCSBL /homeassistant/components/honeywell/ @rdfurman @mkmer diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index b0eb2a9edfa..dd89efed1c9 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -13,6 +13,7 @@ from homematicip.aio.group import AsyncHeatingGroup from homematicip.base.enums import AbsenceType from homematicip.device import Switch from homematicip.functionalHomes import IndoorClimateHome +from homematicip.group import HeatingCoolingProfile from homeassistant.components.climate import ( PRESET_AWAY, @@ -35,6 +36,14 @@ from .hap import HomematicipHAP HEATING_PROFILES = {"PROFILE_1": 0, "PROFILE_2": 1, "PROFILE_3": 2} COOLING_PROFILES = {"PROFILE_4": 3, "PROFILE_5": 4, "PROFILE_6": 5} +NICE_PROFILE_NAMES = { + "PROFILE_1": "Default", + "PROFILE_2": "Alternative 1", + "PROFILE_3": "Alternative 2", + "PROFILE_4": "Cooling 1", + "PROFILE_5": "Cooling 2", + "PROFILE_6": "Cooling 3", +} ATTR_PRESET_END_TIME = "preset_end_time" PERMANENT_END_TIME = "permanent" @@ -164,8 +173,9 @@ class HomematicipHeatingGroup(HomematicipGenericEntity, ClimateEntity): return PRESET_ECO return ( - self._device.activeProfile.name - if self._device.activeProfile.name in self._device_profile_names + self._get_qualified_profile_name(self._device.activeProfile) + if self._get_qualified_profile_name(self._device.activeProfile) + in self._device_profile_names else None ) @@ -218,9 +228,6 @@ class HomematicipHeatingGroup(HomematicipGenericEntity, ClimateEntity): async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" - if preset_mode not in self.preset_modes: - return - if self._device.boostMode and preset_mode != PRESET_BOOST: await self._device.set_boost(False) if preset_mode == PRESET_BOOST: @@ -256,20 +263,30 @@ class HomematicipHeatingGroup(HomematicipGenericEntity, ClimateEntity): return self._home.get_functionalHome(IndoorClimateHome) @property - def _device_profiles(self) -> list[Any]: + def _device_profiles(self) -> list[HeatingCoolingProfile]: """Return the relevant profiles.""" return [ profile for profile in self._device.profiles - if profile.visible - and profile.name != "" - and profile.index in self._relevant_profile_group + if profile.visible and profile.index in self._relevant_profile_group ] @property def _device_profile_names(self) -> list[str]: """Return a collection of profile names.""" - return [profile.name for profile in self._device_profiles] + return [ + self._get_qualified_profile_name(profile) + for profile in self._device_profiles + ] + + def _get_qualified_profile_name(self, profile: HeatingCoolingProfile) -> str: + """Get a name for the given profile. If exists, this is the name of the profile.""" + if profile.name != "": + return profile.name + if profile.index in NICE_PROFILE_NAMES: + return NICE_PROFILE_NAMES[profile.index] + + return profile.index def _get_profile_idx_by_name(self, profile_name: str) -> int: """Return a profile index by name.""" @@ -277,7 +294,7 @@ class HomematicipHeatingGroup(HomematicipGenericEntity, ClimateEntity): index_name = [ profile.index for profile in self._device_profiles - if profile.name == profile_name + if self._get_qualified_profile_name(profile) == profile_name ] return relevant_index[index_name[0]] diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index 580a0f637c1..9da4e1bee05 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -1,7 +1,7 @@ { "domain": "homematicip_cloud", "name": "HomematicIP Cloud", - "codeowners": [], + "codeowners": ["@hahn-th"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homematicip_cloud", "iot_class": "cloud_push", diff --git a/tests/components/homematicip_cloud/fixtures/homematicip_cloud.json b/tests/components/homematicip_cloud/fixtures/homematicip_cloud.json index 83b5f8993bc..922601ca733 100644 --- a/tests/components/homematicip_cloud/fixtures/homematicip_cloud.json +++ b/tests/components/homematicip_cloud/fixtures/homematicip_cloud.json @@ -4791,6 +4791,59 @@ "type": "HEATING_THERMOSTAT", "updateState": "UP_TO_DATE" }, + "3014F71100000000ETRV0013": { + "automaticValveAdaptionNeeded": false, + "availableFirmwareVersion": "2.0.2", + "connectionType": "HMIP_RF", + "firmwareVersion": "2.0.2", + "firmwareVersionInteger": 131074, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F71100000000ETRV0013", + "dutyCycle": false, + "functionalChannelType": "DEVICE_OPERATIONLOCK", + "groupIndex": 0, + "groups": ["00000000-0000-0000-0000-000000000014"], + "index": 0, + "label": "", + "lowBat": false, + "operationLockActive": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -58, + "rssiPeerValue": -58, + "unreach": false, + "supportedOptionalFeatures": {} + }, + "1": { + "deviceId": "3014F71100000000ETRV0013", + "functionalChannelType": "HEATING_THERMOSTAT_CHANNEL", + "groupIndex": 1, + "groups": ["00000000-0000-0000-0005-000000000019"], + "index": 1, + "label": "", + "valveActualTemperature": 20.0, + "setPointTemperature": 5.0, + "temperatureOffset": 0.0, + "valvePosition": 0.0, + "valveState": "ADAPTION_DONE" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F71100000000ETRV0013", + "label": "Heizkörperthermostat4", + "lastStatusUpdate": 1524514007132, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 269, + "modelType": "HMIP-eTRV", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F71100000000ETRV0013", + "type": "HEATING_THERMOSTAT", + "updateState": "UP_TO_DATE" + }, "3014F7110000000000000014": { "automaticValveAdaptionNeeded": false, "availableFirmwareVersion": "2.0.2", @@ -8535,6 +8588,297 @@ "windowOpenTemperature": 5.0, "windowState": null }, + "00000000-0000-0000-0005-000000000019": { + "activeProfile": "PROFILE_1", + "actualTemperature": null, + "boostDuration": 15, + "boostMode": false, + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F71100000000ETRV0013" + } + ], + "controlMode": "AUTOMATIC", + "controllable": true, + "cooling": null, + "coolingAllowed": false, + "coolingIgnored": false, + "dutyCycle": false, + "ecoAllowed": true, + "ecoIgnored": false, + "externalClockCoolingTemperature": 23.0, + "externalClockEnabled": false, + "externalClockHeatingTemperature": 19.0, + "floorHeatingMode": "FLOOR_HEATING_STANDARD", + "homeId": "00000000-0000-0000-0000-000000000001", + "humidity": null, + "humidityLimitEnabled": true, + "humidityLimitValue": 60, + "id": "00000000-0000-0000-0005-000000000019", + "label": "Vorzimmer3", + "lastSetPointReachedTimestamp": 1557767559939, + "lastSetPointUpdatedTimestamp": 1557767559939, + "lastStatusUpdate": 1524514007132, + "lowBat": false, + "maxTemperature": 30.0, + "metaGroupId": "00000000-0000-0000-0000-000000000014", + "minTemperature": 5.0, + "partyMode": false, + "profiles": { + "PROFILE_1": { + "enabled": true, + "groupId": "00000000-0000-0000-0005-000000000019", + "index": "PROFILE_1", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000058", + "visible": true + }, + "PROFILE_2": { + "enabled": true, + "groupId": "00000000-0000-0000-0005-000000000019", + "index": "PROFILE_2", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000059", + "visible": true + }, + "PROFILE_3": { + "enabled": true, + "groupId": "00000000-0000-0000-0005-000000000019", + "index": "PROFILE_3", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000060", + "visible": false + }, + "PROFILE_4": { + "enabled": false, + "groupId": "00000000-0000-0000-0005-000000000019", + "index": "PROFILE_4", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000061", + "visible": true + }, + "PROFILE_5": { + "enabled": false, + "groupId": "00000000-0000-0000-0005-000000000019", + "index": "PROFILE_5", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000062", + "visible": false + }, + "PROFILE_6": { + "enabled": false, + "groupId": "00000000-0000-0000-0005-000000000019", + "index": "PROFILE_6", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000063", + "visible": false + } + }, + "setPointTemperature": 5.0, + "type": "HEATING", + "unreach": false, + "valvePosition": 0.0, + "valveSilentModeEnabled": false, + "valveSilentModeSupported": false, + "heatingFailureSupported": true, + "windowOpenTemperature": 5.0, + "windowState": null + }, + "00000000-0000-0000-0001-000000000019": { + "activeProfile": "PROFILE_1", + "actualTemperature": null, + "boostDuration": 15, + "boostMode": false, + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000013" + } + ], + "controlMode": "AUTOMATIC", + "controllable": true, + "cooling": null, + "coolingAllowed": false, + "coolingIgnored": false, + "dutyCycle": false, + "ecoAllowed": true, + "ecoIgnored": false, + "externalClockCoolingTemperature": 23.0, + "externalClockEnabled": false, + "externalClockHeatingTemperature": 19.0, + "floorHeatingMode": "FLOOR_HEATING_STANDARD", + "homeId": "00000000-0000-0000-0000-000000000001", + "humidity": null, + "humidityLimitEnabled": true, + "humidityLimitValue": 60, + "id": "00000000-0000-0000-0001-000000000019", + "label": "Vorzimmer", + "lastSetPointReachedTimestamp": 1557767559939, + "lastSetPointUpdatedTimestamp": 1557767559939, + "lastStatusUpdate": 1524514007132, + "lowBat": false, + "maxTemperature": 30.0, + "metaGroupId": "00000000-0000-0000-0000-000000000014", + "minTemperature": 5.0, + "partyMode": false, + "profiles": { + "PROFILE_1": { + "enabled": true, + "groupId": "00000000-0000-0000-0001-000000000019", + "index": "PROFILE_1", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000058", + "visible": true + }, + "PROFILE_2": { + "enabled": true, + "groupId": "00000000-0000-0000-0001-000000000019", + "index": "PROFILE_2", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000059", + "visible": false + }, + "PROFILE_3": { + "enabled": true, + "groupId": "00000000-0000-0000-0001-000000000019", + "index": "PROFILE_3", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000060", + "visible": false + }, + "PROFILE_4": { + "enabled": false, + "groupId": "00000000-0000-0000-0001-000000000019", + "index": "PROFILE_4", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000061", + "visible": true + }, + "PROFILE_5": { + "enabled": false, + "groupId": "00000000-0000-0000-0001-000000000019", + "index": "PROFILE_5", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000062", + "visible": false + }, + "PROFILE_6": { + "enabled": false, + "groupId": "00000000-0000-0000-0001-000000000019", + "index": "PROFILE_6", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000063", + "visible": false + } + }, + "setPointTemperature": 5.0, + "type": "HEATING", + "unreach": false, + "valvePosition": 0.0, + "valveSilentModeEnabled": false, + "valveSilentModeSupported": false, + "heatingFailureSupported": true, + "windowOpenTemperature": 5.0, + "windowState": null + }, + "00000000-0000-0001-0001-000000000019": { + "activeProfile": "PROFILE_1", + "actualTemperature": null, + "boostDuration": 15, + "boostMode": false, + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000013" + } + ], + "controlMode": "AUTOMATIC", + "controllable": true, + "cooling": null, + "coolingAllowed": false, + "coolingIgnored": false, + "dutyCycle": false, + "ecoAllowed": true, + "ecoIgnored": false, + "externalClockCoolingTemperature": 23.0, + "externalClockEnabled": false, + "externalClockHeatingTemperature": 19.0, + "floorHeatingMode": "FLOOR_HEATING_STANDARD", + "homeId": "00000000-0000-0000-0000-000000000001", + "humidity": null, + "humidityLimitEnabled": true, + "humidityLimitValue": 60, + "id": "00000000-0000-0001-0001-000000000019", + "label": "Vorzimmer2", + "lastSetPointReachedTimestamp": 1557767559939, + "lastSetPointUpdatedTimestamp": 1557767559939, + "lastStatusUpdate": 1524514007132, + "lowBat": false, + "maxTemperature": 30.0, + "metaGroupId": "00000000-0000-0000-0000-000000000014", + "minTemperature": 5.0, + "partyMode": false, + "profiles": { + "PROFILE_1": { + "enabled": true, + "groupId": "00000000-0000-0001-0001-000000000019", + "index": "PROFILE_1", + "name": "Testprofile", + "profileId": "00000000-0000-0000-0001-000000000058", + "visible": true + }, + "PROFILE_2": { + "enabled": true, + "groupId": "00000000-0000-0001-0001-000000000019", + "index": "PROFILE_2", + "name": "", + "profileId": "00000000-0000-0000-0001-000000000059", + "visible": true + }, + "PROFILE_3": { + "enabled": true, + "groupId": "00000000-0000-0001-0001-000000000019", + "index": "PROFILE_3", + "name": "", + "profileId": "00000000-0000-0000-0001-000000000060", + "visible": false + }, + "PROFILE_4": { + "enabled": false, + "groupId": "00000000-0000-0001-0001-000000000019", + "index": "PROFILE_4", + "name": "", + "profileId": "00000000-0000-0000-0001-000000000061", + "visible": true + }, + "PROFILE_5": { + "enabled": false, + "groupId": "00000000-0000-0001-0001-000000000019", + "index": "PROFILE_5", + "name": "", + "profileId": "00000000-0000-0000-0001-000000000062", + "visible": false + }, + "PROFILE_6": { + "enabled": false, + "groupId": "00000000-0000-0001-0001-000000000019", + "index": "PROFILE_6", + "name": "", + "profileId": "00000000-0000-0000-0001-000000000063", + "visible": false + } + }, + "setPointTemperature": 5.0, + "type": "HEATING", + "unreach": false, + "valvePosition": 0.0, + "valveSilentModeEnabled": false, + "valveSilentModeSupported": false, + "heatingFailureSupported": true, + "windowOpenTemperature": 5.0, + "windowState": null + }, "00000000-AAAA-0000-0000-000000000001": { "actualTemperature": 15.4, "channels": [ diff --git a/tests/components/homematicip_cloud/test_climate.py b/tests/components/homematicip_cloud/test_climate.py index 9ede89859dc..f175e2060df 100644 --- a/tests/components/homematicip_cloud/test_climate.py +++ b/tests/components/homematicip_cloud/test_climate.py @@ -1,6 +1,7 @@ """Tests for HomematicIP Cloud climate.""" import datetime +from unittest.mock import patch from homematicip.base.enums import AbsenceType from homematicip.functionalHomes import IndoorClimateHome @@ -15,7 +16,6 @@ from homeassistant.components.climate import ( PRESET_AWAY, PRESET_BOOST, PRESET_ECO, - PRESET_NONE, HVACAction, HVACMode, ) @@ -217,12 +217,14 @@ async def test_hmip_heating_group_heat( ha_state = hass.states.get(entity_id) assert ha_state.state == HVACMode.AUTO + # hvac mode "dry" is not available. expect a valueerror. await hass.services.async_call( "climate", "set_hvac_mode", {"entity_id": entity_id, "hvac_mode": "dry"}, blocking=True, ) + assert len(hmip_device.mock_calls) == service_call_counter + 24 # Only fire event from last async_manipulate_test_data available. assert hmip_device.mock_calls[-1][0] == "fire_update_event" @@ -429,14 +431,95 @@ async def test_hmip_heating_group_heat_with_radiator( assert ha_state.attributes["min_temp"] == 5.0 assert ha_state.attributes["max_temp"] == 30.0 assert ha_state.attributes["temperature"] == 5.0 - assert ha_state.attributes[ATTR_PRESET_MODE] is None + assert ha_state.attributes[ATTR_PRESET_MODE] == "Default" assert ha_state.attributes[ATTR_PRESET_MODES] == [ - PRESET_NONE, PRESET_BOOST, PRESET_ECO, + "Default", ] +async def test_hmip_heating_profile_default_name( + hass: HomeAssistant, default_mock_hap_factory +) -> None: + """Test visible profile 1 without a name should be displayed as 'Default'.""" + entity_id = "climate.vorzimmer3" + entity_name = "Vorzimmer3" + device_model = None + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["Heizkörperthermostat4"], + test_groups=[entity_name], + ) + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + assert hmip_device + assert ha_state.state == HVACMode.AUTO + assert ha_state.attributes[ATTR_PRESET_MODES] == [ + PRESET_BOOST, + PRESET_ECO, + "Default", + "Alternative 1", + ] + + +async def test_hmip_heating_profile_naming( + hass: HomeAssistant, default_mock_hap_factory +) -> None: + """Test Heating Profile Naming.""" + entity_id = "climate.vorzimmer2" + entity_name = "Vorzimmer2" + device_model = None + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["Heizkörperthermostat2"], + test_groups=[entity_name], + ) + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + assert hmip_device + assert ha_state.state == HVACMode.AUTO + assert ha_state.attributes[ATTR_PRESET_MODES] == [ + PRESET_BOOST, + PRESET_ECO, + "Testprofile", + "Alternative 1", + ] + + +async def test_hmip_heating_profile_name_not_in_list( + hass: HomeAssistant, default_mock_hap_factory +) -> None: + """Test set profile when profile is not in available profiles.""" + expected_profile = "Testprofile" + entity_id = "climate.vorzimmer2" + entity_name = "Vorzimmer2" + device_model = None + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["Heizkörperthermostat2"], + test_groups=[entity_name], + ) + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + with patch( + "homeassistant.components.homematicip_cloud.climate.NICE_PROFILE_NAMES", + return_value={}, + ): + await hass.services.async_call( + "climate", + "set_preset_mode", + {"entity_id": entity_id, "preset_mode": expected_profile}, + blocking=True, + ) + + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_PRESET_MODE] == expected_profile + + async def test_hmip_climate_services( hass: HomeAssistant, mock_hap_with_service ) -> None: diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py index 9fc1f518c64..fb7fe7d7deb 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) == 272 + assert len(mock_hap.hmip_device_by_entity_id) == 278 async def test_hmip_remove_device( From 79c9db408964c14345a4ba3f61e98d6bb5512b3d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 Apr 2024 02:43:02 -0500 Subject: [PATCH 683/967] Bump aiodiscover to 2.1.0 (#115823) --- 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 0d77b997e82..b8abd0a9919 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -15,7 +15,7 @@ "quality_scale": "internal", "requirements": [ "aiodhcpwatcher==1.0.0", - "aiodiscover==2.0.0", + "aiodiscover==2.1.0", "cached_ipaddress==0.3.0" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7f134b1a93d..ec6998055b0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,7 +1,7 @@ # Automatically generated by gen_requirements_all.py, do not edit aiodhcpwatcher==1.0.0 -aiodiscover==2.0.0 +aiodiscover==2.1.0 aiodns==3.2.0 aiohttp-fast-url-dispatcher==0.3.0 aiohttp-isal==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index c33bcedc61d..5724a8df371 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -222,7 +222,7 @@ aiocomelit==0.9.0 aiodhcpwatcher==1.0.0 # homeassistant.components.dhcp -aiodiscover==2.0.0 +aiodiscover==2.1.0 # homeassistant.components.dnsip aiodns==3.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 28be0f25568..c9a3668ca9d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -201,7 +201,7 @@ aiocomelit==0.9.0 aiodhcpwatcher==1.0.0 # homeassistant.components.dhcp -aiodiscover==2.0.0 +aiodiscover==2.1.0 # homeassistant.components.dnsip aiodns==3.2.0 From 8d7ef6ea9a043c87b7ceac251e355463e2cac957 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Apr 2024 12:11:08 +0200 Subject: [PATCH 684/967] Bump actions/upload-artifact from 4.3.1 to 4.3.2 (#115842) --- .github/workflows/builder.yml | 2 +- .github/workflows/ci.yaml | 18 +++++++++--------- .github/workflows/wheels.yml | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index f02a8bacce8..20ae68fbba4 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -69,7 +69,7 @@ jobs: run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T - - name: Upload translations - uses: actions/upload-artifact@v4.3.1 + uses: actions/upload-artifact@v4.3.2 with: name: translations path: translations.tar.gz diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a5bafa0c52d..c84ae4513cc 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -715,7 +715,7 @@ jobs: . venv/bin/activate python -m script.split_tests ${{ needs.info.outputs.test_group_count }} tests - name: Upload pytest_buckets - uses: actions/upload-artifact@v4.3.1 + uses: actions/upload-artifact@v4.3.2 with: name: pytest_buckets path: pytest_buckets.txt @@ -811,14 +811,14 @@ jobs: 2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt - name: Upload pytest output if: success() || failure() && steps.pytest-full.conclusion == 'failure' - uses: actions/upload-artifact@v4.3.1 + uses: actions/upload-artifact@v4.3.2 with: name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }} path: pytest-*.txt overwrite: true - name: Upload coverage artifact if: needs.info.outputs.skip_coverage != 'true' - uses: actions/upload-artifact@v4.3.1 + uses: actions/upload-artifact@v4.3.2 with: name: coverage-${{ matrix.python-version }}-${{ matrix.group }} path: coverage.xml @@ -933,7 +933,7 @@ jobs: 2>&1 | tee pytest-${{ matrix.python-version }}-${mariadb}.txt - name: Upload pytest output if: success() || failure() && steps.pytest-partial.conclusion == 'failure' - uses: actions/upload-artifact@v4.3.1 + uses: actions/upload-artifact@v4.3.2 with: name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ steps.pytest-partial.outputs.mariadb }} @@ -941,7 +941,7 @@ jobs: overwrite: true - name: Upload coverage artifact if: needs.info.outputs.skip_coverage != 'true' - uses: actions/upload-artifact@v4.3.1 + uses: actions/upload-artifact@v4.3.2 with: name: coverage-${{ matrix.python-version }}-${{ steps.pytest-partial.outputs.mariadb }} @@ -1056,7 +1056,7 @@ jobs: 2>&1 | tee pytest-${{ matrix.python-version }}-${postgresql}.txt - name: Upload pytest output if: success() || failure() && steps.pytest-partial.conclusion == 'failure' - uses: actions/upload-artifact@v4.3.1 + uses: actions/upload-artifact@v4.3.2 with: name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ steps.pytest-partial.outputs.postgresql }} @@ -1064,7 +1064,7 @@ jobs: overwrite: true - name: Upload coverage artifact if: needs.info.outputs.skip_coverage != 'true' - uses: actions/upload-artifact@v4.3.1 + uses: actions/upload-artifact@v4.3.2 with: name: coverage-${{ matrix.python-version }}-${{ steps.pytest-partial.outputs.postgresql }} @@ -1193,14 +1193,14 @@ jobs: 2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt - name: Upload pytest output if: success() || failure() && steps.pytest-partial.conclusion == 'failure' - uses: actions/upload-artifact@v4.3.1 + uses: actions/upload-artifact@v4.3.2 with: name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }} path: pytest-*.txt overwrite: true - name: Upload coverage artifact if: needs.info.outputs.skip_coverage != 'true' - uses: actions/upload-artifact@v4.3.1 + uses: actions/upload-artifact@v4.3.2 with: name: coverage-${{ matrix.python-version }}-${{ matrix.group }} path: coverage.xml diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 7102df0ae4d..c6ca78b7847 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -63,14 +63,14 @@ jobs: ) > .env_file - name: Upload env_file - uses: actions/upload-artifact@v4.3.1 + uses: actions/upload-artifact@v4.3.2 with: name: env_file path: ./.env_file overwrite: true - name: Upload requirements_diff - uses: actions/upload-artifact@v4.3.1 + uses: actions/upload-artifact@v4.3.2 with: name: requirements_diff path: ./requirements_diff.txt From f8738d92631c5397328c545f4295f0036988bf96 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Apr 2024 12:22:09 +0200 Subject: [PATCH 685/967] Bump actions/download-artifact from 4.1.4 to 4.1.5 (#115841) --- .github/workflows/builder.yml | 4 ++-- .github/workflows/ci.yaml | 6 +++--- .github/workflows/wheels.yml | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 20ae68fbba4..9d992608317 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -175,7 +175,7 @@ jobs: sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt - name: Download translations - uses: actions/download-artifact@v4.1.4 + uses: actions/download-artifact@v4.1.5 with: name: translations @@ -458,7 +458,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Download translations - uses: actions/download-artifact@v4.1.4 + uses: actions/download-artifact@v4.1.5 with: name: translations diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c84ae4513cc..5f186c32e9a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -776,7 +776,7 @@ jobs: run: | echo "::add-matcher::.github/workflows/matchers/pytest-slow.json" - name: Download pytest_buckets - uses: actions/download-artifact@v4.1.4 + uses: actions/download-artifact@v4.1.5 with: name: pytest_buckets - name: Compile English translations @@ -1088,7 +1088,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v4.1.2 - name: Download all coverage artifacts - uses: actions/download-artifact@v4.1.4 + uses: actions/download-artifact@v4.1.5 with: pattern: coverage-* - name: Upload coverage to Codecov @@ -1221,7 +1221,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v4.1.2 - name: Download all coverage artifacts - uses: actions/download-artifact@v4.1.4 + uses: actions/download-artifact@v4.1.5 with: pattern: coverage-* - name: Upload coverage to Codecov diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index c6ca78b7847..36a9fa1f839 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -91,12 +91,12 @@ jobs: uses: actions/checkout@v4.1.2 - name: Download env_file - uses: actions/download-artifact@v4.1.4 + uses: actions/download-artifact@v4.1.5 with: name: env_file - name: Download requirements_diff - uses: actions/download-artifact@v4.1.4 + uses: actions/download-artifact@v4.1.5 with: name: requirements_diff @@ -129,12 +129,12 @@ jobs: uses: actions/checkout@v4.1.2 - name: Download env_file - uses: actions/download-artifact@v4.1.4 + uses: actions/download-artifact@v4.1.5 with: name: env_file - name: Download requirements_diff - uses: actions/download-artifact@v4.1.4 + uses: actions/download-artifact@v4.1.5 with: name: requirements_diff From 5b082ed6691df748833bc5c0998e91eeffa538d9 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 19 Apr 2024 14:48:18 +0200 Subject: [PATCH 686/967] Add group tests with mixed domain entities (#115849) --- tests/components/group/test_init.py | 107 ++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py index 0f8d487b340..9c2f14f5d74 100644 --- a/tests/components/group/test_init.py +++ b/tests/components/group/test_init.py @@ -2,6 +2,7 @@ from __future__ import annotations +import asyncio from collections import OrderedDict from typing import Any from unittest.mock import patch @@ -15,11 +16,15 @@ from homeassistant.const import ( ATTR_ICON, EVENT_HOMEASSISTANT_START, SERVICE_RELOAD, + STATE_CLOSED, STATE_HOME, + STATE_LOCKED, STATE_NOT_HOME, STATE_OFF, STATE_ON, + STATE_OPEN, STATE_UNKNOWN, + STATE_UNLOCKED, ) from homeassistant.core import CoreState, HomeAssistant from homeassistant.helpers import entity_registry as er @@ -603,6 +608,108 @@ async def test_is_on(hass: HomeAssistant) -> None: assert not group.is_on(hass, "non.existing") +@pytest.mark.parametrize( + ( + "domains", + "states_old", + "states_new", + "state_ison_group_old", + "state_ison_group_new", + ), + [ + ( + ("light", "light"), + (STATE_ON, STATE_OFF), + (STATE_OFF, STATE_OFF), + (STATE_ON, True), + (STATE_OFF, False), + ), + ( + ("cover", "cover"), + (STATE_OPEN, STATE_CLOSED), + (STATE_CLOSED, STATE_CLOSED), + (STATE_OPEN, True), + (STATE_CLOSED, False), + ), + ( + ("lock", "lock"), + (STATE_UNLOCKED, STATE_LOCKED), + (STATE_LOCKED, STATE_LOCKED), + (STATE_UNLOCKED, True), + (STATE_LOCKED, False), + ), + ( + ("cover", "lock"), + (STATE_OPEN, STATE_LOCKED), + (STATE_CLOSED, STATE_LOCKED), + (STATE_ON, True), + (STATE_OFF, False), + ), + ( + ("cover", "lock"), + (STATE_OPEN, STATE_UNLOCKED), + (STATE_CLOSED, STATE_LOCKED), + (STATE_ON, True), + (STATE_OFF, False), + ), + ( + ("cover", "lock", "light"), + (STATE_OPEN, STATE_LOCKED, STATE_ON), + (STATE_CLOSED, STATE_LOCKED, STATE_OFF), + (STATE_ON, True), + (STATE_OFF, False), + ), + ], +) +async def test_is_on_and_state_mixed_domains( + hass: HomeAssistant, + domains: tuple[str,], + states_old: tuple[str,], + states_new: tuple[str,], + state_ison_group_old: tuple[str, bool], + state_ison_group_new: tuple[str, bool], +) -> None: + """Test is_on method with mixed domains.""" + count = len(domains) + entity_ids = [f"{domains[index]}.test_{index}" for index in range(count)] + for index in range(count): + hass.states.async_set(entity_ids[index], states_old[index]) + + assert not group.is_on(hass, "group.none") + await asyncio.gather( + *[async_setup_component(hass, domain, {}) for domain in set(domains)] + ) + assert await async_setup_component(hass, "group", {}) + await hass.async_block_till_done() + + test_group = await group.Group.async_create_group( + hass, + "init_group", + created_by_service=True, + entity_ids=entity_ids, + icon=None, + mode=None, + object_id=None, + order=None, + ) + await hass.async_block_till_done() + + # Assert on old state + state = hass.states.get(test_group.entity_id) + assert state is not None + assert state.state == state_ison_group_old[0] + assert group.is_on(hass, test_group.entity_id) == state_ison_group_old[1] + + # Switch and assert on new state + for index in range(count): + hass.states.async_set(entity_ids[index], states_new[index]) + await hass.async_block_till_done() + state = hass.states.get(test_group.entity_id) + assert state is not None + assert state.state == state_ison_group_new[0] + assert group.is_on(hass, test_group.entity_id) == state_ison_group_new[1] + + async def test_reloading_groups(hass: HomeAssistant) -> None: """Test reloading the group config.""" assert await async_setup_component( From b462fdbf51b3cdb82556155fd2b765b8b416c5d4 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 19 Apr 2024 15:52:09 +0200 Subject: [PATCH 687/967] Bump `gios` to version 4.0.0 (#115822) Bump gios to version 4.0.0 Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/gios/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gios/manifest.json b/homeassistant/components/gios/manifest.json index 2e33bc6741e..b509806d07f 100644 --- a/homeassistant/components/gios/manifest.json +++ b/homeassistant/components/gios/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["dacite", "gios"], "quality_scale": "platinum", - "requirements": ["gios==3.2.2"] + "requirements": ["gios==4.0.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5724a8df371..ea6fcac34c4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -934,7 +934,7 @@ georss-qld-bushfire-alert-client==0.7 getmac==0.9.4 # homeassistant.components.gios -gios==3.2.2 +gios==4.0.0 # homeassistant.components.gitter gitterpy==0.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c9a3668ca9d..24034b934b8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -766,7 +766,7 @@ georss-qld-bushfire-alert-client==0.7 getmac==0.9.4 # homeassistant.components.gios -gios==3.2.2 +gios==4.0.0 # homeassistant.components.glances glances-api==0.6.0 From ff83d9acfffec74bc8ecdc3c2fb4c879711c715a Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 19 Apr 2024 16:45:19 +0200 Subject: [PATCH 688/967] Add missing media_player features to Samsung TV (#115788) * add missing features * fix snapshot --- .../components/samsungtv/media_player.py | 16 +++++++++------- .../samsungtv/snapshots/test_init.ambr | 4 ++-- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 36715c44a9b..ff347431a4a 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -46,15 +46,17 @@ from .triggers.turn_on import async_get_turn_on_trigger SOURCES = {"TV": "KEY_TV", "HDMI": "KEY_HDMI"} SUPPORT_SAMSUNGTV = ( - MediaPlayerEntityFeature.PAUSE - | MediaPlayerEntityFeature.VOLUME_STEP - | MediaPlayerEntityFeature.VOLUME_MUTE - | MediaPlayerEntityFeature.PREVIOUS_TRACK - | MediaPlayerEntityFeature.SELECT_SOURCE - | MediaPlayerEntityFeature.NEXT_TRACK - | MediaPlayerEntityFeature.TURN_OFF + MediaPlayerEntityFeature.NEXT_TRACK + | MediaPlayerEntityFeature.PAUSE | MediaPlayerEntityFeature.PLAY | MediaPlayerEntityFeature.PLAY_MEDIA + | MediaPlayerEntityFeature.PREVIOUS_TRACK + | MediaPlayerEntityFeature.SELECT_SOURCE + | MediaPlayerEntityFeature.STOP + | MediaPlayerEntityFeature.TURN_OFF + | MediaPlayerEntityFeature.VOLUME_MUTE + | MediaPlayerEntityFeature.VOLUME_SET + | MediaPlayerEntityFeature.VOLUME_STEP ) diff --git a/tests/components/samsungtv/snapshots/test_init.ambr b/tests/components/samsungtv/snapshots/test_init.ambr index 404b9a6b3af..1b8cf4c999d 100644 --- a/tests/components/samsungtv/snapshots/test_init.ambr +++ b/tests/components/samsungtv/snapshots/test_init.ambr @@ -9,7 +9,7 @@ 'TV', 'HDMI', ]), - 'supported_features': , + 'supported_features': , }), 'context': , 'entity_id': 'media_player.any', @@ -51,7 +51,7 @@ 'original_name': None, 'platform': 'samsungtv', 'previous_unique_id': None, - 'supported_features': , + 'supported_features': , 'translation_key': None, 'unique_id': 'sample-entry-id', 'unit_of_measurement': None, From c108c7df3883d392d3dfd5714b7154aa058afe10 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 19 Apr 2024 17:38:39 +0200 Subject: [PATCH 689/967] Add reauth flow to Google Tasks (#109517) * Add reauth flow to Google Tasks * Update homeassistant/components/google_tasks/config_flow.py Co-authored-by: Jan-Philipp Benecke * Add tests * Reauth * Remove insta reauth * Fix --------- Co-authored-by: Jan-Philipp Benecke --- .../components/google_tasks/__init__.py | 14 +- .../components/google_tasks/config_flow.py | 47 +++++- .../components/google_tasks/const.py | 5 +- .../components/google_tasks/strings.json | 1 + tests/components/google_tasks/conftest.py | 1 + .../google_tasks/test_config_flow.py | 158 ++++++++++++++++-- tests/components/google_tasks/test_init.py | 2 +- 7 files changed, 204 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/google_tasks/__init__.py b/homeassistant/components/google_tasks/__init__.py index b62bd0fe5a2..29a1b20f2bc 100644 --- a/homeassistant/components/google_tasks/__init__.py +++ b/homeassistant/components/google_tasks/__init__.py @@ -2,12 +2,12 @@ from __future__ import annotations -from aiohttp import ClientError +from aiohttp import ClientError, ClientResponseError from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_entry_oauth2_flow from . import api @@ -18,8 +18,6 @@ PLATFORMS: list[Platform] = [Platform.TODO] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Google Tasks from a config entry.""" - hass.data.setdefault(DOMAIN, {}) - implementation = ( await config_entry_oauth2_flow.async_get_config_entry_implementation( hass, entry @@ -29,10 +27,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: auth = api.AsyncConfigEntryAuth(hass, session) try: await auth.async_get_access_token() + except ClientResponseError as err: + if 400 <= err.status < 500: + raise ConfigEntryAuthFailed( + "OAuth session is not valid, reauth required" + ) from err + raise ConfigEntryNotReady from err except ClientError as err: raise ConfigEntryNotReady from err - hass.data[DOMAIN][entry.entry_id] = auth + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = auth await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/google_tasks/config_flow.py b/homeassistant/components/google_tasks/config_flow.py index a8e283b55c8..a9ef5c7ff23 100644 --- a/homeassistant/components/google_tasks/config_flow.py +++ b/homeassistant/components/google_tasks/config_flow.py @@ -1,5 +1,6 @@ """Config flow for Google Tasks.""" +from collections.abc import Mapping import logging from typing import Any @@ -8,7 +9,7 @@ from googleapiclient.discovery import build from googleapiclient.errors import HttpError from googleapiclient.http import HttpRequest -from homeassistant.config_entries import ConfigFlowResult +from homeassistant.config_entries import ConfigEntry, ConfigFlowResult from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN from homeassistant.helpers import config_entry_oauth2_flow @@ -22,6 +23,8 @@ class OAuth2FlowHandler( DOMAIN = DOMAIN + reauth_entry: ConfigEntry | None = None + @property def logger(self) -> logging.Logger: """Return logger.""" @@ -39,11 +42,21 @@ class OAuth2FlowHandler( async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult: """Create an entry for the flow.""" + credentials = Credentials(token=data[CONF_TOKEN][CONF_ACCESS_TOKEN]) try: + user_resource = build( + "oauth2", + "v2", + credentials=credentials, + ) + user_resource_cmd: HttpRequest = user_resource.userinfo().get() + user_resource_info = await self.hass.async_add_executor_job( + user_resource_cmd.execute + ) resource = build( "tasks", "v1", - credentials=Credentials(token=data[CONF_TOKEN][CONF_ACCESS_TOKEN]), + credentials=credentials, ) cmd: HttpRequest = resource.tasklists().list() await self.hass.async_add_executor_job(cmd.execute) @@ -56,4 +69,32 @@ class OAuth2FlowHandler( except Exception: # pylint: disable=broad-except self.logger.exception("Unknown error occurred") return self.async_abort(reason="unknown") - return self.async_create_entry(title=self.flow_impl.name, data=data) + user_id = user_resource_info["id"] + if not self.reauth_entry: + await self.async_set_unique_id(user_id) + self._abort_if_unique_id_configured() + return self.async_create_entry(title=user_resource_info["name"], data=data) + + if self.reauth_entry.unique_id == user_id or not self.reauth_entry.unique_id: + return self.async_update_reload_and_abort( + self.reauth_entry, unique_id=user_id, data=data + ) + + return self.async_abort(reason="wrong_account") + + 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/google_tasks/const.py b/homeassistant/components/google_tasks/const.py index 87253486127..0cb04bf1d4e 100644 --- a/homeassistant/components/google_tasks/const.py +++ b/homeassistant/components/google_tasks/const.py @@ -6,7 +6,10 @@ DOMAIN = "google_tasks" OAUTH2_AUTHORIZE = "https://accounts.google.com/o/oauth2/v2/auth" OAUTH2_TOKEN = "https://oauth2.googleapis.com/token" -OAUTH2_SCOPES = ["https://www.googleapis.com/auth/tasks"] +OAUTH2_SCOPES = [ + "https://www.googleapis.com/auth/tasks", + "https://www.googleapis.com/auth/userinfo.profile", +] class TaskStatus(StrEnum): diff --git a/homeassistant/components/google_tasks/strings.json b/homeassistant/components/google_tasks/strings.json index 2cf15f0d93d..4479b34935e 100644 --- a/homeassistant/components/google_tasks/strings.json +++ b/homeassistant/components/google_tasks/strings.json @@ -18,6 +18,7 @@ "user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]", "access_not_configured": "Unable to access the Google API:\n\n{message}", "unknown": "[%key:common::config_flow::error::unknown%]", + "wrong_account": "Wrong account: Please authenticate with the right account.", "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]", "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]" diff --git a/tests/components/google_tasks/conftest.py b/tests/components/google_tasks/conftest.py index 87ddb2ed81d..7db78af6232 100644 --- a/tests/components/google_tasks/conftest.py +++ b/tests/components/google_tasks/conftest.py @@ -54,6 +54,7 @@ def mock_config_entry(token_entry: dict[str, Any]) -> MockConfigEntry: """Fixture for a config entry.""" return MockConfigEntry( domain=DOMAIN, + unique_id="123", data={ "auth_implementation": DOMAIN, "token": token_entry, diff --git a/tests/components/google_tasks/test_config_flow.py b/tests/components/google_tasks/test_config_flow.py index 24801959674..5b2d4f11fee 100644 --- a/tests/components/google_tasks/test_config_flow.py +++ b/tests/components/google_tasks/test_config_flow.py @@ -1,9 +1,11 @@ """Test the Google Tasks config flow.""" -from unittest.mock import patch +from collections.abc import Generator +from unittest.mock import Mock, patch from googleapiclient.errors import HttpError from httplib2 import Response +import pytest from homeassistant import config_entries from homeassistant.components.google_tasks.const import ( @@ -15,18 +17,37 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow -from tests.common import load_fixture +from tests.common import MockConfigEntry, load_fixture +from tests.test_util.aiohttp import AiohttpClientMocker CLIENT_ID = "1234" CLIENT_SECRET = "5678" +@pytest.fixture +def user_identifier() -> str: + """Return a unique user ID.""" + return "123" + + +@pytest.fixture +def setup_userinfo(user_identifier: str) -> Generator[Mock, None, None]: + """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 = { + "id": user_identifier, + "name": "Test Name", + } + yield mock + + async def test_full_flow( hass: HomeAssistant, hass_client_no_auth, - aioclient_mock, + aioclient_mock: AiohttpClientMocker, current_request_with_host, setup_credentials, + setup_userinfo, ) -> None: """Check full flow.""" result = await hass.config_entries.flow.async_init( @@ -44,7 +65,8 @@ async def test_full_flow( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" f"&state={state}" - "&scope=https://www.googleapis.com/auth/tasks" + "&scope=https://www.googleapis.com/auth/tasks+" + "https://www.googleapis.com/auth/userinfo.profile" "&access_type=offline&prompt=consent" ) @@ -63,14 +85,13 @@ async def test_full_flow( }, ) - with ( - patch( - "homeassistant.components.google_tasks.async_setup_entry", return_value=True - ) as mock_setup, - patch("homeassistant.components.google_tasks.config_flow.build"), - ): + with patch( + "homeassistant.components.google_tasks.async_setup_entry", return_value=True + ) as mock_setup: result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["result"].unique_id == "123" + assert result["result"].title == "Test Name" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(mock_setup.mock_calls) == 1 @@ -78,9 +99,10 @@ async def test_full_flow( async def test_api_not_enabled( hass: HomeAssistant, hass_client_no_auth, - aioclient_mock, + aioclient_mock: AiohttpClientMocker, current_request_with_host, setup_credentials, + setup_userinfo, ) -> None: """Check flow aborts if api is not enabled.""" result = await hass.config_entries.flow.async_init( @@ -98,7 +120,8 @@ async def test_api_not_enabled( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" f"&state={state}" - "&scope=https://www.googleapis.com/auth/tasks" + "&scope=https://www.googleapis.com/auth/tasks+" + "https://www.googleapis.com/auth/userinfo.profile" "&access_type=offline&prompt=consent" ) @@ -137,9 +160,10 @@ async def test_api_not_enabled( async def test_general_exception( hass: HomeAssistant, hass_client_no_auth, - aioclient_mock, + aioclient_mock: AiohttpClientMocker, current_request_with_host, setup_credentials, + setup_userinfo, ) -> None: """Check flow aborts if exception happens.""" result = await hass.config_entries.flow.async_init( @@ -157,7 +181,8 @@ async def test_general_exception( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" f"&state={state}" - "&scope=https://www.googleapis.com/auth/tasks" + "&scope=https://www.googleapis.com/auth/tasks+" + "https://www.googleapis.com/auth/userinfo.profile" "&access_type=offline&prompt=consent" ) @@ -184,3 +209,108 @@ async def test_general_exception( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" + + +@pytest.mark.parametrize( + ("user_identifier", "abort_reason", "resulting_access_token", "starting_unique_id"), + [ + ( + "123", + "reauth_successful", + "updated-access-token", + "123", + ), + ( + "123", + "reauth_successful", + "updated-access-token", + None, + ), + ( + "345", + "wrong_account", + "mock-access", + "123", + ), + ], +) +async def test_reauth( + hass: HomeAssistant, + hass_client_no_auth, + aioclient_mock: AiohttpClientMocker, + current_request_with_host, + setup_credentials, + setup_userinfo, + user_identifier: str, + abort_reason: str, + resulting_access_token: str, + starting_unique_id: str | None, +) -> None: + """Test the re-authentication case updates the correct config entry.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=starting_unique_id, + data={ + "token": { + "refresh_token": "mock-refresh-token", + "access_token": "mock-access", + } + }, + ) + config_entry.add_to_hass(hass) + + config_entry.async_start_reauth(hass) + await hass.async_block_till_done() + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + result = flows[0] + 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}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}" + "&scope=https://www.googleapis.com/auth/tasks+" + "https://www.googleapis.com/auth/userinfo.profile" + "&access_type=offline&prompt=consent" + ) + 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": "updated-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + with patch( + "homeassistant.components.google_tasks.async_setup_entry", return_value=True + ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + assert result["type"] == "abort" + assert result["reason"] == abort_reason + + assert config_entry.unique_id == "123" + assert "token" in config_entry.data + # Verify access token is refreshed + assert config_entry.data["token"]["access_token"] == resulting_access_token + assert config_entry.data["token"]["refresh_token"] == "mock-refresh-token" diff --git a/tests/components/google_tasks/test_init.py b/tests/components/google_tasks/test_init.py index 0abfce87133..1fe0e4a0c36 100644 --- a/tests/components/google_tasks/test_init.py +++ b/tests/components/google_tasks/test_init.py @@ -68,7 +68,7 @@ async def test_expired_token_refresh_success( ( time.time() - 3600, http.HTTPStatus.UNAUTHORIZED, - ConfigEntryState.SETUP_RETRY, # Will trigger reauth in the future + ConfigEntryState.SETUP_ERROR, ), ( time.time() - 3600, From cc2e0fd9213e38de0dc1262306c019952d5900db Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 19 Apr 2024 18:18:32 +0200 Subject: [PATCH 690/967] Make Withings recoverable after internet outage (#115124) --- homeassistant/components/withings/__init__.py | 7 +- tests/components/withings/test_init.py | 105 ++++++++++++++++++ 2 files changed, 111 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index 1fe85f180da..0b86a2b5201 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -12,6 +12,7 @@ from dataclasses import dataclass, field from datetime import timedelta from typing import TYPE_CHECKING, Any, cast +from aiohttp import ClientError from aiohttp.hdrs import METH_POST from aiohttp.web import Request, Response from aiowithings import NotificationCategory, WithingsClient @@ -274,7 +275,11 @@ class WithingsWebhookManager: async def async_unsubscribe_webhooks(client: WithingsClient) -> None: """Unsubscribe to all Withings webhooks.""" - current_webhooks = await client.list_notification_configurations() + try: + current_webhooks = await client.list_notification_configurations() + except ClientError: + LOGGER.exception("Error when unsubscribing webhooks") + return for webhook_configuration in current_webhooks: LOGGER.debug( diff --git a/tests/components/withings/test_init.py b/tests/components/withings/test_init.py index ff0a098a7cb..3ade0fb7c3a 100644 --- a/tests/components/withings/test_init.py +++ b/tests/components/withings/test_init.py @@ -5,6 +5,7 @@ from typing import Any from unittest.mock import AsyncMock, patch from urllib.parse import urlparse +from aiohttp import ClientConnectionError from aiohttp.hdrs import METH_HEAD from aiowithings import ( NotificationCategory, @@ -425,6 +426,110 @@ async def test_cloud_disconnect( assert withings.subscribe_notification.call_count == 12 +async def test_internet_disconnect( + hass: HomeAssistant, + withings: AsyncMock, + webhook_config_entry: MockConfigEntry, + hass_client_no_auth: ClientSessionGenerator, + freezer: FrozenDateTimeFactory, +) -> None: + """Test we can recover from internet disconnects.""" + await mock_cloud(hass) + await hass.async_block_till_done() + + with ( + patch("homeassistant.components.cloud.async_is_logged_in", return_value=True), + patch.object(cloud, "async_is_connected", return_value=True), + patch.object(cloud, "async_active_subscription", return_value=True), + patch( + "homeassistant.components.cloud.async_create_cloudhook", + return_value="https://hooks.nabu.casa/ABCD", + ), + patch( + "homeassistant.components.withings.async_get_config_entry_implementation", + ), + patch( + "homeassistant.components.cloud.async_delete_cloudhook", + ), + patch( + "homeassistant.components.withings.webhook_generate_url", + ), + ): + await setup_integration(hass, webhook_config_entry) + await prepare_webhook_setup(hass, freezer) + + assert cloud.async_active_subscription(hass) is True + assert cloud.async_is_connected(hass) is True + assert withings.revoke_notification_configurations.call_count == 3 + assert withings.subscribe_notification.call_count == 6 + + await hass.async_block_till_done() + + withings.list_notification_configurations.side_effect = ClientConnectionError + + async_mock_cloud_connection_status(hass, False) + await hass.async_block_till_done() + + assert withings.revoke_notification_configurations.call_count == 3 + + async_mock_cloud_connection_status(hass, True) + await hass.async_block_till_done() + + assert withings.subscribe_notification.call_count == 12 + + +async def test_cloud_disconnect_retry( + hass: HomeAssistant, + withings: AsyncMock, + webhook_config_entry: MockConfigEntry, + hass_client_no_auth: ClientSessionGenerator, + freezer: FrozenDateTimeFactory, +) -> None: + """Test we retry to create webhook connection again after cloud disconnects.""" + await mock_cloud(hass) + await hass.async_block_till_done() + + with ( + patch("homeassistant.components.cloud.async_is_logged_in", return_value=True), + patch.object(cloud, "async_is_connected", return_value=True), + patch.object( + cloud, "async_active_subscription", return_value=True + ) as mock_async_active_subscription, + patch( + "homeassistant.components.cloud.async_create_cloudhook", + return_value="https://hooks.nabu.casa/ABCD", + ), + patch( + "homeassistant.components.withings.async_get_config_entry_implementation", + ), + patch( + "homeassistant.components.cloud.async_delete_cloudhook", + ), + patch( + "homeassistant.components.withings.webhook_generate_url", + ), + ): + await setup_integration(hass, webhook_config_entry) + await prepare_webhook_setup(hass, freezer) + + assert cloud.async_active_subscription(hass) is True + assert cloud.async_is_connected(hass) is True + assert mock_async_active_subscription.call_count == 3 + + await hass.async_block_till_done() + + async_mock_cloud_connection_status(hass, False) + await hass.async_block_till_done() + + assert mock_async_active_subscription.call_count == 3 + + freezer.tick(timedelta(seconds=30)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert mock_async_active_subscription.call_count == 4 + + @pytest.mark.parametrize( ("body", "expected_code"), [ From 18d6581523cba47c7910fcdaa8f6c8de41c7e48e Mon Sep 17 00:00:00 2001 From: avee87 <6134677+avee87@users.noreply.github.com> Date: Fri, 19 Apr 2024 17:21:21 +0100 Subject: [PATCH 691/967] Fix Hyperion light not updating state (#115389) --- homeassistant/components/hyperion/sensor.py | 4 ++-- tests/components/hyperion/test_sensor.py | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hyperion/sensor.py b/homeassistant/components/hyperion/sensor.py index f537c282686..ad972806ae5 100644 --- a/homeassistant/components/hyperion/sensor.py +++ b/homeassistant/components/hyperion/sensor.py @@ -191,13 +191,13 @@ class HyperionVisiblePrioritySensor(HyperionSensor): if priority[KEY_COMPONENTID] == "COLOR": state_value = priority[KEY_VALUE][KEY_RGB] else: - state_value = priority[KEY_OWNER] + state_value = priority.get(KEY_OWNER) attrs = { "component_id": priority[KEY_COMPONENTID], "origin": priority[KEY_ORIGIN], "priority": priority[KEY_PRIORITY], - "owner": priority[KEY_OWNER], + "owner": priority.get(KEY_OWNER), } if priority[KEY_COMPONENTID] == "COLOR": diff --git a/tests/components/hyperion/test_sensor.py b/tests/components/hyperion/test_sensor.py index 65991b4b7e1..8900db177fc 100644 --- a/tests/components/hyperion/test_sensor.py +++ b/tests/components/hyperion/test_sensor.py @@ -159,7 +159,6 @@ async def test_visible_effect_state_changes(hass: HomeAssistant) -> None: KEY_ACTIVE: True, KEY_COMPONENTID: "COLOR", KEY_ORIGIN: "System", - KEY_OWNER: "System", KEY_PRIORITY: 250, KEY_VALUE: {KEY_RGB: [0, 0, 0]}, KEY_VISIBLE: True, From ebbcad17c64bf8eee3b493beef7b89d43811afde Mon Sep 17 00:00:00 2001 From: slyoldfox Date: Fri, 19 Apr 2024 18:22:12 +0200 Subject: [PATCH 692/967] Add scheduled mode to renault charge mode (#115427) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- homeassistant/components/renault/select.py | 2 +- tests/components/renault/const.py | 21 ++++++++++++++++--- .../renault/snapshots/test_select.ambr | 12 +++++++++++ 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/renault/select.py b/homeassistant/components/renault/select.py index f6c8f73d24b..eb79e197937 100644 --- a/homeassistant/components/renault/select.py +++ b/homeassistant/components/renault/select.py @@ -71,6 +71,6 @@ SENSOR_TYPES: tuple[RenaultSelectEntityDescription, ...] = ( coordinator="charge_mode", data_key="chargeMode", translation_key="charge_mode", - options=["always", "always_charging", "schedule_mode"], + options=["always", "always_charging", "schedule_mode", "scheduled"], ), ) diff --git a/tests/components/renault/const.py b/tests/components/renault/const.py index d849c658149..19c40f6ec20 100644 --- a/tests/components/renault/const.py +++ b/tests/components/renault/const.py @@ -127,7 +127,12 @@ MOCK_VEHICLES = { { ATTR_ENTITY_ID: "select.reg_number_charge_mode", ATTR_ICON: "mdi:calendar-remove", - ATTR_OPTIONS: ["always", "always_charging", "schedule_mode"], + ATTR_OPTIONS: [ + "always", + "always_charging", + "schedule_mode", + "scheduled", + ], ATTR_STATE: "always", ATTR_UNIQUE_ID: "vf1aaaaa555777999_charge_mode", }, @@ -363,7 +368,12 @@ MOCK_VEHICLES = { { ATTR_ENTITY_ID: "select.reg_number_charge_mode", ATTR_ICON: "mdi:calendar-clock", - ATTR_OPTIONS: ["always", "always_charging", "schedule_mode"], + ATTR_OPTIONS: [ + "always", + "always_charging", + "schedule_mode", + "scheduled", + ], ATTR_STATE: "schedule_mode", ATTR_UNIQUE_ID: "vf1aaaaa555777999_charge_mode", }, @@ -599,7 +609,12 @@ MOCK_VEHICLES = { { ATTR_ENTITY_ID: "select.reg_number_charge_mode", ATTR_ICON: "mdi:calendar-remove", - ATTR_OPTIONS: ["always", "always_charging", "schedule_mode"], + ATTR_OPTIONS: [ + "always", + "always_charging", + "schedule_mode", + "scheduled", + ], ATTR_STATE: "always", ATTR_UNIQUE_ID: "vf1aaaaa555777123_charge_mode", }, diff --git a/tests/components/renault/snapshots/test_select.ambr b/tests/components/renault/snapshots/test_select.ambr index 7e8356ee070..0722cb5cab3 100644 --- a/tests/components/renault/snapshots/test_select.ambr +++ b/tests/components/renault/snapshots/test_select.ambr @@ -82,6 +82,7 @@ 'always', 'always_charging', 'schedule_mode', + 'scheduled', ]), }), 'config_entry_id': , @@ -121,6 +122,7 @@ 'always', 'always_charging', 'schedule_mode', + 'scheduled', ]), }), 'context': , @@ -175,6 +177,7 @@ 'always', 'always_charging', 'schedule_mode', + 'scheduled', ]), }), 'config_entry_id': , @@ -214,6 +217,7 @@ 'always', 'always_charging', 'schedule_mode', + 'scheduled', ]), }), 'context': , @@ -268,6 +272,7 @@ 'always', 'always_charging', 'schedule_mode', + 'scheduled', ]), }), 'config_entry_id': , @@ -307,6 +312,7 @@ 'always', 'always_charging', 'schedule_mode', + 'scheduled', ]), }), 'context': , @@ -401,6 +407,7 @@ 'always', 'always_charging', 'schedule_mode', + 'scheduled', ]), }), 'config_entry_id': , @@ -440,6 +447,7 @@ 'always', 'always_charging', 'schedule_mode', + 'scheduled', ]), }), 'context': , @@ -494,6 +502,7 @@ 'always', 'always_charging', 'schedule_mode', + 'scheduled', ]), }), 'config_entry_id': , @@ -533,6 +542,7 @@ 'always', 'always_charging', 'schedule_mode', + 'scheduled', ]), }), 'context': , @@ -587,6 +597,7 @@ 'always', 'always_charging', 'schedule_mode', + 'scheduled', ]), }), 'config_entry_id': , @@ -626,6 +637,7 @@ 'always', 'always_charging', 'schedule_mode', + 'scheduled', ]), }), 'context': , From 4529268544d88aafd315ade68e0a32e3e0ce281f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 Apr 2024 11:24:54 -0500 Subject: [PATCH 693/967] Ensure scripts with timeouts of zero timeout immediately (#115830) --- homeassistant/helpers/script.py | 25 ++++- tests/helpers/test_script.py | 178 ++++++++++++++++++++++++++++++++ 2 files changed, 198 insertions(+), 5 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index ea5cc3e571a..62c781ae629 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -650,6 +650,12 @@ class _ScriptRun: # check if condition already okay if condition.async_template(self._hass, wait_template, self._variables, False): self._variables["wait"]["completed"] = True + self._changed() + return + + if timeout == 0: + self._changed() + self._async_handle_timeout() return futures, timeout_handle, timeout_future = self._async_futures_with_timeout( @@ -1078,6 +1084,11 @@ class _ScriptRun: self._variables["wait"] = {"remaining": timeout, "trigger": None} trace_set_result(wait=self._variables["wait"]) + if timeout == 0: + self._changed() + self._async_handle_timeout() + return + futures, timeout_handle, timeout_future = self._async_futures_with_timeout( timeout ) @@ -1108,6 +1119,14 @@ class _ScriptRun: futures, timeout_handle, timeout_future, remove_triggers ) + def _async_handle_timeout(self) -> None: + """Handle timeout.""" + self._variables["wait"]["remaining"] = 0.0 + if not self._action.get(CONF_CONTINUE_ON_TIMEOUT, True): + self._log(_TIMEOUT_MSG) + trace_set_result(wait=self._variables["wait"], timeout=True) + raise _AbortScript from TimeoutError() + async def _async_wait_with_optional_timeout( self, futures: list[asyncio.Future[None]], @@ -1118,11 +1137,7 @@ class _ScriptRun: try: await asyncio.wait(futures, return_when=asyncio.FIRST_COMPLETED) if timeout_future and timeout_future.done(): - self._variables["wait"]["remaining"] = 0.0 - if not self._action.get(CONF_CONTINUE_ON_TIMEOUT, True): - self._log(_TIMEOUT_MSG) - trace_set_result(wait=self._variables["wait"], timeout=True) - raise _AbortScript from TimeoutError() + self._async_handle_timeout() finally: if timeout_future and not timeout_future.done() and timeout_handle: timeout_handle.cancel() diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 9d8170f9953..3d662e772e8 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -1311,6 +1311,184 @@ async def test_wait_timeout( assert_action_trace(expected_trace) +@pytest.mark.parametrize( + "timeout_param", [0, "{{ 0 }}", {"minutes": 0}, {"minutes": "{{ 0 }}"}] +) +async def test_wait_trigger_with_zero_timeout( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, timeout_param: int | str +) -> None: + """Test the wait trigger with zero timeout option.""" + event = "test_event" + events = async_capture_events(hass, event) + action = { + "wait_for_trigger": { + "platform": "state", + "entity_id": "switch.test", + "to": "off", + } + } + action["timeout"] = timeout_param + action["continue_on_timeout"] = True + sequence = cv.SCRIPT_SCHEMA([action, {"event": event}]) + sequence = await script.async_validate_actions_config(hass, sequence) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + wait_started_flag = async_watch_for_action(script_obj, "wait") + hass.states.async_set("switch.test", "on") + hass.async_create_task(script_obj.async_run(context=Context())) + + try: + await asyncio.wait_for(wait_started_flag.wait(), 1) + except (AssertionError, TimeoutError): + await script_obj.async_stop() + raise + + assert not script_obj.is_running + assert len(events) == 1 + assert "(timeout: 0:00:00)" in caplog.text + + variable_wait = {"wait": {"trigger": None, "remaining": 0.0}} + expected_trace = { + "0": [ + { + "result": variable_wait, + "variables": variable_wait, + } + ], + "1": [{"result": {"event": "test_event", "event_data": {}}}], + } + assert_action_trace(expected_trace) + + +@pytest.mark.parametrize( + "timeout_param", [0, "{{ 0 }}", {"minutes": 0}, {"minutes": "{{ 0 }}"}] +) +async def test_wait_trigger_matches_with_zero_timeout( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, timeout_param: int | str +) -> None: + """Test the wait trigger that matches with zero timeout option.""" + event = "test_event" + events = async_capture_events(hass, event) + action = { + "wait_for_trigger": { + "platform": "state", + "entity_id": "switch.test", + "to": "off", + } + } + action["timeout"] = timeout_param + action["continue_on_timeout"] = True + sequence = cv.SCRIPT_SCHEMA([action, {"event": event}]) + sequence = await script.async_validate_actions_config(hass, sequence) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + wait_started_flag = async_watch_for_action(script_obj, "wait") + hass.states.async_set("switch.test", "off") + hass.async_create_task(script_obj.async_run(context=Context())) + + try: + await asyncio.wait_for(wait_started_flag.wait(), 1) + except (AssertionError, TimeoutError): + await script_obj.async_stop() + raise + + assert not script_obj.is_running + assert len(events) == 1 + assert "(timeout: 0:00:00)" in caplog.text + + variable_wait = {"wait": {"trigger": None, "remaining": 0.0}} + expected_trace = { + "0": [ + { + "result": variable_wait, + "variables": variable_wait, + } + ], + "1": [{"result": {"event": "test_event", "event_data": {}}}], + } + assert_action_trace(expected_trace) + + +@pytest.mark.parametrize( + "timeout_param", [0, "{{ 0 }}", {"minutes": 0}, {"minutes": "{{ 0 }}"}] +) +async def test_wait_template_with_zero_timeout( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, timeout_param: int | str +) -> None: + """Test the wait template with zero timeout option.""" + event = "test_event" + events = async_capture_events(hass, event) + action = {"wait_template": "{{ states.switch.test.state == 'off' }}"} + action["timeout"] = timeout_param + action["continue_on_timeout"] = True + sequence = cv.SCRIPT_SCHEMA([action, {"event": event}]) + sequence = await script.async_validate_actions_config(hass, sequence) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + wait_started_flag = async_watch_for_action(script_obj, "wait") + hass.states.async_set("switch.test", "on") + hass.async_create_task(script_obj.async_run(context=Context())) + + try: + await asyncio.wait_for(wait_started_flag.wait(), 1) + except (AssertionError, TimeoutError): + await script_obj.async_stop() + raise + + assert not script_obj.is_running + assert len(events) == 1 + assert "(timeout: 0:00:00)" in caplog.text + variable_wait = {"wait": {"completed": False, "remaining": 0.0}} + expected_trace = { + "0": [ + { + "result": variable_wait, + "variables": variable_wait, + } + ], + "1": [{"result": {"event": "test_event", "event_data": {}}}], + } + assert_action_trace(expected_trace) + + +@pytest.mark.parametrize( + "timeout_param", [0, "{{ 0 }}", {"minutes": 0}, {"minutes": "{{ 0 }}"}] +) +async def test_wait_template_matches_with_zero_timeout( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, timeout_param: int | str +) -> None: + """Test the wait template that matches with zero timeout option.""" + event = "test_event" + events = async_capture_events(hass, event) + action = {"wait_template": "{{ states.switch.test.state == 'off' }}"} + action["timeout"] = timeout_param + action["continue_on_timeout"] = True + sequence = cv.SCRIPT_SCHEMA([action, {"event": event}]) + sequence = await script.async_validate_actions_config(hass, sequence) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + wait_started_flag = async_watch_for_action(script_obj, "wait") + hass.states.async_set("switch.test", "off") + hass.async_create_task(script_obj.async_run(context=Context())) + + try: + await asyncio.wait_for(wait_started_flag.wait(), 1) + except (AssertionError, TimeoutError): + await script_obj.async_stop() + raise + + assert not script_obj.is_running + assert len(events) == 1 + assert "(timeout: 0:00:00)" in caplog.text + variable_wait = {"wait": {"completed": True, "remaining": 0.0}} + expected_trace = { + "0": [ + { + "result": variable_wait, + "variables": variable_wait, + } + ], + "1": [{"result": {"event": "test_event", "event_data": {}}}], + } + assert_action_trace(expected_trace) + + @pytest.mark.parametrize( ("continue_on_timeout", "n_events"), [(False, 0), (True, 1), (None, 1)] ) From a8025a8606fa63be570fb43f0f0643068bef8844 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 19 Apr 2024 18:41:29 +0200 Subject: [PATCH 694/967] Fix mutable objects in group registry class (#115797) --- homeassistant/components/group/registry.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/group/registry.py b/homeassistant/components/group/registry.py index 1441d39d331..6cdb929d60c 100644 --- a/homeassistant/components/group/registry.py +++ b/homeassistant/components/group/registry.py @@ -47,10 +47,12 @@ def _process_group_platform( class GroupIntegrationRegistry: """Class to hold a registry of integrations.""" - on_off_mapping: dict[str, str] = {STATE_ON: STATE_OFF} - off_on_mapping: dict[str, str] = {STATE_OFF: STATE_ON} - on_states_by_domain: dict[str, set] = {} - exclude_domains: set = set() + def __init__(self) -> None: + """Imitialize registry.""" + 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() def exclude_domain(self) -> None: """Exclude the current domain.""" From f9ff3165af7d9b9f70d580af193aa072369f6b79 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 19 Apr 2024 20:25:07 +0200 Subject: [PATCH 695/967] Bump `nextdns` to version 3.0.0 (#115854) Bump nextdns to version 3.0.0 Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/nextdns/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nextdns/manifest.json b/homeassistant/components/nextdns/manifest.json index 611021d73e4..1e7145ef6d1 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==2.1.0"] + "requirements": ["nextdns==3.0.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index ea6fcac34c4..95b83d8f818 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1377,7 +1377,7 @@ nextcloudmonitor==1.5.0 nextcord==2.6.0 # homeassistant.components.nextdns -nextdns==2.1.0 +nextdns==3.0.0 # homeassistant.components.nibe_heatpump nibe==2.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 24034b934b8..1bcd778deef 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1110,7 +1110,7 @@ nextcloudmonitor==1.5.0 nextcord==2.6.0 # homeassistant.components.nextdns -nextdns==2.1.0 +nextdns==3.0.0 # homeassistant.components.nibe_heatpump nibe==2.8.0 From ffd6635c1424f544681165bd54c514cea1a7dd67 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 19 Apr 2024 20:25:57 +0200 Subject: [PATCH 696/967] Bump `nettigo_air_monitor` to version 3.0.0 (#115853) Bump nettigo_air_monitor to version 3.0.0 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 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nam/manifest.json b/homeassistant/components/nam/manifest.json index a4ef9af9aee..7b1c584c293 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==2.2.2"], + "requirements": ["nettigo-air-monitor==3.0.0"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 95b83d8f818..6186083e13d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1362,7 +1362,7 @@ netdata==1.1.0 netmap==0.7.0.2 # homeassistant.components.nam -nettigo-air-monitor==2.2.2 +nettigo-air-monitor==3.0.0 # homeassistant.components.neurio_energy neurio==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1bcd778deef..11b048fdcf6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1098,7 +1098,7 @@ nessclient==1.0.0 netmap==0.7.0.2 # homeassistant.components.nam -nettigo-air-monitor==2.2.2 +nettigo-air-monitor==3.0.0 # homeassistant.components.nexia nexia==2.0.8 From 0ea1564248f290b5d4128d9701632639b8da6c0f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 20 Apr 2024 01:36:46 +0200 Subject: [PATCH 697/967] Bump bluetooth-adapters to 0.19.0 (#115864) --- 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 b41c344bdf2..f6adcbed7d8 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -16,7 +16,7 @@ "requirements": [ "bleak==0.21.1", "bleak-retry-connector==3.5.0", - "bluetooth-adapters==0.18.0", + "bluetooth-adapters==0.19.0", "bluetooth-auto-recovery==1.4.1", "bluetooth-data-tools==1.19.0", "dbus-fast==2.21.1", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index ec6998055b0..50c17024b01 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.21.1 -bluetooth-adapters==0.18.0 +bluetooth-adapters==0.19.0 bluetooth-auto-recovery==1.4.1 bluetooth-data-tools==1.19.0 cached_ipaddress==0.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index 6186083e13d..a7111a73737 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -576,7 +576,7 @@ bluemaestro-ble==0.2.3 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.18.0 +bluetooth-adapters==0.19.0 # homeassistant.components.bluetooth bluetooth-auto-recovery==1.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 11b048fdcf6..70c1b2d244b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -491,7 +491,7 @@ bluecurrent-api==1.2.3 bluemaestro-ble==0.2.3 # homeassistant.components.bluetooth -bluetooth-adapters==0.18.0 +bluetooth-adapters==0.19.0 # homeassistant.components.bluetooth bluetooth-auto-recovery==1.4.1 From 354c20a57b4e513a42a4b0b45fec403dc63d79b6 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sat, 20 Apr 2024 12:13:56 +0200 Subject: [PATCH 698/967] Automatic cleanup of entity and device registry in AVM FRITZ!SmartHome (#114601) --- homeassistant/components/fritzbox/__init__.py | 10 +- .../components/fritzbox/coordinator.py | 57 +++++++-- tests/components/fritzbox/test_coordinator.py | 111 ++++++++++++++++++ tests/components/fritzbox/test_init.py | 62 +--------- 4 files changed, 165 insertions(+), 75 deletions(-) create mode 100644 tests/components/fritzbox/test_coordinator.py diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index 7f4006768c4..904a86d21ae 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -51,12 +51,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: has_templates = await hass.async_add_executor_job(fritz.has_templates) LOGGER.debug("enable smarthome templates: %s", has_templates) - coordinator = FritzboxDataUpdateCoordinator(hass, entry, has_templates) - - await coordinator.async_config_entry_first_refresh() - - hass.data[DOMAIN][entry.entry_id][CONF_COORDINATOR] = coordinator - def _update_unique_id(entry: RegistryEntry) -> dict[str, str] | None: """Update unique ID of entity entry.""" if ( @@ -79,6 +73,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await async_migrate_entries(hass, entry.entry_id, _update_unique_id) + coordinator = FritzboxDataUpdateCoordinator(hass, entry.entry_id, has_templates) + await coordinator.async_setup() + hass.data[DOMAIN][entry.entry_id][CONF_COORDINATOR] = coordinator + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) def logout_fritzbox(event: Event) -> None: diff --git a/homeassistant/components/fritzbox/coordinator.py b/homeassistant/components/fritzbox/coordinator.py index c58665f2b5d..a9cfc25b223 100644 --- a/homeassistant/components/fritzbox/coordinator.py +++ b/homeassistant/components/fritzbox/coordinator.py @@ -12,6 +12,7 @@ from requests.exceptions import ConnectionError as RequestConnectionError, HTTPE from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import CONF_CONNECTIONS, DOMAIN, LOGGER @@ -28,27 +29,55 @@ class FritzboxCoordinatorData: class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorData]): """Fritzbox Smarthome device data update coordinator.""" + config_entry: ConfigEntry configuration_url: str - def __init__( - self, hass: HomeAssistant, entry: ConfigEntry, has_templates: bool - ) -> None: + def __init__(self, hass: HomeAssistant, name: str, has_templates: bool) -> None: """Initialize the Fritzbox Smarthome device coordinator.""" - self.entry = entry - self.fritz: Fritzhome = hass.data[DOMAIN][self.entry.entry_id][CONF_CONNECTIONS] + super().__init__( + hass, + LOGGER, + name=name, + update_interval=timedelta(seconds=30), + ) + + self.fritz: Fritzhome = hass.data[DOMAIN][self.config_entry.entry_id][ + CONF_CONNECTIONS + ] self.configuration_url = self.fritz.get_prefixed_host() self.has_templates = has_templates self.new_devices: set[str] = set() self.new_templates: set[str] = set() - super().__init__( - hass, - LOGGER, - name=entry.entry_id, - update_interval=timedelta(seconds=30), + self.data = FritzboxCoordinatorData({}, {}) + + async def async_setup(self) -> None: + """Set up the coordinator.""" + await self.async_config_entry_first_refresh() + self.cleanup_removed_devices( + list(self.data.devices) + list(self.data.templates) ) - self.data = FritzboxCoordinatorData({}, {}) + def cleanup_removed_devices(self, avaiable_ains: list[str]) -> None: + """Cleanup entity and device registry from removed devices.""" + entity_reg = er.async_get(self.hass) + for entity in er.async_entries_for_config_entry( + entity_reg, self.config_entry.entry_id + ): + if entity.unique_id.split("_")[0] not in avaiable_ains: + LOGGER.debug("Removing obsolete entity entry %s", entity.entity_id) + entity_reg.async_remove(entity.entity_id) + + device_reg = dr.async_get(self.hass) + identifiers = {(DOMAIN, ain) for ain in avaiable_ains} + for device in dr.async_entries_for_config_entry( + device_reg, self.config_entry.entry_id + ): + if not set(device.identifiers) & identifiers: + LOGGER.debug("Removing obsolete device entry %s", device.name) + device_reg.async_update_device( + device.id, remove_config_entry_id=self.config_entry.entry_id + ) def _update_fritz_devices(self) -> FritzboxCoordinatorData: """Update all fritzbox device data.""" @@ -95,6 +124,12 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat self.new_devices = device_data.keys() - self.data.devices.keys() self.new_templates = template_data.keys() - self.data.templates.keys() + if ( + self.data.devices.keys() - device_data.keys() + or self.data.templates.keys() - template_data.keys() + ): + self.cleanup_removed_devices(list(device_data) + list(template_data)) + return FritzboxCoordinatorData(devices=device_data, templates=template_data) async def _async_update_data(self) -> FritzboxCoordinatorData: diff --git a/tests/components/fritzbox/test_coordinator.py b/tests/components/fritzbox/test_coordinator.py new file mode 100644 index 00000000000..401fab8f169 --- /dev/null +++ b/tests/components/fritzbox/test_coordinator.py @@ -0,0 +1,111 @@ +"""Tests for the AVM Fritz!Box integration.""" + +from __future__ import annotations + +from datetime import timedelta +from unittest.mock import Mock + +from pyfritzhome import LoginError +from requests.exceptions import ConnectionError, HTTPError + +from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_DEVICES +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.util.dt import utcnow + +from . import FritzDeviceCoverMock, FritzDeviceSwitchMock +from .const import MOCK_CONFIG + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_coordinator_update_after_reboot( + hass: HomeAssistant, fritz: Mock +) -> None: + """Test coordinator after reboot.""" + entry = MockConfigEntry( + domain=FB_DOMAIN, + data=MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], + unique_id="any", + ) + entry.add_to_hass(hass) + fritz().update_devices.side_effect = [HTTPError(), ""] + + assert await hass.config_entries.async_setup(entry.entry_id) + assert fritz().update_devices.call_count == 2 + assert fritz().update_templates.call_count == 1 + assert fritz().get_devices.call_count == 1 + assert fritz().get_templates.call_count == 1 + assert fritz().login.call_count == 2 + + +async def test_coordinator_update_after_password_change( + hass: HomeAssistant, fritz: Mock +) -> None: + """Test coordinator after password change.""" + entry = MockConfigEntry( + domain=FB_DOMAIN, + data=MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], + unique_id="any", + ) + entry.add_to_hass(hass) + fritz().update_devices.side_effect = HTTPError() + fritz().login.side_effect = ["", LoginError("some_user")] + + assert not await hass.config_entries.async_setup(entry.entry_id) + assert fritz().update_devices.call_count == 1 + assert fritz().get_devices.call_count == 0 + assert fritz().get_templates.call_count == 0 + assert fritz().login.call_count == 2 + + +async def test_coordinator_update_when_unreachable( + hass: HomeAssistant, fritz: Mock +) -> None: + """Test coordinator after reboot.""" + entry = MockConfigEntry( + domain=FB_DOMAIN, + data=MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], + unique_id="any", + ) + entry.add_to_hass(hass) + fritz().update_devices.side_effect = [ConnectionError(), ""] + + assert not await hass.config_entries.async_setup(entry.entry_id) + assert entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_coordinator_automatic_registry_cleanup( + hass: HomeAssistant, + fritz: Mock, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test automatic registry cleanup.""" + fritz().get_devices.return_value = [ + FritzDeviceSwitchMock(ain="fake ain switch", name="fake_switch"), + FritzDeviceCoverMock(ain="fake ain cover", name="fake_cover"), + ] + entry = MockConfigEntry( + domain=FB_DOMAIN, + data=MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], + unique_id="any", + ) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done(wait_background_tasks=True) + + assert len(er.async_entries_for_config_entry(entity_registry, entry.entry_id)) == 11 + assert len(dr.async_entries_for_config_entry(device_registry, entry.entry_id)) == 2 + + fritz().get_devices.return_value = [ + FritzDeviceSwitchMock(ain="fake ain switch", name="fake_switch") + ] + + async_fire_time_changed(hass, utcnow() + timedelta(seconds=35)) + await hass.async_block_till_done(wait_background_tasks=True) + + assert len(er.async_entries_for_config_entry(entity_registry, entry.entry_id)) == 8 + assert len(dr.async_entries_for_config_entry(device_registry, entry.entry_id)) == 1 diff --git a/tests/components/fritzbox/test_init.py b/tests/components/fritzbox/test_init.py index 4ee351f7914..8d7e4249fbd 100644 --- a/tests/components/fritzbox/test_init.py +++ b/tests/components/fritzbox/test_init.py @@ -6,7 +6,7 @@ from unittest.mock import Mock, call, patch from pyfritzhome import LoginError import pytest -from requests.exceptions import ConnectionError, HTTPError +from requests.exceptions import ConnectionError as RequestConnectionError from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN @@ -80,6 +80,7 @@ async def test_update_unique_id( new_unique_id: str, ) -> None: """Test unique_id update of integration.""" + fritz().get_devices.return_value = [FritzDeviceSwitchMock()] entry = MockConfigEntry( domain=FB_DOMAIN, data=MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], @@ -138,6 +139,7 @@ async def test_update_unique_id_no_change( unique_id: str, ) -> None: """Test unique_id is not updated of integration.""" + fritz().get_devices.return_value = [FritzDeviceSwitchMock()] entry = MockConfigEntry( domain=FB_DOMAIN, data=MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], @@ -158,62 +160,6 @@ async def test_update_unique_id_no_change( assert entity_migrated.unique_id == unique_id -async def test_coordinator_update_after_reboot( - hass: HomeAssistant, fritz: Mock -) -> None: - """Test coordinator after reboot.""" - entry = MockConfigEntry( - domain=FB_DOMAIN, - data=MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], - unique_id="any", - ) - entry.add_to_hass(hass) - fritz().update_devices.side_effect = [HTTPError(), ""] - - assert await hass.config_entries.async_setup(entry.entry_id) - assert fritz().update_devices.call_count == 2 - assert fritz().update_templates.call_count == 1 - assert fritz().get_devices.call_count == 1 - assert fritz().get_templates.call_count == 1 - assert fritz().login.call_count == 2 - - -async def test_coordinator_update_after_password_change( - hass: HomeAssistant, fritz: Mock -) -> None: - """Test coordinator after password change.""" - entry = MockConfigEntry( - domain=FB_DOMAIN, - data=MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], - unique_id="any", - ) - entry.add_to_hass(hass) - fritz().update_devices.side_effect = HTTPError() - fritz().login.side_effect = ["", LoginError("some_user")] - - assert not await hass.config_entries.async_setup(entry.entry_id) - assert fritz().update_devices.call_count == 1 - assert fritz().get_devices.call_count == 0 - assert fritz().get_templates.call_count == 0 - assert fritz().login.call_count == 2 - - -async def test_coordinator_update_when_unreachable( - hass: HomeAssistant, fritz: Mock -) -> None: - """Test coordinator after reboot.""" - entry = MockConfigEntry( - domain=FB_DOMAIN, - data=MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], - unique_id="any", - ) - entry.add_to_hass(hass) - fritz().update_devices.side_effect = [ConnectionError(), ""] - - assert not await hass.config_entries.async_setup(entry.entry_id) - assert entry.state is ConfigEntryState.SETUP_RETRY - - async def test_unload_remove(hass: HomeAssistant, fritz: Mock) -> None: """Test unload and remove of integration.""" fritz().get_devices.return_value = [FritzDeviceSwitchMock()] @@ -325,7 +271,7 @@ async def test_raise_config_entry_not_ready_when_offline(hass: HomeAssistant) -> entry.add_to_hass(hass) with patch( "homeassistant.components.fritzbox.Fritzhome.login", - side_effect=ConnectionError(), + side_effect=RequestConnectionError(), ) as mock_login: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() From c8d52c02c511ea83f3dae20bf4e3ed66e5406185 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 20 Apr 2024 12:31:20 +0200 Subject: [PATCH 699/967] Use snapshot testing in NextDNS (#115879) * Use snapshot testing in NextDNS sensor * Use snapshot testing in NextDNS switch * Use snapshot testing in NextDNS binary sensor * Use snapshot testing in NextDNS button --------- Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- .../nextdns/snapshots/test_binary_sensor.ambr | 2277 ++++++++ .../nextdns/snapshots/test_button.ambr | 47 + .../nextdns/snapshots/test_sensor.ambr | 4749 +++++++++++++++++ .../nextdns/snapshots/test_switch.ambr | 4749 +++++++++++++++++ .../components/nextdns/test_binary_sensor.py | 36 +- tests/components/nextdns/test_button.py | 25 +- tests/components/nextdns/test_sensor.py | 274 +- tests/components/nextdns/test_switch.py | 606 +-- 8 files changed, 11880 insertions(+), 883 deletions(-) create mode 100644 tests/components/nextdns/snapshots/test_binary_sensor.ambr create mode 100644 tests/components/nextdns/snapshots/test_button.ambr create mode 100644 tests/components/nextdns/snapshots/test_sensor.ambr create mode 100644 tests/components/nextdns/snapshots/test_switch.ambr diff --git a/tests/components/nextdns/snapshots/test_binary_sensor.ambr b/tests/components/nextdns/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..bd4ecbba084 --- /dev/null +++ b/tests/components/nextdns/snapshots/test_binary_sensor.ambr @@ -0,0 +1,2277 @@ +# serializer version: 1 +# name: test_binary_Sensor[switch.fake_profile_ai_driven_threat_detection-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.fake_profile_ai_driven_threat_detection', + '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': 'AI-Driven threat detection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ai_threat_detection', + 'unique_id': 'xyz12_ai_threat_detection', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_ai_driven_threat_detection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile AI-Driven threat detection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_ai_driven_threat_detection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_allow_affiliate_tracking_links-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.fake_profile_allow_affiliate_tracking_links', + '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': 'Allow affiliate & tracking links', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'allow_affiliate', + 'unique_id': 'xyz12_allow_affiliate', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_allow_affiliate_tracking_links-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Allow affiliate & tracking links', + }), + 'context': , + 'entity_id': 'switch.fake_profile_allow_affiliate_tracking_links', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_anonymized_edns_client_subnet-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.fake_profile_anonymized_edns_client_subnet', + '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': 'Anonymized EDNS client subnet', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'anonymized_ecs', + 'unique_id': 'xyz12_anonymized_ecs', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_anonymized_edns_client_subnet-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Anonymized EDNS client subnet', + }), + 'context': , + 'entity_id': 'switch.fake_profile_anonymized_edns_client_subnet', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_block_9gag-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.fake_profile_block_9gag', + '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': 'Block 9GAG', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_9gag', + 'unique_id': 'xyz12_block_9gag', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_block_bypass_methods-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.fake_profile_block_bypass_methods', + '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': 'Block bypass methods', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_bypass_methods', + 'unique_id': 'xyz12_block_bypass_methods', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_block_bypass_methods-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block bypass methods', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_bypass_methods', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_block_child_sexual_abuse_material-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.fake_profile_block_child_sexual_abuse_material', + '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': 'Block child sexual abuse material', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_csam', + 'unique_id': 'xyz12_block_csam', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_block_child_sexual_abuse_material-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block child sexual abuse material', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_child_sexual_abuse_material', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_block_disguised_third_party_trackers-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.fake_profile_block_disguised_third_party_trackers', + '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': 'Block disguised third-party trackers', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_disguised_trackers', + 'unique_id': 'xyz12_block_disguised_trackers', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_block_disguised_third_party_trackers-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block disguised third-party trackers', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_disguised_third_party_trackers', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_block_dynamic_dns_hostnames-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.fake_profile_block_dynamic_dns_hostnames', + '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': 'Block dynamic DNS hostnames', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_ddns', + 'unique_id': 'xyz12_block_ddns', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_block_dynamic_dns_hostnames-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block dynamic DNS hostnames', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_dynamic_dns_hostnames', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_block_newly_registered_domains-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.fake_profile_block_newly_registered_domains', + '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': 'Block newly registered domains', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_nrd', + 'unique_id': 'xyz12_block_nrd', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_block_newly_registered_domains-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block newly registered domains', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_newly_registered_domains', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_block_page-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.fake_profile_block_page', + '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': 'Block page', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_page', + 'unique_id': 'xyz12_block_page', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_block_page-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block page', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_page', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_block_parked_domains-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.fake_profile_block_parked_domains', + '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': 'Block parked domains', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_parked_domains', + 'unique_id': 'xyz12_block_parked_domains', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_block_parked_domains-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block parked domains', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_parked_domains', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_cache_boost-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.fake_profile_cache_boost', + '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': 'Cache boost', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cache_boost', + 'unique_id': 'xyz12_cache_boost', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_cache_boost-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Cache boost', + }), + 'context': , + 'entity_id': 'switch.fake_profile_cache_boost', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_cname_flattening-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.fake_profile_cname_flattening', + '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': 'CNAME flattening', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cname_flattening', + 'unique_id': 'xyz12_cname_flattening', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_cname_flattening-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile CNAME flattening', + }), + 'context': , + 'entity_id': 'switch.fake_profile_cname_flattening', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_cryptojacking_protection-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.fake_profile_cryptojacking_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': 'Cryptojacking protection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cryptojacking_protection', + 'unique_id': 'xyz12_cryptojacking_protection', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_cryptojacking_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Cryptojacking protection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_cryptojacking_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_dns_rebinding_protection-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.fake_profile_dns_rebinding_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': 'DNS rebinding protection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dns_rebinding_protection', + 'unique_id': 'xyz12_dns_rebinding_protection', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_dns_rebinding_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS rebinding protection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_dns_rebinding_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_domain_generation_algorithms_protection-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.fake_profile_domain_generation_algorithms_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': 'Domain generation algorithms protection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dga_protection', + 'unique_id': 'xyz12_dga_protection', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_domain_generation_algorithms_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Domain generation algorithms protection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_domain_generation_algorithms_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_force_safesearch-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.fake_profile_force_safesearch', + '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': 'Force SafeSearch', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'safesearch', + 'unique_id': 'xyz12_safesearch', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_force_safesearch-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Force SafeSearch', + }), + 'context': , + 'entity_id': 'switch.fake_profile_force_safesearch', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_force_youtube_restricted_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.fake_profile_force_youtube_restricted_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': 'Force YouTube restricted mode', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'youtube_restricted_mode', + 'unique_id': 'xyz12_youtube_restricted_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_force_youtube_restricted_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Force YouTube restricted mode', + }), + 'context': , + 'entity_id': 'switch.fake_profile_force_youtube_restricted_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_google_safe_browsing-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.fake_profile_google_safe_browsing', + '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': 'Google safe browsing', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'google_safe_browsing', + 'unique_id': 'xyz12_google_safe_browsing', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_google_safe_browsing-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Google safe browsing', + }), + 'context': , + 'entity_id': 'switch.fake_profile_google_safe_browsing', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_idn_homograph_attacks_protection-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.fake_profile_idn_homograph_attacks_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': 'IDN homograph attacks protection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'idn_homograph_attacks_protection', + 'unique_id': 'xyz12_idn_homograph_attacks_protection', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_idn_homograph_attacks_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile IDN homograph attacks protection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_idn_homograph_attacks_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_logs-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.fake_profile_logs', + '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': 'Logs', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'logs', + 'unique_id': 'xyz12_logs', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_logs-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Logs', + }), + 'context': , + 'entity_id': 'switch.fake_profile_logs', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_threat_intelligence_feeds-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.fake_profile_threat_intelligence_feeds', + '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': 'Threat intelligence feeds', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'threat_intelligence_feeds', + 'unique_id': 'xyz12_threat_intelligence_feeds', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_threat_intelligence_feeds-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Threat intelligence feeds', + }), + 'context': , + 'entity_id': 'switch.fake_profile_threat_intelligence_feeds', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_typosquatting_protection-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.fake_profile_typosquatting_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': 'Typosquatting protection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'typosquatting_protection', + 'unique_id': 'xyz12_typosquatting_protection', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_typosquatting_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Typosquatting protection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_typosquatting_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_web3-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.fake_profile_web3', + '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': 'Web3', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'web3', + 'unique_id': 'xyz12_web3', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_Sensor[switch.fake_profile_web3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Web3', + }), + 'context': , + 'entity_id': 'switch.fake_profile_web3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[binary_sensor.fake_profile_device_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': , + 'entity_id': 'binary_sensor.fake_profile_device_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': 'Device connection status', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'device_connection_status', + 'unique_id': 'xyz12_this_device_nextdns_connection_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.fake_profile_device_connection_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'Fake Profile Device connection status', + }), + 'context': , + 'entity_id': 'binary_sensor.fake_profile_device_connection_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[binary_sensor.fake_profile_device_profile_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': , + 'entity_id': 'binary_sensor.fake_profile_device_profile_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': 'Device profile connection status', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'device_profile_connection_status', + 'unique_id': 'xyz12_this_device_profile_connection_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.fake_profile_device_profile_connection_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'Fake Profile Device profile connection status', + }), + 'context': , + 'entity_id': 'binary_sensor.fake_profile_device_profile_connection_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_ai_driven_threat_detection-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.fake_profile_ai_driven_threat_detection', + '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': 'AI-Driven threat detection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ai_threat_detection', + 'unique_id': 'xyz12_ai_threat_detection', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_ai_driven_threat_detection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile AI-Driven threat detection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_ai_driven_threat_detection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_allow_affiliate_tracking_links-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.fake_profile_allow_affiliate_tracking_links', + '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': 'Allow affiliate & tracking links', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'allow_affiliate', + 'unique_id': 'xyz12_allow_affiliate', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_allow_affiliate_tracking_links-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Allow affiliate & tracking links', + }), + 'context': , + 'entity_id': 'switch.fake_profile_allow_affiliate_tracking_links', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_anonymized_edns_client_subnet-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.fake_profile_anonymized_edns_client_subnet', + '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': 'Anonymized EDNS client subnet', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'anonymized_ecs', + 'unique_id': 'xyz12_anonymized_ecs', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_anonymized_edns_client_subnet-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Anonymized EDNS client subnet', + }), + 'context': , + 'entity_id': 'switch.fake_profile_anonymized_edns_client_subnet', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_block_9gag-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.fake_profile_block_9gag', + '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': 'Block 9GAG', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_9gag', + 'unique_id': 'xyz12_block_9gag', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_block_bypass_methods-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.fake_profile_block_bypass_methods', + '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': 'Block bypass methods', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_bypass_methods', + 'unique_id': 'xyz12_block_bypass_methods', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_block_bypass_methods-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block bypass methods', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_bypass_methods', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_block_child_sexual_abuse_material-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.fake_profile_block_child_sexual_abuse_material', + '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': 'Block child sexual abuse material', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_csam', + 'unique_id': 'xyz12_block_csam', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_block_child_sexual_abuse_material-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block child sexual abuse material', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_child_sexual_abuse_material', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_block_disguised_third_party_trackers-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.fake_profile_block_disguised_third_party_trackers', + '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': 'Block disguised third-party trackers', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_disguised_trackers', + 'unique_id': 'xyz12_block_disguised_trackers', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_block_disguised_third_party_trackers-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block disguised third-party trackers', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_disguised_third_party_trackers', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_block_dynamic_dns_hostnames-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.fake_profile_block_dynamic_dns_hostnames', + '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': 'Block dynamic DNS hostnames', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_ddns', + 'unique_id': 'xyz12_block_ddns', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_block_dynamic_dns_hostnames-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block dynamic DNS hostnames', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_dynamic_dns_hostnames', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_block_newly_registered_domains-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.fake_profile_block_newly_registered_domains', + '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': 'Block newly registered domains', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_nrd', + 'unique_id': 'xyz12_block_nrd', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_block_newly_registered_domains-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block newly registered domains', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_newly_registered_domains', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_block_page-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.fake_profile_block_page', + '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': 'Block page', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_page', + 'unique_id': 'xyz12_block_page', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_block_page-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block page', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_page', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_block_parked_domains-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.fake_profile_block_parked_domains', + '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': 'Block parked domains', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_parked_domains', + 'unique_id': 'xyz12_block_parked_domains', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_block_parked_domains-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block parked domains', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_parked_domains', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_cache_boost-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.fake_profile_cache_boost', + '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': 'Cache boost', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cache_boost', + 'unique_id': 'xyz12_cache_boost', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_cache_boost-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Cache boost', + }), + 'context': , + 'entity_id': 'switch.fake_profile_cache_boost', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_cname_flattening-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.fake_profile_cname_flattening', + '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': 'CNAME flattening', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cname_flattening', + 'unique_id': 'xyz12_cname_flattening', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_cname_flattening-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile CNAME flattening', + }), + 'context': , + 'entity_id': 'switch.fake_profile_cname_flattening', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_cryptojacking_protection-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.fake_profile_cryptojacking_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': 'Cryptojacking protection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cryptojacking_protection', + 'unique_id': 'xyz12_cryptojacking_protection', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_cryptojacking_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Cryptojacking protection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_cryptojacking_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_dns_rebinding_protection-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.fake_profile_dns_rebinding_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': 'DNS rebinding protection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dns_rebinding_protection', + 'unique_id': 'xyz12_dns_rebinding_protection', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_dns_rebinding_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS rebinding protection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_dns_rebinding_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_domain_generation_algorithms_protection-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.fake_profile_domain_generation_algorithms_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': 'Domain generation algorithms protection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dga_protection', + 'unique_id': 'xyz12_dga_protection', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_domain_generation_algorithms_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Domain generation algorithms protection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_domain_generation_algorithms_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_force_safesearch-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.fake_profile_force_safesearch', + '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': 'Force SafeSearch', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'safesearch', + 'unique_id': 'xyz12_safesearch', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_force_safesearch-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Force SafeSearch', + }), + 'context': , + 'entity_id': 'switch.fake_profile_force_safesearch', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_force_youtube_restricted_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.fake_profile_force_youtube_restricted_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': 'Force YouTube restricted mode', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'youtube_restricted_mode', + 'unique_id': 'xyz12_youtube_restricted_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_force_youtube_restricted_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Force YouTube restricted mode', + }), + 'context': , + 'entity_id': 'switch.fake_profile_force_youtube_restricted_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_google_safe_browsing-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.fake_profile_google_safe_browsing', + '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': 'Google safe browsing', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'google_safe_browsing', + 'unique_id': 'xyz12_google_safe_browsing', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_google_safe_browsing-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Google safe browsing', + }), + 'context': , + 'entity_id': 'switch.fake_profile_google_safe_browsing', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_idn_homograph_attacks_protection-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.fake_profile_idn_homograph_attacks_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': 'IDN homograph attacks protection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'idn_homograph_attacks_protection', + 'unique_id': 'xyz12_idn_homograph_attacks_protection', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_idn_homograph_attacks_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile IDN homograph attacks protection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_idn_homograph_attacks_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_logs-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.fake_profile_logs', + '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': 'Logs', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'logs', + 'unique_id': 'xyz12_logs', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_logs-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Logs', + }), + 'context': , + 'entity_id': 'switch.fake_profile_logs', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_threat_intelligence_feeds-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.fake_profile_threat_intelligence_feeds', + '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': 'Threat intelligence feeds', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'threat_intelligence_feeds', + 'unique_id': 'xyz12_threat_intelligence_feeds', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_threat_intelligence_feeds-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Threat intelligence feeds', + }), + 'context': , + 'entity_id': 'switch.fake_profile_threat_intelligence_feeds', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_typosquatting_protection-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.fake_profile_typosquatting_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': 'Typosquatting protection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'typosquatting_protection', + 'unique_id': 'xyz12_typosquatting_protection', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_typosquatting_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Typosquatting protection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_typosquatting_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[switch.fake_profile_web3-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.fake_profile_web3', + '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': 'Web3', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'web3', + 'unique_id': 'xyz12_web3', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[switch.fake_profile_web3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Web3', + }), + 'context': , + 'entity_id': 'switch.fake_profile_web3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/nextdns/snapshots/test_button.ambr b/tests/components/nextdns/snapshots/test_button.ambr new file mode 100644 index 00000000000..32dc31eea19 --- /dev/null +++ b/tests/components/nextdns/snapshots/test_button.ambr @@ -0,0 +1,47 @@ +# serializer version: 1 +# name: test_button[button.fake_profile_clear_logs-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.fake_profile_clear_logs', + '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': 'Clear logs', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'clear_logs', + 'unique_id': 'xyz12_clear_logs', + 'unit_of_measurement': None, + }) +# --- +# name: test_button[button.fake_profile_clear_logs-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Clear logs', + }), + 'context': , + 'entity_id': 'button.fake_profile_clear_logs', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/nextdns/snapshots/test_sensor.ambr b/tests/components/nextdns/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..34b40433e3b --- /dev/null +++ b/tests/components/nextdns/snapshots/test_sensor.ambr @@ -0,0 +1,4749 @@ +# serializer version: 1 +# name: test_sensor[binary_sensor.fake_profile_device_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': , + 'entity_id': 'binary_sensor.fake_profile_device_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': 'Device connection status', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'device_connection_status', + 'unique_id': 'xyz12_this_device_nextdns_connection_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[binary_sensor.fake_profile_device_connection_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'Fake Profile Device connection status', + }), + 'context': , + 'entity_id': 'binary_sensor.fake_profile_device_connection_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[binary_sensor.fake_profile_device_profile_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': , + 'entity_id': 'binary_sensor.fake_profile_device_profile_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': 'Device profile connection status', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'device_profile_connection_status', + 'unique_id': 'xyz12_this_device_profile_connection_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[binary_sensor.fake_profile_device_profile_connection_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'Fake Profile Device profile connection status', + }), + 'context': , + 'entity_id': 'binary_sensor.fake_profile_device_profile_connection_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_sensor[button.fake_profile_clear_logs-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.fake_profile_clear_logs', + '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': 'Clear logs', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'clear_logs', + 'unique_id': 'xyz12_clear_logs', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[button.fake_profile_clear_logs-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Clear logs', + }), + 'context': , + 'entity_id': 'button.fake_profile_clear_logs', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_over_http_3_queries-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.fake_profile_dns_over_http_3_queries', + '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': 'DNS-over-HTTP/3 queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'doh3_queries', + 'unique_id': 'xyz12_doh3_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_over_http_3_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS-over-HTTP/3 queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_over_http_3_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '15', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_over_http_3_queries_ratio-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.fake_profile_dns_over_http_3_queries_ratio', + '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': 'DNS-over-HTTP/3 queries ratio', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'doh3_queries_ratio', + 'unique_id': 'xyz12_doh3_queries_ratio', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_over_http_3_queries_ratio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS-over-HTTP/3 queries ratio', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_over_http_3_queries_ratio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '13.0', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_over_https_queries-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.fake_profile_dns_over_https_queries', + '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': 'DNS-over-HTTPS queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'doh_queries', + 'unique_id': 'xyz12_doh_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_over_https_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS-over-HTTPS queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_over_https_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_over_https_queries_ratio-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.fake_profile_dns_over_https_queries_ratio', + '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': 'DNS-over-HTTPS queries ratio', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'doh_queries_ratio', + 'unique_id': 'xyz12_doh_queries_ratio', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_over_https_queries_ratio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS-over-HTTPS queries ratio', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_over_https_queries_ratio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '17.4', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_over_quic_queries-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.fake_profile_dns_over_quic_queries', + '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': 'DNS-over-QUIC queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'doq_queries', + 'unique_id': 'xyz12_doq_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_over_quic_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS-over-QUIC queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_over_quic_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_over_quic_queries_ratio-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.fake_profile_dns_over_quic_queries_ratio', + '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': 'DNS-over-QUIC queries ratio', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'doq_queries_ratio', + 'unique_id': 'xyz12_doq_queries_ratio', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_over_quic_queries_ratio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS-over-QUIC queries ratio', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_over_quic_queries_ratio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '8.7', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_over_tls_queries-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.fake_profile_dns_over_tls_queries', + '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': 'DNS-over-TLS queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dot_queries', + 'unique_id': 'xyz12_dot_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_over_tls_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS-over-TLS queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_over_tls_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '30', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_over_tls_queries_ratio-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.fake_profile_dns_over_tls_queries_ratio', + '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': 'DNS-over-TLS queries ratio', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dot_queries_ratio', + 'unique_id': 'xyz12_dot_queries_ratio', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_over_tls_queries_ratio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS-over-TLS queries ratio', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_over_tls_queries_ratio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '26.1', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_queries-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.fake_profile_dns_queries', + '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': 'DNS queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'all_queries', + 'unique_id': 'xyz12_all_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_queries_blocked-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.fake_profile_dns_queries_blocked', + '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': 'DNS queries blocked', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'blocked_queries', + 'unique_id': 'xyz12_blocked_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_queries_blocked-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS queries blocked', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_queries_blocked', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_queries_blocked_ratio-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.fake_profile_dns_queries_blocked_ratio', + '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': 'DNS queries blocked ratio', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'blocked_queries_ratio', + 'unique_id': 'xyz12_blocked_queries_ratio', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_queries_blocked_ratio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS queries blocked ratio', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_queries_blocked_ratio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.0', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_queries_relayed-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.fake_profile_dns_queries_relayed', + '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': 'DNS queries relayed', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'relayed_queries', + 'unique_id': 'xyz12_relayed_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_sensor[sensor.fake_profile_dns_queries_relayed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS queries relayed', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_queries_relayed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10', + }) +# --- +# name: test_sensor[sensor.fake_profile_dnssec_not_validated_queries-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.fake_profile_dnssec_not_validated_queries', + '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': 'DNSSEC not validated queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'not_validated_queries', + 'unique_id': 'xyz12_not_validated_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_sensor[sensor.fake_profile_dnssec_not_validated_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNSSEC not validated queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dnssec_not_validated_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '25', + }) +# --- +# name: test_sensor[sensor.fake_profile_dnssec_validated_queries-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.fake_profile_dnssec_validated_queries', + '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': 'DNSSEC validated queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'validated_queries', + 'unique_id': 'xyz12_validated_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_sensor[sensor.fake_profile_dnssec_validated_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNSSEC validated queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dnssec_validated_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '75', + }) +# --- +# name: test_sensor[sensor.fake_profile_dnssec_validated_queries_ratio-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.fake_profile_dnssec_validated_queries_ratio', + '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': 'DNSSEC validated queries ratio', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'validated_queries_ratio', + 'unique_id': 'xyz12_validated_queries_ratio', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.fake_profile_dnssec_validated_queries_ratio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNSSEC validated queries ratio', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dnssec_validated_queries_ratio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '75.0', + }) +# --- +# name: test_sensor[sensor.fake_profile_encrypted_queries-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.fake_profile_encrypted_queries', + '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': 'Encrypted queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'encrypted_queries', + 'unique_id': 'xyz12_encrypted_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_sensor[sensor.fake_profile_encrypted_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Encrypted queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_encrypted_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '60', + }) +# --- +# name: test_sensor[sensor.fake_profile_encrypted_queries_ratio-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.fake_profile_encrypted_queries_ratio', + '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': 'Encrypted queries ratio', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'encrypted_queries_ratio', + 'unique_id': 'xyz12_encrypted_queries_ratio', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.fake_profile_encrypted_queries_ratio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Encrypted queries ratio', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_encrypted_queries_ratio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '60.0', + }) +# --- +# name: test_sensor[sensor.fake_profile_ipv4_queries-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.fake_profile_ipv4_queries', + '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': 'IPv4 queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ipv4_queries', + 'unique_id': 'xyz12_ipv4_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_sensor[sensor.fake_profile_ipv4_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile IPv4 queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_ipv4_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '90', + }) +# --- +# name: test_sensor[sensor.fake_profile_ipv6_queries-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.fake_profile_ipv6_queries', + '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': 'IPv6 queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ipv6_queries', + 'unique_id': 'xyz12_ipv6_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_sensor[sensor.fake_profile_ipv6_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile IPv6 queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_ipv6_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10', + }) +# --- +# name: test_sensor[sensor.fake_profile_ipv6_queries_ratio-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.fake_profile_ipv6_queries_ratio', + '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': 'IPv6 queries ratio', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ipv6_queries_ratio', + 'unique_id': 'xyz12_ipv6_queries_ratio', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.fake_profile_ipv6_queries_ratio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile IPv6 queries ratio', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_ipv6_queries_ratio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10.0', + }) +# --- +# name: test_sensor[sensor.fake_profile_tcp_queries-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.fake_profile_tcp_queries', + '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': 'TCP queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tcp_queries', + 'unique_id': 'xyz12_tcp_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_sensor[sensor.fake_profile_tcp_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile TCP queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_tcp_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.fake_profile_tcp_queries_ratio-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.fake_profile_tcp_queries_ratio', + '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': 'TCP queries ratio', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tcp_queries_ratio', + 'unique_id': 'xyz12_tcp_queries_ratio', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.fake_profile_tcp_queries_ratio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile TCP queries ratio', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_tcp_queries_ratio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensor[sensor.fake_profile_udp_queries-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.fake_profile_udp_queries', + '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': 'UDP queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'udp_queries', + 'unique_id': 'xyz12_udp_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_sensor[sensor.fake_profile_udp_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile UDP queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_udp_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '40', + }) +# --- +# name: test_sensor[sensor.fake_profile_udp_queries_ratio-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.fake_profile_udp_queries_ratio', + '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': 'UDP queries ratio', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'udp_queries_ratio', + 'unique_id': 'xyz12_udp_queries_ratio', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.fake_profile_udp_queries_ratio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile UDP queries ratio', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_udp_queries_ratio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '34.8', + }) +# --- +# name: test_sensor[sensor.fake_profile_unencrypted_queries-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.fake_profile_unencrypted_queries', + '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': 'Unencrypted queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'unencrypted_queries', + 'unique_id': 'xyz12_unencrypted_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_sensor[sensor.fake_profile_unencrypted_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Unencrypted queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_unencrypted_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '40', + }) +# --- +# name: test_sensor[switch.fake_profile_ai_driven_threat_detection-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.fake_profile_ai_driven_threat_detection', + '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': 'AI-Driven threat detection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ai_threat_detection', + 'unique_id': 'xyz12_ai_threat_detection', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_ai_driven_threat_detection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile AI-Driven threat detection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_ai_driven_threat_detection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_allow_affiliate_tracking_links-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.fake_profile_allow_affiliate_tracking_links', + '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': 'Allow affiliate & tracking links', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'allow_affiliate', + 'unique_id': 'xyz12_allow_affiliate', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_allow_affiliate_tracking_links-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Allow affiliate & tracking links', + }), + 'context': , + 'entity_id': 'switch.fake_profile_allow_affiliate_tracking_links', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_anonymized_edns_client_subnet-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.fake_profile_anonymized_edns_client_subnet', + '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': 'Anonymized EDNS client subnet', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'anonymized_ecs', + 'unique_id': 'xyz12_anonymized_ecs', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_anonymized_edns_client_subnet-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Anonymized EDNS client subnet', + }), + 'context': , + 'entity_id': 'switch.fake_profile_anonymized_edns_client_subnet', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_9gag-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.fake_profile_block_9gag', + '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': 'Block 9GAG', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_9gag', + 'unique_id': 'xyz12_block_9gag', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_9gag-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block 9GAG', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_9gag', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_amazon-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.fake_profile_block_amazon', + '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': 'Block Amazon', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_amazon', + 'unique_id': 'xyz12_block_amazon', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_amazon-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Amazon', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_amazon', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_bereal-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.fake_profile_block_bereal', + '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': 'Block BeReal', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_bereal', + 'unique_id': 'xyz12_block_bereal', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_bereal-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block BeReal', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_bereal', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_blizzard-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.fake_profile_block_blizzard', + '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': 'Block Blizzard', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_blizzard', + 'unique_id': 'xyz12_block_blizzard', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_blizzard-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Blizzard', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_blizzard', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_bypass_methods-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.fake_profile_block_bypass_methods', + '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': 'Block bypass methods', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_bypass_methods', + 'unique_id': 'xyz12_block_bypass_methods', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_bypass_methods-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block bypass methods', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_bypass_methods', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_chatgpt-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.fake_profile_block_chatgpt', + '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': 'Block ChatGPT', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_chatgpt', + 'unique_id': 'xyz12_block_chatgpt', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_chatgpt-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block ChatGPT', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_chatgpt', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_child_sexual_abuse_material-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.fake_profile_block_child_sexual_abuse_material', + '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': 'Block child sexual abuse material', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_csam', + 'unique_id': 'xyz12_block_csam', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_child_sexual_abuse_material-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block child sexual abuse material', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_child_sexual_abuse_material', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_dailymotion-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.fake_profile_block_dailymotion', + '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': 'Block Dailymotion', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_dailymotion', + 'unique_id': 'xyz12_block_dailymotion', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_dailymotion-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Dailymotion', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_dailymotion', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_dating-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.fake_profile_block_dating', + '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': 'Block dating', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_dating', + 'unique_id': 'xyz12_block_dating', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_dating-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block dating', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_dating', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_discord-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.fake_profile_block_discord', + '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': 'Block Discord', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_discord', + 'unique_id': 'xyz12_block_discord', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_discord-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Discord', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_discord', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_disguised_third_party_trackers-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.fake_profile_block_disguised_third_party_trackers', + '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': 'Block disguised third-party trackers', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_disguised_trackers', + 'unique_id': 'xyz12_block_disguised_trackers', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_disguised_third_party_trackers-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block disguised third-party trackers', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_disguised_third_party_trackers', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_disney_plus-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.fake_profile_block_disney_plus', + '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': 'Block Disney Plus', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_disneyplus', + 'unique_id': 'xyz12_block_disneyplus', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_disney_plus-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Disney Plus', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_disney_plus', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_dynamic_dns_hostnames-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.fake_profile_block_dynamic_dns_hostnames', + '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': 'Block dynamic DNS hostnames', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_ddns', + 'unique_id': 'xyz12_block_ddns', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_dynamic_dns_hostnames-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block dynamic DNS hostnames', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_dynamic_dns_hostnames', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_ebay-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.fake_profile_block_ebay', + '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': 'Block eBay', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_ebay', + 'unique_id': 'xyz12_block_ebay', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_ebay-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block eBay', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_ebay', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_facebook-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.fake_profile_block_facebook', + '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': 'Block Facebook', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_facebook', + 'unique_id': 'xyz12_block_facebook', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_facebook-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Facebook', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_facebook', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_fortnite-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.fake_profile_block_fortnite', + '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': 'Block Fortnite', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_fortnite', + 'unique_id': 'xyz12_block_fortnite', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_fortnite-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Fortnite', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_fortnite', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_gambling-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.fake_profile_block_gambling', + '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': 'Block gambling', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_gambling', + 'unique_id': 'xyz12_block_gambling', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_gambling-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block gambling', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_gambling', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_google_chat-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.fake_profile_block_google_chat', + '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': 'Block Google Chat', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_google_chat', + 'unique_id': 'xyz12_block_google_chat', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_google_chat-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Google Chat', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_google_chat', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_hbo_max-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.fake_profile_block_hbo_max', + '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': 'Block HBO Max', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_hbomax', + 'unique_id': 'xyz12_block_hbomax', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_hbo_max-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block HBO Max', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_hbo_max', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_hulu-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.fake_profile_block_hulu', + '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': 'Block Hulu', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'xyz12_block_hulu', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_hulu-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Hulu', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_hulu', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_imgur-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.fake_profile_block_imgur', + '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': 'Block Imgur', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_imgur', + 'unique_id': 'xyz12_block_imgur', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_imgur-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Imgur', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_imgur', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_instagram-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.fake_profile_block_instagram', + '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': 'Block Instagram', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_instagram', + 'unique_id': 'xyz12_block_instagram', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_instagram-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Instagram', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_instagram', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_league_of_legends-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.fake_profile_block_league_of_legends', + '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': 'Block League of Legends', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_leagueoflegends', + 'unique_id': 'xyz12_block_leagueoflegends', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_league_of_legends-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block League of Legends', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_league_of_legends', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_mastodon-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.fake_profile_block_mastodon', + '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': 'Block Mastodon', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_mastodon', + 'unique_id': 'xyz12_block_mastodon', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_mastodon-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Mastodon', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_mastodon', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_messenger-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.fake_profile_block_messenger', + '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': 'Block Messenger', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_messenger', + 'unique_id': 'xyz12_block_messenger', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_messenger-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Messenger', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_messenger', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_minecraft-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.fake_profile_block_minecraft', + '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': 'Block Minecraft', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_minecraft', + 'unique_id': 'xyz12_block_minecraft', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_minecraft-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Minecraft', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_minecraft', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_netflix-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.fake_profile_block_netflix', + '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': 'Block Netflix', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_netflix', + 'unique_id': 'xyz12_block_netflix', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_netflix-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Netflix', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_netflix', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_newly_registered_domains-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.fake_profile_block_newly_registered_domains', + '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': 'Block newly registered domains', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_nrd', + 'unique_id': 'xyz12_block_nrd', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_newly_registered_domains-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block newly registered domains', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_newly_registered_domains', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_online_gaming-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.fake_profile_block_online_gaming', + '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': 'Block online gaming', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_online_gaming', + 'unique_id': 'xyz12_block_online_gaming', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_online_gaming-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block online gaming', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_online_gaming', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_page-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.fake_profile_block_page', + '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': 'Block page', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_page', + 'unique_id': 'xyz12_block_page', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_page-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block page', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_page', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_sensor[switch.fake_profile_block_parked_domains-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.fake_profile_block_parked_domains', + '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': 'Block parked domains', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_parked_domains', + 'unique_id': 'xyz12_block_parked_domains', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_parked_domains-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block parked domains', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_parked_domains', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_pinterest-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.fake_profile_block_pinterest', + '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': 'Block Pinterest', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_pinterest', + 'unique_id': 'xyz12_block_pinterest', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_pinterest-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Pinterest', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_pinterest', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_piracy-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.fake_profile_block_piracy', + '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': 'Block piracy', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_piracy', + 'unique_id': 'xyz12_block_piracy', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_piracy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block piracy', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_piracy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_playstation_network-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.fake_profile_block_playstation_network', + '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': 'Block PlayStation Network', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_playstation_network', + 'unique_id': 'xyz12_block_playstation_network', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_playstation_network-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block PlayStation Network', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_playstation_network', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_porn-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.fake_profile_block_porn', + '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': 'Block porn', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_porn', + 'unique_id': 'xyz12_block_porn', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_porn-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block porn', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_porn', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_prime_video-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.fake_profile_block_prime_video', + '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': 'Block Prime Video', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_primevideo', + 'unique_id': 'xyz12_block_primevideo', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_prime_video-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Prime Video', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_prime_video', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_reddit-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.fake_profile_block_reddit', + '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': 'Block Reddit', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_reddit', + 'unique_id': 'xyz12_block_reddit', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_reddit-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Reddit', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_reddit', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_roblox-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.fake_profile_block_roblox', + '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': 'Block Roblox', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_roblox', + 'unique_id': 'xyz12_block_roblox', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_roblox-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Roblox', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_roblox', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_signal-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.fake_profile_block_signal', + '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': 'Block Signal', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_signal', + 'unique_id': 'xyz12_block_signal', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_signal-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Signal', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_signal', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_skype-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.fake_profile_block_skype', + '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': 'Block Skype', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_skype', + 'unique_id': 'xyz12_block_skype', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_skype-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Skype', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_skype', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_snapchat-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.fake_profile_block_snapchat', + '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': 'Block Snapchat', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_snapchat', + 'unique_id': 'xyz12_block_snapchat', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_snapchat-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Snapchat', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_snapchat', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_social_networks-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.fake_profile_block_social_networks', + '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': 'Block social networks', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_social_networks', + 'unique_id': 'xyz12_block_social_networks', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_social_networks-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block social networks', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_social_networks', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_spotify-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.fake_profile_block_spotify', + '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': 'Block Spotify', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_spotify', + 'unique_id': 'xyz12_block_spotify', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_spotify-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Spotify', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_spotify', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_steam-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.fake_profile_block_steam', + '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': 'Block Steam', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_steam', + 'unique_id': 'xyz12_block_steam', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_steam-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Steam', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_steam', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_telegram-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.fake_profile_block_telegram', + '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': 'Block Telegram', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_telegram', + 'unique_id': 'xyz12_block_telegram', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_telegram-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Telegram', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_telegram', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_tiktok-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.fake_profile_block_tiktok', + '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': 'Block TikTok', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_tiktok', + 'unique_id': 'xyz12_block_tiktok', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_tiktok-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block TikTok', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_tiktok', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_tinder-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.fake_profile_block_tinder', + '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': 'Block Tinder', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_tinder', + 'unique_id': 'xyz12_block_tinder', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_tinder-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Tinder', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_tinder', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_tumblr-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.fake_profile_block_tumblr', + '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': 'Block Tumblr', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_tumblr', + 'unique_id': 'xyz12_block_tumblr', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_tumblr-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Tumblr', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_tumblr', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_twitch-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.fake_profile_block_twitch', + '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': 'Block Twitch', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_twitch', + 'unique_id': 'xyz12_block_twitch', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_twitch-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Twitch', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_twitch', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_video_streaming-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.fake_profile_block_video_streaming', + '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': 'Block video streaming', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_video_streaming', + 'unique_id': 'xyz12_block_video_streaming', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_video_streaming-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block video streaming', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_video_streaming', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_vimeo-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.fake_profile_block_vimeo', + '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': 'Block Vimeo', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_vimeo', + 'unique_id': 'xyz12_block_vimeo', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_vimeo-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Vimeo', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_vimeo', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_vk-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.fake_profile_block_vk', + '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': 'Block VK', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_vk', + 'unique_id': 'xyz12_block_vk', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_vk-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block VK', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_vk', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_whatsapp-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.fake_profile_block_whatsapp', + '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': 'Block WhatsApp', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_whatsapp', + 'unique_id': 'xyz12_block_whatsapp', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_whatsapp-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block WhatsApp', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_whatsapp', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_x_formerly_twitter-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.fake_profile_block_x_formerly_twitter', + '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': 'Block X (formerly Twitter)', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_twitter', + 'unique_id': 'xyz12_block_twitter', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_x_formerly_twitter-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block X (formerly Twitter)', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_x_formerly_twitter', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_xbox_live-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.fake_profile_block_xbox_live', + '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': 'Block Xbox Live', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_xboxlive', + 'unique_id': 'xyz12_block_xboxlive', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_xbox_live-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Xbox Live', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_xbox_live', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_youtube-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.fake_profile_block_youtube', + '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': 'Block YouTube', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_youtube', + 'unique_id': 'xyz12_block_youtube', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_youtube-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block YouTube', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_youtube', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_block_zoom-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.fake_profile_block_zoom', + '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': 'Block Zoom', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_zoom', + 'unique_id': 'xyz12_block_zoom', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_block_zoom-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Zoom', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_zoom', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_cache_boost-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.fake_profile_cache_boost', + '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': 'Cache boost', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cache_boost', + 'unique_id': 'xyz12_cache_boost', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_cache_boost-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Cache boost', + }), + 'context': , + 'entity_id': 'switch.fake_profile_cache_boost', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_cname_flattening-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.fake_profile_cname_flattening', + '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': 'CNAME flattening', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cname_flattening', + 'unique_id': 'xyz12_cname_flattening', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_cname_flattening-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile CNAME flattening', + }), + 'context': , + 'entity_id': 'switch.fake_profile_cname_flattening', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_cryptojacking_protection-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.fake_profile_cryptojacking_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': 'Cryptojacking protection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cryptojacking_protection', + 'unique_id': 'xyz12_cryptojacking_protection', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_cryptojacking_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Cryptojacking protection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_cryptojacking_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_dns_rebinding_protection-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.fake_profile_dns_rebinding_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': 'DNS rebinding protection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dns_rebinding_protection', + 'unique_id': 'xyz12_dns_rebinding_protection', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_dns_rebinding_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS rebinding protection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_dns_rebinding_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_domain_generation_algorithms_protection-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.fake_profile_domain_generation_algorithms_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': 'Domain generation algorithms protection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dga_protection', + 'unique_id': 'xyz12_dga_protection', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_domain_generation_algorithms_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Domain generation algorithms protection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_domain_generation_algorithms_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_force_safesearch-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.fake_profile_force_safesearch', + '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': 'Force SafeSearch', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'safesearch', + 'unique_id': 'xyz12_safesearch', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_force_safesearch-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Force SafeSearch', + }), + 'context': , + 'entity_id': 'switch.fake_profile_force_safesearch', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_sensor[switch.fake_profile_force_youtube_restricted_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.fake_profile_force_youtube_restricted_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': 'Force YouTube restricted mode', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'youtube_restricted_mode', + 'unique_id': 'xyz12_youtube_restricted_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_force_youtube_restricted_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Force YouTube restricted mode', + }), + 'context': , + 'entity_id': 'switch.fake_profile_force_youtube_restricted_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_sensor[switch.fake_profile_google_safe_browsing-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.fake_profile_google_safe_browsing', + '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': 'Google safe browsing', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'google_safe_browsing', + 'unique_id': 'xyz12_google_safe_browsing', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_google_safe_browsing-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Google safe browsing', + }), + 'context': , + 'entity_id': 'switch.fake_profile_google_safe_browsing', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_sensor[switch.fake_profile_idn_homograph_attacks_protection-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.fake_profile_idn_homograph_attacks_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': 'IDN homograph attacks protection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'idn_homograph_attacks_protection', + 'unique_id': 'xyz12_idn_homograph_attacks_protection', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_idn_homograph_attacks_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile IDN homograph attacks protection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_idn_homograph_attacks_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_logs-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.fake_profile_logs', + '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': 'Logs', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'logs', + 'unique_id': 'xyz12_logs', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_logs-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Logs', + }), + 'context': , + 'entity_id': 'switch.fake_profile_logs', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_threat_intelligence_feeds-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.fake_profile_threat_intelligence_feeds', + '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': 'Threat intelligence feeds', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'threat_intelligence_feeds', + 'unique_id': 'xyz12_threat_intelligence_feeds', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_threat_intelligence_feeds-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Threat intelligence feeds', + }), + 'context': , + 'entity_id': 'switch.fake_profile_threat_intelligence_feeds', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_typosquatting_protection-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.fake_profile_typosquatting_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': 'Typosquatting protection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'typosquatting_protection', + 'unique_id': 'xyz12_typosquatting_protection', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_typosquatting_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Typosquatting protection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_typosquatting_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensor[switch.fake_profile_web3-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.fake_profile_web3', + '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': 'Web3', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'web3', + 'unique_id': 'xyz12_web3', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[switch.fake_profile_web3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Web3', + }), + 'context': , + 'entity_id': 'switch.fake_profile_web3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/nextdns/snapshots/test_switch.ambr b/tests/components/nextdns/snapshots/test_switch.ambr new file mode 100644 index 00000000000..8472f02e8c5 --- /dev/null +++ b/tests/components/nextdns/snapshots/test_switch.ambr @@ -0,0 +1,4749 @@ +# serializer version: 1 +# name: test_switch[binary_sensor.fake_profile_device_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': , + 'entity_id': 'binary_sensor.fake_profile_device_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': 'Device connection status', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'device_connection_status', + 'unique_id': 'xyz12_this_device_nextdns_connection_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[binary_sensor.fake_profile_device_connection_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'Fake Profile Device connection status', + }), + 'context': , + 'entity_id': 'binary_sensor.fake_profile_device_connection_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[binary_sensor.fake_profile_device_profile_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': , + 'entity_id': 'binary_sensor.fake_profile_device_profile_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': 'Device profile connection status', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'device_profile_connection_status', + 'unique_id': 'xyz12_this_device_profile_connection_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[binary_sensor.fake_profile_device_profile_connection_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'Fake Profile Device profile connection status', + }), + 'context': , + 'entity_id': 'binary_sensor.fake_profile_device_profile_connection_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switch[button.fake_profile_clear_logs-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.fake_profile_clear_logs', + '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': 'Clear logs', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'clear_logs', + 'unique_id': 'xyz12_clear_logs', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[button.fake_profile_clear_logs-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Clear logs', + }), + 'context': , + 'entity_id': 'button.fake_profile_clear_logs', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_over_http_3_queries-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.fake_profile_dns_over_http_3_queries', + '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': 'DNS-over-HTTP/3 queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'doh3_queries', + 'unique_id': 'xyz12_doh3_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_over_http_3_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS-over-HTTP/3 queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_over_http_3_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '15', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_over_http_3_queries_ratio-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.fake_profile_dns_over_http_3_queries_ratio', + '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': 'DNS-over-HTTP/3 queries ratio', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'doh3_queries_ratio', + 'unique_id': 'xyz12_doh3_queries_ratio', + 'unit_of_measurement': '%', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_over_http_3_queries_ratio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS-over-HTTP/3 queries ratio', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_over_http_3_queries_ratio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '13.0', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_over_https_queries-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.fake_profile_dns_over_https_queries', + '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': 'DNS-over-HTTPS queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'doh_queries', + 'unique_id': 'xyz12_doh_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_over_https_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS-over-HTTPS queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_over_https_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_over_https_queries_ratio-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.fake_profile_dns_over_https_queries_ratio', + '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': 'DNS-over-HTTPS queries ratio', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'doh_queries_ratio', + 'unique_id': 'xyz12_doh_queries_ratio', + 'unit_of_measurement': '%', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_over_https_queries_ratio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS-over-HTTPS queries ratio', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_over_https_queries_ratio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '17.4', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_over_quic_queries-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.fake_profile_dns_over_quic_queries', + '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': 'DNS-over-QUIC queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'doq_queries', + 'unique_id': 'xyz12_doq_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_over_quic_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS-over-QUIC queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_over_quic_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_over_quic_queries_ratio-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.fake_profile_dns_over_quic_queries_ratio', + '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': 'DNS-over-QUIC queries ratio', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'doq_queries_ratio', + 'unique_id': 'xyz12_doq_queries_ratio', + 'unit_of_measurement': '%', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_over_quic_queries_ratio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS-over-QUIC queries ratio', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_over_quic_queries_ratio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '8.7', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_over_tls_queries-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.fake_profile_dns_over_tls_queries', + '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': 'DNS-over-TLS queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dot_queries', + 'unique_id': 'xyz12_dot_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_over_tls_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS-over-TLS queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_over_tls_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '30', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_over_tls_queries_ratio-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.fake_profile_dns_over_tls_queries_ratio', + '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': 'DNS-over-TLS queries ratio', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dot_queries_ratio', + 'unique_id': 'xyz12_dot_queries_ratio', + 'unit_of_measurement': '%', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_over_tls_queries_ratio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS-over-TLS queries ratio', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_over_tls_queries_ratio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '26.1', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_queries-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.fake_profile_dns_queries', + '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': 'DNS queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'all_queries', + 'unique_id': 'xyz12_all_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_queries_blocked-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.fake_profile_dns_queries_blocked', + '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': 'DNS queries blocked', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'blocked_queries', + 'unique_id': 'xyz12_blocked_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_queries_blocked-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS queries blocked', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_queries_blocked', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_queries_blocked_ratio-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.fake_profile_dns_queries_blocked_ratio', + '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': 'DNS queries blocked ratio', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'blocked_queries_ratio', + 'unique_id': 'xyz12_blocked_queries_ratio', + 'unit_of_measurement': '%', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_queries_blocked_ratio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS queries blocked ratio', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_queries_blocked_ratio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.0', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_queries_relayed-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.fake_profile_dns_queries_relayed', + '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': 'DNS queries relayed', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'relayed_queries', + 'unique_id': 'xyz12_relayed_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_switch[sensor.fake_profile_dns_queries_relayed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS queries relayed', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dns_queries_relayed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10', + }) +# --- +# name: test_switch[sensor.fake_profile_dnssec_not_validated_queries-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.fake_profile_dnssec_not_validated_queries', + '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': 'DNSSEC not validated queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'not_validated_queries', + 'unique_id': 'xyz12_not_validated_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_switch[sensor.fake_profile_dnssec_not_validated_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNSSEC not validated queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dnssec_not_validated_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '25', + }) +# --- +# name: test_switch[sensor.fake_profile_dnssec_validated_queries-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.fake_profile_dnssec_validated_queries', + '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': 'DNSSEC validated queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'validated_queries', + 'unique_id': 'xyz12_validated_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_switch[sensor.fake_profile_dnssec_validated_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNSSEC validated queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dnssec_validated_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '75', + }) +# --- +# name: test_switch[sensor.fake_profile_dnssec_validated_queries_ratio-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.fake_profile_dnssec_validated_queries_ratio', + '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': 'DNSSEC validated queries ratio', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'validated_queries_ratio', + 'unique_id': 'xyz12_validated_queries_ratio', + 'unit_of_measurement': '%', + }) +# --- +# name: test_switch[sensor.fake_profile_dnssec_validated_queries_ratio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNSSEC validated queries ratio', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_dnssec_validated_queries_ratio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '75.0', + }) +# --- +# name: test_switch[sensor.fake_profile_encrypted_queries-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.fake_profile_encrypted_queries', + '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': 'Encrypted queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'encrypted_queries', + 'unique_id': 'xyz12_encrypted_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_switch[sensor.fake_profile_encrypted_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Encrypted queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_encrypted_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '60', + }) +# --- +# name: test_switch[sensor.fake_profile_encrypted_queries_ratio-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.fake_profile_encrypted_queries_ratio', + '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': 'Encrypted queries ratio', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'encrypted_queries_ratio', + 'unique_id': 'xyz12_encrypted_queries_ratio', + 'unit_of_measurement': '%', + }) +# --- +# name: test_switch[sensor.fake_profile_encrypted_queries_ratio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Encrypted queries ratio', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_encrypted_queries_ratio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '60.0', + }) +# --- +# name: test_switch[sensor.fake_profile_ipv4_queries-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.fake_profile_ipv4_queries', + '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': 'IPv4 queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ipv4_queries', + 'unique_id': 'xyz12_ipv4_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_switch[sensor.fake_profile_ipv4_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile IPv4 queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_ipv4_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '90', + }) +# --- +# name: test_switch[sensor.fake_profile_ipv6_queries-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.fake_profile_ipv6_queries', + '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': 'IPv6 queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ipv6_queries', + 'unique_id': 'xyz12_ipv6_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_switch[sensor.fake_profile_ipv6_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile IPv6 queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_ipv6_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10', + }) +# --- +# name: test_switch[sensor.fake_profile_ipv6_queries_ratio-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.fake_profile_ipv6_queries_ratio', + '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': 'IPv6 queries ratio', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ipv6_queries_ratio', + 'unique_id': 'xyz12_ipv6_queries_ratio', + 'unit_of_measurement': '%', + }) +# --- +# name: test_switch[sensor.fake_profile_ipv6_queries_ratio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile IPv6 queries ratio', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_ipv6_queries_ratio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10.0', + }) +# --- +# name: test_switch[sensor.fake_profile_tcp_queries-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.fake_profile_tcp_queries', + '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': 'TCP queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tcp_queries', + 'unique_id': 'xyz12_tcp_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_switch[sensor.fake_profile_tcp_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile TCP queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_tcp_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_switch[sensor.fake_profile_tcp_queries_ratio-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.fake_profile_tcp_queries_ratio', + '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': 'TCP queries ratio', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tcp_queries_ratio', + 'unique_id': 'xyz12_tcp_queries_ratio', + 'unit_of_measurement': '%', + }) +# --- +# name: test_switch[sensor.fake_profile_tcp_queries_ratio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile TCP queries ratio', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_tcp_queries_ratio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_switch[sensor.fake_profile_udp_queries-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.fake_profile_udp_queries', + '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': 'UDP queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'udp_queries', + 'unique_id': 'xyz12_udp_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_switch[sensor.fake_profile_udp_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile UDP queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_udp_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '40', + }) +# --- +# name: test_switch[sensor.fake_profile_udp_queries_ratio-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.fake_profile_udp_queries_ratio', + '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': 'UDP queries ratio', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'udp_queries_ratio', + 'unique_id': 'xyz12_udp_queries_ratio', + 'unit_of_measurement': '%', + }) +# --- +# name: test_switch[sensor.fake_profile_udp_queries_ratio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile UDP queries ratio', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_udp_queries_ratio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '34.8', + }) +# --- +# name: test_switch[sensor.fake_profile_unencrypted_queries-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.fake_profile_unencrypted_queries', + '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': 'Unencrypted queries', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'unencrypted_queries', + 'unique_id': 'xyz12_unencrypted_queries', + 'unit_of_measurement': 'queries', + }) +# --- +# name: test_switch[sensor.fake_profile_unencrypted_queries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Unencrypted queries', + 'state_class': , + 'unit_of_measurement': 'queries', + }), + 'context': , + 'entity_id': 'sensor.fake_profile_unencrypted_queries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '40', + }) +# --- +# name: test_switch[switch.fake_profile_ai_driven_threat_detection-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.fake_profile_ai_driven_threat_detection', + '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': 'AI-Driven threat detection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ai_threat_detection', + 'unique_id': 'xyz12_ai_threat_detection', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_ai_driven_threat_detection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile AI-Driven threat detection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_ai_driven_threat_detection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_allow_affiliate_tracking_links-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.fake_profile_allow_affiliate_tracking_links', + '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': 'Allow affiliate & tracking links', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'allow_affiliate', + 'unique_id': 'xyz12_allow_affiliate', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_allow_affiliate_tracking_links-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Allow affiliate & tracking links', + }), + 'context': , + 'entity_id': 'switch.fake_profile_allow_affiliate_tracking_links', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_anonymized_edns_client_subnet-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.fake_profile_anonymized_edns_client_subnet', + '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': 'Anonymized EDNS client subnet', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'anonymized_ecs', + 'unique_id': 'xyz12_anonymized_ecs', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_anonymized_edns_client_subnet-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Anonymized EDNS client subnet', + }), + 'context': , + 'entity_id': 'switch.fake_profile_anonymized_edns_client_subnet', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_9gag-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.fake_profile_block_9gag', + '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': 'Block 9GAG', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_9gag', + 'unique_id': 'xyz12_block_9gag', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_9gag-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block 9GAG', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_9gag', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_amazon-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.fake_profile_block_amazon', + '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': 'Block Amazon', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_amazon', + 'unique_id': 'xyz12_block_amazon', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_amazon-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Amazon', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_amazon', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_bereal-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.fake_profile_block_bereal', + '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': 'Block BeReal', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_bereal', + 'unique_id': 'xyz12_block_bereal', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_bereal-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block BeReal', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_bereal', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_blizzard-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.fake_profile_block_blizzard', + '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': 'Block Blizzard', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_blizzard', + 'unique_id': 'xyz12_block_blizzard', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_blizzard-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Blizzard', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_blizzard', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_bypass_methods-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.fake_profile_block_bypass_methods', + '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': 'Block bypass methods', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_bypass_methods', + 'unique_id': 'xyz12_block_bypass_methods', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_bypass_methods-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block bypass methods', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_bypass_methods', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_chatgpt-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.fake_profile_block_chatgpt', + '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': 'Block ChatGPT', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_chatgpt', + 'unique_id': 'xyz12_block_chatgpt', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_chatgpt-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block ChatGPT', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_chatgpt', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_child_sexual_abuse_material-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.fake_profile_block_child_sexual_abuse_material', + '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': 'Block child sexual abuse material', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_csam', + 'unique_id': 'xyz12_block_csam', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_child_sexual_abuse_material-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block child sexual abuse material', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_child_sexual_abuse_material', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_dailymotion-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.fake_profile_block_dailymotion', + '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': 'Block Dailymotion', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_dailymotion', + 'unique_id': 'xyz12_block_dailymotion', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_dailymotion-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Dailymotion', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_dailymotion', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_dating-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.fake_profile_block_dating', + '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': 'Block dating', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_dating', + 'unique_id': 'xyz12_block_dating', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_dating-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block dating', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_dating', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_discord-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.fake_profile_block_discord', + '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': 'Block Discord', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_discord', + 'unique_id': 'xyz12_block_discord', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_discord-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Discord', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_discord', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_disguised_third_party_trackers-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.fake_profile_block_disguised_third_party_trackers', + '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': 'Block disguised third-party trackers', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_disguised_trackers', + 'unique_id': 'xyz12_block_disguised_trackers', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_disguised_third_party_trackers-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block disguised third-party trackers', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_disguised_third_party_trackers', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_disney_plus-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.fake_profile_block_disney_plus', + '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': 'Block Disney Plus', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_disneyplus', + 'unique_id': 'xyz12_block_disneyplus', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_disney_plus-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Disney Plus', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_disney_plus', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_dynamic_dns_hostnames-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.fake_profile_block_dynamic_dns_hostnames', + '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': 'Block dynamic DNS hostnames', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_ddns', + 'unique_id': 'xyz12_block_ddns', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_dynamic_dns_hostnames-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block dynamic DNS hostnames', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_dynamic_dns_hostnames', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_ebay-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.fake_profile_block_ebay', + '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': 'Block eBay', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_ebay', + 'unique_id': 'xyz12_block_ebay', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_ebay-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block eBay', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_ebay', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_facebook-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.fake_profile_block_facebook', + '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': 'Block Facebook', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_facebook', + 'unique_id': 'xyz12_block_facebook', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_facebook-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Facebook', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_facebook', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_fortnite-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.fake_profile_block_fortnite', + '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': 'Block Fortnite', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_fortnite', + 'unique_id': 'xyz12_block_fortnite', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_fortnite-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Fortnite', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_fortnite', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_gambling-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.fake_profile_block_gambling', + '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': 'Block gambling', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_gambling', + 'unique_id': 'xyz12_block_gambling', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_gambling-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block gambling', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_gambling', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_google_chat-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.fake_profile_block_google_chat', + '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': 'Block Google Chat', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_google_chat', + 'unique_id': 'xyz12_block_google_chat', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_google_chat-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Google Chat', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_google_chat', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_hbo_max-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.fake_profile_block_hbo_max', + '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': 'Block HBO Max', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_hbomax', + 'unique_id': 'xyz12_block_hbomax', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_hbo_max-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block HBO Max', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_hbo_max', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_hulu-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.fake_profile_block_hulu', + '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': 'Block Hulu', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'xyz12_block_hulu', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_hulu-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Hulu', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_hulu', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_imgur-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.fake_profile_block_imgur', + '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': 'Block Imgur', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_imgur', + 'unique_id': 'xyz12_block_imgur', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_imgur-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Imgur', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_imgur', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_instagram-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.fake_profile_block_instagram', + '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': 'Block Instagram', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_instagram', + 'unique_id': 'xyz12_block_instagram', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_instagram-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Instagram', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_instagram', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_league_of_legends-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.fake_profile_block_league_of_legends', + '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': 'Block League of Legends', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_leagueoflegends', + 'unique_id': 'xyz12_block_leagueoflegends', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_league_of_legends-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block League of Legends', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_league_of_legends', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_mastodon-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.fake_profile_block_mastodon', + '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': 'Block Mastodon', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_mastodon', + 'unique_id': 'xyz12_block_mastodon', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_mastodon-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Mastodon', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_mastodon', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_messenger-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.fake_profile_block_messenger', + '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': 'Block Messenger', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_messenger', + 'unique_id': 'xyz12_block_messenger', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_messenger-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Messenger', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_messenger', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_minecraft-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.fake_profile_block_minecraft', + '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': 'Block Minecraft', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_minecraft', + 'unique_id': 'xyz12_block_minecraft', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_minecraft-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Minecraft', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_minecraft', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_netflix-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.fake_profile_block_netflix', + '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': 'Block Netflix', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_netflix', + 'unique_id': 'xyz12_block_netflix', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_netflix-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Netflix', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_netflix', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_newly_registered_domains-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.fake_profile_block_newly_registered_domains', + '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': 'Block newly registered domains', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_nrd', + 'unique_id': 'xyz12_block_nrd', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_newly_registered_domains-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block newly registered domains', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_newly_registered_domains', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_online_gaming-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.fake_profile_block_online_gaming', + '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': 'Block online gaming', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_online_gaming', + 'unique_id': 'xyz12_block_online_gaming', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_online_gaming-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block online gaming', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_online_gaming', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_page-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.fake_profile_block_page', + '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': 'Block page', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_page', + 'unique_id': 'xyz12_block_page', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_page-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block page', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_page', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switch[switch.fake_profile_block_parked_domains-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.fake_profile_block_parked_domains', + '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': 'Block parked domains', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_parked_domains', + 'unique_id': 'xyz12_block_parked_domains', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_parked_domains-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block parked domains', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_parked_domains', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_pinterest-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.fake_profile_block_pinterest', + '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': 'Block Pinterest', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_pinterest', + 'unique_id': 'xyz12_block_pinterest', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_pinterest-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Pinterest', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_pinterest', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_piracy-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.fake_profile_block_piracy', + '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': 'Block piracy', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_piracy', + 'unique_id': 'xyz12_block_piracy', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_piracy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block piracy', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_piracy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_playstation_network-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.fake_profile_block_playstation_network', + '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': 'Block PlayStation Network', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_playstation_network', + 'unique_id': 'xyz12_block_playstation_network', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_playstation_network-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block PlayStation Network', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_playstation_network', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_porn-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.fake_profile_block_porn', + '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': 'Block porn', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_porn', + 'unique_id': 'xyz12_block_porn', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_porn-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block porn', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_porn', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_prime_video-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.fake_profile_block_prime_video', + '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': 'Block Prime Video', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_primevideo', + 'unique_id': 'xyz12_block_primevideo', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_prime_video-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Prime Video', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_prime_video', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_reddit-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.fake_profile_block_reddit', + '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': 'Block Reddit', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_reddit', + 'unique_id': 'xyz12_block_reddit', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_reddit-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Reddit', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_reddit', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_roblox-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.fake_profile_block_roblox', + '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': 'Block Roblox', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_roblox', + 'unique_id': 'xyz12_block_roblox', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_roblox-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Roblox', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_roblox', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_signal-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.fake_profile_block_signal', + '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': 'Block Signal', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_signal', + 'unique_id': 'xyz12_block_signal', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_signal-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Signal', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_signal', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_skype-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.fake_profile_block_skype', + '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': 'Block Skype', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_skype', + 'unique_id': 'xyz12_block_skype', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_skype-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Skype', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_skype', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_snapchat-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.fake_profile_block_snapchat', + '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': 'Block Snapchat', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_snapchat', + 'unique_id': 'xyz12_block_snapchat', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_snapchat-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Snapchat', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_snapchat', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_social_networks-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.fake_profile_block_social_networks', + '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': 'Block social networks', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_social_networks', + 'unique_id': 'xyz12_block_social_networks', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_social_networks-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block social networks', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_social_networks', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_spotify-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.fake_profile_block_spotify', + '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': 'Block Spotify', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_spotify', + 'unique_id': 'xyz12_block_spotify', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_spotify-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Spotify', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_spotify', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_steam-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.fake_profile_block_steam', + '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': 'Block Steam', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_steam', + 'unique_id': 'xyz12_block_steam', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_steam-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Steam', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_steam', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_telegram-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.fake_profile_block_telegram', + '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': 'Block Telegram', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_telegram', + 'unique_id': 'xyz12_block_telegram', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_telegram-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Telegram', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_telegram', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_tiktok-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.fake_profile_block_tiktok', + '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': 'Block TikTok', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_tiktok', + 'unique_id': 'xyz12_block_tiktok', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_tiktok-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block TikTok', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_tiktok', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_tinder-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.fake_profile_block_tinder', + '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': 'Block Tinder', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_tinder', + 'unique_id': 'xyz12_block_tinder', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_tinder-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Tinder', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_tinder', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_tumblr-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.fake_profile_block_tumblr', + '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': 'Block Tumblr', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_tumblr', + 'unique_id': 'xyz12_block_tumblr', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_tumblr-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Tumblr', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_tumblr', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_twitch-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.fake_profile_block_twitch', + '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': 'Block Twitch', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_twitch', + 'unique_id': 'xyz12_block_twitch', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_twitch-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Twitch', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_twitch', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_video_streaming-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.fake_profile_block_video_streaming', + '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': 'Block video streaming', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_video_streaming', + 'unique_id': 'xyz12_block_video_streaming', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_video_streaming-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block video streaming', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_video_streaming', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_vimeo-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.fake_profile_block_vimeo', + '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': 'Block Vimeo', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_vimeo', + 'unique_id': 'xyz12_block_vimeo', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_vimeo-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Vimeo', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_vimeo', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_vk-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.fake_profile_block_vk', + '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': 'Block VK', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_vk', + 'unique_id': 'xyz12_block_vk', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_vk-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block VK', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_vk', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_whatsapp-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.fake_profile_block_whatsapp', + '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': 'Block WhatsApp', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_whatsapp', + 'unique_id': 'xyz12_block_whatsapp', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_whatsapp-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block WhatsApp', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_whatsapp', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_x_formerly_twitter-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.fake_profile_block_x_formerly_twitter', + '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': 'Block X (formerly Twitter)', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_twitter', + 'unique_id': 'xyz12_block_twitter', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_x_formerly_twitter-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block X (formerly Twitter)', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_x_formerly_twitter', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_xbox_live-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.fake_profile_block_xbox_live', + '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': 'Block Xbox Live', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_xboxlive', + 'unique_id': 'xyz12_block_xboxlive', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_xbox_live-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Xbox Live', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_xbox_live', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_youtube-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.fake_profile_block_youtube', + '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': 'Block YouTube', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_youtube', + 'unique_id': 'xyz12_block_youtube', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_youtube-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block YouTube', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_youtube', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_block_zoom-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.fake_profile_block_zoom', + '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': 'Block Zoom', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'block_zoom', + 'unique_id': 'xyz12_block_zoom', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_block_zoom-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Block Zoom', + }), + 'context': , + 'entity_id': 'switch.fake_profile_block_zoom', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_cache_boost-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.fake_profile_cache_boost', + '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': 'Cache boost', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cache_boost', + 'unique_id': 'xyz12_cache_boost', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_cache_boost-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Cache boost', + }), + 'context': , + 'entity_id': 'switch.fake_profile_cache_boost', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_cname_flattening-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.fake_profile_cname_flattening', + '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': 'CNAME flattening', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cname_flattening', + 'unique_id': 'xyz12_cname_flattening', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_cname_flattening-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile CNAME flattening', + }), + 'context': , + 'entity_id': 'switch.fake_profile_cname_flattening', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_cryptojacking_protection-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.fake_profile_cryptojacking_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': 'Cryptojacking protection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cryptojacking_protection', + 'unique_id': 'xyz12_cryptojacking_protection', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_cryptojacking_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Cryptojacking protection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_cryptojacking_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_dns_rebinding_protection-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.fake_profile_dns_rebinding_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': 'DNS rebinding protection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dns_rebinding_protection', + 'unique_id': 'xyz12_dns_rebinding_protection', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_dns_rebinding_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile DNS rebinding protection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_dns_rebinding_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_domain_generation_algorithms_protection-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.fake_profile_domain_generation_algorithms_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': 'Domain generation algorithms protection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dga_protection', + 'unique_id': 'xyz12_dga_protection', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_domain_generation_algorithms_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Domain generation algorithms protection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_domain_generation_algorithms_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_force_safesearch-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.fake_profile_force_safesearch', + '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': 'Force SafeSearch', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'safesearch', + 'unique_id': 'xyz12_safesearch', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_force_safesearch-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Force SafeSearch', + }), + 'context': , + 'entity_id': 'switch.fake_profile_force_safesearch', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switch[switch.fake_profile_force_youtube_restricted_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.fake_profile_force_youtube_restricted_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': 'Force YouTube restricted mode', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'youtube_restricted_mode', + 'unique_id': 'xyz12_youtube_restricted_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_force_youtube_restricted_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Force YouTube restricted mode', + }), + 'context': , + 'entity_id': 'switch.fake_profile_force_youtube_restricted_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switch[switch.fake_profile_google_safe_browsing-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.fake_profile_google_safe_browsing', + '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': 'Google safe browsing', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'google_safe_browsing', + 'unique_id': 'xyz12_google_safe_browsing', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_google_safe_browsing-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Google safe browsing', + }), + 'context': , + 'entity_id': 'switch.fake_profile_google_safe_browsing', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switch[switch.fake_profile_idn_homograph_attacks_protection-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.fake_profile_idn_homograph_attacks_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': 'IDN homograph attacks protection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'idn_homograph_attacks_protection', + 'unique_id': 'xyz12_idn_homograph_attacks_protection', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_idn_homograph_attacks_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile IDN homograph attacks protection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_idn_homograph_attacks_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_logs-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.fake_profile_logs', + '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': 'Logs', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'logs', + 'unique_id': 'xyz12_logs', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_logs-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Logs', + }), + 'context': , + 'entity_id': 'switch.fake_profile_logs', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_threat_intelligence_feeds-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.fake_profile_threat_intelligence_feeds', + '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': 'Threat intelligence feeds', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'threat_intelligence_feeds', + 'unique_id': 'xyz12_threat_intelligence_feeds', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_threat_intelligence_feeds-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Threat intelligence feeds', + }), + 'context': , + 'entity_id': 'switch.fake_profile_threat_intelligence_feeds', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_typosquatting_protection-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.fake_profile_typosquatting_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': 'Typosquatting protection', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'typosquatting_protection', + 'unique_id': 'xyz12_typosquatting_protection', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_typosquatting_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Typosquatting protection', + }), + 'context': , + 'entity_id': 'switch.fake_profile_typosquatting_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[switch.fake_profile_web3-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.fake_profile_web3', + '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': 'Web3', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'web3', + 'unique_id': 'xyz12_web3', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_web3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Web3', + }), + 'context': , + 'entity_id': 'switch.fake_profile_web3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/nextdns/test_binary_sensor.py b/tests/components/nextdns/test_binary_sensor.py index b69db4798d3..f83e55515e8 100644 --- a/tests/components/nextdns/test_binary_sensor.py +++ b/tests/components/nextdns/test_binary_sensor.py @@ -4,8 +4,9 @@ from datetime import timedelta from unittest.mock import patch from nextdns import ApiError +from syrupy import SnapshotAssertion -from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE +from homeassistant.const import STATE_ON, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.util.dt import utcnow @@ -15,31 +16,20 @@ from . import init_integration, mock_nextdns from tests.common import async_fire_time_changed -async def test_binary_Sensor(hass: HomeAssistant) -> None: +async def test_binary_sensor( + hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion +) -> None: """Test states of the binary sensors.""" - registry = er.async_get(hass) + with patch("homeassistant.components.nextdns.PLATFORMS", [Platform.BINARY_SENSOR]): + entry = await init_integration(hass) - await init_integration(hass) + entity_entries = er.async_entries_for_config_entry(entity_registry, entry.entry_id) - state = hass.states.get("binary_sensor.fake_profile_device_connection_status") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("binary_sensor.fake_profile_device_connection_status") - assert entry - assert entry.unique_id == "xyz12_this_device_nextdns_connection_status" - - state = hass.states.get( - "binary_sensor.fake_profile_device_profile_connection_status" - ) - assert state - assert state.state == STATE_OFF - - entry = registry.async_get( - "binary_sensor.fake_profile_device_profile_connection_status" - ) - assert entry - assert entry.unique_id == "xyz12_this_device_profile_connection_status" + assert entity_entries + for entity_entry in entity_entries: + assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry") + assert (state := hass.states.get(entity_entry.entity_id)) + assert state == snapshot(name=f"{entity_entry.entity_id}-state") async def test_availability(hass: HomeAssistant) -> None: diff --git a/tests/components/nextdns/test_button.py b/tests/components/nextdns/test_button.py index b5f7b01aee2..2007af612c8 100644 --- a/tests/components/nextdns/test_button.py +++ b/tests/components/nextdns/test_button.py @@ -2,8 +2,10 @@ from unittest.mock import patch +from syrupy import SnapshotAssertion + from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN -from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.util import dt as dt_util @@ -11,19 +13,20 @@ from homeassistant.util import dt as dt_util from . import init_integration -async def test_button(hass: HomeAssistant) -> None: +async def test_button( + hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion +) -> None: """Test states of the button.""" - registry = er.async_get(hass) + with patch("homeassistant.components.nextdns.PLATFORMS", [Platform.BUTTON]): + entry = await init_integration(hass) - await init_integration(hass) + entity_entries = er.async_entries_for_config_entry(entity_registry, entry.entry_id) - state = hass.states.get("button.fake_profile_clear_logs") - assert state - assert state.state == STATE_UNKNOWN - - entry = registry.async_get("button.fake_profile_clear_logs") - assert entry - assert entry.unique_id == "xyz12_clear_logs" + assert entity_entries + for entity_entry in entity_entries: + assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry") + assert (state := hass.states.get(entity_entry.entity_id)) + assert state == snapshot(name=f"{entity_entry.entity_id}-state") async def test_button_press(hass: HomeAssistant) -> None: diff --git a/tests/components/nextdns/test_sensor.py b/tests/components/nextdns/test_sensor.py index 951d220eccb..9c03cf2b215 100644 --- a/tests/components/nextdns/test_sensor.py +++ b/tests/components/nextdns/test_sensor.py @@ -4,9 +4,9 @@ from datetime import timedelta from unittest.mock import patch from nextdns import ApiError +from syrupy import SnapshotAssertion -from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorStateClass -from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, STATE_UNAVAILABLE +from homeassistant.const import STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.util.dt import utcnow @@ -17,270 +17,30 @@ from tests.common import async_fire_time_changed async def test_sensor( - hass: HomeAssistant, entity_registry_enabled_by_default: None + hass: HomeAssistant, + entity_registry_enabled_by_default: None, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, ) -> None: """Test states of sensors.""" - registry = er.async_get(hass) + with patch("homeassistant.components.nextdns.PLATFORMS", [Platform.SENSOR]): + entry = await init_integration(hass) - await init_integration(hass) + entity_entries = er.async_entries_for_config_entry(entity_registry, entry.entry_id) - state = hass.states.get("sensor.fake_profile_dns_queries") - assert state - assert state.state == "100" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" - - entry = registry.async_get("sensor.fake_profile_dns_queries") - assert entry - assert entry.unique_id == "xyz12_all_queries" - - state = hass.states.get("sensor.fake_profile_dns_queries_blocked") - assert state - assert state.state == "20" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" - - entry = registry.async_get("sensor.fake_profile_dns_queries_blocked") - assert entry - assert entry.unique_id == "xyz12_blocked_queries" - - state = hass.states.get("sensor.fake_profile_dns_queries_blocked_ratio") - assert state - assert state.state == "20.0" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - - entry = registry.async_get("sensor.fake_profile_dns_queries_blocked_ratio") - assert entry - assert entry.unique_id == "xyz12_blocked_queries_ratio" - - state = hass.states.get("sensor.fake_profile_dns_queries_relayed") - assert state - assert state.state == "10" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" - - entry = registry.async_get("sensor.fake_profile_dns_queries_relayed") - assert entry - assert entry.unique_id == "xyz12_relayed_queries" - - state = hass.states.get("sensor.fake_profile_dns_over_https_queries") - assert state - assert state.state == "20" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" - - entry = registry.async_get("sensor.fake_profile_dns_over_https_queries") - assert entry - assert entry.unique_id == "xyz12_doh_queries" - - state = hass.states.get("sensor.fake_profile_dns_over_https_queries_ratio") - assert state - assert state.state == "17.4" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - - entry = registry.async_get("sensor.fake_profile_dns_over_https_queries_ratio") - assert entry - assert entry.unique_id == "xyz12_doh_queries_ratio" - - state = hass.states.get("sensor.fake_profile_dns_over_http_3_queries") - assert state - assert state.state == "15" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" - - entry = registry.async_get("sensor.fake_profile_dns_over_http_3_queries") - assert entry - assert entry.unique_id == "xyz12_doh3_queries" - - state = hass.states.get("sensor.fake_profile_dns_over_http_3_queries_ratio") - assert state - assert state.state == "13.0" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - - entry = registry.async_get("sensor.fake_profile_dns_over_http_3_queries_ratio") - assert entry - assert entry.unique_id == "xyz12_doh3_queries_ratio" - - state = hass.states.get("sensor.fake_profile_dns_over_quic_queries") - assert state - assert state.state == "10" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" - - entry = registry.async_get("sensor.fake_profile_dns_over_quic_queries") - assert entry - assert entry.unique_id == "xyz12_doq_queries" - - state = hass.states.get("sensor.fake_profile_dns_over_quic_queries_ratio") - assert state - assert state.state == "8.7" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - - entry = registry.async_get("sensor.fake_profile_dns_over_quic_queries_ratio") - assert entry - assert entry.unique_id == "xyz12_doq_queries_ratio" - - state = hass.states.get("sensor.fake_profile_dns_over_tls_queries") - assert state - assert state.state == "30" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" - - entry = registry.async_get("sensor.fake_profile_dns_over_tls_queries") - assert entry - assert entry.unique_id == "xyz12_dot_queries" - - state = hass.states.get("sensor.fake_profile_dns_over_tls_queries_ratio") - assert state - assert state.state == "26.1" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - - entry = registry.async_get("sensor.fake_profile_dns_over_tls_queries_ratio") - assert entry - assert entry.unique_id == "xyz12_dot_queries_ratio" - - state = hass.states.get("sensor.fake_profile_dnssec_not_validated_queries") - assert state - assert state.state == "25" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" - - entry = registry.async_get("sensor.fake_profile_dnssec_not_validated_queries") - assert entry - assert entry.unique_id == "xyz12_not_validated_queries" - - state = hass.states.get("sensor.fake_profile_dnssec_validated_queries") - assert state - assert state.state == "75" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" - - entry = registry.async_get("sensor.fake_profile_dnssec_validated_queries") - assert entry - assert entry.unique_id == "xyz12_validated_queries" - - state = hass.states.get("sensor.fake_profile_dnssec_validated_queries_ratio") - assert state - assert state.state == "75.0" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - - entry = registry.async_get("sensor.fake_profile_dnssec_validated_queries_ratio") - assert entry - assert entry.unique_id == "xyz12_validated_queries_ratio" - - state = hass.states.get("sensor.fake_profile_encrypted_queries") - assert state - assert state.state == "60" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" - - entry = registry.async_get("sensor.fake_profile_encrypted_queries") - assert entry - assert entry.unique_id == "xyz12_encrypted_queries" - - state = hass.states.get("sensor.fake_profile_unencrypted_queries") - assert state - assert state.state == "40" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" - - entry = registry.async_get("sensor.fake_profile_unencrypted_queries") - assert entry - assert entry.unique_id == "xyz12_unencrypted_queries" - - state = hass.states.get("sensor.fake_profile_encrypted_queries_ratio") - assert state - assert state.state == "60.0" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - - entry = registry.async_get("sensor.fake_profile_encrypted_queries_ratio") - assert entry - assert entry.unique_id == "xyz12_encrypted_queries_ratio" - - state = hass.states.get("sensor.fake_profile_ipv4_queries") - assert state - assert state.state == "90" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" - - entry = registry.async_get("sensor.fake_profile_ipv4_queries") - assert entry - assert entry.unique_id == "xyz12_ipv4_queries" - - state = hass.states.get("sensor.fake_profile_ipv6_queries") - assert state - assert state.state == "10" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" - - entry = registry.async_get("sensor.fake_profile_ipv6_queries") - assert entry - assert entry.unique_id == "xyz12_ipv6_queries" - - state = hass.states.get("sensor.fake_profile_ipv6_queries_ratio") - assert state - assert state.state == "10.0" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - - entry = registry.async_get("sensor.fake_profile_ipv6_queries_ratio") - assert entry - assert entry.unique_id == "xyz12_ipv6_queries_ratio" - - state = hass.states.get("sensor.fake_profile_tcp_queries") - assert state - assert state.state == "0" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" - - entry = registry.async_get("sensor.fake_profile_tcp_queries") - assert entry - assert entry.unique_id == "xyz12_tcp_queries" - - state = hass.states.get("sensor.fake_profile_tcp_queries_ratio") - assert state - assert state.state == "0.0" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - - entry = registry.async_get("sensor.fake_profile_tcp_queries_ratio") - assert entry - assert entry.unique_id == "xyz12_tcp_queries_ratio" - - state = hass.states.get("sensor.fake_profile_udp_queries") - assert state - assert state.state == "40" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" - - entry = registry.async_get("sensor.fake_profile_udp_queries") - assert entry - assert entry.unique_id == "xyz12_udp_queries" - - state = hass.states.get("sensor.fake_profile_udp_queries_ratio") - assert state - assert state.state == "34.8" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - - entry = registry.async_get("sensor.fake_profile_udp_queries_ratio") - assert entry - assert entry.unique_id == "xyz12_udp_queries_ratio" + assert entity_entries + for entity_entry in entity_entries: + assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry") + assert (state := hass.states.get(entity_entry.entity_id)) + assert state == snapshot(name=f"{entity_entry.entity_id}-state") async def test_availability( - hass: HomeAssistant, entity_registry_enabled_by_default: None + 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.""" - er.async_get(hass) - await init_integration(hass) state = hass.states.get("sensor.fake_profile_dns_queries") diff --git a/tests/components/nextdns/test_switch.py b/tests/components/nextdns/test_switch.py index a9dd0ba5cbd..5e027c6789c 100644 --- a/tests/components/nextdns/test_switch.py +++ b/tests/components/nextdns/test_switch.py @@ -7,6 +7,7 @@ from aiohttp import ClientError from aiohttp.client_exceptions import ClientConnectorError from nextdns import ApiError import pytest +from syrupy import SnapshotAssertion from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ( @@ -16,6 +17,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNAVAILABLE, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -28,602 +30,22 @@ from tests.common import async_fire_time_changed async def test_switch( - hass: HomeAssistant, entity_registry_enabled_by_default: None + hass: HomeAssistant, + entity_registry_enabled_by_default: None, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, ) -> None: """Test states of the switches.""" - registry = er.async_get(hass) + with patch("homeassistant.components.nextdns.PLATFORMS", [Platform.SWITCH]): + entry = await init_integration(hass) - await init_integration(hass) + entity_entries = er.async_entries_for_config_entry(entity_registry, entry.entry_id) - state = hass.states.get("switch.fake_profile_ai_driven_threat_detection") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_ai_driven_threat_detection") - assert entry - assert entry.unique_id == "xyz12_ai_threat_detection" - - state = hass.states.get("switch.fake_profile_allow_affiliate_tracking_links") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_allow_affiliate_tracking_links") - assert entry - assert entry.unique_id == "xyz12_allow_affiliate" - - state = hass.states.get("switch.fake_profile_anonymized_edns_client_subnet") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_anonymized_edns_client_subnet") - assert entry - assert entry.unique_id == "xyz12_anonymized_ecs" - - state = hass.states.get("switch.fake_profile_block_bypass_methods") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_bypass_methods") - assert entry - assert entry.unique_id == "xyz12_block_bypass_methods" - - state = hass.states.get("switch.fake_profile_block_child_sexual_abuse_material") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_child_sexual_abuse_material") - assert entry - assert entry.unique_id == "xyz12_block_csam" - - state = hass.states.get("switch.fake_profile_block_disguised_third_party_trackers") - assert state - assert state.state == STATE_ON - - entry = registry.async_get( - "switch.fake_profile_block_disguised_third_party_trackers" - ) - assert entry - assert entry.unique_id == "xyz12_block_disguised_trackers" - - state = hass.states.get("switch.fake_profile_block_dynamic_dns_hostnames") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_dynamic_dns_hostnames") - assert entry - assert entry.unique_id == "xyz12_block_ddns" - - state = hass.states.get("switch.fake_profile_block_newly_registered_domains") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_newly_registered_domains") - assert entry - assert entry.unique_id == "xyz12_block_nrd" - - state = hass.states.get("switch.fake_profile_block_page") - assert state - assert state.state == STATE_OFF - - entry = registry.async_get("switch.fake_profile_block_page") - assert entry - assert entry.unique_id == "xyz12_block_page" - - state = hass.states.get("switch.fake_profile_block_parked_domains") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_parked_domains") - assert entry - assert entry.unique_id == "xyz12_block_parked_domains" - - state = hass.states.get("switch.fake_profile_cname_flattening") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_cname_flattening") - assert entry - assert entry.unique_id == "xyz12_cname_flattening" - - state = hass.states.get("switch.fake_profile_cache_boost") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_cache_boost") - assert entry - assert entry.unique_id == "xyz12_cache_boost" - - state = hass.states.get("switch.fake_profile_cryptojacking_protection") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_cryptojacking_protection") - assert entry - assert entry.unique_id == "xyz12_cryptojacking_protection" - - state = hass.states.get("switch.fake_profile_dns_rebinding_protection") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_dns_rebinding_protection") - assert entry - assert entry.unique_id == "xyz12_dns_rebinding_protection" - - state = hass.states.get( - "switch.fake_profile_domain_generation_algorithms_protection" - ) - assert state - assert state.state == STATE_ON - - entry = registry.async_get( - "switch.fake_profile_domain_generation_algorithms_protection" - ) - assert entry - assert entry.unique_id == "xyz12_dga_protection" - - state = hass.states.get("switch.fake_profile_force_safesearch") - assert state - assert state.state == STATE_OFF - - entry = registry.async_get("switch.fake_profile_force_safesearch") - assert entry - assert entry.unique_id == "xyz12_safesearch" - - state = hass.states.get("switch.fake_profile_force_youtube_restricted_mode") - assert state - assert state.state == STATE_OFF - - entry = registry.async_get("switch.fake_profile_force_youtube_restricted_mode") - assert entry - assert entry.unique_id == "xyz12_youtube_restricted_mode" - - state = hass.states.get("switch.fake_profile_google_safe_browsing") - assert state - assert state.state == STATE_OFF - - entry = registry.async_get("switch.fake_profile_google_safe_browsing") - assert entry - assert entry.unique_id == "xyz12_google_safe_browsing" - - state = hass.states.get("switch.fake_profile_idn_homograph_attacks_protection") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_idn_homograph_attacks_protection") - assert entry - assert entry.unique_id == "xyz12_idn_homograph_attacks_protection" - - state = hass.states.get("switch.fake_profile_logs") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_logs") - assert entry - assert entry.unique_id == "xyz12_logs" - - state = hass.states.get("switch.fake_profile_threat_intelligence_feeds") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_threat_intelligence_feeds") - assert entry - assert entry.unique_id == "xyz12_threat_intelligence_feeds" - - state = hass.states.get("switch.fake_profile_typosquatting_protection") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_typosquatting_protection") - assert entry - assert entry.unique_id == "xyz12_typosquatting_protection" - - state = hass.states.get("switch.fake_profile_web3") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_web3") - assert entry - assert entry.unique_id == "xyz12_web3" - - state = hass.states.get("switch.fake_profile_block_9gag") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_9gag") - assert entry - assert entry.unique_id == "xyz12_block_9gag" - - state = hass.states.get("switch.fake_profile_block_amazon") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_amazon") - assert entry - assert entry.unique_id == "xyz12_block_amazon" - - state = hass.states.get("switch.fake_profile_block_bereal") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_bereal") - assert entry - assert entry.unique_id == "xyz12_block_bereal" - - state = hass.states.get("switch.fake_profile_block_blizzard") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_blizzard") - assert entry - assert entry.unique_id == "xyz12_block_blizzard" - - state = hass.states.get("switch.fake_profile_block_chatgpt") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_chatgpt") - assert entry - assert entry.unique_id == "xyz12_block_chatgpt" - - state = hass.states.get("switch.fake_profile_block_dailymotion") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_dailymotion") - assert entry - assert entry.unique_id == "xyz12_block_dailymotion" - - state = hass.states.get("switch.fake_profile_block_discord") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_discord") - assert entry - assert entry.unique_id == "xyz12_block_discord" - - state = hass.states.get("switch.fake_profile_block_disney_plus") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_disney_plus") - assert entry - assert entry.unique_id == "xyz12_block_disneyplus" - - state = hass.states.get("switch.fake_profile_block_ebay") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_ebay") - assert entry - assert entry.unique_id == "xyz12_block_ebay" - - state = hass.states.get("switch.fake_profile_block_facebook") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_facebook") - assert entry - assert entry.unique_id == "xyz12_block_facebook" - - state = hass.states.get("switch.fake_profile_block_fortnite") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_fortnite") - assert entry - assert entry.unique_id == "xyz12_block_fortnite" - - state = hass.states.get("switch.fake_profile_block_google_chat") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_google_chat") - assert entry - assert entry.unique_id == "xyz12_block_google_chat" - - state = hass.states.get("switch.fake_profile_block_hbo_max") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_hbo_max") - assert entry - assert entry.unique_id == "xyz12_block_hbomax" - - state = hass.states.get("switch.fake_profile_block_hulu") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_hulu") - assert entry - assert entry.unique_id == "xyz12_block_hulu" - - state = hass.states.get("switch.fake_profile_block_imgur") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_imgur") - assert entry - assert entry.unique_id == "xyz12_block_imgur" - - state = hass.states.get("switch.fake_profile_block_instagram") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_instagram") - assert entry - assert entry.unique_id == "xyz12_block_instagram" - - state = hass.states.get("switch.fake_profile_block_league_of_legends") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_league_of_legends") - assert entry - assert entry.unique_id == "xyz12_block_leagueoflegends" - - state = hass.states.get("switch.fake_profile_block_mastodon") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_mastodon") - assert entry - assert entry.unique_id == "xyz12_block_mastodon" - - state = hass.states.get("switch.fake_profile_block_messenger") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_messenger") - assert entry - assert entry.unique_id == "xyz12_block_messenger" - - state = hass.states.get("switch.fake_profile_block_minecraft") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_minecraft") - assert entry - assert entry.unique_id == "xyz12_block_minecraft" - - state = hass.states.get("switch.fake_profile_block_netflix") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_netflix") - assert entry - assert entry.unique_id == "xyz12_block_netflix" - - state = hass.states.get("switch.fake_profile_block_pinterest") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_pinterest") - assert entry - assert entry.unique_id == "xyz12_block_pinterest" - - state = hass.states.get("switch.fake_profile_block_playstation_network") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_playstation_network") - assert entry - assert entry.unique_id == "xyz12_block_playstation_network" - - state = hass.states.get("switch.fake_profile_block_prime_video") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_prime_video") - assert entry - assert entry.unique_id == "xyz12_block_primevideo" - - state = hass.states.get("switch.fake_profile_block_reddit") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_reddit") - assert entry - assert entry.unique_id == "xyz12_block_reddit" - - state = hass.states.get("switch.fake_profile_block_roblox") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_roblox") - assert entry - assert entry.unique_id == "xyz12_block_roblox" - - state = hass.states.get("switch.fake_profile_block_signal") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_signal") - assert entry - assert entry.unique_id == "xyz12_block_signal" - - state = hass.states.get("switch.fake_profile_block_skype") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_skype") - assert entry - assert entry.unique_id == "xyz12_block_skype" - - state = hass.states.get("switch.fake_profile_block_snapchat") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_snapchat") - assert entry - assert entry.unique_id == "xyz12_block_snapchat" - - state = hass.states.get("switch.fake_profile_block_spotify") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_spotify") - assert entry - assert entry.unique_id == "xyz12_block_spotify" - - state = hass.states.get("switch.fake_profile_block_steam") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_steam") - assert entry - assert entry.unique_id == "xyz12_block_steam" - - state = hass.states.get("switch.fake_profile_block_telegram") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_telegram") - assert entry - assert entry.unique_id == "xyz12_block_telegram" - - state = hass.states.get("switch.fake_profile_block_tiktok") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_tiktok") - assert entry - assert entry.unique_id == "xyz12_block_tiktok" - - state = hass.states.get("switch.fake_profile_block_tinder") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_tinder") - assert entry - assert entry.unique_id == "xyz12_block_tinder" - - state = hass.states.get("switch.fake_profile_block_tumblr") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_tumblr") - assert entry - assert entry.unique_id == "xyz12_block_tumblr" - - state = hass.states.get("switch.fake_profile_block_twitch") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_twitch") - assert entry - assert entry.unique_id == "xyz12_block_twitch" - - state = hass.states.get("switch.fake_profile_block_x_formerly_twitter") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_x_formerly_twitter") - assert entry - assert entry.unique_id == "xyz12_block_twitter" - - state = hass.states.get("switch.fake_profile_block_vimeo") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_vimeo") - assert entry - assert entry.unique_id == "xyz12_block_vimeo" - - state = hass.states.get("switch.fake_profile_block_vk") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_vk") - assert entry - assert entry.unique_id == "xyz12_block_vk" - - state = hass.states.get("switch.fake_profile_block_whatsapp") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_whatsapp") - assert entry - assert entry.unique_id == "xyz12_block_whatsapp" - - state = hass.states.get("switch.fake_profile_block_xbox_live") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_xbox_live") - assert entry - assert entry.unique_id == "xyz12_block_xboxlive" - - state = hass.states.get("switch.fake_profile_block_youtube") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_youtube") - assert entry - assert entry.unique_id == "xyz12_block_youtube" - - state = hass.states.get("switch.fake_profile_block_zoom") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_zoom") - assert entry - assert entry.unique_id == "xyz12_block_zoom" - - state = hass.states.get("switch.fake_profile_block_dating") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_dating") - assert entry - assert entry.unique_id == "xyz12_block_dating" - - state = hass.states.get("switch.fake_profile_block_gambling") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_gambling") - assert entry - assert entry.unique_id == "xyz12_block_gambling" - - state = hass.states.get("switch.fake_profile_block_online_gaming") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_online_gaming") - assert entry - assert entry.unique_id == "xyz12_block_online_gaming" - - state = hass.states.get("switch.fake_profile_block_piracy") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_piracy") - assert entry - assert entry.unique_id == "xyz12_block_piracy" - - state = hass.states.get("switch.fake_profile_block_porn") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_porn") - assert entry - assert entry.unique_id == "xyz12_block_porn" - - state = hass.states.get("switch.fake_profile_block_social_networks") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_social_networks") - assert entry - assert entry.unique_id == "xyz12_block_social_networks" - - state = hass.states.get("switch.fake_profile_block_video_streaming") - assert state - assert state.state == STATE_ON - - entry = registry.async_get("switch.fake_profile_block_video_streaming") - assert entry - assert entry.unique_id == "xyz12_block_video_streaming" + assert entity_entries + for entity_entry in entity_entries: + assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry") + assert (state := hass.states.get(entity_entry.entity_id)) + assert state == snapshot(name=f"{entity_entry.entity_id}-state") async def test_switch_on(hass: HomeAssistant) -> None: From c2450c111214cb760ad05abe5a1347fc0c674d49 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 20 Apr 2024 12:32:11 +0200 Subject: [PATCH 700/967] Use snapshot testing in GIOS sensor (#115876) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- .../gios/snapshots/test_sensor.ambr | 774 ++++++++++++++++++ tests/components/gios/test_sensor.py | 245 +----- 2 files changed, 789 insertions(+), 230 deletions(-) create mode 100644 tests/components/gios/snapshots/test_sensor.ambr diff --git a/tests/components/gios/snapshots/test_sensor.ambr b/tests/components/gios/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..c67cc3e4d7c --- /dev/null +++ b/tests/components/gios/snapshots/test_sensor.ambr @@ -0,0 +1,774 @@ +# serializer version: 1 +# name: test_sensor[sensor.home_air_quality_index-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'very_bad', + 'bad', + 'sufficient', + 'moderate', + 'good', + 'very_good', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_air_quality_index', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Air quality index', + 'platform': 'gios', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'aqi', + 'unique_id': '123-aqi', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.home_air_quality_index-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by GIOŚ', + 'device_class': 'enum', + 'friendly_name': 'Home Air quality index', + 'options': list([ + 'very_bad', + 'bad', + 'sufficient', + 'moderate', + 'good', + 'very_good', + ]), + }), + 'context': , + 'entity_id': 'sensor.home_air_quality_index', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'good', + }) +# --- +# name: test_sensor[sensor.home_benzene-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.home_benzene', + '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': 'Benzene', + 'platform': 'gios', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'c6h6', + 'unique_id': '123-c6h6', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensor[sensor.home_benzene-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by GIOŚ', + 'friendly_name': 'Home Benzene', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.home_benzene', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.23789', + }) +# --- +# name: test_sensor[sensor.home_carbon_monoxide-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.home_carbon_monoxide', + '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': 'Carbon monoxide', + 'platform': 'gios', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'co', + 'unique_id': '123-co', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensor[sensor.home_carbon_monoxide-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by GIOŚ', + 'friendly_name': 'Home Carbon monoxide', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.home_carbon_monoxide', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '251.874', + }) +# --- +# name: test_sensor[sensor.home_nitrogen_dioxide-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.home_nitrogen_dioxide', + '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': 'Nitrogen dioxide', + 'platform': 'gios', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123-no2', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensor[sensor.home_nitrogen_dioxide-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by GIOŚ', + 'device_class': 'nitrogen_dioxide', + 'friendly_name': 'Home Nitrogen dioxide', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.home_nitrogen_dioxide', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.13411', + }) +# --- +# name: test_sensor[sensor.home_nitrogen_dioxide_index-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'very_bad', + 'bad', + 'sufficient', + 'moderate', + 'good', + 'very_good', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_nitrogen_dioxide_index', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Nitrogen dioxide index', + 'platform': 'gios', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'no2_index', + 'unique_id': '123-no2-index', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.home_nitrogen_dioxide_index-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by GIOŚ', + 'device_class': 'enum', + 'friendly_name': 'Home Nitrogen dioxide index', + 'options': list([ + 'very_bad', + 'bad', + 'sufficient', + 'moderate', + 'good', + 'very_good', + ]), + }), + 'context': , + 'entity_id': 'sensor.home_nitrogen_dioxide_index', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'good', + }) +# --- +# name: test_sensor[sensor.home_ozone-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.home_ozone', + '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': 'Ozone', + 'platform': 'gios', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123-o3', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensor[sensor.home_ozone-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by GIOŚ', + 'device_class': 'ozone', + 'friendly_name': 'Home Ozone', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.home_ozone', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '95.7768', + }) +# --- +# name: test_sensor[sensor.home_ozone_index-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'very_bad', + 'bad', + 'sufficient', + 'moderate', + 'good', + 'very_good', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_ozone_index', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Ozone index', + 'platform': 'gios', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'o3_index', + 'unique_id': '123-o3-index', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.home_ozone_index-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by GIOŚ', + 'device_class': 'enum', + 'friendly_name': 'Home Ozone index', + 'options': list([ + 'very_bad', + 'bad', + 'sufficient', + 'moderate', + 'good', + 'very_good', + ]), + }), + 'context': , + 'entity_id': 'sensor.home_ozone_index', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'good', + }) +# --- +# name: test_sensor[sensor.home_pm10-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.home_pm10', + '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': 'PM10', + 'platform': 'gios', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123-pm10', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensor[sensor.home_pm10-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by GIOŚ', + 'device_class': 'pm10', + 'friendly_name': 'Home PM10', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.home_pm10', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '16.8344', + }) +# --- +# name: test_sensor[sensor.home_pm10_index-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'very_bad', + 'bad', + 'sufficient', + 'moderate', + 'good', + 'very_good', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_pm10_index', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'PM10 index', + 'platform': 'gios', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pm10_index', + 'unique_id': '123-pm10-index', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.home_pm10_index-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by GIOŚ', + 'device_class': 'enum', + 'friendly_name': 'Home PM10 index', + 'options': list([ + 'very_bad', + 'bad', + 'sufficient', + 'moderate', + 'good', + 'very_good', + ]), + }), + 'context': , + 'entity_id': 'sensor.home_pm10_index', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'good', + }) +# --- +# name: test_sensor[sensor.home_pm2_5-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.home_pm2_5', + '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': 'PM2.5', + 'platform': 'gios', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123-pm25', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensor[sensor.home_pm2_5-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by GIOŚ', + 'device_class': 'pm25', + 'friendly_name': 'Home PM2.5', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.home_pm2_5', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4', + }) +# --- +# name: test_sensor[sensor.home_pm2_5_index-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'very_bad', + 'bad', + 'sufficient', + 'moderate', + 'good', + 'very_good', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_pm2_5_index', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'PM2.5 index', + 'platform': 'gios', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pm25_index', + 'unique_id': '123-pm25-index', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.home_pm2_5_index-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by GIOŚ', + 'device_class': 'enum', + 'friendly_name': 'Home PM2.5 index', + 'options': list([ + 'very_bad', + 'bad', + 'sufficient', + 'moderate', + 'good', + 'very_good', + ]), + }), + 'context': , + 'entity_id': 'sensor.home_pm2_5_index', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'good', + }) +# --- +# name: test_sensor[sensor.home_sulphur_dioxide-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.home_sulphur_dioxide', + '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': 'Sulphur dioxide', + 'platform': 'gios', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123-so2', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensor[sensor.home_sulphur_dioxide-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by GIOŚ', + 'device_class': 'sulphur_dioxide', + 'friendly_name': 'Home Sulphur dioxide', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.home_sulphur_dioxide', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4.35478', + }) +# --- +# name: test_sensor[sensor.home_sulphur_dioxide_index-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'very_bad', + 'bad', + 'sufficient', + 'moderate', + 'good', + 'very_good', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.home_sulphur_dioxide_index', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Sulphur dioxide index', + 'platform': 'gios', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'so2_index', + 'unique_id': '123-so2-index', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.home_sulphur_dioxide_index-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by GIOŚ', + 'device_class': 'enum', + 'friendly_name': 'Home Sulphur dioxide index', + 'options': list([ + 'very_bad', + 'bad', + 'sufficient', + 'moderate', + 'good', + 'very_good', + ]), + }), + 'context': , + 'entity_id': 'sensor.home_sulphur_dioxide_index', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'very_good', + }) +# --- diff --git a/tests/components/gios/test_sensor.py b/tests/components/gios/test_sensor.py index 60e8722ba24..e760e050f2b 100644 --- a/tests/components/gios/test_sensor.py +++ b/tests/components/gios/test_sensor.py @@ -6,23 +6,11 @@ import json from unittest.mock import patch from gios import ApiError +from syrupy import SnapshotAssertion -from homeassistant.components.gios.const import ATTRIBUTION, DOMAIN -from homeassistant.components.sensor import ( - ATTR_OPTIONS, - ATTR_STATE_CLASS, - DOMAIN as PLATFORM, - SensorDeviceClass, - SensorStateClass, -) -from homeassistant.const import ( - ATTR_ATTRIBUTION, - ATTR_DEVICE_CLASS, - ATTR_ICON, - ATTR_UNIT_OF_MEASUREMENT, - CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - STATE_UNAVAILABLE, -) +from homeassistant.components.gios.const import DOMAIN +from homeassistant.components.sensor import DOMAIN as PLATFORM +from homeassistant.const import STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.util.dt import utcnow @@ -32,223 +20,20 @@ from . import init_integration from tests.common import async_fire_time_changed, load_fixture -async def test_sensor(hass: HomeAssistant, entity_registry: er.EntityRegistry) -> None: +async def test_sensor( + hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion +) -> None: """Test states of the sensor.""" - await init_integration(hass) + with patch("homeassistant.components.gios.PLATFORMS", [Platform.SENSOR]): + entry = await init_integration(hass) - state = hass.states.get("sensor.home_benzene") - assert state - assert state.state == "0.23789" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - ) - assert state.attributes.get(ATTR_ICON) is None + entity_entries = er.async_entries_for_config_entry(entity_registry, entry.entry_id) - entry = entity_registry.async_get("sensor.home_benzene") - assert entry - assert entry.unique_id == "123-c6h6" - - state = hass.states.get("sensor.home_carbon_monoxide") - assert state - assert state.state == "251.874" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_DEVICE_CLASS) is None - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - ) - - entry = entity_registry.async_get("sensor.home_carbon_monoxide") - assert entry - assert entry.unique_id == "123-co" - - state = hass.states.get("sensor.home_nitrogen_dioxide") - assert state - assert state.state == "7.13411" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.NITROGEN_DIOXIDE - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - ) - - entry = entity_registry.async_get("sensor.home_nitrogen_dioxide") - assert entry - assert entry.unique_id == "123-no2" - - state = hass.states.get("sensor.home_nitrogen_dioxide_index") - assert state - assert state.state == "good" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None - assert state.attributes.get(ATTR_OPTIONS) == [ - "very_bad", - "bad", - "sufficient", - "moderate", - "good", - "very_good", - ] - - entry = entity_registry.async_get("sensor.home_nitrogen_dioxide_index") - assert entry - assert entry.unique_id == "123-no2-index" - - state = hass.states.get("sensor.home_ozone") - assert state - assert state.state == "95.7768" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.OZONE - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - ) - - entry = entity_registry.async_get("sensor.home_ozone") - assert entry - assert entry.unique_id == "123-o3" - - state = hass.states.get("sensor.home_ozone_index") - assert state - assert state.state == "good" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None - assert state.attributes.get(ATTR_OPTIONS) == [ - "very_bad", - "bad", - "sufficient", - "moderate", - "good", - "very_good", - ] - - entry = entity_registry.async_get("sensor.home_ozone_index") - assert entry - assert entry.unique_id == "123-o3-index" - - state = hass.states.get("sensor.home_pm10") - assert state - assert state.state == "16.8344" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM10 - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - ) - - entry = entity_registry.async_get("sensor.home_pm10") - assert entry - assert entry.unique_id == "123-pm10" - - state = hass.states.get("sensor.home_pm10_index") - assert state - assert state.state == "good" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None - assert state.attributes.get(ATTR_OPTIONS) == [ - "very_bad", - "bad", - "sufficient", - "moderate", - "good", - "very_good", - ] - - entry = entity_registry.async_get("sensor.home_pm10_index") - assert entry - assert entry.unique_id == "123-pm10-index" - - state = hass.states.get("sensor.home_pm2_5") - assert state - assert state.state == "4" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM25 - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - ) - - entry = entity_registry.async_get("sensor.home_pm2_5") - assert entry - assert entry.unique_id == "123-pm25" - - state = hass.states.get("sensor.home_pm2_5_index") - assert state - assert state.state == "good" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None - assert state.attributes.get(ATTR_OPTIONS) == [ - "very_bad", - "bad", - "sufficient", - "moderate", - "good", - "very_good", - ] - - entry = entity_registry.async_get("sensor.home_pm2_5_index") - assert entry - assert entry.unique_id == "123-pm25-index" - - state = hass.states.get("sensor.home_sulphur_dioxide") - assert state - assert state.state == "4.35478" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.SULPHUR_DIOXIDE - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - ) - - entry = entity_registry.async_get("sensor.home_sulphur_dioxide") - assert entry - assert entry.unique_id == "123-so2" - - state = hass.states.get("sensor.home_sulphur_dioxide_index") - assert state - assert state.state == "very_good" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None - assert state.attributes.get(ATTR_OPTIONS) == [ - "very_bad", - "bad", - "sufficient", - "moderate", - "good", - "very_good", - ] - - entry = entity_registry.async_get("sensor.home_sulphur_dioxide_index") - assert entry - assert entry.unique_id == "123-so2-index" - - state = hass.states.get("sensor.home_air_quality_index") - assert state - assert state.state == "good" - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_STATE_CLASS) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None - assert state.attributes.get(ATTR_OPTIONS) == [ - "very_bad", - "bad", - "sufficient", - "moderate", - "good", - "very_good", - ] - - entry = entity_registry.async_get("sensor.home_air_quality_index") - assert entry - assert entry.unique_id == "123-aqi" + assert entity_entries + for entity_entry in entity_entries: + assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry") + assert (state := hass.states.get(entity_entry.entity_id)) + assert state == snapshot(name=f"{entity_entry.entity_id}-state") async def test_availability(hass: HomeAssistant) -> None: From c4e7a7af21b761473c1a3d66c2fc30a457df5969 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 20 Apr 2024 12:33:08 +0200 Subject: [PATCH 701/967] Use snapshot testing in Brother sensor (#115875) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- .../brother/snapshots/test_sensor.ambr | 1394 +++++++++++++++++ tests/components/brother/test_sensor.py | 395 +---- 2 files changed, 1420 insertions(+), 369 deletions(-) create mode 100644 tests/components/brother/snapshots/test_sensor.ambr diff --git a/tests/components/brother/snapshots/test_sensor.ambr b/tests/components/brother/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..a27c5addd61 --- /dev/null +++ b/tests/components/brother/snapshots/test_sensor.ambr @@ -0,0 +1,1394 @@ +# serializer version: 1 +# name: test_sensors[sensor.hl_l2340dw_b_w_pages-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.hl_l2340dw_b_w_pages', + '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': 'B/W pages', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'bw_pages', + 'unique_id': '0123456789_bw_counter', + 'unit_of_measurement': 'p', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_b_w_pages-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW B/W pages', + 'state_class': , + 'unit_of_measurement': 'p', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_b_w_pages', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '709', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_belt_unit_remaining_lifetime-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.hl_l2340dw_belt_unit_remaining_lifetime', + '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': 'Belt unit remaining lifetime', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'belt_unit_remaining_life', + 'unique_id': '0123456789_belt_unit_remaining_life', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_belt_unit_remaining_lifetime-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Belt unit remaining lifetime', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_belt_unit_remaining_lifetime', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '97', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_black_drum_page_counter-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.hl_l2340dw_black_drum_page_counter', + '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': 'Black drum page counter', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'black_drum_page_counter', + 'unique_id': '0123456789_black_drum_counter', + 'unit_of_measurement': 'p', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_black_drum_page_counter-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Black drum page counter', + 'state_class': , + 'unit_of_measurement': 'p', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_black_drum_page_counter', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1611', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_black_drum_remaining_lifetime-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.hl_l2340dw_black_drum_remaining_lifetime', + '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': 'Black drum remaining lifetime', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'black_drum_remaining_life', + 'unique_id': '0123456789_black_drum_remaining_life', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_black_drum_remaining_lifetime-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Black drum remaining lifetime', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_black_drum_remaining_lifetime', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '92', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_black_drum_remaining_pages-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.hl_l2340dw_black_drum_remaining_pages', + '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': 'Black drum remaining pages', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'black_drum_remaining_pages', + 'unique_id': '0123456789_black_drum_remaining_pages', + 'unit_of_measurement': 'p', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_black_drum_remaining_pages-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Black drum remaining pages', + 'state_class': , + 'unit_of_measurement': 'p', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_black_drum_remaining_pages', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '16389', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_black_toner_remaining-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.hl_l2340dw_black_toner_remaining', + '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': 'Black toner remaining', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'black_toner_remaining', + 'unique_id': '0123456789_black_toner_remaining', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_black_toner_remaining-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Black toner remaining', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_black_toner_remaining', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '75', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_color_pages-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.hl_l2340dw_color_pages', + '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': 'Color pages', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'color_pages', + 'unique_id': '0123456789_color_counter', + 'unit_of_measurement': 'p', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_color_pages-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Color pages', + 'state_class': , + 'unit_of_measurement': 'p', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_color_pages', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '902', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_cyan_drum_page_counter-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.hl_l2340dw_cyan_drum_page_counter', + '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': 'Cyan drum page counter', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cyan_drum_page_counter', + 'unique_id': '0123456789_cyan_drum_counter', + 'unit_of_measurement': 'p', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_cyan_drum_page_counter-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Cyan drum page counter', + 'state_class': , + 'unit_of_measurement': 'p', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_cyan_drum_page_counter', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1611', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_cyan_drum_remaining_lifetime-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.hl_l2340dw_cyan_drum_remaining_lifetime', + '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': 'Cyan drum remaining lifetime', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cyan_drum_remaining_life', + 'unique_id': '0123456789_cyan_drum_remaining_life', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_cyan_drum_remaining_lifetime-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Cyan drum remaining lifetime', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_cyan_drum_remaining_lifetime', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '92', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_cyan_drum_remaining_pages-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.hl_l2340dw_cyan_drum_remaining_pages', + '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': 'Cyan drum remaining pages', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cyan_drum_remaining_pages', + 'unique_id': '0123456789_cyan_drum_remaining_pages', + 'unit_of_measurement': 'p', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_cyan_drum_remaining_pages-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Cyan drum remaining pages', + 'state_class': , + 'unit_of_measurement': 'p', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_cyan_drum_remaining_pages', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '16389', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_cyan_toner_remaining-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.hl_l2340dw_cyan_toner_remaining', + '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': 'Cyan toner remaining', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cyan_toner_remaining', + 'unique_id': '0123456789_cyan_toner_remaining', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_cyan_toner_remaining-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Cyan toner remaining', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_cyan_toner_remaining', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_drum_page_counter-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.hl_l2340dw_drum_page_counter', + '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': 'Drum page counter', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'drum_page_counter', + 'unique_id': '0123456789_drum_counter', + 'unit_of_measurement': 'p', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_drum_page_counter-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Drum page counter', + 'state_class': , + 'unit_of_measurement': 'p', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_drum_page_counter', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '986', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_drum_remaining_lifetime-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.hl_l2340dw_drum_remaining_lifetime', + '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': 'Drum remaining lifetime', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'drum_remaining_life', + 'unique_id': '0123456789_drum_remaining_life', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_drum_remaining_lifetime-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Drum remaining lifetime', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_drum_remaining_lifetime', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '92', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_drum_remaining_pages-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.hl_l2340dw_drum_remaining_pages', + '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': 'Drum remaining pages', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'drum_remaining_pages', + 'unique_id': '0123456789_drum_remaining_pages', + 'unit_of_measurement': 'p', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_drum_remaining_pages-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Drum remaining pages', + 'state_class': , + 'unit_of_measurement': 'p', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_drum_remaining_pages', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '11014', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_duplex_unit_page_counter-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.hl_l2340dw_duplex_unit_page_counter', + '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': 'Duplex unit page counter', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'duplex_unit_page_counter', + 'unique_id': '0123456789_duplex_unit_pages_counter', + 'unit_of_measurement': 'p', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_duplex_unit_page_counter-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Duplex unit page counter', + 'state_class': , + 'unit_of_measurement': 'p', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_duplex_unit_page_counter', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '538', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_fuser_remaining_lifetime-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.hl_l2340dw_fuser_remaining_lifetime', + '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': 'Fuser remaining lifetime', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'fuser_remaining_life', + 'unique_id': '0123456789_fuser_remaining_life', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_fuser_remaining_lifetime-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Fuser remaining lifetime', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_fuser_remaining_lifetime', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '97', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_last_restart-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.hl_l2340dw_last_restart', + '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 restart', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'last_restart', + 'unique_id': '0123456789_uptime', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_last_restart-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'HL-L2340DW Last restart', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_last_restart', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-03-03T15:04:24+00:00', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_magenta_drum_page_counter-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.hl_l2340dw_magenta_drum_page_counter', + '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': 'Magenta drum page counter', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'magenta_drum_page_counter', + 'unique_id': '0123456789_magenta_drum_counter', + 'unit_of_measurement': 'p', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_magenta_drum_page_counter-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Magenta drum page counter', + 'state_class': , + 'unit_of_measurement': 'p', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_magenta_drum_page_counter', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1611', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_magenta_drum_remaining_lifetime-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.hl_l2340dw_magenta_drum_remaining_lifetime', + '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': 'Magenta drum remaining lifetime', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'magenta_drum_remaining_life', + 'unique_id': '0123456789_magenta_drum_remaining_life', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_magenta_drum_remaining_lifetime-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Magenta drum remaining lifetime', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_magenta_drum_remaining_lifetime', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '92', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_magenta_drum_remaining_pages-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.hl_l2340dw_magenta_drum_remaining_pages', + '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': 'Magenta drum remaining pages', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'magenta_drum_remaining_pages', + 'unique_id': '0123456789_magenta_drum_remaining_pages', + 'unit_of_measurement': 'p', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_magenta_drum_remaining_pages-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Magenta drum remaining pages', + 'state_class': , + 'unit_of_measurement': 'p', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_magenta_drum_remaining_pages', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '16389', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_magenta_toner_remaining-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.hl_l2340dw_magenta_toner_remaining', + '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': 'Magenta toner remaining', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'magenta_toner_remaining', + 'unique_id': '0123456789_magenta_toner_remaining', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_magenta_toner_remaining-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Magenta toner remaining', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_magenta_toner_remaining', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '8', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_page_counter-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.hl_l2340dw_page_counter', + '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': 'Page counter', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'page_counter', + 'unique_id': '0123456789_page_counter', + 'unit_of_measurement': 'p', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_page_counter-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Page counter', + 'state_class': , + 'unit_of_measurement': 'p', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_page_counter', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '986', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_pf_kit_1_remaining_lifetime-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.hl_l2340dw_pf_kit_1_remaining_lifetime', + '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': 'PF Kit 1 remaining lifetime', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pf_kit_1_remaining_life', + 'unique_id': '0123456789_pf_kit_1_remaining_life', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_pf_kit_1_remaining_lifetime-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW PF Kit 1 remaining lifetime', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_pf_kit_1_remaining_lifetime', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '98', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_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': , + 'entity_id': 'sensor.hl_l2340dw_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': 'Status', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'status', + 'unique_id': '0123456789_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Status', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'waiting', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_yellow_drum_page_counter-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.hl_l2340dw_yellow_drum_page_counter', + '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': 'Yellow drum page counter', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'yellow_drum_page_counter', + 'unique_id': '0123456789_yellow_drum_counter', + 'unit_of_measurement': 'p', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_yellow_drum_page_counter-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Yellow drum page counter', + 'state_class': , + 'unit_of_measurement': 'p', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_yellow_drum_page_counter', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1611', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_yellow_drum_remaining_lifetime-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.hl_l2340dw_yellow_drum_remaining_lifetime', + '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': 'Yellow drum remaining lifetime', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'yellow_drum_remaining_life', + 'unique_id': '0123456789_yellow_drum_remaining_life', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_yellow_drum_remaining_lifetime-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Yellow drum remaining lifetime', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_yellow_drum_remaining_lifetime', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '92', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_yellow_drum_remaining_pages-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.hl_l2340dw_yellow_drum_remaining_pages', + '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': 'Yellow drum remaining pages', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'yellow_drum_remaining_pages', + 'unique_id': '0123456789_yellow_drum_remaining_pages', + 'unit_of_measurement': 'p', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_yellow_drum_remaining_pages-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Yellow drum remaining pages', + 'state_class': , + 'unit_of_measurement': 'p', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_yellow_drum_remaining_pages', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '16389', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_yellow_toner_remaining-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.hl_l2340dw_yellow_toner_remaining', + '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': 'Yellow toner remaining', + 'platform': 'brother', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'yellow_toner_remaining', + 'unique_id': '0123456789_yellow_toner_remaining', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.hl_l2340dw_yellow_toner_remaining-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL-L2340DW Yellow toner remaining', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.hl_l2340dw_yellow_toner_remaining', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2', + }) +# --- diff --git a/tests/components/brother/test_sensor.py b/tests/components/brother/test_sensor.py index ff29f8cb368..39aa3b83d6f 100644 --- a/tests/components/brother/test_sensor.py +++ b/tests/components/brother/test_sensor.py @@ -1,389 +1,46 @@ """Test sensor of Brother integration.""" -from datetime import datetime, timedelta +from datetime import timedelta import json -from unittest.mock import Mock, patch +from unittest.mock import patch + +from freezegun.api import FrozenDateTimeFactory +from syrupy import SnapshotAssertion from homeassistant.components.brother.const import DOMAIN -from homeassistant.components.brother.sensor import UNIT_PAGES -from homeassistant.components.sensor import ( - ATTR_STATE_CLASS, - DOMAIN as SENSOR_DOMAIN, - SensorDeviceClass, - SensorStateClass, -) -from homeassistant.const import ( - ATTR_DEVICE_CLASS, - ATTR_ENTITY_ID, - ATTR_ICON, - ATTR_UNIT_OF_MEASUREMENT, - PERCENTAGE, - STATE_UNAVAILABLE, -) +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component -from homeassistant.util.dt import UTC, utcnow +from homeassistant.util.dt import utcnow from . import init_integration from tests.common import async_fire_time_changed, load_fixture -ATTR_REMAINING_PAGES = "remaining_pages" -ATTR_COUNTER = "counter" - -async def test_sensors(hass: HomeAssistant, entity_registry: er.EntityRegistry) -> None: - """Test states of the sensors.""" - entry = await init_integration(hass, skip_setup=True) - - # Pre-create registry entries for disabled by default sensors - entity_registry.async_get_or_create( - SENSOR_DOMAIN, - DOMAIN, - "0123456789_uptime", - suggested_object_id="hl_l2340dw_last_restart", - disabled_by=None, - ) - test_time = datetime(2019, 11, 11, 9, 10, 32, tzinfo=UTC) - with ( - patch("brother.Brother.initialize"), - patch("brother.datetime", now=Mock(return_value=test_time)), - patch( - "brother.Brother._get_data", - return_value=json.loads(load_fixture("printer_data.json", "brother")), - ), - ): - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - state = hass.states.get("sensor.hl_l2340dw_status") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.state == "waiting" - assert state.attributes.get(ATTR_STATE_CLASS) is None - - entry = entity_registry.async_get("sensor.hl_l2340dw_status") - assert entry - assert entry.unique_id == "0123456789_status" - - state = hass.states.get("sensor.hl_l2340dw_black_toner_remaining") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - assert state.state == "75" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_black_toner_remaining") - assert entry - assert entry.unique_id == "0123456789_black_toner_remaining" - - state = hass.states.get("sensor.hl_l2340dw_cyan_toner_remaining") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - assert state.state == "10" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_cyan_toner_remaining") - assert entry - assert entry.unique_id == "0123456789_cyan_toner_remaining" - - state = hass.states.get("sensor.hl_l2340dw_magenta_toner_remaining") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - assert state.state == "8" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_magenta_toner_remaining") - assert entry - assert entry.unique_id == "0123456789_magenta_toner_remaining" - - state = hass.states.get("sensor.hl_l2340dw_yellow_toner_remaining") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - assert state.state == "2" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_yellow_toner_remaining") - assert entry - assert entry.unique_id == "0123456789_yellow_toner_remaining" - - state = hass.states.get("sensor.hl_l2340dw_drum_remaining_lifetime") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - assert state.state == "92" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_drum_remaining_lifetime") - assert entry - assert entry.unique_id == "0123456789_drum_remaining_life" - - state = hass.states.get("sensor.hl_l2340dw_drum_remaining_pages") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES - assert state.state == "11014" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_drum_remaining_pages") - assert entry - assert entry.unique_id == "0123456789_drum_remaining_pages" - - state = hass.states.get("sensor.hl_l2340dw_drum_page_counter") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES - assert state.state == "986" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_drum_page_counter") - assert entry - assert entry.unique_id == "0123456789_drum_counter" - - state = hass.states.get("sensor.hl_l2340dw_black_drum_remaining_lifetime") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - assert state.state == "92" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_black_drum_remaining_lifetime") - assert entry - assert entry.unique_id == "0123456789_black_drum_remaining_life" - - state = hass.states.get("sensor.hl_l2340dw_black_drum_remaining_pages") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES - assert state.state == "16389" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_black_drum_remaining_pages") - assert entry - assert entry.unique_id == "0123456789_black_drum_remaining_pages" - - state = hass.states.get("sensor.hl_l2340dw_black_drum_page_counter") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES - assert state.state == "1611" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_black_drum_page_counter") - assert entry - assert entry.unique_id == "0123456789_black_drum_counter" - - state = hass.states.get("sensor.hl_l2340dw_cyan_drum_remaining_lifetime") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - assert state.state == "92" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_cyan_drum_remaining_lifetime") - assert entry - assert entry.unique_id == "0123456789_cyan_drum_remaining_life" - - state = hass.states.get("sensor.hl_l2340dw_cyan_drum_remaining_pages") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES - assert state.state == "16389" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_cyan_drum_remaining_pages") - assert entry - assert entry.unique_id == "0123456789_cyan_drum_remaining_pages" - - state = hass.states.get("sensor.hl_l2340dw_cyan_drum_page_counter") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES - assert state.state == "1611" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_cyan_drum_page_counter") - assert entry - assert entry.unique_id == "0123456789_cyan_drum_counter" - - state = hass.states.get("sensor.hl_l2340dw_magenta_drum_remaining_lifetime") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - assert state.state == "92" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get( - "sensor.hl_l2340dw_magenta_drum_remaining_lifetime" - ) - assert entry - assert entry.unique_id == "0123456789_magenta_drum_remaining_life" - - state = hass.states.get("sensor.hl_l2340dw_magenta_drum_remaining_pages") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES - assert state.state == "16389" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_magenta_drum_remaining_pages") - assert entry - assert entry.unique_id == "0123456789_magenta_drum_remaining_pages" - - state = hass.states.get("sensor.hl_l2340dw_magenta_drum_page_counter") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES - assert state.state == "1611" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_magenta_drum_page_counter") - assert entry - assert entry.unique_id == "0123456789_magenta_drum_counter" - - state = hass.states.get("sensor.hl_l2340dw_yellow_drum_remaining_lifetime") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - assert state.state == "92" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get( - "sensor.hl_l2340dw_yellow_drum_remaining_lifetime" - ) - assert entry - assert entry.unique_id == "0123456789_yellow_drum_remaining_life" - - state = hass.states.get("sensor.hl_l2340dw_yellow_drum_remaining_pages") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES - assert state.state == "16389" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_yellow_drum_remaining_pages") - assert entry - assert entry.unique_id == "0123456789_yellow_drum_remaining_pages" - - state = hass.states.get("sensor.hl_l2340dw_yellow_drum_page_counter") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES - assert state.state == "1611" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_yellow_drum_page_counter") - assert entry - assert entry.unique_id == "0123456789_yellow_drum_counter" - - state = hass.states.get("sensor.hl_l2340dw_fuser_remaining_lifetime") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - assert state.state == "97" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_fuser_remaining_lifetime") - assert entry - assert entry.unique_id == "0123456789_fuser_remaining_life" - - state = hass.states.get("sensor.hl_l2340dw_belt_unit_remaining_lifetime") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - assert state.state == "97" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_belt_unit_remaining_lifetime") - assert entry - assert entry.unique_id == "0123456789_belt_unit_remaining_life" - - state = hass.states.get("sensor.hl_l2340dw_pf_kit_1_remaining_lifetime") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - assert state.state == "98" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_pf_kit_1_remaining_lifetime") - assert entry - assert entry.unique_id == "0123456789_pf_kit_1_remaining_life" - - state = hass.states.get("sensor.hl_l2340dw_page_counter") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES - assert state.state == "986" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_page_counter") - assert entry - assert entry.unique_id == "0123456789_page_counter" - - state = hass.states.get("sensor.hl_l2340dw_duplex_unit_page_counter") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES - assert state.state == "538" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_duplex_unit_page_counter") - assert entry - assert entry.unique_id == "0123456789_duplex_unit_pages_counter" - - state = hass.states.get("sensor.hl_l2340dw_b_w_pages") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES - assert state.state == "709" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_b_w_pages") - assert entry - assert entry.unique_id == "0123456789_bw_counter" - - state = hass.states.get("sensor.hl_l2340dw_color_pages") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES - assert state.state == "902" - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - - entry = entity_registry.async_get("sensor.hl_l2340dw_color_pages") - assert entry - assert entry.unique_id == "0123456789_color_counter" - - state = hass.states.get("sensor.hl_l2340dw_last_restart") - assert state - assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP - assert state.state == "2019-09-24T12:14:56+00:00" - assert state.attributes.get(ATTR_STATE_CLASS) is None - - entry = entity_registry.async_get("sensor.hl_l2340dw_last_restart") - assert entry - assert entry.unique_id == "0123456789_uptime" - - -async def test_disabled_by_default_sensors( - hass: HomeAssistant, entity_registry: er.EntityRegistry +async def test_sensors( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + entity_registry_enabled_by_default: None, + snapshot: SnapshotAssertion, + freezer: FrozenDateTimeFactory, ) -> None: - """Test the disabled by default Brother sensors.""" - await init_integration(hass) + """Test states of the sensors.""" + hass.config.set_time_zone("UTC") + freezer.move_to("2024-04-20 12:00:00+00:00") - state = hass.states.get("sensor.hl_l2340dw_last_restart") - assert state is None + with patch("homeassistant.components.brother.PLATFORMS", [Platform.SENSOR]): + entry = await init_integration(hass) - entry = entity_registry.async_get("sensor.hl_l2340dw_last_restart") - assert entry - assert entry.unique_id == "0123456789_uptime" - assert entry.disabled - assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION + entity_entries = er.async_entries_for_config_entry(entity_registry, entry.entry_id) + + assert entity_entries + for entity_entry in entity_entries: + assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry") + assert (state := hass.states.get(entity_entry.entity_id)) + assert state == snapshot(name=f"{entity_entry.entity_id}-state") async def test_availability(hass: HomeAssistant) -> None: From 194f3366ce47adcff2da00dafa650bddccbae7bf Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 20 Apr 2024 12:34:27 +0200 Subject: [PATCH 702/967] Use snapshot testing in NAM sensor and diagnostics (#115877) * Use snapshot testing in NAM diagnostics * Use snapshot testing in NAM sensor * Add NAM data fixture --------- Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- tests/components/nam/__init__.py | 35 +- .../nam/fixtures/diagnostics_data.json | 33 - tests/components/nam/fixtures/nam_data.json | 30 + .../nam/snapshots/test_diagnostics.ambr | 41 + .../components/nam/snapshots/test_sensor.ambr | 1714 +++++++++++++++++ tests/components/nam/test_diagnostics.py | 12 +- tests/components/nam/test_sensor.py | 474 +---- 7 files changed, 1820 insertions(+), 519 deletions(-) delete mode 100644 tests/components/nam/fixtures/diagnostics_data.json create mode 100644 tests/components/nam/fixtures/nam_data.json create mode 100644 tests/components/nam/snapshots/test_diagnostics.ambr create mode 100644 tests/components/nam/snapshots/test_sensor.ambr diff --git a/tests/components/nam/__init__.py b/tests/components/nam/__init__.py index 0484fc12bd6..9b254de452c 100644 --- a/tests/components/nam/__init__.py +++ b/tests/components/nam/__init__.py @@ -4,44 +4,13 @@ from unittest.mock import AsyncMock, Mock, patch from homeassistant.components.nam.const import DOMAIN -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, load_json_object_fixture INCOMPLETE_NAM_DATA = { "software_version": "NAMF-2020-36", "sensordatavalues": [], } -nam_data = { - "software_version": "NAMF-2020-36", - "uptime": "456987", - "sensordatavalues": [ - {"value_type": "PMS_P0", "value": "6.00"}, - {"value_type": "PMS_P1", "value": "10.00"}, - {"value_type": "PMS_P2", "value": "11.00"}, - {"value_type": "SDS_P1", "value": "18.65"}, - {"value_type": "SDS_P2", "value": "11.03"}, - {"value_type": "SPS30_P0", "value": "31.23"}, - {"value_type": "SPS30_P1", "value": "21.23"}, - {"value_type": "SPS30_P2", "value": "34.32"}, - {"value_type": "SPS30_P4", "value": "24.72"}, - {"value_type": "conc_co2_ppm", "value": "865"}, - {"value_type": "BME280_temperature", "value": "7.56"}, - {"value_type": "BME280_humidity", "value": "45.69"}, - {"value_type": "BME280_pressure", "value": "101101.17"}, - {"value_type": "BMP_temperature", "value": "7.56"}, - {"value_type": "BMP_pressure", "value": "103201.18"}, - {"value_type": "BMP280_temperature", "value": "5.56"}, - {"value_type": "BMP280_pressure", "value": "102201.18"}, - {"value_type": "SHT3X_temperature", "value": "6.28"}, - {"value_type": "SHT3X_humidity", "value": "34.69"}, - {"value_type": "humidity", "value": "46.23"}, - {"value_type": "temperature", "value": "6.26"}, - {"value_type": "HECA_temperature", "value": "7.95"}, - {"value_type": "HECA_humidity", "value": "49.97"}, - {"value_type": "signal", "value": "-72"}, - ], -} - async def init_integration(hass, co2_sensor=True) -> MockConfigEntry: """Set up the Nettigo Air Monitor integration in Home Assistant.""" @@ -52,6 +21,8 @@ async def init_integration(hass, co2_sensor=True) -> MockConfigEntry: data={"host": "10.10.2.3"}, ) + nam_data = load_json_object_fixture("nam/nam_data.json") + if not co2_sensor: # Remove conc_co2_ppm value nam_data["sensordatavalues"].pop(6) diff --git a/tests/components/nam/fixtures/diagnostics_data.json b/tests/components/nam/fixtures/diagnostics_data.json deleted file mode 100644 index a384e8cd386..00000000000 --- a/tests/components/nam/fixtures/diagnostics_data.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "bme280_humidity": 45.7, - "bme280_pressure": 1011.012, - "bme280_temperature": 7.6, - "bmp180_pressure": 1032.012, - "bmp180_temperature": 7.6, - "bmp280_pressure": 1022.012, - "bmp280_temperature": 5.6, - "dht22_humidity": 46.2, - "dht22_temperature": 6.3, - "heca_humidity": 50.0, - "heca_temperature": 8.0, - "mhz14a_carbon_dioxide": 865.0, - "pms_caqi": 19, - "pms_caqi_level": "very_low", - "pms_p0": 6.0, - "pms_p1": 10.0, - "pms_p2": 11.0, - "sds011_caqi": 19, - "sds011_caqi_level": "very_low", - "sds011_p1": 18.6, - "sds011_p2": 11.0, - "sht3x_humidity": 34.7, - "sht3x_temperature": 6.3, - "signal": -72.0, - "sps30_caqi": 54, - "sps30_caqi_level": "medium", - "sps30_p0": 31.2, - "sps30_p1": 21.2, - "sps30_p2": 34.3, - "sps30_p4": 24.7, - "uptime": 456987 -} diff --git a/tests/components/nam/fixtures/nam_data.json b/tests/components/nam/fixtures/nam_data.json new file mode 100644 index 00000000000..93a33d4a552 --- /dev/null +++ b/tests/components/nam/fixtures/nam_data.json @@ -0,0 +1,30 @@ +{ + "software_version": "NAMF-2020-36", + "uptime": "456987", + "sensordatavalues": [ + { "value_type": "PMS_P0", "value": "6.00" }, + { "value_type": "PMS_P1", "value": "10.00" }, + { "value_type": "PMS_P2", "value": "11.00" }, + { "value_type": "SDS_P1", "value": "18.65" }, + { "value_type": "SDS_P2", "value": "11.03" }, + { "value_type": "SPS30_P0", "value": "31.23" }, + { "value_type": "SPS30_P1", "value": "21.23" }, + { "value_type": "SPS30_P2", "value": "34.32" }, + { "value_type": "SPS30_P4", "value": "24.72" }, + { "value_type": "conc_co2_ppm", "value": "865" }, + { "value_type": "BME280_temperature", "value": "7.56" }, + { "value_type": "BME280_humidity", "value": "45.69" }, + { "value_type": "BME280_pressure", "value": "101101.17" }, + { "value_type": "BMP_temperature", "value": "7.56" }, + { "value_type": "BMP_pressure", "value": "103201.18" }, + { "value_type": "BMP280_temperature", "value": "5.56" }, + { "value_type": "BMP280_pressure", "value": "102201.18" }, + { "value_type": "SHT3X_temperature", "value": "6.28" }, + { "value_type": "SHT3X_humidity", "value": "34.69" }, + { "value_type": "humidity", "value": "46.23" }, + { "value_type": "temperature", "value": "6.26" }, + { "value_type": "HECA_temperature", "value": "7.95" }, + { "value_type": "HECA_humidity", "value": "49.97" }, + { "value_type": "signal", "value": "-72" } + ] +} diff --git a/tests/components/nam/snapshots/test_diagnostics.ambr b/tests/components/nam/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..2ebc0246090 --- /dev/null +++ b/tests/components/nam/snapshots/test_diagnostics.ambr @@ -0,0 +1,41 @@ +# serializer version: 1 +# name: test_entry_diagnostics + dict({ + 'data': dict({ + 'bme280_humidity': 45.7, + 'bme280_pressure': 1011.012, + 'bme280_temperature': 7.6, + 'bmp180_pressure': 1032.012, + 'bmp180_temperature': 7.6, + 'bmp280_pressure': 1022.012, + 'bmp280_temperature': 5.6, + 'dht22_humidity': 46.2, + 'dht22_temperature': 6.3, + 'heca_humidity': 50.0, + 'heca_temperature': 8.0, + 'mhz14a_carbon_dioxide': 865.0, + 'pms_caqi': 19, + 'pms_caqi_level': 'very_low', + 'pms_p0': 6.0, + 'pms_p1': 10.0, + 'pms_p2': 11.0, + 'sds011_caqi': 19, + 'sds011_caqi_level': 'very_low', + 'sds011_p1': 18.6, + 'sds011_p2': 11.0, + 'sht3x_humidity': 34.7, + 'sht3x_temperature': 6.3, + 'signal': -72.0, + 'sps30_caqi': 54, + 'sps30_caqi_level': 'medium', + 'sps30_p0': 31.2, + 'sps30_p1': 21.2, + 'sps30_p2': 34.3, + 'sps30_p4': 24.7, + 'uptime': 456987, + }), + 'info': dict({ + 'host': '10.10.2.3', + }), + }) +# --- diff --git a/tests/components/nam/snapshots/test_sensor.ambr b/tests/components/nam/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..bbc655ecbb6 --- /dev/null +++ b/tests/components/nam/snapshots/test_sensor.ambr @@ -0,0 +1,1714 @@ +# serializer version: 1 +# name: test_sensor[button.nettigo_air_monitor_restart-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.nettigo_air_monitor_restart', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Restart', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'aa:bb:cc:dd:ee:ff-restart', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[button.nettigo_air_monitor_restart-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'restart', + 'friendly_name': 'Nettigo Air Monitor Restart', + }), + 'context': , + 'entity_id': 'button.nettigo_air_monitor_restart', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_bme280_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.nettigo_air_monitor_bme280_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': 'BME280 humidity', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'bme280_humidity', + 'unique_id': 'aa:bb:cc:dd:ee:ff-bme280_humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_bme280_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'Nettigo Air Monitor BME280 humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_bme280_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '45.7', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_bme280_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.nettigo_air_monitor_bme280_pressure', + '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': 'BME280 pressure', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'bme280_pressure', + 'unique_id': 'aa:bb:cc:dd:ee:ff-bme280_pressure', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_bme280_pressure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'friendly_name': 'Nettigo Air Monitor BME280 pressure', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_bme280_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1011.012', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_bme280_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_bme280_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': 'BME280 temperature', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'bme280_temperature', + 'unique_id': 'aa:bb:cc:dd:ee:ff-bme280_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_bme280_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Nettigo Air Monitor BME280 temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_bme280_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.6', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_bmp180_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.nettigo_air_monitor_bmp180_pressure', + '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': 'BMP180 pressure', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'bmp180_pressure', + 'unique_id': 'aa:bb:cc:dd:ee:ff-bmp180_pressure', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_bmp180_pressure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'friendly_name': 'Nettigo Air Monitor BMP180 pressure', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_bmp180_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1032.012', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_bmp180_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_bmp180_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': 'BMP180 temperature', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'bmp180_temperature', + 'unique_id': 'aa:bb:cc:dd:ee:ff-bmp180_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_bmp180_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Nettigo Air Monitor BMP180 temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_bmp180_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.6', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_bmp280_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.nettigo_air_monitor_bmp280_pressure', + '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': 'BMP280 pressure', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'bmp280_pressure', + 'unique_id': 'aa:bb:cc:dd:ee:ff-bmp280_pressure', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_bmp280_pressure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'friendly_name': 'Nettigo Air Monitor BMP280 pressure', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_bmp280_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1022.012', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_bmp280_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_bmp280_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': 'BMP280 temperature', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'bmp280_temperature', + 'unique_id': 'aa:bb:cc:dd:ee:ff-bmp280_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_bmp280_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Nettigo Air Monitor BMP280 temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_bmp280_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.6', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_dht22_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.nettigo_air_monitor_dht22_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': 'DHT22 humidity', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dht22_humidity', + 'unique_id': 'aa:bb:cc:dd:ee:ff-dht22_humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_dht22_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'Nettigo Air Monitor DHT22 humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_dht22_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '46.2', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_dht22_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_dht22_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': 'DHT22 temperature', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dht22_temperature', + 'unique_id': 'aa:bb:cc:dd:ee:ff-dht22_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_dht22_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Nettigo Air Monitor DHT22 temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_dht22_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6.3', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_heca_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.nettigo_air_monitor_heca_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': 'HECA humidity', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'heca_humidity', + 'unique_id': 'aa:bb:cc:dd:ee:ff-heca_humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_heca_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'Nettigo Air Monitor HECA humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_heca_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '50.0', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_heca_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_heca_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': 'HECA temperature', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'heca_temperature', + 'unique_id': 'aa:bb:cc:dd:ee:ff-heca_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_heca_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Nettigo Air Monitor HECA temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_heca_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '8.0', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_last_restart-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.nettigo_air_monitor_last_restart', + '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 restart', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'last_restart', + 'unique_id': 'aa:bb:cc:dd:ee:ff-uptime', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_last_restart-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Nettigo Air Monitor Last restart', + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_last_restart', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-04-15T05:03:33+00:00', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_mh_z14a_carbon_dioxide-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_mh_z14a_carbon_dioxide', + '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': 'MH-Z14A carbon dioxide', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'mhz14a_carbon_dioxide', + 'unique_id': 'aa:bb:cc:dd:ee:ff-mhz14a_carbon_dioxide', + 'unit_of_measurement': 'ppm', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_mh_z14a_carbon_dioxide-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'carbon_dioxide', + 'friendly_name': 'Nettigo Air Monitor MH-Z14A carbon dioxide', + 'state_class': , + 'unit_of_measurement': 'ppm', + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_mh_z14a_carbon_dioxide', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '865.0', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_pmsx003_common_air_quality_index-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.nettigo_air_monitor_pmsx003_common_air_quality_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': 'PMSx003 common air quality index', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pmsx003_caqi', + 'unique_id': 'aa:bb:cc:dd:ee:ff-pms_caqi', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_pmsx003_common_air_quality_index-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Nettigo Air Monitor PMSx003 common air quality index', + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_pmsx003_common_air_quality_index', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '19', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_pmsx003_common_air_quality_index_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'very_low', + 'low', + 'medium', + 'high', + 'very_high', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nettigo_air_monitor_pmsx003_common_air_quality_index_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': 'PMSx003 common air quality index level', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pmsx003_caqi_level', + 'unique_id': 'aa:bb:cc:dd:ee:ff-pms_caqi_level', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_pmsx003_common_air_quality_index_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Nettigo Air Monitor PMSx003 common air quality index level', + 'options': list([ + 'very_low', + 'low', + 'medium', + 'high', + 'very_high', + ]), + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_pmsx003_common_air_quality_index_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'very_low', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_pmsx003_pm1-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_pmsx003_pm1', + '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': 'PMSx003 PM1', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pmsx003_pm1', + 'unique_id': 'aa:bb:cc:dd:ee:ff-pms_p0', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_pmsx003_pm1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pm1', + 'friendly_name': 'Nettigo Air Monitor PMSx003 PM1', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_pmsx003_pm1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6.0', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_pmsx003_pm10-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_pmsx003_pm10', + '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': 'PMSx003 PM10', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pmsx003_pm10', + 'unique_id': 'aa:bb:cc:dd:ee:ff-pms_p1', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_pmsx003_pm10-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pm10', + 'friendly_name': 'Nettigo Air Monitor PMSx003 PM10', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_pmsx003_pm10', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10.0', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_pmsx003_pm2_5-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_pmsx003_pm2_5', + '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': 'PMSx003 PM2.5', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pmsx003_pm25', + 'unique_id': 'aa:bb:cc:dd:ee:ff-pms_p2', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_pmsx003_pm2_5-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pm25', + 'friendly_name': 'Nettigo Air Monitor PMSx003 PM2.5', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_pmsx003_pm2_5', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '11.0', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sds011_common_air_quality_index-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.nettigo_air_monitor_sds011_common_air_quality_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': 'SDS011 common air quality index', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sds011_caqi', + 'unique_id': 'aa:bb:cc:dd:ee:ff-sds011_caqi', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sds011_common_air_quality_index-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Nettigo Air Monitor SDS011 common air quality index', + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_sds011_common_air_quality_index', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '19', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sds011_common_air_quality_index_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'very_low', + 'low', + 'medium', + 'high', + 'very_high', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nettigo_air_monitor_sds011_common_air_quality_index_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': 'SDS011 common air quality index level', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sds011_caqi_level', + 'unique_id': 'aa:bb:cc:dd:ee:ff-sds011_caqi_level', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sds011_common_air_quality_index_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Nettigo Air Monitor SDS011 common air quality index level', + 'options': list([ + 'very_low', + 'low', + 'medium', + 'high', + 'very_high', + ]), + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_sds011_common_air_quality_index_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'very_low', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sds011_pm10-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_sds011_pm10', + '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': 'SDS011 PM10', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sds011_pm10', + 'unique_id': 'aa:bb:cc:dd:ee:ff-sds011_p1', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sds011_pm10-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pm10', + 'friendly_name': 'Nettigo Air Monitor SDS011 PM10', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_sds011_pm10', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '18.6', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sds011_pm2_5-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_sds011_pm2_5', + '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': 'SDS011 PM2.5', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sds011_pm25', + 'unique_id': 'aa:bb:cc:dd:ee:ff-sds011_p2', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sds011_pm2_5-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pm25', + 'friendly_name': 'Nettigo Air Monitor SDS011 PM2.5', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_sds011_pm2_5', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '11.0', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sht3x_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.nettigo_air_monitor_sht3x_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': 'SHT3X humidity', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sht3x_humidity', + 'unique_id': 'aa:bb:cc:dd:ee:ff-sht3x_humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sht3x_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'Nettigo Air Monitor SHT3X humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_sht3x_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '34.7', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sht3x_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_sht3x_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': 'SHT3X temperature', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sht3x_temperature', + 'unique_id': 'aa:bb:cc:dd:ee:ff-sht3x_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sht3x_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Nettigo Air Monitor SHT3X temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_sht3x_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6.3', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_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.nettigo_air_monitor_signal_strength', + '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': 'Signal strength', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'aa:bb:cc:dd:ee:ff-signal', + 'unit_of_measurement': 'dBm', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_signal_strength-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'signal_strength', + 'friendly_name': 'Nettigo Air Monitor Signal strength', + 'state_class': , + 'unit_of_measurement': 'dBm', + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_signal_strength', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-72.0', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sps30_common_air_quality_index-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.nettigo_air_monitor_sps30_common_air_quality_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': 'SPS30 common air quality index', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sps30_caqi', + 'unique_id': 'aa:bb:cc:dd:ee:ff-sps30_caqi', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sps30_common_air_quality_index-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Nettigo Air Monitor SPS30 common air quality index', + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_sps30_common_air_quality_index', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '54', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sps30_common_air_quality_index_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'very_low', + 'low', + 'medium', + 'high', + 'very_high', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nettigo_air_monitor_sps30_common_air_quality_index_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': 'SPS30 common air quality index level', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sps30_caqi_level', + 'unique_id': 'aa:bb:cc:dd:ee:ff-sps30_caqi_level', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sps30_common_air_quality_index_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Nettigo Air Monitor SPS30 common air quality index level', + 'options': list([ + 'very_low', + 'low', + 'medium', + 'high', + 'very_high', + ]), + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_sps30_common_air_quality_index_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'medium', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sps30_pm1-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_sps30_pm1', + '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': 'SPS30 PM1', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sps30_pm1', + 'unique_id': 'aa:bb:cc:dd:ee:ff-sps30_p0', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sps30_pm1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pm1', + 'friendly_name': 'Nettigo Air Monitor SPS30 PM1', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_sps30_pm1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '31.2', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sps30_pm10-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_sps30_pm10', + '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': 'SPS30 PM10', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sps30_pm10', + 'unique_id': 'aa:bb:cc:dd:ee:ff-sps30_p1', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sps30_pm10-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pm10', + 'friendly_name': 'Nettigo Air Monitor SPS30 PM10', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_sps30_pm10', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '21.2', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sps30_pm2_5-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_sps30_pm2_5', + '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': 'SPS30 PM2.5', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sps30_pm25', + 'unique_id': 'aa:bb:cc:dd:ee:ff-sps30_p2', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sps30_pm2_5-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pm25', + 'friendly_name': 'Nettigo Air Monitor SPS30 PM2.5', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_sps30_pm2_5', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '34.3', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sps30_pm4-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_sps30_pm4', + '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': 'SPS30 PM4', + 'platform': 'nam', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sps30_pm4', + 'unique_id': 'aa:bb:cc:dd:ee:ff-sps30_p4', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensor[sensor.nettigo_air_monitor_sps30_pm4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Nettigo Air Monitor SPS30 PM4', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.nettigo_air_monitor_sps30_pm4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '24.7', + }) +# --- diff --git a/tests/components/nam/test_diagnostics.py b/tests/components/nam/test_diagnostics.py index 9d13121392f..7ed49a37e0a 100644 --- a/tests/components/nam/test_diagnostics.py +++ b/tests/components/nam/test_diagnostics.py @@ -1,25 +1,23 @@ """Test NAM diagnostics.""" -import json +from syrupy import SnapshotAssertion from homeassistant.core import HomeAssistant from . import init_integration -from tests.common import load_fixture from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator async def test_entry_diagnostics( - hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" entry = await init_integration(hass) - diagnostics_data = json.loads(load_fixture("diagnostics_data.json", "nam")) - result = await get_diagnostics_for_config_entry(hass, hass_client, entry) - assert result["info"] == {"host": "10.10.2.3"} - assert result["data"] == diagnostics_data + assert result == snapshot diff --git a/tests/components/nam/test_sensor.py b/tests/components/nam/test_sensor.py index c88a34ae497..5254c444434 100644 --- a/tests/components/nam/test_sensor.py +++ b/tests/components/nam/test_sensor.py @@ -3,27 +3,18 @@ from datetime import timedelta from unittest.mock import AsyncMock, Mock, patch +from freezegun.api import FrozenDateTimeFactory from nettigo_air_monitor import ApiError +from syrupy import SnapshotAssertion from homeassistant.components.nam.const import DOMAIN -from homeassistant.components.sensor import ( - ATTR_OPTIONS, - ATTR_STATE_CLASS, - DOMAIN as SENSOR_DOMAIN, - SensorDeviceClass, - SensorStateClass, -) +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, - ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, - CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - CONCENTRATION_PARTS_PER_MILLION, - PERCENTAGE, - SIGNAL_STRENGTH_DECIBELS_MILLIWATT, STATE_UNAVAILABLE, - UnitOfPressure, + Platform, UnitOfTemperature, ) from homeassistant.core import HomeAssistant @@ -31,447 +22,32 @@ from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from . import INCOMPLETE_NAM_DATA, init_integration, nam_data +from . import INCOMPLETE_NAM_DATA, init_integration -from tests.common import async_fire_time_changed +from tests.common import async_fire_time_changed, load_json_object_fixture -async def test_sensor(hass: HomeAssistant, entity_registry: er.EntityRegistry) -> None: +async def test_sensor( + hass: HomeAssistant, + entity_registry_enabled_by_default: None, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + freezer: FrozenDateTimeFactory, +) -> None: """Test states of the air_quality.""" - entity_registry.async_get_or_create( - SENSOR_DOMAIN, - DOMAIN, - "aa:bb:cc:dd:ee:ff-signal", - suggested_object_id="nettigo_air_monitor_signal_strength", - disabled_by=None, - ) + hass.config.set_time_zone("UTC") + freezer.move_to("2024-04-20 12:00:00+00:00") - entity_registry.async_get_or_create( - SENSOR_DOMAIN, - DOMAIN, - "aa:bb:cc:dd:ee:ff-uptime", - suggested_object_id="nettigo_air_monitor_uptime", - disabled_by=None, - ) + with patch("homeassistant.components.nam.PLATFORMS", [Platform.SENSOR]): + entry = await init_integration(hass) - # Patch return value from utcnow, with offset to make sure the patch is correct - now = utcnow() - timedelta(hours=1) - with patch("homeassistant.components.nam.sensor.utcnow", return_value=now): - await init_integration(hass) + entity_entries = er.async_entries_for_config_entry(entity_registry, entry.entry_id) - state = hass.states.get("sensor.nettigo_air_monitor_bme280_humidity") - assert state - assert state.state == "45.7" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.HUMIDITY - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_bme280_humidity") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-bme280_humidity" - - state = hass.states.get("sensor.nettigo_air_monitor_bme280_temperature") - assert state - assert state.state == "7.6" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_bme280_temperature") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-bme280_temperature" - - state = hass.states.get("sensor.nettigo_air_monitor_bme280_pressure") - assert state - assert state.state == "1011.012" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPressure.HPA - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_bme280_pressure") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-bme280_pressure" - - state = hass.states.get("sensor.nettigo_air_monitor_bmp180_temperature") - assert state - assert state.state == "7.6" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_bmp180_temperature") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-bmp180_temperature" - - state = hass.states.get("sensor.nettigo_air_monitor_bmp180_pressure") - assert state - assert state.state == "1032.012" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPressure.HPA - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_bmp180_pressure") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-bmp180_pressure" - - state = hass.states.get("sensor.nettigo_air_monitor_bmp280_temperature") - assert state - assert state.state == "5.6" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_bmp280_temperature") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-bmp280_temperature" - - state = hass.states.get("sensor.nettigo_air_monitor_bmp280_pressure") - assert state - assert state.state == "1022.012" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPressure.HPA - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_bmp280_pressure") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-bmp280_pressure" - - state = hass.states.get("sensor.nettigo_air_monitor_sht3x_humidity") - assert state - assert state.state == "34.7" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.HUMIDITY - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_sht3x_humidity") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sht3x_humidity" - - state = hass.states.get("sensor.nettigo_air_monitor_sht3x_temperature") - assert state - assert state.state == "6.3" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_sht3x_temperature") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sht3x_temperature" - - state = hass.states.get("sensor.nettigo_air_monitor_dht22_humidity") - assert state - assert state.state == "46.2" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.HUMIDITY - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_dht22_humidity") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-dht22_humidity" - - state = hass.states.get("sensor.nettigo_air_monitor_dht22_temperature") - assert state - assert state.state == "6.3" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_dht22_temperature") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-dht22_temperature" - - state = hass.states.get("sensor.nettigo_air_monitor_heca_humidity") - assert state - assert state.state == "50.0" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.HUMIDITY - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_heca_humidity") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-heca_humidity" - - state = hass.states.get("sensor.nettigo_air_monitor_heca_temperature") - assert state - assert state.state == "8.0" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_heca_temperature") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-heca_temperature" - - state = hass.states.get("sensor.nettigo_air_monitor_signal_strength") - assert state - assert state.state == "-72.0" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.SIGNAL_STRENGTH - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == SIGNAL_STRENGTH_DECIBELS_MILLIWATT - ) - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_signal_strength") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-signal" - - state = hass.states.get("sensor.nettigo_air_monitor_uptime") - assert state - assert ( - state.state - == (now - timedelta(seconds=456987)).replace(microsecond=0).isoformat() - ) - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP - assert state.attributes.get(ATTR_STATE_CLASS) is None - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_uptime") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-uptime" - - state = hass.states.get( - "sensor.nettigo_air_monitor_pmsx003_common_air_quality_index_level" - ) - assert state - assert state.state == "very_low" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENUM - assert state.attributes.get(ATTR_OPTIONS) == [ - "very_low", - "low", - "medium", - "high", - "very_high", - ] - assert state.attributes.get(ATTR_ICON) is None - - entry = entity_registry.async_get( - "sensor.nettigo_air_monitor_pmsx003_common_air_quality_index_level" - ) - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-pms_caqi_level" - assert entry.translation_key == "pmsx003_caqi_level" - - state = hass.states.get( - "sensor.nettigo_air_monitor_pmsx003_common_air_quality_index" - ) - assert state - assert state.state == "19" - assert state.attributes.get(ATTR_ICON) is None - - entry = entity_registry.async_get( - "sensor.nettigo_air_monitor_pmsx003_common_air_quality_index" - ) - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-pms_caqi" - - state = hass.states.get("sensor.nettigo_air_monitor_pmsx003_pm10") - assert state - assert state.state == "10.0" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM10 - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - ) - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_pmsx003_pm10") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-pms_p1" - - state = hass.states.get("sensor.nettigo_air_monitor_pmsx003_pm2_5") - assert state - assert state.state == "11.0" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM25 - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - ) - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_pmsx003_pm2_5") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-pms_p2" - - state = hass.states.get("sensor.nettigo_air_monitor_pmsx003_pm1") - assert state - assert state.state == "6.0" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM1 - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - ) - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_pmsx003_pm1") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-pms_p0" - - state = hass.states.get("sensor.nettigo_air_monitor_sds011_pm10") - assert state - assert state.state == "18.6" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM10 - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - ) - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_sds011_pm10") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sds011_p1" - - state = hass.states.get( - "sensor.nettigo_air_monitor_sds011_common_air_quality_index" - ) - assert state - assert state.state == "19" - assert state.attributes.get(ATTR_ICON) is None - - entry = entity_registry.async_get( - "sensor.nettigo_air_monitor_sds011_common_air_quality_index" - ) - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sds011_caqi" - - state = hass.states.get( - "sensor.nettigo_air_monitor_sds011_common_air_quality_index_level" - ) - assert state - assert state.state == "very_low" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENUM - assert state.attributes.get(ATTR_OPTIONS) == [ - "very_low", - "low", - "medium", - "high", - "very_high", - ] - assert state.attributes.get(ATTR_ICON) is None - - entry = entity_registry.async_get( - "sensor.nettigo_air_monitor_sds011_common_air_quality_index_level" - ) - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sds011_caqi_level" - assert entry.translation_key == "sds011_caqi_level" - - state = hass.states.get("sensor.nettigo_air_monitor_sds011_pm2_5") - assert state - assert state.state == "11.0" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM25 - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - ) - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_sds011_pm2_5") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sds011_p2" - - state = hass.states.get("sensor.nettigo_air_monitor_sps30_common_air_quality_index") - assert state - assert state.state == "54" - assert state.attributes.get(ATTR_ICON) is None - - entry = entity_registry.async_get( - "sensor.nettigo_air_monitor_sps30_common_air_quality_index" - ) - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sps30_caqi" - - state = hass.states.get( - "sensor.nettigo_air_monitor_sps30_common_air_quality_index_level" - ) - assert state - assert state.state == "medium" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENUM - assert state.attributes.get(ATTR_OPTIONS) == [ - "very_low", - "low", - "medium", - "high", - "very_high", - ] - assert state.attributes.get(ATTR_ICON) is None - - entry = entity_registry.async_get( - "sensor.nettigo_air_monitor_sps30_common_air_quality_index_level" - ) - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sps30_caqi_level" - assert entry.translation_key == "sps30_caqi_level" - - state = hass.states.get("sensor.nettigo_air_monitor_sps30_pm1") - assert state - assert state.state == "31.2" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM1 - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - ) - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_sps30_pm1") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sps30_p0" - - state = hass.states.get("sensor.nettigo_air_monitor_sps30_pm10") - assert state - assert state.state == "21.2" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM10 - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - ) - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_sps30_pm10") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sps30_p1" - - state = hass.states.get("sensor.nettigo_air_monitor_sps30_pm2_5") - assert state - assert state.state == "34.3" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM25 - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - ) - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_sps30_pm2_5") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sps30_p2" - - state = hass.states.get("sensor.nettigo_air_monitor_sps30_pm4") - assert state - assert state.state == "24.7" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - ) - assert state.attributes.get(ATTR_ICON) is None - - entry = entity_registry.async_get("sensor.nettigo_air_monitor_sps30_pm4") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sps30_p4" - - state = hass.states.get("sensor.nettigo_air_monitor_mh_z14a_carbon_dioxide") - assert state - assert state.state == "865.0" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.CO2 - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_PARTS_PER_MILLION - ) - entry = entity_registry.async_get( - "sensor.nettigo_air_monitor_mh_z14a_carbon_dioxide" - ) - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-mhz14a_carbon_dioxide" + assert entity_entries + for entity_entry in entity_entries: + assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry") + assert (state := hass.states.get(entity_entry.entity_id)) + assert state == snapshot(name=f"{entity_entry.entity_id}-state") async def test_sensor_disabled( @@ -524,6 +100,8 @@ async def test_incompleta_data_after_device_restart(hass: HomeAssistant) -> None async def test_availability(hass: HomeAssistant) -> None: """Ensure that we mark the entities unavailable correctly when device causes an error.""" + nam_data = load_json_object_fixture("nam/nam_data.json") + await init_integration(hass) state = hass.states.get("sensor.nettigo_air_monitor_bme280_temperature") @@ -566,6 +144,8 @@ async def test_availability(hass: HomeAssistant) -> None: async def test_manual_update_entity(hass: HomeAssistant) -> None: """Test manual update entity via service homeasasistant/update_entity.""" + nam_data = load_json_object_fixture("nam/nam_data.json") + await init_integration(hass) await async_setup_component(hass, "homeassistant", {}) From e3ce3ed6fd4e66e911a120c1986c3b9e92932723 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sat, 20 Apr 2024 05:36:03 -0500 Subject: [PATCH 703/967] Bump plexapi to 4.15.12 (#115872) --- homeassistant/components/plex/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 85362371715..ff0ab39b150 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_push", "loggers": ["plexapi", "plexwebsocket"], "requirements": [ - "PlexAPI==4.15.11", + "PlexAPI==4.15.12", "plexauth==0.0.6", "plexwebsocket==0.0.14" ], diff --git a/requirements_all.txt b/requirements_all.txt index a7111a73737..a740150a70f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -45,7 +45,7 @@ Mastodon.py==1.8.1 Pillow==10.3.0 # homeassistant.components.plex -PlexAPI==4.15.11 +PlexAPI==4.15.12 # homeassistant.components.progettihwsw ProgettiHWSW==0.1.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 70c1b2d244b..2258a5ba786 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -39,7 +39,7 @@ HATasmota==0.8.0 Pillow==10.3.0 # homeassistant.components.plex -PlexAPI==4.15.11 +PlexAPI==4.15.12 # homeassistant.components.progettihwsw ProgettiHWSW==0.1.3 From 8f73422ce548bd53a94816fd10b34a08da2b8aa1 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Sat, 20 Apr 2024 03:37:35 -0700 Subject: [PATCH 704/967] Bump pylitterbot to 2023.5.0 (#115856) --- homeassistant/components/litterrobot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index 66ade5f356c..88396f9f9c1 100644 --- a/homeassistant/components/litterrobot/manifest.json +++ b/homeassistant/components/litterrobot/manifest.json @@ -12,5 +12,5 @@ "integration_type": "hub", "iot_class": "cloud_push", "loggers": ["pylitterbot"], - "requirements": ["pylitterbot==2023.4.11"] + "requirements": ["pylitterbot==2023.5.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index a740150a70f..a15dd411020 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1941,7 +1941,7 @@ pylibrespot-java==0.1.1 pylitejet==0.6.2 # homeassistant.components.litterrobot -pylitterbot==2023.4.11 +pylitterbot==2023.5.0 # homeassistant.components.lutron_caseta pylutron-caseta==0.20.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2258a5ba786..ac721b30c22 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1516,7 +1516,7 @@ pylibrespot-java==0.1.1 pylitejet==0.6.2 # homeassistant.components.litterrobot -pylitterbot==2023.4.11 +pylitterbot==2023.5.0 # homeassistant.components.lutron_caseta pylutron-caseta==0.20.0 From 16e31d8f74d6e92675af8382d042117df4bf42e7 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 20 Apr 2024 14:49:57 +0200 Subject: [PATCH 705/967] Add test helper to snapshot a platform (#115880) * Add test helper to snapshot a platform * Add test helper to snapshot a platform --- tests/common.py | 20 ++++++++++++++++++++ tests/components/withings/test_sensor.py | 13 ++++--------- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/tests/common.py b/tests/common.py index b12f0ed37da..d53db1beb37 100644 --- a/tests/common.py +++ b/tests/common.py @@ -22,6 +22,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 import voluptuous as vol from homeassistant import auth, bootstrap, config_entries, loader @@ -1733,3 +1734,22 @@ def setup_test_component_platform( mock_platform(hass, f"test.{domain}", platform, built_in=built_in) return platform + + +async def snapshot_platform( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + config_entry_id: str, +) -> None: + """Snapshot a platform.""" + 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." + for entity_entry in entity_entries: + assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry") + assert entity_entry.disabled_by is None, "Please enable all entities." + assert (state := hass.states.get(entity_entry.entity_id)) + assert state == snapshot(name=f"{entity_entry.entity_id}-state") diff --git a/tests/components/withings/test_sensor.py b/tests/components/withings/test_sensor.py index 72da4b9d973..8966006e47f 100644 --- a/tests/components/withings/test_sensor.py +++ b/tests/components/withings/test_sensor.py @@ -21,7 +21,7 @@ from . import ( setup_integration, ) -from tests.common import MockConfigEntry, async_fire_time_changed +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform @pytest.mark.freeze_time("2023-10-21") @@ -36,15 +36,10 @@ async def test_all_entities( """Test all entities.""" with patch("homeassistant.components.withings.PLATFORMS", [Platform.SENSOR]): await setup_integration(hass, polling_config_entry) - entity_entries = er.async_entries_for_config_entry( - entity_registry, polling_config_entry.entry_id - ) - assert entity_entries - for entity_entry in entity_entries: - assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry") - assert (state := hass.states.get(entity_entry.entity_id)) - assert state == snapshot(name=f"{entity_entry.entity_id}-state") + await snapshot_platform( + hass, entity_registry, snapshot, polling_config_entry.entry_id + ) async def test_update_failed( From 5796b651afd6edfe510124ece56a32ed985dc53e Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 20 Apr 2024 17:14:42 +0200 Subject: [PATCH 706/967] Use snapshot test helper in Brother (#115885) --- tests/components/brother/test_sensor.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/tests/components/brother/test_sensor.py b/tests/components/brother/test_sensor.py index 39aa3b83d6f..069a5ddc152 100644 --- a/tests/components/brother/test_sensor.py +++ b/tests/components/brother/test_sensor.py @@ -17,7 +17,7 @@ from homeassistant.util.dt import utcnow from . import init_integration -from tests.common import async_fire_time_changed, load_fixture +from tests.common import async_fire_time_changed, load_fixture, snapshot_platform async def test_sensors( @@ -34,13 +34,7 @@ async def test_sensors( with patch("homeassistant.components.brother.PLATFORMS", [Platform.SENSOR]): entry = await init_integration(hass) - entity_entries = er.async_entries_for_config_entry(entity_registry, entry.entry_id) - - assert entity_entries - for entity_entry in entity_entries: - assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry") - assert (state := hass.states.get(entity_entry.entity_id)) - assert state == snapshot(name=f"{entity_entry.entity_id}-state") + await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) async def test_availability(hass: HomeAssistant) -> None: From b328981868182df33a355b438e64fd5549aa9909 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 20 Apr 2024 17:24:40 +0200 Subject: [PATCH 707/967] Use snapshot test helper in Accuweather (#115884) --- .../accuweather/snapshots/test_weather.ambr | 218 +++++------------- tests/components/accuweather/test_sensor.py | 10 +- tests/components/accuweather/test_weather.py | 57 +---- 3 files changed, 75 insertions(+), 210 deletions(-) diff --git a/tests/components/accuweather/snapshots/test_weather.ambr b/tests/components/accuweather/snapshots/test_weather.ambr index 081e7bf595a..1542d22aa7b 100644 --- a/tests/components/accuweather/snapshots/test_weather.ambr +++ b/tests/components/accuweather/snapshots/test_weather.ambr @@ -1,158 +1,4 @@ # serializer version: 1 -# name: test_forecast_service - dict({ - 'forecast': list([ - dict({ - 'apparent_temperature': 29.8, - 'cloud_coverage': 58, - 'condition': 'lightning-rainy', - 'datetime': '2020-07-26T05:00:00+00:00', - 'precipitation': 2.5, - 'precipitation_probability': 60, - 'temperature': 29.5, - 'templow': 15.4, - 'uv_index': 5, - 'wind_bearing': 166, - 'wind_gust_speed': 29.6, - 'wind_speed': 13.0, - }), - dict({ - 'apparent_temperature': 28.9, - 'cloud_coverage': 52, - 'condition': 'partlycloudy', - 'datetime': '2020-07-27T05:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 25, - 'temperature': 26.2, - 'templow': 15.9, - 'uv_index': 7, - 'wind_bearing': 297, - 'wind_gust_speed': 14.8, - 'wind_speed': 9.3, - }), - dict({ - 'apparent_temperature': 31.6, - 'cloud_coverage': 65, - 'condition': 'partlycloudy', - 'datetime': '2020-07-28T05:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 10, - 'temperature': 31.7, - 'templow': 16.8, - 'uv_index': 7, - 'wind_bearing': 198, - 'wind_gust_speed': 24.1, - 'wind_speed': 16.7, - }), - dict({ - 'apparent_temperature': 26.5, - 'cloud_coverage': 45, - 'condition': 'partlycloudy', - 'datetime': '2020-07-29T05:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 9, - 'temperature': 24.0, - 'templow': 11.7, - 'uv_index': 6, - 'wind_bearing': 293, - 'wind_gust_speed': 24.1, - 'wind_speed': 13.0, - }), - dict({ - 'apparent_temperature': 22.2, - 'cloud_coverage': 50, - 'condition': 'partlycloudy', - 'datetime': '2020-07-30T05:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 1, - 'temperature': 21.4, - 'templow': 12.2, - 'uv_index': 7, - 'wind_bearing': 280, - 'wind_gust_speed': 27.8, - 'wind_speed': 18.5, - }), - ]), - }) -# --- -# name: test_forecast_service[forecast] - dict({ - 'weather.home': dict({ - 'forecast': list([ - dict({ - 'apparent_temperature': 29.8, - 'cloud_coverage': 58, - 'condition': 'lightning-rainy', - 'datetime': '2020-07-26T05:00:00+00:00', - 'precipitation': 2.5, - 'precipitation_probability': 60, - 'temperature': 29.5, - 'templow': 15.4, - 'uv_index': 5, - 'wind_bearing': 166, - 'wind_gust_speed': 29.6, - 'wind_speed': 13.0, - }), - dict({ - 'apparent_temperature': 28.9, - 'cloud_coverage': 52, - 'condition': 'partlycloudy', - 'datetime': '2020-07-27T05:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 25, - 'temperature': 26.2, - 'templow': 15.9, - 'uv_index': 7, - 'wind_bearing': 297, - 'wind_gust_speed': 14.8, - 'wind_speed': 9.3, - }), - dict({ - 'apparent_temperature': 31.6, - 'cloud_coverage': 65, - 'condition': 'partlycloudy', - 'datetime': '2020-07-28T05:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 10, - 'temperature': 31.7, - 'templow': 16.8, - 'uv_index': 7, - 'wind_bearing': 198, - 'wind_gust_speed': 24.1, - 'wind_speed': 16.7, - }), - dict({ - 'apparent_temperature': 26.5, - 'cloud_coverage': 45, - 'condition': 'partlycloudy', - 'datetime': '2020-07-29T05:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 9, - 'temperature': 24.0, - 'templow': 11.7, - 'uv_index': 6, - 'wind_bearing': 293, - 'wind_gust_speed': 24.1, - 'wind_speed': 13.0, - }), - dict({ - 'apparent_temperature': 22.2, - 'cloud_coverage': 50, - 'condition': 'partlycloudy', - 'datetime': '2020-07-30T05:00:00+00:00', - 'precipitation': 0.0, - 'precipitation_probability': 1, - 'temperature': 21.4, - 'templow': 12.2, - 'uv_index': 7, - 'wind_bearing': 280, - 'wind_gust_speed': 27.8, - 'wind_speed': 18.5, - }), - ]), - }), - }) -# --- # name: test_forecast_service[get_forecast] dict({ 'forecast': list([ @@ -455,3 +301,67 @@ }), ]) # --- +# name: test_weather[weather.home-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'weather', + 'entity_category': None, + 'entity_id': 'weather.home', + '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': 'accuweather', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '0123456', + 'unit_of_measurement': None, + }) +# --- +# name: test_weather[weather.home-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'apparent_temperature': 22.8, + 'attribution': 'Data provided by AccuWeather', + 'cloud_coverage': 10, + 'dew_point': 16.2, + 'friendly_name': 'Home', + 'humidity': 67, + 'precipitation_unit': , + 'pressure': 1012.0, + 'pressure_unit': , + 'supported_features': , + 'temperature': 22.6, + 'temperature_unit': , + 'uv_index': 6, + 'visibility': 16.1, + 'visibility_unit': , + 'wind_bearing': 180, + 'wind_gust_speed': 20.3, + 'wind_speed': 14.5, + 'wind_speed_unit': , + }), + 'context': , + 'entity_id': 'weather.home', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'sunny', + }) +# --- diff --git a/tests/components/accuweather/test_sensor.py b/tests/components/accuweather/test_sensor.py index e79e49db96d..127e4d74cd8 100644 --- a/tests/components/accuweather/test_sensor.py +++ b/tests/components/accuweather/test_sensor.py @@ -30,6 +30,7 @@ from tests.common import ( async_fire_time_changed, load_json_array_fixture, load_json_object_fixture, + snapshot_platform, ) @@ -42,14 +43,7 @@ async def test_sensor( """Test states of the sensor.""" with patch("homeassistant.components.accuweather.PLATFORMS", [Platform.SENSOR]): entry = await init_integration(hass) - - entity_entries = er.async_entries_for_config_entry(entity_registry, entry.entry_id) - - assert entity_entries - for entity_entry in entity_entries: - assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry") - assert (state := hass.states.get(entity_entry.entity_id)) - assert state == snapshot(name=f"{entity_entry.entity_id}-state") + await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) async def test_availability(hass: HomeAssistant) -> None: diff --git a/tests/components/accuweather/test_weather.py b/tests/components/accuweather/test_weather.py index b3237ca2958..d97a5d3da3c 100644 --- a/tests/components/accuweather/test_weather.py +++ b/tests/components/accuweather/test_weather.py @@ -7,34 +7,14 @@ from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.accuweather.const import ( - ATTRIBUTION, - UPDATE_INTERVAL_DAILY_FORECAST, -) +from homeassistant.components.accuweather.const import UPDATE_INTERVAL_DAILY_FORECAST from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, - ATTR_WEATHER_APPARENT_TEMPERATURE, - ATTR_WEATHER_CLOUD_COVERAGE, - ATTR_WEATHER_DEW_POINT, - ATTR_WEATHER_HUMIDITY, - ATTR_WEATHER_PRESSURE, - ATTR_WEATHER_TEMPERATURE, - ATTR_WEATHER_UV_INDEX, - ATTR_WEATHER_VISIBILITY, - ATTR_WEATHER_WIND_BEARING, - ATTR_WEATHER_WIND_GUST_SPEED, - ATTR_WEATHER_WIND_SPEED, DOMAIN as WEATHER_DOMAIN, LEGACY_SERVICE_GET_FORECAST, SERVICE_GET_FORECASTS, - WeatherEntityFeature, -) -from homeassistant.const import ( - ATTR_ATTRIBUTION, - ATTR_ENTITY_ID, - ATTR_SUPPORTED_FEATURES, - STATE_UNAVAILABLE, ) +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -46,37 +26,18 @@ from tests.common import ( async_fire_time_changed, load_json_array_fixture, load_json_object_fixture, + snapshot_platform, ) from tests.typing import WebSocketGenerator -async def test_weather(hass: HomeAssistant, entity_registry: er.EntityRegistry) -> None: +async def test_weather( + hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion +) -> None: """Test states of the weather without forecast.""" - await init_integration(hass) - - state = hass.states.get("weather.home") - assert state - assert state.state == "sunny" - assert state.attributes.get(ATTR_WEATHER_HUMIDITY) == 67 - assert state.attributes.get(ATTR_WEATHER_PRESSURE) == 1012.0 - assert state.attributes.get(ATTR_WEATHER_TEMPERATURE) == 22.6 - assert state.attributes.get(ATTR_WEATHER_VISIBILITY) == 16.1 - assert state.attributes.get(ATTR_WEATHER_WIND_BEARING) == 180 - assert state.attributes.get(ATTR_WEATHER_WIND_SPEED) == 14.5 # 4.03 m/s -> km/h - assert state.attributes.get(ATTR_WEATHER_APPARENT_TEMPERATURE) == 22.8 - assert state.attributes.get(ATTR_WEATHER_DEW_POINT) == 16.2 - assert state.attributes.get(ATTR_WEATHER_CLOUD_COVERAGE) == 10 - assert state.attributes.get(ATTR_WEATHER_WIND_GUST_SPEED) == 20.3 - assert state.attributes.get(ATTR_WEATHER_UV_INDEX) == 6 - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert ( - state.attributes.get(ATTR_SUPPORTED_FEATURES) - is WeatherEntityFeature.FORECAST_DAILY - ) - - entry = entity_registry.async_get("weather.home") - assert entry - assert entry.unique_id == "0123456" + with patch("homeassistant.components.accuweather.PLATFORMS", [Platform.WEATHER]): + entry = await init_integration(hass) + await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) async def test_availability(hass: HomeAssistant) -> None: From de1312f7e4189b60dd1df491d1d168d52f11de00 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 20 Apr 2024 18:43:25 +0200 Subject: [PATCH 708/967] Use snapshot test helper in GIOS (#115893) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- tests/components/gios/test_sensor.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/tests/components/gios/test_sensor.py b/tests/components/gios/test_sensor.py index e760e050f2b..b24d88ccb8d 100644 --- a/tests/components/gios/test_sensor.py +++ b/tests/components/gios/test_sensor.py @@ -17,7 +17,7 @@ from homeassistant.util.dt import utcnow from . import init_integration -from tests.common import async_fire_time_changed, load_fixture +from tests.common import async_fire_time_changed, load_fixture, snapshot_platform async def test_sensor( @@ -27,13 +27,7 @@ async def test_sensor( with patch("homeassistant.components.gios.PLATFORMS", [Platform.SENSOR]): entry = await init_integration(hass) - entity_entries = er.async_entries_for_config_entry(entity_registry, entry.entry_id) - - assert entity_entries - for entity_entry in entity_entries: - assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry") - assert (state := hass.states.get(entity_entry.entity_id)) - assert state == snapshot(name=f"{entity_entry.entity_id}-state") + await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) async def test_availability(hass: HomeAssistant) -> None: From 5e345b7129b01d89b98589153374d98924e31804 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 20 Apr 2024 18:43:33 +0200 Subject: [PATCH 709/967] Use snapshot test helper in NAM (#115894) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- tests/components/nam/test_sensor.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/tests/components/nam/test_sensor.py b/tests/components/nam/test_sensor.py index 5254c444434..2b307b4b02a 100644 --- a/tests/components/nam/test_sensor.py +++ b/tests/components/nam/test_sensor.py @@ -24,7 +24,11 @@ from homeassistant.util.dt import utcnow from . import INCOMPLETE_NAM_DATA, init_integration -from tests.common import async_fire_time_changed, load_json_object_fixture +from tests.common import ( + async_fire_time_changed, + load_json_object_fixture, + snapshot_platform, +) async def test_sensor( @@ -41,13 +45,7 @@ async def test_sensor( with patch("homeassistant.components.nam.PLATFORMS", [Platform.SENSOR]): entry = await init_integration(hass) - entity_entries = er.async_entries_for_config_entry(entity_registry, entry.entry_id) - - assert entity_entries - for entity_entry in entity_entries: - assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry") - assert (state := hass.states.get(entity_entry.entity_id)) - assert state == snapshot(name=f"{entity_entry.entity_id}-state") + await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) async def test_sensor_disabled( From 10be2cc0044b6b010cb2d16c4a31961dc4cc4ea1 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 20 Apr 2024 18:43:40 +0200 Subject: [PATCH 710/967] Use snapshot test helper in NextDNS (#115895) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- tests/components/nextdns/test_binary_sensor.py | 10 ++-------- tests/components/nextdns/test_button.py | 10 +++------- tests/components/nextdns/test_sensor.py | 10 ++-------- tests/components/nextdns/test_switch.py | 10 ++-------- 4 files changed, 9 insertions(+), 31 deletions(-) diff --git a/tests/components/nextdns/test_binary_sensor.py b/tests/components/nextdns/test_binary_sensor.py index f83e55515e8..19cad755fb4 100644 --- a/tests/components/nextdns/test_binary_sensor.py +++ b/tests/components/nextdns/test_binary_sensor.py @@ -13,7 +13,7 @@ from homeassistant.util.dt import utcnow from . import init_integration, mock_nextdns -from tests.common import async_fire_time_changed +from tests.common import async_fire_time_changed, snapshot_platform async def test_binary_sensor( @@ -23,13 +23,7 @@ async def test_binary_sensor( with patch("homeassistant.components.nextdns.PLATFORMS", [Platform.BINARY_SENSOR]): entry = await init_integration(hass) - entity_entries = er.async_entries_for_config_entry(entity_registry, entry.entry_id) - - assert entity_entries - for entity_entry in entity_entries: - assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry") - assert (state := hass.states.get(entity_entry.entity_id)) - assert state == snapshot(name=f"{entity_entry.entity_id}-state") + await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) async def test_availability(hass: HomeAssistant) -> None: diff --git a/tests/components/nextdns/test_button.py b/tests/components/nextdns/test_button.py index 2007af612c8..51970b9bb48 100644 --- a/tests/components/nextdns/test_button.py +++ b/tests/components/nextdns/test_button.py @@ -12,6 +12,8 @@ from homeassistant.util import dt as dt_util from . import init_integration +from tests.common import snapshot_platform + async def test_button( hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion @@ -20,13 +22,7 @@ async def test_button( with patch("homeassistant.components.nextdns.PLATFORMS", [Platform.BUTTON]): entry = await init_integration(hass) - entity_entries = er.async_entries_for_config_entry(entity_registry, entry.entry_id) - - assert entity_entries - for entity_entry in entity_entries: - assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry") - assert (state := hass.states.get(entity_entry.entity_id)) - assert state == snapshot(name=f"{entity_entry.entity_id}-state") + await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) async def test_button_press(hass: HomeAssistant) -> None: diff --git a/tests/components/nextdns/test_sensor.py b/tests/components/nextdns/test_sensor.py index 9c03cf2b215..e7ea7a3f56b 100644 --- a/tests/components/nextdns/test_sensor.py +++ b/tests/components/nextdns/test_sensor.py @@ -13,7 +13,7 @@ from homeassistant.util.dt import utcnow from . import init_integration, mock_nextdns -from tests.common import async_fire_time_changed +from tests.common import async_fire_time_changed, snapshot_platform async def test_sensor( @@ -26,13 +26,7 @@ async def test_sensor( with patch("homeassistant.components.nextdns.PLATFORMS", [Platform.SENSOR]): entry = await init_integration(hass) - entity_entries = er.async_entries_for_config_entry(entity_registry, entry.entry_id) - - assert entity_entries - for entity_entry in entity_entries: - assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry") - assert (state := hass.states.get(entity_entry.entity_id)) - assert state == snapshot(name=f"{entity_entry.entity_id}-state") + await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) async def test_availability( diff --git a/tests/components/nextdns/test_switch.py b/tests/components/nextdns/test_switch.py index 5e027c6789c..2936bad1c67 100644 --- a/tests/components/nextdns/test_switch.py +++ b/tests/components/nextdns/test_switch.py @@ -26,7 +26,7 @@ from homeassistant.util.dt import utcnow from . import init_integration, mock_nextdns -from tests.common import async_fire_time_changed +from tests.common import async_fire_time_changed, snapshot_platform async def test_switch( @@ -39,13 +39,7 @@ async def test_switch( with patch("homeassistant.components.nextdns.PLATFORMS", [Platform.SWITCH]): entry = await init_integration(hass) - entity_entries = er.async_entries_for_config_entry(entity_registry, entry.entry_id) - - assert entity_entries - for entity_entry in entity_entries: - assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry") - assert (state := hass.states.get(entity_entry.entity_id)) - assert state == snapshot(name=f"{entity_entry.entity_id}-state") + await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) async def test_switch_on(hass: HomeAssistant) -> None: From d478b87af79cf7f2e62b8f1a73bf92bf893d2cb4 Mon Sep 17 00:00:00 2001 From: mtielen <6302356+mtielen@users.noreply.github.com> Date: Sat, 20 Apr 2024 19:09:32 +0200 Subject: [PATCH 711/967] Fix Wolf Smart Set Authentication and Session Management (#115815) * Fix Wolf Smart Set Authentication and Session Management Fix in the library to respect Wolf API token lifetime and implement Session Management * Updatie requirments * Update Code Owner --- CODEOWNERS | 4 ++-- homeassistant/components/wolflink/manifest.json | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 98f52070ed1..0a833a94e4e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1582,8 +1582,8 @@ build.json @home-assistant/supervisor /tests/components/wiz/ @sbidy /homeassistant/components/wled/ @frenck /tests/components/wled/ @frenck -/homeassistant/components/wolflink/ @adamkrol93 -/tests/components/wolflink/ @adamkrol93 +/homeassistant/components/wolflink/ @adamkrol93 @mtielen +/tests/components/wolflink/ @adamkrol93 @mtielen /homeassistant/components/workday/ @fabaff @gjohansson-ST /tests/components/workday/ @fabaff @gjohansson-ST /homeassistant/components/worldclock/ @fabaff diff --git a/homeassistant/components/wolflink/manifest.json b/homeassistant/components/wolflink/manifest.json index 6b51c0fb2cb..88dcce39993 100644 --- a/homeassistant/components/wolflink/manifest.json +++ b/homeassistant/components/wolflink/manifest.json @@ -1,10 +1,10 @@ { "domain": "wolflink", "name": "Wolf SmartSet Service", - "codeowners": ["@adamkrol93"], + "codeowners": ["@adamkrol93", "@mtielen"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wolflink", "iot_class": "cloud_polling", "loggers": ["wolf_comm"], - "requirements": ["wolf-comm==0.0.6"] + "requirements": ["wolf-comm==0.0.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index a15dd411020..1f066526f58 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2866,7 +2866,7 @@ wirelesstagpy==0.8.1 wled==0.17.0 # homeassistant.components.wolflink -wolf-comm==0.0.6 +wolf-comm==0.0.7 # homeassistant.components.wyoming wyoming==1.5.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ac721b30c22..91a3c65c3fd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2222,7 +2222,7 @@ wiffi==1.1.2 wled==0.17.0 # homeassistant.components.wolflink -wolf-comm==0.0.6 +wolf-comm==0.0.7 # homeassistant.components.wyoming wyoming==1.5.3 From c7530937410c925e7bebe9eedc36fac6fc75c6fa Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 20 Apr 2024 19:10:56 +0200 Subject: [PATCH 712/967] Use snapshot test helper in AO Smith (#115890) --- .../aosmith/snapshots/test_sensor.ambr | 81 +++++++++++- .../aosmith/snapshots/test_water_heater.ambr | 121 ++++++++++++++---- tests/components/aosmith/test_sensor.py | 44 ++----- tests/components/aosmith/test_water_heater.py | 53 +++----- 4 files changed, 207 insertions(+), 92 deletions(-) diff --git a/tests/components/aosmith/snapshots/test_sensor.ambr b/tests/components/aosmith/snapshots/test_sensor.ambr index 150e0c2934f..7aae9713037 100644 --- a/tests/components/aosmith/snapshots/test_sensor.ambr +++ b/tests/components/aosmith/snapshots/test_sensor.ambr @@ -1,5 +1,43 @@ # serializer version: 1 -# name: test_state[sensor.my_water_heater_energy_usage] +# name: test_state[sensor.my_water_heater_energy_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.my_water_heater_energy_usage', + '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': 'Energy usage', + 'platform': 'aosmith', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'energy_usage', + 'unique_id': 'energy_usage_junctionId', + 'unit_of_measurement': , + }) +# --- +# name: test_state[sensor.my_water_heater_energy_usage-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', @@ -15,7 +53,46 @@ 'state': '132.825', }) # --- -# name: test_state[sensor.my_water_heater_hot_water_availability] +# name: test_state[sensor.my_water_heater_hot_water_availability-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'low', + 'medium', + 'high', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.my_water_heater_hot_water_availability', + '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 availability', + 'platform': 'aosmith', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'hot_water_availability', + 'unique_id': 'hot_water_availability_junctionId', + 'unit_of_measurement': None, + }) +# --- +# name: test_state[sensor.my_water_heater_hot_water_availability-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'enum', diff --git a/tests/components/aosmith/snapshots/test_water_heater.ambr b/tests/components/aosmith/snapshots/test_water_heater.ambr index c3740341c17..deb079570f1 100644 --- a/tests/components/aosmith/snapshots/test_water_heater.ambr +++ b/tests/components/aosmith/snapshots/test_water_heater.ambr @@ -1,5 +1,103 @@ # serializer version: 1 -# name: test_state +# name: test_state[False][water_heater.my_water_heater-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_temp': 130, + 'min_temp': 95, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'water_heater', + 'entity_category': None, + 'entity_id': 'water_heater.my_water_heater', + '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': 'aosmith', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'junctionId', + 'unit_of_measurement': None, + }) +# --- +# name: test_state[False][water_heater.my_water_heater-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'away_mode': 'off', + 'current_temperature': None, + 'friendly_name': 'My water heater', + 'max_temp': 130, + 'min_temp': 95, + 'supported_features': , + 'target_temp_high': None, + 'target_temp_low': None, + 'temperature': 130, + }), + 'context': , + 'entity_id': 'water_heater.my_water_heater', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'electric', + }) +# --- +# name: test_state[True][water_heater.my_water_heater-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_temp': 130, + 'min_temp': 95, + 'operation_list': list([ + 'electric', + 'eco', + 'heat_pump', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'water_heater', + 'entity_category': None, + 'entity_id': 'water_heater.my_water_heater', + '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': 'aosmith', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'junctionId', + 'unit_of_measurement': None, + }) +# --- +# name: test_state[True][water_heater.my_water_heater-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'away_mode': 'off', @@ -26,24 +124,3 @@ 'state': 'heat_pump', }) # --- -# name: test_state_non_heat_pump[False] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'away_mode': 'off', - 'current_temperature': None, - 'friendly_name': 'My water heater', - 'max_temp': 130, - 'min_temp': 95, - 'supported_features': , - 'target_temp_high': None, - 'target_temp_low': None, - 'temperature': 130, - }), - 'context': , - 'entity_id': 'water_heater.my_water_heater', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'electric', - }) -# --- diff --git a/tests/components/aosmith/test_sensor.py b/tests/components/aosmith/test_sensor.py index f94dfdb710c..d6acd8865d8 100644 --- a/tests/components/aosmith/test_sensor.py +++ b/tests/components/aosmith/test_sensor.py @@ -1,50 +1,30 @@ """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 homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, snapshot_platform -@pytest.mark.parametrize( - ("entity_id", "unique_id"), - [ - ( - "sensor.my_water_heater_hot_water_availability", - "hot_water_availability_junctionId", - ), - ("sensor.my_water_heater_energy_usage", "energy_usage_junctionId"), - ], -) -async def test_setup( - hass: HomeAssistant, - entity_registry: er.EntityRegistry, - init_integration: MockConfigEntry, - entity_id: str, - unique_id: str, -) -> None: - """Test the setup of the sensor entities.""" - entry = entity_registry.async_get(entity_id) - assert entry - assert entry.unique_id == unique_id +@pytest.fixture(autouse=True) +async def platforms() -> AsyncGenerator[list[str], None]: + """Return the platforms to be loaded for this test.""" + with patch("homeassistant.components.aosmith.PLATFORMS", [Platform.SENSOR]): + yield -@pytest.mark.parametrize( - ("entity_id"), - [ - "sensor.my_water_heater_hot_water_availability", - "sensor.my_water_heater_energy_usage", - ], -) async def test_state( hass: HomeAssistant, init_integration: MockConfigEntry, snapshot: SnapshotAssertion, - entity_id: str, + entity_registry: er.EntityRegistry, ) -> None: """Test the state of the sensor entities.""" - state = hass.states.get(entity_id) - assert state == snapshot + await snapshot_platform(hass, entity_registry, snapshot, init_integration.entry_id) diff --git a/tests/components/aosmith/test_water_heater.py b/tests/components/aosmith/test_water_heater.py index a256f720c0a..567121ac0b0 100644 --- a/tests/components/aosmith/test_water_heater.py +++ b/tests/components/aosmith/test_water_heater.py @@ -1,6 +1,7 @@ """Tests for the water heater platform of the A. O. Smith integration.""" -from unittest.mock import MagicMock +from collections.abc import AsyncGenerator +from unittest.mock import MagicMock, patch from py_aosmith.models import OperationMode import pytest @@ -19,53 +20,33 @@ from homeassistant.components.water_heater import ( STATE_HEAT_PUMP, WaterHeaterEntityFeature, ) -from homeassistant.const import ( - ATTR_ENTITY_ID, - ATTR_FRIENDLY_NAME, - ATTR_SUPPORTED_FEATURES, -) +from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, snapshot_platform -async def test_setup( - hass: HomeAssistant, - entity_registry: er.EntityRegistry, - init_integration: MockConfigEntry, -) -> None: - """Test the setup of the water heater entity.""" - entry = entity_registry.async_get("water_heater.my_water_heater") - assert entry - assert entry.unique_id == "junctionId" - - state = hass.states.get("water_heater.my_water_heater") - assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My water heater" - - -async def test_state( - hass: HomeAssistant, init_integration: MockConfigEntry, snapshot: SnapshotAssertion -) -> None: - """Test the state of the water heater entity.""" - state = hass.states.get("water_heater.my_water_heater") - assert state == snapshot +@pytest.fixture(autouse=True) +async def platforms() -> AsyncGenerator[list[str], None]: + """Return the platforms to be loaded for this test.""" + with patch("homeassistant.components.aosmith.PLATFORMS", [Platform.WATER_HEATER]): + yield @pytest.mark.parametrize( ("get_devices_fixture_heat_pump"), - [ - False, - ], + [False, True], ) -async def test_state_non_heat_pump( - hass: HomeAssistant, init_integration: MockConfigEntry, snapshot: SnapshotAssertion +async def test_state( + hass: HomeAssistant, + init_integration: MockConfigEntry, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, ) -> None: - """Test the state of the water heater entity for a non heat pump device.""" - state = hass.states.get("water_heater.my_water_heater") - assert state == snapshot + """Test the state of the water heater entities.""" + await snapshot_platform(hass, entity_registry, snapshot, init_integration.entry_id) @pytest.mark.parametrize( From c94b0a82ca8ee6df1f8d5d11cb99be610141ffb0 Mon Sep 17 00:00:00 2001 From: Alberto Montes Date: Sat, 20 Apr 2024 20:01:49 +0200 Subject: [PATCH 713/967] Make release channel a hardcoded enum rather than a free form string (#115595) * Make release channel a hardcoded enum rather than a free form string * Update enum comparison to remove equality and us identity comparison * Fix comparison condition to match the previous implementation * Update tests to use Enum instead of string --- homeassistant/core.py | 18 ++++++++++------ homeassistant/helpers/device_registry.py | 26 ++++++++++++++++-------- homeassistant/helpers/entity.py | 3 ++- tests/helpers/test_device_registry.py | 10 ++++----- tests/helpers/test_entity.py | 9 ++++---- tests/test_core.py | 13 +++++++----- 6 files changed, 50 insertions(+), 29 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 01536f8ffdb..919e0adb758 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -36,7 +36,6 @@ from typing import ( TYPE_CHECKING, Any, Generic, - Literal, NotRequired, ParamSpec, Self, @@ -279,17 +278,24 @@ def async_get_hass() -> HomeAssistant: return _hass.hass +class ReleaseChannel(enum.StrEnum): + BETA = "beta" + DEV = "dev" + NIGHTLY = "nightly" + STABLE = "stable" + + @callback -def get_release_channel() -> Literal["beta", "dev", "nightly", "stable"]: +def get_release_channel() -> ReleaseChannel: """Find release channel based on version number.""" version = __version__ if "dev0" in version: - return "dev" + return ReleaseChannel.DEV if "dev" in version: - return "nightly" + return ReleaseChannel.NIGHTLY if "b" in version: - return "beta" - return "stable" + return ReleaseChannel.BETA + return ReleaseChannel.STABLE @enum.unique diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 3a9d047810b..00d0a0ba62f 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -13,7 +13,13 @@ import attr from yarl import URL from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import Event, HomeAssistant, callback, get_release_channel +from homeassistant.core import ( + Event, + HomeAssistant, + ReleaseChannel, + callback, + get_release_channel, +) from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import async_suggest_report_issue from homeassistant.util.event_type import EventType @@ -608,7 +614,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): try: return name.format(**translation_placeholders) except KeyError as err: - if get_release_channel() != "stable": + if get_release_channel() is not ReleaseChannel.STABLE: raise HomeAssistantError("Missing placeholder %s" % err) from err report_issue = async_suggest_report_issue( self.hass, integration_domain=domain @@ -963,12 +969,16 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): tuple(conn) # type: ignore[misc] for conn in device["connections"] }, - disabled_by=DeviceEntryDisabler(device["disabled_by"]) - if device["disabled_by"] - else None, - entry_type=DeviceEntryType(device["entry_type"]) - if device["entry_type"] - else None, + disabled_by=( + DeviceEntryDisabler(device["disabled_by"]) + if device["disabled_by"] + else None + ), + entry_type=( + DeviceEntryType(device["entry_type"]) + if device["entry_type"] + else None + ), hw_version=device["hw_version"], id=device["id"], identifiers={ diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 20948a7130a..086def8a8be 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -52,6 +52,7 @@ from homeassistant.core import ( Event, HassJobType, HomeAssistant, + ReleaseChannel, callback, get_hassjob_callable_job_type, get_release_channel, @@ -657,7 +658,7 @@ class Entity( return name.format(**self.translation_placeholders) except KeyError as err: if not self._name_translation_placeholders_reported: - if get_release_channel() != "stable": + if get_release_channel() is not ReleaseChannel.STABLE: raise HomeAssistantError("Missing placeholder %s" % err) from err report_issue = self._suggest_report_issue() _LOGGER.warning( diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index bed3dea4dc1..ee895e3fd3e 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -11,7 +11,7 @@ from yarl import URL from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STARTED -from homeassistant.core import CoreState, HomeAssistant +from homeassistant.core import CoreState, HomeAssistant, ReleaseChannel from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import ( area_registry as ar, @@ -2390,7 +2390,7 @@ async def test_device_name_translation_placeholders( }, }, {"placeholder": "special"}, - "stable", + ReleaseChannel.STABLE, nullcontext(), ( "has translation placeholders '{'placeholder': 'special'}' which do " @@ -2405,7 +2405,7 @@ async def test_device_name_translation_placeholders( }, }, {"placeholder": "special"}, - "beta", + ReleaseChannel.BETA, pytest.raises( HomeAssistantError, match="Missing placeholder '2ndplaceholder'" ), @@ -2419,7 +2419,7 @@ async def test_device_name_translation_placeholders( }, }, None, - "stable", + ReleaseChannel.STABLE, nullcontext(), ( "has translation placeholders '{}' which do " @@ -2434,7 +2434,7 @@ async def test_device_name_translation_placeholders_errors( translation_key: str | None, translations: dict[str, str] | None, placeholders: dict[str, str] | None, - release_channel: str, + release_channel: ReleaseChannel, expectation: AbstractContextManager, expected_error: str, caplog: pytest.LogCaptureFixture, diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 70d917dbc7b..fb2793a75c7 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -28,6 +28,7 @@ from homeassistant.core import ( HassJobType, HomeAssistant, HomeAssistantError, + ReleaseChannel, callback, ) from homeassistant.helpers import device_registry as dr, entity, entity_registry as er @@ -1249,7 +1250,7 @@ async def test_entity_name_translation_placeholders( }, }, {"placeholder": "special"}, - "stable", + ReleaseChannel.STABLE, ( "has translation placeholders '{'placeholder': 'special'}' which do " "not match the name '{placeholder} English ent {2ndplaceholder}'" @@ -1263,7 +1264,7 @@ async def test_entity_name_translation_placeholders( }, }, {"placeholder": "special"}, - "beta", + ReleaseChannel.BETA, "HomeAssistantError: Missing placeholder '2ndplaceholder'", ), ( @@ -1274,7 +1275,7 @@ async def test_entity_name_translation_placeholders( }, }, None, - "stable", + ReleaseChannel.STABLE, ( "has translation placeholders '{}' which do " "not match the name '{placeholder} English ent'" @@ -1287,7 +1288,7 @@ async def test_entity_name_translation_placeholder_errors( translation_key: str | None, translations: dict[str, str] | None, placeholders: dict[str, str] | None, - release_channel: str, + release_channel: ReleaseChannel, expected_error: str, caplog: pytest.LogCaptureFixture, ) -> None: diff --git a/tests/test_core.py b/tests/test_core.py index 5d687d89833..8f0d7f53277 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -42,6 +42,7 @@ from homeassistant.core import ( CoreState, HassJob, HomeAssistant, + ReleaseChannel, ServiceCall, ServiceResponse, State, @@ -3060,13 +3061,15 @@ async def test_validate_state(hass: HomeAssistant) -> None: @pytest.mark.parametrize( ("version", "release_channel"), [ - ("0.115.0.dev20200815", "nightly"), - ("0.115.0", "stable"), - ("0.115.0b4", "beta"), - ("0.115.0dev0", "dev"), + ("0.115.0.dev20200815", ReleaseChannel.NIGHTLY), + ("0.115.0", ReleaseChannel.STABLE), + ("0.115.0b4", ReleaseChannel.BETA), + ("0.115.0dev0", ReleaseChannel.DEV), ], ) -async def test_get_release_channel(version: str, release_channel: str) -> None: +async def test_get_release_channel( + version: str, release_channel: ReleaseChannel +) -> None: """Test if release channel detection works from Home Assistant version number.""" with patch("homeassistant.core.__version__", f"{version}"): assert get_release_channel() == release_channel From ee116713cf2838de4b99ef4bd1b6694210a7f6f8 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 20 Apr 2024 21:27:54 +0200 Subject: [PATCH 714/967] Use snapshot test helper in Analytics insights (#115889) --- tests/components/analytics_insights/test_sensor.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/tests/components/analytics_insights/test_sensor.py b/tests/components/analytics_insights/test_sensor.py index e0850bbd55b..3ede971c8f8 100644 --- a/tests/components/analytics_insights/test_sensor.py +++ b/tests/components/analytics_insights/test_sensor.py @@ -16,7 +16,7 @@ from homeassistant.helpers import entity_registry as er from . import setup_integration -from tests.common import MockConfigEntry, async_fire_time_changed +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform async def test_all_entities( @@ -32,17 +32,10 @@ async def test_all_entities( [Platform.SENSOR], ): await setup_integration(hass, mock_config_entry) - entity_entries = er.async_entries_for_config_entry( - entity_registry, mock_config_entry.entry_id + await snapshot_platform( + hass, entity_registry, snapshot, mock_config_entry.entry_id ) - assert entity_entries - for entity_entry in entity_entries: - assert hass.states.get(entity_entry.entity_id) == snapshot( - name=f"{entity_entry.entity_id}-state" - ) - assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry") - async def test_connection_error( hass: HomeAssistant, From 48d1692cd6e26c9aec6e2e10fc562257f5222631 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 20 Apr 2024 21:29:14 +0200 Subject: [PATCH 715/967] Use snapshot test helper in Ambient Network (#115887) --- .../snapshots/test_sensor.ambr | 119 ++++++++++++++++-- .../components/ambient_network/test_sensor.py | 14 +-- 2 files changed, 110 insertions(+), 23 deletions(-) diff --git a/tests/components/ambient_network/snapshots/test_sensor.ambr b/tests/components/ambient_network/snapshots/test_sensor.ambr index 377018c54be..fadb15ad015 100644 --- a/tests/components/ambient_network/snapshots/test_sensor.ambr +++ b/tests/components/ambient_network/snapshots/test_sensor.ambr @@ -10,7 +10,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.station_a_absolute_pressure', @@ -22,6 +22,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -38,7 +41,21 @@ }) # --- # name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_absolute_pressure-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'pressure', + 'friendly_name': 'Station A Absolute pressure', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_a_absolute_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '977.616536580043', + }) # --- # name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_daily_rain-entry] EntityRegistryEntrySnapshot({ @@ -332,7 +349,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.station_a_irradiance', @@ -344,6 +361,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -357,7 +377,21 @@ }) # --- # name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_irradiance-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'irradiance', + 'friendly_name': 'Station A Irradiance', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_a_irradiance', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '37.64', + }) # --- # name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_last_rain-entry] EntityRegistryEntrySnapshot({ @@ -368,7 +402,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.station_a_last_rain', @@ -393,7 +427,19 @@ }) # --- # name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_last_rain-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'timestamp', + 'friendly_name': 'Station A Last rain', + }), + 'context': , + 'entity_id': 'sensor.station_a_last_rain', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2023-10-30T09:45:00+00:00', + }) # --- # name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_max_daily_gust-entry] EntityRegistryEntrySnapshot({ @@ -464,7 +510,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.station_a_monthly_rain', @@ -476,6 +522,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -492,7 +541,21 @@ }) # --- # name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_monthly_rain-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'precipitation', + 'friendly_name': 'Station A Monthly rain', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_a_monthly_rain', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) # --- # name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_relative_pressure-entry] EntityRegistryEntrySnapshot({ @@ -672,7 +735,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.station_a_weekly_rain', @@ -684,6 +747,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -700,7 +766,21 @@ }) # --- # name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_weekly_rain-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'device_class': 'precipitation', + 'friendly_name': 'Station A Weekly rain', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.station_a_weekly_rain', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) # --- # name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_wind_direction-entry] EntityRegistryEntrySnapshot({ @@ -711,7 +791,7 @@ 'config_entry_id': , 'device_class': None, 'device_id': , - 'disabled_by': , + 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.station_a_wind_direction', @@ -723,6 +803,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': None, 'original_icon': None, @@ -736,7 +819,19 @@ }) # --- # name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_wind_direction-state] - None + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by ambientnetwork.net', + 'friendly_name': 'Station A Wind direction', + 'unit_of_measurement': '°', + }), + 'context': , + 'entity_id': 'sensor.station_a_wind_direction', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '11', + }) # --- # name: test_sensors[AA:AA:AA:AA:AA:AA][sensor.station_a_wind_gust-entry] EntityRegistryEntrySnapshot({ diff --git a/tests/components/ambient_network/test_sensor.py b/tests/components/ambient_network/test_sensor.py index b556c0c9c7c..35aa90ffe05 100644 --- a/tests/components/ambient_network/test_sensor.py +++ b/tests/components/ambient_network/test_sensor.py @@ -14,11 +14,12 @@ from homeassistant.helpers import entity_registry as er from .conftest import setup_platform -from tests.common import async_fire_time_changed +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) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensors( hass: HomeAssistant, open_api: OpenAPI, @@ -30,16 +31,7 @@ async def test_sensors( """Test all sensors under normal operation.""" await setup_platform(True, hass, config_entry) - entity_entries = er.async_entries_for_config_entry( - entity_registry, config_entry.entry_id - ) - - assert entity_entries - for entity_entry in entity_entries: - assert hass.states.get(entity_entry.entity_id) == snapshot( - name=f"{entity_entry.entity_id}-state" - ) - assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry") + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) @freeze_time("2023-11-09") From b450918f66c951e8dc4cc36fadfb0f5e07805141 Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Sat, 20 Apr 2024 21:35:02 +0200 Subject: [PATCH 716/967] Bump ruff to 0.4.1 (#115873) --- .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 cd42fecbfa1..ceb8ee7f9c4 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.3.7 + rev: v0.4.1 hooks: - id: ruff args: diff --git a/pyproject.toml b/pyproject.toml index 4b3b15f7bde..91f75c96fd6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -659,7 +659,7 @@ filterwarnings = [ ] [tool.ruff] -required-version = ">=0.3.7" +required-version = ">=0.4.1" [tool.ruff.lint] select = [ diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 46ade953da2..4f21f6d4a0c 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.2.6 -ruff==0.3.7 +ruff==0.4.1 yamllint==1.35.1 From 68225abce557d73dc401618428e8d200a8139bbc Mon Sep 17 00:00:00 2001 From: r-binder <40315895+r-binder@users.noreply.github.com> Date: Sat, 20 Apr 2024 23:08:29 +0200 Subject: [PATCH 717/967] Add tls support for AVM Fritz!Tools (#112714) --- homeassistant/components/fritz/__init__.py | 10 +- homeassistant/components/fritz/common.py | 8 +- homeassistant/components/fritz/config_flow.py | 39 ++++- homeassistant/components/fritz/const.py | 4 +- homeassistant/components/fritz/strings.json | 6 +- tests/components/fritz/conftest.py | 14 +- tests/components/fritz/const.py | 11 ++ tests/components/fritz/test_config_flow.py | 143 +++++++++++++++--- tests/components/fritz/test_switch.py | 26 +++- 9 files changed, 210 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/fritz/__init__.py b/homeassistant/components/fritz/__init__.py index ba9e2191901..bab97569eda 100644 --- a/homeassistant/components/fritz/__init__.py +++ b/homeassistant/components/fritz/__init__.py @@ -3,13 +3,20 @@ import logging from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_SSL, + CONF_USERNAME, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from .common import AvmWrapper, FritzData from .const import ( DATA_FRITZ, + DEFAULT_SSL, DOMAIN, FRITZ_AUTH_EXCEPTIONS, FRITZ_EXCEPTIONS, @@ -29,6 +36,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: port=entry.data[CONF_PORT], username=entry.data[CONF_USERNAME], password=entry.data[CONF_PASSWORD], + use_tls=entry.data.get(CONF_SSL, DEFAULT_SSL), ) try: diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index e4d5e92b742..f051c824847 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -48,7 +48,7 @@ from .const import ( DEFAULT_CONF_OLD_DISCOVERY, DEFAULT_DEVICE_NAME, DEFAULT_HOST, - DEFAULT_PORT, + DEFAULT_SSL, DEFAULT_USERNAME, DOMAIN, FRITZ_EXCEPTIONS, @@ -184,9 +184,10 @@ class FritzBoxTools( self, hass: HomeAssistant, password: str, + port: int, username: str = DEFAULT_USERNAME, host: str = DEFAULT_HOST, - port: int = DEFAULT_PORT, + use_tls: bool = DEFAULT_SSL, ) -> None: """Initialize FritzboxTools class.""" super().__init__( @@ -211,6 +212,7 @@ class FritzBoxTools( self.password = password self.port = port self.username = username + self.use_tls = use_tls self.has_call_deflections: bool = False self._model: str | None = None self._current_firmware: str | None = None @@ -230,11 +232,13 @@ class FritzBoxTools( def setup(self) -> None: """Set up FritzboxTools class.""" + self.connection = FritzConnection( address=self.host, port=self.port, user=self.username, password=self.password, + use_tls=self.use_tls, timeout=60.0, pool_maxsize=30, ) diff --git a/homeassistant/components/fritz/config_flow.py b/homeassistant/components/fritz/config_flow.py index a217adf935c..1cfa3af39fb 100644 --- a/homeassistant/components/fritz/config_flow.py +++ b/homeassistant/components/fritz/config_flow.py @@ -25,14 +25,22 @@ from homeassistant.config_entries import ( OptionsFlow, OptionsFlowWithConfigEntry, ) -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_SSL, + CONF_USERNAME, +) from homeassistant.core import callback from .const import ( CONF_OLD_DISCOVERY, DEFAULT_CONF_OLD_DISCOVERY, DEFAULT_HOST, - DEFAULT_PORT, + DEFAULT_HTTP_PORT, + DEFAULT_HTTPS_PORT, + DEFAULT_SSL, DOMAIN, ERROR_AUTH_INVALID, ERROR_CANNOT_CONNECT, @@ -61,6 +69,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): self._entry: ConfigEntry | None = None self._name: str = "" self._password: str = "" + self._use_tls: bool = False self._port: int | None = None self._username: str = "" self._model: str = "" @@ -74,6 +83,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): port=self._port, user=self._username, password=self._password, + use_tls=self._use_tls, timeout=60.0, pool_maxsize=30, ) @@ -120,6 +130,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): CONF_PASSWORD: self._password, CONF_PORT: self._port, CONF_USERNAME: self._username, + CONF_SSL: self._use_tls, }, options={ CONF_CONSIDER_HOME: DEFAULT_CONSIDER_HOME.total_seconds(), @@ -133,7 +144,6 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a flow initialized by discovery.""" ssdp_location: ParseResult = urlparse(discovery_info.ssdp_location or "") self._host = ssdp_location.hostname - self._port = ssdp_location.port self._name = ( discovery_info.upnp.get(ssdp.ATTR_UPNP_FRIENDLY_NAME) or discovery_info.upnp[ssdp.ATTR_UPNP_MODEL_NAME] @@ -178,6 +188,8 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): self._username = user_input[CONF_USERNAME] self._password = user_input[CONF_PASSWORD] + self._use_tls = user_input[CONF_SSL] + self._port = DEFAULT_HTTPS_PORT if self._use_tls else DEFAULT_HTTP_PORT error = await self.hass.async_add_executor_job(self.fritz_tools_init) @@ -191,14 +203,22 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): self, errors: dict[str, str] | None = None ) -> ConfigFlowResult: """Show the setup form to the user.""" + + advanced_data_schema = {} + if self.show_advanced_options: + advanced_data_schema = { + vol.Optional(CONF_PORT): vol.Coerce(int), + } + return self.async_show_form( step_id="user", data_schema=vol.Schema( { vol.Optional(CONF_HOST, default=DEFAULT_HOST): str, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): vol.Coerce(int), + **advanced_data_schema, vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): bool, } ), errors=errors or {}, @@ -214,6 +234,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): { vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): bool, } ), description_placeholders={"name": self._name}, @@ -227,9 +248,14 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): if user_input is None: return self._show_setup_form_init() self._host = user_input[CONF_HOST] - self._port = user_input[CONF_PORT] self._username = user_input[CONF_USERNAME] self._password = user_input[CONF_PASSWORD] + self._use_tls = user_input[CONF_SSL] + + if (port := user_input.get(CONF_PORT)) is None: + self._port = DEFAULT_HTTPS_PORT if self._use_tls else DEFAULT_HTTP_PORT + else: + self._port = port if not (error := await self.hass.async_add_executor_job(self.fritz_tools_init)): self._name = self._model @@ -251,6 +277,8 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): self._port = entry_data[CONF_PORT] self._username = entry_data[CONF_USERNAME] self._password = entry_data[CONF_PASSWORD] + self._use_tls = entry_data[CONF_SSL] + return await self.async_step_reauth_confirm() def _show_setup_form_reauth_confirm( @@ -295,6 +323,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): CONF_PASSWORD: self._password, CONF_PORT: self._port, CONF_USERNAME: self._username, + CONF_SSL: self._use_tls, }, ) await self.hass.config_entries.async_reload(self._entry.entry_id) diff --git a/homeassistant/components/fritz/const.py b/homeassistant/components/fritz/const.py index caa7d44c378..3794a83dd7f 100644 --- a/homeassistant/components/fritz/const.py +++ b/homeassistant/components/fritz/const.py @@ -46,8 +46,10 @@ DSL_CONNECTION: Literal["dsl"] = "dsl" DEFAULT_DEVICE_NAME = "Unknown device" DEFAULT_HOST = "192.168.178.1" -DEFAULT_PORT = 49000 +DEFAULT_HTTP_PORT = 49000 +DEFAULT_HTTPS_PORT = 49443 DEFAULT_USERNAME = "" +DEFAULT_SSL = False ERROR_AUTH_INVALID = "invalid_auth" ERROR_CANNOT_CONNECT = "cannot_connect" diff --git a/homeassistant/components/fritz/strings.json b/homeassistant/components/fritz/strings.json index 5eed2f59fc4..4899edb6938 100644 --- a/homeassistant/components/fritz/strings.json +++ b/homeassistant/components/fritz/strings.json @@ -25,10 +25,12 @@ "host": "[%key:common::config_flow::data::host%]", "port": "[%key:common::config_flow::data::port%]", "username": "[%key:common::config_flow::data::username%]", - "password": "[%key:common::config_flow::data::password%]" + "password": "[%key:common::config_flow::data::password%]", + "ssl": "[%key:common::config_flow::data::ssl%]" }, "data_description": { - "host": "The hostname or IP address of your FRITZ!Box router." + "host": "The hostname or IP address of your FRITZ!Box router.", + "port": "Leave it empty to use the default port." } } }, diff --git a/tests/components/fritz/conftest.py b/tests/components/fritz/conftest.py index e32ca55f65d..acf6b0e98cd 100644 --- a/tests/components/fritz/conftest.py +++ b/tests/components/fritz/conftest.py @@ -74,16 +74,6 @@ class FritzConnectionMock: return self._services[service][action] -class FritzHostMock(FritzHosts): - """FritzHosts mocking.""" - - get_mesh_topology = MagicMock() - get_mesh_topology.return_value = MOCK_MESH_DATA - - get_hosts_attributes = MagicMock() - get_hosts_attributes.return_value = MOCK_HOST_ATTRIBUTES_DATA - - @pytest.fixture(name="fc_data") def fc_data_mock(): """Fixture for default fc_data.""" @@ -105,6 +95,8 @@ def fh_class_mock(): """Fixture that sets up a mocked FritzHosts class.""" with patch( "homeassistant.components.fritz.common.FritzHosts", - new=FritzHostMock, + new=FritzHosts, ) as result: + result.get_mesh_topology = MagicMock(return_value=MOCK_MESH_DATA) + result.get_hosts_attributes = MagicMock(return_value=MOCK_HOST_ATTRIBUTES_DATA) yield result diff --git a/tests/components/fritz/const.py b/tests/components/fritz/const.py index ce530e32964..0d1222dfcda 100644 --- a/tests/components/fritz/const.py +++ b/tests/components/fritz/const.py @@ -8,6 +8,7 @@ from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_PORT, + CONF_SSL, CONF_USERNAME, ) @@ -22,10 +23,12 @@ MOCK_CONFIG = { CONF_PORT: "1234", CONF_PASSWORD: "fake_pass", CONF_USERNAME: "fake_user", + CONF_SSL: False, } ] } } + MOCK_HOST = "fake_host" MOCK_IPS = { "fritz.box": "192.168.178.1", @@ -902,6 +905,14 @@ MOCK_HOST_ATTRIBUTES_DATA = [ ] MOCK_USER_DATA = MOCK_CONFIG[DOMAIN][CONF_DEVICES][0] +MOCK_USER_INPUT_ADVANCED = MOCK_USER_DATA +MOCK_USER_INPUT_SIMPLE = { + CONF_HOST: "fake_host", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + CONF_SSL: False, +} + MOCK_DEVICE_INFO = { ATTR_HOST: MOCK_HOST, ATTR_NEW_SERIAL_NUMBER: MOCK_SERIAL_NUMBER, diff --git a/tests/components/fritz/test_config_flow.py b/tests/components/fritz/test_config_flow.py index 074d32bf0ca..64bf3cd9064 100644 --- a/tests/components/fritz/test_config_flow.py +++ b/tests/components/fritz/test_config_flow.py @@ -24,7 +24,13 @@ from homeassistant.components.fritz.const import ( ) from homeassistant.components.ssdp import ATTR_UPNP_UDN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_SSDP, SOURCE_USER -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_SSL, + CONF_USERNAME, +) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -34,12 +40,59 @@ from .const import ( MOCK_REQUEST, MOCK_SSDP_DATA, MOCK_USER_DATA, + MOCK_USER_INPUT_ADVANCED, + MOCK_USER_INPUT_SIMPLE, ) from tests.common import MockConfigEntry -async def test_user(hass: HomeAssistant, fc_class_mock, mock_get_source_ip) -> None: +@pytest.mark.parametrize( + ("show_advanced_options", "user_input", "expected_config"), + [ + ( + True, + MOCK_USER_INPUT_ADVANCED, + { + CONF_HOST: "fake_host", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + CONF_PORT: 1234, + CONF_SSL: False, + }, + ), + ( + False, + MOCK_USER_INPUT_SIMPLE, + { + CONF_HOST: "fake_host", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + CONF_PORT: 49000, + CONF_SSL: False, + }, + ), + ( + False, + {**MOCK_USER_INPUT_SIMPLE, CONF_SSL: True}, + { + CONF_HOST: "fake_host", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + CONF_PORT: 49443, + CONF_SSL: True, + }, + ), + ], +) +async def test_user( + hass: HomeAssistant, + fc_class_mock, + mock_get_source_ip, + show_advanced_options: bool, + user_input: dict, + expected_config: dict, +) -> None: """Test starting a flow by user.""" with ( patch( @@ -68,18 +121,20 @@ async def test_user(hass: HomeAssistant, fc_class_mock, mock_get_source_ip) -> N mock_request_post.return_value.text = MOCK_REQUEST result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} + DOMAIN, + context={ + "source": SOURCE_USER, + "show_advanced_options": show_advanced_options, + }, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_USER_DATA + result["flow_id"], user_input=user_input ) assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["data"][CONF_HOST] == "fake_host" - assert result["data"][CONF_PASSWORD] == "fake_pass" - assert result["data"][CONF_USERNAME] == "fake_user" + assert result["data"] == expected_config assert ( result["options"][CONF_CONSIDER_HOME] == DEFAULT_CONSIDER_HOME.total_seconds() @@ -90,12 +145,20 @@ async def test_user(hass: HomeAssistant, fc_class_mock, mock_get_source_ip) -> N assert mock_setup_entry.called +@pytest.mark.parametrize( + ("show_advanced_options", "user_input"), + [(True, MOCK_USER_INPUT_ADVANCED), (False, MOCK_USER_INPUT_SIMPLE)], +) async def test_user_already_configured( - hass: HomeAssistant, fc_class_mock, mock_get_source_ip + hass: HomeAssistant, + fc_class_mock, + mock_get_source_ip, + show_advanced_options: bool, + user_input, ) -> None: """Test starting a flow by user with an already configured device.""" - mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + mock_config = MockConfigEntry(domain=DOMAIN, data=user_input) mock_config.add_to_hass(hass) with ( @@ -124,13 +187,17 @@ async def test_user_already_configured( mock_request_post.return_value.text = MOCK_REQUEST result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} + DOMAIN, + context={ + "source": SOURCE_USER, + "show_advanced_options": show_advanced_options, + }, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_USER_DATA + result["flow_id"], user_input=MOCK_USER_INPUT_SIMPLE ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -141,13 +208,22 @@ async def test_user_already_configured( "error", FRITZ_AUTH_EXCEPTIONS, ) +@pytest.mark.parametrize( + ("show_advanced_options", "user_input"), + [(True, MOCK_USER_INPUT_ADVANCED), (False, MOCK_USER_INPUT_SIMPLE)], +) async def test_exception_security( - hass: HomeAssistant, mock_get_source_ip, error + hass: HomeAssistant, + mock_get_source_ip, + error, + show_advanced_options: bool, + user_input, ) -> None: """Test starting a flow by user with invalid credentials.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} + DOMAIN, + context={"source": SOURCE_USER, "show_advanced_options": show_advanced_options}, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -157,7 +233,7 @@ async def test_exception_security( side_effect=error, ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_USER_DATA + result["flow_id"], user_input=user_input ) assert result["type"] is FlowResultType.FORM @@ -165,11 +241,21 @@ async def test_exception_security( assert result["errors"]["base"] == ERROR_AUTH_INVALID -async def test_exception_connection(hass: HomeAssistant, mock_get_source_ip) -> None: +@pytest.mark.parametrize( + ("show_advanced_options", "user_input"), + [(True, MOCK_USER_INPUT_ADVANCED), (False, MOCK_USER_INPUT_SIMPLE)], +) +async def test_exception_connection( + hass: HomeAssistant, + mock_get_source_ip, + show_advanced_options: bool, + user_input, +) -> None: """Test starting a flow by user with a connection error.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} + DOMAIN, + context={"source": SOURCE_USER, "show_advanced_options": show_advanced_options}, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -179,7 +265,7 @@ async def test_exception_connection(hass: HomeAssistant, mock_get_source_ip) -> side_effect=FritzConnectionException, ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_USER_DATA + result["flow_id"], user_input=user_input ) assert result["type"] is FlowResultType.FORM @@ -187,11 +273,18 @@ async def test_exception_connection(hass: HomeAssistant, mock_get_source_ip) -> assert result["errors"]["base"] == ERROR_CANNOT_CONNECT -async def test_exception_unknown(hass: HomeAssistant, mock_get_source_ip) -> None: +@pytest.mark.parametrize( + ("show_advanced_options", "user_input"), + [(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 +) -> None: """Test starting a flow by user with an unknown exception.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} + DOMAIN, + context={"source": SOURCE_USER, "show_advanced_options": show_advanced_options}, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -201,7 +294,7 @@ async def test_exception_unknown(hass: HomeAssistant, mock_get_source_ip) -> Non side_effect=OSError, ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_USER_DATA + result["flow_id"], user_input=user_input ) assert result["type"] is FlowResultType.FORM @@ -210,7 +303,9 @@ async def test_exception_unknown(hass: HomeAssistant, mock_get_source_ip) -> Non async def test_reauth_successful( - hass: HomeAssistant, fc_class_mock, mock_get_source_ip + hass: HomeAssistant, + fc_class_mock, + mock_get_source_ip, ) -> None: """Test starting a reauthentication flow.""" @@ -273,7 +368,11 @@ async def test_reauth_successful( ], ) async def test_reauth_not_successful( - hass: HomeAssistant, fc_class_mock, mock_get_source_ip, side_effect, error + hass: HomeAssistant, + fc_class_mock, + mock_get_source_ip, + side_effect, + error, ) -> None: """Test starting a reauthentication flow but no connection found.""" diff --git a/tests/components/fritz/test_switch.py b/tests/components/fritz/test_switch.py index adb5c3f6799..b82587d42bd 100644 --- a/tests/components/fritz/test_switch.py +++ b/tests/components/fritz/test_switch.py @@ -15,6 +15,8 @@ from tests.common import MockConfigEntry MOCK_WLANCONFIGS_SAME_SSID: dict[str, dict] = { "WLANConfiguration1": { + "GetSSID": {"NewSSID": "WiFi"}, + "GetSecurityKeys": {"NewKeyPassphrase": "mysecret"}, "GetInfo": { "NewEnable": True, "NewStatus": "Up", @@ -34,9 +36,11 @@ MOCK_WLANCONFIGS_SAME_SSID: dict[str, dict] = { "NewMinCharsPSK": 64, "NewMaxCharsPSK": 64, "NewAllowedCharsPSK": "0123456789ABCDEFabcdef", - } + }, }, "WLANConfiguration2": { + "GetSSID": {"NewSSID": "WiFi"}, + "GetSecurityKeys": {"NewKeyPassphrase": "mysecret"}, "GetInfo": { "NewEnable": True, "NewStatus": "Up", @@ -56,11 +60,13 @@ MOCK_WLANCONFIGS_SAME_SSID: dict[str, dict] = { "NewMinCharsPSK": 64, "NewMaxCharsPSK": 64, "NewAllowedCharsPSK": "0123456789ABCDEFabcdef", - } + }, }, } MOCK_WLANCONFIGS_DIFF_SSID: dict[str, dict] = { "WLANConfiguration1": { + "GetSSID": {"NewSSID": "WiFi"}, + "GetSecurityKeys": {"NewKeyPassphrase": "mysecret"}, "GetInfo": { "NewEnable": True, "NewStatus": "Up", @@ -80,9 +86,11 @@ MOCK_WLANCONFIGS_DIFF_SSID: dict[str, dict] = { "NewMinCharsPSK": 64, "NewMaxCharsPSK": 64, "NewAllowedCharsPSK": "0123456789ABCDEFabcdef", - } + }, }, "WLANConfiguration2": { + "GetSSID": {"NewSSID": "WiFi2"}, + "GetSecurityKeys": {"NewKeyPassphrase": "mysecret"}, "GetInfo": { "NewEnable": True, "NewStatus": "Up", @@ -102,11 +110,13 @@ MOCK_WLANCONFIGS_DIFF_SSID: dict[str, dict] = { "NewMinCharsPSK": 64, "NewMaxCharsPSK": 64, "NewAllowedCharsPSK": "0123456789ABCDEFabcdef", - } + }, }, } MOCK_WLANCONFIGS_DIFF2_SSID: dict[str, dict] = { "WLANConfiguration1": { + "GetSSID": {"NewSSID": "WiFi"}, + "GetSecurityKeys": {"NewKeyPassphrase": "mysecret"}, "GetInfo": { "NewEnable": True, "NewStatus": "Up", @@ -126,9 +136,11 @@ MOCK_WLANCONFIGS_DIFF2_SSID: dict[str, dict] = { "NewMinCharsPSK": 64, "NewMaxCharsPSK": 64, "NewAllowedCharsPSK": "0123456789ABCDEFabcdef", - } + }, }, "WLANConfiguration2": { + "GetSSID": {"NewSSID": "WiFi+"}, + "GetSecurityKeys": {"NewKeyPassphrase": "mysecret"}, "GetInfo": { "NewEnable": True, "NewStatus": "Up", @@ -148,7 +160,7 @@ MOCK_WLANCONFIGS_DIFF2_SSID: dict[str, dict] = { "NewMinCharsPSK": 64, "NewMaxCharsPSK": 64, "NewAllowedCharsPSK": "0123456789ABCDEFabcdef", - } + }, }, } @@ -179,7 +191,7 @@ async def test_switch_setup( entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert entry.state is ConfigEntryState.LOADED switches = hass.states.async_all(Platform.SWITCH) From 7d386b0d26592c08d46e6ef13c70003bb12ae1a2 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Sun, 21 Apr 2024 07:54:24 +1000 Subject: [PATCH 718/967] Fix sensor entity description in Teslemetry (#115614) Add description back to sensor entity --- homeassistant/components/teslemetry/sensor.py | 5 +- .../teslemetry/snapshots/test_sensor.ambr | 538 ++++++++++++++---- 2 files changed, 428 insertions(+), 115 deletions(-) diff --git a/homeassistant/components/teslemetry/sensor.py b/homeassistant/components/teslemetry/sensor.py index cced1090e2a..6380a4d0c71 100644 --- a/homeassistant/components/teslemetry/sensor.py +++ b/homeassistant/components/teslemetry/sensor.py @@ -58,7 +58,7 @@ SHIFT_STATES = {"P": "p", "D": "d", "R": "r", "N": "n"} class TeslemetrySensorEntityDescription(SensorEntityDescription): """Describes Teslemetry Sensor entity.""" - value_fn: Callable[[StateType], StateType | datetime] = lambda x: x + value_fn: Callable[[StateType], StateType] = lambda x: x VEHICLE_DESCRIPTIONS: tuple[TeslemetrySensorEntityDescription, ...] = ( @@ -447,12 +447,13 @@ class TeslemetryVehicleSensorEntity(TeslemetryVehicleEntity, SensorEntity): description: TeslemetrySensorEntityDescription, ) -> None: """Initialize the sensor.""" + self.entity_description = description super().__init__(vehicle, description.key) @property def native_value(self) -> StateType: """Return the state of the sensor.""" - return self._value + return self.entity_description.value_fn(self._value) class TeslemetryVehicleTimeSensorEntity(TeslemetryVehicleEntity, SensorEntity): diff --git a/tests/components/teslemetry/snapshots/test_sensor.ambr b/tests/components/teslemetry/snapshots/test_sensor.ambr index 81142e40901..0d817ad1f7e 100644 --- a/tests/components/teslemetry/snapshots/test_sensor.ambr +++ b/tests/components/teslemetry/snapshots/test_sensor.ambr @@ -719,7 +719,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -736,7 +738,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Battery level', 'platform': 'teslemetry', @@ -744,13 +746,16 @@ 'supported_features': 0, 'translation_key': 'charge_state_battery_level', 'unique_id': 'VINVINVIN-charge_state_battery_level', - 'unit_of_measurement': None, + 'unit_of_measurement': '%', }) # --- # name: test_sensors[sensor.test_battery_level-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'battery', 'friendly_name': 'Test Battery level', + 'state_class': , + 'unit_of_measurement': '%', }), 'context': , 'entity_id': 'sensor.test_battery_level', @@ -763,7 +768,10 @@ # name: test_sensors[sensor.test_battery_level-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'battery', 'friendly_name': 'Test Battery level', + 'state_class': , + 'unit_of_measurement': '%', }), 'context': , 'entity_id': 'sensor.test_battery_level', @@ -778,7 +786,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -794,8 +804,14 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Battery range', 'platform': 'teslemetry', @@ -803,33 +819,39 @@ 'supported_features': 0, 'translation_key': 'charge_state_battery_range', 'unique_id': 'VINVINVIN-charge_state_battery_range', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.test_battery_range-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'distance', 'friendly_name': 'Test Battery range', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_battery_range', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '266.87', + 'state': '429.48563328', }) # --- # name: test_sensors[sensor.test_battery_range-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'distance', 'friendly_name': 'Test Battery range', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_battery_range', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '266.87', + 'state': '429.48563328', }) # --- # name: test_sensors[sensor.test_charge_cable-entry] @@ -843,7 +865,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.test_charge_cable', 'has_entity_name': True, 'hidden_by': None, @@ -896,7 +918,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -912,8 +936,11 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Charge energy added', 'platform': 'teslemetry', @@ -921,13 +948,16 @@ 'supported_features': 0, 'translation_key': 'charge_state_charge_energy_added', 'unique_id': 'VINVINVIN-charge_state_charge_energy_added', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.test_charge_energy_added-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'energy', 'friendly_name': 'Test Charge energy added', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_charge_energy_added', @@ -940,7 +970,10 @@ # name: test_sensors[sensor.test_charge_energy_added-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'energy', 'friendly_name': 'Test Charge energy added', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_charge_energy_added', @@ -955,13 +988,15 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.test_charge_rate', 'has_entity_name': True, 'hidden_by': None, @@ -971,8 +1006,11 @@ }), 'name': None, 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Charge rate', 'platform': 'teslemetry', @@ -980,13 +1018,16 @@ 'supported_features': 0, 'translation_key': 'charge_state_charge_rate', 'unique_id': 'VINVINVIN-charge_state_charge_rate', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.test_charge_rate-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'speed', 'friendly_name': 'Test Charge rate', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_charge_rate', @@ -999,7 +1040,10 @@ # name: test_sensors[sensor.test_charge_rate-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'speed', 'friendly_name': 'Test Charge rate', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_charge_rate', @@ -1014,13 +1058,15 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.test_charger_current', 'has_entity_name': True, 'hidden_by': None, @@ -1031,7 +1077,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Charger current', 'platform': 'teslemetry', @@ -1039,13 +1085,16 @@ 'supported_features': 0, 'translation_key': 'charge_state_charger_actual_current', 'unique_id': 'VINVINVIN-charge_state_charger_actual_current', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.test_charger_current-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'current', 'friendly_name': 'Test Charger current', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_charger_current', @@ -1058,7 +1107,10 @@ # name: test_sensors[sensor.test_charger_current-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'current', 'friendly_name': 'Test Charger current', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_charger_current', @@ -1073,7 +1125,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -1090,7 +1144,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Charger power', 'platform': 'teslemetry', @@ -1098,13 +1152,16 @@ 'supported_features': 0, 'translation_key': 'charge_state_charger_power', 'unique_id': 'VINVINVIN-charge_state_charger_power', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.test_charger_power-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'power', 'friendly_name': 'Test Charger power', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_charger_power', @@ -1117,7 +1174,10 @@ # name: test_sensors[sensor.test_charger_power-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'power', 'friendly_name': 'Test Charger power', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_charger_power', @@ -1132,13 +1192,15 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.test_charger_voltage', 'has_entity_name': True, 'hidden_by': None, @@ -1149,7 +1211,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Charger voltage', 'platform': 'teslemetry', @@ -1157,13 +1219,16 @@ 'supported_features': 0, 'translation_key': 'charge_state_charger_voltage', 'unique_id': 'VINVINVIN-charge_state_charger_voltage', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.test_charger_voltage-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', 'friendly_name': 'Test Charger voltage', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_charger_voltage', @@ -1176,7 +1241,10 @@ # name: test_sensors[sensor.test_charger_voltage-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', 'friendly_name': 'Test Charger voltage', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_charger_voltage', @@ -1191,7 +1259,16 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'options': list([ + 'starting', + 'charging', + 'stopped', + 'complete', + 'disconnected', + 'no_power', + ]), + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -1208,7 +1285,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Charging', 'platform': 'teslemetry', @@ -1222,27 +1299,45 @@ # name: test_sensors[sensor.test_charging-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'enum', 'friendly_name': 'Test Charging', + 'options': list([ + 'starting', + 'charging', + 'stopped', + 'complete', + 'disconnected', + 'no_power', + ]), }), 'context': , 'entity_id': 'sensor.test_charging', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'Stopped', + 'state': 'stopped', }) # --- # name: test_sensors[sensor.test_charging-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'enum', 'friendly_name': 'Test Charging', + 'options': list([ + 'starting', + 'charging', + 'stopped', + 'complete', + 'disconnected', + 'no_power', + ]), }), 'context': , 'entity_id': 'sensor.test_charging', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'Stopped', + 'state': 'stopped', }) # --- # name: test_sensors[sensor.test_distance_to_arrival-entry] @@ -1250,7 +1345,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -1266,8 +1363,11 @@ }), 'name': None, 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Distance to arrival', 'platform': 'teslemetry', @@ -1275,26 +1375,32 @@ 'supported_features': 0, 'translation_key': 'drive_state_active_route_miles_to_arrival', 'unique_id': 'VINVINVIN-drive_state_active_route_miles_to_arrival', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.test_distance_to_arrival-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'distance', 'friendly_name': 'Test Distance to arrival', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_distance_to_arrival', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.039491', + 'state': '0.063555', }) # --- # name: test_sensors[sensor.test_distance_to_arrival-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'distance', 'friendly_name': 'Test Distance to arrival', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_distance_to_arrival', @@ -1309,13 +1415,15 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.test_driver_temperature_setting', 'has_entity_name': True, 'hidden_by': None, @@ -1325,8 +1433,11 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Driver temperature setting', 'platform': 'teslemetry', @@ -1334,13 +1445,16 @@ 'supported_features': 0, 'translation_key': 'climate_state_driver_temp_setting', 'unique_id': 'VINVINVIN-climate_state_driver_temp_setting', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.test_driver_temperature_setting-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', 'friendly_name': 'Test Driver temperature setting', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_driver_temperature_setting', @@ -1353,7 +1467,10 @@ # name: test_sensors[sensor.test_driver_temperature_setting-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', 'friendly_name': 'Test Driver temperature setting', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_driver_temperature_setting', @@ -1368,7 +1485,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -1384,8 +1503,14 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Estimate battery range', 'platform': 'teslemetry', @@ -1393,33 +1518,39 @@ 'supported_features': 0, 'translation_key': 'charge_state_est_battery_range', 'unique_id': 'VINVINVIN-charge_state_est_battery_range', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.test_estimate_battery_range-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'distance', 'friendly_name': 'Test Estimate battery range', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_estimate_battery_range', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '275.04', + 'state': '442.63397376', }) # --- # name: test_sensors[sensor.test_estimate_battery_range-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'distance', 'friendly_name': 'Test Estimate battery range', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_estimate_battery_range', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '275.04', + 'state': '442.63397376', }) # --- # name: test_sensors[sensor.test_fast_charger_type-entry] @@ -1433,7 +1564,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.test_fast_charger_type', 'has_entity_name': True, 'hidden_by': None, @@ -1486,7 +1617,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -1502,8 +1635,14 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Ideal battery range', 'platform': 'teslemetry', @@ -1511,33 +1650,39 @@ 'supported_features': 0, 'translation_key': 'charge_state_ideal_battery_range', 'unique_id': 'VINVINVIN-charge_state_ideal_battery_range', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.test_ideal_battery_range-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'distance', 'friendly_name': 'Test Ideal battery range', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_ideal_battery_range', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '266.87', + 'state': '429.48563328', }) # --- # name: test_sensors[sensor.test_ideal_battery_range-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'distance', 'friendly_name': 'Test Ideal battery range', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_ideal_battery_range', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '266.87', + 'state': '429.48563328', }) # --- # name: test_sensors[sensor.test_inside_temperature-entry] @@ -1545,7 +1690,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -1561,8 +1708,11 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Inside temperature', 'platform': 'teslemetry', @@ -1570,13 +1720,16 @@ 'supported_features': 0, 'translation_key': 'climate_state_inside_temp', 'unique_id': 'VINVINVIN-climate_state_inside_temp', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.test_inside_temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', 'friendly_name': 'Test Inside temperature', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_inside_temperature', @@ -1589,7 +1742,10 @@ # name: test_sensors[sensor.test_inside_temperature-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', 'friendly_name': 'Test Inside temperature', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_inside_temperature', @@ -1604,13 +1760,15 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.test_odometer', 'has_entity_name': True, 'hidden_by': None, @@ -1620,8 +1778,14 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Odometer', 'platform': 'teslemetry', @@ -1629,33 +1793,39 @@ 'supported_features': 0, 'translation_key': 'vehicle_state_odometer', 'unique_id': 'VINVINVIN-vehicle_state_odometer', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.test_odometer-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'distance', 'friendly_name': 'Test Odometer', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_odometer', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '6481.019282', + 'state': '10430.189495371', }) # --- # name: test_sensors[sensor.test_odometer-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'distance', 'friendly_name': 'Test Odometer', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_odometer', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '6481.019282', + 'state': '10430.189495371', }) # --- # name: test_sensors[sensor.test_outside_temperature-entry] @@ -1663,7 +1833,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -1679,8 +1851,11 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Outside temperature', 'platform': 'teslemetry', @@ -1688,13 +1863,16 @@ 'supported_features': 0, 'translation_key': 'climate_state_outside_temp', 'unique_id': 'VINVINVIN-climate_state_outside_temp', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.test_outside_temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', 'friendly_name': 'Test Outside temperature', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_outside_temperature', @@ -1707,7 +1885,10 @@ # name: test_sensors[sensor.test_outside_temperature-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', 'friendly_name': 'Test Outside temperature', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_outside_temperature', @@ -1722,13 +1903,15 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.test_passenger_temperature_setting', 'has_entity_name': True, 'hidden_by': None, @@ -1738,8 +1921,11 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Passenger temperature setting', 'platform': 'teslemetry', @@ -1747,13 +1933,16 @@ 'supported_features': 0, 'translation_key': 'climate_state_passenger_temp_setting', 'unique_id': 'VINVINVIN-climate_state_passenger_temp_setting', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.test_passenger_temperature_setting-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', 'friendly_name': 'Test Passenger temperature setting', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_passenger_temperature_setting', @@ -1766,7 +1955,10 @@ # name: test_sensors[sensor.test_passenger_temperature_setting-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', 'friendly_name': 'Test Passenger temperature setting', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_passenger_temperature_setting', @@ -1781,13 +1973,15 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.test_power', 'has_entity_name': True, 'hidden_by': None, @@ -1798,7 +1992,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Power', 'platform': 'teslemetry', @@ -1806,13 +2000,16 @@ 'supported_features': 0, 'translation_key': 'drive_state_power', 'unique_id': 'VINVINVIN-drive_state_power', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.test_power-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'power', 'friendly_name': 'Test Power', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_power', @@ -1825,7 +2022,10 @@ # name: test_sensors[sensor.test_power-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'power', 'friendly_name': 'Test Power', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_power', @@ -1840,7 +2040,14 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'options': list([ + 'p', + 'd', + 'r', + 'n', + ]), + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -1857,7 +2064,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Shift state', 'platform': 'teslemetry', @@ -1871,27 +2078,41 @@ # name: test_sensors[sensor.test_shift_state-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'enum', 'friendly_name': 'Test Shift state', + 'options': list([ + 'p', + 'd', + 'r', + 'n', + ]), }), 'context': , 'entity_id': 'sensor.test_shift_state', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'p', }) # --- # name: test_sensors[sensor.test_shift_state-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'enum', 'friendly_name': 'Test Shift state', + 'options': list([ + 'p', + 'd', + 'r', + 'n', + ]), }), 'context': , 'entity_id': 'sensor.test_shift_state', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'p', }) # --- # name: test_sensors[sensor.test_speed-entry] @@ -1899,7 +2120,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -1915,8 +2138,11 @@ }), 'name': None, 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Speed', 'platform': 'teslemetry', @@ -1924,33 +2150,39 @@ 'supported_features': 0, 'translation_key': 'drive_state_speed', 'unique_id': 'VINVINVIN-drive_state_speed', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.test_speed-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'speed', 'friendly_name': 'Test Speed', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_speed', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0', }) # --- # name: test_sensors[sensor.test_speed-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'speed', 'friendly_name': 'Test Speed', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_speed', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0', }) # --- # name: test_sensors[sensor.test_state_of_charge_at_arrival-entry] @@ -1958,13 +2190,15 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.test_state_of_charge_at_arrival', 'has_entity_name': True, 'hidden_by': None, @@ -1975,7 +2209,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'State of charge at arrival', 'platform': 'teslemetry', @@ -1983,13 +2217,16 @@ 'supported_features': 0, 'translation_key': 'drive_state_active_route_energy_at_arrival', 'unique_id': 'VINVINVIN-drive_state_active_route_energy_at_arrival', - 'unit_of_measurement': None, + 'unit_of_measurement': '%', }) # --- # name: test_sensors[sensor.test_state_of_charge_at_arrival-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'battery', 'friendly_name': 'Test State of charge at arrival', + 'state_class': , + 'unit_of_measurement': '%', }), 'context': , 'entity_id': 'sensor.test_state_of_charge_at_arrival', @@ -2002,7 +2239,10 @@ # name: test_sensors[sensor.test_state_of_charge_at_arrival-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'battery', 'friendly_name': 'Test State of charge at arrival', + 'state_class': , + 'unit_of_measurement': '%', }), 'context': , 'entity_id': 'sensor.test_state_of_charge_at_arrival', @@ -2139,13 +2379,15 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.test_tire_pressure_front_left', 'has_entity_name': True, 'hidden_by': None, @@ -2155,8 +2397,14 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Tire pressure front left', 'platform': 'teslemetry', @@ -2164,33 +2412,39 @@ 'supported_features': 0, 'translation_key': 'vehicle_state_tpms_pressure_fl', 'unique_id': 'VINVINVIN-vehicle_state_tpms_pressure_fl', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.test_tire_pressure_front_left-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', 'friendly_name': 'Test Tire pressure front left', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_tire_pressure_front_left', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2.775', + 'state': '40.2479739314961', }) # --- # name: test_sensors[sensor.test_tire_pressure_front_left-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', 'friendly_name': 'Test Tire pressure front left', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_tire_pressure_front_left', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2.775', + 'state': '40.2479739314961', }) # --- # name: test_sensors[sensor.test_tire_pressure_front_right-entry] @@ -2198,13 +2452,15 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.test_tire_pressure_front_right', 'has_entity_name': True, 'hidden_by': None, @@ -2214,8 +2470,14 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Tire pressure front right', 'platform': 'teslemetry', @@ -2223,33 +2485,39 @@ 'supported_features': 0, 'translation_key': 'vehicle_state_tpms_pressure_fr', 'unique_id': 'VINVINVIN-vehicle_state_tpms_pressure_fr', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.test_tire_pressure_front_right-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', 'friendly_name': 'Test Tire pressure front right', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_tire_pressure_front_right', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2.8', + 'state': '40.6105682912393', }) # --- # name: test_sensors[sensor.test_tire_pressure_front_right-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', 'friendly_name': 'Test Tire pressure front right', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_tire_pressure_front_right', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2.8', + 'state': '40.6105682912393', }) # --- # name: test_sensors[sensor.test_tire_pressure_rear_left-entry] @@ -2257,13 +2525,15 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.test_tire_pressure_rear_left', 'has_entity_name': True, 'hidden_by': None, @@ -2273,8 +2543,14 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Tire pressure rear left', 'platform': 'teslemetry', @@ -2282,33 +2558,39 @@ 'supported_features': 0, 'translation_key': 'vehicle_state_tpms_pressure_rl', 'unique_id': 'VINVINVIN-vehicle_state_tpms_pressure_rl', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.test_tire_pressure_rear_left-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', 'friendly_name': 'Test Tire pressure rear left', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_tire_pressure_rear_left', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2.775', + 'state': '40.2479739314961', }) # --- # name: test_sensors[sensor.test_tire_pressure_rear_left-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', 'friendly_name': 'Test Tire pressure rear left', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_tire_pressure_rear_left', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2.775', + 'state': '40.2479739314961', }) # --- # name: test_sensors[sensor.test_tire_pressure_rear_right-entry] @@ -2316,13 +2598,15 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.test_tire_pressure_rear_right', 'has_entity_name': True, 'hidden_by': None, @@ -2332,8 +2616,14 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Tire pressure rear right', 'platform': 'teslemetry', @@ -2341,33 +2631,39 @@ 'supported_features': 0, 'translation_key': 'vehicle_state_tpms_pressure_rr', 'unique_id': 'VINVINVIN-vehicle_state_tpms_pressure_rr', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.test_tire_pressure_rear_right-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', 'friendly_name': 'Test Tire pressure rear right', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_tire_pressure_rear_right', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2.775', + 'state': '40.2479739314961', }) # --- # name: test_sensors[sensor.test_tire_pressure_rear_right-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', 'friendly_name': 'Test Tire pressure rear right', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_tire_pressure_rear_right', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2.775', + 'state': '40.2479739314961', }) # --- # name: test_sensors[sensor.test_traffic_delay-entry] @@ -2375,7 +2671,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -2392,7 +2690,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Traffic delay', 'platform': 'teslemetry', @@ -2400,13 +2698,16 @@ 'supported_features': 0, 'translation_key': 'drive_state_active_route_traffic_minutes_delay', 'unique_id': 'VINVINVIN-drive_state_active_route_traffic_minutes_delay', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.test_traffic_delay-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'duration', 'friendly_name': 'Test Traffic delay', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_traffic_delay', @@ -2419,7 +2720,10 @@ # name: test_sensors[sensor.test_traffic_delay-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'duration', 'friendly_name': 'Test Traffic delay', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_traffic_delay', @@ -2434,7 +2738,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -2451,7 +2757,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Usable battery level', 'platform': 'teslemetry', @@ -2459,13 +2765,16 @@ 'supported_features': 0, 'translation_key': 'charge_state_usable_battery_level', 'unique_id': 'VINVINVIN-charge_state_usable_battery_level', - 'unit_of_measurement': None, + 'unit_of_measurement': '%', }) # --- # name: test_sensors[sensor.test_usable_battery_level-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'battery', 'friendly_name': 'Test Usable battery level', + 'state_class': , + 'unit_of_measurement': '%', }), 'context': , 'entity_id': 'sensor.test_usable_battery_level', @@ -2478,7 +2787,10 @@ # name: test_sensors[sensor.test_usable_battery_level-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'battery', 'friendly_name': 'Test Usable battery level', + 'state_class': , + 'unit_of_measurement': '%', }), 'context': , 'entity_id': 'sensor.test_usable_battery_level', From 29bfed72f70b18c525e21ba060cc5abd772258ce Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 21 Apr 2024 01:08:40 +0200 Subject: [PATCH 719/967] Fix flaky history stats test (#115824) --- tests/components/history_stats/test_sensor.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index 9a7d8ef110a..4b4592c2104 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -1376,9 +1376,12 @@ async def test_measure_cet(recorder_mock: Recorder, hass: HomeAssistant) -> None ] } - with patch( - "homeassistant.components.recorder.history.state_changes_during_period", - _fake_states, + with ( + patch( + "homeassistant.components.recorder.history.state_changes_during_period", + _fake_states, + ), + freeze_time(start_time), ): await async_setup_component( hass, From d8117fd2bd93f81bfc5fc9cd3d5ffc3ddcaf53f5 Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Sat, 20 Apr 2024 22:57:05 -0400 Subject: [PATCH 720/967] Fix Roborock status not correctly mapping for some devices (#115646) Use device_info.model instead of name --- homeassistant/components/roborock/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/roborock/__init__.py b/homeassistant/components/roborock/__init__.py index b72fec5a8e1..12a884dba48 100644 --- a/homeassistant/components/roborock/__init__.py +++ b/homeassistant/components/roborock/__init__.py @@ -107,7 +107,9 @@ async def setup_device( home_data_rooms: list[HomeDataRoom], ) -> RoborockDataUpdateCoordinator | None: """Set up a device Coordinator.""" - mqtt_client = RoborockMqttClientV1(user_data, DeviceData(device, product_info.name)) + mqtt_client = RoborockMqttClientV1( + user_data, DeviceData(device, product_info.model) + ) try: networking = await mqtt_client.get_networking() if networking is None: From 30a60fd38b03009b5dc807962b533b3cfbb91ca2 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Sun, 21 Apr 2024 04:17:11 +0100 Subject: [PATCH 721/967] Improve debug logging for evohome (#110256) better logging --- homeassistant/components/evohome/__init__.py | 21 +++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 3017685a307..49920d79ff3 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -19,7 +19,10 @@ from evohomeasync2.schema.const import ( SZ_ALLOWED_SYSTEM_MODES, SZ_AUTO_WITH_RESET, SZ_CAN_BE_TEMPORARY, + SZ_GATEWAY_ID, + SZ_GATEWAY_INFO, SZ_HEAT_SETPOINT, + SZ_LOCATION_ID, SZ_LOCATION_INFO, SZ_SETPOINT_STATUS, SZ_STATE_STATUS, @@ -261,14 +264,18 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return False if _LOGGER.isEnabledFor(logging.DEBUG): - _config: dict[str, Any] = { - SZ_LOCATION_INFO: {SZ_TIME_ZONE: None}, - GWS: [{TCS: None}], + loc_info = { + SZ_LOCATION_ID: loc_config[SZ_LOCATION_INFO][SZ_LOCATION_ID], + SZ_TIME_ZONE: loc_config[SZ_LOCATION_INFO][SZ_TIME_ZONE], + } + gwy_info = { + SZ_GATEWAY_ID: loc_config[GWS][0][SZ_GATEWAY_INFO][SZ_GATEWAY_ID], + TCS: loc_config[GWS][0][TCS], + } + _config = { + SZ_LOCATION_INFO: loc_info, + GWS: [{SZ_GATEWAY_INFO: gwy_info, TCS: loc_config[GWS][0][TCS]}], } - _config[SZ_LOCATION_INFO][SZ_TIME_ZONE] = loc_config[SZ_LOCATION_INFO][ - SZ_TIME_ZONE - ] - _config[GWS][0][TCS] = loc_config[GWS][0][TCS] _LOGGER.debug("Config = %s", _config) client_v1 = ev1.EvohomeClient( From 27bccf0b2447c574541142909ec48bf195c6379a Mon Sep 17 00:00:00 2001 From: Adam Goode Date: Sat, 20 Apr 2024 23:20:01 -0400 Subject: [PATCH 722/967] Add test for prometheus export of entities becoming unavailable and available again (#112157) Add test for state change to unavailable and back --- tests/components/prometheus/test_init.py | 121 +++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index 99b73209ad7..499d1a5df14 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -57,6 +57,7 @@ from homeassistant.const import ( STATE_ON, STATE_OPEN, STATE_OPENING, + STATE_UNAVAILABLE, STATE_UNLOCKED, UnitOfEnergy, UnitOfTemperature, @@ -1053,6 +1054,126 @@ async def test_disabling_entity( ) +@pytest.mark.parametrize("namespace", [""]) +async def test_entity_becomes_unavailable_with_export( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + client: ClientSessionGenerator, + sensor_entities: dict[str, er.RegistryEntry], +) -> None: + """Test an entity that becomes unavailable is still exported.""" + data = {**sensor_entities} + + await hass.async_block_till_done() + body = await generate_latest_metrics(client) + + assert ( + 'sensor_temperature_celsius{domain="sensor",' + 'entity="sensor.outside_temperature",' + 'friendly_name="Outside Temperature"} 15.6' in body + ) + + assert ( + 'state_change_total{domain="sensor",' + 'entity="sensor.outside_temperature",' + 'friendly_name="Outside Temperature"} 1.0' in body + ) + + assert ( + 'entity_available{domain="sensor",' + 'entity="sensor.outside_temperature",' + 'friendly_name="Outside Temperature"} 1.0' in body + ) + + assert ( + 'sensor_humidity_percent{domain="sensor",' + 'entity="sensor.outside_humidity",' + 'friendly_name="Outside Humidity"} 54.0' in body + ) + + assert ( + 'state_change_total{domain="sensor",' + 'entity="sensor.outside_humidity",' + 'friendly_name="Outside Humidity"} 1.0' in body + ) + + assert ( + 'entity_available{domain="sensor",' + 'entity="sensor.outside_humidity",' + 'friendly_name="Outside Humidity"} 1.0' in body + ) + + # Make sensor_1 unavailable. + set_state_with_entry( + hass, data["sensor_1"], STATE_UNAVAILABLE, data["sensor_1_attributes"] + ) + + await hass.async_block_till_done() + body = await generate_latest_metrics(client) + + # Check that only the availability changed on sensor_1. + assert ( + 'sensor_temperature_celsius{domain="sensor",' + 'entity="sensor.outside_temperature",' + 'friendly_name="Outside Temperature"} 15.6' in body + ) + + assert ( + 'state_change_total{domain="sensor",' + 'entity="sensor.outside_temperature",' + 'friendly_name="Outside Temperature"} 2.0' in body + ) + + assert ( + 'entity_available{domain="sensor",' + 'entity="sensor.outside_temperature",' + 'friendly_name="Outside Temperature"} 0.0' in body + ) + + # The other sensor should be unchanged. + assert ( + 'sensor_humidity_percent{domain="sensor",' + 'entity="sensor.outside_humidity",' + 'friendly_name="Outside Humidity"} 54.0' in body + ) + + assert ( + 'state_change_total{domain="sensor",' + 'entity="sensor.outside_humidity",' + 'friendly_name="Outside Humidity"} 1.0' in body + ) + + assert ( + 'entity_available{domain="sensor",' + 'entity="sensor.outside_humidity",' + 'friendly_name="Outside Humidity"} 1.0' in body + ) + + # Bring sensor_1 back and check that it is correct. + set_state_with_entry(hass, data["sensor_1"], 200.0, data["sensor_1_attributes"]) + + await hass.async_block_till_done() + body = await generate_latest_metrics(client) + + assert ( + 'sensor_temperature_celsius{domain="sensor",' + 'entity="sensor.outside_temperature",' + 'friendly_name="Outside Temperature"} 200.0' in body + ) + + assert ( + 'state_change_total{domain="sensor",' + 'entity="sensor.outside_temperature",' + 'friendly_name="Outside Temperature"} 3.0' in body + ) + + assert ( + 'entity_available{domain="sensor",' + 'entity="sensor.outside_temperature",' + 'friendly_name="Outside Temperature"} 1.0' in body + ) + + @pytest.fixture(name="sensor_entities") async def sensor_fixture( hass: HomeAssistant, entity_registry: er.EntityRegistry From b592225a8720aeb3a8e7b0b7dface7e7c78116e5 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 21 Apr 2024 08:54:23 +0200 Subject: [PATCH 723/967] Improve service validation exception test and translation key (#115843) * Small improvement to service validation exception test and translation key * Apply suggestions from code review Co-authored-by: Martin Hjelmare * Refactor string assertion --------- Co-authored-by: Martin Hjelmare --- homeassistant/components/homeassistant/strings.json | 2 +- homeassistant/core.py | 2 +- tests/test_core.py | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/homeassistant/strings.json b/homeassistant/components/homeassistant/strings.json index d46a2e50bfd..09b2f17c947 100644 --- a/homeassistant/components/homeassistant/strings.json +++ b/homeassistant/components/homeassistant/strings.json @@ -192,7 +192,7 @@ "service_not_found": { "message": "Service {domain}.{service} not found." }, - "service_does_not_supports_reponse": { + "service_does_not_support_response": { "message": "A service which does not return responses can't be called with {return_response}." }, "service_lacks_response_request": { diff --git a/homeassistant/core.py b/homeassistant/core.py index 919e0adb758..8471d2c4dcc 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -2589,7 +2589,7 @@ class ServiceRegistry: if handler.supports_response is SupportsResponse.NONE: raise ServiceValidationError( translation_domain=DOMAIN, - translation_key="service_does_not_supports_reponse", + translation_key="service_does_not_support_response", translation_placeholders={ "return_response": "return_response=True" }, diff --git a/tests/test_core.py b/tests/test_core.py index 8f0d7f53277..ce71fcd42e5 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1803,9 +1803,9 @@ async def test_services_call_return_response_requires_blocking( blocking=False, return_response=True, ) - assert ( - str(exc.value) - == "A non blocking service call with argument blocking=False can't be used together with argument return_response=True" + assert str(exc.value) == ( + "A non blocking service call with argument blocking=False " + "can't be used together with argument return_response=True" ) From 1c0c0bb0bc0e3e0ecdae11c84dc4154e22358265 Mon Sep 17 00:00:00 2001 From: wittypluck Date: Sun, 21 Apr 2024 11:08:39 +0200 Subject: [PATCH 724/967] Allow manual delete of stale Unifi device from UI (#115267) * Allow manual delete of stale device from UI * Add unit tests for remove_config_entry_device --- homeassistant/components/unifi/__init__.py | 13 +++ tests/components/unifi/test_init.py | 100 ++++++++++++++++++++- 2 files changed, 112 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index 5174a1a7796..69a6ec423ae 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -7,6 +7,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import ConfigType @@ -73,6 +74,18 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return await hub.async_reset() +async def async_remove_config_entry_device( + hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry +) -> bool: + """Remove config entry from a device.""" + hub: UnifiHub = hass.data[UNIFI_DOMAIN][config_entry.entry_id] + return not any( + identifier + for _, identifier in device_entry.connections + if identifier in hub.api.clients or identifier in hub.api.devices + ) + + class UnifiWirelessClients: """Class to store clients known to be wireless. diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py index 9053b47cbaf..bd9a29f2c8b 100644 --- a/tests/components/unifi/test_init.py +++ b/tests/components/unifi/test_init.py @@ -3,10 +3,20 @@ from typing import Any from unittest.mock import patch +from aiounifi.models.message import MessageKey + +from homeassistant import loader from homeassistant.components import unifi -from homeassistant.components.unifi.const import DOMAIN as UNIFI_DOMAIN +from homeassistant.components.unifi.const import ( + CONF_ALLOW_BANDWIDTH_SENSORS, + CONF_ALLOW_UPTIME_SENSORS, + CONF_TRACK_CLIENTS, + CONF_TRACK_DEVICES, + DOMAIN as UNIFI_DOMAIN, +) from homeassistant.components.unifi.errors import AuthenticationRequired, CannotConnect 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, setup_unifi_integration @@ -103,3 +113,91 @@ async def test_wireless_clients( "00:00:00:00:00:01", "00:00:00:00:00:02", ] + + +async def test_remove_config_entry_device( + hass: HomeAssistant, + hass_storage: dict[str, Any], + aioclient_mock: AiohttpClientMocker, + device_registry: dr.DeviceRegistry, + mock_unifi_websocket, +) -> None: + """Verify removing a device manually.""" + client_1 = { + "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, + } + client_2 = { + "is_wired": False, + "mac": "00:00:00:00:00:02", + "name": "Wireless client", + "oui": "Producer", + "rx_bytes": 2345000000, + "tx_bytes": 6789000000, + "uptime": 60, + } + 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", + } + options = { + CONF_ALLOW_BANDWIDTH_SENSORS: True, + CONF_ALLOW_UPTIME_SENSORS: True, + CONF_TRACK_CLIENTS: True, + CONF_TRACK_DEVICES: True, + } + + config_entry = await setup_unifi_integration( + hass, + aioclient_mock, + options=options, + clients_response=[client_1, client_2], + devices_response=[device_1], + ) + + integration = await loader.async_get_integration(hass, config_entry.domain) + component = await integration.async_get_component() + + # Remove a client + mock_unifi_websocket(message=MessageKey.CLIENT_REMOVED, data=[client_2]) + await hass.async_block_till_done() + + # Try to remove an active client: not allowed + device_entry = device_registry.async_get_device( + connections={(dr.CONNECTION_NETWORK_MAC, client_1["mac"])} + ) + assert not await component.async_remove_config_entry_device( + hass, config_entry, device_entry + ) + # Try to remove an active device: not allowed + device_entry = device_registry.async_get_device( + connections={(dr.CONNECTION_NETWORK_MAC, device_1["mac"])} + ) + assert not await component.async_remove_config_entry_device( + hass, config_entry, device_entry + ) + # Try to remove an inactive client: allowed + device_entry = device_registry.async_get_device( + connections={(dr.CONNECTION_NETWORK_MAC, client_2["mac"])} + ) + assert await component.async_remove_config_entry_device( + hass, config_entry, device_entry + ) From ec066472ae47b5533f77784c3c4c1d4b2bb92ebe Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 21 Apr 2024 11:44:58 +0200 Subject: [PATCH 725/967] Fix geo location attributes of Tankerkoenig sensors (#115914) * geo location attributes needs to be float * make mypy happy --- homeassistant/components/tankerkoenig/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tankerkoenig/sensor.py b/homeassistant/components/tankerkoenig/sensor.py index f2fdc2c45b7..33476e75262 100644 --- a/homeassistant/components/tankerkoenig/sensor.py +++ b/homeassistant/components/tankerkoenig/sensor.py @@ -91,7 +91,7 @@ class FuelPriceSensor(TankerkoenigCoordinatorEntity, SensorEntity): self._fuel_type = fuel_type self._attr_translation_key = fuel_type self._attr_unique_id = f"{station.id}_{fuel_type}" - attrs = { + attrs: dict[str, int | str | float | None] = { ATTR_BRAND: station.brand, ATTR_FUEL_TYPE: fuel_type, ATTR_STATION_NAME: station.name, @@ -102,8 +102,8 @@ class FuelPriceSensor(TankerkoenigCoordinatorEntity, SensorEntity): } if coordinator.show_on_map: - attrs[ATTR_LATITUDE] = str(station.lat) - attrs[ATTR_LONGITUDE] = str(station.lng) + attrs[ATTR_LATITUDE] = station.lat + attrs[ATTR_LONGITUDE] = station.lng self._attr_extra_state_attributes = attrs @property From 95b858648eb30eac0def1873b91de3d00ef1ec4e Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 21 Apr 2024 17:36:19 +0200 Subject: [PATCH 726/967] Refactor Totalconnect binary sensor (#115629) --- .../components/totalconnect/binary_sensor.py | 289 ++++++++++-------- 1 file changed, 159 insertions(+), 130 deletions(-) diff --git a/homeassistant/components/totalconnect/binary_sensor.py b/homeassistant/components/totalconnect/binary_sensor.py index 6043d15d2d4..696f0dbcf6f 100644 --- a/homeassistant/components/totalconnect/binary_sensor.py +++ b/homeassistant/components/totalconnect/binary_sensor.py @@ -1,7 +1,12 @@ """Interfaces with TotalConnect sensors.""" +from collections.abc import Callable +from dataclasses import dataclass import logging +from total_connect_client.location import TotalConnectLocation +from total_connect_client.zone import TotalConnectZone + from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, @@ -12,7 +17,9 @@ from homeassistant.const import EntityCategory 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 . import TotalConnectDataUpdateCoordinator from .const import DOMAIN LOW_BATTERY = "low_battery" @@ -23,172 +30,194 @@ ZONE = "zone" _LOGGER = logging.getLogger(__name__) +@dataclass(frozen=True, kw_only=True) +class TotalConnectZoneBinarySensorEntityDescription(BinarySensorEntityDescription): + """Describes TotalConnect binary sensor entity.""" + + device_class_fn: Callable[[TotalConnectZone], BinarySensorDeviceClass] | None = None + is_on_fn: Callable[[TotalConnectZone], bool] + + +def get_security_zone_device_class(zone: TotalConnectZone) -> BinarySensorDeviceClass: + """Return the device class of a TotalConnect security zone.""" + if zone.is_type_fire(): + return BinarySensorDeviceClass.SMOKE + if zone.is_type_carbon_monoxide(): + return BinarySensorDeviceClass.GAS + if zone.is_type_motion(): + return BinarySensorDeviceClass.MOTION + if zone.is_type_medical(): + return BinarySensorDeviceClass.SAFETY + if zone.is_type_temperature(): + return BinarySensorDeviceClass.PROBLEM + return BinarySensorDeviceClass.DOOR + + +SECURITY_BINARY_SENSOR = TotalConnectZoneBinarySensorEntityDescription( + key=ZONE, + name="", + device_class_fn=get_security_zone_device_class, + is_on_fn=lambda zone: zone.is_faulted() or zone.is_triggered(), +) + +NO_BUTTON_BINARY_SENSORS: tuple[TotalConnectZoneBinarySensorEntityDescription, ...] = ( + TotalConnectZoneBinarySensorEntityDescription( + key=LOW_BATTERY, + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + name=" low battery", + is_on_fn=lambda zone: zone.is_low_battery(), + ), + TotalConnectZoneBinarySensorEntityDescription( + key=TAMPER, + device_class=BinarySensorDeviceClass.TAMPER, + entity_category=EntityCategory.DIAGNOSTIC, + name=f" {TAMPER}", + is_on_fn=lambda zone: zone.is_tampered(), + ), +) + + +@dataclass(frozen=True, kw_only=True) +class TotalConnectAlarmBinarySensorEntityDescription(BinarySensorEntityDescription): + """Describes TotalConnect binary sensor entity.""" + + is_on_fn: Callable[[TotalConnectLocation], bool] + + +LOCATION_BINARY_SENSORS: tuple[TotalConnectAlarmBinarySensorEntityDescription, ...] = ( + TotalConnectAlarmBinarySensorEntityDescription( + key=LOW_BATTERY, + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + name=" low battery", + is_on_fn=lambda location: location.is_low_battery(), + ), + TotalConnectAlarmBinarySensorEntityDescription( + key=TAMPER, + device_class=BinarySensorDeviceClass.TAMPER, + entity_category=EntityCategory.DIAGNOSTIC, + name=f" {TAMPER}", + is_on_fn=lambda location: location.is_cover_tampered(), + ), + TotalConnectAlarmBinarySensorEntityDescription( + key=POWER, + device_class=BinarySensorDeviceClass.POWER, + entity_category=EntityCategory.DIAGNOSTIC, + name=f" {POWER}", + is_on_fn=lambda location: location.is_ac_loss(), + ), +) + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up TotalConnect device sensors based on a config entry.""" sensors: list = [] - client_locations = hass.data[DOMAIN][entry.entry_id].client.locations + coordinator: TotalConnectDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + client_locations = coordinator.client.locations for location_id, location in client_locations.items(): - sensors.append(TotalConnectAlarmLowBatteryBinarySensor(location)) - sensors.append(TotalConnectAlarmTamperBinarySensor(location)) - sensors.append(TotalConnectAlarmPowerBinarySensor(location)) + sensors.extend( + TotalConnectAlarmBinarySensor(coordinator, description, location) + for description in LOCATION_BINARY_SENSORS + ) for zone in location.zones.values(): - sensors.append(TotalConnectZoneSecurityBinarySensor(location_id, zone)) + sensors.append( + TotalConnectZoneBinarySensor( + coordinator, SECURITY_BINARY_SENSOR, location_id, zone + ) + ) if not zone.is_type_button(): - sensors.append(TotalConnectLowBatteryBinarySensor(location_id, zone)) - sensors.append(TotalConnectTamperBinarySensor(location_id, zone)) + sensors.extend( + TotalConnectZoneBinarySensor( + coordinator, + description, + location_id, + zone, + ) + for description in NO_BUTTON_BINARY_SENSORS + ) - async_add_entities(sensors, True) + async_add_entities(sensors) -class TotalConnectZoneBinarySensor(BinarySensorEntity): +class TotalConnectZoneBinarySensor( + CoordinatorEntity[TotalConnectDataUpdateCoordinator], BinarySensorEntity +): """Represent an TotalConnect zone.""" - def __init__(self, location_id, zone): + entity_description: TotalConnectZoneBinarySensorEntityDescription + + def __init__( + self, + coordinator: TotalConnectDataUpdateCoordinator, + entity_description: TotalConnectZoneBinarySensorEntityDescription, + location_id: str, + zone: TotalConnectZone, + ) -> None: """Initialize the TotalConnect status.""" + super().__init__(coordinator) + self.entity_description = entity_description self._location_id = location_id self._zone = zone - self._attr_name = f"{zone.description}{self.entity_description.name}" - self._attr_unique_id = ( - f"{location_id}_{zone.zoneid}_{self.entity_description.key}" - ) + self._attr_name = f"{zone.description}{entity_description.name}" + self._attr_unique_id = f"{location_id}_{zone.zoneid}_{entity_description.key}" self._attr_is_on = None self._attr_extra_state_attributes = { - "zone_id": self._zone.zoneid, + "zone_id": zone.zoneid, "location_id": self._location_id, - "partition": self._zone.partition, + "partition": zone.partition, } - - @property - def device_info(self) -> DeviceInfo: - """Return device info.""" - identifier = self._zone.sensor_serial_number or f"zone_{self._zone.zoneid}" - return DeviceInfo( - name=self._zone.description, + identifier = zone.sensor_serial_number or f"zone_{zone.zoneid}" + self._attr_device_info = DeviceInfo( + name=zone.description, identifiers={(DOMAIN, identifier)}, - serial_number=self._zone.sensor_serial_number, + serial_number=zone.sensor_serial_number, ) - -class TotalConnectZoneSecurityBinarySensor(TotalConnectZoneBinarySensor): - """Represent an TotalConnect security zone.""" - - entity_description: BinarySensorEntityDescription = BinarySensorEntityDescription( - key=ZONE, name="" - ) + @property + def is_on(self) -> bool: + """Return the state of the entity.""" + return self.entity_description.is_on_fn(self._zone) @property - def device_class(self): + def device_class(self) -> BinarySensorDeviceClass | None: """Return the class of this zone.""" - if self._zone.is_type_fire(): - return BinarySensorDeviceClass.SMOKE - if self._zone.is_type_carbon_monoxide(): - return BinarySensorDeviceClass.GAS - if self._zone.is_type_motion(): - return BinarySensorDeviceClass.MOTION - if self._zone.is_type_medical(): - return BinarySensorDeviceClass.SAFETY - if self._zone.is_type_temperature(): - return BinarySensorDeviceClass.PROBLEM - return BinarySensorDeviceClass.DOOR - - def update(self): - """Return the state of the device.""" - if self._zone.is_faulted() or self._zone.is_triggered(): - self._attr_is_on = True - else: - self._attr_is_on = False + if self.entity_description.device_class_fn: + return self.entity_description.device_class_fn(self._zone) + return super().device_class -class TotalConnectLowBatteryBinarySensor(TotalConnectZoneBinarySensor): - """Represent an TotalConnect zone low battery status.""" +class TotalConnectAlarmBinarySensor( + CoordinatorEntity[TotalConnectDataUpdateCoordinator], BinarySensorEntity +): + """Represent a TotalConnect alarm device binary sensors.""" - entity_description: BinarySensorEntityDescription = BinarySensorEntityDescription( - key=LOW_BATTERY, - device_class=BinarySensorDeviceClass.BATTERY, - entity_category=EntityCategory.DIAGNOSTIC, - name=" low battery", - ) + entity_description: TotalConnectAlarmBinarySensorEntityDescription - def update(self): - """Return the state of the device.""" - self._attr_is_on = self._zone.is_low_battery() - - -class TotalConnectTamperBinarySensor(TotalConnectZoneBinarySensor): - """Represent an TotalConnect zone tamper status.""" - - entity_description: BinarySensorEntityDescription = BinarySensorEntityDescription( - key=TAMPER, - device_class=BinarySensorDeviceClass.TAMPER, - entity_category=EntityCategory.DIAGNOSTIC, - name=f" {TAMPER}", - ) - - def update(self): - """Return the state of the device.""" - self._attr_is_on = self._zone.is_tampered() - - -class TotalConnectAlarmBinarySensor(BinarySensorEntity): - """Represent an TotalConnect alarm device binary sensors.""" - - def __init__(self, location): + def __init__( + self, + coordinator: TotalConnectDataUpdateCoordinator, + entity_description: TotalConnectAlarmBinarySensorEntityDescription, + location: TotalConnectLocation, + ) -> None: """Initialize the TotalConnect alarm device binary sensor.""" + super().__init__(coordinator) + self.entity_description = entity_description self._location = location - self._attr_name = f"{location.location_name}{self.entity_description.name}" - self._attr_unique_id = f"{location.location_id}_{self.entity_description.key}" - self._attr_is_on = None + self._attr_name = f"{location.location_name}{entity_description.name}" + self._attr_unique_id = f"{location.location_id}_{entity_description.key}" self._attr_extra_state_attributes = { - "location_id": self._location.location_id, + "location_id": location.location_id, } - -class TotalConnectAlarmLowBatteryBinarySensor(TotalConnectAlarmBinarySensor): - """Represent an TotalConnect Alarm low battery status.""" - - entity_description: BinarySensorEntityDescription = BinarySensorEntityDescription( - key=LOW_BATTERY, - device_class=BinarySensorDeviceClass.BATTERY, - entity_category=EntityCategory.DIAGNOSTIC, - name=" low battery", - ) - - def update(self): - """Return the state of the device.""" - self._attr_is_on = self._location.is_low_battery() - - -class TotalConnectAlarmTamperBinarySensor(TotalConnectAlarmBinarySensor): - """Represent an TotalConnect alarm tamper status.""" - - entity_description: BinarySensorEntityDescription = BinarySensorEntityDescription( - key=TAMPER, - device_class=BinarySensorDeviceClass.TAMPER, - entity_category=EntityCategory.DIAGNOSTIC, - name=f" {TAMPER}", - ) - - def update(self): - """Return the state of the device.""" - self._attr_is_on = self._location.is_cover_tampered() - - -class TotalConnectAlarmPowerBinarySensor(TotalConnectAlarmBinarySensor): - """Represent an TotalConnect alarm power status.""" - - entity_description: BinarySensorEntityDescription = BinarySensorEntityDescription( - key=POWER, - device_class=BinarySensorDeviceClass.POWER, - entity_category=EntityCategory.DIAGNOSTIC, - name=f" {POWER}", - ) - - def update(self): - """Return the state of the device.""" - self._attr_is_on = not self._location.is_ac_loss() + @property + def is_on(self) -> bool: + """Return the state of the entity.""" + return self.entity_description.is_on_fn(self._location) From 83370a5bde1c386c668431d97b851ac415956227 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 21 Apr 2024 20:27:44 +0200 Subject: [PATCH 727/967] Remove sensor exposing UniFi WLAN password (#115929) --- homeassistant/components/unifi/sensor.py | 13 ----- tests/components/unifi/test_sensor.py | 71 ------------------------ 2 files changed, 84 deletions(-) diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index 360f40384c9..7d9720cde1a 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -350,19 +350,6 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = ( value_fn=async_device_state_value_fn, options=list(DEVICE_STATES.values()), ), - UnifiSensorEntityDescription[Wlans, Wlan]( - key="WLAN password", - entity_category=EntityCategory.DIAGNOSTIC, - entity_registry_enabled_default=False, - api_handler_fn=lambda api: api.wlans, - available_fn=async_wlan_available_fn, - device_info_fn=async_wlan_device_info_fn, - name_fn=lambda wlan: "Password", - object_fn=lambda api, obj_id: api.wlans[obj_id], - supported_fn=lambda hub, obj_id: hub.api.wlans[obj_id].x_passphrase is not None, - unique_id_fn=lambda hub, obj_id: f"password-{obj_id}", - value_fn=lambda hub, obj: obj.x_passphrase, - ), UnifiSensorEntityDescription[Devices, Device]( key="Device CPU utilization", entity_category=EntityCategory.DIAGNOSTIC, diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index e8f9f763409..e3b4ddd3b63 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -1000,77 +1000,6 @@ async def test_device_state( assert hass.states.get("sensor.device_state").state == DEVICE_STATES[i] -async def test_wlan_password( - hass: HomeAssistant, - entity_registry: er.EntityRegistry, - aioclient_mock: AiohttpClientMocker, - mock_unifi_websocket, - websocket_mock, -) -> None: - """Test the WLAN password sensor behavior.""" - await setup_unifi_integration(hass, aioclient_mock, wlans_response=[WLAN]) - - sensor_password = "sensor.ssid_1_password" - password = "password" - new_password = "new_password" - - ent_reg_entry = entity_registry.async_get(sensor_password) - assert ent_reg_entry.unique_id == "password-012345678910111213141516" - assert ent_reg_entry.disabled_by == RegistryEntryDisabler.INTEGRATION - assert ent_reg_entry.entity_category is EntityCategory.DIAGNOSTIC - - # Enable entity - entity_registry.async_update_entity(entity_id=sensor_password, 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 - wlan_password_sensor_1 = hass.states.get(sensor_password) - assert wlan_password_sensor_1.state == password - - # Update state object - same password - no change to state - mock_unifi_websocket(message=MessageKey.WLAN_CONF_UPDATED, data=WLAN) - await hass.async_block_till_done() - wlan_password_sensor_2 = hass.states.get(sensor_password) - assert wlan_password_sensor_1.state == wlan_password_sensor_2.state - - # 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) - await hass.async_block_till_done() - wlan_password_sensor_3 = hass.states.get(sensor_password) - assert wlan_password_sensor_1.state != wlan_password_sensor_3.state - - # Availability signaling - - # Controller disconnects - await websocket_mock.disconnect() - assert hass.states.get(sensor_password).state == STATE_UNAVAILABLE - - # Controller reconnects - await websocket_mock.reconnect() - assert hass.states.get(sensor_password).state == new_password - - # WLAN gets disabled - wlan_1 = deepcopy(WLAN) - wlan_1["enabled"] = False - mock_unifi_websocket(message=MessageKey.WLAN_CONF_UPDATED, data=wlan_1) - await hass.async_block_till_done() - assert hass.states.get(sensor_password).state == STATE_UNAVAILABLE - - # WLAN gets re-enabled - wlan_1["enabled"] = True - mock_unifi_websocket(message=MessageKey.WLAN_CONF_UPDATED, data=wlan_1) - await hass.async_block_till_done() - assert hass.states.get(sensor_password).state == password - - async def test_device_system_stats( hass: HomeAssistant, entity_registry: er.EntityRegistry, From ddb415b77e655e6b56366aef8fc0ffa9ff997f7b Mon Sep 17 00:00:00 2001 From: Austin Mroczek Date: Sun, 21 Apr 2024 11:27:50 -0700 Subject: [PATCH 728/967] Bump total_connect_client to 2023.12.1 (#115928) bump total_connect_client to 2023.12.1 --- homeassistant/components/totalconnect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/totalconnect/manifest.json b/homeassistant/components/totalconnect/manifest.json index 183919f05f2..d1afb01210d 100644 --- a/homeassistant/components/totalconnect/manifest.json +++ b/homeassistant/components/totalconnect/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/totalconnect", "iot_class": "cloud_polling", "loggers": ["total_connect_client"], - "requirements": ["total-connect-client==2023.2"] + "requirements": ["total-connect-client==2023.12.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1f066526f58..4a4ef23b583 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2731,7 +2731,7 @@ tololib==1.1.0 toonapi==0.3.0 # homeassistant.components.totalconnect -total-connect-client==2023.2 +total-connect-client==2023.12.1 # homeassistant.components.tplink_lte tp-connected==0.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 91a3c65c3fd..b935fcbaf42 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2108,7 +2108,7 @@ tololib==1.1.0 toonapi==0.3.0 # homeassistant.components.totalconnect -total-connect-client==2023.2 +total-connect-client==2023.12.1 # homeassistant.components.tplink_omada tplink-omada-client==1.3.12 From 2620443a888ab683fd8dd4c89e13e885b8b718c2 Mon Sep 17 00:00:00 2001 From: mkmer Date: Sun, 21 Apr 2024 15:19:48 -0400 Subject: [PATCH 729/967] Add error translations to Blink (#115924) * Add translations Catch timeout in snap * Grammer cleanup --- homeassistant/components/blink/camera.py | 19 +++++++++++++++---- homeassistant/components/blink/strings.json | 21 ++++++++++++++++++--- homeassistant/components/blink/switch.py | 6 ++++-- 3 files changed, 37 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/blink/camera.py b/homeassistant/components/blink/camera.py index 318bb18772a..7461d7b2a2b 100644 --- a/homeassistant/components/blink/camera.py +++ b/homeassistant/components/blink/camera.py @@ -3,7 +3,6 @@ from __future__ import annotations from collections.abc import Mapping -import contextlib import logging from typing import Any @@ -97,7 +96,10 @@ class BlinkCamera(CoordinatorEntity[BlinkUpdateCoordinator], Camera): await self._camera.async_arm(True) except TimeoutError as er: - raise HomeAssistantError("Blink failed to arm camera") from er + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="failed_arm", + ) from er self._camera.motion_enabled = True await self.coordinator.async_refresh() @@ -107,7 +109,10 @@ class BlinkCamera(CoordinatorEntity[BlinkUpdateCoordinator], Camera): try: await self._camera.async_arm(False) except TimeoutError as er: - raise HomeAssistantError("Blink failed to disarm camera") from er + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="failed_disarm", + ) from er self._camera.motion_enabled = False await self.coordinator.async_refresh() @@ -124,8 +129,14 @@ class BlinkCamera(CoordinatorEntity[BlinkUpdateCoordinator], Camera): async def trigger_camera(self) -> None: """Trigger camera to take a snapshot.""" - with contextlib.suppress(TimeoutError): + try: await self._camera.snap_picture() + except TimeoutError as er: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="failed_snap", + ) from er + self.async_write_ha_state() def camera_image( diff --git a/homeassistant/components/blink/strings.json b/homeassistant/components/blink/strings.json index 2260acede1c..2c0be3d972c 100644 --- a/homeassistant/components/blink/strings.json +++ b/homeassistant/components/blink/strings.json @@ -106,16 +106,31 @@ }, "exceptions": { "integration_not_found": { - "message": "Integration \"{target}\" not found in registry" + "message": "Integration \"{target}\" not found in registry." }, "no_path": { "message": "Can't write to directory {target}, no access to path!" }, "cant_write": { - "message": "Can't write to file" + "message": "Can't write to file." }, "not_loaded": { - "message": "{target} is not loaded" + "message": "{target} is not loaded." + }, + "failed_arm": { + "message": "Blink failed to arm camera." + }, + "failed_disarm": { + "message": "Blink failed to disarm camera." + }, + "failed_snap": { + "message": "Blink failed to snap a picture." + }, + "failed_arm_motion": { + "message": "Blink failed to arm camera motion detection." + }, + "failed_disarm_motion": { + "message": "Blink failed to disarm camera motion detection." } }, "issues": { diff --git a/homeassistant/components/blink/switch.py b/homeassistant/components/blink/switch.py index 1bfd257ecbe..ab9b825ded1 100644 --- a/homeassistant/components/blink/switch.py +++ b/homeassistant/components/blink/switch.py @@ -75,7 +75,8 @@ class BlinkSwitch(CoordinatorEntity[BlinkUpdateCoordinator], SwitchEntity): except TimeoutError as er: raise HomeAssistantError( - "Blink failed to arm camera motion detection" + translation_domain=DOMAIN, + translation_key="failed_arm_motion", ) from er await self.coordinator.async_refresh() @@ -87,7 +88,8 @@ class BlinkSwitch(CoordinatorEntity[BlinkUpdateCoordinator], SwitchEntity): except TimeoutError as er: raise HomeAssistantError( - "Blink failed to dis-arm camera motion detection" + translation_domain=DOMAIN, + translation_key="failed_disarm_motion", ) from er await self.coordinator.async_refresh() From 5a24690d795d9c233541e3fc55d0a733a12c90dc Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 21 Apr 2024 22:26:57 +0200 Subject: [PATCH 730/967] Make use of snapshot testing in Synology DSM (#115931) --- .../snapshots/test_config_flow.ambr | 86 +++++++++++++++++ .../synology_dsm/test_config_flow.py | 95 +++++-------------- 2 files changed, 110 insertions(+), 71 deletions(-) create mode 100644 tests/components/synology_dsm/snapshots/test_config_flow.ambr diff --git a/tests/components/synology_dsm/snapshots/test_config_flow.ambr b/tests/components/synology_dsm/snapshots/test_config_flow.ambr new file mode 100644 index 00000000000..807ec764e52 --- /dev/null +++ b/tests/components/synology_dsm/snapshots/test_config_flow.ambr @@ -0,0 +1,86 @@ +# serializer version: 1 +# name: test_discovered_via_zeroconf + dict({ + 'host': '192.168.1.5', + 'mac': list([ + '00-11-32-XX-XX-59', + '00-11-32-XX-XX-5A', + ]), + 'password': 'password', + 'port': 5001, + 'ssl': True, + 'username': 'Home_Assistant', + 'verify_ssl': False, + }) +# --- +# name: test_form_ssdp + dict({ + 'host': '192.168.1.5', + 'mac': list([ + '00-11-32-XX-XX-59', + '00-11-32-XX-XX-5A', + ]), + 'password': 'password', + 'port': 5001, + 'ssl': True, + 'username': 'Home_Assistant', + 'verify_ssl': False, + }) +# --- +# name: test_user + dict({ + 'host': 'nas.meontheinternet.com', + 'mac': list([ + '00-11-32-XX-XX-59', + '00-11-32-XX-XX-5A', + ]), + 'password': 'password', + 'port': 1234, + 'ssl': True, + 'username': 'Home_Assistant', + 'verify_ssl': False, + }) +# --- +# name: test_user.1 + dict({ + 'host': 'nas.meontheinternet.com', + 'mac': list([ + '00-11-32-XX-XX-59', + '00-11-32-XX-XX-5A', + ]), + 'password': 'password', + 'port': 5000, + 'ssl': False, + 'username': 'Home_Assistant', + 'verify_ssl': False, + }) +# --- +# name: test_user_2sa + dict({ + 'device_token': 'Dév!cè_T0k€ñ', + 'host': 'nas.meontheinternet.com', + 'mac': list([ + '00-11-32-XX-XX-59', + '00-11-32-XX-XX-5A', + ]), + 'password': 'password', + 'port': 5001, + 'ssl': True, + 'username': 'Home_Assistant', + 'verify_ssl': False, + }) +# --- +# name: test_user_vdsm + dict({ + 'host': 'nas.meontheinternet.com', + 'mac': list([ + '00-11-32-XX-XX-59', + '00-11-32-XX-XX-5A', + ]), + 'password': 'password', + 'port': 1234, + 'ssl': True, + 'username': 'Home_Assistant', + 'verify_ssl': False, + }) +# --- diff --git a/tests/components/synology_dsm/test_config_flow.py b/tests/components/synology_dsm/test_config_flow.py index 483e22f2359..85814f84aad 100644 --- a/tests/components/synology_dsm/test_config_flow.py +++ b/tests/components/synology_dsm/test_config_flow.py @@ -11,19 +11,15 @@ from synology_dsm.exceptions import ( SynologyDSMLoginInvalidException, SynologyDSMRequestException, ) +from syrupy import SnapshotAssertion from homeassistant.components import ssdp, zeroconf from homeassistant.components.synology_dsm.config_flow import CONF_OTP_CODE from homeassistant.components.synology_dsm.const import ( CONF_SNAPSHOT_QUALITY, - CONF_VOLUMES, - DEFAULT_PORT, - DEFAULT_PORT_SSL, DEFAULT_SCAN_INTERVAL, DEFAULT_SNAPSHOT_QUALITY, DEFAULT_TIMEOUT, - DEFAULT_USE_SSL, - DEFAULT_VERIFY_SSL, DOMAIN, ) from homeassistant.config_entries import ( @@ -33,7 +29,6 @@ from homeassistant.config_entries import ( SOURCE_ZEROCONF, ) from homeassistant.const import ( - CONF_DISKS, CONF_HOST, CONF_MAC, CONF_PASSWORD, @@ -149,7 +144,11 @@ def mock_controller_service_failed(): @pytest.mark.usefixtures("mock_setup_entry") -async def test_user(hass: HomeAssistant, service: MagicMock) -> None: +async def test_user( + hass: HomeAssistant, + service: MagicMock, + snapshot: SnapshotAssertion, +) -> None: """Test user config.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=None @@ -177,16 +176,7 @@ async def test_user(hass: HomeAssistant, service: MagicMock) -> None: assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == HOST - assert result["data"][CONF_HOST] == HOST - assert result["data"][CONF_PORT] == PORT - assert result["data"][CONF_SSL] == USE_SSL - assert result["data"][CONF_VERIFY_SSL] == VERIFY_SSL - assert result["data"][CONF_USERNAME] == USERNAME - assert result["data"][CONF_PASSWORD] == PASSWORD - assert result["data"][CONF_MAC] == MACS - assert result["data"].get("device_token") is None - assert result["data"].get(CONF_DISKS) is None - assert result["data"].get(CONF_VOLUMES) is None + assert result["data"] == snapshot service.information.serial = SERIAL_2 with patch( @@ -208,20 +198,13 @@ async def test_user(hass: HomeAssistant, service: MagicMock) -> None: assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL_2 assert result["title"] == HOST - assert result["data"][CONF_HOST] == HOST - assert result["data"][CONF_PORT] == DEFAULT_PORT - assert not result["data"][CONF_SSL] - assert result["data"][CONF_VERIFY_SSL] == VERIFY_SSL - assert result["data"][CONF_USERNAME] == USERNAME - assert result["data"][CONF_PASSWORD] == PASSWORD - assert result["data"][CONF_MAC] == MACS - assert result["data"].get("device_token") is None - assert result["data"].get(CONF_DISKS) is None - assert result["data"].get(CONF_VOLUMES) is None + assert result["data"] == snapshot @pytest.mark.usefixtures("mock_setup_entry") -async def test_user_2sa(hass: HomeAssistant, service_2sa: MagicMock) -> None: +async def test_user_2sa( + hass: HomeAssistant, service_2sa: MagicMock, snapshot: SnapshotAssertion +) -> None: """Test user with 2sa authentication config.""" with patch( "homeassistant.components.synology_dsm.config_flow.SynologyDSM", @@ -261,20 +244,13 @@ async def test_user_2sa(hass: HomeAssistant, service_2sa: MagicMock) -> None: assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == HOST - assert result["data"][CONF_HOST] == HOST - assert result["data"][CONF_PORT] == DEFAULT_PORT_SSL - assert result["data"][CONF_SSL] == DEFAULT_USE_SSL - assert result["data"][CONF_VERIFY_SSL] == DEFAULT_VERIFY_SSL - assert result["data"][CONF_USERNAME] == USERNAME - assert result["data"][CONF_PASSWORD] == PASSWORD - assert result["data"][CONF_MAC] == MACS - assert result["data"].get("device_token") == DEVICE_TOKEN - assert result["data"].get(CONF_DISKS) is None - assert result["data"].get(CONF_VOLUMES) is None + assert result["data"] == snapshot @pytest.mark.usefixtures("mock_setup_entry") -async def test_user_vdsm(hass: HomeAssistant, service_vdsm: MagicMock) -> None: +async def test_user_vdsm( + hass: HomeAssistant, service_vdsm: MagicMock, snapshot: SnapshotAssertion +) -> None: """Test user config.""" with patch( "homeassistant.components.synology_dsm.config_flow.SynologyDSM", @@ -306,16 +282,7 @@ async def test_user_vdsm(hass: HomeAssistant, service_vdsm: MagicMock) -> None: assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == HOST - assert result["data"][CONF_HOST] == HOST - assert result["data"][CONF_PORT] == PORT - assert result["data"][CONF_SSL] == USE_SSL - assert result["data"][CONF_VERIFY_SSL] == VERIFY_SSL - assert result["data"][CONF_USERNAME] == USERNAME - assert result["data"][CONF_PASSWORD] == PASSWORD - assert result["data"][CONF_MAC] == MACS - assert result["data"].get("device_token") is None - assert result["data"].get(CONF_DISKS) is None - assert result["data"].get(CONF_VOLUMES) is None + assert result["data"] == snapshot @pytest.mark.usefixtures("mock_setup_entry") @@ -467,7 +434,9 @@ async def test_missing_data_after_login( @pytest.mark.usefixtures("mock_setup_entry") -async def test_form_ssdp(hass: HomeAssistant, service: MagicMock) -> None: +async def test_form_ssdp( + hass: HomeAssistant, service: MagicMock, snapshot: SnapshotAssertion +) -> None: """Test we can setup from ssdp.""" result = await hass.config_entries.flow.async_init( @@ -498,16 +467,7 @@ async def test_form_ssdp(hass: HomeAssistant, service: MagicMock) -> None: assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == "mydsm" - assert result["data"][CONF_HOST] == "192.168.1.5" - assert result["data"][CONF_PORT] == 5001 - assert result["data"][CONF_SSL] == DEFAULT_USE_SSL - assert result["data"][CONF_VERIFY_SSL] == DEFAULT_VERIFY_SSL - assert result["data"][CONF_USERNAME] == USERNAME - assert result["data"][CONF_PASSWORD] == PASSWORD - assert result["data"][CONF_MAC] == MACS - assert result["data"].get("device_token") is None - assert result["data"].get(CONF_DISKS) is None - assert result["data"].get(CONF_VOLUMES) is None + assert result["data"] == snapshot @pytest.mark.usefixtures("mock_setup_entry") @@ -664,7 +624,9 @@ async def test_options_flow(hass: HomeAssistant, service: MagicMock) -> None: @pytest.mark.usefixtures("mock_setup_entry") -async def test_discovered_via_zeroconf(hass: HomeAssistant, service: MagicMock) -> None: +async def test_discovered_via_zeroconf( + hass: HomeAssistant, service: MagicMock, snapshot: SnapshotAssertion +) -> None: """Test we can setup from zeroconf.""" result = await hass.config_entries.flow.async_init( @@ -697,16 +659,7 @@ async def test_discovered_via_zeroconf(hass: HomeAssistant, service: MagicMock) assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == "mydsm" - assert result["data"][CONF_HOST] == "192.168.1.5" - assert result["data"][CONF_PORT] == 5001 - assert result["data"][CONF_SSL] == DEFAULT_USE_SSL - assert result["data"][CONF_VERIFY_SSL] == DEFAULT_VERIFY_SSL - assert result["data"][CONF_USERNAME] == USERNAME - assert result["data"][CONF_PASSWORD] == PASSWORD - assert result["data"][CONF_MAC] == MACS - assert result["data"].get("device_token") is None - assert result["data"].get(CONF_DISKS) is None - assert result["data"].get(CONF_VOLUMES) is None + assert result["data"] == snapshot @pytest.mark.usefixtures("mock_setup_entry") From 423544401ea191482c56fd03b7954c22c7c8ea6c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 21 Apr 2024 22:33:58 +0200 Subject: [PATCH 731/967] Convert MQTT to use asyncio (#115910) --- homeassistant/components/mqtt/__init__.py | 14 +- homeassistant/components/mqtt/client.py | 292 +++++++++++++++++----- tests/common.py | 2 +- tests/components/mqtt/test_init.py | 206 ++++++++++++++- tests/components/tasmota/test_common.py | 12 +- tests/conftest.py | 28 ++- 6 files changed, 464 insertions(+), 90 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 28cb7d0944b..cc1ae3ddce1 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -265,7 +265,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: conf: dict[str, Any] mqtt_data: MqttData - async def _setup_client() -> tuple[MqttData, dict[str, Any]]: + async def _setup_client( + client_available: asyncio.Future[bool], + ) -> tuple[MqttData, dict[str, Any]]: """Set up the MQTT client.""" # Fetch configuration conf = dict(entry.data) @@ -294,7 +296,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.add_update_listener(_async_config_entry_updated) ) - await mqtt_data.client.async_connect() + await mqtt_data.client.async_connect(client_available) return (mqtt_data, conf) client_available: asyncio.Future[bool] @@ -303,13 +305,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: else: client_available = hass.data[DATA_MQTT_AVAILABLE] - setup_ok: bool = False - try: - mqtt_data, conf = await _setup_client() - setup_ok = True - finally: - if not client_available.done(): - client_available.set_result(setup_ok) + mqtt_data, conf = await _setup_client(client_available) async def async_publish_service(call: ServiceCall) -> None: """Handle MQTT publish service calls.""" diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 978123e169c..021ecf1cc36 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -3,12 +3,14 @@ from __future__ import annotations import asyncio -from collections.abc import Callable, Coroutine, Iterable +from collections.abc import AsyncGenerator, Callable, Coroutine, Iterable +import contextlib from dataclasses import dataclass -from functools import lru_cache +from functools import lru_cache, partial from itertools import chain, groupby import logging from operator import attrgetter +import socket import ssl import time from typing import TYPE_CHECKING, Any @@ -35,7 +37,7 @@ from homeassistant.core import ( callback, ) from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from homeassistant.util import dt as dt_util @@ -92,6 +94,9 @@ INITIAL_SUBSCRIBE_COOLDOWN = 1.0 SUBSCRIBE_COOLDOWN = 0.1 UNSUBSCRIBE_COOLDOWN = 0.1 TIMEOUT_ACK = 10 +RECONNECT_INTERVAL_SECONDS = 10 + +SocketType = socket.socket | ssl.SSLSocket | Any SubscribePayloadType = str | bytes # Only bytes if encoding is None @@ -258,7 +263,9 @@ 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(client_id, protocol=proto, transport=transport) + self._client = mqtt.Client( + client_id, protocol=proto, transport=transport, reconnect_on_failure=False + ) # Enable logging self._client.enable_logger() @@ -404,12 +411,17 @@ class MQTT: self._ha_started = asyncio.Event() self._cleanup_on_unload: list[Callable[[], None]] = [] - self._paho_lock = asyncio.Lock() # Prevents parallel calls to the MQTT client + self._connection_lock = asyncio.Lock() self._pending_operations: dict[int, asyncio.Event] = {} self._pending_operations_condition = asyncio.Condition() self._subscribe_debouncer = EnsureJobAfterCooldown( INITIAL_SUBSCRIBE_COOLDOWN, self._async_perform_subscriptions ) + self._misc_task: asyncio.Task | None = None + self._reconnect_task: asyncio.Task | None = None + self._should_reconnect: bool = True + self._available_future: asyncio.Future[bool] | None = None + self._max_qos: dict[str, int] = {} # topic, max qos self._pending_subscriptions: dict[str, int] = {} # topic, qos self._unsubscribe_debouncer = EnsureJobAfterCooldown( @@ -456,25 +468,140 @@ class MQTT: while self._cleanup_on_unload: self._cleanup_on_unload.pop()() + @contextlib.asynccontextmanager + async def _async_connect_in_executor(self) -> AsyncGenerator[None, 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. + mqttc = self._mqttc + try: + mqttc.on_socket_open = self._on_socket_open + mqttc.on_socket_register_write = self._on_socket_register_write + yield + finally: + # Once the executor job is done, we can switch back to + # handling these in the event loop. + 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: """Initialize paho client.""" - self._mqttc = MqttClientSetup(self.conf).client - self._mqttc.on_connect = self._mqtt_on_connect - self._mqttc.on_disconnect = self._mqtt_on_disconnect - self._mqttc.on_message = self._mqtt_on_message - self._mqttc.on_publish = self._mqtt_on_callback - self._mqttc.on_subscribe = self._mqtt_on_callback - self._mqttc.on_unsubscribe = self._mqtt_on_callback + mqttc = MqttClientSetup(self.conf).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 + mqttc.on_socket_unregister_write = self._async_on_socket_unregister_write + + # These will be called in the event loop + mqttc.on_connect = self._async_mqtt_on_connect + mqttc.on_disconnect = self._async_mqtt_on_disconnect + mqttc.on_message = self._async_mqtt_on_message + mqttc.on_publish = self._async_mqtt_on_callback + mqttc.on_subscribe = self._async_mqtt_on_callback + mqttc.on_unsubscribe = self._async_mqtt_on_callback if will := self.conf.get(CONF_WILL_MESSAGE, DEFAULT_WILL): will_message = PublishMessage(**will) - self._mqttc.will_set( + mqttc.will_set( topic=will_message.topic, payload=will_message.payload, qos=will_message.qos, retain=will_message.retain, ) + 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.""" + if (status := client.loop_read()) != 0: + 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 _on_socket_open( + self, client: mqtt.Client, userdata: Any, sock: SocketType + ) -> None: + """Handle socket open.""" + self.loop.call_soon_threadsafe( + self._async_on_socket_open, client, userdata, sock + ) + + @callback + def _async_on_socket_open( + self, client: mqtt.Client, userdata: Any, sock: SocketType + ) -> None: + """Handle socket open.""" + fileno = sock.fileno() + _LOGGER.debug("%s: connection opened %s", self.config_entry.title, fileno) + if fileno > -1: + self.loop.add_reader(sock, partial(self._async_reader_callback, client)) + self._async_start_misc_loop() + + @callback + def _async_on_socket_close( + self, client: mqtt.Client, userdata: Any, sock: SocketType + ) -> None: + """Handle socket close.""" + fileno = sock.fileno() + _LOGGER.debug("%s: connection closed %s", self.config_entry.title, fileno) + # If socket close is called before the connect + # result is set make sure the first connection result is set + 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() + + @callback + def _async_writer_callback(self, client: mqtt.Client) -> None: + """Handle writing data to the socket.""" + if (status := client.loop_write()) != 0: + self._async_on_disconnect(status) + + def _on_socket_register_write( + self, client: mqtt.Client, userdata: Any, sock: SocketType + ) -> None: + """Register the socket for writing.""" + self.loop.call_soon_threadsafe( + self._async_on_socket_register_write, client, None, sock + ) + + @callback + def _async_on_socket_register_write( + self, client: mqtt.Client, userdata: Any, sock: SocketType + ) -> None: + """Register the socket for writing.""" + fileno = sock.fileno() + _LOGGER.debug("%s: register write %s", self.config_entry.title, fileno) + if fileno > -1: + self.loop.add_writer(sock, partial(self._async_writer_callback, client)) + + @callback + def _async_on_socket_unregister_write( + self, client: mqtt.Client, userdata: Any, sock: SocketType + ) -> None: + """Unregister the socket for writing.""" + fileno = sock.fileno() + _LOGGER.debug("%s: unregister write %s", self.config_entry.title, fileno) + if fileno > -1: + self.loop.remove_writer(sock) + def _is_active_subscription(self, topic: str) -> bool: """Check if a topic has an active subscription.""" return topic in self._simple_subscriptions or any( @@ -485,10 +612,7 @@ class MQTT: self, topic: str, payload: PublishPayloadType, qos: int, retain: bool ) -> None: """Publish a MQTT message.""" - async with self._paho_lock: - msg_info = await self.hass.async_add_executor_job( - self._mqttc.publish, topic, payload, qos, retain - ) + msg_info = self._mqttc.publish(topic, payload, qos, retain) _LOGGER.debug( "Transmitting%s message on %s: '%s', mid: %s, qos: %s", " retained" if retain else "", @@ -500,37 +624,71 @@ class MQTT: _raise_on_error(msg_info.rc) await self._wait_for_mid(msg_info.mid) - async def async_connect(self) -> None: + async def async_connect(self, client_available: asyncio.Future[bool]) -> None: """Connect to the host. Does not process messages yet.""" # pylint: disable-next=import-outside-toplevel import paho.mqtt.client as mqtt result: int | None = None + self._available_future = client_available + self._should_reconnect = True try: - result = await self.hass.async_add_executor_job( - self._mqttc.connect, - self.conf[CONF_BROKER], - self.conf.get(CONF_PORT, DEFAULT_PORT), - self.conf.get(CONF_KEEPALIVE, DEFAULT_KEEPALIVE), - ) + async with self._connection_lock, self._async_connect_in_executor(): + result = await self.hass.async_add_executor_job( + self._mqttc.connect, + self.conf[CONF_BROKER], + self.conf.get(CONF_PORT, DEFAULT_PORT), + self.conf.get(CONF_KEEPALIVE, DEFAULT_KEEPALIVE), + ) except OSError as err: _LOGGER.error("Failed to connect to MQTT server due to exception: %s", err) + self._async_connection_result(False) + finally: + if result is not None and result != 0: + if result is not None: + _LOGGER.error( + "Failed to connect to MQTT server: %s", + mqtt.error_string(result), + ) + self._async_connection_result(False) - if result is not None and result != 0: - _LOGGER.error( - "Failed to connect to MQTT server: %s", mqtt.error_string(result) + @callback + def _async_connection_result(self, connected: bool) -> None: + """Handle a connection result.""" + if self._available_future and not self._available_future.done(): + self._available_future.set_result(connected) + + if connected: + self._async_cancel_reconnect() + elif self._should_reconnect and not self._reconnect_task: + self._reconnect_task = self.config_entry.async_create_background_task( + self.hass, self._reconnect_loop(), "mqtt reconnect loop" ) - self._mqttc.loop_start() + @callback + def _async_cancel_reconnect(self) -> None: + """Cancel the reconnect task.""" + if self._reconnect_task: + self._reconnect_task.cancel() + self._reconnect_task = None + + async def _reconnect_loop(self) -> None: + """Reconnect to the MQTT server.""" + while True: + if not self.connected: + try: + async with self._connection_lock, self._async_connect_in_executor(): + await self.hass.async_add_executor_job(self._mqttc.reconnect) + except OSError as err: + _LOGGER.debug( + "Error re-connecting to MQTT server due to exception: %s", err + ) + + await asyncio.sleep(RECONNECT_INTERVAL_SECONDS) async def async_disconnect(self) -> None: """Stop the MQTT client.""" - def stop() -> None: - """Stop the MQTT client.""" - # Do not disconnect, we want the broker to always publish will - self._mqttc.loop_stop() - def no_more_acks() -> bool: """Return False if there are unprocessed ACKs.""" return not any(not op.is_set() for op in self._pending_operations.values()) @@ -549,8 +707,10 @@ class MQTT: await self._pending_operations_condition.wait_for(no_more_acks) # stop the MQTT loop - async with self._paho_lock: - await self.hass.async_add_executor_job(stop) + async with self._connection_lock: + self._should_reconnect = False + self._async_cancel_reconnect() + self._mqttc.disconnect() @callback def async_restore_tracked_subscriptions( @@ -689,11 +849,8 @@ class MQTT: subscriptions: dict[str, int] = self._pending_subscriptions self._pending_subscriptions = {} - async with self._paho_lock: - subscription_list = list(subscriptions.items()) - result, mid = await self.hass.async_add_executor_job( - self._mqttc.subscribe, subscription_list - ) + subscription_list = list(subscriptions.items()) + result, mid = self._mqttc.subscribe(subscription_list) for topic, qos in subscriptions.items(): _LOGGER.debug("Subscribing to %s, mid: %s, qos: %s", topic, mid, qos) @@ -712,17 +869,15 @@ class MQTT: topics = list(self._pending_unsubscribes) self._pending_unsubscribes = set() - async with self._paho_lock: - result, mid = await self.hass.async_add_executor_job( - self._mqttc.unsubscribe, topics - ) + result, mid = self._mqttc.unsubscribe(topics) _raise_on_error(result) for topic in topics: _LOGGER.debug("Unsubscribing from %s, mid: %s", topic, mid) await self._wait_for_mid(mid) - def _mqtt_on_connect( + @callback + def _async_mqtt_on_connect( self, _mqttc: mqtt.Client, _userdata: None, @@ -746,7 +901,7 @@ class MQTT: return self.connected = True - dispatcher_send(self.hass, MQTT_CONNECTED) + async_dispatcher_send(self.hass, MQTT_CONNECTED) _LOGGER.info( "Connected to MQTT server %s:%s (%s)", self.conf[CONF_BROKER], @@ -754,7 +909,7 @@ class MQTT: result_code, ) - self.hass.create_task(self._async_resubscribe()) + self.hass.async_create_task(self._async_resubscribe()) if birth := self.conf.get(CONF_BIRTH_MESSAGE, DEFAULT_BIRTH): @@ -771,13 +926,17 @@ class MQTT: ) birth_message = PublishMessage(**birth) - asyncio.run_coroutine_threadsafe( - publish_birth_message(birth_message), self.hass.loop + self.config_entry.async_create_background_task( + self.hass, + publish_birth_message(birth_message), + name="mqtt birth message", ) else: # Update subscribe cooldown period to a shorter time self._subscribe_debouncer.set_timeout(SUBSCRIBE_COOLDOWN) + self._async_connection_result(True) + async def _async_resubscribe(self) -> None: """Resubscribe on reconnect.""" self._max_qos.clear() @@ -796,16 +955,6 @@ class MQTT: ) await self._async_perform_subscriptions() - def _mqtt_on_message( - self, _mqttc: mqtt.Client, _userdata: None, msg: mqtt.MQTTMessage - ) -> None: - """Message received callback.""" - # MQTT messages tend to be high volume, - # and since they come in via a thread and need to be processed in the event loop, - # we want to avoid hass.add_job since most of the time is spent calling - # inspect to figure out how to run the callback. - self.loop.call_soon_threadsafe(self._mqtt_handle_message, msg) - @lru_cache(None) # pylint: disable=method-cache-max-size-none def _matching_subscriptions(self, topic: str) -> list[Subscription]: subscriptions: list[Subscription] = [] @@ -819,7 +968,9 @@ class MQTT: return subscriptions @callback - def _mqtt_handle_message(self, msg: mqtt.MQTTMessage) -> None: + def _async_mqtt_on_message( + self, _mqttc: mqtt.Client, _userdata: None, msg: mqtt.MQTTMessage + ) -> None: topic = msg.topic # msg.topic is a property that decodes the topic to a string # every time it is accessed. Save the result to avoid @@ -878,7 +1029,8 @@ class MQTT: self.hass.async_run_hass_job(subscription.job, receive_msg) self._mqtt_data.state_write_requests.process_write_state_requests(msg) - def _mqtt_on_callback( + @callback + def _async_mqtt_on_callback( self, _mqttc: mqtt.Client, _userdata: None, @@ -890,7 +1042,7 @@ class MQTT: # The callback signature for on_unsubscribe is different from on_subscribe # see https://github.com/eclipse/paho.mqtt.python/issues/687 # properties and reasoncodes are not used in Home Assistant - self.hass.create_task(self._mqtt_handle_mid(mid)) + self.hass.async_create_task(self._mqtt_handle_mid(mid)) async def _mqtt_handle_mid(self, mid: int) -> None: # Create the mid event if not created, either _mqtt_handle_mid or _wait_for_mid @@ -906,7 +1058,8 @@ class MQTT: if mid not in self._pending_operations: self._pending_operations[mid] = asyncio.Event() - def _mqtt_on_disconnect( + @callback + def _async_mqtt_on_disconnect( self, _mqttc: mqtt.Client, _userdata: None, @@ -914,8 +1067,19 @@ class MQTT: properties: mqtt.Properties | None = None, ) -> None: """Disconnected callback.""" + self._async_on_disconnect(result_code) + + @callback + def _async_on_disconnect(self, result_code: int) -> None: + if not self.connected: + # This function is re-entrant and may be called multiple times + # when there is a broken pipe error. + return + # If disconnect is called before the connect + # result is set make sure the first connection result is set + self._async_connection_result(False) self.connected = False - dispatcher_send(self.hass, MQTT_DISCONNECTED) + async_dispatcher_send(self.hass, MQTT_DISCONNECTED) _LOGGER.warning( "Disconnected from MQTT server %s:%s (%s)", self.conf[CONF_BROKER], diff --git a/tests/common.py b/tests/common.py index d53db1beb37..b5fe0f7bae1 100644 --- a/tests/common.py +++ b/tests/common.py @@ -452,7 +452,7 @@ def async_fire_mqtt_message( mqtt_data: MqttData = hass.data["mqtt"] assert mqtt_data.client - mqtt_data.client._mqtt_handle_message(msg) + mqtt_data.client._async_mqtt_on_message(Mock(), None, msg) fire_mqtt_message = threadsafe_callback_factory(async_fire_mqtt_message) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 3e444e8d4c8..37f7e0cf587 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -4,17 +4,22 @@ import asyncio from copy import deepcopy from datetime import datetime, timedelta import json +import socket import ssl from typing import Any, TypedDict from unittest.mock import ANY, MagicMock, call, mock_open, patch 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 EnsureJobAfterCooldown +from homeassistant.components.mqtt.client import ( + RECONNECT_INTERVAL_SECONDS, + EnsureJobAfterCooldown, +) from homeassistant.components.mqtt.mixins import MQTT_ENTITY_DEVICE_INFO_SCHEMA from homeassistant.components.mqtt.models import ( MessageCallbackType, @@ -146,7 +151,7 @@ async def test_mqtt_disconnects_on_home_assistant_stop( hass.bus.fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() await hass.async_block_till_done() - assert mqtt_client_mock.loop_stop.call_count == 1 + assert mqtt_client_mock.disconnect.call_count == 1 async def test_mqtt_await_ack_at_disconnect( @@ -161,8 +166,14 @@ async def test_mqtt_await_ack_at_disconnect( rc = 0 with patch("paho.mqtt.client.Client") as mock_client: - mock_client().connect = MagicMock(return_value=0) - mock_client().publish = MagicMock(return_value=FakeInfo()) + 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"}, @@ -1669,6 +1680,7 @@ async def test_not_calling_subscribe_when_unsubscribed_within_cooldown( the subscribe cool down period has ended. """ mqtt_mock = await mqtt_mock_entry() + mqtt_client_mock.subscribe.reset_mock() # Fake that the client is connected mqtt_mock().connected = True @@ -1925,6 +1937,7 @@ async def test_canceling_debouncer_on_shutdown( """Test canceling the debouncer when HA shuts down.""" mqtt_mock = await mqtt_mock_entry() + mqtt_client_mock.subscribe.reset_mock() # Fake that the client is connected mqtt_mock().connected = True @@ -2008,7 +2021,7 @@ async def test_initial_setup_logs_error( """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.return_value = 1 + mqtt_client_mock.connect.side_effect = MagicMock(return_value=1) try: assert await hass.config_entries.async_setup(entry.entry_id) except HomeAssistantError: @@ -2230,7 +2243,12 @@ async def test_handle_mqtt_timeout_on_callback( mock_client = mock_client.return_value mock_client.publish.return_value = FakeInfo() mock_client.subscribe.side_effect = _mock_ack - mock_client.connect.return_value = 0 + 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"} @@ -4144,3 +4162,179 @@ async def test_multi_platform_discovery( ) is not None ) + + +@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, + 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.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 + + +@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, + 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.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 + + unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) + + 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() + unsub() + + # Should have failed + assert len(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_client_sock_failure_after_connect( + hass: HomeAssistant, + mqtt_client_mock: MqttMockPahoClient, + mqtt_mock_entry: MqttMockHAClientGenerator, + calls: list[ReceiveMessage], + record_calls: MessageCallbackType, +) -> 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 + + 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_mock.connected is True + 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(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, + 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.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 mock-broker:1883 (7)" in caplog.text diff --git a/tests/components/tasmota/test_common.py b/tests/components/tasmota/test_common.py index 360794e280f..499e732719c 100644 --- a/tests/components/tasmota/test_common.py +++ b/tests/components/tasmota/test_common.py @@ -163,7 +163,7 @@ async def help_test_availability_when_connection_lost( # Disconnected from MQTT server -> state changed to unavailable mqtt_mock.connected = False - await hass.async_add_executor_job(mqtt_client_mock.on_disconnect, None, None, 0) + mqtt_client_mock.on_disconnect(None, None, 0) await hass.async_block_till_done() await hass.async_block_till_done() await hass.async_block_till_done() @@ -172,7 +172,7 @@ async def help_test_availability_when_connection_lost( # Reconnected to MQTT server -> state still unavailable mqtt_mock.connected = True - await hass.async_add_executor_job(mqtt_client_mock.on_connect, None, None, None, 0) + mqtt_client_mock.on_connect(None, None, None, 0) await hass.async_block_till_done() await hass.async_block_till_done() await hass.async_block_till_done() @@ -224,7 +224,7 @@ async def help_test_deep_sleep_availability_when_connection_lost( # Disconnected from MQTT server -> state changed to unavailable mqtt_mock.connected = False - await hass.async_add_executor_job(mqtt_client_mock.on_disconnect, None, None, 0) + mqtt_client_mock.on_disconnect(None, None, 0) await hass.async_block_till_done() await hass.async_block_till_done() await hass.async_block_till_done() @@ -233,7 +233,7 @@ async def help_test_deep_sleep_availability_when_connection_lost( # Reconnected to MQTT server -> state no longer unavailable mqtt_mock.connected = True - await hass.async_add_executor_job(mqtt_client_mock.on_connect, None, None, None, 0) + mqtt_client_mock.on_connect(None, None, None, 0) await hass.async_block_till_done() await hass.async_block_till_done() await hass.async_block_till_done() @@ -476,7 +476,7 @@ async def help_test_availability_poll_state( # Disconnected from MQTT server mqtt_mock.connected = False - await hass.async_add_executor_job(mqtt_client_mock.on_disconnect, None, None, 0) + mqtt_client_mock.on_disconnect(None, None, 0) await hass.async_block_till_done() await hass.async_block_till_done() await hass.async_block_till_done() @@ -484,7 +484,7 @@ async def help_test_availability_poll_state( # Reconnected to MQTT server mqtt_mock.connected = True - await hass.async_add_executor_job(mqtt_client_mock.on_connect, None, None, None, 0) + mqtt_client_mock.on_connect(None, None, None, 0) await hass.async_block_till_done() await hass.async_block_till_done() await hass.async_block_till_done() diff --git a/tests/conftest.py b/tests/conftest.py index a38da17f44b..3a95e0e58b3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -904,26 +904,45 @@ def mqtt_client_mock(hass: HomeAssistant) -> Generator[MqttMockPahoClient, None, self.rc = 0 with patch("paho.mqtt.client.Client") 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. @ha.callback def _async_fire_mqtt_message(topic, payload, qos, retain): async_fire_mqtt_message(hass, topic, payload, qos, retain) mid = get_mid() - mock_client.on_publish(0, 0, mid) + hass.loop.call_soon(mock_client.on_publish, 0, 0, mid) return FakeInfo(mid) def _subscribe(topic, qos=0): mid = get_mid() - mock_client.on_subscribe(0, 0, mid) + hass.loop.call_soon(mock_client.on_subscribe, 0, 0, mid) return (0, mid) def _unsubscribe(topic): mid = get_mid() - mock_client.on_unsubscribe(0, 0, mid) + hass.loop.call_soon(mock_client.on_unsubscribe, 0, 0, mid) return (0, mid) + def _connect(*args, **kwargs): + # Connect always calls reconnect once, but we + # mock it out so we call reconnect to simulate + # the behavior. + mock_client.reconnect() + hass.loop.call_soon_threadsafe( + mock_client.on_connect, mock_client, None, 0, 0, 0 + ) + mock_client.on_socket_open( + mock_client, None, Mock(fileno=Mock(return_value=-1)) + ) + mock_client.on_socket_register_write( + mock_client, None, Mock(fileno=Mock(return_value=-1)) + ) + return 0 + mock_client = mock_client.return_value - mock_client.connect.return_value = 0 + mock_client.connect.side_effect = _connect mock_client.subscribe.side_effect = _subscribe mock_client.unsubscribe.side_effect = _unsubscribe mock_client.publish.side_effect = _async_fire_mqtt_message @@ -985,6 +1004,7 @@ async def _mqtt_mock_entry( # connected set to True to get a more realistic behavior when subscribing mock_mqtt_instance.connected = True + mqtt_client_mock.on_connect(mqtt_client_mock, None, 0, 0, 0) async_dispatcher_send(hass, mqtt.MQTT_CONNECTED) await hass.async_block_till_done() From 8754b12d08bfafbd5075d979868078ab48aaf886 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 21 Apr 2024 22:57:57 +0200 Subject: [PATCH 732/967] Temporarily pickup mqtt codeowner (#115934) --- CODEOWNERS | 4 ++-- homeassistant/components/mqtt/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 0a833a94e4e..ef997cfa896 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -875,8 +875,8 @@ build.json @home-assistant/supervisor /tests/components/motioneye/ @dermotduffy /homeassistant/components/motionmount/ @RJPoelstra /tests/components/motionmount/ @RJPoelstra -/homeassistant/components/mqtt/ @emontnemery @jbouwh -/tests/components/mqtt/ @emontnemery @jbouwh +/homeassistant/components/mqtt/ @emontnemery @jbouwh @bdraco +/tests/components/mqtt/ @emontnemery @jbouwh @bdraco /homeassistant/components/msteams/ @peroyvind /homeassistant/components/mullvad/ @meichthys /tests/components/mullvad/ @meichthys diff --git a/homeassistant/components/mqtt/manifest.json b/homeassistant/components/mqtt/manifest.json index 3a284c6719c..5f923868270 100644 --- a/homeassistant/components/mqtt/manifest.json +++ b/homeassistant/components/mqtt/manifest.json @@ -1,7 +1,7 @@ { "domain": "mqtt", "name": "MQTT", - "codeowners": ["@emontnemery", "@jbouwh"], + "codeowners": ["@emontnemery", "@jbouwh", "@bdraco"], "config_flow": true, "dependencies": ["file_upload", "http"], "documentation": "https://www.home-assistant.io/integrations/mqtt", From 895f73d8e437cbe91a17b05e68f3f5f806357ffe Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Sun, 21 Apr 2024 23:25:27 +0200 Subject: [PATCH 733/967] Enable Ruff A001 (#115654) --- .../assist_pipeline/websocket_api.py | 7 +- homeassistant/components/jellyfin/__init__.py | 2 +- .../components/media_extractor/__init__.py | 8 +- homeassistant/components/nest/device_info.py | 2 +- .../components/onewire/binary_sensor.py | 24 ++-- homeassistant/components/onewire/sensor.py | 12 +- homeassistant/components/onewire/switch.py | 48 ++++---- homeassistant/components/risco/sensor.py | 4 +- homeassistant/components/roborock/vacuum.py | 6 +- .../components/synology_dsm/__init__.py | 4 +- homeassistant/components/zwave_js/siren.py | 3 +- homeassistant/helpers/template.py | 4 +- pyproject.toml | 3 +- tests/components/alexa/test_common.py | 8 +- .../components/device_automation/test_init.py | 24 ++-- .../devolo_home_control/test_siren.py | 12 +- tests/components/hue/test_config_flow.py | 5 +- tests/components/mqtt/test_init.py | 22 ++-- tests/components/recorder/common.py | 26 ++-- tests/components/recorder/test_util.py | 12 +- .../components/recorder/test_websocket_api.py | 8 +- tests/components/risco/test_binary_sensor.py | 24 ++-- tests/components/risco/test_sensor.py | 12 +- tests/components/sensor/test_recorder.py | 94 +++++++-------- .../components/shelly/test_device_trigger.py | 8 +- tests/components/smartthings/conftest.py | 2 +- tests/components/tautulli/test_config_flow.py | 6 +- tests/components/trace/test_websocket_api.py | 112 +++++++++--------- tests/components/twinkly/test_init.py | 6 +- tests/components/vizio/conftest.py | 2 +- tests/components/zwave_js/test_diagnostics.py | 4 +- tests/helpers/test_condition.py | 8 +- tests/helpers/test_entity.py | 36 +++--- tests/test_core.py | 16 +-- tests/util/test_percentage.py | 84 ++++++------- tests/util/yaml/test_init.py | 10 +- 36 files changed, 347 insertions(+), 321 deletions(-) diff --git a/homeassistant/components/assist_pipeline/websocket_api.py b/homeassistant/components/assist_pipeline/websocket_api.py index 7550f860a9b..3e8cdf6fa42 100644 --- a/homeassistant/components/assist_pipeline/websocket_api.py +++ b/homeassistant/components/assist_pipeline/websocket_api.py @@ -291,8 +291,11 @@ def websocket_list_runs( msg["id"], { "pipeline_runs": [ - {"pipeline_run_id": id, "timestamp": pipeline_run.timestamp} - for id, pipeline_run in pipeline_debug.items() + { + "pipeline_run_id": pipeline_run_id, + "timestamp": pipeline_run.timestamp, + } + for pipeline_run_id, pipeline_run in pipeline_debug.items() ] }, ) diff --git a/homeassistant/components/jellyfin/__init__.py b/homeassistant/components/jellyfin/__init__.py index c24f06d7b19..de9fa805f02 100644 --- a/homeassistant/components/jellyfin/__init__.py +++ b/homeassistant/components/jellyfin/__init__.py @@ -73,6 +73,6 @@ async def async_remove_config_entry_device( return not device_entry.identifiers.intersection( ( (DOMAIN, coordinator.server_id), - *((DOMAIN, id) for id in coordinator.device_ids), + *((DOMAIN, device_id) for device_id in coordinator.device_ids), ) ) diff --git a/homeassistant/components/media_extractor/__init__.py b/homeassistant/components/media_extractor/__init__.py index 228a012a04f..139acf06cf6 100644 --- a/homeassistant/components/media_extractor/__init__.py +++ b/homeassistant/components/media_extractor/__init__.py @@ -278,9 +278,9 @@ def get_best_stream_youtube(formats: list[dict[str, Any]]) -> str: return get_best_stream( [ - format - for format in formats - if format.get("acodec", "none") != "none" - and format.get("vcodec", "none") != "none" + stream_format + for stream_format in formats + if stream_format.get("acodec", "none") != "none" + and stream_format.get("vcodec", "none") != "none" ] ) diff --git a/homeassistant/components/nest/device_info.py b/homeassistant/components/nest/device_info.py index f269e3e89d6..33793fe836b 100644 --- a/homeassistant/components/nest/device_info.py +++ b/homeassistant/components/nest/device_info.py @@ -73,7 +73,7 @@ class NestDeviceInfo: """Return device suggested area based on the Google Home room.""" if parent_relations := self._device.parent_relations: items = sorted(parent_relations.items()) - names = [name for id, name in items] + names = [name for _, name in items] return " ".join(names) return None diff --git a/homeassistant/components/onewire/binary_sensor.py b/homeassistant/components/onewire/binary_sensor.py index fea78fd3760..d2e66609103 100644 --- a/homeassistant/components/onewire/binary_sensor.py +++ b/homeassistant/components/onewire/binary_sensor.py @@ -36,33 +36,33 @@ class OneWireBinarySensorEntityDescription( DEVICE_BINARY_SENSORS: dict[str, tuple[OneWireBinarySensorEntityDescription, ...]] = { "12": tuple( OneWireBinarySensorEntityDescription( - key=f"sensed.{id}", + key=f"sensed.{device_key}", entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, translation_key="sensed_id", - translation_placeholders={"id": str(id)}, + translation_placeholders={"id": str(device_key)}, ) - for id in DEVICE_KEYS_A_B + for device_key in DEVICE_KEYS_A_B ), "29": tuple( OneWireBinarySensorEntityDescription( - key=f"sensed.{id}", + key=f"sensed.{device_key}", entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, translation_key="sensed_id", - translation_placeholders={"id": str(id)}, + translation_placeholders={"id": str(device_key)}, ) - for id in DEVICE_KEYS_0_7 + for device_key in DEVICE_KEYS_0_7 ), "3A": tuple( OneWireBinarySensorEntityDescription( - key=f"sensed.{id}", + key=f"sensed.{device_key}", entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, translation_key="sensed_id", - translation_placeholders={"id": str(id)}, + translation_placeholders={"id": str(device_key)}, ) - for id in DEVICE_KEYS_A_B + for device_key in DEVICE_KEYS_A_B ), "EF": (), # "HobbyBoard": special } @@ -71,15 +71,15 @@ DEVICE_BINARY_SENSORS: dict[str, tuple[OneWireBinarySensorEntityDescription, ... HOBBYBOARD_EF: dict[str, tuple[OneWireBinarySensorEntityDescription, ...]] = { "HB_HUB": tuple( OneWireBinarySensorEntityDescription( - key=f"hub/short.{id}", + key=f"hub/short.{device_key}", entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, entity_category=EntityCategory.DIAGNOSTIC, device_class=BinarySensorDeviceClass.PROBLEM, translation_key="hub_short_id", - translation_placeholders={"id": str(id)}, + translation_placeholders={"id": str(device_key)}, ) - for id in DEVICE_KEYS_0_3 + for device_key in DEVICE_KEYS_0_3 ), } diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index d32afce7fa9..46f18842d51 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -233,14 +233,14 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { "42": (SIMPLE_TEMPERATURE_SENSOR_DESCRIPTION,), "1D": tuple( OneWireSensorEntityDescription( - key=f"counter.{id}", + key=f"counter.{device_key}", native_unit_of_measurement="count", read_mode=READ_MODE_INT, state_class=SensorStateClass.TOTAL_INCREASING, translation_key="counter_id", - translation_placeholders={"id": str(id)}, + translation_placeholders={"id": str(device_key)}, ) - for id in DEVICE_KEYS_A_B + for device_key in DEVICE_KEYS_A_B ), } @@ -273,15 +273,15 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { ), "HB_MOISTURE_METER": tuple( OneWireSensorEntityDescription( - key=f"moisture/sensor.{id}", + key=f"moisture/sensor.{device_key}", device_class=SensorDeviceClass.PRESSURE, native_unit_of_measurement=UnitOfPressure.CBAR, read_mode=READ_MODE_FLOAT, state_class=SensorStateClass.MEASUREMENT, translation_key="moisture_id", - translation_placeholders={"id": str(id)}, + translation_placeholders={"id": str(device_key)}, ) - for id in DEVICE_KEYS_0_3 + for device_key in DEVICE_KEYS_0_3 ), } diff --git a/homeassistant/components/onewire/switch.py b/homeassistant/components/onewire/switch.py index cdf1315394e..41276218540 100644 --- a/homeassistant/components/onewire/switch.py +++ b/homeassistant/components/onewire/switch.py @@ -40,23 +40,23 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = { "12": tuple( [ OneWireSwitchEntityDescription( - key=f"PIO.{id}", + key=f"PIO.{device_key}", entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, translation_key="pio_id", - translation_placeholders={"id": str(id)}, + translation_placeholders={"id": str(device_key)}, ) - for id in DEVICE_KEYS_A_B + for device_key in DEVICE_KEYS_A_B ] + [ OneWireSwitchEntityDescription( - key=f"latch.{id}", + key=f"latch.{device_key}", entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, translation_key="latch_id", - translation_placeholders={"id": str(id)}, + translation_placeholders={"id": str(device_key)}, ) - for id in DEVICE_KEYS_A_B + for device_key in DEVICE_KEYS_A_B ] ), "26": ( @@ -71,34 +71,34 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = { "29": tuple( [ OneWireSwitchEntityDescription( - key=f"PIO.{id}", + key=f"PIO.{device_key}", entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, translation_key="pio_id", - translation_placeholders={"id": str(id)}, + translation_placeholders={"id": str(device_key)}, ) - for id in DEVICE_KEYS_0_7 + for device_key in DEVICE_KEYS_0_7 ] + [ OneWireSwitchEntityDescription( - key=f"latch.{id}", + key=f"latch.{device_key}", entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, translation_key="latch_id", - translation_placeholders={"id": str(id)}, + translation_placeholders={"id": str(device_key)}, ) - for id in DEVICE_KEYS_0_7 + for device_key in DEVICE_KEYS_0_7 ] ), "3A": tuple( OneWireSwitchEntityDescription( - key=f"PIO.{id}", + key=f"PIO.{device_key}", entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, translation_key="pio_id", - translation_placeholders={"id": str(id)}, + translation_placeholders={"id": str(device_key)}, ) - for id in DEVICE_KEYS_A_B + for device_key in DEVICE_KEYS_A_B ), "EF": (), # "HobbyBoard": special } @@ -108,37 +108,37 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = { HOBBYBOARD_EF: dict[str, tuple[OneWireEntityDescription, ...]] = { "HB_HUB": tuple( OneWireSwitchEntityDescription( - key=f"hub/branch.{id}", + key=f"hub/branch.{device_key}", entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, entity_category=EntityCategory.CONFIG, translation_key="hub_branch_id", - translation_placeholders={"id": str(id)}, + translation_placeholders={"id": str(device_key)}, ) - for id in DEVICE_KEYS_0_3 + for device_key in DEVICE_KEYS_0_3 ), "HB_MOISTURE_METER": tuple( [ OneWireSwitchEntityDescription( - key=f"moisture/is_leaf.{id}", + key=f"moisture/is_leaf.{device_key}", entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, entity_category=EntityCategory.CONFIG, translation_key="leaf_sensor_id", - translation_placeholders={"id": str(id)}, + translation_placeholders={"id": str(device_key)}, ) - for id in DEVICE_KEYS_0_3 + for device_key in DEVICE_KEYS_0_3 ] + [ OneWireSwitchEntityDescription( - key=f"moisture/is_moisture.{id}", + key=f"moisture/is_moisture.{device_key}", entity_registry_enabled_default=False, read_mode=READ_MODE_BOOL, entity_category=EntityCategory.CONFIG, translation_key="moisture_sensor_id", - translation_placeholders={"id": str(id)}, + translation_placeholders={"id": str(device_key)}, ) - for id in DEVICE_KEYS_0_3 + for device_key in DEVICE_KEYS_0_3 ] ), } diff --git a/homeassistant/components/risco/sensor.py b/homeassistant/components/risco/sensor.py index f4d6ddaf451..8f97c76c879 100644 --- a/homeassistant/components/risco/sensor.py +++ b/homeassistant/components/risco/sensor.py @@ -56,8 +56,8 @@ async def async_setup_entry( config_entry.entry_id ][EVENTS_COORDINATOR] sensors = [ - RiscoSensor(coordinator, id, [], name, config_entry.entry_id) - for id, name in CATEGORIES.items() + RiscoSensor(coordinator, category_id, [], name, config_entry.entry_id) + for category_id, name in CATEGORIES.items() ] sensors.append( RiscoSensor( diff --git a/homeassistant/components/roborock/vacuum.py b/homeassistant/components/roborock/vacuum.py index d8108abf78c..16cf518aa02 100644 --- a/homeassistant/components/roborock/vacuum.py +++ b/homeassistant/components/roborock/vacuum.py @@ -178,4 +178,8 @@ class RoborockVacuum(RoborockCoordinatedEntity, StateVacuumEntity): async def get_maps(self) -> ServiceResponse: """Get map information such as map id and room ids.""" - return {"maps": [asdict(map) for map in self.coordinator.maps.values()]} + return { + "maps": [ + asdict(vacuum_map) for vacuum_map in self.coordinator.maps.values() + ] + } diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index ec13ec929a5..2748b27c93d 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -161,6 +161,8 @@ async def async_remove_config_entry_device( return not device_entry.identifiers.intersection( ( (DOMAIN, serial), # Base device - *((DOMAIN, f"{serial}_{id}") for id in device_ids), # Storage and cameras + *( + (DOMAIN, f"{serial}_{device_id}") for device_id in device_ids + ), # Storage and cameras ) ) diff --git a/homeassistant/components/zwave_js/siren.py b/homeassistant/components/zwave_js/siren.py index b3f54ae9904..413186da9bf 100644 --- a/homeassistant/components/zwave_js/siren.py +++ b/homeassistant/components/zwave_js/siren.py @@ -63,7 +63,8 @@ class ZwaveSirenEntity(ZWaveBaseEntity, SirenEntity): super().__init__(config_entry, driver, info) # Entity class attributes self._attr_available_tones = { - int(id): val for id, val in self.info.primary_value.metadata.states.items() + int(state_id): val + for state_id, val in self.info.primary_value.metadata.states.items() } self._attr_supported_features = ( SirenEntityFeature.TURN_ON diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 1f0742e896d..16379c1d05c 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1347,8 +1347,8 @@ def device_id(hass: HomeAssistant, entity_id_or_device_name: str) -> str | None: dev_reg = device_registry.async_get(hass) return next( ( - id - for id, device in dev_reg.devices.items() + device_id + for device_id, device in dev_reg.devices.items() if (name := device.name_by_user or device.name) and (str(entity_id_or_device_name) == name) ), diff --git a/pyproject.toml b/pyproject.toml index 91f75c96fd6..d3487d50a17 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -251,7 +251,7 @@ disable = [ "nested-min-max", # PLW3301 "pointless-statement", # B018 "raise-missing-from", # B904 - # "redefined-builtin", # A001, ruff is way more stricter, needs work + "redefined-builtin", # A001 "try-except-raise", # TRY302 "unused-argument", # ARG001, we don't use it "unused-format-string-argument", #F507 @@ -663,6 +663,7 @@ required-version = ">=0.4.1" [tool.ruff.lint] select = [ + "A001", # Variable {name} is shadowing a Python builtin "B002", # Python does not support the unary prefix increment "B005", # Using .strip() with multi-character strings is misleading "B007", # Loop control variable {name} not used within loop body diff --git a/tests/components/alexa/test_common.py b/tests/components/alexa/test_common.py index 0cc4d995efa..9fdcc1c89c1 100644 --- a/tests/components/alexa/test_common.py +++ b/tests/components/alexa/test_common.py @@ -158,14 +158,14 @@ async def assert_power_controller_works( _, response = await assert_request_calls_service( "Alexa.PowerController", "TurnOn", endpoint, on_service, hass ) - for property in response["context"]["properties"]: - assert property["timeOfSample"] == timestamp + for context_property in response["context"]["properties"]: + assert context_property["timeOfSample"] == timestamp _, response = await assert_request_calls_service( "Alexa.PowerController", "TurnOff", endpoint, off_service, hass ) - for property in response["context"]["properties"]: - assert property["timeOfSample"] == timestamp + for context_property in response["context"]["properties"]: + assert context_property["timeOfSample"] == timestamp async def assert_scene_controller_works( diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index 4526a9d9b67..3c3101d7a1f 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -328,23 +328,23 @@ async def test_websocket_get_action_capabilities( assert msg["success"] actions = msg["result"] - id = 2 + msg_id = 2 assert len(actions) == 3 for action in actions: await client.send_json( { - "id": id, + "id": msg_id, "type": "device_automation/action/capabilities", "action": action, } ) msg = await client.receive_json() - assert msg["id"] == id + assert msg["id"] == msg_id assert msg["type"] == TYPE_RESULT assert msg["success"] capabilities = msg["result"] assert capabilities == expected_capabilities[action["type"]] - id = id + 1 + msg_id = msg_id + 1 async def test_websocket_get_action_capabilities_unknown_domain( @@ -487,23 +487,23 @@ async def test_websocket_get_condition_capabilities( assert msg["success"] conditions = msg["result"] - id = 2 + msg_id = 2 assert len(conditions) == 2 for condition in conditions: await client.send_json( { - "id": id, + "id": msg_id, "type": "device_automation/condition/capabilities", "condition": condition, } ) msg = await client.receive_json() - assert msg["id"] == id + assert msg["id"] == msg_id assert msg["type"] == TYPE_RESULT assert msg["success"] capabilities = msg["result"] assert capabilities == expected_capabilities - id = id + 1 + msg_id = msg_id + 1 async def test_websocket_get_condition_capabilities_unknown_domain( @@ -775,23 +775,23 @@ async def test_websocket_get_trigger_capabilities( assert msg["success"] triggers = msg["result"] - id = 2 + msg_id = 2 assert len(triggers) == 3 # toggled, turned_on, turned_off for trigger in triggers: await client.send_json( { - "id": id, + "id": msg_id, "type": "device_automation/trigger/capabilities", "trigger": trigger, } ) msg = await client.receive_json() - assert msg["id"] == id + assert msg["id"] == msg_id assert msg["type"] == TYPE_RESULT assert msg["success"] capabilities = msg["result"] assert capabilities == expected_capabilities - id = id + 1 + msg_id = msg_id + 1 async def test_websocket_get_trigger_capabilities_unknown_domain( diff --git a/tests/components/devolo_home_control/test_siren.py b/tests/components/devolo_home_control/test_siren.py index 037d7b5021f..be662418967 100644 --- a/tests/components/devolo_home_control/test_siren.py +++ b/tests/components/devolo_home_control/test_siren.py @@ -66,7 +66,7 @@ async def test_siren_switching( with patch( "devolo_home_control_api.properties.multi_level_switch_property.MultiLevelSwitchProperty.set" - ) as set: + ) as property_set: await hass.services.async_call( "siren", "turn_on", @@ -78,11 +78,11 @@ async def test_siren_switching( "Test", ("devolo.SirenMultiLevelSwitch:Test", 1) ) await hass.async_block_till_done() - set.assert_called_once_with(1) + property_set.assert_called_once_with(1) with patch( "devolo_home_control_api.properties.multi_level_switch_property.MultiLevelSwitchProperty.set" - ) as set: + ) as property_set: await hass.services.async_call( "siren", "turn_off", @@ -95,7 +95,7 @@ async def test_siren_switching( ) await hass.async_block_till_done() assert hass.states.get(f"{DOMAIN}.test").state == STATE_OFF - set.assert_called_once_with(0) + property_set.assert_called_once_with(0) @pytest.mark.usefixtures("mock_zeroconf") @@ -119,7 +119,7 @@ async def test_siren_change_default_tone( with patch( "devolo_home_control_api.properties.multi_level_switch_property.MultiLevelSwitchProperty.set" - ) as set: + ) as property_set: test_gateway.publisher.dispatch("Test", ("mss:Test", 2)) await hass.services.async_call( "siren", @@ -127,7 +127,7 @@ async def test_siren_change_default_tone( {"entity_id": f"{DOMAIN}.test"}, blocking=True, ) - set.assert_called_once_with(2) + property_set.assert_called_once_with(2) @pytest.mark.usefixtures("mock_zeroconf") diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index 325c32227e3..692bd1405cf 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -36,7 +36,10 @@ def create_mock_api_discovery(aioclient_mock, bridges): """Patch aiohttp responses with fake data for bridge discovery.""" aioclient_mock.get( URL_NUPNP, - json=[{"internalipaddress": host, "id": id} for (host, id) in bridges], + json=[ + {"internalipaddress": host, "id": bridge_id} + for (host, bridge_id) in bridges + ], ) for host, bridge_id in bridges: aioclient_mock.get( diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 37f7e0cf587..7bb43568b30 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -3033,14 +3033,16 @@ async def test_debug_info_multiple_devices( for dev in devices: data = json.dumps(dev["config"]) domain = dev["domain"] - id = dev["config"]["device"]["identifiers"][0] - async_fire_mqtt_message(hass, f"homeassistant/{domain}/{id}/config", data) + device_id = dev["config"]["device"]["identifiers"][0] + async_fire_mqtt_message( + hass, f"homeassistant/{domain}/{device_id}/config", data + ) await hass.async_block_till_done() for dev in devices: domain = dev["domain"] - id = dev["config"]["device"]["identifiers"][0] - device = device_registry.async_get_device(identifiers={("mqtt", id)}) + device_id = dev["config"]["device"]["identifiers"][0] + device = device_registry.async_get_device(identifiers={("mqtt", device_id)}) assert device is not None debug_info_data = debug_info.info_for_device(hass, device.id) @@ -3058,7 +3060,7 @@ async def test_debug_info_multiple_devices( assert len(debug_info_data["triggers"]) == 1 discovery_data = debug_info_data["triggers"][0]["discovery_data"] - assert discovery_data["topic"] == f"homeassistant/{domain}/{id}/config" + assert discovery_data["topic"] == f"homeassistant/{domain}/{device_id}/config" assert discovery_data["payload"] == dev["config"] @@ -3116,8 +3118,10 @@ async def test_debug_info_multiple_entities_triggers( data = json.dumps(c["config"]) domain = c["domain"] # Use topic as discovery_id - id = c["config"].get("topic", c["config"].get("state_topic")) - async_fire_mqtt_message(hass, f"homeassistant/{domain}/{id}/config", data) + discovery_id = c["config"].get("topic", c["config"].get("state_topic")) + async_fire_mqtt_message( + hass, f"homeassistant/{domain}/{discovery_id}/config", data + ) await hass.async_block_till_done() device_id = config[0]["config"]["device"]["identifiers"][0] @@ -3131,7 +3135,7 @@ async def test_debug_info_multiple_entities_triggers( # Test we get debug info for each entity and trigger domain = c["domain"] # Use topic as discovery_id - id = c["config"].get("topic", c["config"].get("state_topic")) + discovery_id = c["config"].get("topic", c["config"].get("state_topic")) if c["domain"] != "device_automation": discovery_data = [e["discovery_data"] for e in debug_info_data["entities"]] @@ -3143,7 +3147,7 @@ async def test_debug_info_multiple_entities_triggers( discovery_data = [e["discovery_data"] for e in debug_info_data["triggers"]] assert { - "topic": f"homeassistant/{domain}/{id}/config", + "topic": f"homeassistant/{domain}/{discovery_id}/config", "payload": c["config"], } in discovery_data diff --git a/tests/components/recorder/common.py b/tests/components/recorder/common.py index 7a57b226d77..e0f43323f25 100644 --- a/tests/components/recorder/common.py +++ b/tests/components/recorder/common.py @@ -109,7 +109,9 @@ async def async_wait_recording_done(hass: HomeAssistant) -> None: await hass.async_block_till_done() -async def async_wait_purge_done(hass: HomeAssistant, max: int | None = None) -> None: +async def async_wait_purge_done( + hass: HomeAssistant, max_number: int | None = None +) -> None: """Wait for max number of purge events. Because a purge may insert another PurgeTask into @@ -117,9 +119,9 @@ async def async_wait_purge_done(hass: HomeAssistant, max: int | None = None) -> a maximum number of WaitTasks that we will put into the queue. """ - if not max: - max = DEFAULT_PURGE_TASKS - for _ in range(max + 1): + if not max_number: + max_number = DEFAULT_PURGE_TASKS + for _ in range(max_number + 1): await async_wait_recording_done(hass) @@ -325,10 +327,10 @@ def convert_pending_states_to_meta(instance: Recorder, session: Session) -> None entity_ids: set[str] = set() states: set[States] = set() states_meta_objects: dict[str, StatesMeta] = {} - for object in session: - if isinstance(object, States): - entity_ids.add(object.entity_id) - states.add(object) + for session_object in session: + if isinstance(session_object, States): + entity_ids.add(session_object.entity_id) + states.add(session_object) entity_id_to_metadata_ids = instance.states_meta_manager.get_many( entity_ids, session, True @@ -352,10 +354,10 @@ def convert_pending_events_to_event_types(instance: Recorder, session: Session) event_types: set[str] = set() events: set[Events] = set() event_types_objects: dict[str, EventTypes] = {} - for object in session: - if isinstance(object, Events): - event_types.add(object.event_type) - events.add(object) + for session_object in session: + if isinstance(session_object, Events): + event_types.add(session_object.event_type) + events.add(session_object) event_type_to_event_type_ids = instance.event_type_manager.get_many( event_types, session, True diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index 549280efba2..9e32fa2c500 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -1040,14 +1040,14 @@ async def test_resolve_period(hass: HomeAssistant) -> None: def test_chunked_or_all(): """Test chunked_or_all can iterate chunk sizes larger than the passed in collection.""" - all = [] + all_items = [] incoming = (1, 2, 3, 4) for chunk in chunked_or_all(incoming, 2): assert len(chunk) == 2 - all.extend(chunk) - assert all == [1, 2, 3, 4] + all_items.extend(chunk) + assert all_items == [1, 2, 3, 4] - all = [] + all_items = [] incoming = (1, 2, 3, 4) for chunk in chunked_or_all(incoming, 5): assert len(chunk) == 4 @@ -1055,5 +1055,5 @@ def test_chunked_or_all(): # collection since we want to avoid copying the collection # if we don't need to assert chunk is incoming - all.extend(chunk) - assert all == [1, 2, 3, 4] + all_items.extend(chunk) + assert all_items == [1, 2, 3, 4] diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index d594218e9d4..4a1410d45a4 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -641,12 +641,12 @@ async def test_statistic_during_period_hole( recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test statistic_during_period when there are holes in the data.""" - id = 1 + stat_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal stat_id + stat_id += 1 + return stat_id now = dt_util.utcnow() diff --git a/tests/components/risco/test_binary_sensor.py b/tests/components/risco/test_binary_sensor.py index ea18c59e236..b6ea723064e 100644 --- a/tests/components/risco/test_binary_sensor.py +++ b/tests/components/risco/test_binary_sensor.py @@ -122,11 +122,11 @@ async def test_local_setup( async def _check_local_state( - hass, zones, property, value, entity_id, zone_id, callback + hass, zones, entity_property, value, entity_id, zone_id, callback ): with patch.object( zones[zone_id], - property, + entity_property, new_callable=PropertyMock(return_value=value), ): await callback(zone_id, zones[zone_id]) @@ -210,19 +210,19 @@ async def test_armed_local_states( ) -async def _check_system_state(hass, system, property, value, callback): +async def _check_system_state(hass, system, entity_property, value, callback): with patch.object( system, - property, + entity_property, new_callable=PropertyMock(return_value=value), ): await callback(system) await hass.async_block_till_done() expected_value = STATE_ON if value else STATE_OFF - if property == "ac_trouble": - property = "a_c_trouble" - entity_id = f"binary_sensor.test_site_name_{property}" + if entity_property == "ac_trouble": + entity_property = "a_c_trouble" + entity_id = f"binary_sensor.test_site_name_{entity_property}" assert hass.states.get(entity_id).state == expected_value @@ -275,6 +275,10 @@ async def test_system_states( "clock_trouble", "box_tamper", ] - for property in properties: - await _check_system_state(hass, system_only_local, property, True, callback) - await _check_system_state(hass, system_only_local, property, False, callback) + for entity_property in properties: + await _check_system_state( + hass, system_only_local, entity_property, True, callback + ) + await _check_system_state( + hass, system_only_local, entity_property, False, callback + ) diff --git a/tests/components/risco/test_sensor.py b/tests/components/risco/test_sensor.py index 157eb3e62b5..a8236ad3d87 100644 --- a/tests/components/risco/test_sensor.py +++ b/tests/components/risco/test_sensor.py @@ -133,8 +133,8 @@ async def test_error_on_login( await hass.async_block_till_done() registry = er.async_get(hass) - for id in ENTITY_IDS.values(): - assert not registry.async_is_registered(id) + for entity_id in ENTITY_IDS.values(): + assert not registry.async_is_registered(entity_id) def _check_state(hass, category, entity_id): @@ -184,8 +184,8 @@ async def test_cloud_setup( ) -> None: """Test entity setup.""" registry = er.async_get(hass) - for id in ENTITY_IDS.values(): - assert registry.async_is_registered(id) + for entity_id in ENTITY_IDS.values(): + assert registry.async_is_registered(entity_id) save_mock.assert_awaited_once_with({LAST_EVENT_TIMESTAMP_KEY: TEST_EVENTS[0].time}) for category, entity_id in ENTITY_IDS.items(): @@ -213,5 +213,5 @@ async def test_local_setup( ) -> None: """Test entity setup.""" registry = er.async_get(hass) - for id in ENTITY_IDS.values(): - assert not registry.async_is_registered(id) + for entity_id in ENTITY_IDS.values(): + assert not registry.async_is_registered(entity_id) diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 8084fe69e89..a7aaf938410 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -560,7 +560,7 @@ def test_compile_hourly_statistics_purged_state_changes( ) assert_dict_of_states_equal_without_context_and_last_changed(states, hist) - mean = min = max = float(hist["sensor.test1"][-1].state) + mean = min_value = max_value = float(hist["sensor.test1"][-1].state) # Purge all states from the database with freeze_time(four): @@ -594,8 +594,8 @@ def test_compile_hourly_statistics_purged_state_changes( "start": process_timestamp(zero).timestamp(), "end": process_timestamp(zero + timedelta(minutes=5)).timestamp(), "mean": pytest.approx(mean), - "min": pytest.approx(min), - "max": pytest.approx(max), + "min": pytest.approx(min_value), + "max": pytest.approx(max_value), "last_reset": None, "state": None, "sum": None, @@ -4113,12 +4113,12 @@ async def test_validate_unit_change_convertible( The test also asserts that the sensor's device class is ignored. """ - id = 1 + msg_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id async def assert_validation_result(client, expected_result): await client.send_json( @@ -4228,12 +4228,12 @@ async def test_validate_statistics_unit_ignore_device_class( The test asserts that the sensor's device class is ignored. """ - id = 1 + msg_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id async def assert_validation_result(client, expected_result): await client.send_json( @@ -4321,14 +4321,14 @@ async def test_validate_statistics_unit_change_no_device_class( conversion, and the unit is then changed to a unit which can and cannot be converted to the original unit. """ - id = 1 + msg_id = 1 attributes = dict(attributes) attributes.pop("device_class") def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id async def assert_validation_result(client, expected_result): await client.send_json( @@ -4436,12 +4436,12 @@ async def test_validate_statistics_unsupported_state_class( unit, ) -> None: """Test validate_statistics.""" - id = 1 + msg_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id async def assert_validation_result(client, expected_result): await client.send_json( @@ -4505,12 +4505,12 @@ async def test_validate_statistics_sensor_no_longer_recorded( unit, ) -> None: """Test validate_statistics.""" - id = 1 + msg_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id async def assert_validation_result(client, expected_result): await client.send_json( @@ -4573,12 +4573,12 @@ async def test_validate_statistics_sensor_not_recorded( unit, ) -> None: """Test validate_statistics.""" - id = 1 + msg_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id async def assert_validation_result(client, expected_result): await client.send_json( @@ -4638,12 +4638,12 @@ async def test_validate_statistics_sensor_removed( unit, ) -> None: """Test validate_statistics.""" - id = 1 + msg_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id async def assert_validation_result(client, expected_result): await client.send_json( @@ -4702,12 +4702,12 @@ async def test_validate_statistics_unit_change_no_conversion( unit2, ) -> None: """Test validate_statistics.""" - id = 1 + msg_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id async def assert_validation_result(client, expected_result): await client.send_json( @@ -4837,12 +4837,12 @@ async def test_validate_statistics_unit_change_equivalent_units( This tests no validation issue is created when a sensor's unit changes to an equivalent unit. """ - id = 1 + msg_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id async def assert_validation_result(client, expected_result): await client.send_json( @@ -4923,12 +4923,12 @@ async def test_validate_statistics_unit_change_equivalent_units_2( equivalent unit which is not known to the unit converters. """ - id = 1 + msg_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id async def assert_validation_result(client, expected_result): await client.send_json( @@ -5005,12 +5005,12 @@ async def test_validate_statistics_other_domain( recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test sensor does not raise issues for statistics for other domains.""" - id = 1 + msg_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id async def assert_validation_result(client, expected_result): await client.send_json( diff --git a/tests/components/shelly/test_device_trigger.py b/tests/components/shelly/test_device_trigger.py index c4db8acaf6d..39238f1674a 100644 --- a/tests/components/shelly/test_device_trigger.py +++ b/tests/components/shelly/test_device_trigger.py @@ -96,11 +96,11 @@ async def test_get_triggers_rpc_device( CONF_PLATFORM: "device", CONF_DEVICE_ID: device.id, CONF_DOMAIN: DOMAIN, - CONF_TYPE: type, + CONF_TYPE: trigger_type, CONF_SUBTYPE: "button1", "metadata": {}, } - for type in [ + for trigger_type in [ "btn_down", "btn_up", "single_push", @@ -130,11 +130,11 @@ async def test_get_triggers_button( CONF_PLATFORM: "device", CONF_DEVICE_ID: device.id, CONF_DOMAIN: DOMAIN, - CONF_TYPE: type, + CONF_TYPE: trigger_type, CONF_SUBTYPE: "button", "metadata": {}, } - for 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/smartthings/conftest.py b/tests/components/smartthings/conftest.py index b6d34b9d98a..d25cc8849e5 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -342,7 +342,7 @@ def event_request_factory_fixture(event_factory): if events is None: events = [] if device_ids: - events.extend([event_factory(id) for id in device_ids]) + events.extend([event_factory(device_id) for device_id in device_ids]) events.append(event_factory(uuid4())) events.append(event_factory(device_ids[0], event_type="OTHER")) request.events = events diff --git a/tests/components/tautulli/test_config_flow.py b/tests/components/tautulli/test_config_flow.py index b731067cd72..ca563cfad77 100644 --- a/tests/components/tautulli/test_config_flow.py +++ b/tests/components/tautulli/test_config_flow.py @@ -133,7 +133,7 @@ async def test_flow_user_multiple_entries_allowed(hass: HomeAssistant) -> None: assert result["step_id"] == "user" assert result["errors"] == {} - input = { + user_input = { CONF_URL: "http://1.2.3.5:8181/test", CONF_API_KEY: "efgh", CONF_VERIFY_SSL: True, @@ -141,13 +141,13 @@ async def test_flow_user_multiple_entries_allowed(hass: HomeAssistant) -> None: with patch_config_flow_tautulli(AsyncMock()): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input=input, + user_input=user_input, ) await hass.async_block_till_done() assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == NAME - assert result2["data"] == input + assert result2["data"] == user_input async def test_flow_reauth( diff --git a/tests/components/trace/test_websocket_api.py b/tests/components/trace/test_websocket_api.py index 5c5d882b721..f2cfb6a109f 100644 --- a/tests/components/trace/test_websocket_api.py +++ b/tests/components/trace/test_websocket_api.py @@ -133,12 +133,12 @@ async def test_get_trace( ) -> None: """Test tracing a script or automation.""" await async_setup_component(hass, "homeassistant", {}) - id = 1 + msg_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id sun_config = { "id": "sun", @@ -429,12 +429,12 @@ async def test_restore_traces( ) -> None: """Test restored traces.""" hass.set_state(CoreState.not_running) - id = 1 + msg_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id saved_traces = json.loads(load_fixture(f"trace/{domain}_saved_traces.json")) hass_storage["trace.saved_traces"] = saved_traces @@ -522,7 +522,7 @@ async def test_trace_overflow( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, domain, stored_traces ) -> None: """Test the number of stored traces per script or automation is limited.""" - id = 1 + msg_id = 1 trace_uuids = [] @@ -532,9 +532,9 @@ async def test_trace_overflow( return trace_uuids[-1] def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id sun_config = { "id": "sun", @@ -601,7 +601,7 @@ async def test_restore_traces_overflow( ) -> None: """Test restored traces are evicted first.""" hass.set_state(CoreState.not_running) - id = 1 + msg_id = 1 trace_uuids = [] @@ -611,9 +611,9 @@ async def test_restore_traces_overflow( return trace_uuids[-1] def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id saved_traces = json.loads(load_fixture(f"trace/{domain}_saved_traces.json")) hass_storage["trace.saved_traces"] = saved_traces @@ -682,7 +682,7 @@ async def test_restore_traces_late_overflow( ) -> None: """Test restored traces are evicted first.""" hass.set_state(CoreState.not_running) - id = 1 + msg_id = 1 trace_uuids = [] @@ -692,9 +692,9 @@ async def test_restore_traces_late_overflow( return trace_uuids[-1] def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id saved_traces = json.loads(load_fixture(f"trace/{domain}_saved_traces.json")) hass_storage["trace.saved_traces"] = saved_traces @@ -743,12 +743,12 @@ async def test_trace_no_traces( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, domain ) -> None: """Test the storing traces for a script or automation can be disabled.""" - id = 1 + msg_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id sun_config = { "id": "sun", @@ -810,12 +810,12 @@ async def test_list_traces( ) -> None: """Test listing script and automation traces.""" await async_setup_component(hass, "homeassistant", {}) - id = 1 + msg_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id sun_config = { "id": "sun", @@ -943,12 +943,12 @@ async def test_nested_traces( extra_trace_keys, ) -> None: """Test nested automation and script traces.""" - id = 1 + msg_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id sun_config = { "id": "sun", @@ -1003,12 +1003,12 @@ async def test_breakpoints( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, domain, prefix ) -> None: """Test script and automation breakpoints.""" - id = 1 + msg_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id async def assert_last_step(item_id, expected_action, expected_state): await client.send_json( @@ -1173,12 +1173,12 @@ async def test_breakpoints_2( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, domain, prefix ) -> None: """Test execution resumes and breakpoints are removed after subscription removed.""" - id = 1 + msg_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id async def assert_last_step(item_id, expected_action, expected_state): await client.send_json( @@ -1278,12 +1278,12 @@ async def test_breakpoints_3( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, domain, prefix ) -> None: """Test breakpoints can be cleared.""" - id = 1 + msg_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id async def assert_last_step(item_id, expected_action, expected_state): await client.send_json( @@ -1434,12 +1434,12 @@ async def test_script_mode( script_execution, ) -> None: """Test overlapping runs with max_runs > 1.""" - id = 1 + msg_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id flag = asyncio.Event() @@ -1502,12 +1502,12 @@ async def test_script_mode_2( script_execution, ) -> None: """Test overlapping runs with max_runs > 1.""" - id = 1 + msg_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id flag = asyncio.Event() @@ -1577,12 +1577,12 @@ async def test_trace_blueprint_automation( ) -> None: """Test trace of blueprint automation.""" await async_setup_component(hass, "homeassistant", {}) - id = 1 + msg_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id domain = "automation" sun_config = { diff --git a/tests/components/twinkly/test_init.py b/tests/components/twinkly/test_init.py index 794d4d5e773..6642807ac3f 100644 --- a/tests/components/twinkly/test_init.py +++ b/tests/components/twinkly/test_init.py @@ -17,16 +17,16 @@ async def test_load_unload_entry(hass: HomeAssistant) -> None: """Validate that setup entry also configure the client.""" client = ClientMock() - id = str(uuid4()) + device_id = str(uuid4()) config_entry = MockConfigEntry( domain=TWINKLY_DOMAIN, data={ CONF_HOST: TEST_HOST, - CONF_ID: id, + CONF_ID: device_id, CONF_NAME: TEST_NAME_ORIGINAL, CONF_MODEL: TEST_MODEL, }, - entry_id=id, + entry_id=device_id, ) config_entry.add_to_hass(hass) diff --git a/tests/components/vizio/conftest.py b/tests/components/vizio/conftest.py index 6ce36b38c8f..783ed8b4585 100644 --- a/tests/components/vizio/conftest.py +++ b/tests/components/vizio/conftest.py @@ -37,7 +37,7 @@ class MockInput: def get_mock_inputs(input_list): """Return list of MockInput.""" - return [MockInput(input) for input in input_list] + return [MockInput(device_input) for device_input in input_list] @pytest.fixture(name="vizio_get_unique_id", autouse=True) diff --git a/tests/components/zwave_js/test_diagnostics.py b/tests/components/zwave_js/test_diagnostics.py index 054906cd0f6..ea354ab80d3 100644 --- a/tests/components/zwave_js/test_diagnostics.py +++ b/tests/components/zwave_js/test_diagnostics.py @@ -128,7 +128,9 @@ async def test_device_diagnostics( ) assert diagnostics_data["state"] == { **multisensor_6.data, - "values": {id: val.data for id, val in multisensor_6.values.items()}, + "values": { + value_id: val.data for value_id, val in multisensor_6.values.items() + }, "endpoints": { str(idx): endpoint.data for idx, endpoint in multisensor_6.endpoints.items() }, diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index 701bc342760..20dea85c3e4 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -2178,12 +2178,12 @@ def _find_run_id(traces, trace_type, item_id): async def assert_automation_condition_trace(hass_ws_client, automation_id, expected): """Test the result of automation condition.""" - id = 1 + msg_id = 1 def next_id(): - nonlocal id - id += 1 - return id + nonlocal msg_id + msg_id += 1 + return msg_id client = await hass_ws_client() diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index fb2793a75c7..690592a850b 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -2330,30 +2330,30 @@ async def test_cached_entity_properties( async def test_cached_entity_property_delete_attr(hass: HomeAssistant) -> None: """Test deleting an _attr corresponding to a cached property.""" - property = "has_entity_name" + property_name = "has_entity_name" ent = entity.Entity() - assert not hasattr(ent, f"_attr_{property}") + assert not hasattr(ent, f"_attr_{property_name}") with pytest.raises(AttributeError): - delattr(ent, f"_attr_{property}") - assert getattr(ent, property) is False + delattr(ent, f"_attr_{property_name}") + assert getattr(ent, property_name) is False with pytest.raises(AttributeError): - delattr(ent, f"_attr_{property}") - assert not hasattr(ent, f"_attr_{property}") - assert getattr(ent, property) is False + delattr(ent, f"_attr_{property_name}") + assert not hasattr(ent, f"_attr_{property_name}") + assert getattr(ent, property_name) is False - setattr(ent, f"_attr_{property}", True) - assert getattr(ent, property) is True + setattr(ent, f"_attr_{property_name}", True) + assert getattr(ent, property_name) is True - delattr(ent, f"_attr_{property}") - assert not hasattr(ent, f"_attr_{property}") - assert getattr(ent, property) is False + delattr(ent, f"_attr_{property_name}") + assert not hasattr(ent, f"_attr_{property_name}") + assert getattr(ent, property_name) is False async def test_cached_entity_property_class_attribute(hass: HomeAssistant) -> None: """Test entity properties on class level work in derived classes.""" - property = "attribution" + property_name = "attribution" values = ["abcd", "efgh"] class EntityWithClassAttribute1(entity.Entity): @@ -2408,15 +2408,15 @@ async def test_cached_entity_property_class_attribute(hass: HomeAssistant) -> No ] for ent in entities: - assert getattr(ent[0], property) == values[0] - assert getattr(ent[1], property) == values[0] + assert getattr(ent[0], property_name) == values[0] + assert getattr(ent[1], property_name) == values[0] # Test update for ent in entities: - setattr(ent[0], f"_attr_{property}", values[1]) + setattr(ent[0], f"_attr_{property_name}", values[1]) for ent in entities: - assert getattr(ent[0], property) == values[1] - assert getattr(ent[1], property) == values[0] + assert getattr(ent[0], property_name) == values[1] + assert getattr(ent[1], property_name) == values[0] async def test_cached_entity_property_override(hass: HomeAssistant) -> None: diff --git a/tests/test_core.py b/tests/test_core.py index ce71fcd42e5..30665619fcd 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1148,11 +1148,11 @@ async def test_eventbus_filtered_listener(hass: HomeAssistant) -> None: calls.append(event) @ha.callback - def filter(event_data): + def mock_filter(event_data): """Mock filter.""" return not event_data["filtered"] - unsub = hass.bus.async_listen("test", listener, event_filter=filter) + unsub = hass.bus.async_listen("test", listener, event_filter=mock_filter) hass.bus.async_fire("test", {"filtered": True}) await hass.async_block_till_done() @@ -3274,11 +3274,11 @@ async def test_eventbus_lazy_object_creation(hass: HomeAssistant) -> None: calls.append(event) @ha.callback - def filter(event_data): + def mock_filter(event_data): """Mock filter.""" return not event_data["filtered"] - unsub = hass.bus.async_listen("test_1", listener, event_filter=filter) + unsub = hass.bus.async_listen("test_1", listener, event_filter=mock_filter) # Test lazy creation of Event objects with patch("homeassistant.core.Event") as mock_event: @@ -3343,7 +3343,7 @@ async def test_statemachine_report_state(hass: HomeAssistant) -> None: """Test report state event.""" @ha.callback - def filter(event_data): + def mock_filter(event_data): """Mock filter.""" return True @@ -3354,7 +3354,7 @@ async def test_statemachine_report_state(hass: HomeAssistant) -> None: hass.states.async_set("light.bowl", "on", {}) state_changed_events = async_capture_events(hass, EVENT_STATE_CHANGED) state_reported_events = [] - hass.bus.async_listen(EVENT_STATE_REPORTED, listener, event_filter=filter) + hass.bus.async_listen(EVENT_STATE_REPORTED, listener, event_filter=mock_filter) hass.states.async_set("light.bowl", "on") await hass.async_block_till_done() @@ -3385,7 +3385,7 @@ async def test_report_state_listener_restrictions(hass: HomeAssistant) -> None: """Mock listener.""" @ha.callback - def filter(event_data): + def mock_filter(event_data): """Mock filter.""" return False @@ -3394,7 +3394,7 @@ async def test_report_state_listener_restrictions(hass: HomeAssistant) -> None: hass.bus.async_listen(EVENT_STATE_REPORTED, listener) # Both filter and run_immediately - hass.bus.async_listen(EVENT_STATE_REPORTED, listener, event_filter=filter) + hass.bus.async_listen(EVENT_STATE_REPORTED, listener, event_filter=mock_filter) @pytest.mark.parametrize( diff --git a/tests/util/test_percentage.py b/tests/util/test_percentage.py index 2fc054fb4f1..3af42310e94 100644 --- a/tests/util/test_percentage.py +++ b/tests/util/test_percentage.py @@ -104,77 +104,77 @@ async def test_percentage_to_ordered_list_item() -> None: async def test_ranged_value_to_percentage_large() -> None: """Test a large range of low and high values convert a single value to a percentage.""" - range = (1, 255) + value_range = (1, 255) - assert ranged_value_to_percentage(range, 255) == 100 - assert ranged_value_to_percentage(range, 127) == 49 - assert ranged_value_to_percentage(range, 10) == 3 - assert ranged_value_to_percentage(range, 1) == 0 + assert ranged_value_to_percentage(value_range, 255) == 100 + assert ranged_value_to_percentage(value_range, 127) == 49 + assert ranged_value_to_percentage(value_range, 10) == 3 + assert ranged_value_to_percentage(value_range, 1) == 0 async def test_percentage_to_ranged_value_large() -> None: """Test a large range of low and high values convert a percentage to a single value.""" - range = (1, 255) + value_range = (1, 255) - assert percentage_to_ranged_value(range, 100) == 255 - assert percentage_to_ranged_value(range, 50) == 127.5 - assert percentage_to_ranged_value(range, 4) == 10.2 + assert percentage_to_ranged_value(value_range, 100) == 255 + assert percentage_to_ranged_value(value_range, 50) == 127.5 + assert percentage_to_ranged_value(value_range, 4) == 10.2 - assert math.ceil(percentage_to_ranged_value(range, 100)) == 255 - assert math.ceil(percentage_to_ranged_value(range, 50)) == 128 - assert math.ceil(percentage_to_ranged_value(range, 4)) == 11 + assert math.ceil(percentage_to_ranged_value(value_range, 100)) == 255 + assert math.ceil(percentage_to_ranged_value(value_range, 50)) == 128 + assert math.ceil(percentage_to_ranged_value(value_range, 4)) == 11 async def test_ranged_value_to_percentage_small() -> None: """Test a small range of low and high values convert a single value to a percentage.""" - range = (1, 6) + value_range = (1, 6) - assert ranged_value_to_percentage(range, 1) == 16 - assert ranged_value_to_percentage(range, 2) == 33 - assert ranged_value_to_percentage(range, 3) == 50 - assert ranged_value_to_percentage(range, 4) == 66 - assert ranged_value_to_percentage(range, 5) == 83 - assert ranged_value_to_percentage(range, 6) == 100 + assert ranged_value_to_percentage(value_range, 1) == 16 + assert ranged_value_to_percentage(value_range, 2) == 33 + assert ranged_value_to_percentage(value_range, 3) == 50 + assert ranged_value_to_percentage(value_range, 4) == 66 + assert ranged_value_to_percentage(value_range, 5) == 83 + assert ranged_value_to_percentage(value_range, 6) == 100 async def test_percentage_to_ranged_value_small() -> None: """Test a small range of low and high values convert a percentage to a single value.""" - range = (1, 6) + value_range = (1, 6) - assert math.ceil(percentage_to_ranged_value(range, 16)) == 1 - assert math.ceil(percentage_to_ranged_value(range, 33)) == 2 - assert math.ceil(percentage_to_ranged_value(range, 50)) == 3 - assert math.ceil(percentage_to_ranged_value(range, 66)) == 4 - assert math.ceil(percentage_to_ranged_value(range, 83)) == 5 - assert math.ceil(percentage_to_ranged_value(range, 100)) == 6 + assert math.ceil(percentage_to_ranged_value(value_range, 16)) == 1 + assert math.ceil(percentage_to_ranged_value(value_range, 33)) == 2 + assert math.ceil(percentage_to_ranged_value(value_range, 50)) == 3 + assert math.ceil(percentage_to_ranged_value(value_range, 66)) == 4 + assert math.ceil(percentage_to_ranged_value(value_range, 83)) == 5 + assert math.ceil(percentage_to_ranged_value(value_range, 100)) == 6 async def test_ranged_value_to_percentage_starting_at_one() -> None: """Test a range that starts with 1.""" - range = (1, 4) + value_range = (1, 4) - assert ranged_value_to_percentage(range, 1) == 25 - assert ranged_value_to_percentage(range, 2) == 50 - assert ranged_value_to_percentage(range, 3) == 75 - assert ranged_value_to_percentage(range, 4) == 100 + assert ranged_value_to_percentage(value_range, 1) == 25 + assert ranged_value_to_percentage(value_range, 2) == 50 + assert ranged_value_to_percentage(value_range, 3) == 75 + assert ranged_value_to_percentage(value_range, 4) == 100 async def test_ranged_value_to_percentage_starting_high() -> None: """Test a range that does not start with 1.""" - range = (101, 255) + value_range = (101, 255) - assert ranged_value_to_percentage(range, 101) == 0 - assert ranged_value_to_percentage(range, 139) == 25 - assert ranged_value_to_percentage(range, 178) == 50 - assert ranged_value_to_percentage(range, 217) == 75 - assert ranged_value_to_percentage(range, 255) == 100 + assert ranged_value_to_percentage(value_range, 101) == 0 + assert ranged_value_to_percentage(value_range, 139) == 25 + assert ranged_value_to_percentage(value_range, 178) == 50 + assert ranged_value_to_percentage(value_range, 217) == 75 + assert ranged_value_to_percentage(value_range, 255) == 100 async def test_ranged_value_to_percentage_starting_zero() -> None: """Test a range that starts with 0.""" - range = (0, 3) + value_range = (0, 3) - assert ranged_value_to_percentage(range, 0) == 25 - assert ranged_value_to_percentage(range, 1) == 50 - assert ranged_value_to_percentage(range, 2) == 75 - assert ranged_value_to_percentage(range, 3) == 100 + assert ranged_value_to_percentage(value_range, 0) == 25 + assert ranged_value_to_percentage(value_range, 1) == 50 + assert ranged_value_to_percentage(value_range, 2) == 75 + assert ranged_value_to_percentage(value_range, 3) == 100 diff --git a/tests/util/yaml/test_init.py b/tests/util/yaml/test_init.py index 113a348c1d1..f17489e1488 100644 --- a/tests/util/yaml/test_init.py +++ b/tests/util/yaml/test_init.py @@ -568,13 +568,13 @@ def test_no_recursive_secrets( def test_input_class() -> None: """Test input class.""" - input = yaml_loader.Input("hello") - input2 = yaml_loader.Input("hello") + yaml_input = yaml_loader.Input("hello") + yaml_input2 = yaml_loader.Input("hello") - assert input.name == "hello" - assert input == input2 + assert yaml_input.name == "hello" + assert yaml_input == yaml_input2 - assert len({input, input2}) == 1 + assert len({yaml_input, yaml_input2}) == 1 def test_input(try_both_loaders, try_both_dumpers) -> None: From f26ac465b5ce0e4ca36caf770c38d31c078fb2d0 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 21 Apr 2024 23:38:32 +0200 Subject: [PATCH 734/967] Introduce base entity for totalconnect (#115936) --- .../totalconnect/alarm_control_panel.py | 6 ++-- .../components/totalconnect/binary_sensor.py | 33 +++++------------ .../components/totalconnect/entity.py | 35 +++++++++++++++++++ 3 files changed, 46 insertions(+), 28 deletions(-) create mode 100644 homeassistant/components/totalconnect/entity.py diff --git a/homeassistant/components/totalconnect/alarm_control_panel.py b/homeassistant/components/totalconnect/alarm_control_panel.py index 436e3198650..fcafd47037d 100644 --- a/homeassistant/components/totalconnect/alarm_control_panel.py +++ b/homeassistant/components/totalconnect/alarm_control_panel.py @@ -23,10 +23,10 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_platform from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import TotalConnectDataUpdateCoordinator from .const import DOMAIN +from .entity import TotalConnectEntity SERVICE_ALARM_ARM_AWAY_INSTANT = "arm_away_instant" SERVICE_ALARM_ARM_HOME_INSTANT = "arm_home_instant" @@ -70,9 +70,7 @@ async def async_setup_entry( ) -class TotalConnectAlarm( - CoordinatorEntity[TotalConnectDataUpdateCoordinator], alarm.AlarmControlPanelEntity -): +class TotalConnectAlarm(TotalConnectEntity, alarm.AlarmControlPanelEntity): """Represent an TotalConnect status.""" _attr_supported_features = ( diff --git a/homeassistant/components/totalconnect/binary_sensor.py b/homeassistant/components/totalconnect/binary_sensor.py index 696f0dbcf6f..18340d5d6d3 100644 --- a/homeassistant/components/totalconnect/binary_sensor.py +++ b/homeassistant/components/totalconnect/binary_sensor.py @@ -15,12 +15,11 @@ from homeassistant.components.binary_sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory 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 . import TotalConnectDataUpdateCoordinator from .const import DOMAIN +from .entity import TotalConnectEntity, TotalConnectZoneEntity LOW_BATTERY = "low_battery" TAMPER = "tamper" @@ -129,7 +128,7 @@ async def async_setup_entry( for zone in location.zones.values(): sensors.append( TotalConnectZoneBinarySensor( - coordinator, SECURITY_BINARY_SENSOR, location_id, zone + coordinator, SECURITY_BINARY_SENSOR, zone, location_id ) ) @@ -138,8 +137,8 @@ async def async_setup_entry( TotalConnectZoneBinarySensor( coordinator, description, - location_id, zone, + location_id, ) for description in NO_BUTTON_BINARY_SENSORS ) @@ -147,10 +146,8 @@ async def async_setup_entry( async_add_entities(sensors) -class TotalConnectZoneBinarySensor( - CoordinatorEntity[TotalConnectDataUpdateCoordinator], BinarySensorEntity -): - """Represent an TotalConnect zone.""" +class TotalConnectZoneBinarySensor(TotalConnectZoneEntity, BinarySensorEntity): + """Represent a TotalConnect zone.""" entity_description: TotalConnectZoneBinarySensorEntityDescription @@ -158,28 +155,18 @@ class TotalConnectZoneBinarySensor( self, coordinator: TotalConnectDataUpdateCoordinator, entity_description: TotalConnectZoneBinarySensorEntityDescription, - location_id: str, zone: TotalConnectZone, + location_id: str, ) -> None: """Initialize the TotalConnect status.""" - super().__init__(coordinator) + super().__init__(coordinator, zone, location_id, entity_description.key) self.entity_description = entity_description - self._location_id = location_id - self._zone = zone self._attr_name = f"{zone.description}{entity_description.name}" - self._attr_unique_id = f"{location_id}_{zone.zoneid}_{entity_description.key}" - self._attr_is_on = None self._attr_extra_state_attributes = { "zone_id": zone.zoneid, - "location_id": self._location_id, + "location_id": location_id, "partition": zone.partition, } - identifier = zone.sensor_serial_number or f"zone_{zone.zoneid}" - self._attr_device_info = DeviceInfo( - name=zone.description, - identifiers={(DOMAIN, identifier)}, - serial_number=zone.sensor_serial_number, - ) @property def is_on(self) -> bool: @@ -194,9 +181,7 @@ class TotalConnectZoneBinarySensor( return super().device_class -class TotalConnectAlarmBinarySensor( - CoordinatorEntity[TotalConnectDataUpdateCoordinator], BinarySensorEntity -): +class TotalConnectAlarmBinarySensor(TotalConnectEntity, BinarySensorEntity): """Represent a TotalConnect alarm device binary sensors.""" entity_description: TotalConnectAlarmBinarySensorEntityDescription diff --git a/homeassistant/components/totalconnect/entity.py b/homeassistant/components/totalconnect/entity.py new file mode 100644 index 00000000000..e7ab4b3575c --- /dev/null +++ b/homeassistant/components/totalconnect/entity.py @@ -0,0 +1,35 @@ +"""Base class for TotalConnect entities.""" + +from total_connect_client.zone import TotalConnectZone + +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import DOMAIN, TotalConnectDataUpdateCoordinator + + +class TotalConnectEntity(CoordinatorEntity[TotalConnectDataUpdateCoordinator]): + """Represent a TotalConnect entity.""" + + +class TotalConnectZoneEntity(TotalConnectEntity): + """Represent a TotalConnect zone.""" + + def __init__( + self, + coordinator: TotalConnectDataUpdateCoordinator, + zone: TotalConnectZone, + location_id: str, + key: str, + ) -> None: + """Initialize the TotalConnect zone.""" + super().__init__(coordinator) + self._location_id = location_id + self._zone = zone + self._attr_unique_id = f"{location_id}_{zone.zoneid}_{key}" + identifier = zone.sensor_serial_number or f"zone_{zone.zoneid}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, identifier)}, + name=zone.description, + serial_number=zone.sensor_serial_number, + ) From e29b301dd1dc0b94baf0001677e20218333a8b3d Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 21 Apr 2024 15:52:47 -0700 Subject: [PATCH 735/967] Bump ical to 8.0.0 (#115907) Co-authored-by: J. Nick Koston --- 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 00561cb5fd6..ac43dc58953 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==7.0.3"] + "requirements": ["gcal-sync==6.0.4", "oauth2client==4.1.3", "ical==8.0.0"] } diff --git a/homeassistant/components/local_calendar/manifest.json b/homeassistant/components/local_calendar/manifest.json index 1c13970503d..b1c7d6a3a34 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==7.0.3"] + "requirements": ["ical==8.0.0"] } diff --git a/homeassistant/components/local_todo/manifest.json b/homeassistant/components/local_todo/manifest.json index 3bcb8af9f43..44c76a56a8f 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==7.0.3"] + "requirements": ["ical==8.0.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4a4ef23b583..dade5079fbd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1113,7 +1113,7 @@ ibmiotf==0.3.4 # homeassistant.components.google # homeassistant.components.local_calendar # homeassistant.components.local_todo -ical==7.0.3 +ical==8.0.0 # homeassistant.components.ping icmplib==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b935fcbaf42..d1bfeff488f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -906,7 +906,7 @@ ibeacon-ble==1.2.0 # homeassistant.components.google # homeassistant.components.local_calendar # homeassistant.components.local_todo -ical==7.0.3 +ical==8.0.0 # homeassistant.components.ping icmplib==3.0 From 70d4b4d20d950c7bfcef261f3d24656e30d8ae4b Mon Sep 17 00:00:00 2001 From: andarotajo <55669170+andarotajo@users.noreply.github.com> Date: Mon, 22 Apr 2024 09:13:09 +0200 Subject: [PATCH 736/967] Add optional location based region to dwd_weather_warnings (#96027) * Add device tracker option * Update const name to be more understandable * Clean up sensor code * Clean up init and coordinator * Add tests and update util function and it's usage * Switch to using the registry entry and add tests * Clean up code * Consolidate duplicate code and adjust tests * Fix runtime error * Fix blocking of the event loop * Adjust API object handling * Update homeassistant/components/dwd_weather_warnings/exceptions.py * Optimize coordinator data update --------- Co-authored-by: Erik Montnemery --- .../dwd_weather_warnings/__init__.py | 11 +- .../dwd_weather_warnings/config_flow.py | 75 +++++++-- .../components/dwd_weather_warnings/const.py | 1 + .../dwd_weather_warnings/coordinator.py | 66 +++++++- .../dwd_weather_warnings/exceptions.py | 7 + .../components/dwd_weather_warnings/sensor.py | 24 +-- .../dwd_weather_warnings/strings.json | 13 +- .../components/dwd_weather_warnings/util.py | 39 +++++ .../dwd_weather_warnings/test_config_flow.py | 143 ++++++++++++++++-- .../dwd_weather_warnings/test_init.py | 87 ++++++++++- 10 files changed, 403 insertions(+), 63 deletions(-) create mode 100644 homeassistant/components/dwd_weather_warnings/exceptions.py create mode 100644 homeassistant/components/dwd_weather_warnings/util.py diff --git a/homeassistant/components/dwd_weather_warnings/__init__.py b/homeassistant/components/dwd_weather_warnings/__init__.py index 275d47d15ca..9cf73a90a73 100644 --- a/homeassistant/components/dwd_weather_warnings/__init__.py +++ b/homeassistant/components/dwd_weather_warnings/__init__.py @@ -2,23 +2,16 @@ from __future__ import annotations -from dwdwfsapi import DwdWeatherWarningsAPI - from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .const import CONF_REGION_IDENTIFIER, DOMAIN, PLATFORMS +from .const import DOMAIN, PLATFORMS from .coordinator import DwdWeatherWarningsCoordinator async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - region_identifier: str = entry.data[CONF_REGION_IDENTIFIER] - - # Initialize the API and coordinator. - api = await hass.async_add_executor_job(DwdWeatherWarningsAPI, region_identifier) - coordinator = DwdWeatherWarningsCoordinator(hass, api) - + coordinator = DwdWeatherWarningsCoordinator(hass, entry) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator diff --git a/homeassistant/components/dwd_weather_warnings/config_flow.py b/homeassistant/components/dwd_weather_warnings/config_flow.py index 5076dbae187..f148f4e05ac 100644 --- a/homeassistant/components/dwd_weather_warnings/config_flow.py +++ b/homeassistant/components/dwd_weather_warnings/config_flow.py @@ -8,9 +8,15 @@ from dwdwfsapi import DwdWeatherWarningsAPI import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.helpers import entity_registry as er import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.selector import EntitySelector, EntitySelectorConfig -from .const import CONF_REGION_IDENTIFIER, DOMAIN +from .const import CONF_REGION_DEVICE_TRACKER, CONF_REGION_IDENTIFIER, DOMAIN +from .exceptions import EntityNotFoundError +from .util import get_position_data + +EXCLUSIVE_OPTIONS = (CONF_REGION_IDENTIFIER, CONF_REGION_DEVICE_TRACKER) class DwdWeatherWarningsConfigFlow(ConfigFlow, domain=DOMAIN): @@ -25,27 +31,70 @@ class DwdWeatherWarningsConfigFlow(ConfigFlow, domain=DOMAIN): errors: dict = {} if user_input is not None: - region_identifier = user_input[CONF_REGION_IDENTIFIER] + # Check, if either CONF_REGION_IDENTIFIER or CONF_GPS_TRACKER has been set. + if all(k not in user_input for k in EXCLUSIVE_OPTIONS): + errors["base"] = "no_identifier" + elif all(k in user_input for k in EXCLUSIVE_OPTIONS): + errors["base"] = "ambiguous_identifier" + elif CONF_REGION_IDENTIFIER in user_input: + # Validate region identifier using the API + identifier = user_input[CONF_REGION_IDENTIFIER] - # Validate region identifier using the API - if not await self.hass.async_add_executor_job( - DwdWeatherWarningsAPI, region_identifier - ): - errors["base"] = "invalid_identifier" + if not await self.hass.async_add_executor_job( + DwdWeatherWarningsAPI, identifier + ): + errors["base"] = "invalid_identifier" - if not errors: - # Set the unique ID for this config entry. - await self.async_set_unique_id(region_identifier) - self._abort_if_unique_id_configured() + if not errors: + # Set the unique ID for this config entry. + await self.async_set_unique_id(identifier) + self._abort_if_unique_id_configured() - return self.async_create_entry(title=region_identifier, data=user_input) + return self.async_create_entry(title=identifier, data=user_input) + else: # CONF_REGION_DEVICE_TRACKER + device_tracker = user_input[CONF_REGION_DEVICE_TRACKER] + registry = er.async_get(self.hass) + entity_entry = registry.async_get(device_tracker) + + if entity_entry is None: + errors["base"] = "entity_not_found" + else: + try: + position = get_position_data(self.hass, entity_entry.id) + except EntityNotFoundError: + errors["base"] = "entity_not_found" + except AttributeError: + errors["base"] = "attribute_not_found" + else: + # Validate position using the API + if not await self.hass.async_add_executor_job( + DwdWeatherWarningsAPI, position + ): + errors["base"] = "invalid_identifier" + + # Position is valid here, because the API call was successful. + if not errors and position is not None and entity_entry is not None: + # Set the unique ID for this config entry. + await self.async_set_unique_id(entity_entry.id) + self._abort_if_unique_id_configured() + + # Replace entity ID with registry ID for more stability. + user_input[CONF_REGION_DEVICE_TRACKER] = entity_entry.id + + return self.async_create_entry( + title=device_tracker.removeprefix("device_tracker."), + data=user_input, + ) return self.async_show_form( step_id="user", errors=errors, data_schema=vol.Schema( { - vol.Required(CONF_REGION_IDENTIFIER): cv.string, + vol.Optional(CONF_REGION_IDENTIFIER): cv.string, + vol.Optional(CONF_REGION_DEVICE_TRACKER): EntitySelector( + EntitySelectorConfig(domain="device_tracker") + ), } ), ) diff --git a/homeassistant/components/dwd_weather_warnings/const.py b/homeassistant/components/dwd_weather_warnings/const.py index 75969dee119..4f0a6767660 100644 --- a/homeassistant/components/dwd_weather_warnings/const.py +++ b/homeassistant/components/dwd_weather_warnings/const.py @@ -14,6 +14,7 @@ DOMAIN: Final = "dwd_weather_warnings" CONF_REGION_NAME: Final = "region_name" CONF_REGION_IDENTIFIER: Final = "region_identifier" +CONF_REGION_DEVICE_TRACKER: Final = "region_device_tracker" ATTR_REGION_NAME: Final = "region_name" ATTR_REGION_ID: Final = "region_id" diff --git a/homeassistant/components/dwd_weather_warnings/coordinator.py b/homeassistant/components/dwd_weather_warnings/coordinator.py index a1232697130..465a7c09750 100644 --- a/homeassistant/components/dwd_weather_warnings/coordinator.py +++ b/homeassistant/components/dwd_weather_warnings/coordinator.py @@ -4,23 +4,79 @@ from __future__ import annotations from dwdwfsapi import DwdWeatherWarningsAPI +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util import location -from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER +from .const import ( + CONF_REGION_DEVICE_TRACKER, + CONF_REGION_IDENTIFIER, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + LOGGER, +) +from .exceptions import EntityNotFoundError +from .util import get_position_data class DwdWeatherWarningsCoordinator(DataUpdateCoordinator[None]): """Custom coordinator for the dwd_weather_warnings integration.""" - def __init__(self, hass: HomeAssistant, api: DwdWeatherWarningsAPI) -> None: + config_entry: ConfigEntry + api: DwdWeatherWarningsAPI + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: """Initialize the dwd_weather_warnings coordinator.""" super().__init__( hass, LOGGER, name=DOMAIN, update_interval=DEFAULT_SCAN_INTERVAL ) - self.api = api + self._device_tracker = None + self._previous_position = None + + async def async_config_entry_first_refresh(self) -> None: + """Perform first refresh.""" + if region_identifier := self.config_entry.data.get(CONF_REGION_IDENTIFIER): + self.api = await self.hass.async_add_executor_job( + DwdWeatherWarningsAPI, region_identifier + ) + else: + self._device_tracker = self.config_entry.data.get( + CONF_REGION_DEVICE_TRACKER + ) + + await super().async_config_entry_first_refresh() async def _async_update_data(self) -> None: """Get the latest data from the DWD Weather Warnings API.""" - await self.hass.async_add_executor_job(self.api.update) + if self._device_tracker: + try: + position = get_position_data(self.hass, self._device_tracker) + except (EntityNotFoundError, AttributeError) as err: + raise UpdateFailed(f"Error fetching position: {repr(err)}") from err + + distance = None + if self._previous_position is not None: + distance = location.distance( + self._previous_position[0], + self._previous_position[1], + position[0], + position[1], + ) + + if distance is None or distance > 50: + # Only create a new object on the first update + # or when the distance to the previous position + # changes by more than 50 meters (to take GPS + # inaccuracy into account). + self.api = await self.hass.async_add_executor_job( + DwdWeatherWarningsAPI, position + ) + else: + # Otherwise update the API to check for new warnings. + await self.hass.async_add_executor_job(self.api.update) + + self._previous_position = position + else: + await self.hass.async_add_executor_job(self.api.update) diff --git a/homeassistant/components/dwd_weather_warnings/exceptions.py b/homeassistant/components/dwd_weather_warnings/exceptions.py new file mode 100644 index 00000000000..cd61cfa6bae --- /dev/null +++ b/homeassistant/components/dwd_weather_warnings/exceptions.py @@ -0,0 +1,7 @@ +"""Exceptions for the dwd_weather_warnings integration.""" + +from homeassistant.exceptions import HomeAssistantError + + +class EntityNotFoundError(HomeAssistantError): + """When a referenced entity was not found.""" diff --git a/homeassistant/components/dwd_weather_warnings/sensor.py b/homeassistant/components/dwd_weather_warnings/sensor.py index d3e3b4a3772..d62c0f4f192 100644 --- a/homeassistant/components/dwd_weather_warnings/sensor.py +++ b/homeassistant/components/dwd_weather_warnings/sensor.py @@ -11,6 +11,8 @@ Wetterwarnungen (Stufe 1) from __future__ import annotations +from typing import Any + from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -93,29 +95,27 @@ class DwdWeatherWarningsSensor( entry_type=DeviceEntryType.SERVICE, ) - self.api = coordinator.api - @property - def native_value(self): + def native_value(self) -> int | None: """Return the state of the sensor.""" if self.entity_description.key == CURRENT_WARNING_SENSOR: - return self.api.current_warning_level + return self.coordinator.api.current_warning_level - return self.api.expected_warning_level + return self.coordinator.api.expected_warning_level @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the sensor.""" data = { - ATTR_REGION_NAME: self.api.warncell_name, - ATTR_REGION_ID: self.api.warncell_id, - ATTR_LAST_UPDATE: self.api.last_update, + ATTR_REGION_NAME: self.coordinator.api.warncell_name, + ATTR_REGION_ID: self.coordinator.api.warncell_id, + ATTR_LAST_UPDATE: self.coordinator.api.last_update, } if self.entity_description.key == CURRENT_WARNING_SENSOR: - searched_warnings = self.api.current_warnings + searched_warnings = self.coordinator.api.current_warnings else: - searched_warnings = self.api.expected_warnings + searched_warnings = self.coordinator.api.expected_warnings data[ATTR_WARNING_COUNT] = len(searched_warnings) @@ -142,4 +142,4 @@ class DwdWeatherWarningsSensor( @property def available(self) -> bool: """Could the device be accessed during the last update call.""" - return self.api.data_valid + return self.coordinator.api.data_valid diff --git a/homeassistant/components/dwd_weather_warnings/strings.json b/homeassistant/components/dwd_weather_warnings/strings.json index aa460dcc6d5..3f421d338a7 100644 --- a/homeassistant/components/dwd_weather_warnings/strings.json +++ b/homeassistant/components/dwd_weather_warnings/strings.json @@ -2,17 +2,22 @@ "config": { "step": { "user": { - "description": "To identify the desired region, the warncell ID / name is required.", + "description": "To identify the desired region, either the warncell ID / name or device tracker is required. The provided device tracker has to contain the attributes 'latitude' and 'longitude'.", "data": { - "region_identifier": "Warncell ID or name" + "region_identifier": "Warncell ID or name", + "region_device_tracker": "Device tracker entity" } } }, "error": { - "invalid_identifier": "The specified region identifier is invalid." + "no_identifier": "Either the region identifier or device tracker is required.", + "ambiguous_identifier": "The region identifier and device tracker can not be specified together.", + "invalid_identifier": "The specified region identifier / device tracker is invalid.", + "entity_not_found": "The specified device tracker entity was not found.", + "attribute_not_found": "The required `latitude` or `longitude` attribute was not found in the specified device tracker." }, "abort": { - "already_configured": "Warncell ID / name is already configured.", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "invalid_identifier": "[%key:component::dwd_weather_warnings::config::error::invalid_identifier%]" } }, diff --git a/homeassistant/components/dwd_weather_warnings/util.py b/homeassistant/components/dwd_weather_warnings/util.py new file mode 100644 index 00000000000..730ebf4b71e --- /dev/null +++ b/homeassistant/components/dwd_weather_warnings/util.py @@ -0,0 +1,39 @@ +"""Util functions for the dwd_weather_warnings integration.""" + +from __future__ import annotations + +from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from .exceptions import EntityNotFoundError + + +def get_position_data( + hass: HomeAssistant, registry_id: str +) -> tuple[float, float] | None: + """Extract longitude and latitude from a device tracker.""" + registry = er.async_get(hass) + registry_entry = registry.async_get(registry_id) + if registry_entry is None: + raise EntityNotFoundError(f"Failed to find registry entry {registry_id}") + + entity = hass.states.get(registry_entry.entity_id) + if entity is None: + raise EntityNotFoundError(f"Failed to find entity {registry_entry.entity_id}") + + latitude = entity.attributes.get(ATTR_LATITUDE) + if not latitude: + raise AttributeError( + f"Failed to find attribute '{ATTR_LATITUDE}' in {registry_entry.entity_id}", + ATTR_LATITUDE, + ) + + longitude = entity.attributes.get(ATTR_LONGITUDE) + if not longitude: + raise AttributeError( + f"Failed to find attribute '{ATTR_LONGITUDE}' in {registry_entry.entity_id}", + ATTR_LONGITUDE, + ) + + return (latitude, longitude) diff --git a/tests/components/dwd_weather_warnings/test_config_flow.py b/tests/components/dwd_weather_warnings/test_config_flow.py index 3558ff5ed93..119c029767a 100644 --- a/tests/components/dwd_weather_warnings/test_config_flow.py +++ b/tests/components/dwd_weather_warnings/test_config_flow.py @@ -6,34 +6,31 @@ from unittest.mock import patch import pytest from homeassistant.components.dwd_weather_warnings.const import ( - ADVANCE_WARNING_SENSOR, + CONF_REGION_DEVICE_TRACKER, CONF_REGION_IDENTIFIER, - CONF_REGION_NAME, - CURRENT_WARNING_SENSOR, DOMAIN, ) from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_NAME +from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, STATE_HOME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry -DEMO_CONFIG_ENTRY: Final = { +DEMO_CONFIG_ENTRY_REGION: Final = { CONF_REGION_IDENTIFIER: "807111000", } -DEMO_YAML_CONFIGURATION: Final = { - CONF_NAME: "Unit Test", - CONF_REGION_NAME: "807111000", - CONF_MONITORED_CONDITIONS: [CURRENT_WARNING_SENSOR, ADVANCE_WARNING_SENSOR], +DEMO_CONFIG_ENTRY_GPS: Final = { + CONF_REGION_DEVICE_TRACKER: "device_tracker.test_gps", } pytestmark = pytest.mark.usefixtures("mock_setup_entry") -async def test_create_entry(hass: HomeAssistant) -> None: - """Test that the full config flow works.""" +async def test_create_entry_region(hass: HomeAssistant) -> None: + """Test that the full config flow works for a region identifier.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) @@ -45,7 +42,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: return_value=False, ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=DEMO_CONFIG_ENTRY + result["flow_id"], user_input=DEMO_CONFIG_ENTRY_REGION ) # Test for invalid region identifier. @@ -58,7 +55,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: return_value=True, ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=DEMO_CONFIG_ENTRY + result["flow_id"], user_input=DEMO_CONFIG_ENTRY_REGION ) # Test for successfully created entry. @@ -70,12 +67,95 @@ async def test_create_entry(hass: HomeAssistant) -> None: } +async def test_create_entry_gps( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: + """Test that the full config flow works for a device tracker.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + + # Test for missing registry entry error. + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=DEMO_CONFIG_ENTRY_GPS + ) + + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "entity_not_found"} + + # Test for missing device tracker error. + registry_entry = entity_registry.async_get_or_create( + "device_tracker", DOMAIN, "uuid", suggested_object_id="test_gps" + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=DEMO_CONFIG_ENTRY_GPS + ) + + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "entity_not_found"} + + # Test for missing attribute error. + hass.states.async_set( + DEMO_CONFIG_ENTRY_GPS[CONF_REGION_DEVICE_TRACKER], + STATE_HOME, + {ATTR_LONGITUDE: "7.610263"}, + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=DEMO_CONFIG_ENTRY_GPS + ) + + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "attribute_not_found"} + + # Test for invalid provided identifier. + hass.states.async_set( + DEMO_CONFIG_ENTRY_GPS[CONF_REGION_DEVICE_TRACKER], + STATE_HOME, + {ATTR_LATITUDE: "50.180454", ATTR_LONGITUDE: "7.610263"}, + ) + + with patch( + "homeassistant.components.dwd_weather_warnings.config_flow.DwdWeatherWarningsAPI", + return_value=False, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=DEMO_CONFIG_ENTRY_GPS + ) + + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "invalid_identifier"} + + # Test for successfully created entry. + with patch( + "homeassistant.components.dwd_weather_warnings.config_flow.DwdWeatherWarningsAPI", + return_value=True, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=DEMO_CONFIG_ENTRY_GPS + ) + + await hass.async_block_till_done() + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "test_gps" + assert result["data"] == { + CONF_REGION_DEVICE_TRACKER: registry_entry.id, + } + + async def test_config_flow_already_configured(hass: HomeAssistant) -> None: """Test aborting, if the warncell ID / name is already configured during the config.""" entry = MockConfigEntry( domain=DOMAIN, - data=DEMO_CONFIG_ENTRY.copy(), - unique_id=DEMO_CONFIG_ENTRY[CONF_REGION_IDENTIFIER], + data=DEMO_CONFIG_ENTRY_REGION.copy(), + unique_id=DEMO_CONFIG_ENTRY_REGION[CONF_REGION_IDENTIFIER], ) entry.add_to_hass(hass) @@ -92,9 +172,40 @@ async def test_config_flow_already_configured(hass: HomeAssistant) -> None: return_value=True, ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=DEMO_CONFIG_ENTRY + result["flow_id"], user_input=DEMO_CONFIG_ENTRY_REGION ) await hass.async_block_till_done() assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" + + +async def test_config_flow_with_errors(hass: HomeAssistant) -> None: + """Test error scenarios during the configuration.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + + # Test error for empty input data. + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "no_identifier"} + + # Test error for setting both options during configuration. + demo_input = DEMO_CONFIG_ENTRY_REGION.copy() + demo_input.update(DEMO_CONFIG_ENTRY_GPS.copy()) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=demo_input, + ) + + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "ambiguous_identifier"} diff --git a/tests/components/dwd_weather_warnings/test_init.py b/tests/components/dwd_weather_warnings/test_init.py index db7afaadec9..bfd03b2fdd4 100644 --- a/tests/components/dwd_weather_warnings/test_init.py +++ b/tests/components/dwd_weather_warnings/test_init.py @@ -4,26 +4,40 @@ from typing import Final from homeassistant.components.dwd_weather_warnings.const import ( ADVANCE_WARNING_SENSOR, + CONF_REGION_DEVICE_TRACKER, CONF_REGION_IDENTIFIER, CURRENT_WARNING_SENSOR, DOMAIN, ) from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_NAME +from homeassistant.const import ( + ATTR_LATITUDE, + ATTR_LONGITUDE, + CONF_MONITORED_CONDITIONS, + CONF_NAME, + STATE_HOME, +) from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry -DEMO_CONFIG_ENTRY: Final = { +DEMO_IDENTIFIER_CONFIG_ENTRY: Final = { CONF_NAME: "Unit Test", CONF_REGION_IDENTIFIER: "807111000", CONF_MONITORED_CONDITIONS: [CURRENT_WARNING_SENSOR, ADVANCE_WARNING_SENSOR], } +DEMO_TRACKER_CONFIG_ENTRY: Final = { + CONF_NAME: "Unit Test", + CONF_REGION_DEVICE_TRACKER: "device_tracker.test_gps", + CONF_MONITORED_CONDITIONS: [CURRENT_WARNING_SENSOR, ADVANCE_WARNING_SENSOR], +} + async def test_load_unload_entry(hass: HomeAssistant) -> None: - """Test loading and unloading the integration.""" - entry = MockConfigEntry(domain=DOMAIN, data=DEMO_CONFIG_ENTRY) + """Test loading and unloading the integration with a region identifier based entry.""" + entry = MockConfigEntry(domain=DOMAIN, data=DEMO_IDENTIFIER_CONFIG_ENTRY) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -36,3 +50,68 @@ async def test_load_unload_entry(hass: HomeAssistant) -> None: assert entry.state is ConfigEntryState.NOT_LOADED assert entry.entry_id not in hass.data[DOMAIN] + + +async def test_load_invalid_registry_entry(hass: HomeAssistant) -> None: + """Test loading the integration with an invalid registry entry ID.""" + INVALID_DATA = DEMO_TRACKER_CONFIG_ENTRY.copy() + INVALID_DATA[CONF_REGION_DEVICE_TRACKER] = "invalid_registry_id" + entry = MockConfigEntry(domain=DOMAIN, data=INVALID_DATA) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.SETUP_RETRY + + +async def test_load_missing_device_tracker(hass: HomeAssistant) -> None: + """Test loading the integration with a missing device tracker.""" + entry = MockConfigEntry(domain=DOMAIN, data=DEMO_TRACKER_CONFIG_ENTRY) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.SETUP_RETRY + + +async def test_load_missing_required_attribute(hass: HomeAssistant) -> None: + """Test loading the integration with a device tracker missing a required attribute.""" + entry = MockConfigEntry(domain=DOMAIN, data=DEMO_TRACKER_CONFIG_ENTRY) + entry.add_to_hass(hass) + + hass.states.async_set( + DEMO_TRACKER_CONFIG_ENTRY[CONF_REGION_DEVICE_TRACKER], + STATE_HOME, + {ATTR_LONGITUDE: "7.610263"}, + ) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.SETUP_RETRY + + +async def test_load_valid_device_tracker( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: + """Test loading the integration with a valid device tracker based entry.""" + entry = MockConfigEntry(domain=DOMAIN, data=DEMO_TRACKER_CONFIG_ENTRY) + entry.add_to_hass(hass) + entity_registry.async_get_or_create( + "device_tracker", + entry.domain, + "uuid", + suggested_object_id="test_gps", + config_entry=entry, + ) + + hass.states.async_set( + DEMO_TRACKER_CONFIG_ENTRY[CONF_REGION_DEVICE_TRACKER], + STATE_HOME, + {ATTR_LATITUDE: "50.180454", ATTR_LONGITUDE: "7.610263"}, + ) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.LOADED + assert entry.entry_id in hass.data[DOMAIN] From aeaa1f84c06f5ff027fe51439e973195e469a0aa Mon Sep 17 00:00:00 2001 From: rappenze Date: Mon, 22 Apr 2024 09:29:10 +0200 Subject: [PATCH 737/967] Add type hints in fibaro device (#106874) * Add typings in fibaro device * Fix type hints * Fix type hints * Remove unused method parameter * Improve log message --------- Co-authored-by: Erik Montnemery --- homeassistant/components/fibaro/__init__.py | 29 ++++++++++--------- .../components/fibaro/binary_sensor.py | 4 +-- homeassistant/components/fibaro/cover.py | 6 ++-- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index 2c1405130b4..4c1feb27629 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -454,37 +454,38 @@ class FibaroDevice(Entity): if not fibaro_device.visible: self._attr_entity_registry_visible_default = False - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" self.controller.register(self.fibaro_device.fibaro_id, self._update_callback) - def _update_callback(self): + def _update_callback(self) -> None: """Update the state.""" self.schedule_update_ha_state(True) @property - def level(self): + def level(self) -> int | None: """Get the level of Fibaro device.""" if self.fibaro_device.value.has_value: return self.fibaro_device.value.int_value() return None @property - def level2(self): + def level2(self) -> int | None: """Get the tilt level of Fibaro device.""" if self.fibaro_device.value_2.has_value: return self.fibaro_device.value_2.int_value() return None - def dont_know_message(self, action): + def dont_know_message(self, cmd: str) -> None: """Make a warning in case we don't know how to perform an action.""" _LOGGER.warning( - "Not sure how to setValue: %s (available actions: %s)", + "Not sure how to %s: %s (available actions: %s)", + cmd, str(self.ha_id), str(self.fibaro_device.actions), ) - def set_level(self, level): + def set_level(self, level: int) -> None: """Set the level of Fibaro device.""" self.action("setValue", level) if self.fibaro_device.value.has_value: @@ -492,21 +493,21 @@ class FibaroDevice(Entity): if self.fibaro_device.has_brightness: self.fibaro_device.properties["brightness"] = level - def set_level2(self, level): + def set_level2(self, level: int) -> None: """Set the level2 of Fibaro device.""" self.action("setValue2", level) if self.fibaro_device.value_2.has_value: self.fibaro_device.properties["value2"] = level - def call_turn_on(self): + def call_turn_on(self) -> None: """Turn on the Fibaro device.""" self.action("turnOn") - def call_turn_off(self): + def call_turn_off(self) -> None: """Turn off the Fibaro device.""" self.action("turnOff") - def call_set_color(self, red, green, blue, white): + def call_set_color(self, red: int, green: int, blue: int, white: int) -> None: """Set the color of Fibaro device.""" red = int(max(0, min(255, red))) green = int(max(0, min(255, green))) @@ -516,7 +517,7 @@ class FibaroDevice(Entity): self.fibaro_device.properties["color"] = color_str self.action("setColor", str(red), str(green), str(blue), str(white)) - def action(self, cmd, *args): + def action(self, cmd: str, *args: Any) -> None: """Perform an action on the Fibaro HC.""" if cmd in self.fibaro_device.actions: self.fibaro_device.execute_action(cmd, args) @@ -525,12 +526,12 @@ class FibaroDevice(Entity): self.dont_know_message(cmd) @property - def current_binary_state(self): + def current_binary_state(self) -> bool: """Return the current binary state.""" return self.fibaro_device.value.bool_value(False) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> Mapping[str, Any]: """Return the state attributes of the device.""" attr = {"fibaro_id": self.fibaro_device.fibaro_id} diff --git a/homeassistant/components/fibaro/binary_sensor.py b/homeassistant/components/fibaro/binary_sensor.py index c0980025555..3c965c11b34 100644 --- a/homeassistant/components/fibaro/binary_sensor.py +++ b/homeassistant/components/fibaro/binary_sensor.py @@ -76,9 +76,9 @@ class FibaroBinarySensor(FibaroDevice, BinarySensorEntity): self._attr_icon = SENSOR_TYPES[self._fibaro_sensor_type][1] @property - def extra_state_attributes(self) -> Mapping[str, Any] | None: + def extra_state_attributes(self) -> Mapping[str, Any]: """Return the extra state attributes of the device.""" - return super().extra_state_attributes | self._own_extra_state_attributes + return {**super().extra_state_attributes, **self._own_extra_state_attributes} def update(self) -> None: """Get the latest data and update the state.""" diff --git a/homeassistant/components/fibaro/cover.py b/homeassistant/components/fibaro/cover.py index 16be6e98ae1..e71ae8982e7 100644 --- a/homeassistant/components/fibaro/cover.py +++ b/homeassistant/components/fibaro/cover.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Any +from typing import Any, cast from pyfibaro.fibaro_device import DeviceModel @@ -80,11 +80,11 @@ class FibaroCover(FibaroDevice, CoverEntity): def set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" - self.set_level(kwargs.get(ATTR_POSITION)) + self.set_level(cast(int, kwargs.get(ATTR_POSITION))) def set_cover_tilt_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" - self.set_level2(kwargs.get(ATTR_TILT_POSITION)) + self.set_level2(cast(int, kwargs.get(ATTR_TILT_POSITION))) @property def is_closed(self) -> bool | None: From de75f8223517029abc1fe94af731a15a55e08df1 Mon Sep 17 00:00:00 2001 From: rappenze Date: Mon, 22 Apr 2024 09:29:58 +0200 Subject: [PATCH 738/967] Refactor fibaro connect (#106875) * Refactor fibaro connect * Remove obsolete test * Add comment about ignored return value --- homeassistant/components/fibaro/__init__.py | 21 +++++-------- .../components/fibaro/config_flow.py | 11 ++----- tests/components/fibaro/test_config_flow.py | 30 ------------------- 3 files changed, 9 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index 4c1feb27629..5b7908ddf08 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -108,26 +108,21 @@ class FibaroController: # Device infos by fibaro device id self._device_infos: dict[int, DeviceInfo] = {} - def connect(self) -> bool: + def connect(self) -> None: """Start the communication with the Fibaro controller.""" - connected = self._client.connect() + # Return value doesn't need to be checked, + # it is only relevant when connecting without credentials + self._client.connect() info = self._client.read_info() self.hub_serial = info.serial_number self.hub_name = info.hc_name self.hub_model = info.platform self.hub_software_version = info.current_version - if connected is False: - _LOGGER.error( - "Invalid login for Fibaro HC. Please check username and password" - ) - return False - self._room_map = {room.fibaro_id: room for room in self._client.read_rooms()} self._read_devices() self._scenes = self._client.read_scenes() - return True def connect_with_error_handling(self) -> None: """Translate connect errors to easily differentiate auth and connect failures. @@ -135,9 +130,7 @@ class FibaroController: When there is a better error handling in the used library this can be improved. """ try: - connected = self.connect() - if not connected: - raise FibaroConnectFailed("Connect status is false") + self.connect() except HTTPError as http_ex: if http_ex.response.status_code == 403: raise FibaroAuthFailed from http_ex @@ -382,7 +375,7 @@ class FibaroController: pass -def _init_controller(data: Mapping[str, Any]) -> FibaroController: +def init_controller(data: Mapping[str, Any]) -> FibaroController: """Validate the user input allows us to connect to fibaro.""" controller = FibaroController(data) controller.connect_with_error_handling() @@ -395,7 +388,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: The unique id of the config entry is the serial number of the home center. """ try: - controller = await hass.async_add_executor_job(_init_controller, entry.data) + controller = await hass.async_add_executor_job(init_controller, entry.data) except FibaroConnectFailed as connect_ex: raise ConfigEntryNotReady( f"Could not connect to controller at {entry.data[CONF_URL]}" diff --git a/homeassistant/components/fibaro/config_flow.py b/homeassistant/components/fibaro/config_flow.py index 8c2fb502488..9003704348d 100644 --- a/homeassistant/components/fibaro/config_flow.py +++ b/homeassistant/components/fibaro/config_flow.py @@ -13,7 +13,7 @@ from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResu from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME from homeassistant.core import HomeAssistant -from . import FibaroAuthFailed, FibaroConnectFailed, FibaroController +from . import FibaroAuthFailed, FibaroConnectFailed, init_controller from .const import CONF_IMPORT_PLUGINS, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -28,19 +28,12 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) -def _connect_to_fibaro(data: dict[str, Any]) -> FibaroController: - """Validate the user input allows us to connect to fibaro.""" - controller = FibaroController(data) - controller.connect_with_error_handling() - return controller - - async def _validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: """Validate the user input allows us to connect. Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. """ - controller = await hass.async_add_executor_job(_connect_to_fibaro, data) + controller = await hass.async_add_executor_job(init_controller, data) _LOGGER.debug( "Successfully connected to fibaro home center %s with name %s", diff --git a/tests/components/fibaro/test_config_flow.py b/tests/components/fibaro/test_config_flow.py index dcf5f12a24a..b6b4e3992cd 100644 --- a/tests/components/fibaro/test_config_flow.py +++ b/tests/components/fibaro/test_config_flow.py @@ -89,36 +89,6 @@ async def test_config_flow_user_initiated_success(hass: HomeAssistant) -> None: } -async def test_config_flow_user_initiated_connect_failure( - hass: HomeAssistant, mock_fibaro_client: Mock -) -> None: - """Connect failure in flow manually initialized by the user.""" - 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"] == "user" - assert result["errors"] == {} - - mock_fibaro_client.connect.return_value = False - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_URL: TEST_URL, - CONF_USERNAME: TEST_USERNAME, - CONF_PASSWORD: TEST_PASSWORD, - }, - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "user" - assert result["errors"] == {"base": "cannot_connect"} - - await _recovery_after_failure_works(hass, mock_fibaro_client, result) - - async def test_config_flow_user_initiated_auth_failure( hass: HomeAssistant, mock_fibaro_client: Mock ) -> None: From 66ea528e94222f33d081cf60919acf764827d45c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Apr 2024 09:43:19 +0200 Subject: [PATCH 739/967] Bump actions/checkout from 4.1.2 to 4.1.3 (#115945) 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 9d992608317..a440de225be 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.2 + uses: actions/checkout@v4.1.3 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.2 + uses: actions/checkout@v4.1.3 - 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.2 + uses: actions/checkout@v4.1.3 - 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.2 + uses: actions/checkout@v4.1.3 - 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.2 + uses: actions/checkout@v4.1.3 - name: Install Cosign uses: sigstore/cosign-installer@v3.4.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.2 + uses: actions/checkout@v4.1.3 - 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 5f186c32e9a..581a36be953 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.2 + uses: actions/checkout@v4.1.3 - name: Generate partial Python venv restore key id: generate_python_cache_key run: >- @@ -223,7 +223,7 @@ jobs: - info steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v5.1.0 @@ -269,7 +269,7 @@ jobs: - pre-commit steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v5.1.0 id: python @@ -309,7 +309,7 @@ jobs: - pre-commit steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v5.1.0 id: python @@ -348,7 +348,7 @@ jobs: - pre-commit steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v5.1.0 id: python @@ -442,7 +442,7 @@ jobs: python-version: ${{ fromJSON(needs.info.outputs.python_versions) }} steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Set up Python ${{ matrix.python-version }} id: python uses: actions/setup-python@v5.1.0 @@ -513,7 +513,7 @@ jobs: - base steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v5.1.0 @@ -545,7 +545,7 @@ jobs: - base steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v5.1.0 @@ -578,7 +578,7 @@ jobs: - base steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v5.1.0 @@ -622,7 +622,7 @@ jobs: - base steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v5.1.0 @@ -694,7 +694,7 @@ jobs: ffmpeg \ libgammu-dev - name: Check out code from GitHub - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v5.1.0 @@ -754,7 +754,7 @@ jobs: ffmpeg \ libgammu-dev - name: Check out code from GitHub - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Set up Python ${{ matrix.python-version }} id: python uses: actions/setup-python@v5.1.0 @@ -869,7 +869,7 @@ jobs: ffmpeg \ libmariadb-dev-compat - name: Check out code from GitHub - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Set up Python ${{ matrix.python-version }} id: python uses: actions/setup-python@v5.1.0 @@ -991,7 +991,7 @@ jobs: ffmpeg \ postgresql-server-dev-14 - name: Check out code from GitHub - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Set up Python ${{ matrix.python-version }} id: python uses: actions/setup-python@v5.1.0 @@ -1086,7 +1086,7 @@ jobs: timeout-minutes: 10 steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Download all coverage artifacts uses: actions/download-artifact@v4.1.5 with: @@ -1132,7 +1132,7 @@ jobs: ffmpeg \ libgammu-dev - name: Check out code from GitHub - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Set up Python ${{ matrix.python-version }} id: python uses: actions/setup-python@v5.1.0 @@ -1219,7 +1219,7 @@ jobs: timeout-minutes: 10 steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Download all coverage artifacts uses: actions/download-artifact@v4.1.5 with: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 2b9a2af127f..6a366a7ab8d 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.2 + uses: actions/checkout@v4.1.3 - name: Initialize CodeQL uses: github/codeql-action/init@v3.25.1 diff --git a/.github/workflows/translations.yml b/.github/workflows/translations.yml index e61eef36f0b..3f0559de541 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.2 + uses: actions/checkout@v4.1.3 - 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 36a9fa1f839..24033a92fd5 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -28,7 +28,7 @@ jobs: architectures: ${{ steps.info.outputs.architectures }} steps: - name: Checkout the repository - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Get information id: info @@ -88,7 +88,7 @@ jobs: arch: ${{ fromJson(needs.init.outputs.architectures) }} steps: - name: Checkout the repository - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Download env_file uses: actions/download-artifact@v4.1.5 @@ -126,7 +126,7 @@ jobs: arch: ${{ fromJson(needs.init.outputs.architectures) }} steps: - name: Checkout the repository - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Download env_file uses: actions/download-artifact@v4.1.5 From f927b27ed4ddfa497b47c11b989e325919e93512 Mon Sep 17 00:00:00 2001 From: Quentame Date: Mon, 22 Apr 2024 09:54:47 +0200 Subject: [PATCH 740/967] Add Epic Games Store integration (#104725) * Add Epic Games Store integration Squashed commit of the following PR: #81167 * Bump epicstore-api to 0.1.7 as it handle better error 1004 Thanks to https://github.com/SD4RK/epicstore_api/commit/d7469f7c99508c06b3867fecbcf291ebf86c4c72 * Use extra_state_attributes instead of overriding state_attributes * Review: change how config_flow.validate_input is handled * Use LanguageSelector and rename locale to language * Review: init-better use of hass.data.setdefault Co-authored-by: Joost Lekkerkerker * Review: don't need to update at init Co-authored-by: Joost Lekkerkerker * Revert "Review: don't need to update at init" --> not working otherwise This reverts commit 1445a87c8e9b7247f1c9835bf2e2d7297dd02586. * Review: fix config_flow.validate_input/retactor following lib bump * review: merge async_update function with event property Co-authored-by: Joost Lekkerkerker * hassfest * Fix duplicates data from applied comment review 5035055 * review: thanks to 5035055 async_add_entities update_before_add param is not required anymore Co-authored-by: Joost Lekkerkerker * Fix Christmas special "Holiday sale" case * gen_requirements_all * Use CONF_LANGUAGE from HA const * Move CalendarType to const * manifest: integration_type -> service Co-authored-by: Sid <27780930+autinerd@users.noreply.github.com> * calendar: remove date start/end assert Co-authored-by: Erik Montnemery * const: rename SUPPORTED_LANGUAGES * hassfest * config: Move to ConfigFlowResult * coordinator: main file comment Co-authored-by: Erik Montnemery * ruff & hassfest * review: do not guess country * Add @hacf-fr as codeowner * review: remove games extra_attrs Was dropped somehow: - 73c20f34803b0a0ec242bf0740494f17a68f6f59 review: move games extra_attrs to data service - other commit that removed the service part * review: remove unused error class was removed: - 040cf945bb5346b6d42b3782b5061a13fb7b1f6b --------- Co-authored-by: Joost Lekkerkerker Co-authored-by: Sid <27780930+autinerd@users.noreply.github.com> Co-authored-by: Erik Montnemery --- .coveragerc | 2 + CODEOWNERS | 2 + .../components/epic_games_store/__init__.py | 35 + .../components/epic_games_store/calendar.py | 97 + .../epic_games_store/config_flow.py | 96 + .../components/epic_games_store/const.py | 31 + .../epic_games_store/coordinator.py | 81 + .../components/epic_games_store/helper.py | 92 + .../components/epic_games_store/manifest.json | 10 + .../components/epic_games_store/strings.json | 38 + homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/epic_games_store/__init__.py | 1 + tests/components/epic_games_store/common.py | 31 + tests/components/epic_games_store/conftest.py | 44 + tests/components/epic_games_store/const.py | 25 + .../error_1004_attribute_not_found.json | 1026 ++++++++ .../fixtures/error_5222_wrong_country.json | 23 + .../epic_games_store/fixtures/free_games.json | 2189 +++++++++++++++++ .../free_games_christmas_special.json | 253 ++ .../fixtures/free_games_one.json | 658 +++++ .../epic_games_store/test_calendar.py | 162 ++ .../epic_games_store/test_config_flow.py | 142 ++ .../epic_games_store/test_helper.py | 74 + 26 files changed, 5125 insertions(+) create mode 100644 homeassistant/components/epic_games_store/__init__.py create mode 100644 homeassistant/components/epic_games_store/calendar.py create mode 100644 homeassistant/components/epic_games_store/config_flow.py create mode 100644 homeassistant/components/epic_games_store/const.py create mode 100644 homeassistant/components/epic_games_store/coordinator.py create mode 100644 homeassistant/components/epic_games_store/helper.py create mode 100644 homeassistant/components/epic_games_store/manifest.json create mode 100644 homeassistant/components/epic_games_store/strings.json create mode 100644 tests/components/epic_games_store/__init__.py create mode 100644 tests/components/epic_games_store/common.py create mode 100644 tests/components/epic_games_store/conftest.py create mode 100644 tests/components/epic_games_store/const.py create mode 100644 tests/components/epic_games_store/fixtures/error_1004_attribute_not_found.json create mode 100644 tests/components/epic_games_store/fixtures/error_5222_wrong_country.json create mode 100644 tests/components/epic_games_store/fixtures/free_games.json create mode 100644 tests/components/epic_games_store/fixtures/free_games_christmas_special.json create mode 100644 tests/components/epic_games_store/fixtures/free_games_one.json create mode 100644 tests/components/epic_games_store/test_calendar.py create mode 100644 tests/components/epic_games_store/test_config_flow.py create mode 100644 tests/components/epic_games_store/test_helper.py diff --git a/.coveragerc b/.coveragerc index ceff3384202..f6368de7d89 100644 --- a/.coveragerc +++ b/.coveragerc @@ -361,6 +361,8 @@ omit = homeassistant/components/environment_canada/weather.py homeassistant/components/envisalink/* homeassistant/components/ephember/climate.py + homeassistant/components/epic_games_store/__init__.py + homeassistant/components/epic_games_store/coordinator.py homeassistant/components/epion/__init__.py homeassistant/components/epion/coordinator.py homeassistant/components/epion/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index ef997cfa896..5dcf4b3df81 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -398,6 +398,8 @@ build.json @home-assistant/supervisor /homeassistant/components/environment_canada/ @gwww @michaeldavie /tests/components/environment_canada/ @gwww @michaeldavie /homeassistant/components/ephember/ @ttroy50 +/homeassistant/components/epic_games_store/ @hacf-fr @Quentame +/tests/components/epic_games_store/ @hacf-fr @Quentame /homeassistant/components/epion/ @lhgravendeel /tests/components/epion/ @lhgravendeel /homeassistant/components/epson/ @pszafer diff --git a/homeassistant/components/epic_games_store/__init__.py b/homeassistant/components/epic_games_store/__init__.py new file mode 100644 index 00000000000..af25eb98137 --- /dev/null +++ b/homeassistant/components/epic_games_store/__init__.py @@ -0,0 +1,35 @@ +"""The Epic Games Store integration.""" + +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .coordinator import EGSCalendarUpdateCoordinator + +PLATFORMS: list[Platform] = [ + Platform.CALENDAR, +] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Epic Games Store from a config entry.""" + + coordinator = EGSCalendarUpdateCoordinator(hass, entry) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = 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.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/epic_games_store/calendar.py b/homeassistant/components/epic_games_store/calendar.py new file mode 100644 index 00000000000..75c448e8467 --- /dev/null +++ b/homeassistant/components/epic_games_store/calendar.py @@ -0,0 +1,97 @@ +"""Calendar platform for a Epic Games Store.""" + +from __future__ import annotations + +from collections import namedtuple +from datetime import datetime +from typing import Any + +from homeassistant.components.calendar import CalendarEntity, CalendarEvent +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 .const import DOMAIN, CalendarType +from .coordinator import EGSCalendarUpdateCoordinator + +DateRange = namedtuple("DateRange", ["start", "end"]) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the local calendar platform.""" + coordinator: EGSCalendarUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + entities = [ + EGSCalendar(coordinator, entry.entry_id, CalendarType.FREE), + EGSCalendar(coordinator, entry.entry_id, CalendarType.DISCOUNT), + ] + async_add_entities(entities) + + +class EGSCalendar(CoordinatorEntity[EGSCalendarUpdateCoordinator], CalendarEntity): + """A calendar entity by Epic Games Store.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: EGSCalendarUpdateCoordinator, + config_entry_id: str, + cal_type: CalendarType, + ) -> None: + """Initialize EGSCalendar.""" + super().__init__(coordinator) + self._cal_type = cal_type + self._attr_translation_key = f"{cal_type}_games" + self._attr_unique_id = f"{config_entry_id}-{cal_type}" + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, config_entry_id)}, + manufacturer="Epic Games Store", + name="Epic Games Store", + ) + + @property + def event(self) -> CalendarEvent | None: + """Return the next upcoming event.""" + if event := self.coordinator.data[self._cal_type]: + return _get_calendar_event(event[0]) + return None + + async def async_get_events( + self, hass: HomeAssistant, start_date: datetime, end_date: datetime + ) -> list[CalendarEvent]: + """Get all events in a specific time frame.""" + events = filter( + lambda game: _are_date_range_overlapping( + DateRange(start=game["discount_start_at"], end=game["discount_end_at"]), + DateRange(start=start_date, end=end_date), + ), + self.coordinator.data[self._cal_type], + ) + return [_get_calendar_event(event) for event in events] + + +def _get_calendar_event(event: dict[str, Any]) -> CalendarEvent: + """Return a CalendarEvent from an API event.""" + return CalendarEvent( + summary=event["title"], + start=event["discount_start_at"], + end=event["discount_end_at"], + description=f"{event['description']}\n\n{event['url']}", + ) + + +def _are_date_range_overlapping(range1: DateRange, range2: DateRange) -> bool: + """Return a CalendarEvent from an API event.""" + latest_start = max(range1.start, range2.start) + earliest_end = min(range1.end, range2.end) + delta = (earliest_end - latest_start).days + 1 + overlap = max(0, delta) + return overlap > 0 diff --git a/homeassistant/components/epic_games_store/config_flow.py b/homeassistant/components/epic_games_store/config_flow.py new file mode 100644 index 00000000000..2ae86060ba2 --- /dev/null +++ b/homeassistant/components/epic_games_store/config_flow.py @@ -0,0 +1,96 @@ +"""Config flow for Epic Games Store integration.""" + +from __future__ import annotations + +import logging +from typing import Any + +from epicstore_api import EpicGamesStoreAPI +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlowResult +from homeassistant.const import CONF_COUNTRY, CONF_LANGUAGE +from homeassistant.core import HomeAssistant +from homeassistant.helpers.selector import ( + CountrySelector, + LanguageSelector, + LanguageSelectorConfig, +) + +from .const import DOMAIN, SUPPORTED_LANGUAGES + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_LANGUAGE): LanguageSelector( + LanguageSelectorConfig(languages=SUPPORTED_LANGUAGES) + ), + vol.Required(CONF_COUNTRY): CountrySelector(), + } +) + + +def get_default_language(hass: HomeAssistant) -> str | None: + """Get default language code based on Home Assistant config.""" + language_code = f"{hass.config.language}-{hass.config.country}" + if language_code in SUPPORTED_LANGUAGES: + return language_code + if hass.config.language in SUPPORTED_LANGUAGES: + return hass.config.language + return None + + +async def validate_input(hass: HomeAssistant, user_input: dict[str, Any]) -> None: + """Validate the user input allows us to connect.""" + api = EpicGamesStoreAPI(user_input[CONF_LANGUAGE], user_input[CONF_COUNTRY]) + data = await hass.async_add_executor_job(api.get_free_games) + + if data.get("errors"): + _LOGGER.warning(data["errors"]) + + assert data["data"]["Catalog"]["searchStore"]["elements"] + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Epic Games Store.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the initial step.""" + data_schema = self.add_suggested_values_to_schema( + STEP_USER_DATA_SCHEMA, + user_input + or { + CONF_LANGUAGE: get_default_language(self.hass), + CONF_COUNTRY: self.hass.config.country, + }, + ) + if user_input is None: + return self.async_show_form(step_id="user", data_schema=data_schema) + + await self.async_set_unique_id( + f"freegames-{user_input[CONF_LANGUAGE]}-{user_input[CONF_COUNTRY]}" + ) + self._abort_if_unique_id_configured() + + errors = {} + + try: + await validate_input(self.hass, user_input) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_create_entry( + title=f"Epic Games Store - Free Games ({user_input[CONF_LANGUAGE]}-{user_input[CONF_COUNTRY]})", + data=user_input, + ) + + return self.async_show_form( + step_id="user", data_schema=data_schema, errors=errors + ) diff --git a/homeassistant/components/epic_games_store/const.py b/homeassistant/components/epic_games_store/const.py new file mode 100644 index 00000000000..c397698fd0c --- /dev/null +++ b/homeassistant/components/epic_games_store/const.py @@ -0,0 +1,31 @@ +"""Constants for the Epic Games Store integration.""" + +from enum import StrEnum + +DOMAIN = "epic_games_store" + +SUPPORTED_LANGUAGES = [ + "ar", + "de", + "en-US", + "es-ES", + "es-MX", + "fr", + "it", + "ja", + "ko", + "pl", + "pt-BR", + "ru", + "th", + "tr", + "zh-CN", + "zh-Hant", +] + + +class CalendarType(StrEnum): + """Calendar types.""" + + FREE = "free" + DISCOUNT = "discount" diff --git a/homeassistant/components/epic_games_store/coordinator.py b/homeassistant/components/epic_games_store/coordinator.py new file mode 100644 index 00000000000..d9c48f5da02 --- /dev/null +++ b/homeassistant/components/epic_games_store/coordinator.py @@ -0,0 +1,81 @@ +"""The Epic Games Store integration data coordinator.""" + +from __future__ import annotations + +from datetime import timedelta +import logging +from typing import Any + +from epicstore_api import EpicGamesStoreAPI + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_COUNTRY, CONF_LANGUAGE +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN, CalendarType +from .helper import format_game_data + +SCAN_INTERVAL = timedelta(days=1) + +_LOGGER = logging.getLogger(__name__) + + +class EGSCalendarUpdateCoordinator( + DataUpdateCoordinator[dict[str, list[dict[str, Any]]]] +): + """Class to manage fetching data from the Epic Game Store.""" + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Initialize.""" + self._api = EpicGamesStoreAPI( + entry.data[CONF_LANGUAGE], + entry.data[CONF_COUNTRY], + ) + self.language = entry.data[CONF_LANGUAGE] + + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=SCAN_INTERVAL, + ) + + async def _async_update_data(self) -> dict[str, list[dict[str, Any]]]: + """Update data via library.""" + raw_data = await self.hass.async_add_executor_job(self._api.get_free_games) + _LOGGER.debug(raw_data) + data = raw_data["data"]["Catalog"]["searchStore"]["elements"] + + discount_games = filter( + lambda game: game.get("promotions") + and ( + # Current discount(s) + game["promotions"]["promotionalOffers"] + or + # Upcoming discount(s) + game["promotions"]["upcomingPromotionalOffers"] + ), + data, + ) + + return_data: dict[str, list[dict[str, Any]]] = { + CalendarType.DISCOUNT: [], + CalendarType.FREE: [], + } + for discount_game in discount_games: + game = format_game_data(discount_game, self.language) + + if game["discount_type"]: + return_data[game["discount_type"]].append(game) + + return_data[CalendarType.DISCOUNT] = sorted( + return_data[CalendarType.DISCOUNT], + key=lambda game: game["discount_start_at"], + ) + return_data[CalendarType.FREE] = sorted( + return_data[CalendarType.FREE], key=lambda game: game["discount_start_at"] + ) + + _LOGGER.debug(return_data) + return return_data diff --git a/homeassistant/components/epic_games_store/helper.py b/homeassistant/components/epic_games_store/helper.py new file mode 100644 index 00000000000..2510c7699e5 --- /dev/null +++ b/homeassistant/components/epic_games_store/helper.py @@ -0,0 +1,92 @@ +"""Helper for Epic Games Store.""" + +import contextlib +from typing import Any + +from homeassistant.util import dt as dt_util + + +def format_game_data(raw_game_data: dict[str, Any], language: str) -> dict[str, Any]: + """Format raw API game data for Home Assistant users.""" + img_portrait = None + img_landscape = None + + for image in raw_game_data["keyImages"]: + if image["type"] == "OfferImageTall": + img_portrait = image["url"] + if image["type"] == "OfferImageWide": + img_landscape = image["url"] + + current_promotions = raw_game_data["promotions"]["promotionalOffers"] + upcoming_promotions = raw_game_data["promotions"]["upcomingPromotionalOffers"] + + promotion_data = {} + if ( + current_promotions + and raw_game_data["price"]["totalPrice"]["discountPrice"] == 0 + ): + promotion_data = current_promotions[0]["promotionalOffers"][0] + else: + promotion_data = (current_promotions or upcoming_promotions)[0][ + "promotionalOffers" + ][0] + + return { + "title": raw_game_data["title"].replace("\xa0", " "), + "description": raw_game_data["description"].strip().replace("\xa0", " "), + "released_at": dt_util.parse_datetime(raw_game_data["effectiveDate"]), + "original_price": raw_game_data["price"]["totalPrice"]["fmtPrice"][ + "originalPrice" + ].replace("\xa0", " "), + "publisher": raw_game_data["seller"]["name"], + "url": get_game_url(raw_game_data, language), + "img_portrait": img_portrait, + "img_landscape": img_landscape, + "discount_type": ("free" if is_free_game(raw_game_data) else "discount") + if promotion_data + else None, + "discount_start_at": dt_util.parse_datetime(promotion_data["startDate"]) + if promotion_data + else None, + "discount_end_at": dt_util.parse_datetime(promotion_data["endDate"]) + if promotion_data + else None, + } + + +def get_game_url(raw_game_data: dict[str, Any], language: str) -> str: + """Format raw API game data for Home Assistant users.""" + url_bundle_or_product = "bundles" if raw_game_data["offerType"] == "BUNDLE" else "p" + url_slug: str | None = None + try: + url_slug = raw_game_data["offerMappings"][0]["pageSlug"] + except Exception: # pylint: disable=broad-except + with contextlib.suppress(Exception): + url_slug = raw_game_data["catalogNs"]["mappings"][0]["pageSlug"] + + if not url_slug: + url_slug = raw_game_data["urlSlug"] + + return f"https://store.epicgames.com/{language}/{url_bundle_or_product}/{url_slug}" + + +def is_free_game(game: dict[str, Any]) -> bool: + """Return if the game is free or will be free.""" + return ( + # Current free game(s) + game["promotions"]["promotionalOffers"] + and game["promotions"]["promotionalOffers"][0]["promotionalOffers"][0][ + "discountSetting" + ]["discountPercentage"] + == 0 + and + # Checking current price, maybe not necessary + game["price"]["totalPrice"]["discountPrice"] == 0 + ) or ( + # Upcoming free game(s) + game["promotions"]["upcomingPromotionalOffers"] + and game["promotions"]["upcomingPromotionalOffers"][0]["promotionalOffers"][0][ + "discountSetting" + ]["discountPercentage"] + == 0 + ) diff --git a/homeassistant/components/epic_games_store/manifest.json b/homeassistant/components/epic_games_store/manifest.json new file mode 100644 index 00000000000..665eaec6668 --- /dev/null +++ b/homeassistant/components/epic_games_store/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "epic_games_store", + "name": "Epic Games Store", + "codeowners": ["@hacf-fr", "@Quentame"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/epic_games_store", + "integration_type": "service", + "iot_class": "cloud_polling", + "requirements": ["epicstore-api==0.1.7"] +} diff --git a/homeassistant/components/epic_games_store/strings.json b/homeassistant/components/epic_games_store/strings.json new file mode 100644 index 00000000000..58a87a55f81 --- /dev/null +++ b/homeassistant/components/epic_games_store/strings.json @@ -0,0 +1,38 @@ +{ + "config": { + "step": { + "user": { + "data": { + "language": "Language", + "country": "Country" + } + } + }, + "error": { + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + } + }, + "entity": { + "calendar": { + "free_games": { + "name": "Free games", + "state_attributes": { + "games": { + "name": "Games" + } + } + }, + "discount_games": { + "name": "Discount games", + "state_attributes": { + "games": { + "name": "[%key:component::epic_games_store::entity::calendar::free_games::state_attributes::games::name%]" + } + } + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index c02d8a2987e..e5d5f37ad5a 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -152,6 +152,7 @@ FLOWS = { "enocean", "enphase_envoy", "environment_canada", + "epic_games_store", "epion", "epson", "eq3btsmart", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 2b1e5b4fb91..0ee796d5376 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1649,6 +1649,12 @@ "config_flow": false, "iot_class": "local_polling" }, + "epic_games_store": { + "name": "Epic Games Store", + "integration_type": "service", + "config_flow": true, + "iot_class": "cloud_polling" + }, "epion": { "name": "Epion", "integration_type": "hub", diff --git a/requirements_all.txt b/requirements_all.txt index dade5079fbd..055db11d63a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -806,6 +806,9 @@ env-canada==0.6.0 # homeassistant.components.season ephem==4.1.5 +# homeassistant.components.epic_games_store +epicstore-api==0.1.7 + # homeassistant.components.epion epion==0.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d1bfeff488f..ff19a6a5c89 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -660,6 +660,9 @@ env-canada==0.6.0 # homeassistant.components.season ephem==4.1.5 +# homeassistant.components.epic_games_store +epicstore-api==0.1.7 + # homeassistant.components.epion epion==0.0.3 diff --git a/tests/components/epic_games_store/__init__.py b/tests/components/epic_games_store/__init__.py new file mode 100644 index 00000000000..1c5baf3704f --- /dev/null +++ b/tests/components/epic_games_store/__init__.py @@ -0,0 +1 @@ +"""Tests for the Epic Games Store integration.""" diff --git a/tests/components/epic_games_store/common.py b/tests/components/epic_games_store/common.py new file mode 100644 index 00000000000..95191ad97f9 --- /dev/null +++ b/tests/components/epic_games_store/common.py @@ -0,0 +1,31 @@ +"""Common methods used across tests for Epic Games Store.""" + +from unittest.mock import patch + +from homeassistant.components.epic_games_store.const import DOMAIN +from homeassistant.const import CONF_COUNTRY, CONF_LANGUAGE +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from .const import MOCK_COUNTRY, MOCK_LANGUAGE + +from tests.common import MockConfigEntry + + +async def setup_platform(hass: HomeAssistant, platform: str) -> MockConfigEntry: + """Set up the Epic Games Store platform.""" + mock_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_LANGUAGE: MOCK_LANGUAGE, + CONF_COUNTRY: MOCK_COUNTRY, + }, + unique_id=f"freegames-{MOCK_LANGUAGE}-{MOCK_COUNTRY}", + ) + mock_entry.add_to_hass(hass) + + with patch("homeassistant.components.epic_games_store.PLATFORMS", [platform]): + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + return mock_entry diff --git a/tests/components/epic_games_store/conftest.py b/tests/components/epic_games_store/conftest.py new file mode 100644 index 00000000000..e02997a429e --- /dev/null +++ b/tests/components/epic_games_store/conftest.py @@ -0,0 +1,44 @@ +"""Define fixtures for Epic Games Store tests.""" + +from unittest.mock import Mock, patch + +import pytest + +from .const import ( + DATA_ERROR_ATTRIBUTE_NOT_FOUND, + DATA_FREE_GAMES, + DATA_FREE_GAMES_CHRISTMAS_SPECIAL, +) + + +@pytest.fixture(name="service_multiple") +def mock_service_multiple(): + """Mock a successful service with multiple free & discount games.""" + with patch( + "homeassistant.components.epic_games_store.coordinator.EpicGamesStoreAPI" + ) as service_mock: + instance = service_mock.return_value + instance.get_free_games = Mock(return_value=DATA_FREE_GAMES) + yield service_mock + + +@pytest.fixture(name="service_christmas_special") +def mock_service_christmas_special(): + """Mock a successful service with Christmas special case.""" + with patch( + "homeassistant.components.epic_games_store.coordinator.EpicGamesStoreAPI" + ) as service_mock: + instance = service_mock.return_value + instance.get_free_games = Mock(return_value=DATA_FREE_GAMES_CHRISTMAS_SPECIAL) + yield service_mock + + +@pytest.fixture(name="service_attribute_not_found") +def mock_service_attribute_not_found(): + """Mock a successful service returning a not found attribute error with free & discount games.""" + with patch( + "homeassistant.components.epic_games_store.coordinator.EpicGamesStoreAPI" + ) as service_mock: + instance = service_mock.return_value + instance.get_free_games = Mock(return_value=DATA_ERROR_ATTRIBUTE_NOT_FOUND) + yield service_mock diff --git a/tests/components/epic_games_store/const.py b/tests/components/epic_games_store/const.py new file mode 100644 index 00000000000..dcd82c7e03e --- /dev/null +++ b/tests/components/epic_games_store/const.py @@ -0,0 +1,25 @@ +"""Test constants.""" + +from homeassistant.components.epic_games_store.const import DOMAIN + +from tests.common import load_json_object_fixture + +MOCK_LANGUAGE = "fr" +MOCK_COUNTRY = "FR" + +DATA_ERROR_ATTRIBUTE_NOT_FOUND = load_json_object_fixture( + "error_1004_attribute_not_found.json", DOMAIN +) + +DATA_ERROR_WRONG_COUNTRY = load_json_object_fixture( + "error_5222_wrong_country.json", DOMAIN +) + +# free games +DATA_FREE_GAMES = load_json_object_fixture("free_games.json", DOMAIN) + +DATA_FREE_GAMES_ONE = load_json_object_fixture("free_games_one.json", DOMAIN) + +DATA_FREE_GAMES_CHRISTMAS_SPECIAL = load_json_object_fixture( + "free_games_christmas_special.json", DOMAIN +) diff --git a/tests/components/epic_games_store/fixtures/error_1004_attribute_not_found.json b/tests/components/epic_games_store/fixtures/error_1004_attribute_not_found.json new file mode 100644 index 00000000000..6cb14608c2b --- /dev/null +++ b/tests/components/epic_games_store/fixtures/error_1004_attribute_not_found.json @@ -0,0 +1,1026 @@ +{ + "errors": [ + { + "message": "CatalogOffer/offerMappings: Request failed with status code 404", + "locations": [ + { + "line": 73, + "column": 17 + } + ], + "correlationId": "0451aa13-b1d6-4f90-8ca5-d12bf917675a", + "serviceResponse": "{\"errorMessage\":\"The item or resource being requested could not be found.\",\"errorCode\":\"errors.com.epicgames.not_found\",\"numericErrorCode\":1004,\"errorStatus\":404}", + "stack": null, + "path": ["Catalog", "searchStore", "elements", 4, "offerMappings"] + }, + { + "message": "CatalogNamespace/mappings: Request failed with status code 404", + "locations": [ + { + "line": 68, + "column": 19 + } + ], + "correlationId": "0451aa13-b1d6-4f90-8ca5-d12bf917675a", + "serviceResponse": "{\"errorMessage\":\"The item or resource being requested could not be found.\",\"errorCode\":\"errors.com.epicgames.not_found\",\"numericErrorCode\":1004,\"errorStatus\":404}", + "stack": null, + "path": ["Catalog", "searchStore", "elements", 4, "catalogNs", "mappings"] + } + ], + "data": { + "Catalog": { + "searchStore": { + "elements": [ + { + "title": "Godlike Burger", + "id": "d9300ace164b41ac90a7b54e59d47953", + "namespace": "beb7e64d3da74ae780405da48cccb581", + "description": "Dans Godlike Burger, vous g\u00e9rez le restaurant le plus d\u00e9ment de la galaxie\u00a0! Assommez, empoisonnez et tuez les clients... pour les transformer en steaks\u00a0! Mais nulle crainte\u00a0: la client\u00e8le alien reviendra si vous la jouez fine, car c'est trop bon de s'adonner au cannibalisme.", + "effectiveDate": "2022-04-21T17:00:00.000Z", + "offerType": "BASE_GAME", + "expiryDate": null, + "viewableDate": "2022-03-28T18:00:00.000Z", + "status": "ACTIVE", + "isCodeRedemptionOnly": false, + "keyImages": [ + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/spt-assets/f42598038b9343e58d27e0a8c0b831b6/godlike-burger-offer-1trpc.jpg" + }, + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/spt-assets/f42598038b9343e58d27e0a8c0b831b6/download-godlike-burger-offer-8u2uh.jpg" + }, + { + "type": "Thumbnail", + "url": "https://cdn1.epicgames.com/spt-assets/f42598038b9343e58d27e0a8c0b831b6/download-godlike-burger-offer-8u2uh.jpg" + } + ], + "seller": { + "id": "o-d2ygr9bjcjfebgt8842wvvbmswympz", + "name": "Daedalic Entertainment" + }, + "productSlug": null, + "urlSlug": "37b001690e2a4d6f872567cdd06f0c6f", + "url": null, + "items": [ + { + "id": "c027f1bc9db54f189ad938634500e542", + "namespace": "beb7e64d3da74ae780405da48cccb581" + } + ], + "customAttributes": [ + { + "key": "autoGeneratedPrice", + "value": "false" + }, + { + "key": "isManuallySetPCReleaseDate", + "value": "false" + } + ], + "categories": [ + { + "path": "freegames" + }, + { + "path": "games" + }, + { + "path": "games/edition" + }, + { + "path": "games/edition/base" + } + ], + "tags": [ + { + "id": "1216" + }, + { + "id": "21894" + }, + { + "id": "19847" + }, + { + "id": "1083" + }, + { + "id": "9547" + }, + { + "id": "9549" + }, + { + "id": "1263" + }, + { + "id": "10719" + } + ], + "catalogNs": { + "mappings": [ + { + "pageSlug": "godlike-burger-4150a0", + "pageType": "productHome" + } + ] + }, + "offerMappings": [ + { + "pageSlug": "godlike-burger-4150a0", + "pageType": "productHome" + } + ], + "price": { + "totalPrice": { + "discountPrice": 0, + "originalPrice": 1999, + "voucherDiscount": 0, + "discount": 1999, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "19,99\u00a0\u20ac", + "discountPrice": "0", + "intermediatePrice": "0" + } + }, + "lineOffers": [ + { + "appliedRules": [ + { + "id": "1c2dc8194022428da305eedb42ed574d", + "endDate": "2023-10-12T15:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE" + } + } + ] + } + ] + }, + "promotions": { + "promotionalOffers": [ + { + "promotionalOffers": [ + { + "startDate": "2023-10-05T15:00:00.000Z", + "endDate": "2023-10-12T15:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 0 + } + } + ] + } + ], + "upcomingPromotionalOffers": [] + } + }, + { + "title": "Destiny\u00a02\u00a0: Pack 30e anniversaire Bungie", + "id": "e7b9e222c7274dd28714aba2e06d2a01", + "namespace": "428115def4ca4deea9d69c99c5a5a99e", + "description": "Le Pack 30e anniversaire inclut un nouveau donjon, le lance-roquettes exotique Gjallarhorn, de nouvelles armes et armures, et plus encore. ", + "effectiveDate": "2022-08-23T13:00:00.000Z", + "offerType": "DLC", + "expiryDate": null, + "viewableDate": "2022-08-08T15:00:00.000Z", + "status": "ACTIVE", + "isCodeRedemptionOnly": false, + "keyImages": [ + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/offer/428115def4ca4deea9d69c99c5a5a99e/FR_Bungie_Bungie_30th_Anniversary_Pack_S4_1200x1600_1200x1600-04ebd49752c682d003014680f3d5be18" + }, + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/offer/428115def4ca4deea9d69c99c5a5a99e/FR_Bungie_Bungie_30th_Anniversary_Pack_S3_2560x1440_2560x1440-b2f882323923927c414ab23faf1022ca" + }, + { + "type": "ProductLogo", + "url": "https://cdn1.epicgames.com/offer/428115def4ca4deea9d69c99c5a5a99e/FR_Bungie_Bungie_30th_Anniversary_Pack_OfferLogo_200x200_200x200-234225abe0aca2bfa7f5c5bc6e6fe348" + }, + { + "type": "Thumbnail", + "url": "https://cdn1.epicgames.com/offer/428115def4ca4deea9d69c99c5a5a99e/FR_Bungie_Bungie_30th_Anniversary_Pack_S4_1200x1600_1200x1600-04ebd49752c682d003014680f3d5be18" + }, + { + "type": "featuredMedia", + "url": "https://cdn1.epicgames.com/offer/428115def4ca4deea9d69c99c5a5a99e/FR_Bungie_Bungie_30th_Anniversary_Pack_S3_2560x1440_2560x1440-b2f882323923927c414ab23faf1022ca" + }, + { + "type": "featuredMedia", + "url": "https://cdn1.epicgames.com/offer/428115def4ca4deea9d69c99c5a5a99e/Screenshot1_1920x1080-37c070caa0106b08910518150bf96e94" + }, + { + "type": "featuredMedia", + "url": "https://cdn1.epicgames.com/offer/428115def4ca4deea9d69c99c5a5a99e/Screenshot2_1920x1080-14490e3ec01dceedce23d870774b2393" + }, + { + "type": "featuredMedia", + "url": "https://cdn1.epicgames.com/offer/428115def4ca4deea9d69c99c5a5a99e/Screenshot3_1920x1080-fdf882ad2cc98be7e63516b4ad28d6e9" + }, + { + "type": "featuredMedia", + "url": "https://cdn1.epicgames.com/offer/428115def4ca4deea9d69c99c5a5a99e/Screenshot4_1920x1080-079d4e12a8a04b31f7d4def7f4b745e7" + }, + { + "type": "featuredMedia", + "url": "https://cdn1.epicgames.com/offer/428115def4ca4deea9d69c99c5a5a99e/Screenshot5_1920x1080-f3c958c685629b6678544cba8bffc483" + }, + { + "type": "featuredMedia", + "url": "https://cdn1.epicgames.com/offer/428115def4ca4deea9d69c99c5a5a99e/Screenshot6_1920x1080-f13bb310baf9c158d15d473474c11586" + }, + { + "type": "featuredMedia", + "url": "https://cdn1.epicgames.com/offer/428115def4ca4deea9d69c99c5a5a99e/Screenshot7_1920x1080-6d2b714d2cfd64623cdcc39487d0b429" + }, + { + "type": "featuredMedia", + "url": "https://cdn1.epicgames.com/offer/428115def4ca4deea9d69c99c5a5a99e/Screenshot8_1920x1080-0956ff1a3a4969d9a3f2b96d87bdc19d" + } + ], + "seller": { + "id": "o-49lqsefbl6zr5sy3ztak77ej97cuvh", + "name": "Bungie" + }, + "productSlug": null, + "urlSlug": "destiny-2--bungie-30th-anniversary-pack", + "url": null, + "items": [ + { + "id": "904b57fb8bcd41a6be6c690a92ab3c15", + "namespace": "428115def4ca4deea9d69c99c5a5a99e" + } + ], + "customAttributes": [], + "categories": [ + { + "path": "addons" + }, + { + "path": "freegames" + }, + { + "path": "addons/durable" + }, + { + "path": "applications" + } + ], + "tags": [ + { + "id": "1203" + }, + { + "id": "1210" + }, + { + "id": "1370" + } + ], + "catalogNs": { + "mappings": [ + { + "pageSlug": "destiny-2", + "pageType": "productHome" + } + ] + }, + "offerMappings": [ + { + "pageSlug": "destiny-2--bungie-30th-anniversary-pack", + "pageType": "addon--cms-hybrid" + } + ], + "price": { + "totalPrice": { + "discountPrice": 2499, + "originalPrice": 2499, + "voucherDiscount": 0, + "discount": 0, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "24,99\u00a0\u20ac", + "discountPrice": "24,99\u00a0\u20ac", + "intermediatePrice": "24,99\u00a0\u20ac" + } + }, + "lineOffers": [ + { + "appliedRules": [] + } + ] + }, + "promotions": { + "promotionalOffers": [], + "upcomingPromotionalOffers": [ + { + "promotionalOffers": [ + { + "startDate": "2023-10-11T16:00:00.000Z", + "endDate": "2023-10-25T16:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 60 + } + } + ] + } + ] + } + }, + { + "title": "Gloomhaven", + "id": "9232fdbc352445cc820a54bdc97ed2bb", + "namespace": "bc079f73f020432fac896d30c8e2c330", + "description": "Que vous soyez arriv\u00e9s \u00e0 Gloomhaven en r\u00e9pondant \u00e0 l'appel de l'aventure ou au d\u00e9sir cupide de l'\u00e9clat de l'or, votre destin n'en sera pas chang\u00e9...", + "effectiveDate": "2022-09-22T15:00:00.000Z", + "offerType": "BASE_GAME", + "expiryDate": null, + "viewableDate": "2022-09-22T15:00:00.000Z", + "status": "ACTIVE", + "isCodeRedemptionOnly": false, + "keyImages": [ + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/spt-assets/ef2777467a3c49059a076e42fd9b41f0/gloomhaven-offer-1j9mc.jpg" + }, + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/spt-assets/ef2777467a3c49059a076e42fd9b41f0/download-gloomhaven-offer-1ho2x.jpg" + }, + { + "type": "Thumbnail", + "url": "https://cdn1.epicgames.com/spt-assets/ef2777467a3c49059a076e42fd9b41f0/download-gloomhaven-offer-1ho2x.jpg" + } + ], + "seller": { + "id": "o-4x4bpaww55p5g3f6xpyqe2cneqxd5d", + "name": "Asmodee" + }, + "productSlug": null, + "urlSlug": "0d48da287df14493a7415b560ec1bbb3", + "url": null, + "items": [ + { + "id": "6047532dd78a456593d0ffd6602a7218", + "namespace": "bc079f73f020432fac896d30c8e2c330" + } + ], + "customAttributes": [ + { + "key": "autoGeneratedPrice", + "value": "false" + }, + { + "key": "isManuallySetViewableDate", + "value": "true" + }, + { + "key": "isManuallySetPCReleaseDate", + "value": "true" + }, + { + "key": "isBlockchainUsed", + "value": "false" + } + ], + "categories": [ + { + "path": "freegames" + }, + { + "path": "games/edition/base" + }, + { + "path": "games/edition" + }, + { + "path": "games" + } + ], + "tags": [ + { + "id": "29088" + }, + { + "id": "21122" + }, + { + "id": "1188" + }, + { + "id": "21127" + }, + { + "id": "19847" + }, + { + "id": "21129" + }, + { + "id": "1386" + }, + { + "id": "9547" + }, + { + "id": "9549" + }, + { + "id": "1264" + }, + { + "id": "21137" + }, + { + "id": "21138" + }, + { + "id": "21139" + }, + { + "id": "16979" + }, + { + "id": "21140" + }, + { + "id": "21141" + }, + { + "id": "1367" + }, + { + "id": "22776" + }, + { + "id": "1370" + }, + { + "id": "1115" + }, + { + "id": "21147" + }, + { + "id": "21149" + } + ], + "catalogNs": { + "mappings": [ + { + "pageSlug": "gloomhaven-92f741", + "pageType": "productHome" + } + ] + }, + "offerMappings": [ + { + "pageSlug": "gloomhaven-92f741", + "pageType": "productHome" + } + ], + "price": { + "totalPrice": { + "discountPrice": 3499, + "originalPrice": 3499, + "voucherDiscount": 0, + "discount": 0, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "34,99\u00a0\u20ac", + "discountPrice": "34,99\u00a0\u20ac", + "intermediatePrice": "34,99\u00a0\u20ac" + } + }, + "lineOffers": [ + { + "appliedRules": [] + } + ] + }, + "promotions": null + }, + { + "title": "911 Operator", + "id": "268fd6ea355740d6ba4c76c3ffd4cbe0", + "namespace": "d923c737f0d243ccab407605ea40d39e", + "description": "911 OPERATOR est un jeu o\u00f9 tu deviens op\u00e9rateur de la ligne des urgences et o\u00f9 tu r\u00e9sous des incidents en fournissant des instruction et en g\u00e9rant des \u00e9quipes de secours. Tu peux jouer sur la carte de n\u2019importe quelle ville* du monde!", + "effectiveDate": "2023-09-14T15:00:00.000Z", + "offerType": "BASE_GAME", + "expiryDate": null, + "viewableDate": "2023-09-07T15:00:00.000Z", + "status": "ACTIVE", + "isCodeRedemptionOnly": false, + "keyImages": [ + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/spt-assets/c06cc46c27954f55974e9e7a4f3b3849/911-operator-omkv7.jpg" + }, + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/spt-assets/c06cc46c27954f55974e9e7a4f3b3849/911-operator-8dcp7.jpg" + }, + { + "type": "Thumbnail", + "url": "https://cdn1.epicgames.com/spt-assets/c06cc46c27954f55974e9e7a4f3b3849/911-operator-8dcp7.jpg" + } + ], + "seller": { + "id": "o-8dv8wz77w8tqnymmm8e99p28eny7kg", + "name": "Games Operators S.A." + }, + "productSlug": null, + "urlSlug": "ecb09cc5f55345e6bf6d3d9354c12876", + "url": null, + "items": [ + { + "id": "07499df5530b45c3ad8464a96cbe26c7", + "namespace": "d923c737f0d243ccab407605ea40d39e" + } + ], + "customAttributes": [ + { + "key": "autoGeneratedPrice", + "value": "false" + }, + { + "key": "isManuallySetViewableDate", + "value": "true" + }, + { + "key": "isManuallySetPCReleaseDate", + "value": "true" + }, + { + "key": "isBlockchainUsed", + "value": "false" + } + ], + "categories": [ + { + "path": "freegames" + }, + { + "path": "games" + }, + { + "path": "games/edition" + }, + { + "path": "games/edition/base" + } + ], + "tags": [ + { + "id": "1393" + }, + { + "id": "19847" + }, + { + "id": "1370" + }, + { + "id": "1115" + }, + { + "id": "9547" + }, + { + "id": "1263" + } + ], + "catalogNs": { + "mappings": [ + { + "pageSlug": "911-operator-585edd", + "pageType": "productHome" + } + ] + }, + "offerMappings": [ + { + "pageSlug": "911-operator-585edd", + "pageType": "productHome" + } + ], + "price": { + "totalPrice": { + "discountPrice": 1349, + "originalPrice": 1349, + "voucherDiscount": 0, + "discount": 0, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "13,49\u00a0\u20ac", + "discountPrice": "13,49\u00a0\u20ac", + "intermediatePrice": "13,49\u00a0\u20ac" + } + }, + "lineOffers": [ + { + "appliedRules": [] + } + ] + }, + "promotions": { + "promotionalOffers": [], + "upcomingPromotionalOffers": [ + { + "promotionalOffers": [ + { + "startDate": "2023-10-23T14:00:00.000Z", + "endDate": "2023-10-30T15:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 50 + } + } + ] + } + ] + } + }, + { + "title": "Q.U.B.E. ULTIMATE BUNDLE", + "id": "f18f14a76a874aa883a651fcc8c513d0", + "namespace": "0712c5eca64b47bbbced82cabba9f0d7", + "description": "Q.U.B.E. ULTIMATE BUNDLE", + "effectiveDate": "2023-10-12T15:00:00.000Z", + "offerType": "BUNDLE", + "expiryDate": null, + "viewableDate": "2023-10-05T14:25:00.000Z", + "status": "ACTIVE", + "isCodeRedemptionOnly": false, + "keyImages": [ + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/offer/0712c5eca64b47bbbced82cabba9f0d7/EGSBundle_Portrait_V2_1200x1600-981ac683de50fd5afed2c87dbc26494a" + }, + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/offer/0712c5eca64b47bbbced82cabba9f0d7/EGSBundle_Landscape_V2_2560x1440-50dbecaa32e134e246717f8a5e60ad25" + }, + { + "type": "ProductLogo", + "url": "https://cdn1.epicgames.com/offer/0712c5eca64b47bbbced82cabba9f0d7/EGSBundle_Logo_V2_400x400-99dcb7d141728efbe2b1b4e993ce6339" + }, + { + "type": "Thumbnail", + "url": "https://cdn1.epicgames.com/offer/0712c5eca64b47bbbced82cabba9f0d7/EGSBundle_Portrait_V2_1200x1600-981ac683de50fd5afed2c87dbc26494a" + } + ], + "seller": { + "id": "o-kk34ewvmscclj5a2ukx49ff6qknn7a", + "name": "Ten Hut Games" + }, + "productSlug": "qube-ultimate-bundle", + "urlSlug": "qube-ultimate-bundle", + "url": null, + "items": [ + { + "id": "11d229f51ac1445a8925b8d14da82b9b", + "namespace": "ad43401ad02840c2b2bee5f1f1a59988" + }, + { + "id": "0e7ec1d579ab481c93dff6056c19299f", + "namespace": "4b5f1eb366dc45f0920d397c01b291ba" + } + ], + "customAttributes": [ + { + "key": "com.epicgames.app.productSlug", + "value": "qube-ultimate-bundle" + } + ], + "categories": [ + { + "path": "bundles" + }, + { + "path": "freegames" + }, + { + "path": "bundles/games" + } + ], + "tags": [ + { + "id": "1216" + }, + { + "id": "1298" + }, + { + "id": "1203" + }, + { + "id": "1117" + }, + { + "id": "1294" + } + ], + "catalogNs": { + "mappings": null + }, + "offerMappings": null, + "price": { + "totalPrice": { + "discountPrice": 4499, + "originalPrice": 4499, + "voucherDiscount": 0, + "discount": 0, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "44,99\u00a0\u20ac", + "discountPrice": "44,99\u00a0\u20ac", + "intermediatePrice": "44,99\u00a0\u20ac" + } + }, + "lineOffers": [ + { + "appliedRules": [] + } + ] + }, + "promotions": { + "promotionalOffers": [], + "upcomingPromotionalOffers": [ + { + "promotionalOffers": [ + { + "startDate": "2023-10-12T15:00:00.000Z", + "endDate": "2023-10-19T15:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 0 + } + } + ] + } + ] + } + }, + { + "title": "PAYDAY 2", + "id": "de434b7be57940d98ede93b50cdacfc2", + "namespace": "d5241c76f178492ea1540fce45616757", + "description": "PAYDAY 2 is an action-packed, four-player co-op shooter that once again lets gamers don the masks of the original PAYDAY crew - Dallas, Hoxton, Wolf and Chains - as they descend on Washington DC for an epic crime spree.", + "effectiveDate": "2099-01-01T00:00:00.000Z", + "offerType": "OTHERS", + "expiryDate": null, + "viewableDate": "2023-06-01T14:25:00.000Z", + "status": "ACTIVE", + "isCodeRedemptionOnly": true, + "keyImages": [ + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/mammoth-h1nvv_2560x1440-ac346d6ece5ec356561e112fbddb2dc1" + }, + { + "type": "VaultClosed", + "url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/EN-mega-sale-vault-16x9-asset_1920x1080-a27cf3919dde320a72936374a1d47813" + } + ], + "seller": { + "id": "o-ufmrk5furrrxgsp5tdngefzt5rxdcn", + "name": "Epic Dev Test Account" + }, + "productSlug": "payday-2-c66369", + "urlSlug": "mystery-game-7", + "url": null, + "items": [ + { + "id": "8341d7c7e4534db7848cc428aa4cbe5a", + "namespace": "d5241c76f178492ea1540fce45616757" + } + ], + "customAttributes": [ + { + "key": "com.epicgames.app.freegames.vault.close", + "value": "[]" + }, + { + "key": "com.epicgames.app.blacklist", + "value": "[]" + }, + { + "key": "com.epicgames.app.freegames.vault.slug", + "value": "sales-and-specials/mega-sale" + }, + { + "key": "com.epicgames.app.freegames.vault.open", + "value": "[]" + }, + { + "key": "com.epicgames.app.productSlug", + "value": "payday-2-c66369" + } + ], + "categories": [ + { + "path": "freegames/vaulted" + }, + { + "path": "freegames" + }, + { + "path": "games" + }, + { + "path": "applications" + } + ], + "tags": [], + "catalogNs": { + "mappings": [] + }, + "offerMappings": [], + "price": { + "totalPrice": { + "discountPrice": 0, + "originalPrice": 0, + "voucherDiscount": 0, + "discount": 0, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "0", + "discountPrice": "0", + "intermediatePrice": "0" + } + }, + "lineOffers": [ + { + "appliedRules": [] + } + ] + }, + "promotions": null + }, + { + "title": "Blazing Sails", + "id": "363d0be3b57d4741a046d38da0e6355e", + "namespace": "aee7dd76aa6746578f476dc47f8d1d7f", + "description": "Survivez \u00e0 Blazing Sails, un jeu de pirate en JcJ tr\u00e9pidant\u00a0! Cr\u00e9ez votre navire et vos pirates uniques. Naviguez en \u00e9quipe avec d'autres joueurs\u00a0! D\u00e9couvrez diff\u00e9rents modes de jeu, cartes, armes, types de navires et bien plus encore. Battez les \u00e9quipages adverses dans d'\u00e9piques combats sur terre et en mer\u00a0!", + "effectiveDate": "2099-04-06T17:35:00.000Z", + "offerType": "BASE_GAME", + "expiryDate": null, + "viewableDate": "2023-03-30T15:00:00.000Z", + "status": "ACTIVE", + "isCodeRedemptionOnly": false, + "keyImages": [ + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/offer/aee7dd76aa6746578f476dc47f8d1d7f/EGS_BlazingSails_GetUpGames_S2_1200x1600-bae3831e97b560958dc785e830ebed8c" + }, + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/offer/aee7dd76aa6746578f476dc47f8d1d7f/EGS_BlazingSails_GetUpGames_S1_2560x1440-fd7a7b3d357555880cb7969634553c5b" + }, + { + "type": "ProductLogo", + "url": "https://cdn1.epicgames.com/offer/aee7dd76aa6746578f476dc47f8d1d7f/EGS_BlazingSails_GetUpGames_IC1_400x400-a7b91f257fcbd9ced825d3da95298170" + }, + { + "type": "Thumbnail", + "url": "https://cdn1.epicgames.com/offer/aee7dd76aa6746578f476dc47f8d1d7f/EGS_BlazingSails_GetUpGames_S2_1200x1600-bae3831e97b560958dc785e830ebed8c" + } + ], + "seller": { + "id": "o-ftmts7pjfvdywkby885rdzl4hdbtys", + "name": "Iceberg Interactive" + }, + "productSlug": "blazing-sails", + "urlSlug": "blazing-sails", + "url": null, + "items": [ + { + "id": "30aec28f450a41499dd27e0d27294b56", + "namespace": "aee7dd76aa6746578f476dc47f8d1d7f" + } + ], + "customAttributes": [ + { + "key": "com.epicgames.app.blacklist", + "value": "KR" + }, + { + "key": "com.epicgames.app.productSlug", + "value": "blazing-sails" + } + ], + "categories": [ + { + "path": "freegames" + }, + { + "path": "games" + }, + { + "path": "games/edition" + }, + { + "path": "games/edition/base" + }, + { + "path": "applications" + } + ], + "tags": [ + { + "id": "1216" + }, + { + "id": "1264" + }, + { + "id": "1203" + }, + { + "id": "9547" + } + ], + "catalogNs": { + "mappings": [ + { + "pageSlug": "blazing-sails", + "pageType": "productHome" + } + ] + }, + "offerMappings": [], + "price": { + "totalPrice": { + "discountPrice": 1479, + "originalPrice": 1479, + "voucherDiscount": 0, + "discount": 0, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "14,79\u00a0\u20ac", + "discountPrice": "14,79\u00a0\u20ac", + "intermediatePrice": "14,79\u00a0\u20ac" + } + }, + "lineOffers": [ + { + "appliedRules": [] + } + ] + }, + "promotions": { + "promotionalOffers": [], + "upcomingPromotionalOffers": [ + { + "promotionalOffers": [ + { + "startDate": "2023-10-12T15:00:00.000Z", + "endDate": "2023-10-19T15:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 0 + } + } + ] + } + ] + } + } + ], + "paging": { + "count": 1000, + "total": 7 + } + } + } + }, + "extensions": {} +} diff --git a/tests/components/epic_games_store/fixtures/error_5222_wrong_country.json b/tests/components/epic_games_store/fixtures/error_5222_wrong_country.json new file mode 100644 index 00000000000..c91d5551ff9 --- /dev/null +++ b/tests/components/epic_games_store/fixtures/error_5222_wrong_country.json @@ -0,0 +1,23 @@ +{ + "errors": [ + { + "message": "CatalogQuery/searchStore: Request failed with status code 400", + "locations": [ + { + "line": 18, + "column": 9 + } + ], + "correlationId": "e10ad58e-a4f9-4097-af5d-cafdbe0d8bbd", + "serviceResponse": "{\"errorCode\":\"errors.com.epicgames.catalog.invalid_country_code\",\"errorMessage\":\"Sorry the value you entered: en-US, does not appear to be a valid ISO country code.\",\"messageVars\":[\"en-US\"],\"numericErrorCode\":5222,\"originatingService\":\"com.epicgames.catalog.public\",\"intent\":\"prod\",\"errorStatus\":400}", + "stack": null, + "path": ["Catalog", "searchStore"] + } + ], + "data": { + "Catalog": { + "searchStore": null + } + }, + "extensions": {} +} diff --git a/tests/components/epic_games_store/fixtures/free_games.json b/tests/components/epic_games_store/fixtures/free_games.json new file mode 100644 index 00000000000..29ff43f32a0 --- /dev/null +++ b/tests/components/epic_games_store/fixtures/free_games.json @@ -0,0 +1,2189 @@ +{ + "data": { + "Catalog": { + "searchStore": { + "elements": [ + { + "title": "Rising Storm 2: Vietnam", + "id": "b19d810d322240e7b37bcf84ffac60ce", + "namespace": "3542a1df211e492bb2abecb7c734f7f9", + "description": "Red Orchestra Series' take on Vietnam: 64-player MP matches; 20+ maps; US Army & Marines, PAVN/NVA, NLF/VC; Australians and ARVN forces; 50+ weapons; 4 flyable helicopters; mines, traps and tunnels; Brutal. Authentic. Gritty. Character customization.", + "effectiveDate": "2020-10-08T15:00:00.000Z", + "offerType": "BASE_GAME", + "expiryDate": null, + "status": "ACTIVE", + "isCodeRedemptionOnly": false, + "keyImages": [ + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/3542a1df211e492bb2abecb7c734f7f9/offer/EGS_RisingStorm2Vietnam_AntimatterGamesTripwireInteractive_S3-2560x1440-e08edd93cb71bf15b50a74f3de2d17b0.jpg" + }, + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/3542a1df211e492bb2abecb7c734f7f9/offer/EGS_RisingStorm2Vietnam_AntimatterGamesTripwireInteractive_S4-1200x1600-5e3b2f8107e17cc008237e52761d67e5.jpg" + }, + { + "type": "DieselStoreFrontWide", + "url": "https://cdn1.epicgames.com/3542a1df211e492bb2abecb7c734f7f9/offer/EGS_RisingStorm2Vietnam_AntimatterGamesTripwireInteractive_S3-2560x1440-e08edd93cb71bf15b50a74f3de2d17b0.jpg" + }, + { + "type": "DieselStoreFrontTall", + "url": "https://cdn1.epicgames.com/3542a1df211e492bb2abecb7c734f7f9/offer/EGS_RisingStorm2Vietnam_AntimatterGamesTripwireInteractive_S4-1200x1600-5e3b2f8107e17cc008237e52761d67e5.jpg" + }, + { + "type": "Thumbnail", + "url": "https://cdn1.epicgames.com/3542a1df211e492bb2abecb7c734f7f9/offer/EGS_RisingStorm2Vietnam_AntimatterGamesTripwireInteractive_S4-1200x1600-5e3b2f8107e17cc008237e52761d67e5.jpg" + }, + { + "type": "CodeRedemption_340x440", + "url": "https://cdn1.epicgames.com/3542a1df211e492bb2abecb7c734f7f9/offer/EGS_RisingStorm2Vietnam_AntimatterGamesTripwireInteractive_S4-1200x1600-5e3b2f8107e17cc008237e52761d67e5.jpg" + } + ], + "seller": { + "id": "o-2baznhy8tfh7fmyb55ul656v7ggt7r", + "name": "Tripwire Interactive" + }, + "productSlug": "rising-storm-2-vietnam/home", + "urlSlug": "risingstorm2vietnam", + "url": null, + "items": [ + { + "id": "685765c3f37049c49b45bea4173725d2", + "namespace": "3542a1df211e492bb2abecb7c734f7f9" + }, + { + "id": "c7c6d65ac4cc4ef0ae12e8e89f134684", + "namespace": "3542a1df211e492bb2abecb7c734f7f9" + } + ], + "customAttributes": [ + { + "key": "com.epicgames.app.blacklist", + "value": "[]" + }, + { + "key": "publisherName", + "value": "Tripwire Interactive" + }, + { + "key": "developerName", + "value": "Antimatter Games" + }, + { + "key": "com.epicgames.app.productSlug", + "value": "rising-storm-2-vietnam/home" + } + ], + "categories": [ + { + "path": "freegames" + }, + { + "path": "games" + }, + { + "path": "games/edition" + }, + { + "path": "games/edition/base" + }, + { + "path": "applications" + } + ], + "tags": [ + { + "id": "1216" + }, + { + "id": "21122" + }, + { + "id": "21125" + }, + { + "id": "21129" + }, + { + "id": "14346" + }, + { + "id": "9547" + }, + { + "id": "16011" + }, + { + "id": "15375" + }, + { + "id": "21135" + }, + { + "id": "21138" + }, + { + "id": "1299" + }, + { + "id": "16979" + }, + { + "id": "21139" + }, + { + "id": "21140" + }, + { + "id": "17493" + }, + { + "id": "21141" + }, + { + "id": "22485" + }, + { + "id": "18777" + }, + { + "id": "18778" + }, + { + "id": "1115" + }, + { + "id": "21148" + }, + { + "id": "21149" + }, + { + "id": "14944" + }, + { + "id": "19242" + }, + { + "id": "18607" + }, + { + "id": "1203" + } + ], + "catalogNs": { + "mappings": [ + { + "pageSlug": "rising-storm-2-vietnam", + "pageType": "productHome" + } + ] + }, + "offerMappings": [], + "price": { + "totalPrice": { + "discountPrice": 2199, + "originalPrice": 2199, + "voucherDiscount": 0, + "discount": 0, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "\u20ac21.99", + "discountPrice": "\u20ac21.99", + "intermediatePrice": "\u20ac21.99" + } + }, + "lineOffers": [ + { + "appliedRules": [] + } + ] + }, + "promotions": { + "promotionalOffers": [], + "upcomingPromotionalOffers": [ + { + "promotionalOffers": [ + { + "startDate": "2022-11-03T15:00:00.000Z", + "endDate": "2022-11-10T16:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 0 + } + } + ] + } + ] + } + }, + { + "title": "Idle Champions of the Forgotten Realms", + "id": "a9748abde1c94b66aae5250bb9fc5503", + "namespace": "7e508f543b05465abe3a935960eb70ac", + "description": "Idle Champions is a licensed Dungeons & Dragons strategy management video game uniting iconic characters from novels, campaigns, and shows into one epic adventure.", + "effectiveDate": "2021-02-16T17:00:00.000Z", + "offerType": "BASE_GAME", + "expiryDate": null, + "status": "ACTIVE", + "isCodeRedemptionOnly": false, + "keyImages": [ + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/offer/7e508f543b05465abe3a935960eb70ac/EGS_IdleChampionsoftheForgottenRealms_CodenameEntertainment_S2_1200x1600-dd9a8f25ad56089231f43cf639bde217" + }, + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/offer/7e508f543b05465abe3a935960eb70ac/EGS_IdleChampionsoftheForgottenRealms_CodenameEntertainment_S1_2560x1440-e2a1ffd224f443594d5deff3a47a45e2" + }, + { + "type": "Thumbnail", + "url": "https://cdn1.epicgames.com/offer/7e508f543b05465abe3a935960eb70ac/EGS_IdleChampionsoftheForgottenRealms_CodenameEntertainment_S2_1200x1600-dd9a8f25ad56089231f43cf639bde217" + }, + { + "type": "DieselStoreFrontTall", + "url": "https://cdn1.epicgames.com/offer/7e508f543b05465abe3a935960eb70ac/EGS_IdleChampionsoftheForgottenRealms_CodenameEntertainment_S2_1200x1600-dd9a8f25ad56089231f43cf639bde217" + }, + { + "type": "DieselStoreFrontWide", + "url": "https://cdn1.epicgames.com/offer/7e508f543b05465abe3a935960eb70ac/EGS_IdleChampionsoftheForgottenRealms_CodenameEntertainment_S1_2560x1440-e2a1ffd224f443594d5deff3a47a45e2" + } + ], + "seller": { + "id": "o-3kpjwtwqwfl2p9wdwvpad7yqz4kt6c", + "name": "Codename Entertainment" + }, + "productSlug": "idle-champions-of-the-forgotten-realms", + "urlSlug": "banegeneralaudience", + "url": null, + "items": [ + { + "id": "9a4e1a1eb6b140f6a9e5e4dcb5a2bf55", + "namespace": "7e508f543b05465abe3a935960eb70ac" + } + ], + "customAttributes": [ + { + "key": "com.epicgames.app.blacklist", + "value": "KR" + }, + { + "key": "publisherName", + "value": "Codename Entertainment" + }, + { + "key": "developerName", + "value": "Codename Entertainment" + }, + { + "key": "com.epicgames.app.productSlug", + "value": "idle-champions-of-the-forgotten-realms" + } + ], + "categories": [ + { + "path": "freegames" + }, + { + "path": "games" + }, + { + "path": "games/edition" + }, + { + "path": "games/edition/base" + }, + { + "path": "applications" + } + ], + "tags": [ + { + "id": "21136" + }, + { + "id": "21122" + }, + { + "id": "21138" + }, + { + "id": "21139" + }, + { + "id": "1188" + }, + { + "id": "1141" + }, + { + "id": "1370" + }, + { + "id": "1115" + }, + { + "id": "9547" + }, + { + "id": "21149" + }, + { + "id": "21119" + } + ], + "catalogNs": { + "mappings": [ + { + "pageSlug": "idle-champions-of-the-forgotten-realms", + "pageType": "productHome" + } + ] + }, + "offerMappings": [], + "price": { + "totalPrice": { + "discountPrice": 0, + "originalPrice": 0, + "voucherDiscount": 0, + "discount": 0, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "0", + "discountPrice": "0", + "intermediatePrice": "0" + } + }, + "lineOffers": [ + { + "appliedRules": [] + } + ] + }, + "promotions": { + "promotionalOffers": [], + "upcomingPromotionalOffers": [] + } + }, + { + "title": "Hundred Days - Winemaking Simulator", + "id": "141eee80fbe041d48e16e7b998829295", + "namespace": "4d8b727a49144090b103f6b6ba471e71", + "description": "Winemaking could be your best adventure. Make the best wine interacting with soil and nature and take your winery to the top. Your beautiful journey into the winemaking tradition starts now.", + "effectiveDate": "2021-05-13T14:00:00.000Z", + "offerType": "BASE_GAME", + "expiryDate": null, + "status": "ACTIVE", + "isCodeRedemptionOnly": false, + "keyImages": [ + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/4d8b727a49144090b103f6b6ba471e71/offer/EGS_HundredDaysWinemakingSimulatorDEMO_BrokenArmsGames_Demo_G1C_00-1920x1080-0ffeb0645f0badb615627b481b4a913e.jpg" + }, + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/4d8b727a49144090b103f6b6ba471e71/offer/EGS_HundredDaysWinemakingSimulatorDEMO_BrokenArmsGames_Demo_S2-1200x1600-35531ec1fa868e3876fac76471a24017.jpg" + }, + { + "type": "Thumbnail", + "url": "https://cdn1.epicgames.com/4d8b727a49144090b103f6b6ba471e71/offer/EGS_HundredDaysWinemakingSimulatorDEMO_BrokenArmsGames_Demo_S2-1200x1600-35531ec1fa868e3876fac76471a24017.jpg" + }, + { + "type": "CodeRedemption_340x440", + "url": "https://cdn1.epicgames.com/4d8b727a49144090b103f6b6ba471e71/offer/EGS_HundredDaysWinemakingSimulatorDEMO_BrokenArmsGames_Demo_S2-1200x1600-35531ec1fa868e3876fac76471a24017.jpg" + }, + { + "type": "DieselStoreFrontWide", + "url": "https://cdn1.epicgames.com/4d8b727a49144090b103f6b6ba471e71/offer/EGS_HundredDaysWinemakingSimulatorDEMO_BrokenArmsGames_Demo_S1-2560x1440-8f0dd95b6027cd1243361d430b3bf552.jpg" + }, + { + "type": "DieselStoreFrontTall", + "url": "https://cdn1.epicgames.com/4d8b727a49144090b103f6b6ba471e71/offer/EGS_HundredDaysWinemakingSimulatorDEMO_BrokenArmsGames_Demo_S2-1200x1600-35531ec1fa868e3876fac76471a24017.jpg" + } + ], + "seller": { + "id": "o-ty5rvlnsbgdnfffytsywat86gcedkm", + "name": "Broken Arms Games srls" + }, + "productSlug": "hundred-days-winemaking-simulator", + "urlSlug": "hundred-days-winemaking-simulator", + "url": null, + "items": [ + { + "id": "03cacb8754f243bfbc536c9dda0eb32e", + "namespace": "4d8b727a49144090b103f6b6ba471e71" + } + ], + "customAttributes": [ + { + "key": "com.epicgames.app.blacklist", + "value": "[]" + }, + { + "key": "developerName", + "value": "Broken Arms Games" + }, + { + "key": "com.epicgames.app.productSlug", + "value": "hundred-days-winemaking-simulator" + } + ], + "categories": [ + { + "path": "freegames" + }, + { + "path": "games" + }, + { + "path": "games/edition" + }, + { + "path": "games/edition/base" + }, + { + "path": "applications" + } + ], + "tags": [ + { + "id": "1188" + }, + { + "id": "21894" + }, + { + "id": "21127" + }, + { + "id": "19242" + }, + { + "id": "21130" + }, + { + "id": "16011" + }, + { + "id": "9547" + }, + { + "id": "1263" + }, + { + "id": "15375" + }, + { + "id": "18607" + }, + { + "id": "1393" + }, + { + "id": "21138" + }, + { + "id": "16979" + }, + { + "id": "21140" + }, + { + "id": "17493" + }, + { + "id": "21141" + }, + { + "id": "18777" + }, + { + "id": "1370" + }, + { + "id": "18778" + }, + { + "id": "21146" + }, + { + "id": "1115" + }, + { + "id": "21149" + }, + { + "id": "10719" + }, + { + "id": "21119" + } + ], + "catalogNs": { + "mappings": [ + { + "pageSlug": "hundred-days-winemaking-simulator", + "pageType": "productHome" + } + ] + }, + "offerMappings": [], + "price": { + "totalPrice": { + "discountPrice": 1999, + "originalPrice": 1999, + "voucherDiscount": 0, + "discount": 0, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "\u20ac19.99", + "discountPrice": "\u20ac19.99", + "intermediatePrice": "\u20ac19.99" + } + }, + "lineOffers": [ + { + "appliedRules": [] + } + ] + }, + "promotions": null + }, + { + "title": "Shadow of the Tomb Raider: Definitive Edition", + "id": "ee7f3c6725fd4fd4b8aeab8622cb770e", + "namespace": "4b5461ca8d1c488787b5200b420de066", + "description": "In Shadow of the Tomb Raider Definitive Edition experience the final chapter of Lara\u2019s origin as she is forged into the Tomb Raider she is destined to be.", + "effectiveDate": "2021-12-30T16:00:00.000Z", + "offerType": "BASE_GAME", + "expiryDate": null, + "status": "ACTIVE", + "isCodeRedemptionOnly": false, + "keyImages": [ + { + "type": "CodeRedemption_340x440", + "url": "https://cdn1.epicgames.com/offer/4b5461ca8d1c488787b5200b420de066/egs-shadowofthetombraiderdefinitiveedition-eidosmontralcrystaldynamicsnixxessoftware-s4-1200x1600-7ee40d6fa744_1200x1600-950cdb624cc75d04fe3c8c0b62ce98de" + }, + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/offer/4b5461ca8d1c488787b5200b420de066/egs-shadowofthetombraiderdefinitiveedition-eidosmontralcrystaldynamicsnixxessoftware-s4-1200x1600-7ee40d6fa744_1200x1600-950cdb624cc75d04fe3c8c0b62ce98de" + }, + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/offer/4b5461ca8d1c488787b5200b420de066/egs-shadowofthetombraiderdefinitiveedition-eidosmontralcrystaldynamicsnixxessoftware-s1-2560x1440-eca6506e95a1_2560x1440-193582a5fd76a593804e0171d6395cf4" + }, + { + "type": "Thumbnail", + "url": "https://cdn1.epicgames.com/offer/4b5461ca8d1c488787b5200b420de066/egs-shadowofthetombraiderdefinitiveedition-eidosmontralcrystaldynamicsnixxessoftware-s4-1200x1600-7ee40d6fa744_1200x1600-950cdb624cc75d04fe3c8c0b62ce98de" + }, + { + "type": "DieselStoreFrontTall", + "url": "https://cdn1.epicgames.com/offer/4b5461ca8d1c488787b5200b420de066/egs-shadowofthetombraiderdefinitiveedition-eidosmontralcrystaldynamicsnixxessoftware-s4-1200x1600-7ee40d6fa744_1200x1600-950cdb624cc75d04fe3c8c0b62ce98de" + }, + { + "type": "DieselStoreFrontWide", + "url": "https://cdn1.epicgames.com/offer/4b5461ca8d1c488787b5200b420de066/egs-shadowofthetombraiderdefinitiveedition-eidosmontralcrystaldynamicsnixxessoftware-s1-2560x1440-eca6506e95a1_2560x1440-193582a5fd76a593804e0171d6395cf4" + } + ], + "seller": { + "id": "o-7petn7mrlk8g86ktqm7uglcr7lfaja", + "name": "Square Enix" + }, + "productSlug": "shadow-of-the-tomb-raider", + "urlSlug": "shadow-of-the-tomb-raider", + "url": null, + "items": [ + { + "id": "e7f90759e0544e42be9391d10a5c6000", + "namespace": "4b5461ca8d1c488787b5200b420de066" + } + ], + "customAttributes": [ + { + "key": "com.epicgames.app.blacklist", + "value": "[]" + }, + { + "key": "com.epicgames.app.productSlug", + "value": "shadow-of-the-tomb-raider" + } + ], + "categories": [ + { + "path": "freegames" + }, + { + "path": "games" + }, + { + "path": "games/edition" + }, + { + "path": "games/edition/base" + }, + { + "path": "applications" + } + ], + "tags": [ + { + "id": "1216" + }, + { + "id": "21122" + }, + { + "id": "18051" + }, + { + "id": "1188" + }, + { + "id": "21894" + }, + { + "id": "21127" + }, + { + "id": "9547" + }, + { + "id": "9549" + }, + { + "id": "21138" + }, + { + "id": "21139" + }, + { + "id": "21140" + }, + { + "id": "21109" + }, + { + "id": "21141" + }, + { + "id": "22485" + }, + { + "id": "1370" + }, + { + "id": "21146" + }, + { + "id": "1117" + }, + { + "id": "21149" + }, + { + "id": "21119" + } + ], + "catalogNs": { + "mappings": [ + { + "pageSlug": "shadow-of-the-tomb-raider", + "pageType": "productHome" + } + ] + }, + "offerMappings": [], + "price": { + "totalPrice": { + "discountPrice": 1319, + "originalPrice": 3999, + "voucherDiscount": 0, + "discount": 2680, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "\u20ac39.99", + "discountPrice": "\u20ac13.19", + "intermediatePrice": "\u20ac13.19" + } + }, + "lineOffers": [ + { + "appliedRules": [ + { + "id": "35111a3c715340d08910a9f6a5b3e846", + "endDate": "2022-11-01T15:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE" + } + } + ] + } + ] + }, + "promotions": { + "promotionalOffers": [ + { + "promotionalOffers": [ + { + "startDate": "2022-10-18T15:00:00.000Z", + "endDate": "2022-11-01T15:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 33 + } + } + ] + } + ], + "upcomingPromotionalOffers": [] + } + }, + { + "title": "Terraforming Mars", + "id": "f2496286331e405793d69807755b7b23", + "namespace": "25d726130e6c4fe68f88e71933bda955", + "description": "The taming of the Red Planet has begun!\n\nControl your corporation, play project cards, build up production, place your cities and green areas on the map, and race for milestones and awards!\n\nWill your corporation lead the way into humanity's new era?", + "effectiveDate": "2022-05-05T15:00:00.000Z", + "offerType": "BASE_GAME", + "expiryDate": null, + "status": "ACTIVE", + "isCodeRedemptionOnly": false, + "keyImages": [ + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/spt-assets/5199b206e46947ebad5e5c282e95776f/terraforming-mars-offer-1j70f.jpg" + }, + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/spt-assets/5199b206e46947ebad5e5c282e95776f/download-terraforming-mars-offer-13t2e.jpg" + }, + { + "type": "Thumbnail", + "url": "https://cdn1.epicgames.com/spt-assets/5199b206e46947ebad5e5c282e95776f/download-terraforming-mars-offer-13t2e.jpg" + } + ], + "seller": { + "id": "o-4x4bpaww55p5g3f6xpyqe2cneqxd5d", + "name": "Asmodee" + }, + "productSlug": null, + "urlSlug": "24cdfcde68bf4a7e8b8618ac2c0c460b", + "url": null, + "items": [ + { + "id": "ee49486d7346465dba1f1dec85725aee", + "namespace": "25d726130e6c4fe68f88e71933bda955" + } + ], + "customAttributes": [ + { + "key": "autoGeneratedPrice", + "value": "false" + }, + { + "key": "isManuallySetPCReleaseDate", + "value": "true" + } + ], + "categories": [ + { + "path": "freegames" + }, + { + "path": "games/edition/base" + }, + { + "path": "games/edition" + }, + { + "path": "games" + } + ], + "tags": [ + { + "id": "18051" + }, + { + "id": "1188" + }, + { + "id": "21125" + }, + { + "id": "1386" + }, + { + "id": "9547" + }, + { + "id": "21138" + }, + { + "id": "1203" + }, + { + "id": "1299" + }, + { + "id": "21139" + }, + { + "id": "21140" + }, + { + "id": "21141" + }, + { + "id": "1370" + }, + { + "id": "1115" + }, + { + "id": "21148" + }, + { + "id": "21149" + }, + { + "id": "10719" + } + ], + "catalogNs": { + "mappings": [ + { + "pageSlug": "terraforming-mars-18c3ad", + "pageType": "productHome" + } + ] + }, + "offerMappings": [ + { + "pageSlug": "terraforming-mars-18c3ad", + "pageType": "productHome" + } + ], + "price": { + "totalPrice": { + "discountPrice": 1399, + "originalPrice": 1999, + "voucherDiscount": 0, + "discount": 600, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "\u20ac19.99", + "discountPrice": "\u20ac13.99", + "intermediatePrice": "\u20ac13.99" + } + }, + "lineOffers": [ + { + "appliedRules": [ + { + "id": "8e9732952e714f6583416e66fc451cd7", + "endDate": "2022-11-01T15:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE" + } + } + ] + } + ] + }, + "promotions": { + "promotionalOffers": [ + { + "promotionalOffers": [ + { + "startDate": "2022-10-18T15:00:00.000Z", + "endDate": "2022-11-01T15:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 70 + } + } + ] + } + ], + "upcomingPromotionalOffers": [] + } + }, + { + "title": "Car Mechanic Simulator 2018", + "id": "5eb27cf1747c40b5a0d4f5492774678d", + "namespace": "226306adde104c9092247dcd4bfa1499", + "description": "Build and expand your repair service empire in this incredibly detailed and highly realistic simulation game, where attention to car detail is astonishing. Find classic, unique cars in the new Barn Find module and Junkyard module.", + "effectiveDate": "2022-06-23T15:00:00.000Z", + "offerType": "BASE_GAME", + "expiryDate": null, + "status": "ACTIVE", + "isCodeRedemptionOnly": false, + "keyImages": [ + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/offer/226306adde104c9092247dcd4bfa1499/EGS_CarMechanicSimulator2018_RedDotGames_S2_1200x1600-f285924f9144353f57ac4631f0c689e6" + }, + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/offer/226306adde104c9092247dcd4bfa1499/EGS_CarMechanicSimulator2018_RedDotGames_S1_2560x1440-3489ef1499e64c168fdf4b14926d2c23" + }, + { + "type": "Thumbnail", + "url": "https://cdn1.epicgames.com/offer/226306adde104c9092247dcd4bfa1499/EGS_CarMechanicSimulator2018_RedDotGames_S2_1200x1600-f285924f9144353f57ac4631f0c689e6" + } + ], + "seller": { + "id": "o-5n5cbrasl5yzexjc529rypg8eh8lfb", + "name": "PlayWay" + }, + "productSlug": "car-mechanic-simulator-2018", + "urlSlug": "car-mechanic-simulator-2018", + "url": null, + "items": [ + { + "id": "49a3a8597c4240ecaf1f9068106c9869", + "namespace": "226306adde104c9092247dcd4bfa1499" + } + ], + "customAttributes": [ + { + "key": "com.epicgames.app.blacklist", + "value": "[]" + }, + { + "key": "com.epicgames.app.productSlug", + "value": "car-mechanic-simulator-2018" + } + ], + "categories": [ + { + "path": "freegames" + }, + { + "path": "games" + }, + { + "path": "games/edition" + }, + { + "path": "games/edition/base" + }, + { + "path": "applications" + } + ], + "tags": [ + { + "id": "21120" + }, + { + "id": "1188" + }, + { + "id": "21127" + }, + { + "id": "9547" + }, + { + "id": "1393" + }, + { + "id": "21138" + }, + { + "id": "21139" + }, + { + "id": "21140" + }, + { + "id": "21141" + }, + { + "id": "1370" + }, + { + "id": "21146" + }, + { + "id": "21148" + }, + { + "id": "21149" + }, + { + "id": "21119" + } + ], + "catalogNs": { + "mappings": [ + { + "pageSlug": "car-mechanic-simulator-2018", + "pageType": "productHome" + } + ] + }, + "offerMappings": [], + "price": { + "totalPrice": { + "discountPrice": 1599, + "originalPrice": 1599, + "voucherDiscount": 0, + "discount": 0, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "\u20ac15.99", + "discountPrice": "\u20ac15.99", + "intermediatePrice": "\u20ac15.99" + } + }, + "lineOffers": [ + { + "appliedRules": [] + } + ] + }, + "promotions": null + }, + { + "title": "A Game Of Thrones: The Board Game Digital Edition", + "id": "a125d72a47a1490aba78c4e79a40395d", + "namespace": "1b737464d3c441f8956315433be02d3b", + "description": "It is the digital adaptation of the top-selling strategy board game from Fantasy Flight Games.", + "effectiveDate": "2022-06-23T15:00:00.000Z", + "offerType": "BASE_GAME", + "expiryDate": null, + "status": "ACTIVE", + "isCodeRedemptionOnly": false, + "keyImages": [ + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/spt-assets/61c1413e3db0423f9ddd4a5edbee717e/a-game-of-thrones-offer-11gxu.jpg" + }, + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/spt-assets/61c1413e3db0423f9ddd4a5edbee717e/download-a-game-of-thrones-offer-1q8ei.jpg" + }, + { + "type": "Thumbnail", + "url": "https://cdn1.epicgames.com/spt-assets/61c1413e3db0423f9ddd4a5edbee717e/download-a-game-of-thrones-offer-1q8ei.jpg" + } + ], + "seller": { + "id": "o-4x4bpaww55p5g3f6xpyqe2cneqxd5d", + "name": "Asmodee" + }, + "productSlug": null, + "urlSlug": "ce6f7ab4edab4cc2aa7e0ff4c19540e2", + "url": null, + "items": [ + { + "id": "dc6ae31efba7401fa72ed93f0bd37c6a", + "namespace": "1b737464d3c441f8956315433be02d3b" + } + ], + "customAttributes": [ + { + "key": "autoGeneratedPrice", + "value": "false" + }, + { + "key": "isManuallySetPCReleaseDate", + "value": "true" + } + ], + "categories": [ + { + "path": "freegames" + }, + { + "path": "games/edition/base" + }, + { + "path": "games/edition" + }, + { + "path": "games" + } + ], + "tags": [ + { + "id": "18051" + }, + { + "id": "1188" + }, + { + "id": "21125" + }, + { + "id": "9547" + }, + { + "id": "21138" + }, + { + "id": "1203" + }, + { + "id": "1299" + }, + { + "id": "21139" + }, + { + "id": "21140" + }, + { + "id": "21141" + }, + { + "id": "1370" + }, + { + "id": "1115" + }, + { + "id": "21149" + } + ], + "catalogNs": { + "mappings": [ + { + "pageSlug": "a-game-of-thrones-5858a3", + "pageType": "productHome" + } + ] + }, + "offerMappings": [ + { + "pageSlug": "a-game-of-thrones-5858a3", + "pageType": "productHome" + } + ], + "price": { + "totalPrice": { + "discountPrice": 1399, + "originalPrice": 1999, + "voucherDiscount": 0, + "discount": 600, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "\u20ac19.99", + "discountPrice": "\u20ac13.99", + "intermediatePrice": "\u20ac13.99" + } + }, + "lineOffers": [ + { + "appliedRules": [ + { + "id": "689de276cf3245a7bffdfa0d20500150", + "endDate": "2022-11-01T15:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE" + } + } + ] + } + ] + }, + "promotions": { + "promotionalOffers": [ + { + "promotionalOffers": [ + { + "startDate": "2022-10-18T15:00:00.000Z", + "endDate": "2022-11-01T15:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 70 + } + } + ] + } + ], + "upcomingPromotionalOffers": [] + } + }, + { + "title": "Filament", + "id": "296453e71c884f95aecf4d582cf66915", + "namespace": "89fb09a222a54e53b692e9c36e68d0a1", + "description": "Solve challenging cable-based puzzles and uncover what really happened to the crew of The Alabaster. Now with Hint System (for those ultra tricky puzzles).", + "effectiveDate": "2022-08-11T11:00:00.000Z", + "offerType": "BASE_GAME", + "expiryDate": null, + "status": "ACTIVE", + "isCodeRedemptionOnly": false, + "keyImages": [ + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/spt-assets/5a72e62648d747189d2f5e7abb47444c/filament-offer-qrwye.jpg" + }, + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/spt-assets/5a72e62648d747189d2f5e7abb47444c/download-filament-offer-mk58q.jpg" + }, + { + "type": "Thumbnail", + "url": "https://cdn1.epicgames.com/spt-assets/5a72e62648d747189d2f5e7abb47444c/download-filament-offer-mk58q.jpg" + } + ], + "seller": { + "id": "o-fnqgc5v2xczx9fgawvcejwj88z2mnx", + "name": "Kasedo Games Ltd" + }, + "productSlug": null, + "urlSlug": "323de464947e4ee5a035c525b6b78021", + "url": null, + "items": [ + { + "id": "d4fa1325ef014725a89cc40e9b99e43d", + "namespace": "89fb09a222a54e53b692e9c36e68d0a1" + } + ], + "customAttributes": [ + { + "key": "autoGeneratedPrice", + "value": "false" + }, + { + "key": "isManuallySetPCReleaseDate", + "value": "true" + } + ], + "categories": [ + { + "path": "freegames" + }, + { + "path": "games/edition/base" + }, + { + "path": "games/edition" + }, + { + "path": "games" + } + ], + "tags": [ + { + "id": "1298" + }, + { + "id": "21894" + }, + { + "id": "19847" + }, + { + "id": "1370" + }, + { + "id": "9547" + }, + { + "id": "9549" + }, + { + "id": "1263" + } + ], + "catalogNs": { + "mappings": [ + { + "pageSlug": "filament-332a92", + "pageType": "productHome" + } + ] + }, + "offerMappings": [ + { + "pageSlug": "filament-332a92", + "pageType": "productHome" + } + ], + "price": { + "totalPrice": { + "discountPrice": 1699, + "originalPrice": 1699, + "voucherDiscount": 0, + "discount": 0, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "\u20ac16.99", + "discountPrice": "\u20ac16.99", + "intermediatePrice": "\u20ac16.99" + } + }, + "lineOffers": [ + { + "appliedRules": [] + } + ] + }, + "promotions": { + "promotionalOffers": [], + "upcomingPromotionalOffers": [ + { + "promotionalOffers": [ + { + "startDate": "2022-11-03T15:00:00.000Z", + "endDate": "2022-11-10T16:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 0 + } + } + ] + } + ] + } + }, + { + "title": "Warhammer 40,000: Mechanicus - Standard Edition", + "id": "559b16fa81134dce83b5b8b7cf67b5b3", + "namespace": "144f9e231e2846d1a4381d9bb678f69d", + "description": "Take control of the most technologically advanced army in the Imperium - The Adeptus Mechanicus. Your every decision will weigh heavily on the outcome of the mission, in this turn-based tactical game. Will you be blessed by the Omnissiah?", + "effectiveDate": "2022-08-11T11:00:00.000Z", + "offerType": "BASE_GAME", + "expiryDate": null, + "status": "ACTIVE", + "isCodeRedemptionOnly": false, + "keyImages": [ + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/spt-assets/d26f2f9ea65c462dbd39040ae8389d36/warhammer-mechanicus-offer-17fnz.jpg" + }, + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/spt-assets/d26f2f9ea65c462dbd39040ae8389d36/download-warhammer-mechanicus-offer-1f6bv.jpg" + }, + { + "type": "Thumbnail", + "url": "https://cdn1.epicgames.com/spt-assets/d26f2f9ea65c462dbd39040ae8389d36/download-warhammer-mechanicus-offer-1f6bv.jpg" + } + ], + "seller": { + "id": "o-fnqgc5v2xczx9fgawvcejwj88z2mnx", + "name": "Kasedo Games Ltd" + }, + "productSlug": null, + "urlSlug": "f37159d9bd96489ab1b99bdad1ee796c", + "url": null, + "items": [ + { + "id": "f923ad9f3428472ab67baa4618c205a0", + "namespace": "144f9e231e2846d1a4381d9bb678f69d" + } + ], + "customAttributes": [ + { + "key": "autoGeneratedPrice", + "value": "false" + }, + { + "key": "isManuallySetPCReleaseDate", + "value": "true" + } + ], + "categories": [ + { + "path": "freegames" + }, + { + "path": "games/edition/base" + }, + { + "path": "games/edition" + }, + { + "path": "games" + } + ], + "tags": [ + { + "id": "21894" + }, + { + "id": "19847" + }, + { + "id": "1386" + }, + { + "id": "1115" + }, + { + "id": "9547" + }, + { + "id": "9549" + } + ], + "catalogNs": { + "mappings": [ + { + "pageSlug": "warhammer-mechanicus-0e4b71", + "pageType": "productHome" + } + ] + }, + "offerMappings": [ + { + "pageSlug": "warhammer-mechanicus-0e4b71", + "pageType": "productHome" + } + ], + "price": { + "totalPrice": { + "discountPrice": 0, + "originalPrice": 2999, + "voucherDiscount": 0, + "discount": 2999, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "\u20ac29.99", + "discountPrice": "0", + "intermediatePrice": "0" + } + }, + "lineOffers": [ + { + "appliedRules": [ + { + "id": "7a3ee39632f5458990b6a9ad295881b8", + "endDate": "2022-11-03T15:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE" + } + } + ] + } + ] + }, + "promotions": { + "promotionalOffers": [ + { + "promotionalOffers": [ + { + "startDate": "2022-10-27T15:00:00.000Z", + "endDate": "2022-11-03T15:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 0 + } + } + ] + } + ], + "upcomingPromotionalOffers": [] + } + }, + { + "title": "Fallout 3: Game of the Year Edition", + "id": "d6f01b1827c64ed388191ae507fe7c1b", + "namespace": "fa702d34a37248ba98fb17f680c085e3", + "description": "Prepare for the Future\u2122\nExperience the most acclaimed game of 2008 like never before with Fallout 3: Game of the Year Edition. Create a character of your choosing and descend into a post-apocalyptic world where every minute is a fight for survival", + "effectiveDate": "2022-10-20T15:00:00.000Z", + "offerType": "BASE_GAME", + "expiryDate": null, + "status": "ACTIVE", + "isCodeRedemptionOnly": false, + "keyImages": [ + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/offer/fa702d34a37248ba98fb17f680c085e3/EGS_Fallout3GameoftheYearEdition_BethesdaGameStudios_S2_1200x1600-e2ba392652a1f57c4feb65d6bbd1f963" + }, + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/offer/fa702d34a37248ba98fb17f680c085e3/EGS_Fallout3GameoftheYearEdition_BethesdaGameStudios_S1_2560x1440-073f5b4cf358f437a052a3c29806efa0" + }, + { + "type": "ProductLogo", + "url": "https://cdn1.epicgames.com/offer/fa702d34a37248ba98fb17f680c085e3/EGS_Fallout3GameoftheYearEdition_BethesdaGameStudios_IC1_400x400-5e37dfe1d35c9ccf25c8889fe7218613" + }, + { + "type": "Thumbnail", + "url": "https://cdn1.epicgames.com/offer/fa702d34a37248ba98fb17f680c085e3/EGS_Fallout3GameoftheYearEdition_BethesdaGameStudios_S2_1200x1600-e2ba392652a1f57c4feb65d6bbd1f963" + } + ], + "seller": { + "id": "o-bthbhn6wd7fzj73v5p4436ucn3k37u", + "name": "Bethesda Softworks LLC" + }, + "productSlug": "fallout-3-game-of-the-year-edition", + "urlSlug": "fallout-3-game-of-the-year-edition", + "url": null, + "items": [ + { + "id": "6b750e631e414927bde5b3e13b647443", + "namespace": "fa702d34a37248ba98fb17f680c085e3" + } + ], + "customAttributes": [ + { + "key": "com.epicgames.app.blacklist", + "value": "[]" + }, + { + "key": "com.epicgames.app.productSlug", + "value": "fallout-3-game-of-the-year-edition" + } + ], + "categories": [ + { + "path": "freegames" + }, + { + "path": "games" + }, + { + "path": "games/edition" + }, + { + "path": "games/edition/base" + }, + { + "path": "applications" + } + ], + "tags": [ + { + "id": "21122" + }, + { + "id": "1188" + }, + { + "id": "21894" + }, + { + "id": "21127" + }, + { + "id": "9547" + }, + { + "id": "21137" + }, + { + "id": "21138" + }, + { + "id": "21139" + }, + { + "id": "21140" + }, + { + "id": "21141" + }, + { + "id": "1367" + }, + { + "id": "1370" + }, + { + "id": "1307" + }, + { + "id": "21147" + }, + { + "id": "21148" + }, + { + "id": "1117" + }, + { + "id": "21149" + } + ], + "catalogNs": { + "mappings": [ + { + "pageSlug": "fallout-3-game-of-the-year-edition", + "pageType": "productHome" + } + ] + }, + "offerMappings": [], + "price": { + "totalPrice": { + "discountPrice": 659, + "originalPrice": 1999, + "voucherDiscount": 0, + "discount": 1340, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "\u20ac19.99", + "discountPrice": "\u20ac6.59", + "intermediatePrice": "\u20ac6.59" + } + }, + "lineOffers": [ + { + "appliedRules": [ + { + "id": "779554ee7a604b0091a4335a60b6e55a", + "endDate": "2022-11-01T15:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE" + } + } + ] + } + ] + }, + "promotions": { + "promotionalOffers": [ + { + "promotionalOffers": [ + { + "startDate": "2022-10-27T15:00:00.000Z", + "endDate": "2022-11-01T15:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 33 + } + } + ] + } + ], + "upcomingPromotionalOffers": [] + } + }, + { + "title": "Evoland Legendary Edition", + "id": "e068e168886a4a90a4e36a310e3bda32", + "namespace": "3f7bd21610f743e598fa8e955500f5b7", + "description": "Evoland Legendary Edition brings you two great and unique RPGs, with their graphic style and gameplay changing as you progress through the game!", + "effectiveDate": "2022-10-20T15:00:00.000Z", + "offerType": "BASE_GAME", + "expiryDate": null, + "status": "ACTIVE", + "isCodeRedemptionOnly": false, + "keyImages": [ + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/spt-assets/aafde465b31e4bd5a169ff1c8a164a17/evoland-legendary-edition-1y7m0.png" + }, + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/spt-assets/aafde465b31e4bd5a169ff1c8a164a17/evoland-legendary-edition-1j93v.png" + }, + { + "type": "Thumbnail", + "url": "https://cdn1.epicgames.com/spt-assets/aafde465b31e4bd5a169ff1c8a164a17/evoland-legendary-edition-1j93v.png" + } + ], + "seller": { + "id": "o-ealhln64lfep9ww929uq9qcdmbyfn4", + "name": "Shiro Games SAS" + }, + "productSlug": null, + "urlSlug": "224c60bb93864e1c8a1900bcf7d661dd", + "url": null, + "items": [ + { + "id": "c829f27d0ab0406db8edf2b97562ee93", + "namespace": "3f7bd21610f743e598fa8e955500f5b7" + } + ], + "customAttributes": [ + { + "key": "autoGeneratedPrice", + "value": "false" + }, + { + "key": "isManuallySetPCReleaseDate", + "value": "true" + } + ], + "categories": [ + { + "path": "freegames" + }, + { + "path": "games/edition" + }, + { + "path": "games" + }, + { + "path": "games/edition/base" + } + ], + "tags": [ + { + "id": "1216" + }, + { + "id": "21109" + }, + { + "id": "1367" + }, + { + "id": "1370" + }, + { + "id": "9547" + }, + { + "id": "1117" + }, + { + "id": "9549" + } + ], + "catalogNs": { + "mappings": [ + { + "pageSlug": "evoland-legendary-edition-5753ec", + "pageType": "productHome" + } + ] + }, + "offerMappings": [ + { + "pageSlug": "evoland-legendary-edition-5753ec", + "pageType": "productHome" + } + ], + "price": { + "totalPrice": { + "discountPrice": 1999, + "originalPrice": 1999, + "voucherDiscount": 0, + "discount": 0, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "\u20ac19.99", + "discountPrice": "\u20ac19.99", + "intermediatePrice": "\u20ac19.99" + } + }, + "lineOffers": [ + { + "appliedRules": [] + } + ] + }, + "promotions": null + }, + { + "title": "Saturnalia", + "id": "275d5915ebd2479f983f51025b22a1b8", + "namespace": "c749cd78da34408d8434a46271f4bb79", + "description": "A Survival Horror Adventure: as an ensemble cast, explore an isolated village of ancient ritual \u2013 its labyrinthine roads change each time you lose all your characters.", + "effectiveDate": "2022-10-27T15:00:00.000Z", + "offerType": "BASE_GAME", + "expiryDate": null, + "status": "ACTIVE", + "isCodeRedemptionOnly": false, + "keyImages": [ + { + "type": "CodeRedemption_340x440", + "url": "https://cdn1.epicgames.com/offer/c749cd78da34408d8434a46271f4bb79/EGS_Saturnalia_SantaRagione_S4_1200x1600-2216ff4aa6997dfb13d8bd4c6f2fa99e" + }, + { + "type": "DieselStoreFrontTall", + "url": "https://cdn1.epicgames.com/offer/c749cd78da34408d8434a46271f4bb79/EGS_Saturnalia_SantaRagione_S4_1200x1600-2216ff4aa6997dfb13d8bd4c6f2fa99e" + }, + { + "type": "DieselStoreFrontWide", + "url": "https://cdn1.epicgames.com/offer/c749cd78da34408d8434a46271f4bb79/EGS_Saturnalia_SantaRagione_S3_2560x1440-3cd916a7260b77c8488f8f2b0f3a51ab" + }, + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/offer/c749cd78da34408d8434a46271f4bb79/EGS_Saturnalia_SantaRagione_S4_1200x1600-2216ff4aa6997dfb13d8bd4c6f2fa99e" + }, + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/offer/c749cd78da34408d8434a46271f4bb79/EGS_Saturnalia_SantaRagione_S3_2560x1440-3cd916a7260b77c8488f8f2b0f3a51ab" + }, + { + "type": "Thumbnail", + "url": "https://cdn1.epicgames.com/offer/c749cd78da34408d8434a46271f4bb79/EGS_Saturnalia_SantaRagione_S4_1200x1600-2216ff4aa6997dfb13d8bd4c6f2fa99e" + } + ], + "seller": { + "id": "o-cjwnkas5rn476tzk72fbh2ftutnc2y", + "name": "Santa Ragione" + }, + "productSlug": "saturnalia", + "urlSlug": "saturnalia", + "url": null, + "items": [ + { + "id": "dbce8ecb6923490c9404529651251216", + "namespace": "c749cd78da34408d8434a46271f4bb79" + } + ], + "customAttributes": [ + { + "key": "com.epicgames.app.productSlug", + "value": "saturnalia" + } + ], + "categories": [ + { + "path": "freegames" + }, + { + "path": "games" + }, + { + "path": "games/edition/base" + }, + { + "path": "games/edition" + }, + { + "path": "applications" + } + ], + "tags": [ + { + "id": "1218" + }, + { + "id": "19847" + }, + { + "id": "1080" + }, + { + "id": "1370" + }, + { + "id": "9547" + }, + { + "id": "1117" + }, + { + "id": "10719" + } + ], + "catalogNs": { + "mappings": [ + { + "pageSlug": "saturnalia", + "pageType": "productHome" + } + ] + }, + "offerMappings": [], + "price": { + "totalPrice": { + "discountPrice": 0, + "originalPrice": 1999, + "voucherDiscount": 0, + "discount": 1999, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "\u20ac19.99", + "discountPrice": "0", + "intermediatePrice": "0" + } + }, + "lineOffers": [ + { + "appliedRules": [ + { + "id": "8fa8f62eac9e4cab9fe242987c0f0988", + "endDate": "2022-11-03T15:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE" + } + } + ] + } + ] + }, + "promotions": { + "promotionalOffers": [ + { + "promotionalOffers": [ + { + "startDate": "2022-10-27T15:00:00.000Z", + "endDate": "2022-11-03T15:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 0 + } + } + ] + } + ], + "upcomingPromotionalOffers": [] + } + }, + { + "title": "Maneater", + "id": "a22a7af179c54b86a93f3193ace8f7f4", + "namespace": "d5241c76f178492ea1540fce45616757", + "description": "Maneater", + "effectiveDate": "2099-01-01T00:00:00.000Z", + "offerType": "OTHERS", + "expiryDate": null, + "status": "ACTIVE", + "isCodeRedemptionOnly": true, + "keyImages": [ + { + "type": "VaultClosed", + "url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/egs-vault-tease-generic-promo-1920x1080_1920x1080-f7742c265e217510835ed14e04c48b4b" + }, + { + "type": "DieselStoreFrontTall", + "url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/egs-vault-tease-generic-promo-1920x1080_1920x1080-f7742c265e217510835ed14e04c48b4b" + }, + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/egs-vault-carousel-mobile-thumbnail-1200x1600_1200x1600-1f45bf1ceb21c1ca2947f6df5ece5346" + }, + { + "type": "VaultOpened", + "url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/egs-vault-w4-1920x1080_1920x1080-2df36fe63c18ff6fcb5febf3dd7ed06e" + }, + { + "type": "DieselStoreFrontWide", + "url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/egs-vault-w4-1920x1080_1920x1080-2df36fe63c18ff6fcb5febf3dd7ed06e" + }, + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/egs-vault-w4-1920x1080_1920x1080-2df36fe63c18ff6fcb5febf3dd7ed06e" + } + ], + "seller": { + "id": "o-ufmrk5furrrxgsp5tdngefzt5rxdcn", + "name": "Epic Dev Test Account" + }, + "productSlug": "maneater", + "urlSlug": "game-4", + "url": null, + "items": [ + { + "id": "8341d7c7e4534db7848cc428aa4cbe5a", + "namespace": "d5241c76f178492ea1540fce45616757" + } + ], + "customAttributes": [ + { + "key": "com.epicgames.app.freegames.vault.close", + "value": "[]" + }, + { + "key": "com.epicgames.app.freegames.vault.slug", + "value": "free-games" + }, + { + "key": "com.epicgames.app.freegames.vault.open", + "value": "[]" + }, + { + "key": "com.epicgames.app.productSlug", + "value": "maneater" + } + ], + "categories": [ + { + "path": "freegames/vaulted" + }, + { + "path": "freegames" + }, + { + "path": "games" + }, + { + "path": "applications" + } + ], + "tags": [], + "catalogNs": { + "mappings": [] + }, + "offerMappings": [], + "price": { + "totalPrice": { + "discountPrice": 0, + "originalPrice": 0, + "voucherDiscount": 0, + "discount": 0, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "0", + "discountPrice": "0", + "intermediatePrice": "0" + } + }, + "lineOffers": [ + { + "appliedRules": [] + } + ] + }, + "promotions": null + }, + { + "title": "Wolfenstein: The New Order", + "id": "1d41b93230e54bdd80c559d72adb7f4f", + "namespace": "d5241c76f178492ea1540fce45616757", + "description": "Wolfenstein: The New Order", + "effectiveDate": "2099-01-01T00:00:00.000Z", + "offerType": "OTHERS", + "expiryDate": null, + "status": "ACTIVE", + "isCodeRedemptionOnly": true, + "keyImages": [ + { + "type": "VaultClosed", + "url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/egs-vault-tease-generic-promo-1920x1080_1920x1080-f7742c265e217510835ed14e04c48b4b" + }, + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/egs-vault-carousel-mobile-thumbnail-1200x1600_1200x1600-1f45bf1ceb21c1ca2947f6df5ece5346" + }, + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/egs-vault-w3-1920x1080_1920x1080-4a501d33fb4ac641e3e1e290dcc0e6c1" + }, + { + "type": "DieselStoreFrontWide", + "url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/egs-vault-w3-1920x1080_1920x1080-4a501d33fb4ac641e3e1e290dcc0e6c1" + }, + { + "type": "VaultOpened", + "url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/egs-vault-w3-1920x1080_1920x1080-4a501d33fb4ac641e3e1e290dcc0e6c1" + } + ], + "seller": { + "id": "o-ufmrk5furrrxgsp5tdngefzt5rxdcn", + "name": "Epic Dev Test Account" + }, + "productSlug": "wolfenstein-the-new-order", + "urlSlug": "game-3", + "url": null, + "items": [ + { + "id": "8341d7c7e4534db7848cc428aa4cbe5a", + "namespace": "d5241c76f178492ea1540fce45616757" + } + ], + "customAttributes": [ + { + "key": "com.epicgames.app.freegames.vault.close", + "value": "[]" + }, + { + "key": "com.epicgames.app.freegames.vault.slug", + "value": "free-games" + }, + { + "key": "com.epicgames.app.freegames.vault.open", + "value": "[]" + }, + { + "key": "com.epicgames.app.productSlug", + "value": "wolfenstein-the-new-order" + } + ], + "categories": [ + { + "path": "freegames/vaulted" + }, + { + "path": "freegames" + }, + { + "path": "games" + }, + { + "path": "applications" + } + ], + "tags": [], + "catalogNs": { + "mappings": [] + }, + "offerMappings": [], + "price": { + "totalPrice": { + "discountPrice": 0, + "originalPrice": 0, + "voucherDiscount": 0, + "discount": 0, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "0", + "discountPrice": "0", + "intermediatePrice": "0" + } + }, + "lineOffers": [ + { + "appliedRules": [] + } + ] + }, + "promotions": null + } + ], + "paging": { + "count": 1000, + "total": 14 + } + } + } + }, + "extensions": {} +} diff --git a/tests/components/epic_games_store/fixtures/free_games_christmas_special.json b/tests/components/epic_games_store/fixtures/free_games_christmas_special.json new file mode 100644 index 00000000000..0c65f47d3a0 --- /dev/null +++ b/tests/components/epic_games_store/fixtures/free_games_christmas_special.json @@ -0,0 +1,253 @@ +{ + "data": { + "Catalog": { + "searchStore": { + "elements": [ + { + "title": "Cursed to Golf", + "id": "0e4551e4ae65492b88009f8a4e41d778", + "namespace": "d5241c76f178492ea1540fce45616757", + "description": "Cursed to Golf", + "effectiveDate": "2023-12-27T16:00:00.000Z", + "offerType": "OTHERS", + "expiryDate": "2023-12-28T16:00:00.000Z", + "viewableDate": "2023-12-26T15:25:00.000Z", + "status": "ACTIVE", + "isCodeRedemptionOnly": true, + "keyImages": [ + { + "type": "DieselStoreFrontWide", + "url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/Free-Game-9_1920x1080-418a8fa10dd305bb2a219a7ec869c5ef" + }, + { + "type": "VaultClosed", + "url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/Free-Game-9-teaser_1920x1080-e71ae0041736db5ac259a355cb301116" + } + ], + "seller": { + "id": "o-ufmrk5furrrxgsp5tdngefzt5rxdcn", + "name": "Epic Dev Test Account" + }, + "productSlug": "cursed-to-golf-a6bc22", + "urlSlug": "mysterygame-9", + "url": null, + "items": [ + { + "id": "8341d7c7e4534db7848cc428aa4cbe5a", + "namespace": "d5241c76f178492ea1540fce45616757" + } + ], + "customAttributes": [ + { + "key": "com.epicgames.app.freegames.vault.close", + "value": "[]" + }, + { + "key": "com.epicgames.app.blacklist", + "value": "[]" + }, + { + "key": "com.epicgames.app.freegames.vault.slug", + "value": "sales-and-specials/holiday-sale" + }, + { + "key": "com.epicgames.app.freegames.vault.open", + "value": "[]" + }, + { + "key": "com.epicgames.app.productSlug", + "value": "cursed-to-golf-a6bc22" + } + ], + "categories": [ + { + "path": "freegames/vaulted" + }, + { + "path": "freegames" + }, + { + "path": "games" + }, + { + "path": "applications" + } + ], + "tags": [], + "catalogNs": { + "mappings": [] + }, + "offerMappings": [], + "price": { + "totalPrice": { + "discountPrice": 0, + "originalPrice": 0, + "voucherDiscount": 0, + "discount": 0, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "0", + "discountPrice": "0", + "intermediatePrice": "0" + } + }, + "lineOffers": [ + { + "appliedRules": [] + } + ] + }, + "promotions": { + "promotionalOffers": [ + { + "promotionalOffers": [ + { + "startDate": "2023-12-27T16:00:00.000Z", + "endDate": "2023-12-28T16:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 0 + } + }, + { + "startDate": "2023-12-27T16:00:00.000Z", + "endDate": "2023-12-28T16:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 0 + } + } + ] + } + ], + "upcomingPromotionalOffers": [] + } + }, + { + "title": "Mystery Game Day 10", + "id": "a8c3537a579943a688e3bd355ae36209", + "namespace": "d5241c76f178492ea1540fce45616757", + "description": "Mystery Game Day 10", + "effectiveDate": "2099-01-01T16:00:00.000Z", + "offerType": "OTHERS", + "expiryDate": null, + "viewableDate": "2023-12-27T15:25:00.000Z", + "status": "ACTIVE", + "isCodeRedemptionOnly": true, + "keyImages": [ + { + "type": "VaultClosed", + "url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/Free-Game-10-teaser_1920x1080-3ea48042a44263bf1a0a59c725b6d95b" + }, + { + "type": "DieselStoreFrontWide", + "url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/Free-Game-10-teaser_1920x1080-3ea48042a44263bf1a0a59c725b6d95b" + } + ], + "seller": { + "id": "o-ufmrk5furrrxgsp5tdngefzt5rxdcn", + "name": "Epic Dev Test Account" + }, + "productSlug": "[]", + "urlSlug": "mysterygame-10", + "url": null, + "items": [ + { + "id": "8341d7c7e4534db7848cc428aa4cbe5a", + "namespace": "d5241c76f178492ea1540fce45616757" + } + ], + "customAttributes": [ + { + "key": "com.epicgames.app.freegames.vault.close", + "value": "[]" + }, + { + "key": "com.epicgames.app.blacklist", + "value": "[]" + }, + { + "key": "com.epicgames.app.freegames.vault.slug", + "value": "sales-and-specials/holiday-sale" + }, + { + "key": "com.epicgames.app.freegames.vault.open", + "value": "[]" + }, + { + "key": "com.epicgames.app.productSlug", + "value": "[]" + } + ], + "categories": [ + { + "path": "freegames/vaulted" + }, + { + "path": "freegames" + }, + { + "path": "games" + }, + { + "path": "applications" + } + ], + "tags": [], + "catalogNs": { + "mappings": [] + }, + "offerMappings": [], + "price": { + "totalPrice": { + "discountPrice": 0, + "originalPrice": 0, + "voucherDiscount": 0, + "discount": 0, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "0", + "discountPrice": "0", + "intermediatePrice": "0" + } + }, + "lineOffers": [ + { + "appliedRules": [] + } + ] + }, + "promotions": { + "promotionalOffers": [], + "upcomingPromotionalOffers": [ + { + "promotionalOffers": [ + { + "startDate": "2023-12-28T16:00:00.000Z", + "endDate": "2023-12-29T16:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 0 + } + } + ] + } + ] + } + } + ], + "paging": { + "count": 1000, + "total": 2 + } + } + } + }, + "extensions": {} +} diff --git a/tests/components/epic_games_store/fixtures/free_games_one.json b/tests/components/epic_games_store/fixtures/free_games_one.json new file mode 100644 index 00000000000..48cd64f68d4 --- /dev/null +++ b/tests/components/epic_games_store/fixtures/free_games_one.json @@ -0,0 +1,658 @@ +{ + "data": { + "Catalog": { + "searchStore": { + "elements": [ + { + "title": "Borderlands 3 Season Pass", + "id": "c3913a91e07b43cfbbbcfd8244c86dcc", + "namespace": "catnip", + "description": "Prolongez votre aventure dans Borderlands\u00a03 avec le Season Pass, regroupant des \u00e9l\u00e9ments cosm\u00e9tiques exclusifs et quatre histoires additionnelles, pour encore plus de missions et de d\u00e9fis\u00a0!", + "effectiveDate": "2019-09-11T12:00:00.000Z", + "offerType": "DLC", + "expiryDate": null, + "status": "ACTIVE", + "isCodeRedemptionOnly": false, + "keyImages": [ + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/offer/catnip/Diesel_productv2_borderlands-3_season-pass_BL3_SEASONPASS_Hero-3840x2160-4411e63a005a43811a2bc516ae7ec584598fd4aa-3840x2160-b8988ebb0f3d9159671e8968af991f30_3840x2160-b8988ebb0f3d9159671e8968af991f30" + }, + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/offer/catnip/2KGMKT_BL3_Season_Pass_EGS_1200x1600_1200x1600-a7438a079c5576d328a74b9121278075" + }, + { + "type": "CodeRedemption_340x440", + "url": "https://cdn1.epicgames.com/offer/catnip/2KGMKT_BL3_Season_Pass_EGS_1200x1600_1200x1600-a7438a079c5576d328a74b9121278075" + }, + { + "type": "Thumbnail", + "url": "https://cdn1.epicgames.com/offer/catnip/2KGMKT_BL3_Season_Pass_EGS_1200x1600_1200x1600-a7438a079c5576d328a74b9121278075" + } + ], + "seller": { + "id": "o-37m6jbj5wcvrcvm4wusv7nazdfvbjk", + "name": "2K Games, Inc." + }, + "productSlug": "borderlands-3/season-pass", + "urlSlug": "borderlands-3--season-pass", + "url": null, + "items": [ + { + "id": "e9fdc1a9f47b4a5e8e63841c15de2b12", + "namespace": "catnip" + }, + { + "id": "fbc46bb6056940d2847ee1e80037a9af", + "namespace": "catnip" + }, + { + "id": "ff8e1152ddf742b68f9ac0cecd378917", + "namespace": "catnip" + }, + { + "id": "939e660825764e208938ab4f26b4da56", + "namespace": "catnip" + }, + { + "id": "4c43a9a691114ccd91c1884ab18f4e27", + "namespace": "catnip" + }, + { + "id": "3a6a3f9b351b4b599808df3267669b83", + "namespace": "catnip" + }, + { + "id": "ab030a9f53f3428fb2baf2ddbb0bb5ac", + "namespace": "catnip" + }, + { + "id": "ff96eef22b0e4c498e8ed80ac0030325", + "namespace": "catnip" + }, + { + "id": "5021e93a73374d6db1c1ce6c92234f8f", + "namespace": "catnip" + }, + { + "id": "9c0b1eb3265340678dff0fcb106402b1", + "namespace": "catnip" + }, + { + "id": "8c826db6e14f44aeac8816e1bd593632", + "namespace": "catnip" + } + ], + "customAttributes": [ + { + "key": "com.epicgames.app.blacklist", + "value": "SA" + }, + { + "key": "publisherName", + "value": "2K" + }, + { + "key": "developerName", + "value": "Gearbox Software" + }, + { + "key": "com.epicgames.app.productSlug", + "value": "borderlands-3/season-pass" + } + ], + "categories": [ + { + "path": "addons" + }, + { + "path": "freegames" + }, + { + "path": "addons/durable" + }, + { + "path": "applications" + } + ], + "tags": [ + { + "id": "1264" + }, + { + "id": "16004" + }, + { + "id": "14869" + }, + { + "id": "26789" + }, + { + "id": "1367" + }, + { + "id": "1370" + }, + { + "id": "9547" + }, + { + "id": "9549" + }, + { + "id": "1294" + } + ], + "catalogNs": { + "mappings": [ + { + "pageSlug": "borderlands-3", + "pageType": "productHome" + } + ] + }, + "offerMappings": [ + { + "pageSlug": "borderlands-3--season-pass", + "pageType": "addon--cms-hybrid" + } + ], + "price": { + "totalPrice": { + "discountPrice": 4999, + "originalPrice": 4999, + "voucherDiscount": 0, + "discount": 0, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "49,99\u00a0\u20ac", + "discountPrice": "49,99\u00a0\u20ac", + "intermediatePrice": "49,99\u00a0\u20ac" + } + }, + "lineOffers": [ + { + "appliedRules": [] + } + ] + }, + "promotions": { + "promotionalOffers": [], + "upcomingPromotionalOffers": [ + { + "promotionalOffers": [ + { + "startDate": "2023-03-09T16:00:00.000Z", + "endDate": "2023-03-16T16:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 30 + } + }, + { + "startDate": "2023-03-09T16:00:00.000Z", + "endDate": "2023-03-16T16:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 25 + } + }, + { + "startDate": "2023-03-09T16:00:00.000Z", + "endDate": "2023-03-16T16:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 25 + } + }, + { + "startDate": "2023-03-09T16:00:00.000Z", + "endDate": "2023-03-16T16:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 30 + } + } + ] + } + ] + } + }, + { + "title": "Call of the Sea", + "id": "92da5d8d918543b6b408e36d9af81765", + "namespace": "5e427319eea1401ab20c6cd78a4163c4", + "description": "Call of the Sea is an otherworldly tale of mystery and love set in the 1930s South Pacific. Explore a lush island paradise, solve puzzles and unlock secrets in the hunt for your husband\u2019s missing expedition.", + "effectiveDate": "2022-02-17T15:00:00.000Z", + "offerType": "BASE_GAME", + "expiryDate": null, + "status": "ACTIVE", + "isCodeRedemptionOnly": false, + "keyImages": [ + { + "type": "DieselStoreFrontWide", + "url": "https://cdn1.epicgames.com/salesEvent/salesEvent/EGS_CalloftheSea_OutoftheBlue_S1_2560x1440-204699c6410deef9c18be0ee392f8335" + }, + { + "type": "DieselStoreFrontTall", + "url": "https://cdn1.epicgames.com/salesEvent/salesEvent/EGS_CalloftheSea_OutoftheBlue_S2_1200x1600-db63acf0c479c185e0ef8f8e73c8f0d8" + }, + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/salesEvent/salesEvent/EGS_CalloftheSea_OutoftheBlue_S5_1920x1080-7b22dfebdd9fcdde6e526c5dc4c16eb1" + }, + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/salesEvent/salesEvent/EGS_CalloftheSea_OutoftheBlue_S2_1200x1600-db63acf0c479c185e0ef8f8e73c8f0d8" + }, + { + "type": "CodeRedemption_340x440", + "url": "https://cdn1.epicgames.com/salesEvent/salesEvent/EGS_CalloftheSea_OutoftheBlue_S2_1200x1600-db63acf0c479c185e0ef8f8e73c8f0d8" + }, + { + "type": "Thumbnail", + "url": "https://cdn1.epicgames.com/salesEvent/salesEvent/EGS_CalloftheSea_OutoftheBlue_S2_1200x1600-db63acf0c479c185e0ef8f8e73c8f0d8" + } + ], + "seller": { + "id": "o-fay4ghw9hhamujs53rfhy83ffexb7k", + "name": "Raw Fury" + }, + "productSlug": "call-of-the-sea", + "urlSlug": "call-of-the-sea", + "url": null, + "items": [ + { + "id": "cbc9c76c4bfc4bc6b28abb3afbcbf07a", + "namespace": "5e427319eea1401ab20c6cd78a4163c4" + } + ], + "customAttributes": [ + { + "key": "com.epicgames.app.productSlug", + "value": "call-of-the-sea" + } + ], + "categories": [ + { + "path": "freegames" + }, + { + "path": "games" + }, + { + "path": "games/edition" + }, + { + "path": "games/edition/base" + }, + { + "path": "applications" + } + ], + "tags": [ + { + "id": "1296" + }, + { + "id": "1298" + }, + { + "id": "21894" + }, + { + "id": "1370" + }, + { + "id": "9547" + }, + { + "id": "1117" + } + ], + "catalogNs": { + "mappings": [ + { + "pageSlug": "call-of-the-sea", + "pageType": "productHome" + } + ] + }, + "offerMappings": [], + "price": { + "totalPrice": { + "discountPrice": 1999, + "originalPrice": 1999, + "voucherDiscount": 0, + "discount": 0, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "19,99\u00a0\u20ac", + "discountPrice": "19,99\u00a0\u20ac", + "intermediatePrice": "19,99\u00a0\u20ac" + } + }, + "lineOffers": [ + { + "appliedRules": [] + } + ] + }, + "promotions": { + "promotionalOffers": [], + "upcomingPromotionalOffers": [ + { + "promotionalOffers": [ + { + "startDate": "2023-03-09T16:00:00.000Z", + "endDate": "2023-03-16T16:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 60 + } + }, + { + "startDate": "2023-03-09T16:00:00.000Z", + "endDate": "2023-03-16T16:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 0 + } + }, + { + "startDate": "2023-03-09T16:00:00.000Z", + "endDate": "2023-03-16T16:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 60 + } + } + ] + } + ] + } + }, + { + "title": "Rise of Industry", + "id": "c04a2ab8ff4442cba0a41fb83453e701", + "namespace": "9f101e25b1a9427a9e6971d2b21c5f82", + "description": "Mettez vos comp\u00e9tences entrepreneuriales \u00e0 l'\u00e9preuve en cr\u00e9ant et en optimisant des cha\u00eenes de production complexes tout en gardant un \u0153il sur les r\u00e9sultats financiers. \u00c0 l'aube du 20e si\u00e8cle, appr\u00eatez-vous \u00e0 entrer dans un \u00e2ge d'or industriel, ou une d\u00e9pression historique.", + "effectiveDate": "2022-08-11T11:00:00.000Z", + "offerType": "BASE_GAME", + "expiryDate": null, + "status": "ACTIVE", + "isCodeRedemptionOnly": false, + "keyImages": [ + { + "type": "OfferImageWide", + "url": "https://cdn1.epicgames.com/spt-assets/a6aeec29591b4b56b4383b4d2d7d0e1e/rise-of-industry-offer-1p22f.jpg" + }, + { + "type": "OfferImageTall", + "url": "https://cdn1.epicgames.com/spt-assets/a6aeec29591b4b56b4383b4d2d7d0e1e/download-rise-of-industry-offer-1uujr.jpg" + }, + { + "type": "Thumbnail", + "url": "https://cdn1.epicgames.com/spt-assets/a6aeec29591b4b56b4383b4d2d7d0e1e/download-rise-of-industry-offer-1uujr.jpg" + } + ], + "seller": { + "id": "o-fnqgc5v2xczx9fgawvcejwj88z2mnx", + "name": "Kasedo Games Ltd" + }, + "productSlug": null, + "urlSlug": "f88fedc022fe488caaedaa5c782ff90d", + "url": null, + "items": [ + { + "id": "9f5b48a778824e6aa330d2c1a47f41b2", + "namespace": "9f101e25b1a9427a9e6971d2b21c5f82" + } + ], + "customAttributes": [ + { + "key": "autoGeneratedPrice", + "value": "false" + }, + { + "key": "isManuallySetPCReleaseDate", + "value": "true" + } + ], + "categories": [ + { + "path": "freegames" + }, + { + "path": "games/edition/base" + }, + { + "path": "games/edition" + }, + { + "path": "games" + } + ], + "tags": [ + { + "id": "26789" + }, + { + "id": "19847" + }, + { + "id": "1370" + }, + { + "id": "1115" + }, + { + "id": "9547" + }, + { + "id": "10719" + } + ], + "catalogNs": { + "mappings": [ + { + "pageSlug": "rise-of-industry-0af838", + "pageType": "productHome" + } + ] + }, + "offerMappings": [ + { + "pageSlug": "rise-of-industry-0af838", + "pageType": "productHome" + } + ], + "price": { + "totalPrice": { + "discountPrice": 0, + "originalPrice": 2999, + "voucherDiscount": 0, + "discount": 2999, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "29,99\u00a0\u20ac", + "discountPrice": "0", + "intermediatePrice": "0" + } + }, + "lineOffers": [ + { + "appliedRules": [ + { + "id": "a19d30dc34f44923993e68b82b75a084", + "endDate": "2023-03-09T16:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE" + } + } + ] + } + ] + }, + "promotions": { + "promotionalOffers": [ + { + "promotionalOffers": [ + { + "startDate": "2023-03-02T16:00:00.000Z", + "endDate": "2023-03-09T16:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 0 + } + } + ] + } + ], + "upcomingPromotionalOffers": [ + { + "promotionalOffers": [ + { + "startDate": "2023-03-09T16:00:00.000Z", + "endDate": "2023-03-16T16:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 25 + } + }, + { + "startDate": "2023-03-09T16:00:00.000Z", + "endDate": "2023-03-16T16:00:00.000Z", + "discountSetting": { + "discountType": "PERCENTAGE", + "discountPercentage": 25 + } + } + ] + } + ] + } + }, + { + "title": "Dishonored - Definitive Edition", + "id": "4d25d74b88d1474a8ab21ffb88ca6d37", + "namespace": "d5241c76f178492ea1540fce45616757", + "description": "Experience the definitive Dishonored collection. This complete compilation includes Dishonored as well as all of its additional content - Dunwall City Trials, The Knife of Dunwall, The Brigmore Witches and Void Walker\u2019s Arsenal.", + "effectiveDate": "2099-01-01T00:00:00.000Z", + "offerType": "OTHERS", + "expiryDate": null, + "status": "ACTIVE", + "isCodeRedemptionOnly": true, + "keyImages": [ + { + "type": "VaultClosed", + "url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/15days-day15-wrapped-desktop-carousel-image_1920x1080-ebecfa7c79f02a9de5bca79560bee953" + }, + { + "type": "DieselStoreFrontWide", + "url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/15days-day15-Unwrapped-desktop-carousel-image1_1920x1080-1992edb42bb8554ddeb14d430ba3f858" + }, + { + "type": "DieselStoreFrontTall", + "url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/DAY15-carousel-mobile-unwrapped-image1_1200x1600-9716d77667d2a82931c55a4e4130989e" + } + ], + "seller": { + "id": "o-ufmrk5furrrxgsp5tdngefzt5rxdcn", + "name": "Epic Dev Test Account" + }, + "productSlug": "dishonored-definitive-edition", + "urlSlug": "mystery-game15", + "url": null, + "items": [ + { + "id": "8341d7c7e4534db7848cc428aa4cbe5a", + "namespace": "d5241c76f178492ea1540fce45616757" + } + ], + "customAttributes": [ + { + "key": "com.epicgames.app.freegames.vault.close", + "value": "[]" + }, + { + "key": "com.epicgames.app.freegames.vault.slug", + "value": "sales-and-specials/holiday-sale" + }, + { + "key": "com.epicgames.app.blacklist", + "value": "[]" + }, + { + "key": "com.epicgames.app.freegames.vault.open", + "value": "[]" + }, + { + "key": "com.epicgames.app.productSlug", + "value": "dishonored-definitive-edition" + } + ], + "categories": [ + { + "path": "freegames/vaulted" + }, + { + "path": "freegames" + }, + { + "path": "games" + }, + { + "path": "applications" + } + ], + "tags": [], + "catalogNs": { + "mappings": [] + }, + "offerMappings": [], + "price": { + "totalPrice": { + "discountPrice": 0, + "originalPrice": 0, + "voucherDiscount": 0, + "discount": 0, + "currencyCode": "EUR", + "currencyInfo": { + "decimals": 2 + }, + "fmtPrice": { + "originalPrice": "0", + "discountPrice": "0", + "intermediatePrice": "0" + } + }, + "lineOffers": [ + { + "appliedRules": [] + } + ] + }, + "promotions": null + } + ], + "paging": { + "count": 1000, + "total": 4 + } + } + } + }, + "extensions": {} +} diff --git a/tests/components/epic_games_store/test_calendar.py b/tests/components/epic_games_store/test_calendar.py new file mode 100644 index 00000000000..46ca974f85c --- /dev/null +++ b/tests/components/epic_games_store/test_calendar.py @@ -0,0 +1,162 @@ +"""Tests for the Epic Games Store calendars.""" + +from unittest.mock import Mock + +from freezegun.api import FrozenDateTimeFactory + +from homeassistant.components.calendar import ( + DOMAIN as CALENDAR_DOMAIN, + EVENT_END_DATETIME, + EVENT_START_DATETIME, + SERVICE_GET_EVENTS, +) +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant +from homeassistant.util import dt as dt_util + +from .common import setup_platform + +from tests.common import async_fire_time_changed + + +async def test_setup_component(hass: HomeAssistant, service_multiple: Mock) -> None: + """Test setup component.""" + await setup_platform(hass, CALENDAR_DOMAIN) + + state = hass.states.get("calendar.epic_games_store_discount_games") + assert state.name == "Epic Games Store Discount games" + state = hass.states.get("calendar.epic_games_store_free_games") + assert state.name == "Epic Games Store Free games" + + +async def test_discount_games( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + service_multiple: Mock, +) -> None: + """Test discount games calendar.""" + freezer.move_to("2022-10-15T00:00:00.000Z") + + await setup_platform(hass, CALENDAR_DOMAIN) + + state = hass.states.get("calendar.epic_games_store_discount_games") + assert state.state == STATE_OFF + + freezer.move_to("2022-10-30T00:00:00.000Z") + async_fire_time_changed(hass) + + state = hass.states.get("calendar.epic_games_store_discount_games") + assert state.state == STATE_ON + + cal_attrs = dict(state.attributes) + assert cal_attrs == { + "friendly_name": "Epic Games Store Discount games", + "message": "Shadow of the Tomb Raider: Definitive Edition", + "all_day": False, + "start_time": "2022-10-18 08:00:00", + "end_time": "2022-11-01 08:00:00", + "location": "", + "description": "In Shadow of the Tomb Raider Definitive Edition experience the final chapter of Lara\u2019s origin as she is forged into the Tomb Raider she is destined to be.\n\nhttps://store.epicgames.com/fr/p/shadow-of-the-tomb-raider", + } + + +async def test_free_games( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + service_multiple: Mock, +) -> None: + """Test free games calendar.""" + freezer.move_to("2022-10-30T00:00:00.000Z") + + await setup_platform(hass, CALENDAR_DOMAIN) + + state = hass.states.get("calendar.epic_games_store_free_games") + assert state.state == STATE_ON + + cal_attrs = dict(state.attributes) + assert cal_attrs == { + "friendly_name": "Epic Games Store Free games", + "message": "Warhammer 40,000: Mechanicus - Standard Edition", + "all_day": False, + "start_time": "2022-10-27 08:00:00", + "end_time": "2022-11-03 08:00:00", + "location": "", + "description": "Take control of the most technologically advanced army in the Imperium - The Adeptus Mechanicus. Your every decision will weigh heavily on the outcome of the mission, in this turn-based tactical game. Will you be blessed by the Omnissiah?\n\nhttps://store.epicgames.com/fr/p/warhammer-mechanicus-0e4b71", + } + + +async def test_attribute_not_found( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + service_attribute_not_found: Mock, +) -> None: + """Test setup calendars with attribute not found error.""" + freezer.move_to("2023-10-12T00:00:00.000Z") + + await setup_platform(hass, CALENDAR_DOMAIN) + + state = hass.states.get("calendar.epic_games_store_discount_games") + assert state.name == "Epic Games Store Discount games" + state = hass.states.get("calendar.epic_games_store_free_games") + assert state.name == "Epic Games Store Free games" + assert state.state == STATE_ON + + +async def test_christmas_special( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + service_christmas_special: Mock, +) -> None: + """Test setup calendars with Christmas special case.""" + freezer.move_to("2023-12-28T00:00:00.000Z") + + await setup_platform(hass, CALENDAR_DOMAIN) + + state = hass.states.get("calendar.epic_games_store_discount_games") + assert state.name == "Epic Games Store Discount games" + assert state.state == STATE_OFF + + state = hass.states.get("calendar.epic_games_store_free_games") + assert state.name == "Epic Games Store Free games" + assert state.state == STATE_ON + + +async def test_get_events( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + service_multiple: Mock, +) -> None: + """Test setup component with calendars.""" + freezer.move_to("2022-10-30T00:00:00.000Z") + + await setup_platform(hass, CALENDAR_DOMAIN) + + # 1 week in range of data + result = await hass.services.async_call( + CALENDAR_DOMAIN, + SERVICE_GET_EVENTS, + { + ATTR_ENTITY_ID: ["calendar.epic_games_store_discount_games"], + EVENT_START_DATETIME: dt_util.parse_datetime("2022-10-20T00:00:00.000Z"), + EVENT_END_DATETIME: dt_util.parse_datetime("2022-10-27T00:00:00.000Z"), + }, + blocking=True, + return_response=True, + ) + + assert len(result["calendar.epic_games_store_discount_games"]["events"]) == 3 + + # 1 week out of range of data + result = await hass.services.async_call( + CALENDAR_DOMAIN, + SERVICE_GET_EVENTS, + { + ATTR_ENTITY_ID: ["calendar.epic_games_store_discount_games"], + EVENT_START_DATETIME: dt_util.parse_datetime("1970-01-01T00:00:00.000Z"), + EVENT_END_DATETIME: dt_util.parse_datetime("1970-01-08T00:00:00.000Z"), + }, + blocking=True, + return_response=True, + ) + + assert len(result["calendar.epic_games_store_discount_games"]["events"]) == 0 diff --git a/tests/components/epic_games_store/test_config_flow.py b/tests/components/epic_games_store/test_config_flow.py new file mode 100644 index 00000000000..83e9cf9e99e --- /dev/null +++ b/tests/components/epic_games_store/test_config_flow.py @@ -0,0 +1,142 @@ +"""Test the Epic Games Store config flow.""" + +from http.client import HTTPException +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.epic_games_store.config_flow import get_default_language +from homeassistant.components.epic_games_store.const import DOMAIN +from homeassistant.const import CONF_COUNTRY, CONF_LANGUAGE +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from .const import ( + DATA_ERROR_ATTRIBUTE_NOT_FOUND, + DATA_ERROR_WRONG_COUNTRY, + DATA_FREE_GAMES, + MOCK_COUNTRY, + MOCK_LANGUAGE, +) + + +async def test_default_language(hass: HomeAssistant) -> None: + """Test we get the form.""" + hass.config.language = "fr" + hass.config.country = "FR" + assert get_default_language(hass) == "fr" + + hass.config.language = "es" + hass.config.country = "ES" + assert get_default_language(hass) == "es-ES" + + hass.config.language = "en" + hass.config.country = "AZ" + assert get_default_language(hass) is None + + +async def test_form(hass: HomeAssistant) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.epic_games_store.config_flow.EpicGamesStoreAPI.get_free_games", + return_value=DATA_FREE_GAMES, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_LANGUAGE: MOCK_LANGUAGE, + CONF_COUNTRY: MOCK_COUNTRY, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["result"].unique_id == f"freegames-{MOCK_LANGUAGE}-{MOCK_COUNTRY}" + assert ( + result2["title"] + == f"Epic Games Store - Free Games ({MOCK_LANGUAGE}-{MOCK_COUNTRY})" + ) + assert result2["data"] == { + CONF_LANGUAGE: MOCK_LANGUAGE, + CONF_COUNTRY: MOCK_COUNTRY, + } + + +async def test_form_cannot_connect(hass: HomeAssistant) -> None: + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.epic_games_store.config_flow.EpicGamesStoreAPI.get_free_games", + side_effect=HTTPException, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_LANGUAGE: MOCK_LANGUAGE, + CONF_COUNTRY: MOCK_COUNTRY, + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "unknown"} + + +async def test_form_cannot_connect_wrong_param(hass: HomeAssistant) -> None: + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.epic_games_store.config_flow.EpicGamesStoreAPI.get_free_games", + return_value=DATA_ERROR_WRONG_COUNTRY, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_LANGUAGE: MOCK_LANGUAGE, + CONF_COUNTRY: MOCK_COUNTRY, + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "unknown"} + + +async def test_form_service_error(hass: HomeAssistant) -> None: + """Test we handle service error gracefully.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.epic_games_store.config_flow.EpicGamesStoreAPI.get_free_games", + return_value=DATA_ERROR_ATTRIBUTE_NOT_FOUND, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_LANGUAGE: MOCK_LANGUAGE, + CONF_COUNTRY: MOCK_COUNTRY, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["result"].unique_id == f"freegames-{MOCK_LANGUAGE}-{MOCK_COUNTRY}" + assert ( + result2["title"] + == f"Epic Games Store - Free Games ({MOCK_LANGUAGE}-{MOCK_COUNTRY})" + ) + assert result2["data"] == { + CONF_LANGUAGE: MOCK_LANGUAGE, + CONF_COUNTRY: MOCK_COUNTRY, + } diff --git a/tests/components/epic_games_store/test_helper.py b/tests/components/epic_games_store/test_helper.py new file mode 100644 index 00000000000..155ccb7d211 --- /dev/null +++ b/tests/components/epic_games_store/test_helper.py @@ -0,0 +1,74 @@ +"""Tests for the Epic Games Store helpers.""" + +from typing import Any + +import pytest + +from homeassistant.components.epic_games_store.helper import ( + format_game_data, + get_game_url, + is_free_game, +) + +from .const import DATA_ERROR_ATTRIBUTE_NOT_FOUND, DATA_FREE_GAMES_ONE + +FREE_GAMES_API = DATA_FREE_GAMES_ONE["data"]["Catalog"]["searchStore"]["elements"] +FREE_GAME = FREE_GAMES_API[2] +NOT_FREE_GAME = FREE_GAMES_API[0] + + +def test_format_game_data() -> None: + """Test game data format.""" + game_data = format_game_data(FREE_GAME, "fr") + assert game_data + assert game_data["title"] + assert game_data["description"] + assert game_data["released_at"] + assert game_data["original_price"] + assert game_data["publisher"] + assert game_data["url"] + assert game_data["img_portrait"] + assert game_data["img_landscape"] + assert game_data["discount_type"] == "free" + assert game_data["discount_start_at"] + assert game_data["discount_end_at"] + + +@pytest.mark.parametrize( + ("raw_game_data", "expected_result"), + [ + ( + DATA_ERROR_ATTRIBUTE_NOT_FOUND["data"]["Catalog"]["searchStore"][ + "elements" + ][1], + "/p/destiny-2--bungie-30th-anniversary-pack", + ), + ( + DATA_ERROR_ATTRIBUTE_NOT_FOUND["data"]["Catalog"]["searchStore"][ + "elements" + ][4], + "/bundles/qube-ultimate-bundle", + ), + ( + DATA_ERROR_ATTRIBUTE_NOT_FOUND["data"]["Catalog"]["searchStore"][ + "elements" + ][5], + "/p/mystery-game-7", + ), + ], +) +def test_get_game_url(raw_game_data: dict[str, Any], expected_result: bool) -> None: + """Test to get the game URL.""" + assert get_game_url(raw_game_data, "fr").endswith(expected_result) + + +@pytest.mark.parametrize( + ("raw_game_data", "expected_result"), + [ + (FREE_GAME, True), + (NOT_FREE_GAME, False), + ], +) +def test_is_free_game(raw_game_data: dict[str, Any], expected_result: bool) -> None: + """Test if this game is free.""" + assert is_free_game(raw_game_data) == expected_result From 46941adb51975c73930ca09da80340f016b41dab Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 22 Apr 2024 20:30:14 +1200 Subject: [PATCH 741/967] Bump aioesphomeapi to 24.2.0 (#115943) --- 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 e700dddbb96..0e9a2bdc87f 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -15,7 +15,7 @@ "iot_class": "local_push", "loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"], "requirements": [ - "aioesphomeapi==24.1.0", + "aioesphomeapi==24.2.0", "esphome-dashboard-api==1.2.3", "bleak-esphome==1.0.0" ], diff --git a/requirements_all.txt b/requirements_all.txt index 055db11d63a..fa3e5893eef 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.1.0 +aioesphomeapi==24.2.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ff19a6a5c89..8a10ee1c176 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.1.0 +aioesphomeapi==24.2.0 # homeassistant.components.flo aioflo==2021.11.0 From 09ae8b9f52cadb1fca68056ca368a61d1bc331a1 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 22 Apr 2024 10:41:26 +0200 Subject: [PATCH 742/967] Introduce base location entity for totalconnect (#115938) * Introduce base location entity for totalconnect * Update homeassistant/components/totalconnect/entity.py Co-authored-by: TheJulianJES --------- Co-authored-by: TheJulianJES --- .../totalconnect/alarm_control_panel.py | 58 +++++++------------ .../components/totalconnect/binary_sensor.py | 9 ++- .../components/totalconnect/entity.py | 20 +++++++ 3 files changed, 45 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/totalconnect/alarm_control_panel.py b/homeassistant/components/totalconnect/alarm_control_panel.py index fcafd47037d..9b2abedbf52 100644 --- a/homeassistant/components/totalconnect/alarm_control_panel.py +++ b/homeassistant/components/totalconnect/alarm_control_panel.py @@ -4,9 +4,12 @@ from __future__ import annotations from total_connect_client import ArmingHelper from total_connect_client.exceptions import BadResultCodeError, UsercodeInvalid +from total_connect_client.location import TotalConnectLocation -import homeassistant.components.alarm_control_panel as alarm -from homeassistant.components.alarm_control_panel import AlarmControlPanelEntityFeature +from homeassistant.components.alarm_control_panel import ( + AlarmControlPanelEntity, + AlarmControlPanelEntityFeature, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, @@ -21,12 +24,11 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_platform -from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TotalConnectDataUpdateCoordinator from .const import DOMAIN -from .entity import TotalConnectEntity +from .entity import TotalConnectLocationEntity SERVICE_ALARM_ARM_AWAY_INSTANT = "arm_away_instant" SERVICE_ALARM_ARM_HOME_INSTANT = "arm_home_instant" @@ -40,14 +42,12 @@ async def async_setup_entry( coordinator: TotalConnectDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - for location_id, location in coordinator.client.locations.items(): - location_name = location.location_name + for location in coordinator.client.locations.values(): alarms.extend( TotalConnectAlarm( - coordinator=coordinator, - name=location_name, - location_id=location_id, - partition_id=partition_id, + coordinator, + location, + partition_id, ) for partition_id in location.partitions ) @@ -70,8 +70,8 @@ async def async_setup_entry( ) -class TotalConnectAlarm(TotalConnectEntity, alarm.AlarmControlPanelEntity): - """Represent an TotalConnect status.""" +class TotalConnectAlarm(TotalConnectLocationEntity, AlarmControlPanelEntity): + """Represent a TotalConnect alarm panel.""" _attr_supported_features = ( AlarmControlPanelEntityFeature.ARM_HOME @@ -82,19 +82,13 @@ class TotalConnectAlarm(TotalConnectEntity, alarm.AlarmControlPanelEntity): def __init__( self, coordinator: TotalConnectDataUpdateCoordinator, - name, - location_id, - partition_id, + location: TotalConnectLocation, + partition_id: int, ) -> None: """Initialize the TotalConnect status.""" - super().__init__(coordinator) - self._location_id = location_id - self._location = coordinator.client.locations[location_id] + super().__init__(coordinator, location) self._partition_id = partition_id self._partition = self._location.partitions[partition_id] - self._device = self._location.devices[self._location.security_device_id] - self._state: str | None = None - self._attr_extra_state_attributes = {} """ Set unique_id to location_id for partition 1 to avoid breaking change @@ -102,27 +96,18 @@ class TotalConnectAlarm(TotalConnectEntity, alarm.AlarmControlPanelEntity): Add _# for partition 2 and beyond. """ if partition_id == 1: - self._attr_name = name - self._attr_unique_id = f"{location_id}" + self._attr_name = self.device.name + self._attr_unique_id = str(location.location_id) else: - self._attr_name = f"{name} partition {partition_id}" - self._attr_unique_id = f"{location_id}_{partition_id}" - - @property - def device_info(self) -> DeviceInfo: - """Return device info.""" - return DeviceInfo( - identifiers={(DOMAIN, self._device.serial_number)}, - name=self._device.name, - serial_number=self._device.serial_number, - ) + self._attr_name = f"{self.device.name} partition {partition_id}" + self._attr_unique_id = f"{location.location_id}_{partition_id}" @property def state(self) -> str | None: """Return the state of the device.""" attr = { "location_name": self.name, - "location_id": self._location_id, + "location_id": self._location.location_id, "partition": self._partition_id, "ac_loss": self._location.ac_loss, "low_battery": self._location.low_battery, @@ -156,10 +141,9 @@ class TotalConnectAlarm(TotalConnectEntity, alarm.AlarmControlPanelEntity): state = STATE_ALARM_TRIGGERED attr["triggered_source"] = "Carbon Monoxide" - self._state = state self._attr_extra_state_attributes = attr - return self._state + return state async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" diff --git a/homeassistant/components/totalconnect/binary_sensor.py b/homeassistant/components/totalconnect/binary_sensor.py index 18340d5d6d3..9ff25e07d03 100644 --- a/homeassistant/components/totalconnect/binary_sensor.py +++ b/homeassistant/components/totalconnect/binary_sensor.py @@ -19,7 +19,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TotalConnectDataUpdateCoordinator from .const import DOMAIN -from .entity import TotalConnectEntity, TotalConnectZoneEntity +from .entity import TotalConnectLocationEntity, TotalConnectZoneEntity LOW_BATTERY = "low_battery" TAMPER = "tamper" @@ -181,7 +181,7 @@ class TotalConnectZoneBinarySensor(TotalConnectZoneEntity, BinarySensorEntity): return super().device_class -class TotalConnectAlarmBinarySensor(TotalConnectEntity, BinarySensorEntity): +class TotalConnectAlarmBinarySensor(TotalConnectLocationEntity, BinarySensorEntity): """Represent a TotalConnect alarm device binary sensors.""" entity_description: TotalConnectAlarmBinarySensorEntityDescription @@ -193,10 +193,9 @@ class TotalConnectAlarmBinarySensor(TotalConnectEntity, BinarySensorEntity): location: TotalConnectLocation, ) -> None: """Initialize the TotalConnect alarm device binary sensor.""" - super().__init__(coordinator) + super().__init__(coordinator, location) self.entity_description = entity_description - self._location = location - self._attr_name = f"{location.location_name}{entity_description.name}" + self._attr_name = f"{self.device.name}{entity_description.name}" self._attr_unique_id = f"{location.location_id}_{entity_description.key}" self._attr_extra_state_attributes = { "location_id": location.location_id, diff --git a/homeassistant/components/totalconnect/entity.py b/homeassistant/components/totalconnect/entity.py index e7ab4b3575c..deef0c5aa2a 100644 --- a/homeassistant/components/totalconnect/entity.py +++ b/homeassistant/components/totalconnect/entity.py @@ -1,5 +1,6 @@ """Base class for TotalConnect entities.""" +from total_connect_client.location import TotalConnectLocation from total_connect_client.zone import TotalConnectZone from homeassistant.helpers.device_registry import DeviceInfo @@ -12,6 +13,25 @@ class TotalConnectEntity(CoordinatorEntity[TotalConnectDataUpdateCoordinator]): """Represent a TotalConnect entity.""" +class TotalConnectLocationEntity(TotalConnectEntity): + """Represent a TotalConnect location.""" + + def __init__( + self, + coordinator: TotalConnectDataUpdateCoordinator, + location: TotalConnectLocation, + ) -> None: + """Initialize the TotalConnect location.""" + super().__init__(coordinator) + self._location = location + self.device = location.devices[location.security_device_id] + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self.device.serial_number)}, + name=self.device.name, + serial_number=self.device.serial_number, + ) + + class TotalConnectZoneEntity(TotalConnectEntity): """Represent a TotalConnect zone.""" From 354e8e92f39841826fc7f40ad39a8b6157d75d4b Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 22 Apr 2024 11:19:35 +0200 Subject: [PATCH 743/967] Move NextDNS data update coordinators to the coordinator module (#115919) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/nextdns/__init__.py | 126 ++---------------- .../components/nextdns/binary_sensor.py | 2 +- homeassistant/components/nextdns/button.py | 2 +- .../components/nextdns/coordinator.py | 124 +++++++++++++++++ homeassistant/components/nextdns/sensor.py | 2 +- homeassistant/components/nextdns/switch.py | 2 +- 6 files changed, 139 insertions(+), 119 deletions(-) create mode 100644 homeassistant/components/nextdns/coordinator.py diff --git a/homeassistant/components/nextdns/__init__.py b/homeassistant/components/nextdns/__init__.py index 389173a2694..c7e4a0842fb 100644 --- a/homeassistant/components/nextdns/__init__.py +++ b/homeassistant/components/nextdns/__init__.py @@ -4,31 +4,15 @@ from __future__ import annotations import asyncio from datetime import timedelta -import logging -from typing import TypeVar from aiohttp.client_exceptions import ClientConnectorError -from nextdns import ( - AnalyticsDnssec, - AnalyticsEncryption, - AnalyticsIpVersions, - AnalyticsProtocols, - AnalyticsStatus, - ApiError, - ConnectionStatus, - InvalidApiKeyError, - NextDns, - Settings, -) -from nextdns.model import NextDnsData +from nextdns import ApiError, NextDns from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, 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 DeviceEntryType, DeviceInfo -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( ATTR_CONNECTION, @@ -44,104 +28,16 @@ from .const import ( UPDATE_INTERVAL_CONNECTION, UPDATE_INTERVAL_SETTINGS, ) - -CoordinatorDataT = TypeVar("CoordinatorDataT", bound=NextDnsData) - - -class NextDnsUpdateCoordinator(DataUpdateCoordinator[CoordinatorDataT]): # pylint: disable=hass-enforce-coordinator-module - """Class to manage fetching NextDNS data API.""" - - def __init__( - self, - hass: HomeAssistant, - nextdns: NextDns, - profile_id: str, - update_interval: timedelta, - ) -> None: - """Initialize.""" - self.nextdns = nextdns - self.profile_id = profile_id - self.profile_name = nextdns.get_profile_name(profile_id) - self.device_info = DeviceInfo( - configuration_url=f"https://my.nextdns.io/{profile_id}/setup", - entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, str(profile_id))}, - manufacturer="NextDNS Inc.", - name=self.profile_name, - ) - - super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) - - 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: - raise UpdateFailed(err) from err - - async def _async_update_data_internal(self) -> CoordinatorDataT: - """Update data via library.""" - raise NotImplementedError("Update method not implemented") - - -class NextDnsStatusUpdateCoordinator(NextDnsUpdateCoordinator[AnalyticsStatus]): # pylint: disable=hass-enforce-coordinator-module - """Class to manage fetching NextDNS analytics status data from API.""" - - async def _async_update_data_internal(self) -> AnalyticsStatus: - """Update data via library.""" - return await self.nextdns.get_analytics_status(self.profile_id) - - -class NextDnsDnssecUpdateCoordinator(NextDnsUpdateCoordinator[AnalyticsDnssec]): # pylint: disable=hass-enforce-coordinator-module - """Class to manage fetching NextDNS analytics Dnssec data from API.""" - - async def _async_update_data_internal(self) -> AnalyticsDnssec: - """Update data via library.""" - return await self.nextdns.get_analytics_dnssec(self.profile_id) - - -class NextDnsEncryptionUpdateCoordinator(NextDnsUpdateCoordinator[AnalyticsEncryption]): # pylint: disable=hass-enforce-coordinator-module - """Class to manage fetching NextDNS analytics encryption data from API.""" - - async def _async_update_data_internal(self) -> AnalyticsEncryption: - """Update data via library.""" - return await self.nextdns.get_analytics_encryption(self.profile_id) - - -class NextDnsIpVersionsUpdateCoordinator(NextDnsUpdateCoordinator[AnalyticsIpVersions]): # pylint: disable=hass-enforce-coordinator-module - """Class to manage fetching NextDNS analytics IP versions data from API.""" - - async def _async_update_data_internal(self) -> AnalyticsIpVersions: - """Update data via library.""" - return await self.nextdns.get_analytics_ip_versions(self.profile_id) - - -class NextDnsProtocolsUpdateCoordinator(NextDnsUpdateCoordinator[AnalyticsProtocols]): # pylint: disable=hass-enforce-coordinator-module - """Class to manage fetching NextDNS analytics protocols data from API.""" - - async def _async_update_data_internal(self) -> AnalyticsProtocols: - """Update data via library.""" - return await self.nextdns.get_analytics_protocols(self.profile_id) - - -class NextDnsSettingsUpdateCoordinator(NextDnsUpdateCoordinator[Settings]): # pylint: disable=hass-enforce-coordinator-module - """Class to manage fetching NextDNS connection data from API.""" - - async def _async_update_data_internal(self) -> Settings: - """Update data via library.""" - return await self.nextdns.get_settings(self.profile_id) - - -class NextDnsConnectionUpdateCoordinator(NextDnsUpdateCoordinator[ConnectionStatus]): # pylint: disable=hass-enforce-coordinator-module - """Class to manage fetching NextDNS connection data from API.""" - - async def _async_update_data_internal(self) -> ConnectionStatus: - """Update data via library.""" - return await self.nextdns.connection_status(self.profile_id) - - -_LOGGER = logging.getLogger(__name__) +from .coordinator import ( + NextDnsConnectionUpdateCoordinator, + NextDnsDnssecUpdateCoordinator, + NextDnsEncryptionUpdateCoordinator, + NextDnsIpVersionsUpdateCoordinator, + NextDnsProtocolsUpdateCoordinator, + NextDnsSettingsUpdateCoordinator, + NextDnsStatusUpdateCoordinator, + NextDnsUpdateCoordinator, +) PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR, Platform.SWITCH] COORDINATORS: list[tuple[str, type[NextDnsUpdateCoordinator], timedelta]] = [ diff --git a/homeassistant/components/nextdns/binary_sensor.py b/homeassistant/components/nextdns/binary_sensor.py index f6860586808..1bb79cf4fce 100644 --- a/homeassistant/components/nextdns/binary_sensor.py +++ b/homeassistant/components/nextdns/binary_sensor.py @@ -19,8 +19,8 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import CoordinatorDataT, NextDnsConnectionUpdateCoordinator from .const import ATTR_CONNECTION, DOMAIN +from .coordinator import CoordinatorDataT, NextDnsConnectionUpdateCoordinator PARALLEL_UPDATES = 1 diff --git a/homeassistant/components/nextdns/button.py b/homeassistant/components/nextdns/button.py index d74152248a5..d61c953f260 100644 --- a/homeassistant/components/nextdns/button.py +++ b/homeassistant/components/nextdns/button.py @@ -9,8 +9,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import NextDnsStatusUpdateCoordinator from .const import ATTR_STATUS, DOMAIN +from .coordinator import NextDnsStatusUpdateCoordinator PARALLEL_UPDATES = 1 diff --git a/homeassistant/components/nextdns/coordinator.py b/homeassistant/components/nextdns/coordinator.py new file mode 100644 index 00000000000..cad1aeac070 --- /dev/null +++ b/homeassistant/components/nextdns/coordinator.py @@ -0,0 +1,124 @@ +"""NextDns coordinator.""" + +import asyncio +from datetime import timedelta +import logging +from typing import TypeVar + +from aiohttp.client_exceptions import ClientConnectorError +from nextdns import ( + AnalyticsDnssec, + AnalyticsEncryption, + AnalyticsIpVersions, + AnalyticsProtocols, + AnalyticsStatus, + ApiError, + ConnectionStatus, + InvalidApiKeyError, + NextDns, + Settings, +) +from nextdns.model import NextDnsData + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +CoordinatorDataT = TypeVar("CoordinatorDataT", bound=NextDnsData) + + +class NextDnsUpdateCoordinator(DataUpdateCoordinator[CoordinatorDataT]): + """Class to manage fetching NextDNS data API.""" + + def __init__( + self, + hass: HomeAssistant, + nextdns: NextDns, + profile_id: str, + update_interval: timedelta, + ) -> None: + """Initialize.""" + self.nextdns = nextdns + self.profile_id = profile_id + self.profile_name = nextdns.get_profile_name(profile_id) + self.device_info = DeviceInfo( + configuration_url=f"https://my.nextdns.io/{profile_id}/setup", + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, str(profile_id))}, + manufacturer="NextDNS Inc.", + name=self.profile_name, + ) + + super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) + + 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: + raise UpdateFailed(err) from err + + async def _async_update_data_internal(self) -> CoordinatorDataT: + """Update data via library.""" + raise NotImplementedError("Update method not implemented") + + +class NextDnsStatusUpdateCoordinator(NextDnsUpdateCoordinator[AnalyticsStatus]): + """Class to manage fetching NextDNS analytics status data from API.""" + + async def _async_update_data_internal(self) -> AnalyticsStatus: + """Update data via library.""" + return await self.nextdns.get_analytics_status(self.profile_id) + + +class NextDnsDnssecUpdateCoordinator(NextDnsUpdateCoordinator[AnalyticsDnssec]): + """Class to manage fetching NextDNS analytics Dnssec data from API.""" + + async def _async_update_data_internal(self) -> AnalyticsDnssec: + """Update data via library.""" + return await self.nextdns.get_analytics_dnssec(self.profile_id) + + +class NextDnsEncryptionUpdateCoordinator(NextDnsUpdateCoordinator[AnalyticsEncryption]): + """Class to manage fetching NextDNS analytics encryption data from API.""" + + async def _async_update_data_internal(self) -> AnalyticsEncryption: + """Update data via library.""" + return await self.nextdns.get_analytics_encryption(self.profile_id) + + +class NextDnsIpVersionsUpdateCoordinator(NextDnsUpdateCoordinator[AnalyticsIpVersions]): + """Class to manage fetching NextDNS analytics IP versions data from API.""" + + async def _async_update_data_internal(self) -> AnalyticsIpVersions: + """Update data via library.""" + return await self.nextdns.get_analytics_ip_versions(self.profile_id) + + +class NextDnsProtocolsUpdateCoordinator(NextDnsUpdateCoordinator[AnalyticsProtocols]): + """Class to manage fetching NextDNS analytics protocols data from API.""" + + async def _async_update_data_internal(self) -> AnalyticsProtocols: + """Update data via library.""" + return await self.nextdns.get_analytics_protocols(self.profile_id) + + +class NextDnsSettingsUpdateCoordinator(NextDnsUpdateCoordinator[Settings]): + """Class to manage fetching NextDNS connection data from API.""" + + async def _async_update_data_internal(self) -> Settings: + """Update data via library.""" + return await self.nextdns.get_settings(self.profile_id) + + +class NextDnsConnectionUpdateCoordinator(NextDnsUpdateCoordinator[ConnectionStatus]): + """Class to manage fetching NextDNS connection data from API.""" + + async def _async_update_data_internal(self) -> ConnectionStatus: + """Update data via library.""" + return await self.nextdns.connection_status(self.profile_id) diff --git a/homeassistant/components/nextdns/sensor.py b/homeassistant/components/nextdns/sensor.py index 4357179cbdb..3ac2179ed31 100644 --- a/homeassistant/components/nextdns/sensor.py +++ b/homeassistant/components/nextdns/sensor.py @@ -26,7 +26,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import CoordinatorDataT, NextDnsUpdateCoordinator from .const import ( ATTR_DNSSEC, ATTR_ENCRYPTION, @@ -35,6 +34,7 @@ from .const import ( ATTR_STATUS, DOMAIN, ) +from .coordinator import CoordinatorDataT, NextDnsUpdateCoordinator PARALLEL_UPDATES = 1 diff --git a/homeassistant/components/nextdns/switch.py b/homeassistant/components/nextdns/switch.py index 81bf8b4e8c6..dfb796efd8c 100644 --- a/homeassistant/components/nextdns/switch.py +++ b/homeassistant/components/nextdns/switch.py @@ -18,8 +18,8 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import CoordinatorDataT, NextDnsSettingsUpdateCoordinator from .const import ATTR_SETTINGS, DOMAIN +from .coordinator import CoordinatorDataT, NextDnsSettingsUpdateCoordinator PARALLEL_UPDATES = 1 From 6985d36f18d5dd3a8bddac190a89f42ef1fb187f Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Mon, 22 Apr 2024 11:39:53 +0100 Subject: [PATCH 744/967] Update ovoenergy to 2.0.0 (#115921) Co-authored-by: J. Nick Koston --- .../components/ovo_energy/__init__.py | 25 ++++++++++++------- .../components/ovo_energy/config_flow.py | 24 ++++++++++++++---- .../components/ovo_energy/manifest.json | 2 +- homeassistant/components/ovo_energy/sensor.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/ovo_energy/test_config_flow.py | 22 ++++++++++++---- 7 files changed, 56 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/ovo_energy/__init__.py b/homeassistant/components/ovo_energy/__init__.py index e0c2b77664a..d207f3161f4 100644 --- a/homeassistant/components/ovo_energy/__init__.py +++ b/homeassistant/components/ovo_energy/__init__.py @@ -7,13 +7,14 @@ from datetime import timedelta import logging import aiohttp +from ovoenergy import OVOEnergy from ovoenergy.models import OVODailyUsage -from ovoenergy.ovoenergy import OVOEnergy from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, 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 DeviceEntryType, DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -32,29 +33,35 @@ PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up OVO Energy from a config entry.""" - client = OVOEnergy() + client = OVOEnergy( + client_session=async_get_clientsession(hass), + ) + + if custom_account := entry.data.get(CONF_ACCOUNT) is not None: + client.custom_account_id = custom_account try: - authenticated = await client.authenticate( + if not await client.authenticate( entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], - entry.data[CONF_ACCOUNT], - ) + ): + raise ConfigEntryAuthFailed + + await client.bootstrap_accounts() except aiohttp.ClientError as exception: _LOGGER.warning(exception) raise ConfigEntryNotReady from exception - if not authenticated: - raise ConfigEntryAuthFailed - async def async_update_data() -> OVODailyUsage: """Fetch data from OVO Energy.""" + if custom_account := entry.data.get(CONF_ACCOUNT) is not None: + client.custom_account_id = custom_account + async with asyncio.timeout(10): try: authenticated = await client.authenticate( entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], - entry.data[CONF_ACCOUNT], ) except aiohttp.ClientError as exception: raise UpdateFailed(exception) from exception diff --git a/homeassistant/components/ovo_energy/config_flow.py b/homeassistant/components/ovo_energy/config_flow.py index 41c64913764..87d53e5fbf9 100644 --- a/homeassistant/components/ovo_energy/config_flow.py +++ b/homeassistant/components/ovo_energy/config_flow.py @@ -6,11 +6,12 @@ from collections.abc import Mapping from typing import Any import aiohttp -from ovoenergy.ovoenergy import OVOEnergy +from ovoenergy import OVOEnergy import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_ACCOUNT, DOMAIN @@ -41,13 +42,19 @@ class OVOEnergyFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a flow initiated by the user.""" errors = {} if user_input is not None: - client = OVOEnergy() + client = OVOEnergy( + client_session=async_get_clientsession(self.hass), + ) + + if custom_account := user_input.get(CONF_ACCOUNT) is not None: + client.custom_account_id = custom_account + try: authenticated = await client.authenticate( user_input[CONF_USERNAME], user_input[CONF_PASSWORD], - user_input.get(CONF_ACCOUNT, None), ) + await client.bootstrap_accounts() except aiohttp.ClientError: errors["base"] = "cannot_connect" else: @@ -86,10 +93,17 @@ class OVOEnergyFlowHandler(ConfigFlow, domain=DOMAIN): self.context["title_placeholders"] = {CONF_USERNAME: self.username} if user_input is not None and user_input.get(CONF_PASSWORD) is not None: - client = OVOEnergy() + client = OVOEnergy( + client_session=async_get_clientsession(self.hass), + ) + + if self.account is not None: + client.custom_account_id = self.account + try: authenticated = await client.authenticate( - self.username, user_input[CONF_PASSWORD], self.account + self.username, + user_input[CONF_PASSWORD], ) except aiohttp.ClientError: errors["base"] = "connection_error" diff --git a/homeassistant/components/ovo_energy/manifest.json b/homeassistant/components/ovo_energy/manifest.json index 9435958f1fe..af4a313206e 100644 --- a/homeassistant/components/ovo_energy/manifest.json +++ b/homeassistant/components/ovo_energy/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["ovoenergy"], - "requirements": ["ovoenergy==1.3.1"] + "requirements": ["ovoenergy==2.0.0"] } diff --git a/homeassistant/components/ovo_energy/sensor.py b/homeassistant/components/ovo_energy/sensor.py index d5384837e9c..5b16e8cdef5 100644 --- a/homeassistant/components/ovo_energy/sensor.py +++ b/homeassistant/components/ovo_energy/sensor.py @@ -7,8 +7,8 @@ import dataclasses from datetime import datetime, timedelta from typing import Final +from ovoenergy import OVOEnergy from ovoenergy.models import OVODailyUsage -from ovoenergy.ovoenergy import OVOEnergy from homeassistant.components.sensor import ( SensorDeviceClass, diff --git a/requirements_all.txt b/requirements_all.txt index fa3e5893eef..573f2f4a8d3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1495,7 +1495,7 @@ orvibo==1.1.2 ourgroceries==1.5.4 # homeassistant.components.ovo_energy -ovoenergy==1.3.1 +ovoenergy==2.0.0 # homeassistant.components.p1_monitor p1monitor==3.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8a10ee1c176..b3e0b9feaf6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1189,7 +1189,7 @@ oralb-ble==0.17.6 ourgroceries==1.5.4 # homeassistant.components.ovo_energy -ovoenergy==1.3.1 +ovoenergy==2.0.0 # homeassistant.components.p1_monitor p1monitor==3.0.0 diff --git a/tests/components/ovo_energy/test_config_flow.py b/tests/components/ovo_energy/test_config_flow.py index 7575f1edb29..00899e745b9 100644 --- a/tests/components/ovo_energy/test_config_flow.py +++ b/tests/components/ovo_energy/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import patch import aiohttp from homeassistant import config_entries -from homeassistant.components.ovo_energy.const import DOMAIN +from homeassistant.components.ovo_energy.const import CONF_ACCOUNT, DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -13,7 +13,11 @@ from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry FIXTURE_REAUTH_INPUT = {CONF_PASSWORD: "something1"} -FIXTURE_USER_INPUT = {CONF_USERNAME: "example@example.com", CONF_PASSWORD: "something"} +FIXTURE_USER_INPUT = { + CONF_USERNAME: "example@example.com", + CONF_PASSWORD: "something", + CONF_ACCOUNT: "123456", +} UNIQUE_ID = "example@example.com" @@ -37,9 +41,14 @@ async def test_authorization_error(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" - with patch( - "homeassistant.components.ovo_energy.config_flow.OVOEnergy.authenticate", - return_value=False, + with ( + patch( + "homeassistant.components.ovo_energy.config_flow.OVOEnergy.authenticate", + return_value=False, + ), + patch( + "homeassistant.components.ovo_energy.config_flow.OVOEnergy.bootstrap_accounts", + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -88,6 +97,9 @@ async def test_full_flow_implementation(hass: HomeAssistant) -> None: "homeassistant.components.ovo_energy.config_flow.OVOEnergy.authenticate", return_value=True, ), + patch( + "homeassistant.components.ovo_energy.config_flow.OVOEnergy.bootstrap_accounts", + ), patch( "homeassistant.components.ovo_energy.config_flow.OVOEnergy.username", "some_name", From 693bd08a0ba3bcb56a27bfae6adaef3a613dd788 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 22 Apr 2024 13:01:31 +0200 Subject: [PATCH 745/967] Add snapshot tests to Totalconnect (#115952) * Add snapshot tests to Totalconnect * Add snapshot tests to Totalconnect --- .../snapshots/test_alarm_control_panel.ambr | 117 ++ .../snapshots/test_binary_sensor.ambr | 1095 +++++++++++++++++ .../totalconnect/test_alarm_control_panel.py | 23 +- .../totalconnect/test_binary_sensor.py | 34 +- 4 files changed, 1229 insertions(+), 40 deletions(-) create mode 100644 tests/components/totalconnect/snapshots/test_alarm_control_panel.ambr create mode 100644 tests/components/totalconnect/snapshots/test_binary_sensor.ambr diff --git a/tests/components/totalconnect/snapshots/test_alarm_control_panel.ambr b/tests/components/totalconnect/snapshots/test_alarm_control_panel.ambr new file mode 100644 index 00000000000..4dc6b576ba3 --- /dev/null +++ b/tests/components/totalconnect/snapshots/test_alarm_control_panel.ambr @@ -0,0 +1,117 @@ +# serializer version: 1 +# name: test_attributes[alarm_control_panel.test-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'alarm_control_panel', + 'entity_category': None, + 'entity_id': 'alarm_control_panel.test', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'test', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '123456', + 'unit_of_measurement': None, + }) +# --- +# name: test_attributes[alarm_control_panel.test-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'ac_loss': False, + 'changed_by': None, + 'code_arm_required': True, + 'code_format': None, + 'cover_tampered': False, + 'friendly_name': 'test', + 'location_id': '123456', + 'location_name': 'test', + 'low_battery': False, + 'partition': 1, + 'supported_features': , + 'triggered_source': None, + 'triggered_zone': None, + }), + 'context': , + 'entity_id': 'alarm_control_panel.test', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'disarmed', + }) +# --- +# name: test_attributes[alarm_control_panel.test_partition_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'alarm_control_panel', + 'entity_category': None, + 'entity_id': 'alarm_control_panel.test_partition_2', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'test partition 2', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '123456_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_attributes[alarm_control_panel.test_partition_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'ac_loss': False, + 'changed_by': None, + 'code_arm_required': True, + 'code_format': None, + 'cover_tampered': False, + 'friendly_name': 'test partition 2', + 'location_id': '123456', + 'location_name': 'test partition 2', + 'low_battery': False, + 'partition': 2, + 'supported_features': , + 'triggered_source': None, + 'triggered_zone': None, + }), + 'context': , + 'entity_id': 'alarm_control_panel.test_partition_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'disarmed', + }) +# --- diff --git a/tests/components/totalconnect/snapshots/test_binary_sensor.ambr b/tests/components/totalconnect/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..a79f609488d --- /dev/null +++ b/tests/components/totalconnect/snapshots/test_binary_sensor.ambr @@ -0,0 +1,1095 @@ +# serializer version: 1 +# name: test_entity_registry[binary_sensor.fire-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.fire', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Fire', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456_2_zone', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.fire-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'smoke', + 'friendly_name': 'Fire', + 'location_id': '123456', + 'partition': '1', + 'zone_id': '2', + }), + 'context': , + 'entity_id': 'binary_sensor.fire', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_registry[binary_sensor.fire_low_battery-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.fire_low_battery', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Fire low battery', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456_2_low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.fire_low_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Fire low battery', + 'location_id': '123456', + 'partition': '1', + 'zone_id': '2', + }), + 'context': , + 'entity_id': 'binary_sensor.fire_low_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.fire_tamper-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.fire_tamper', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Fire tamper', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456_2_tamper', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.fire_tamper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'Fire tamper', + 'location_id': '123456', + 'partition': '1', + 'zone_id': '2', + }), + 'context': , + 'entity_id': 'binary_sensor.fire_tamper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_registry[binary_sensor.gas-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.gas', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Gas', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456_3_zone', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.gas-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'gas', + 'friendly_name': 'Gas', + 'location_id': '123456', + 'partition': '1', + 'zone_id': '3', + }), + 'context': , + 'entity_id': 'binary_sensor.gas', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_registry[binary_sensor.gas_low_battery-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.gas_low_battery', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Gas low battery', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456_3_low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.gas_low_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Gas low battery', + 'location_id': '123456', + 'partition': '1', + 'zone_id': '3', + }), + 'context': , + 'entity_id': 'binary_sensor.gas_low_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_registry[binary_sensor.gas_tamper-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.gas_tamper', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Gas tamper', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456_3_tamper', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.gas_tamper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'Gas tamper', + 'location_id': '123456', + 'partition': '1', + 'zone_id': '3', + }), + 'context': , + 'entity_id': 'binary_sensor.gas_tamper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.medical-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.medical', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Medical', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456_5_zone', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.medical-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'safety', + 'friendly_name': 'Medical', + 'location_id': '123456', + 'partition': '1', + 'zone_id': '5', + }), + 'context': , + 'entity_id': 'binary_sensor.medical', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_registry[binary_sensor.motion-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.motion', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Motion', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456_4_zone', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.motion-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'motion', + 'friendly_name': 'Motion', + 'location_id': '123456', + 'partition': '1', + 'zone_id': '4', + }), + 'context': , + 'entity_id': 'binary_sensor.motion', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_registry[binary_sensor.motion_low_battery-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.motion_low_battery', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Motion low battery', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456_4_low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.motion_low_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Motion low battery', + 'location_id': '123456', + 'partition': '1', + 'zone_id': '4', + }), + 'context': , + 'entity_id': 'binary_sensor.motion_low_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_registry[binary_sensor.motion_tamper-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.motion_tamper', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Motion tamper', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456_4_tamper', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.motion_tamper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'Motion tamper', + 'location_id': '123456', + 'partition': '1', + 'zone_id': '4', + }), + 'context': , + 'entity_id': 'binary_sensor.motion_tamper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_registry[binary_sensor.security-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.security', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Security', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456_1_zone', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.security-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'door', + 'friendly_name': 'Security', + 'location_id': '123456', + 'partition': '1', + 'zone_id': '1', + }), + 'context': , + 'entity_id': 'binary_sensor.security', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.security_low_battery-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.security_low_battery', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Security low battery', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456_1_low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.security_low_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Security low battery', + 'location_id': '123456', + 'partition': '1', + 'zone_id': '1', + }), + 'context': , + 'entity_id': 'binary_sensor.security_low_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_registry[binary_sensor.security_tamper-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.security_tamper', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Security tamper', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456_1_tamper', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.security_tamper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'Security tamper', + 'location_id': '123456', + 'partition': '1', + 'zone_id': '1', + }), + 'context': , + 'entity_id': 'binary_sensor.security_tamper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_registry[binary_sensor.temperature-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.temperature', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456_7_zone', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Temperature', + 'location_id': '123456', + 'partition': '1', + 'zone_id': 7, + }), + 'context': , + 'entity_id': 'binary_sensor.temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_registry[binary_sensor.temperature_low_battery-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.temperature_low_battery', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature low battery', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456_7_low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.temperature_low_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Temperature low battery', + 'location_id': '123456', + 'partition': '1', + 'zone_id': 7, + }), + 'context': , + 'entity_id': 'binary_sensor.temperature_low_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_registry[binary_sensor.temperature_tamper-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.temperature_tamper', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature tamper', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456_7_tamper', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.temperature_tamper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'Temperature tamper', + 'location_id': '123456', + 'partition': '1', + 'zone_id': 7, + }), + 'context': , + 'entity_id': 'binary_sensor.temperature_tamper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_registry[binary_sensor.test_low_battery-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_low_battery', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'test low battery', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456_low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.test_low_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'test low battery', + 'location_id': '123456', + }), + 'context': , + 'entity_id': 'binary_sensor.test_low_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_registry[binary_sensor.test_power-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_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': 'test power', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456_power', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.test_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'test power', + 'location_id': '123456', + }), + 'context': , + 'entity_id': 'binary_sensor.test_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_registry[binary_sensor.test_tamper-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_tamper', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'test tamper', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456_tamper', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.test_tamper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'test tamper', + 'location_id': '123456', + }), + 'context': , + 'entity_id': 'binary_sensor.test_tamper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_registry[binary_sensor.unknown-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.unknown', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Unknown', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456_6_zone', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.unknown-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'door', + 'friendly_name': 'Unknown', + 'location_id': '123456', + 'partition': '1', + 'zone_id': '6', + }), + 'context': , + 'entity_id': 'binary_sensor.unknown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_registry[binary_sensor.unknown_low_battery-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.unknown_low_battery', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Unknown low battery', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456_6_low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.unknown_low_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Unknown low battery', + 'location_id': '123456', + 'partition': '1', + 'zone_id': '6', + }), + 'context': , + 'entity_id': 'binary_sensor.unknown_low_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity_registry[binary_sensor.unknown_tamper-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.unknown_tamper', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Unknown tamper', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456_6_tamper', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.unknown_tamper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'Unknown tamper', + 'location_id': '123456', + 'partition': '1', + 'zone_id': '6', + }), + 'context': , + 'entity_id': 'binary_sensor.unknown_tamper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/totalconnect/test_alarm_control_panel.py b/tests/components/totalconnect/test_alarm_control_panel.py index fa2e997756d..176fe54c34a 100644 --- a/tests/components/totalconnect/test_alarm_control_panel.py +++ b/tests/components/totalconnect/test_alarm_control_panel.py @@ -4,6 +4,7 @@ from datetime import timedelta from unittest.mock import patch import pytest +from syrupy import SnapshotAssertion from total_connect_client.exceptions import ServiceUnavailable, TotalConnectError from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN @@ -14,7 +15,6 @@ from homeassistant.components.totalconnect.alarm_control_panel import ( ) from homeassistant.const import ( ATTR_ENTITY_ID, - ATTR_FRIENDLY_NAME, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_NIGHT, @@ -36,7 +36,6 @@ from homeassistant.helpers.entity_component import async_update_entity from homeassistant.util import dt as dt_util from .common import ( - LOCATION_ID, RESPONSE_ARM_FAILURE, RESPONSE_ARM_SUCCESS, RESPONSE_ARMED_AWAY, @@ -58,7 +57,7 @@ from .common import ( setup_platform, ) -from tests.common import async_fire_time_changed +from tests.common import async_fire_time_changed, snapshot_platform ENTITY_ID = "alarm_control_panel.test" ENTITY_ID_2 = "alarm_control_panel.test_partition_2" @@ -67,28 +66,20 @@ DATA = {ATTR_ENTITY_ID: ENTITY_ID} DELAY = timedelta(seconds=10) -async def test_attributes(hass: HomeAssistant) -> None: +async def test_attributes( + hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion +) -> None: """Test the alarm control panel attributes are correct.""" - await setup_platform(hass, ALARM_DOMAIN) + entry = await setup_platform(hass, ALARM_DOMAIN) with patch( "homeassistant.components.totalconnect.TotalConnectClient.request", return_value=RESPONSE_DISARMED, ) as mock_request: await async_update_entity(hass, ENTITY_ID) await hass.async_block_till_done() - state = hass.states.get(ENTITY_ID) - assert state.state == STATE_ALARM_DISARMED mock_request.assert_called_once() - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "test" - entity_registry = er.async_get(hass) - entry = entity_registry.async_get(ENTITY_ID) - # TotalConnect partition #1 alarm device unique_id is the location_id - assert entry.unique_id == LOCATION_ID - - entry2 = entity_registry.async_get(ENTITY_ID_2) - # TotalConnect partition #2 unique_id is the location_id + "_{partition_number}" - assert entry2.unique_id == LOCATION_ID + "_2" + await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) assert mock_request.call_count == 1 diff --git a/tests/components/totalconnect/test_binary_sensor.py b/tests/components/totalconnect/test_binary_sensor.py index 8ff548850d9..1a8a65391f5 100644 --- a/tests/components/totalconnect/test_binary_sensor.py +++ b/tests/components/totalconnect/test_binary_sensor.py @@ -2,6 +2,8 @@ from unittest.mock import patch +from syrupy import SnapshotAssertion + from homeassistant.components.binary_sensor import ( DOMAIN as BINARY_SENSOR, BinarySensorDeviceClass, @@ -10,7 +12,9 @@ from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from .common import LOCATION_ID, RESPONSE_DISARMED, ZONE_NORMAL, setup_platform +from .common import RESPONSE_DISARMED, ZONE_NORMAL, setup_platform + +from tests.common import snapshot_platform ZONE_ENTITY_ID = "binary_sensor.security" ZONE_LOW_BATTERY_ID = "binary_sensor.security_low_battery" @@ -20,31 +24,13 @@ PANEL_TAMPER_ID = "binary_sensor.test_tamper" PANEL_POWER_ID = "binary_sensor.test_power" -async def test_entity_registry(hass: HomeAssistant) -> None: +async def test_entity_registry( + hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion +) -> None: """Test the binary sensor is registered in entity registry.""" - await setup_platform(hass, BINARY_SENSOR) - entity_registry = er.async_get(hass) + entry = await setup_platform(hass, BINARY_SENSOR) - # ensure zone 1 plus two diagnostic zones are created - entry = entity_registry.async_get(ZONE_ENTITY_ID) - entry_low_battery = entity_registry.async_get(ZONE_LOW_BATTERY_ID) - entry_tamper = entity_registry.async_get(ZONE_TAMPER_ID) - - assert entry.unique_id == f"{LOCATION_ID}_{ZONE_NORMAL['ZoneID']}_zone" - assert ( - entry_low_battery.unique_id - == f"{LOCATION_ID}_{ZONE_NORMAL['ZoneID']}_low_battery" - ) - assert entry_tamper.unique_id == f"{LOCATION_ID}_{ZONE_NORMAL['ZoneID']}_tamper" - - # ensure panel diagnostic zones are created - panel_battery = entity_registry.async_get(PANEL_BATTERY_ID) - panel_tamper = entity_registry.async_get(PANEL_TAMPER_ID) - panel_power = entity_registry.async_get(PANEL_POWER_ID) - - assert panel_battery.unique_id == f"{LOCATION_ID}_low_battery" - assert panel_tamper.unique_id == f"{LOCATION_ID}_tamper" - assert panel_power.unique_id == f"{LOCATION_ID}_power" + await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) async def test_state_and_attributes(hass: HomeAssistant) -> None: From 9b6863f18279a9e2169e56dd7808f6d19eca30a1 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 22 Apr 2024 23:12:22 +1200 Subject: [PATCH 746/967] ESPHome: Add datetime entities (#115942) --- homeassistant/components/esphome/datetime.py | 48 +++++++++++ .../components/esphome/entry_data.py | 2 + tests/components/esphome/test_datetime.py | 79 +++++++++++++++++++ 3 files changed, 129 insertions(+) create mode 100644 homeassistant/components/esphome/datetime.py create mode 100644 tests/components/esphome/test_datetime.py diff --git a/homeassistant/components/esphome/datetime.py b/homeassistant/components/esphome/datetime.py new file mode 100644 index 00000000000..15509a46158 --- /dev/null +++ b/homeassistant/components/esphome/datetime.py @@ -0,0 +1,48 @@ +"""Support for esphome datetimes.""" + +from __future__ import annotations + +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 + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + 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): + """A datetime implementation for esphome.""" + + @property + @esphome_state_property + def native_value(self) -> datetime | None: + """Return the state of the entity.""" + state = self._state + if state.missing_state: + return None + return dt_util.utc_from_timestamp(state.epoch_seconds) + + async def async_set_value(self, value: datetime) -> None: + """Update the current datetime.""" + self._client.datetime_command(self._key, int(value.timestamp())) diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 52dc1f17ad6..a840fc3a17e 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -20,6 +20,7 @@ from aioesphomeapi import ( ClimateInfo, CoverInfo, DateInfo, + DateTimeInfo, DeviceInfo, EntityInfo, EntityState, @@ -68,6 +69,7 @@ INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], Platform] = { ClimateInfo: Platform.CLIMATE, CoverInfo: Platform.COVER, DateInfo: Platform.DATE, + DateTimeInfo: Platform.DATETIME, FanInfo: Platform.FAN, LightInfo: Platform.LIGHT, LockInfo: Platform.LOCK, diff --git a/tests/components/esphome/test_datetime.py b/tests/components/esphome/test_datetime.py new file mode 100644 index 00000000000..3bdc196de95 --- /dev/null +++ b/tests/components/esphome/test_datetime.py @@ -0,0 +1,79 @@ +"""Test ESPHome datetimes.""" + +from unittest.mock import call + +from aioesphomeapi import APIClient, DateTimeInfo, DateTimeState + +from homeassistant.components.datetime import ( + ATTR_DATETIME, + DOMAIN as DATETIME_DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.core import HomeAssistant + + +async def test_generic_datetime_entity( + hass: HomeAssistant, + mock_client: APIClient, + mock_generic_device_entry, +) -> None: + """Test a generic datetime entity.""" + entity_info = [ + DateTimeInfo( + object_id="mydatetime", + key=1, + name="my datetime", + unique_id="my_datetime", + ) + ] + states = [DateTimeState(key=1, epoch_seconds=1713270896)] + 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("datetime.test_mydatetime") + assert state is not None + assert state.state == "2024-04-16T12:34:56+00:00" + + await hass.services.async_call( + DATETIME_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: "datetime.test_mydatetime", + ATTR_DATETIME: "2000-01-01T01:23:45+00:00", + }, + blocking=True, + ) + mock_client.datetime_command.assert_has_calls([call(1, 946689825)]) + mock_client.datetime_command.reset_mock() + + +async def test_generic_datetime_missing_state( + hass: HomeAssistant, + mock_client: APIClient, + mock_generic_device_entry, +) -> None: + """Test a generic datetime entity with missing state.""" + entity_info = [ + DateTimeInfo( + object_id="mydatetime", + key=1, + name="my datetime", + unique_id="my_datetime", + ) + ] + states = [DateTimeState(key=1, missing_state=True)] + 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("datetime.test_mydatetime") + assert state is not None + assert state.state == STATE_UNKNOWN From 5a7e921ae3fb455f076b3fdf8786bd207e743331 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 22 Apr 2024 13:24:23 +0200 Subject: [PATCH 747/967] Address late review for AVM Fritz!Smarthome (#115960) fix typo --- homeassistant/components/fritzbox/coordinator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/fritzbox/coordinator.py b/homeassistant/components/fritzbox/coordinator.py index a9cfc25b223..06454fa912a 100644 --- a/homeassistant/components/fritzbox/coordinator.py +++ b/homeassistant/components/fritzbox/coordinator.py @@ -58,18 +58,18 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat list(self.data.devices) + list(self.data.templates) ) - def cleanup_removed_devices(self, avaiable_ains: list[str]) -> None: + def cleanup_removed_devices(self, available_ains: list[str]) -> None: """Cleanup entity and device registry from removed devices.""" entity_reg = er.async_get(self.hass) for entity in er.async_entries_for_config_entry( entity_reg, self.config_entry.entry_id ): - if entity.unique_id.split("_")[0] not in avaiable_ains: + if entity.unique_id.split("_")[0] not in available_ains: LOGGER.debug("Removing obsolete entity entry %s", entity.entity_id) entity_reg.async_remove(entity.entity_id) device_reg = dr.async_get(self.hass) - identifiers = {(DOMAIN, ain) for ain in avaiable_ains} + identifiers = {(DOMAIN, ain) for ain in available_ains} for device in dr.async_entries_for_config_entry( device_reg, self.config_entry.entry_id ): From 65b2c1519c45be8decaf53135f666773a513ce7f Mon Sep 17 00:00:00 2001 From: Marc-Olivier Arsenault Date: Mon, 22 Apr 2024 10:43:01 -0400 Subject: [PATCH 748/967] Reduce ecobee throttle (#115968) reduce ecobee throttle --- homeassistant/components/ecobee/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ecobee/__init__.py b/homeassistant/components/ecobee/__init__.py index 8083d0efcb4..c9d45b512bd 100644 --- a/homeassistant/components/ecobee/__init__.py +++ b/homeassistant/components/ecobee/__init__.py @@ -22,7 +22,7 @@ from .const import ( PLATFORMS, ) -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=180) +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5) CONFIG_SCHEMA = vol.Schema( {DOMAIN: vol.Schema({vol.Optional(CONF_API_KEY): cv.string})}, extra=vol.ALLOW_EXTRA From 37d329c2867629db59a12cc3ed63675315aa288f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 22 Apr 2024 16:51:19 +0200 Subject: [PATCH 749/967] Improve reliability of homeassistant_alerts updates (#115974) --- .../components/homeassistant_alerts/__init__.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homeassistant_alerts/__init__.py b/homeassistant/components/homeassistant_alerts/__init__.py index 7dcd9f8db97..ef5e330699a 100644 --- a/homeassistant/components/homeassistant_alerts/__init__.py +++ b/homeassistant/components/homeassistant_alerts/__init__.py @@ -20,7 +20,7 @@ from homeassistant.helpers.issue_registry import ( async_create_issue, async_delete_issue, ) -from homeassistant.helpers.start import async_at_start +from homeassistant.helpers.start import async_at_started from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.setup import EventComponentLoaded @@ -30,6 +30,8 @@ DOMAIN = "homeassistant_alerts" UPDATE_INTERVAL = timedelta(hours=3) _LOGGER = logging.getLogger(__name__) +REQUEST_TIMEOUT = aiohttp.ClientTimeout(total=30) + CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) @@ -52,7 +54,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: try: response = await async_get_clientsession(hass).get( f"https://alerts.home-assistant.io/alerts/{alert.alert_id}.json", - timeout=aiohttp.ClientTimeout(total=30), + timeout=REQUEST_TIMEOUT, ) except TimeoutError: _LOGGER.warning("Error fetching %s: timeout", alert.filename) @@ -106,7 +108,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await coordinator.async_refresh() hass.bus.async_listen(EVENT_COMPONENT_LOADED, _component_loaded) - async_at_start(hass, initial_refresh) + async_at_started(hass, initial_refresh) return True @@ -146,7 +148,7 @@ class AlertUpdateCoordinator(DataUpdateCoordinator[dict[str, IntegrationAlert]]) async def _async_update_data(self) -> dict[str, IntegrationAlert]: response = await async_get_clientsession(self.hass).get( "https://alerts.home-assistant.io/alerts.json", - timeout=aiohttp.ClientTimeout(total=10), + timeout=REQUEST_TIMEOUT, ) alerts = await response.json() From 20adc5be70a57a454ca0e250716d27259ec4aad8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 22 Apr 2024 16:52:04 +0200 Subject: [PATCH 750/967] Small fixes for processing integration requirements (#115973) --- homeassistant/requirements.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index e78398ebf03..e282ced90ac 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -122,6 +122,11 @@ 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.""" @@ -144,16 +149,13 @@ class RequirementsManager: is invalid, RequirementNotFound if there was some type of failure to install requirements. """ - if done is None: done = {domain} else: done.add(domain) - integration = await async_get_integration(self.hass, domain) - if self.hass.config.skip_pip: - return integration + return await async_get_integration(self.hass, domain) cache = self.integrations_with_reqs int_or_fut = cache.get(domain, UNDEFINED) @@ -170,19 +172,19 @@ class RequirementsManager: if int_or_fut is not UNDEFINED: return cast(Integration, int_or_fut) - event = cache[domain] = self.hass.loop.create_future() + 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: del cache[domain] - if not event.done(): - event.set_result(None) raise + finally: + _set_result_unless_done(future) cache[domain] = integration - if not event.done(): - event.set_result(None) + _set_result_unless_done(future) return integration async def _async_process_integration( From 2afaa3d3337df0332d024928686636119e435392 Mon Sep 17 00:00:00 2001 From: David Knowles Date: Mon, 22 Apr 2024 10:54:04 -0400 Subject: [PATCH 751/967] Remove YAML support from Hydrawise (#115966) --- .../components/hydrawise/__init__.py | 39 +----- .../components/hydrawise/config_flow.py | 52 -------- .../components/hydrawise/strings.json | 6 - .../components/hydrawise/test_config_flow.py | 118 +----------------- tests/components/hydrawise/test_init.py | 20 +-- 5 files changed, 4 insertions(+), 231 deletions(-) diff --git a/homeassistant/components/hydrawise/__init__.py b/homeassistant/components/hydrawise/__init__.py index 541d4211e49..62a4cacc5c4 100644 --- a/homeassistant/components/hydrawise/__init__.py +++ b/homeassistant/components/hydrawise/__init__.py @@ -1,52 +1,17 @@ """Support for Hydrawise cloud.""" from pydrawise import legacy -import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ( - CONF_ACCESS_TOKEN, - CONF_API_KEY, - CONF_SCAN_INTERVAL, - Platform, -) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY, Platform from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, SCAN_INTERVAL from .coordinator import HydrawiseDataUpdateCoordinator -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_ACCESS_TOKEN): cv.string, - vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): cv.time_period, - } - ) - }, - extra=vol.ALLOW_EXTRA, -) - PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH] -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Hunter Hydrawise component.""" - if DOMAIN not in config: - return True - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_API_KEY: config[DOMAIN][CONF_ACCESS_TOKEN]}, - ) - ) - return True - - async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up Hydrawise from a config entry.""" access_token = config_entry.data[CONF_API_KEY] diff --git a/homeassistant/components/hydrawise/config_flow.py b/homeassistant/components/hydrawise/config_flow.py index cfaaefcd03a..8233074c3cd 100644 --- a/homeassistant/components/hydrawise/config_flow.py +++ b/homeassistant/components/hydrawise/config_flow.py @@ -11,9 +11,6 @@ import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_KEY -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN -from homeassistant.data_entry_flow import AbortFlow, FlowResultType -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from .const import DOMAIN, LOGGER @@ -42,40 +39,6 @@ class HydrawiseConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_create_entry(title="Hydrawise", data={CONF_API_KEY: api_key}) - def _import_issue(self, error_type: str) -> ConfigFlowResult: - """Create an issue about a YAML import failure.""" - async_create_issue( - self.hass, - DOMAIN, - f"deprecated_yaml_import_issue_{error_type}", - breaks_in_ha_version="2024.4.0", - is_fixable=False, - severity=IssueSeverity.ERROR, - translation_key="deprecated_yaml_import_issue", - translation_placeholders={ - "error_type": error_type, - "url": "/config/integrations/dashboard/add?domain=hydrawise", - }, - ) - return self.async_abort(reason=error_type) - - def _deprecated_yaml_issue(self) -> None: - """Create an issue about YAML deprecation.""" - async_create_issue( - self.hass, - HOMEASSISTANT_DOMAIN, - f"deprecated_yaml_{DOMAIN}", - breaks_in_ha_version="2024.4.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "Hydrawise", - }, - ) - async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: @@ -94,18 +57,3 @@ class HydrawiseConfigFlow(ConfigFlow, domain=DOMAIN): data_schema=vol.Schema({vol.Required(CONF_API_KEY): str}), errors=errors, ) - - async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult: - """Import data from YAML.""" - try: - result = await self._create_entry( - import_data.get(CONF_API_KEY, ""), - on_failure=self._import_issue, - ) - except AbortFlow: - self._deprecated_yaml_issue() - raise - - if result["type"] == FlowResultType.CREATE_ENTRY: - self._deprecated_yaml_issue() - return result diff --git a/homeassistant/components/hydrawise/strings.json b/homeassistant/components/hydrawise/strings.json index 8f079abcc7d..1c96098db35 100644 --- a/homeassistant/components/hydrawise/strings.json +++ b/homeassistant/components/hydrawise/strings.json @@ -16,12 +16,6 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" } }, - "issues": { - "deprecated_yaml_import_issue": { - "title": "The Hydrawise YAML configuration import failed", - "description": "Configuring Hydrawise using YAML is being removed but there was an {error_type} error importing your YAML configuration.\n\nEnsure connection to Hydrawise works and restart Home Assistant to try again or remove the Hydrawise YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." - } - }, "entity": { "binary_sensor": { "watering": { diff --git a/tests/components/hydrawise/test_config_flow.py b/tests/components/hydrawise/test_config_flow.py index b0d5b098309..be0ef90becd 100644 --- a/tests/components/hydrawise/test_config_flow.py +++ b/tests/components/hydrawise/test_config_flow.py @@ -8,12 +8,8 @@ import pytest from homeassistant import config_entries from homeassistant.components.hydrawise.const import DOMAIN -from homeassistant.const import CONF_API_KEY, CONF_SCAN_INTERVAL -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -import homeassistant.helpers.issue_registry as ir - -from tests.common import MockConfigEntry pytestmark = pytest.mark.usefixtures("mock_setup_entry") @@ -87,115 +83,3 @@ async def test_form_connect_timeout( mock_pydrawise.get_user.return_value = user result2 = await hass.config_entries.flow.async_configure(result["flow_id"], data) assert result2["type"] is FlowResultType.CREATE_ENTRY - - -async def test_flow_import_success( - hass: HomeAssistant, mock_pydrawise: AsyncMock, user: User -) -> None: - """Test that we can import a YAML config.""" - mock_pydrawise.get_user.return_value = User - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_API_KEY: "__api_key__", - CONF_SCAN_INTERVAL: 120, - }, - ) - await hass.async_block_till_done() - - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "Hydrawise" - assert result["data"] == { - CONF_API_KEY: "__api_key__", - } - - issue_registry = ir.async_get(hass) - issue = issue_registry.async_get_issue( - HOMEASSISTANT_DOMAIN, "deprecated_yaml_hydrawise" - ) - assert issue.translation_key == "deprecated_yaml" - - -async def test_flow_import_api_error( - hass: HomeAssistant, mock_pydrawise: AsyncMock -) -> None: - """Test that we handle API errors on YAML import.""" - mock_pydrawise.get_user.side_effect = ClientError - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_API_KEY: "__api_key__", - CONF_SCAN_INTERVAL: 120, - }, - ) - await hass.async_block_till_done() - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "cannot_connect" - - issue_registry = ir.async_get(hass) - issue = issue_registry.async_get_issue( - DOMAIN, "deprecated_yaml_import_issue_cannot_connect" - ) - assert issue.translation_key == "deprecated_yaml_import_issue" - - -async def test_flow_import_connect_timeout( - hass: HomeAssistant, mock_pydrawise: AsyncMock -) -> None: - """Test that we handle connection timeouts on YAML import.""" - mock_pydrawise.get_user.side_effect = TimeoutError - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_API_KEY: "__api_key__", - CONF_SCAN_INTERVAL: 120, - }, - ) - await hass.async_block_till_done() - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "timeout_connect" - - issue_registry = ir.async_get(hass) - issue = issue_registry.async_get_issue( - DOMAIN, "deprecated_yaml_import_issue_timeout_connect" - ) - assert issue.translation_key == "deprecated_yaml_import_issue" - - -async def test_flow_import_already_imported( - hass: HomeAssistant, mock_pydrawise: AsyncMock, user: User -) -> None: - """Test that we can handle a YAML config already imported.""" - mock_config_entry = MockConfigEntry( - title="Hydrawise", - domain=DOMAIN, - data={ - CONF_API_KEY: "__api_key__", - }, - unique_id="hydrawise-12345", - ) - mock_config_entry.add_to_hass(hass) - - mock_pydrawise.get_user.return_value = user - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_API_KEY: "__api_key__", - CONF_SCAN_INTERVAL: 120, - }, - ) - await hass.async_block_till_done() - - assert result["type"] is FlowResultType.ABORT - assert result.get("reason") == "already_configured" - - issue_registry = ir.async_get(hass) - issue = issue_registry.async_get_issue( - HOMEASSISTANT_DOMAIN, "deprecated_yaml_hydrawise" - ) - assert issue.translation_key == "deprecated_yaml" diff --git a/tests/components/hydrawise/test_init.py b/tests/components/hydrawise/test_init.py index 6b41867b044..91c99833531 100644 --- a/tests/components/hydrawise/test_init.py +++ b/tests/components/hydrawise/test_init.py @@ -5,29 +5,11 @@ from unittest.mock import AsyncMock from aiohttp import ClientError from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import CONF_ACCESS_TOKEN -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant -import homeassistant.helpers.issue_registry as ir -from homeassistant.setup import async_setup_component +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry -async def test_setup_import_success( - hass: HomeAssistant, mock_pydrawise: AsyncMock -) -> None: - """Test that setup with a YAML config triggers an import and warning.""" - config = {"hydrawise": {CONF_ACCESS_TOKEN: "_access-token_"}} - assert await async_setup_component(hass, "hydrawise", config) - await hass.async_block_till_done() - - issue_registry = ir.async_get(hass) - issue = issue_registry.async_get_issue( - HOMEASSISTANT_DOMAIN, "deprecated_yaml_hydrawise" - ) - assert issue.translation_key == "deprecated_yaml" - - async def test_connect_retry( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_pydrawise: AsyncMock ) -> None: From 124eca4d534f705ac1092910223643e38ff18af9 Mon Sep 17 00:00:00 2001 From: Raj Laud <50647620+rajlaud@users.noreply.github.com> Date: Mon, 22 Apr 2024 11:46:12 -0400 Subject: [PATCH 752/967] Use start helper in squeezebox for server discovery (#115978) --- homeassistant/components/squeezebox/media_player.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index 7d072fa2570..a3a404fe1ae 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -28,7 +28,6 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_PORT, CONF_USERNAME, - EVENT_HOMEASSISTANT_START, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import ( @@ -44,6 +43,7 @@ from homeassistant.helpers.dispatcher import ( ) from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.start import async_at_start from homeassistant.util.dt import utcnow from .browse_media import ( @@ -207,12 +207,7 @@ async def async_setup_entry( platform.async_register_entity_service(SERVICE_UNSYNC, None, "async_unsync") # Start server discovery task if not already running - if hass.is_running: - hass.async_create_task(start_server_discovery(hass)) - else: - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, start_server_discovery(hass) - ) + config_entry.async_on_unload(async_at_start(hass, start_server_discovery)) class SqueezeBoxEntity(MediaPlayerEntity): From 2caca7fbe3e39acc9295ca42383e8392d7a50020 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 22 Apr 2024 19:23:08 +0200 Subject: [PATCH 753/967] Generate requirements per supported architecture (#115708) * Generate requirements per supported architecture * Don't store wheels requirements in the repo * Dry run * Set Python version * Install base packages * Fix * Fix * Fix * Fix typo Co-authored-by: Martin Hjelmare * Genarate requirements_all_pytest.txt * Fix hassfest * Reenable building wheels * Remove unneeded code * Address review comment * Fix lying comment * Add tests, address review comments * Deduplicate * Fix file name * Add comment --------- Co-authored-by: Martin Hjelmare --- .github/workflows/ci.yaml | 8 +- .github/workflows/wheels.yml | 60 +++++++------ script/gen_requirements_all.py | 104 +++++++++++++++++++--- script/hassfest/requirements.py | 10 +-- tests/script/__init__.py | 1 + tests/script/test_gen_requirements_all.py | 25 ++++++ 6 files changed, 165 insertions(+), 43 deletions(-) create mode 100644 tests/script/__init__.py create mode 100644 tests/script/test_gen_requirements_all.py diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 581a36be953..0dc8f34570c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -97,7 +97,8 @@ jobs: hashFiles('requirements_test.txt', 'requirements_test_pre_commit.txt') }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_all.txt') }}-${{ - hashFiles('homeassistant/package_constraints.txt') }}" >> $GITHUB_OUTPUT + hashFiles('homeassistant/package_constraints.txt') }}-${{ + hashFiles('script/gen_requirements_all.py') }}" >> $GITHUB_OUTPUT - name: Generate partial pre-commit restore key id: generate_pre-commit_cache_key run: >- @@ -497,8 +498,9 @@ jobs: python --version pip install "$(grep '^uv' < requirements_test.txt)" uv pip install -U "pip>=21.3.1" setuptools wheel - uv pip install -r requirements_all.txt - uv pip install "$(grep 'python-gammu' < requirements_all.txt | sed -e 's|# python-gammu|python-gammu|g')" + uv pip install -r requirements.txt + python -m script.gen_requirements_all ci + uv pip install -r requirements_all_pytest.txt uv pip install -r requirements_test.txt uv pip install -e . --config-settings editable_mode=compat diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 24033a92fd5..6618eb9963b 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -14,6 +14,10 @@ on: - "homeassistant/package_constraints.txt" - "requirements_all.txt" - "requirements.txt" + - "script/gen_requirements_all.py" + +env: + DEFAULT_PYTHON: "3.12" concurrency: group: ${{ github.workflow }}-${{ github.ref_name}} @@ -30,6 +34,21 @@ jobs: - name: Checkout the repository uses: actions/checkout@v4.1.3 + - 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: Create Python virtual environment + run: | + python -m venv venv + . venv/bin/activate + python --version + pip install "$(grep '^uv' < requirements_test.txt)" + uv pip install -r requirements.txt + - name: Get information id: info uses: home-assistant/actions/helpers/info@master @@ -76,6 +95,17 @@ jobs: path: ./requirements_diff.txt overwrite: true + - name: Generate requirements + run: | + . venv/bin/activate + python -m script.gen_requirements_all ci + + - name: Upload requirements_all_wheels + uses: actions/upload-artifact@v4.3.1 + with: + name: requirements_all_wheels + path: ./requirements_all_wheels_*.txt + core: name: Build Core wheels ${{ matrix.abi }} for ${{ matrix.arch }} (musllinux_1_2) if: github.repository_owner == 'home-assistant' @@ -138,30 +168,10 @@ jobs: with: name: requirements_diff - - name: (Un)comment packages - run: | - requirement_files="requirements_all.txt requirements_diff.txt" - for requirement_file in ${requirement_files}; do - sed -i "s|# pyuserinput|pyuserinput|g" ${requirement_file} - sed -i "s|# evdev|evdev|g" ${requirement_file} - sed -i "s|# pycups|pycups|g" ${requirement_file} - sed -i "s|# decora-wifi|decora-wifi|g" ${requirement_file} - sed -i "s|# python-gammu|python-gammu|g" ${requirement_file} - - # Some packages are not buildable on armhf anymore - if [ "${{ matrix.arch }}" = "armhf" ]; then - - # Pandas has issues building on armhf, it is expected they - # will drop the platform in the near future (they consider it - # "flimsy" on 386). The following packages depend on pandas, - # so we comment them out. - sed -i "s|env-canada|# env-canada|g" ${requirement_file} - sed -i "s|noaa-coops|# noaa-coops|g" ${requirement_file} - sed -i "s|pyezviz|# pyezviz|g" ${requirement_file} - sed -i "s|pykrakenapi|# pykrakenapi|g" ${requirement_file} - fi - - done + - name: Download requirements_all_wheels + uses: actions/download-artifact@v4.1.4 + with: + name: requirements_all_wheels - name: Split requirements all run: | @@ -169,7 +179,7 @@ jobs: # This is to prevent the build from running out of memory when # resolving packages on 32-bits systems (like armhf, armv7). - split -l $(expr $(expr $(cat requirements_all.txt | wc -l) + 1) / 3) requirements_all.txt requirements_all.txt + split -l $(expr $(expr $(cat requirements_all.txt | wc -l) + 1) / 3) requirements_all_wheels_${{ matrix.arch }}.txt requirements_all.txt - name: Create requirements for cython<3 run: | diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 7fc0907e756..a5db9997d9d 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -17,7 +17,10 @@ from typing import Any from homeassistant.util.yaml.loader import load_yaml from script.hassfest.model import Integration -COMMENT_REQUIREMENTS = ( +# Requirements which can't be installed on all systems because they rely on additional +# system packages. Requirements listed in EXCLUDED_REQUIREMENTS_ALL will be commented-out +# in requirements_all.txt and requirements_test_all.txt. +EXCLUDED_REQUIREMENTS_ALL = { "atenpdu", # depends on pysnmp which is not maintained at this time "avea", # depends on bluepy "avion", @@ -36,10 +39,39 @@ COMMENT_REQUIREMENTS = ( "pyuserinput", "tensorflow", "tf-models-official", -) +} -COMMENT_REQUIREMENTS_NORMALIZED = { - commented.lower().replace("_", "-") for commented in COMMENT_REQUIREMENTS +# Requirements excluded by EXCLUDED_REQUIREMENTS_ALL which should be included when +# building integration wheels for all architectures. +INCLUDED_REQUIREMENTS_WHEELS = { + "decora-wifi", + "evdev", + "pycups", + "python-gammu", + "pyuserinput", +} + + +# Requirements to exclude or include when running github actions. +# Requirements listed in "exclude" will be commented-out in +# requirements_all_{action}.txt +# Requirements listed in "include" must be listed in EXCLUDED_REQUIREMENTS_CI, and +# will be included in requirements_all_{action}.txt + +OVERRIDDEN_REQUIREMENTS_ACTIONS = { + "pytest": {"exclude": set(), "include": {"python-gammu"}}, + "wheels_aarch64": {"exclude": set(), "include": INCLUDED_REQUIREMENTS_WHEELS}, + # Pandas has issues building on armhf, it is expected they + # will drop the platform in the near future (they consider it + # "flimsy" on 386). The following packages depend on pandas, + # so we comment them out. + "wheels_armhf": { + "exclude": {"env-canada", "noaa-coops", "pyezviz", "pykrakenapi"}, + "include": INCLUDED_REQUIREMENTS_WHEELS, + }, + "wheels_armv7": {"exclude": set(), "include": INCLUDED_REQUIREMENTS_WHEELS}, + "wheels_amd64": {"exclude": set(), "include": INCLUDED_REQUIREMENTS_WHEELS}, + "wheels_i386": {"exclude": set(), "include": INCLUDED_REQUIREMENTS_WHEELS}, } IGNORE_PIN = ("colorlog>2.1,<3", "urllib3") @@ -254,6 +286,12 @@ def gather_recursive_requirements( return reqs +def _normalize_package_name(package_name: str) -> str: + """Normalize a package name.""" + # pipdeptree needs lowercase and dash instead of underscore or period as separator + return package_name.lower().replace("_", "-").replace(".", "-") + + def normalize_package_name(requirement: str) -> str: """Return a normalized package name from a requirement string.""" # This function is also used in hassfest. @@ -262,12 +300,24 @@ def normalize_package_name(requirement: str) -> str: return "" # pipdeptree needs lowercase and dash instead of underscore or period as separator - return match.group(1).lower().replace("_", "-").replace(".", "-") + return _normalize_package_name(match.group(1)) def comment_requirement(req: str) -> bool: """Comment out requirement. Some don't install on all systems.""" - return normalize_package_name(req) in COMMENT_REQUIREMENTS_NORMALIZED + return normalize_package_name(req) in EXCLUDED_REQUIREMENTS_ALL + + +def process_action_requirement(req: str, action: str) -> str: + """Process requirement for a specific github action.""" + normalized_package_name = normalize_package_name(req) + if normalized_package_name in OVERRIDDEN_REQUIREMENTS_ACTIONS[action]["exclude"]: + return f"# {req}" + if normalized_package_name in OVERRIDDEN_REQUIREMENTS_ACTIONS[action]["include"]: + return req + if normalized_package_name in EXCLUDED_REQUIREMENTS_ALL: + return f"# {req}" + return req def gather_modules() -> dict[str, list[str]] | None: @@ -353,6 +403,16 @@ def generate_requirements_list(reqs: dict[str, list[str]]) -> str: return "".join(output) +def generate_action_requirements_list(reqs: dict[str, list[str]], action: str) -> str: + """Generate a pip file based on requirements.""" + output = [] + for pkg, requirements in sorted(reqs.items(), key=itemgetter(0)): + output.extend(f"\n# {req}" for req in sorted(requirements)) + processed_pkg = process_action_requirement(pkg, action) + output.append(f"\n{processed_pkg}\n") + return "".join(output) + + def requirements_output() -> str: """Generate output for requirements.""" output = [ @@ -379,6 +439,18 @@ def requirements_all_output(reqs: dict[str, list[str]]) -> str: return "".join(output) +def requirements_all_action_output(reqs: dict[str, list[str]], action: str) -> str: + """Generate output for requirements_all_{action}.""" + output = [ + f"# Home Assistant Core, full dependency set for {action}\n", + GENERATED_MESSAGE, + "-r requirements.txt\n", + ] + output.append(generate_action_requirements_list(reqs, action)) + + return "".join(output) + + def requirements_test_all_output(reqs: dict[str, list[str]]) -> str: """Generate output for test_requirements.""" output = [ @@ -459,7 +531,7 @@ def diff_file(filename: str, content: str) -> list[str]: ) -def main(validate: bool) -> int: +def main(validate: bool, ci: bool) -> int: """Run the script.""" if not os.path.isfile("requirements_all.txt"): print("Run this from HA root dir") @@ -472,17 +544,28 @@ def main(validate: bool) -> int: reqs_file = requirements_output() reqs_all_file = requirements_all_output(data) + reqs_all_action_files = { + action: requirements_all_action_output(data, action) + for action in OVERRIDDEN_REQUIREMENTS_ACTIONS + } reqs_test_all_file = requirements_test_all_output(data) + # Always calling requirements_pre_commit_output is intentional to ensure + # the code is called by the pre-commit hooks. reqs_pre_commit_file = requirements_pre_commit_output() constraints = gather_constraints() - files = ( + files = [ ("requirements.txt", reqs_file), ("requirements_all.txt", reqs_all_file), ("requirements_test_pre_commit.txt", reqs_pre_commit_file), ("requirements_test_all.txt", reqs_test_all_file), ("homeassistant/package_constraints.txt", constraints), - ) + ] + if ci: + files.extend( + (f"requirements_all_{action}.txt", reqs_all_file) + for action, reqs_all_file in reqs_all_action_files.items() + ) if validate: errors = [] @@ -511,4 +594,5 @@ def main(validate: bool) -> int: if __name__ == "__main__": _VAL = sys.argv[-1] == "validate" - sys.exit(main(_VAL)) + _CI = sys.argv[-1] == "ci" + sys.exit(main(_VAL, _CI)) diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py index ee63bf07f90..2c4ed47b158 100644 --- a/script/hassfest/requirements.py +++ b/script/hassfest/requirements.py @@ -15,13 +15,13 @@ from awesomeversion import AwesomeVersion, AwesomeVersionStrategy from tqdm import tqdm import homeassistant.util.package as pkg_util -from script.gen_requirements_all import COMMENT_REQUIREMENTS, normalize_package_name +from script.gen_requirements_all import ( + EXCLUDED_REQUIREMENTS_ALL, + normalize_package_name, +) from .model import Config, Integration -IGNORE_PACKAGES = { - commented.lower().replace("_", "-") for commented in COMMENT_REQUIREMENTS -} PACKAGE_REGEX = re.compile( r"^(?:--.+\s)?([-_,\.\w\d\[\]]+)(==|>=|<=|~=|!=|<|>|===)*(.*)$" ) @@ -116,7 +116,7 @@ def validate_requirements(integration: Integration) -> None: f"Failed to normalize package name from requirement {req}", ) return - if package in IGNORE_PACKAGES: + if package in EXCLUDED_REQUIREMENTS_ALL: continue integration_requirements.add(req) integration_packages.add(package) diff --git a/tests/script/__init__.py b/tests/script/__init__.py new file mode 100644 index 00000000000..209299782c9 --- /dev/null +++ b/tests/script/__init__.py @@ -0,0 +1 @@ +"""Tests for scripts.""" diff --git a/tests/script/test_gen_requirements_all.py b/tests/script/test_gen_requirements_all.py new file mode 100644 index 00000000000..793b3de63c5 --- /dev/null +++ b/tests/script/test_gen_requirements_all.py @@ -0,0 +1,25 @@ +"""Tests for the gen_requirements_all script.""" + +from script import gen_requirements_all + + +def test_overrides_normalized() -> None: + """Test override lists are using normalized package names.""" + for req in gen_requirements_all.EXCLUDED_REQUIREMENTS_ALL: + assert req == gen_requirements_all._normalize_package_name(req) + for req in gen_requirements_all.INCLUDED_REQUIREMENTS_WHEELS: + assert req == gen_requirements_all._normalize_package_name(req) + for overrides in gen_requirements_all.OVERRIDDEN_REQUIREMENTS_ACTIONS.values(): + for req in overrides["exclude"]: + assert req == gen_requirements_all._normalize_package_name(req) + for req in overrides["include"]: + assert req == gen_requirements_all._normalize_package_name(req) + + +def test_include_overrides_subsets() -> None: + """Test packages in include override lists are present in the exclude list.""" + for req in gen_requirements_all.INCLUDED_REQUIREMENTS_WHEELS: + assert req in gen_requirements_all.EXCLUDED_REQUIREMENTS_ALL + for overrides in gen_requirements_all.OVERRIDDEN_REQUIREMENTS_ACTIONS.values(): + for req in overrides["include"]: + assert req in gen_requirements_all.EXCLUDED_REQUIREMENTS_ALL From f2adae45240a78ab78fe6108dc957991ff80d5e5 Mon Sep 17 00:00:00 2001 From: Marc-Olivier Arsenault Date: Mon, 22 Apr 2024 13:28:08 -0400 Subject: [PATCH 754/967] Revert "Reduce ecobee throttle (#115968)" (#115981) --- homeassistant/components/ecobee/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ecobee/__init__.py b/homeassistant/components/ecobee/__init__.py index c9d45b512bd..8083d0efcb4 100644 --- a/homeassistant/components/ecobee/__init__.py +++ b/homeassistant/components/ecobee/__init__.py @@ -22,7 +22,7 @@ from .const import ( PLATFORMS, ) -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5) +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=180) CONFIG_SCHEMA = vol.Schema( {DOMAIN: vol.Schema({vol.Optional(CONF_API_KEY): cv.string})}, extra=vol.ALLOW_EXTRA From 0ed56694b04f998e0f2815bdeb7e3f151d54ece0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 22 Apr 2024 20:09:45 +0200 Subject: [PATCH 755/967] Migrate MQTT EnsureJobAfterCooldown to use eager start (#115977) --- homeassistant/components/mqtt/client.py | 3 ++- tests/components/mqtt/test_discovery.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 021ecf1cc36..9a344e13023 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -41,6 +41,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from homeassistant.util import dt as dt_util +from homeassistant.util.async_ import create_eager_task from homeassistant.util.logging import catch_log_exception from .const import ( @@ -352,7 +353,7 @@ class EnsureJobAfterCooldown: return self._async_cancel_timer() - self._task = asyncio.create_task(self._async_job()) + self._task = create_eager_task(self._async_job()) self._task.add_done_callback(self._async_task_done) @callback diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 24891895fad..a00af080bf1 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -1487,6 +1487,7 @@ async def test_mqtt_integration_discovery_subscribe_unsubscribe( 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 @@ -1537,6 +1538,7 @@ async def test_mqtt_discovery_unsubscribe_once( 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 From 2ac44f60839cb572627f1e7b47a5c8f87adefa2f Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Mon, 22 Apr 2024 11:10:18 -0700 Subject: [PATCH 756/967] Make recorder.purge_entities require at least one entity filter value (#110066) Co-authored-by: J. Nick Koston --- homeassistant/components/recorder/services.py | 30 ++++++++++++++----- .../components/recorder/services.yaml | 9 +++--- .../components/recorder/strings.json | 4 +++ tests/components/recorder/test_purge.py | 11 +++---- 4 files changed, 37 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/recorder/services.py b/homeassistant/components/recorder/services.py index b4d719a9481..2be02fe8091 100644 --- a/homeassistant/components/recorder/services.py +++ b/homeassistant/components/recorder/services.py @@ -7,6 +7,7 @@ from typing import cast import voluptuous as vol +from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant, ServiceCall, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import generate_filter @@ -36,15 +37,28 @@ SERVICE_PURGE_SCHEMA = vol.Schema( ATTR_DOMAINS = "domains" ATTR_ENTITY_GLOBS = "entity_globs" -SERVICE_PURGE_ENTITIES_SCHEMA = vol.Schema( - { - vol.Optional(ATTR_DOMAINS, default=[]): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(ATTR_ENTITY_GLOBS, default=[]): vol.All( - cv.ensure_list, [cv.string] +SERVICE_PURGE_ENTITIES_SCHEMA = vol.All( + vol.Schema( + { + vol.Optional(ATTR_ENTITY_ID, default=[]): cv.entity_ids, + vol.Optional(ATTR_DOMAINS, default=[]): vol.All( + cv.ensure_list, [cv.string] + ), + vol.Optional(ATTR_ENTITY_GLOBS, default=[]): vol.All( + cv.ensure_list, [cv.string] + ), + vol.Optional(ATTR_KEEP_DAYS, default=0): cv.positive_int, + } + ), + vol.Any( + vol.Schema({vol.Required(ATTR_ENTITY_ID): vol.IsTrue()}, extra=vol.ALLOW_EXTRA), + vol.Schema({vol.Required(ATTR_DOMAINS): vol.IsTrue()}, extra=vol.ALLOW_EXTRA), + vol.Schema( + {vol.Required(ATTR_ENTITY_GLOBS): vol.IsTrue()}, extra=vol.ALLOW_EXTRA ), - vol.Optional(ATTR_KEEP_DAYS, default=0): cv.positive_int, - } -).extend(cv.ENTITY_SERVICE_FIELDS) + msg="At least one of entity_id, domains, or entity_globs must have a value", + ), +) SERVICE_ENABLE_SCHEMA = vol.Schema({}) SERVICE_DISABLE_SCHEMA = vol.Schema({}) diff --git a/homeassistant/components/recorder/services.yaml b/homeassistant/components/recorder/services.yaml index b74dcc2a494..7d7b926548c 100644 --- a/homeassistant/components/recorder/services.yaml +++ b/homeassistant/components/recorder/services.yaml @@ -20,20 +20,21 @@ purge: boolean: purge_entities: - target: - entity: {} fields: + entity_id: + required: false + selector: + entity: + multiple: true domains: example: "sun" required: false - default: [] selector: object: entity_globs: example: "domain*.object_id*" required: false - default: [] selector: object: diff --git a/homeassistant/components/recorder/strings.json b/homeassistant/components/recorder/strings.json index 74b248354d7..bf5d95ae1fc 100644 --- a/homeassistant/components/recorder/strings.json +++ b/homeassistant/components/recorder/strings.json @@ -41,6 +41,10 @@ "name": "Purge entities", "description": "Starts a purge task to remove the data related to specific entities from your database.", "fields": { + "entity_id": { + "name": "Entities to remove", + "description": "List of entities for which the data is to be removed from the recorder database." + }, "domains": { "name": "Domains to remove", "description": "List of domains for which the data needs to be removed from the recorder database." diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index b2da3f1d62f..e80bc7ca7d1 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -9,6 +9,7 @@ from freezegun import freeze_time import pytest from sqlalchemy.exc import DatabaseError, OperationalError from sqlalchemy.orm.session import Session +from voluptuous.error import MultipleInvalid from homeassistant.components import recorder from homeassistant.components.recorder.const import SupportedDialect @@ -1446,20 +1447,20 @@ async def test_purge_entities( _add_purge_records(hass) - # Confirm calling service without arguments matches all records (default filter behavior) + # Confirm calling service without arguments is invalid with session_scope(hass=hass) as session: states = session.query(States) assert states.count() == 190 - await _purge_entities(hass, [], [], []) + with pytest.raises(MultipleInvalid): + await _purge_entities(hass, [], [], []) with session_scope(hass=hass, read_only=True) as session: states = session.query(States) - assert states.count() == 0 + assert states.count() == 190 - # The states_meta table should be empty states_meta_remain = session.query(StatesMeta) - assert states_meta_remain.count() == 0 + assert states_meta_remain.count() == 4 async def _add_test_states(hass: HomeAssistant, wait_recording_done: bool = True): From 5318a6f4650e440fbd7ade3b552e5cb46ef39161 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 22 Apr 2024 21:33:56 +0200 Subject: [PATCH 757/967] Bump holidays to 0.47 (#115992) --- 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 5a1edcd3c3f..3494798b50b 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.46", "babel==2.13.1"] + "requirements": ["holidays==0.47", "babel==2.13.1"] } diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index 314f4c6bcf4..e0813cd90cd 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.46"] + "requirements": ["holidays==0.47"] } diff --git a/requirements_all.txt b/requirements_all.txt index 573f2f4a8d3..3df28349edd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1072,7 +1072,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.46 +holidays==0.47 # homeassistant.components.frontend home-assistant-frontend==20240404.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b3e0b9feaf6..eeb7014b62d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -874,7 +874,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.46 +holidays==0.47 # homeassistant.components.frontend home-assistant-frontend==20240404.2 From b69f589c30e3e1908f26edd3c9103a13ab1d8417 Mon Sep 17 00:00:00 2001 From: Kim de Vos Date: Mon, 22 Apr 2024 22:39:46 +0200 Subject: [PATCH 758/967] Add bandwidth sensor for unifi device ports (#115362) --- homeassistant/components/unifi/sensor.py | 37 ++++++ tests/components/unifi/test_sensor.py | 149 +++++++++++++++++++++++ 2 files changed, 186 insertions(+) diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index 7d9720cde1a..3979f45ecd8 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -11,6 +11,7 @@ from dataclasses import dataclass from datetime import date, datetime, timedelta from decimal import Decimal from functools import partial +from typing import cast from aiounifi.interfaces.api_handlers import ItemEvent from aiounifi.interfaces.clients import Clients @@ -239,6 +240,42 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = ( unique_id_fn=lambda hub, obj_id: f"poe_power-{obj_id}", value_fn=lambda _, obj: obj.poe_power if obj.poe_mode != "off" else "0", ), + UnifiSensorEntityDescription[Ports, Port]( + key="Port Bandwidth sensor RX", + device_class=SensorDeviceClass.DATA_RATE, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, + suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND, + icon="mdi:download", + allowed_fn=lambda hub, _: hub.config.option_allow_bandwidth_sensors, + api_handler_fn=lambda api: api.ports, + available_fn=async_device_available_fn, + device_info_fn=async_device_device_info_fn, + name_fn=lambda port: f"{port.name} RX", + object_fn=lambda api, obj_id: api.ports[obj_id], + unique_id_fn=lambda hub, obj_id: f"port_rx-{obj_id}", + value_fn=lambda hub, port: cast(float, port.raw.get("rx_bytes-r", 0)), + ), + UnifiSensorEntityDescription[Ports, Port]( + key="Port Bandwidth sensor TX", + device_class=SensorDeviceClass.DATA_RATE, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, + suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND, + icon="mdi:upload", + allowed_fn=lambda hub, _: hub.config.option_allow_bandwidth_sensors, + api_handler_fn=lambda api: api.ports, + available_fn=async_device_available_fn, + device_info_fn=async_device_device_info_fn, + name_fn=lambda port: f"{port.name} TX", + object_fn=lambda api, obj_id: api.ports[obj_id], + unique_id_fn=lambda hub, obj_id: f"port_tx-{obj_id}", + value_fn=lambda hub, port: cast(float, port.raw.get("tx_bytes-r", 0)), + ), UnifiSensorEntityDescription[Clients, Client]( key="Client uptime", device_class=SensorDeviceClass.TIMESTAMP, diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index e3b4ddd3b63..26eadfa498e 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -1042,3 +1042,152 @@ async def test_device_system_stats( assert hass.states.get("sensor.device_cpu_utilization").state == "7.7" assert hass.states.get("sensor.device_memory_utilization").state == "33.3" + + +async def test_bandwidth_port_sensors( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + aioclient_mock: AiohttpClientMocker, + mock_unifi_websocket, +) -> 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 + + p1rx_reg_entry = entity_registry.async_get("sensor.mock_name_port_1_rx") + assert p1rx_reg_entry.disabled_by == RegistryEntryDisabler.INTEGRATION + assert p1rx_reg_entry.entity_category is EntityCategory.DIAGNOSTIC + + p1tx_reg_entry = entity_registry.async_get("sensor.mock_name_port_1_tx") + assert p1tx_reg_entry.disabled_by == RegistryEntryDisabler.INTEGRATION + assert p1tx_reg_entry.entity_category is EntityCategory.DIAGNOSTIC + + # Enable entity + entity_registry.async_update_entity( + entity_id="sensor.mock_name_port_1_rx", disabled_by=None + ) + entity_registry.async_update_entity( + entity_id="sensor.mock_name_port_1_tx", disabled_by=None + ) + entity_registry.async_update_entity( + entity_id="sensor.mock_name_port_2_rx", disabled_by=None + ) + entity_registry.async_update_entity( + entity_id="sensor.mock_name_port_2_tx", 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()) == 9 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 6 + + # Verify sensor attributes and state + p1rx_sensor = hass.states.get("sensor.mock_name_port_1_rx") + assert p1rx_sensor.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DATA_RATE + assert p1rx_sensor.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + assert p1rx_sensor.state == "0.00921" + + p1tx_sensor = hass.states.get("sensor.mock_name_port_1_tx") + assert p1tx_sensor.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DATA_RATE + assert p1tx_sensor.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + assert p1tx_sensor.state == "0.04089" + + p2rx_sensor = hass.states.get("sensor.mock_name_port_2_rx") + assert p2rx_sensor.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DATA_RATE + assert p2rx_sensor.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + assert p2rx_sensor.state == "0.01229" + + p2tx_sensor = hass.states.get("sensor.mock_name_port_2_tx") + assert p2tx_sensor.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DATA_RATE + assert p2tx_sensor.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + 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 + + mock_unifi_websocket(message=MessageKey.DEVICE, data=device_reponse) + 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[CONF_ALLOW_BANDWIDTH_SENSORS] = False + hass.config_entries.async_update_entry(config_entry, options=options.copy()) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 5 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 2 + + assert hass.states.get("sensor.mock_name_uptime") + assert hass.states.get("sensor.mock_name_state") + assert hass.states.get("sensor.mock_name_port_1_rx") is None + 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 From c32961f1bc0cd2a17b4e851d2fbb0cf88b71feec Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 23 Apr 2024 07:48:25 +0200 Subject: [PATCH 759/967] Bump aiounifi to v76 (#116005) * Bump aiounifi to v76 --- homeassistant/components/unifi/manifest.json | 2 +- homeassistant/components/unifi/sensor.py | 5 ++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index 305400a4b9d..982d654c8fe 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==75"], + "requirements": ["aiounifi==76"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index 3979f45ecd8..cec87b36416 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -11,7 +11,6 @@ from dataclasses import dataclass from datetime import date, datetime, timedelta from decimal import Decimal from functools import partial -from typing import cast from aiounifi.interfaces.api_handlers import ItemEvent from aiounifi.interfaces.clients import Clients @@ -256,7 +255,7 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = ( name_fn=lambda port: f"{port.name} RX", object_fn=lambda api, obj_id: api.ports[obj_id], unique_id_fn=lambda hub, obj_id: f"port_rx-{obj_id}", - value_fn=lambda hub, port: cast(float, port.raw.get("rx_bytes-r", 0)), + value_fn=lambda hub, port: port.rx_bytes_r, ), UnifiSensorEntityDescription[Ports, Port]( key="Port Bandwidth sensor TX", @@ -274,7 +273,7 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = ( name_fn=lambda port: f"{port.name} TX", object_fn=lambda api, obj_id: api.ports[obj_id], unique_id_fn=lambda hub, obj_id: f"port_tx-{obj_id}", - value_fn=lambda hub, port: cast(float, port.raw.get("tx_bytes-r", 0)), + value_fn=lambda hub, port: port.tx_bytes_r, ), UnifiSensorEntityDescription[Clients, Client]( key="Client uptime", diff --git a/requirements_all.txt b/requirements_all.txt index 3df28349edd..058e8102e18 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -383,7 +383,7 @@ aiotankerkoenig==0.4.1 aiotractive==0.5.6 # homeassistant.components.unifi -aiounifi==75 +aiounifi==76 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index eeb7014b62d..879f2b9123e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -356,7 +356,7 @@ aiotankerkoenig==0.4.1 aiotractive==0.5.6 # homeassistant.components.unifi -aiounifi==75 +aiounifi==76 # homeassistant.components.vlc_telnet aiovlc==0.1.0 From 2fafdc64d5934fcd700f7d336c7e798546258b73 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Tue, 23 Apr 2024 08:48:35 +0200 Subject: [PATCH 760/967] Bump uv to 0.1.35 (#115985) --- Dockerfile | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 28b65d6383d..c916a3d2f3c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ ENV \ ARG QEMU_CPU # Install uv -RUN pip3 install uv==0.1.27 +RUN pip3 install uv==0.1.35 WORKDIR /usr/src diff --git a/requirements_test.txt b/requirements_test.txt index f13e0e6a36b..e42a94091ad 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -50,4 +50,4 @@ types-pytz==2024.1.0.20240203 types-PyYAML==6.0.12.20240311 types-requests==2.31.0.3 types-xmltodict==0.13.0.3 -uv==0.1.27 +uv==0.1.35 From 917f4136a7d6836c6cd349a561feb02c96394d2a Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 23 Apr 2024 08:55:39 +0200 Subject: [PATCH 761/967] Add config flow to Folder Watcher (#105605) * Add config flow to Folder Watcher * Add tests config flow * docstrings * watcher is sync * Fix strings * Fix * setup_entry issue * ConfigFlowResult * Review comments * Review comment * ruff * new date --- .../components/folder_watcher/__init__.py | 70 +++++-- .../components/folder_watcher/config_flow.py | 116 +++++++++++ .../components/folder_watcher/const.py | 6 + .../components/folder_watcher/manifest.json | 1 + .../components/folder_watcher/strings.json | 46 +++++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 2 +- tests/components/folder_watcher/conftest.py | 17 ++ .../folder_watcher/test_config_flow.py | 186 ++++++++++++++++++ 9 files changed, 430 insertions(+), 15 deletions(-) create mode 100644 homeassistant/components/folder_watcher/config_flow.py create mode 100644 homeassistant/components/folder_watcher/const.py create mode 100644 homeassistant/components/folder_watcher/strings.json create mode 100644 tests/components/folder_watcher/conftest.py create mode 100644 tests/components/folder_watcher/test_config_flow.py diff --git a/homeassistant/components/folder_watcher/__init__.py b/homeassistant/components/folder_watcher/__init__.py index d111fe03c5c..3f0b9e8f6da 100644 --- a/homeassistant/components/folder_watcher/__init__.py +++ b/homeassistant/components/folder_watcher/__init__.py @@ -4,7 +4,7 @@ from __future__ import annotations import logging import os -from typing import cast +from typing import Any, cast import voluptuous as vol from watchdog.events import ( @@ -19,17 +19,17 @@ from watchdog.events import ( ) from watchdog.observers import Observer +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, 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 .const import CONF_FOLDER, CONF_PATTERNS, DEFAULT_PATTERN, DOMAIN + _LOGGER = logging.getLogger(__name__) -CONF_FOLDER = "folder" -CONF_PATTERNS = "patterns" -DEFAULT_PATTERN = "*" -DOMAIN = "folder_watcher" CONFIG_SCHEMA = vol.Schema( { @@ -51,20 +51,62 @@ CONFIG_SCHEMA = vol.Schema( ) -def setup(hass: HomeAssistant, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the folder watcher.""" - conf = config[DOMAIN] - for watcher in conf: - path: str = watcher[CONF_FOLDER] - patterns: list[str] = watcher[CONF_PATTERNS] - if not hass.config.is_allowed_path(path): - _LOGGER.error("Folder %s is not valid or allowed", path) - return False - Watcher(path, patterns, hass) + if DOMAIN in config: + conf: list[dict[str, Any]] = config[DOMAIN] + for watcher in conf: + path: str = watcher[CONF_FOLDER] + if not hass.config.is_allowed_path(path): + async_create_issue( + hass, + DOMAIN, + f"import_failed_not_allowed_path_{path}", + is_fixable=False, + is_persistent=False, + severity=IssueSeverity.ERROR, + translation_key="import_failed_not_allowed_path", + translation_placeholders={ + "path": path, + "config_variable": "allowlist_external_dirs", + }, + ) + continue + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=watcher + ) + ) return True +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Folder watcher from a config entry.""" + + path: str = entry.options[CONF_FOLDER] + patterns: list[str] = entry.options[CONF_PATTERNS] + if not hass.config.is_allowed_path(path): + _LOGGER.error("Folder %s is not valid or allowed", path) + async_create_issue( + hass, + DOMAIN, + f"setup_not_allowed_path_{path}", + is_fixable=False, + is_persistent=False, + severity=IssueSeverity.ERROR, + translation_key="setup_not_allowed_path", + translation_placeholders={ + "path": path, + "config_variable": "allowlist_external_dirs", + }, + learn_more_url="https://www.home-assistant.io/docs/configuration/basic/#allowlist_external_dirs", + ) + return False + await hass.async_add_executor_job(Watcher, path, patterns, hass) + return True + + def create_event_handler(patterns: list[str], hass: HomeAssistant) -> EventHandler: """Return the Watchdog EventHandler object.""" diff --git a/homeassistant/components/folder_watcher/config_flow.py b/homeassistant/components/folder_watcher/config_flow.py new file mode 100644 index 00000000000..50d198df3c3 --- /dev/null +++ b/homeassistant/components/folder_watcher/config_flow.py @@ -0,0 +1,116 @@ +"""Adds config flow for Folder watcher.""" + +from __future__ import annotations + +from collections.abc import Mapping +import os +from typing import Any + +import voluptuous as vol + +from homeassistant.components.homeassistant import DOMAIN as HOMEASSISTANT_DOMAIN +from homeassistant.config_entries import ConfigFlowResult +from homeassistant.core import callback +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaCommonFlowHandler, + SchemaConfigFlowHandler, + SchemaFlowError, + SchemaFlowFormStep, +) +from homeassistant.helpers.selector import ( + SelectSelector, + SelectSelectorConfig, + SelectSelectorMode, + TextSelector, +) + +from .const import CONF_FOLDER, CONF_PATTERNS, DEFAULT_PATTERN, DOMAIN + + +async def validate_setup( + handler: SchemaCommonFlowHandler, user_input: dict[str, Any] +) -> dict[str, Any]: + """Check path is a folder.""" + value: str = user_input[CONF_FOLDER] + dir_in = os.path.expanduser(str(value)) + handler.parent_handler._async_abort_entries_match({CONF_FOLDER: value}) # pylint: disable=protected-access + + if not os.path.isdir(dir_in): + raise SchemaFlowError("not_dir") + if not os.access(dir_in, os.R_OK): + raise SchemaFlowError("not_readable_dir") + if not handler.parent_handler.hass.config.is_allowed_path(value): + raise SchemaFlowError("not_allowed_dir") + + return user_input + + +async def validate_import_setup( + handler: SchemaCommonFlowHandler, user_input: dict[str, Any] +) -> dict[str, Any]: + """Create issue on successful import.""" + async_create_issue( + handler.parent_handler.hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.11.0", + is_fixable=False, + is_persistent=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Folder Watcher", + }, + ) + return user_input + + +OPTIONS_SCHEMA = vol.Schema( + { + vol.Optional(CONF_PATTERNS, default=[DEFAULT_PATTERN]): SelectSelector( + SelectSelectorConfig( + options=[DEFAULT_PATTERN], + multiple=True, + custom_value=True, + mode=SelectSelectorMode.DROPDOWN, + ) + ), + } +) +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_FOLDER): TextSelector(), + } +).extend(OPTIONS_SCHEMA.schema) + +CONFIG_FLOW = { + "user": SchemaFlowFormStep(schema=DATA_SCHEMA, validate_user_input=validate_setup), + "import": SchemaFlowFormStep( + schema=DATA_SCHEMA, validate_user_input=validate_import_setup + ), +} +OPTIONS_FLOW = { + "init": SchemaFlowFormStep(schema=OPTIONS_SCHEMA), +} + + +class FolderWatcherConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): + """Handle a config flow for Folder Watcher.""" + + config_flow = CONFIG_FLOW + options_flow = OPTIONS_FLOW + + def async_config_entry_title(self, options: Mapping[str, Any]) -> str: + """Return config entry title.""" + return f"Folder Watcher {options[CONF_FOLDER]}" + + @callback + def async_create_entry( + self, data: Mapping[str, Any], **kwargs: Any + ) -> ConfigFlowResult: + """Finish config flow and create a config entry.""" + self._async_abort_entries_match({CONF_FOLDER: data[CONF_FOLDER]}) + return super().async_create_entry(data, **kwargs) diff --git a/homeassistant/components/folder_watcher/const.py b/homeassistant/components/folder_watcher/const.py new file mode 100644 index 00000000000..22dae3b9164 --- /dev/null +++ b/homeassistant/components/folder_watcher/const.py @@ -0,0 +1,6 @@ +"""Constants for Folder watcher.""" + +CONF_FOLDER = "folder" +CONF_PATTERNS = "patterns" +DEFAULT_PATTERN = "*" +DOMAIN = "folder_watcher" diff --git a/homeassistant/components/folder_watcher/manifest.json b/homeassistant/components/folder_watcher/manifest.json index 96decd0b8cf..7b471e08fcc 100644 --- a/homeassistant/components/folder_watcher/manifest.json +++ b/homeassistant/components/folder_watcher/manifest.json @@ -2,6 +2,7 @@ "domain": "folder_watcher", "name": "Folder Watcher", "codeowners": [], + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/folder_watcher", "iot_class": "local_polling", "loggers": ["watchdog"], diff --git a/homeassistant/components/folder_watcher/strings.json b/homeassistant/components/folder_watcher/strings.json new file mode 100644 index 00000000000..bd1742b8ce3 --- /dev/null +++ b/homeassistant/components/folder_watcher/strings.json @@ -0,0 +1,46 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + }, + "error": { + "not_dir": "Configured path is not a directory", + "not_readable_dir": "Configured path is not readable", + "not_allowed_dir": "Configured path is not in allowlist" + }, + "step": { + "user": { + "data": { + "folder": "Path to the watched folder", + "patterns": "Pattern(s) to monitor" + }, + "data_description": { + "folder": "Path needs to be from root, as example `/config`", + "patterns": "Example: `*.yaml` to only see yaml files" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "patterns": "[%key:component::folder_watcher::config::step::user::data::patterns%]" + }, + "data_description": { + "patterns": "[%key:component::folder_watcher::config::step::user::data_description::patterns%]" + } + } + } + }, + "issues": { + "import_failed_not_allowed_path": { + "title": "The Folder Watcher YAML configuration could not be imported", + "description": "Configuring Folder Watcher using YAML is being removed but your configuration could not be imported as the folder {path} is not in the configured allowlist.\n\nPlease add it to `{config_variable}` in config.yaml and restart Home Assistant to import it and fix this issue." + }, + "setup_not_allowed_path": { + "title": "The Folder Watcher configuration for {path} could not start", + "description": "The path {path} is not accessible or not allowed to be accessed.\n\nPlease check the path is accessible and add it to `{config_variable}` in config.yaml and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index e5d5f37ad5a..6f6ce237904 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -175,6 +175,7 @@ FLOWS = { "flo", "flume", "flux_led", + "folder_watcher", "forecast_solar", "forked_daapd", "foscam", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 0ee796d5376..e6a103989d1 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1956,7 +1956,7 @@ "folder_watcher": { "name": "Folder Watcher", "integration_type": "hub", - "config_flow": false, + "config_flow": true, "iot_class": "local_polling" }, "foobot": { diff --git a/tests/components/folder_watcher/conftest.py b/tests/components/folder_watcher/conftest.py new file mode 100644 index 00000000000..06c0a41d49c --- /dev/null +++ b/tests/components/folder_watcher/conftest.py @@ -0,0 +1,17 @@ +"""Fixtures for Folder Watcher integration tests.""" + +from __future__ import annotations + +from collections.abc import Generator +from unittest.mock import patch + +import pytest + + +@pytest.fixture +def mock_setup_entry() -> Generator[None, None, None]: + """Mock setting up a config entry.""" + with patch( + "homeassistant.components.folder_watcher.async_setup_entry", return_value=True + ): + yield diff --git a/tests/components/folder_watcher/test_config_flow.py b/tests/components/folder_watcher/test_config_flow.py new file mode 100644 index 00000000000..745059717fb --- /dev/null +++ b/tests/components/folder_watcher/test_config_flow.py @@ -0,0 +1,186 @@ +"""Test the Folder Watcher config flow.""" + +from pathlib import Path +from unittest.mock import patch + +import pytest + +from homeassistant import config_entries +from homeassistant.components.folder_watcher.const import ( + CONF_FOLDER, + CONF_PATTERNS, + DOMAIN, +) +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + +pytestmark = pytest.mark.usefixtures("mock_setup_entry") + + +async def test_form(hass: HomeAssistant, tmp_path: Path) -> None: + """Test we get the form.""" + path = tmp_path.as_posix() + hass.config.allowlist_external_dirs = {path} + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_FOLDER: path}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == f"Folder Watcher {path}" + assert result["options"] == {CONF_FOLDER: path, CONF_PATTERNS: ["*"]} + + +async def test_form_not_allowed_path(hass: HomeAssistant, tmp_path: Path) -> None: + """Test we handle not allowed path.""" + path = tmp_path.as_posix() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_FOLDER: path}, + ) + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "not_allowed_dir"} + + hass.config.allowlist_external_dirs = {tmp_path} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_FOLDER: path}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == f"Folder Watcher {path}" + assert result["options"] == {CONF_FOLDER: path, CONF_PATTERNS: ["*"]} + + +async def test_form_not_directory(hass: HomeAssistant, tmp_path: Path) -> None: + """Test we handle not a directory.""" + path = tmp_path.as_posix() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_FOLDER: "not_a_directory"}, + ) + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "not_dir"} + + hass.config.allowlist_external_dirs = {path} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_FOLDER: path}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == f"Folder Watcher {path}" + assert result["options"] == {CONF_FOLDER: path, CONF_PATTERNS: ["*"]} + + +async def test_form_not_readable_dir(hass: HomeAssistant, tmp_path: Path) -> None: + """Test we handle not able to read directory.""" + path = tmp_path.as_posix() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("os.access", return_value=False): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_FOLDER: path}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "not_readable_dir"} + + hass.config.allowlist_external_dirs = {path} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_FOLDER: path}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == f"Folder Watcher {path}" + assert result["options"] == {CONF_FOLDER: path, CONF_PATTERNS: ["*"]} + + +async def test_form_already_configured(hass: HomeAssistant, tmp_path: Path) -> None: + """Test we abort when entry is already configured.""" + path = tmp_path.as_posix() + hass.config.allowlist_external_dirs = {path} + + entry = MockConfigEntry( + domain=DOMAIN, + title=f"Folder Watcher {path}", + data={CONF_FOLDER: path}, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_FOLDER: path}, + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_import(hass: HomeAssistant, tmp_path: Path) -> None: + """Test import flow.""" + path = tmp_path.as_posix() + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_FOLDER: path, CONF_PATTERNS: ["*"]}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == f"Folder Watcher {path}" + assert result["options"] == {CONF_FOLDER: path, CONF_PATTERNS: ["*"]} + + +async def test_import_already_configured(hass: HomeAssistant, tmp_path: Path) -> None: + """Test we abort import when entry is already configured.""" + path = tmp_path.as_posix() + + entry = MockConfigEntry( + domain=DOMAIN, + title=f"Folder Watcher {path}", + data={CONF_FOLDER: path}, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_FOLDER: path}, + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" From b8f44fb7229bdae693f23dbbfaeb4df75a231f88 Mon Sep 17 00:00:00 2001 From: Thomas Kistler Date: Tue, 23 Apr 2024 00:01:25 -0700 Subject: [PATCH 762/967] Update Hydrawise from the legacy API to the new GraphQL API (#106904) * Update Hydrawise from the legacy API to the new GraphQL API. * Cleanup --- .../components/hydrawise/__init__.py | 16 +++- .../components/hydrawise/config_flow.py | 66 ++++++++++++---- .../components/hydrawise/strings.json | 8 +- tests/components/hydrawise/conftest.py | 47 +++++++++++- .../components/hydrawise/test_config_flow.py | 75 +++++++++++++++++-- tests/components/hydrawise/test_init.py | 13 ++++ 6 files changed, 197 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/hydrawise/__init__.py b/homeassistant/components/hydrawise/__init__.py index 62a4cacc5c4..b4e14c42709 100644 --- a/homeassistant/components/hydrawise/__init__.py +++ b/homeassistant/components/hydrawise/__init__.py @@ -1,10 +1,11 @@ """Support for Hydrawise cloud.""" -from pydrawise import legacy +from pydrawise import auth, client from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, Platform +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from .const import DOMAIN, SCAN_INTERVAL from .coordinator import HydrawiseDataUpdateCoordinator @@ -14,8 +15,15 @@ PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.S async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up Hydrawise from a config entry.""" - access_token = config_entry.data[CONF_API_KEY] - hydrawise = legacy.LegacyHydrawiseAsync(access_token) + if CONF_USERNAME not in config_entry.data or CONF_PASSWORD not in config_entry.data: + # The GraphQL API requires username and password to authenticate. If either is + # missing, reauth is required. + raise ConfigEntryAuthFailed + + hydrawise = client.Hydrawise( + auth.Auth(config_entry.data[CONF_USERNAME], config_entry.data[CONF_PASSWORD]) + ) + coordinator = HydrawiseDataUpdateCoordinator(hass, hydrawise, SCAN_INTERVAL) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator diff --git a/homeassistant/components/hydrawise/config_flow.py b/homeassistant/components/hydrawise/config_flow.py index 8233074c3cd..1c2c1c5cf29 100644 --- a/homeassistant/components/hydrawise/config_flow.py +++ b/homeassistant/components/hydrawise/config_flow.py @@ -2,15 +2,16 @@ from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Mapping from typing import Any from aiohttp import ClientError -from pydrawise import legacy +from pydrawise import auth, client +from pydrawise.exceptions import NotAuthorizedError import voluptuous as vol -from homeassistant.config_entries import ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_API_KEY +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from .const import DOMAIN, LOGGER @@ -20,14 +21,26 @@ class HydrawiseConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 - async def _create_entry( - self, api_key: str, *, on_failure: Callable[[str], ConfigFlowResult] + def __init__(self) -> None: + """Construct a ConfigFlow.""" + self.reauth_entry: ConfigEntry | None = None + + async def _create_or_update_entry( + self, + username: str, + password: str, + *, + on_failure: Callable[[str], ConfigFlowResult], ) -> ConfigFlowResult: """Create the config entry.""" - api = legacy.LegacyHydrawiseAsync(api_key) + + # 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(fetch_zones=False) + user = await api.get_user() + except NotAuthorizedError: + return on_failure("invalid_auth") except TimeoutError: return on_failure("timeout_connect") except ClientError as ex: @@ -35,17 +48,33 @@ class HydrawiseConfigFlow(ConfigFlow, domain=DOMAIN): return on_failure("cannot_connect") await self.async_set_unique_id(f"hydrawise-{user.customer_id}") - self._abort_if_unique_id_configured() - return self.async_create_entry(title="Hydrawise", data={CONF_API_KEY: api_key}) + if not self.reauth_entry: + self._abort_if_unique_id_configured() + return self.async_create_entry( + title="Hydrawise", + data={CONF_USERNAME: username, CONF_PASSWORD: password}, + ) + + self.hass.config_entries.async_update_entry( + self.reauth_entry, + data=self.reauth_entry.data + | {CONF_USERNAME: username, CONF_PASSWORD: password}, + ) + await self.hass.config_entries.async_reload(self.reauth_entry.entry_id) + return self.async_abort(reason="reauth_successful") async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle the initial setup.""" if user_input is not None: - api_key = user_input[CONF_API_KEY] - return await self._create_entry(api_key, on_failure=self._show_form) + username = user_input[CONF_USERNAME] + password = user_input[CONF_PASSWORD] + + return await self._create_or_update_entry( + username=username, password=password, on_failure=self._show_form + ) return self._show_form() def _show_form(self, error_type: str | None = None) -> ConfigFlowResult: @@ -54,6 +83,17 @@ class HydrawiseConfigFlow(ConfigFlow, domain=DOMAIN): errors["base"] = error_type return self.async_show_form( step_id="user", - data_schema=vol.Schema({vol.Required(CONF_API_KEY): str}), + data_schema=vol.Schema( + {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} + ), errors=errors, ) + + async def async_step_reauth( + self, user_input: Mapping[str, Any] + ) -> ConfigFlowResult: + """Perform reauth after updating config to username/password.""" + self.reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + return await self.async_step_user() diff --git a/homeassistant/components/hydrawise/strings.json b/homeassistant/components/hydrawise/strings.json index 1c96098db35..ee5cc0a541c 100644 --- a/homeassistant/components/hydrawise/strings.json +++ b/homeassistant/components/hydrawise/strings.json @@ -2,8 +2,11 @@ "config": { "step": { "user": { + "title": "Hydrawise Login", + "description": "Please provide the username and password for your Hydrawise cloud account:", "data": { - "api_key": "[%key:common::config_flow::data::api_key%]" + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" } } }, @@ -13,7 +16,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%]" } }, "entity": { diff --git a/tests/components/hydrawise/conftest.py b/tests/components/hydrawise/conftest.py index 8e22fbe84f7..11670cb3565 100644 --- a/tests/components/hydrawise/conftest.py +++ b/tests/components/hydrawise/conftest.py @@ -15,7 +15,7 @@ from pydrawise.schema import ( import pytest from homeassistant.components.hydrawise.const import DOMAIN -from homeassistant.const import CONF_API_KEY +from homeassistant.const import CONF_API_KEY, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util @@ -32,7 +32,7 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture -def mock_pydrawise( +def mock_legacy_pydrawise( user: User, controller: Controller, zones: list[Zone], @@ -47,10 +47,32 @@ def mock_pydrawise( yield mock_pydrawise.return_value +@pytest.fixture +def mock_pydrawise( + mock_auth: AsyncMock, + user: User, + controller: Controller, + zones: list[Zone], +) -> Generator[AsyncMock, None, None]: + """Mock Hydrawise.""" + with patch("pydrawise.client.Hydrawise", autospec=True) as mock_pydrawise: + user.controllers = [controller] + controller.zones = zones + mock_pydrawise.return_value.get_user.return_value = user + yield mock_pydrawise.return_value + + +@pytest.fixture +def mock_auth() -> Generator[AsyncMock, None, None]: + """Mock pydrawise Auth.""" + with patch("pydrawise.auth.Auth", autospec=True) as mock_auth: + yield mock_auth.return_value + + @pytest.fixture def user() -> User: """Hydrawise User fixture.""" - return User(customer_id=12345) + return User(customer_id=12345, email="asdf@asdf.com") @pytest.fixture @@ -102,7 +124,7 @@ def zones() -> list[Zone]: @pytest.fixture -def mock_config_entry() -> MockConfigEntry: +def mock_config_entry_legacy() -> MockConfigEntry: """Mock ConfigEntry.""" return MockConfigEntry( title="Hydrawise", @@ -111,6 +133,23 @@ def mock_config_entry() -> MockConfigEntry: CONF_API_KEY: "abc123", }, unique_id="hydrawise-customerid", + version=1, + ) + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Mock ConfigEntry.""" + return MockConfigEntry( + title="Hydrawise", + domain=DOMAIN, + data={ + CONF_USERNAME: "asfd@asdf.com", + CONF_PASSWORD: "__password__", + }, + unique_id="hydrawise-customerid", + version=1, + minor_version=2, ) diff --git a/tests/components/hydrawise/test_config_flow.py b/tests/components/hydrawise/test_config_flow.py index be0ef90becd..a7fbc008aab 100644 --- a/tests/components/hydrawise/test_config_flow.py +++ b/tests/components/hydrawise/test_config_flow.py @@ -3,14 +3,18 @@ from unittest.mock import AsyncMock from aiohttp import ClientError +from pydrawise.exceptions import NotAuthorizedError from pydrawise.schema import User import pytest from homeassistant import config_entries from homeassistant.components.hydrawise.const import DOMAIN +from homeassistant.const import CONF_API_KEY, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from tests.common import MockConfigEntry + pytestmark = pytest.mark.usefixtures("mock_setup_entry") @@ -29,16 +33,20 @@ async def test_form( assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {"api_key": "abc123"} + result["flow_id"], + {CONF_USERNAME: "asdf@asdf.com", CONF_PASSWORD: "__password__"}, ) mock_pydrawise.get_user.return_value = user await hass.async_block_till_done() assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Hydrawise" - assert result2["data"] == {"api_key": "abc123"} + assert result2["data"] == { + CONF_USERNAME: "asdf@asdf.com", + CONF_PASSWORD: "__password__", + } assert len(mock_setup_entry.mock_calls) == 1 - mock_pydrawise.get_user.assert_called_once_with(fetch_zones=False) + mock_pydrawise.get_user.assert_called_once_with() async def test_form_api_error( @@ -50,7 +58,7 @@ async def test_form_api_error( init_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - data = {"api_key": "abc123"} + data = {CONF_USERNAME: "asdf@asdf.com", CONF_PASSWORD: "__password__"} result = await hass.config_entries.flow.async_configure( init_result["flow_id"], data ) @@ -71,7 +79,7 @@ async def test_form_connect_timeout( init_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - data = {"api_key": "abc123"} + data = {CONF_USERNAME: "asdf@asdf.com", CONF_PASSWORD: "__password__"} result = await hass.config_entries.flow.async_configure( init_result["flow_id"], data ) @@ -83,3 +91,60 @@ async def test_form_connect_timeout( mock_pydrawise.get_user.return_value = user result2 = await hass.config_entries.flow.async_configure(result["flow_id"], data) assert result2["type"] is FlowResultType.CREATE_ENTRY + + +async def test_form_not_authorized_error( + hass: HomeAssistant, mock_pydrawise: AsyncMock, user: User +) -> None: + """Test we handle API errors.""" + mock_pydrawise.get_user.side_effect = NotAuthorizedError + + init_result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + data = {CONF_USERNAME: "asdf@asdf.com", CONF_PASSWORD: "__password__"} + result = await hass.config_entries.flow.async_configure( + init_result["flow_id"], data + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "invalid_auth"} + + mock_pydrawise.get_user.reset_mock(side_effect=True) + mock_pydrawise.get_user.return_value = user + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], data) + assert result2["type"] is FlowResultType.CREATE_ENTRY + + +async def test_reauth( + hass: HomeAssistant, + user: User, + mock_pydrawise: AsyncMock, +) -> None: + """Test that re-authorization works.""" + mock_config_entry = MockConfigEntry( + title="Hydrawise", + domain=DOMAIN, + data={ + CONF_API_KEY: "__api_key__", + }, + unique_id="hydrawise-12345", + ) + mock_config_entry.add_to_hass(hass) + + mock_config_entry.async_start_reauth(hass) + await hass.async_block_till_done() + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + [result] = flows + assert result["step_id"] == "user" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_USERNAME: "asdf@asdf.com", CONF_PASSWORD: "__password__"}, + ) + mock_pydrawise.get_user.return_value = user + await hass.async_block_till_done() + + assert result2["type"] is FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" diff --git a/tests/components/hydrawise/test_init.py b/tests/components/hydrawise/test_init.py index 91c99833531..8ec3c3da648 100644 --- a/tests/components/hydrawise/test_init.py +++ b/tests/components/hydrawise/test_init.py @@ -19,3 +19,16 @@ async def test_connect_retry( 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 + + +async def test_update_version( + hass: HomeAssistant, mock_config_entry_legacy: MockConfigEntry +) -> None: + """Test updating to the GaphQL API works.""" + mock_config_entry_legacy.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry_legacy.entry_id) + await hass.async_block_till_done() + assert mock_config_entry_legacy.state is ConfigEntryState.SETUP_ERROR + + # Make sure reauth flow has been initiated + assert any(mock_config_entry_legacy.async_get_active_flows(hass, {"reauth"})) From e0c785b2b4b79965f31b43ccc074e2f31e114fa4 Mon Sep 17 00:00:00 2001 From: Shai Ungar Date: Tue, 23 Apr 2024 10:01:45 +0300 Subject: [PATCH 763/967] Add coordinator to 17Track (#115057) * Add coordinator to 17Track * Add coordinator to 17Track remove SensorEntityDescription (different PR) * Update homeassistant/components/seventeentrack/coordinator.py Co-authored-by: Joost Lekkerkerker * Update homeassistant/components/seventeentrack/sensor.py Co-authored-by: Joost Lekkerkerker * Add coordinator to 17Track fix CR * Add coordinator to 17Track fix second CR * Add coordinator to 17Track remove commented out code + fix display name * Add coordinator to 17Track created a set outside _async_create_remove_entities function * Add coordinator to 17Track fix CR * Add coordinator to 17Track fix CR 2 * Update homeassistant/components/seventeentrack/coordinator.py Co-authored-by: Joost Lekkerkerker * Add coordinator to 17Track raise UpdateFailed if API throws an exception * Add coordinator to 17Track merge calls --------- Co-authored-by: Joost Lekkerkerker --- .../components/seventeentrack/__init__.py | 9 +- .../components/seventeentrack/const.py | 3 + .../components/seventeentrack/coordinator.py | 84 +++++ .../components/seventeentrack/sensor.py | 340 ++++++++---------- tests/components/seventeentrack/__init__.py | 2 +- tests/components/seventeentrack/conftest.py | 8 +- .../components/seventeentrack/test_sensor.py | 51 +-- 7 files changed, 257 insertions(+), 240 deletions(-) create mode 100644 homeassistant/components/seventeentrack/coordinator.py diff --git a/homeassistant/components/seventeentrack/__init__.py b/homeassistant/components/seventeentrack/__init__.py index 183d1bd4068..1f9879cdcbc 100644 --- a/homeassistant/components/seventeentrack/__init__.py +++ b/homeassistant/components/seventeentrack/__init__.py @@ -10,8 +10,9 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN +from .coordinator import SeventeenTrackCoordinator -PLATFORMS = [Platform.SENSOR] +PLATFORMS: list[Platform] = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -25,8 +26,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except SeventeenTrackError as err: raise ConfigEntryNotReady from err - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = client + coordinator = SeventeenTrackCoordinator(hass, client) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - return True diff --git a/homeassistant/components/seventeentrack/const.py b/homeassistant/components/seventeentrack/const.py index 6f8ae1b221c..fc7ca7b2e7f 100644 --- a/homeassistant/components/seventeentrack/const.py +++ b/homeassistant/components/seventeentrack/const.py @@ -1,6 +1,9 @@ """Constants for the 17track.net component.""" from datetime import timedelta +import logging + +LOGGER = logging.getLogger(__package__) ATTR_DESTINATION_COUNTRY = "destination_country" ATTR_INFO_TEXT = "info_text" diff --git a/homeassistant/components/seventeentrack/coordinator.py b/homeassistant/components/seventeentrack/coordinator.py new file mode 100644 index 00000000000..84bdf1e1359 --- /dev/null +++ b/homeassistant/components/seventeentrack/coordinator.py @@ -0,0 +1,84 @@ +"""Coordinator for 17Track.""" + +from dataclasses import dataclass +from typing import Any + +from py17track import Client as SeventeenTrackClient +from py17track.errors import SeventeenTrackError +from py17track.package import Package + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util import slugify + +from .const import ( + CONF_SHOW_ARCHIVED, + CONF_SHOW_DELIVERED, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + LOGGER, +) + + +@dataclass +class SeventeenTrackData: + """Class for handling the data retrieval.""" + + summary: dict[str, dict[str, Any]] + live_packages: dict[str, Package] + + +class SeventeenTrackCoordinator(DataUpdateCoordinator[SeventeenTrackData]): + """Class to manage fetching 17Track data.""" + + config_entry: ConfigEntry + + def __init__(self, hass: HomeAssistant, client: SeventeenTrackClient) -> None: + """Initialize.""" + super().__init__( + hass, + LOGGER, + name=DOMAIN, + update_interval=DEFAULT_SCAN_INTERVAL, + ) + self.show_delivered = self.config_entry.options[CONF_SHOW_DELIVERED] + self.account_id = client.profile.account_id + + self._show_archived = self.config_entry.options[CONF_SHOW_ARCHIVED] + self._client = client + + async def _async_update_data(self) -> SeventeenTrackData: + """Fetch data from 17Track API.""" + + try: + summary = await self._client.profile.summary( + show_archived=self._show_archived + ) + + live_packages = set( + await self._client.profile.packages(show_archived=self._show_archived) + ) + + except SeventeenTrackError as err: + raise UpdateFailed(err) from err + + summary_dict = {} + live_packages_dict = {} + + for status, quantity in summary.items(): + summary_dict[slugify(status)] = { + "quantity": quantity, + "packages": [], + "status_name": status, + } + + for package in live_packages: + live_packages_dict[package.tracking_number] = package + summary_value = summary_dict.get(slugify(package.status)) + if summary_value: + summary_value["packages"].append(package) + + return SeventeenTrackData( + summary=summary_dict, live_packages=live_packages_dict + ) diff --git a/homeassistant/components/seventeentrack/sensor.py b/homeassistant/components/seventeentrack/sensor.py index 1de627fab39..cbad01d0b0a 100644 --- a/homeassistant/components/seventeentrack/sensor.py +++ b/homeassistant/components/seventeentrack/sensor.py @@ -2,10 +2,8 @@ from __future__ import annotations -import logging +from typing import Any -from py17track.errors import SeventeenTrackError -from py17track.package import Package import voluptuous as vol from homeassistant.components import persistent_notification @@ -17,15 +15,15 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_USERNAME, ) -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback from homeassistant.data_entry_flow import FlowResultType -from homeassistant.helpers import config_validation as cv, entity, entity_registry as er +from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_call_later from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType -from homeassistant.util import Throttle, slugify +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import SeventeenTrackCoordinator from .const import ( ATTR_DESTINATION_COUNTRY, ATTR_INFO_TEXT, @@ -39,17 +37,15 @@ from .const import ( ATTRIBUTION, CONF_SHOW_ARCHIVED, CONF_SHOW_DELIVERED, - DEFAULT_SCAN_INTERVAL, DOMAIN, ENTITY_ID_TEMPLATE, + LOGGER, NOTIFICATION_DELIVERED_MESSAGE, NOTIFICATION_DELIVERED_TITLE, UNIQUE_ID_TEMPLATE, VALUE_DELIVERED, ) -_LOGGER = logging.getLogger(__name__) - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_USERNAME): cv.string, @@ -111,81 +107,155 @@ async def async_setup_entry( ) -> None: """Set up a 17Track sensor entry.""" - client = hass.data[DOMAIN][config_entry.entry_id] + coordinator: SeventeenTrackCoordinator = hass.data[DOMAIN][config_entry.entry_id] + previous_tracking_numbers: set[str] = set() - data = SeventeenTrackData( - client, - async_add_entities, - DEFAULT_SCAN_INTERVAL, - config_entry.options[CONF_SHOW_ARCHIVED], - config_entry.options[CONF_SHOW_DELIVERED], - str(hass.config.time_zone), + @callback + def _async_create_remove_entities(): + live_tracking_numbers = set(coordinator.data.live_packages.keys()) + + new_tracking_numbers = live_tracking_numbers - previous_tracking_numbers + old_tracking_numbers = previous_tracking_numbers - live_tracking_numbers + + previous_tracking_numbers.update(live_tracking_numbers) + + packages_to_add = [ + coordinator.data.live_packages[tracking_number] + for tracking_number in new_tracking_numbers + ] + + for package_data in coordinator.data.live_packages.values(): + if ( + package_data.status == VALUE_DELIVERED + and not coordinator.show_delivered + ): + old_tracking_numbers.add(package_data.tracking_number) + notify_delivered( + hass, + package_data.friendly_name, + package_data.tracking_number, + ) + + remove_packages(hass, coordinator.account_id, old_tracking_numbers) + + async_add_entities( + SeventeenTrackPackageSensor( + coordinator, + package_data.tracking_number, + ) + for package_data in packages_to_add + if not ( + not coordinator.show_delivered and package_data.status == "Delivered" + ) + ) + + async_add_entities( + SeventeenTrackSummarySensor(status, summary_data["status_name"], coordinator) + for status, summary_data in coordinator.data.summary.items() + ) + + _async_create_remove_entities() + + config_entry.async_on_unload( + coordinator.async_add_listener(_async_create_remove_entities) ) - await data.async_update() -class SeventeenTrackSummarySensor(SensorEntity): +class SeventeenTrackSummarySensor( + CoordinatorEntity[SeventeenTrackCoordinator], SensorEntity +): """Define a summary sensor.""" _attr_attribution = ATTRIBUTION _attr_icon = "mdi:package" _attr_native_unit_of_measurement = "packages" - def __init__(self, data, status, initial_state) -> None: - """Initialize.""" - self._attr_extra_state_attributes = {} - self._data = data - self._state = initial_state + def __init__( + self, + status: str, + status_name: str, + coordinator: SeventeenTrackCoordinator, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator) self._status = status - self._attr_name = f"Seventeentrack Packages {status}" - self._attr_unique_id = f"summary_{data.account_id}_{slugify(status)}" + self._attr_name = f"Seventeentrack Packages {status_name}" + self._attr_unique_id = f"summary_{coordinator.account_id}_{self._status}" @property def available(self) -> bool: """Return whether the entity is available.""" - return self._state is not None + return self._status in self.coordinator.data.summary @property def native_value(self) -> StateType: - """Return the state.""" - return self._state + """Return the state of the sensor.""" + return self.coordinator.data.summary[self._status]["quantity"] - async def async_update(self) -> None: - """Update the sensor.""" - await self._data.async_update() - - package_data = [] - for package in self._data.packages.values(): - if package.status != self._status: - continue - - package_data.append( + @property + def extra_state_attributes(self) -> dict[str, Any] | None: + """Return the state attributes.""" + packages = self.coordinator.data.summary[self._status]["packages"] + return { + ATTR_PACKAGES: [ { - ATTR_FRIENDLY_NAME: package.friendly_name, - ATTR_INFO_TEXT: package.info_text, - ATTR_TIMESTAMP: package.timestamp, - ATTR_STATUS: package.status, - ATTR_LOCATION: package.location, ATTR_TRACKING_NUMBER: package.tracking_number, + ATTR_LOCATION: package.location, + ATTR_STATUS: package.status, + ATTR_TIMESTAMP: package.timestamp, + ATTR_INFO_TEXT: package.info_text, + ATTR_FRIENDLY_NAME: package.friendly_name, } - ) - - self._attr_extra_state_attributes[ATTR_PACKAGES] = ( - package_data if package_data else None - ) - - self._state = self._data.summary.get(self._status) + for package in packages + ] + } -class SeventeenTrackPackageSensor(SensorEntity): +class SeventeenTrackPackageSensor( + CoordinatorEntity[SeventeenTrackCoordinator], SensorEntity +): """Define an individual package sensor.""" _attr_attribution = ATTRIBUTION _attr_icon = "mdi:package" - def __init__(self, data, package) -> None: - """Initialize.""" - self._attr_extra_state_attributes = { + def __init__( + self, + coordinator: SeventeenTrackCoordinator, + tracking_number: str, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator) + self._tracking_number = tracking_number + self._previous_status = coordinator.data.live_packages[tracking_number].status + self.entity_id = ENTITY_ID_TEMPLATE.format(tracking_number) + self._attr_unique_id = UNIQUE_ID_TEMPLATE.format( + coordinator.account_id, tracking_number + ) + + @property + def available(self) -> bool: + """Return whether the entity is available.""" + return self._tracking_number in self.coordinator.data.live_packages + + @property + def name(self) -> str: + """Return the name.""" + package = self.coordinator.data.live_packages.get(self._tracking_number) + if package is None or not (name := package.friendly_name): + name = self._tracking_number + return f"Seventeentrack Package: {name}" + + @property + def native_value(self) -> StateType: + """Return the state.""" + return self.coordinator.data.live_packages[self._tracking_number].status + + @property + def extra_state_attributes(self) -> dict[str, Any] | None: + """Return the state attributes.""" + package = self.coordinator.data.live_packages[self._tracking_number] + return { ATTR_DESTINATION_COUNTRY: package.destination_country, ATTR_INFO_TEXT: package.info_text, ATTR_TIMESTAMP: package.timestamp, @@ -195,158 +265,30 @@ class SeventeenTrackPackageSensor(SensorEntity): ATTR_TRACKING_INFO_LANGUAGE: package.tracking_info_language, ATTR_TRACKING_NUMBER: package.tracking_number, } - self._data = data - self._friendly_name = package.friendly_name - self._state = package.status - self._tracking_number = package.tracking_number - self.entity_id = ENTITY_ID_TEMPLATE.format(self._tracking_number) - self._attr_unique_id = UNIQUE_ID_TEMPLATE.format( - data.account_id, self._tracking_number - ) - @property - def available(self) -> bool: - """Return whether the entity is available.""" - return self._data.packages.get(self._tracking_number) is not None - @property - def name(self) -> str: - """Return the name.""" - if not (name := self._friendly_name): - name = self._tracking_number - return f"Seventeentrack Package: {name}" - - @property - def native_value(self) -> StateType: - """Return the state.""" - return self._state - - async def async_update(self) -> None: - """Update the sensor.""" - await self._data.async_update() - - if not self.available: - # Entity cannot be removed while its being added - async_call_later(self.hass, 1, self._remove) - return - - package = self._data.packages.get(self._tracking_number, None) - - # If the user has elected to not see delivered packages and one gets - # delivered, post a notification: - if package.status == VALUE_DELIVERED and not self._data.show_delivered: - self._notify_delivered() - # Entity cannot be removed while its being added - async_call_later(self.hass, 1, self._remove) - return - - self._attr_extra_state_attributes.update( - { - ATTR_INFO_TEXT: package.info_text, - ATTR_TIMESTAMP: package.timestamp, - ATTR_LOCATION: package.location, - } - ) - self._state = package.status - self._friendly_name = package.friendly_name - - async def _remove(self, *_): - """Remove entity itself.""" - await self.async_remove(force_remove=True) - - reg = er.async_get(self.hass) +def remove_packages(hass: HomeAssistant, account_id: str, packages: set[str]) -> None: + """Remove entity itself.""" + reg = er.async_get(hass) + for package in packages: entity_id = reg.async_get_entity_id( "sensor", "seventeentrack", - UNIQUE_ID_TEMPLATE.format(self._data.account_id, self._tracking_number), + UNIQUE_ID_TEMPLATE.format(account_id, package), ) if entity_id: reg.async_remove(entity_id) - def _notify_delivered(self): - """Notify when package is delivered.""" - _LOGGER.info("Package delivered: %s", self._tracking_number) - identification = ( - self._friendly_name if self._friendly_name else self._tracking_number - ) - message = NOTIFICATION_DELIVERED_MESSAGE.format( - identification, self._tracking_number - ) - title = NOTIFICATION_DELIVERED_TITLE.format(identification) - notification_id = NOTIFICATION_DELIVERED_TITLE.format(self._tracking_number) +def notify_delivered(hass: HomeAssistant, friendly_name: str, tracking_number: str): + """Notify when package is delivered.""" + LOGGER.debug("Package delivered: %s", tracking_number) - persistent_notification.create( - self.hass, message, title=title, notification_id=notification_id - ) + identification = friendly_name if friendly_name else tracking_number + message = NOTIFICATION_DELIVERED_MESSAGE.format(identification, tracking_number) + title = NOTIFICATION_DELIVERED_TITLE.format(identification) + notification_id = NOTIFICATION_DELIVERED_TITLE.format(tracking_number) - -class SeventeenTrackData: - """Define a data handler for 17track.net.""" - - def __init__( - self, - client, - async_add_entities, - scan_interval, - show_archived, - show_delivered, - timezone, - ) -> None: - """Initialize.""" - self._async_add_entities = async_add_entities - self._client = client - self._scan_interval = scan_interval - self._show_archived = show_archived - self.account_id = client.profile.account_id - self.packages: dict[str, Package] = {} - self.show_delivered = show_delivered - self.timezone = timezone - self.summary: dict[str, int] = {} - self.async_update = Throttle(self._scan_interval)(self._async_update) - self.first_update = True - - async def _async_update(self): - """Get updated data from 17track.net.""" - entities: list[entity.Entity] = [] - - try: - packages = await self._client.profile.packages( - show_archived=self._show_archived, tz=self.timezone - ) - _LOGGER.debug("New package data received: %s", packages) - - new_packages = {p.tracking_number: p for p in packages} - - to_add = set(new_packages) - set(self.packages) - - _LOGGER.debug("Will add new tracking numbers: %s", to_add) - if to_add: - entities.extend( - SeventeenTrackPackageSensor(self, new_packages[tracking_number]) - for tracking_number in to_add - ) - - self.packages = new_packages - except SeventeenTrackError as err: - _LOGGER.error("There was an error retrieving packages: %s", err) - - try: - self.summary = await self._client.profile.summary( - show_archived=self._show_archived - ) - _LOGGER.debug("New summary data received: %s", self.summary) - - # creating summary sensors on first update - if self.first_update: - self.first_update = False - entities.extend( - SeventeenTrackSummarySensor(self, status, quantity) - for status, quantity in self.summary.items() - ) - - except SeventeenTrackError as err: - _LOGGER.error("There was an error retrieving the summary: %s", err) - self.summary = {} - - self._async_add_entities(entities, True) + persistent_notification.create( + hass, message, title=title, notification_id=notification_id + ) diff --git a/tests/components/seventeentrack/__init__.py b/tests/components/seventeentrack/__init__.py index 4101f34496e..b3452b38f96 100644 --- a/tests/components/seventeentrack/__init__.py +++ b/tests/components/seventeentrack/__init__.py @@ -4,7 +4,7 @@ from datetime import timedelta from freezegun.api import FrozenDateTimeFactory -from homeassistant.components.seventeentrack.sensor import DEFAULT_SCAN_INTERVAL +from homeassistant.components.seventeentrack.const import DEFAULT_SCAN_INTERVAL from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, async_fire_time_changed diff --git a/tests/components/seventeentrack/conftest.py b/tests/components/seventeentrack/conftest.py index 2865b3f2599..2e266a9b13c 100644 --- a/tests/components/seventeentrack/conftest.py +++ b/tests/components/seventeentrack/conftest.py @@ -7,12 +7,10 @@ from py17track.package import Package import pytest from homeassistant.components.seventeentrack.const import ( - DEFAULT_SHOW_ARCHIVED, - DEFAULT_SHOW_DELIVERED, -) -from homeassistant.components.seventeentrack.sensor import ( CONF_SHOW_ARCHIVED, CONF_SHOW_DELIVERED, + DEFAULT_SHOW_ARCHIVED, + DEFAULT_SHOW_DELIVERED, ) from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -28,6 +26,8 @@ DEFAULT_SUMMARY = { "Returned": 0, } +DEFAULT_SUMMARY_LENGTH = len(DEFAULT_SUMMARY) + ACCOUNT_ID = "1234" NEW_SUMMARY_DATA = { diff --git a/tests/components/seventeentrack/test_sensor.py b/tests/components/seventeentrack/test_sensor.py index aa7f61ad318..27de64ca89f 100644 --- a/tests/components/seventeentrack/test_sensor.py +++ b/tests/components/seventeentrack/test_sensor.py @@ -14,6 +14,7 @@ from homeassistant.setup import async_setup_component from . import goto_future, init_integration from .conftest import ( DEFAULT_SUMMARY, + DEFAULT_SUMMARY_LENGTH, NEW_SUMMARY_DATA, VALID_PLATFORM_CONFIG_FULL, get_package, @@ -72,11 +73,10 @@ async def test_add_package( """Ensure package is added correctly when user add a new package.""" package = get_package() mock_seventeentrack.return_value.profile.packages.return_value = [package] - mock_seventeentrack.return_value.profile.summary.return_value = {} await init_integration(hass, mock_config_entry) - assert hass.states.get("sensor.seventeentrack_package_456") is not None - assert len(hass.states.async_entity_ids()) == 1 + assert hass.states.get("sensor.seventeentrack_package_456") + assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH + 1 package2 = get_package( tracking_number="789", @@ -90,7 +90,7 @@ async def test_add_package( await goto_future(hass, freezer) assert hass.states.get("sensor.seventeentrack_package_789") is not None - assert len(hass.states.async_entity_ids()) == 2 + assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH + 2 async def test_add_package_default_friendly_name( @@ -101,13 +101,12 @@ async def test_add_package_default_friendly_name( """Ensure package is added correctly with default friendly name when user add a new package without his own friendly name.""" package = get_package(friendly_name=None) mock_seventeentrack.return_value.profile.packages.return_value = [package] - mock_seventeentrack.return_value.profile.summary.return_value = {} await init_integration(hass, mock_config_entry) state_456 = hass.states.get("sensor.seventeentrack_package_456") assert state_456 is not None assert state_456.attributes["friendly_name"] == "Seventeentrack Package: 456" - assert len(hass.states.async_entity_ids()) == 1 + assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH + 1 async def test_remove_package( @@ -130,26 +129,20 @@ async def test_remove_package( package1, package2, ] - mock_seventeentrack.return_value.profile.summary.return_value = {} await init_integration(hass, mock_config_entry) assert hass.states.get("sensor.seventeentrack_package_456") is not None assert hass.states.get("sensor.seventeentrack_package_789") is not None - assert len(hass.states.async_entity_ids()) == 2 + assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH + 2 mock_seventeentrack.return_value.profile.packages.return_value = [package2] await goto_future(hass, freezer) - assert hass.states.get("sensor.seventeentrack_package_456").state == "unavailable" - assert len(hass.states.async_entity_ids()) == 2 - - await goto_future(hass, freezer) - assert hass.states.get("sensor.seventeentrack_package_456") is None assert hass.states.get("sensor.seventeentrack_package_789") is not None - assert len(hass.states.async_entity_ids()) == 1 + assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH + 1 async def test_package_error( @@ -176,12 +169,11 @@ async def test_friendly_name_changed( """Test friendly name change.""" package = get_package() mock_seventeentrack.return_value.profile.packages.return_value = [package] - mock_seventeentrack.return_value.profile.summary.return_value = {} await init_integration(hass, mock_config_entry) assert hass.states.get("sensor.seventeentrack_package_456") is not None - assert len(hass.states.async_entity_ids()) == 1 + assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH + 1 package = get_package(friendly_name="friendly name 2") mock_seventeentrack.return_value.profile.packages.return_value = [package] @@ -193,7 +185,7 @@ async def test_friendly_name_changed( "sensor.seventeentrack_package_456" ) assert entity.name == "Seventeentrack Package: friendly name 2" - assert len(hass.states.async_entity_ids()) == 1 + assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH + 1 async def test_delivered_not_shown( @@ -205,7 +197,6 @@ async def test_delivered_not_shown( """Ensure delivered packages are not shown.""" package = get_package(status=40) mock_seventeentrack.return_value.profile.packages.return_value = [package] - mock_seventeentrack.return_value.profile.summary.return_value = {} with patch( "homeassistant.components.seventeentrack.sensor.persistent_notification" @@ -213,7 +204,7 @@ async def test_delivered_not_shown( await init_integration(hass, mock_config_entry_with_default_options) await goto_future(hass, freezer) - assert not hass.states.async_entity_ids() + assert hass.states.get("sensor.seventeentrack_package_456") is None persistent_notification_mock.create.assert_called() @@ -225,7 +216,6 @@ async def test_delivered_shown( """Ensure delivered packages are show when user choose to show them.""" package = get_package(status=40) mock_seventeentrack.return_value.profile.packages.return_value = [package] - mock_seventeentrack.return_value.profile.summary.return_value = {} with patch( "homeassistant.components.seventeentrack.sensor.persistent_notification" @@ -233,7 +223,7 @@ async def test_delivered_shown( await init_integration(hass, mock_config_entry) assert hass.states.get("sensor.seventeentrack_package_456") is not None - assert len(hass.states.async_entity_ids()) == 1 + assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH + 1 persistent_notification_mock.create.assert_not_called() @@ -246,12 +236,11 @@ async def test_becomes_delivered_not_shown_notification( """Ensure notification is triggered when package becomes delivered.""" package = get_package() mock_seventeentrack.return_value.profile.packages.return_value = [package] - mock_seventeentrack.return_value.profile.summary.return_value = {} await init_integration(hass, mock_config_entry_with_default_options) assert hass.states.get("sensor.seventeentrack_package_456") is not None - assert len(hass.states.async_entity_ids()) == 1 + assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH + 1 package_delivered = get_package(status=40) mock_seventeentrack.return_value.profile.packages.return_value = [package_delivered] @@ -260,10 +249,9 @@ async def test_becomes_delivered_not_shown_notification( "homeassistant.components.seventeentrack.sensor.persistent_notification" ) as persistent_notification_mock: await goto_future(hass, freezer) - await goto_future(hass, freezer) persistent_notification_mock.create.assert_called() - assert not hass.states.async_entity_ids() + assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH async def test_summary_correctly_updated( @@ -275,11 +263,10 @@ async def test_summary_correctly_updated( """Ensure summary entities are not duplicated.""" package = get_package(status=30) mock_seventeentrack.return_value.profile.packages.return_value = [package] - mock_seventeentrack.return_value.profile.summary.return_value = DEFAULT_SUMMARY await init_integration(hass, mock_config_entry) - assert len(hass.states.async_entity_ids()) == 8 + assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH + 1 state_ready_picked = hass.states.get( "sensor.seventeentrack_packages_ready_to_be_picked_up" @@ -290,10 +277,9 @@ async def test_summary_correctly_updated( mock_seventeentrack.return_value.profile.packages.return_value = [] mock_seventeentrack.return_value.profile.summary.return_value = NEW_SUMMARY_DATA - await goto_future(hass, freezer) await goto_future(hass, freezer) - assert len(hass.states.async_entity_ids()) == 7 + assert len(hass.states.async_entity_ids()) == len(NEW_SUMMARY_DATA) for state in hass.states.async_all(): assert state.state == "1" @@ -301,7 +287,7 @@ async def test_summary_correctly_updated( "sensor.seventeentrack_packages_ready_to_be_picked_up" ) assert state_ready_picked is not None - assert state_ready_picked.attributes["packages"] is None + assert len(state_ready_picked.attributes["packages"]) == 0 async def test_summary_error( @@ -318,7 +304,7 @@ async def test_summary_error( await init_integration(hass, mock_config_entry) - assert len(hass.states.async_entity_ids()) == 1 + assert len(hass.states.async_entity_ids()) == 0 assert ( hass.states.get("sensor.seventeentrack_packages_ready_to_be_picked_up") is None @@ -334,12 +320,11 @@ async def test_utc_timestamp( package = get_package(tz="Asia/Jakarta") mock_seventeentrack.return_value.profile.packages.return_value = [package] - mock_seventeentrack.return_value.profile.summary.return_value = {} await init_integration(hass, mock_config_entry) assert hass.states.get("sensor.seventeentrack_package_456") is not None - assert len(hass.states.async_entity_ids()) == 1 + assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH + 1 state_456 = hass.states.get("sensor.seventeentrack_package_456") assert state_456 is not None assert str(state_456.attributes.get("timestamp")) == "2020-08-10 03:32:00+00:00" From 616c7ce68b1ead9854ac8541fe230b8c5fa79eea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Apr 2024 09:23:45 +0200 Subject: [PATCH 764/967] Bump actions/download-artifact from 4.1.4 to 4.1.6 (#116017) --- .github/workflows/builder.yml | 4 ++-- .github/workflows/ci.yaml | 6 +++--- .github/workflows/wheels.yml | 10 +++++----- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index a440de225be..90c1c3692e9 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -175,7 +175,7 @@ jobs: sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt - name: Download translations - uses: actions/download-artifact@v4.1.5 + uses: actions/download-artifact@v4.1.6 with: name: translations @@ -458,7 +458,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Download translations - uses: actions/download-artifact@v4.1.5 + uses: actions/download-artifact@v4.1.6 with: name: translations diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0dc8f34570c..11d0e04cec1 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -778,7 +778,7 @@ jobs: run: | echo "::add-matcher::.github/workflows/matchers/pytest-slow.json" - name: Download pytest_buckets - uses: actions/download-artifact@v4.1.5 + uses: actions/download-artifact@v4.1.6 with: name: pytest_buckets - name: Compile English translations @@ -1090,7 +1090,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v4.1.3 - name: Download all coverage artifacts - uses: actions/download-artifact@v4.1.5 + uses: actions/download-artifact@v4.1.6 with: pattern: coverage-* - name: Upload coverage to Codecov @@ -1223,7 +1223,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v4.1.3 - name: Download all coverage artifacts - uses: actions/download-artifact@v4.1.5 + uses: actions/download-artifact@v4.1.6 with: pattern: coverage-* - name: Upload coverage to Codecov diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 6618eb9963b..0dacd45a22e 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -121,12 +121,12 @@ jobs: uses: actions/checkout@v4.1.3 - name: Download env_file - uses: actions/download-artifact@v4.1.5 + uses: actions/download-artifact@v4.1.6 with: name: env_file - name: Download requirements_diff - uses: actions/download-artifact@v4.1.5 + uses: actions/download-artifact@v4.1.6 with: name: requirements_diff @@ -159,17 +159,17 @@ jobs: uses: actions/checkout@v4.1.3 - name: Download env_file - uses: actions/download-artifact@v4.1.5 + uses: actions/download-artifact@v4.1.6 with: name: env_file - name: Download requirements_diff - uses: actions/download-artifact@v4.1.5 + uses: actions/download-artifact@v4.1.6 with: name: requirements_diff - name: Download requirements_all_wheels - uses: actions/download-artifact@v4.1.4 + uses: actions/download-artifact@v4.1.6 with: name: requirements_all_wheels From e2b401397d2940bea87189842785d458d29fcd70 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Apr 2024 09:24:32 +0200 Subject: [PATCH 765/967] Bump actions/upload-artifact from 4.3.1 to 4.3.3 (#116015) --- .github/workflows/builder.yml | 2 +- .github/workflows/ci.yaml | 18 +++++++++--------- .github/workflows/wheels.yml | 6 +++--- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 90c1c3692e9..bc70eafd3f4 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -69,7 +69,7 @@ jobs: run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T - - name: Upload translations - uses: actions/upload-artifact@v4.3.2 + uses: actions/upload-artifact@v4.3.3 with: name: translations path: translations.tar.gz diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 11d0e04cec1..62daa6863d9 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -717,7 +717,7 @@ jobs: . venv/bin/activate python -m script.split_tests ${{ needs.info.outputs.test_group_count }} tests - name: Upload pytest_buckets - uses: actions/upload-artifact@v4.3.2 + uses: actions/upload-artifact@v4.3.3 with: name: pytest_buckets path: pytest_buckets.txt @@ -813,14 +813,14 @@ jobs: 2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt - name: Upload pytest output if: success() || failure() && steps.pytest-full.conclusion == 'failure' - uses: actions/upload-artifact@v4.3.2 + uses: actions/upload-artifact@v4.3.3 with: name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }} path: pytest-*.txt overwrite: true - name: Upload coverage artifact if: needs.info.outputs.skip_coverage != 'true' - uses: actions/upload-artifact@v4.3.2 + uses: actions/upload-artifact@v4.3.3 with: name: coverage-${{ matrix.python-version }}-${{ matrix.group }} path: coverage.xml @@ -935,7 +935,7 @@ jobs: 2>&1 | tee pytest-${{ matrix.python-version }}-${mariadb}.txt - name: Upload pytest output if: success() || failure() && steps.pytest-partial.conclusion == 'failure' - uses: actions/upload-artifact@v4.3.2 + uses: actions/upload-artifact@v4.3.3 with: name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ steps.pytest-partial.outputs.mariadb }} @@ -943,7 +943,7 @@ jobs: overwrite: true - name: Upload coverage artifact if: needs.info.outputs.skip_coverage != 'true' - uses: actions/upload-artifact@v4.3.2 + uses: actions/upload-artifact@v4.3.3 with: name: coverage-${{ matrix.python-version }}-${{ steps.pytest-partial.outputs.mariadb }} @@ -1058,7 +1058,7 @@ jobs: 2>&1 | tee pytest-${{ matrix.python-version }}-${postgresql}.txt - name: Upload pytest output if: success() || failure() && steps.pytest-partial.conclusion == 'failure' - uses: actions/upload-artifact@v4.3.2 + uses: actions/upload-artifact@v4.3.3 with: name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ steps.pytest-partial.outputs.postgresql }} @@ -1066,7 +1066,7 @@ jobs: overwrite: true - name: Upload coverage artifact if: needs.info.outputs.skip_coverage != 'true' - uses: actions/upload-artifact@v4.3.2 + uses: actions/upload-artifact@v4.3.3 with: name: coverage-${{ matrix.python-version }}-${{ steps.pytest-partial.outputs.postgresql }} @@ -1195,14 +1195,14 @@ jobs: 2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt - name: Upload pytest output if: success() || failure() && steps.pytest-partial.conclusion == 'failure' - uses: actions/upload-artifact@v4.3.2 + uses: actions/upload-artifact@v4.3.3 with: name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }} path: pytest-*.txt overwrite: true - name: Upload coverage artifact if: needs.info.outputs.skip_coverage != 'true' - uses: actions/upload-artifact@v4.3.2 + uses: actions/upload-artifact@v4.3.3 with: name: coverage-${{ matrix.python-version }}-${{ matrix.group }} path: coverage.xml diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 0dacd45a22e..2627ac70795 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -82,14 +82,14 @@ jobs: ) > .env_file - name: Upload env_file - uses: actions/upload-artifact@v4.3.2 + uses: actions/upload-artifact@v4.3.3 with: name: env_file path: ./.env_file overwrite: true - name: Upload requirements_diff - uses: actions/upload-artifact@v4.3.2 + uses: actions/upload-artifact@v4.3.3 with: name: requirements_diff path: ./requirements_diff.txt @@ -101,7 +101,7 @@ jobs: python -m script.gen_requirements_all ci - name: Upload requirements_all_wheels - uses: actions/upload-artifact@v4.3.1 + uses: actions/upload-artifact@v4.3.3 with: name: requirements_all_wheels path: ./requirements_all_wheels_*.txt From 8f56d170b9f1b4f7c840dadbb3468d5718651089 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 23 Apr 2024 09:48:17 +0200 Subject: [PATCH 766/967] Use generator expression in totalconnect (#116020) --- .../totalconnect/alarm_control_panel.py | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/totalconnect/alarm_control_panel.py b/homeassistant/components/totalconnect/alarm_control_panel.py index 9b2abedbf52..b0ad2f19069 100644 --- a/homeassistant/components/totalconnect/alarm_control_panel.py +++ b/homeassistant/components/totalconnect/alarm_control_panel.py @@ -38,21 +38,17 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up TotalConnect alarm panels based on a config entry.""" - alarms: list[TotalConnectAlarm] = [] - coordinator: TotalConnectDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - for location in coordinator.client.locations.values(): - alarms.extend( - TotalConnectAlarm( - coordinator, - location, - partition_id, - ) - for partition_id in location.partitions + async_add_entities( + TotalConnectAlarm( + coordinator, + location, + partition_id, ) - - async_add_entities(alarms) + for location in coordinator.client.locations.values() + for partition_id in location.partitions + ) # Set up services platform = entity_platform.async_get_current_platform() From 3d59303433a02a2a2ac47e7e4543a25e7d995601 Mon Sep 17 00:00:00 2001 From: myhomeiot <70070601+myhomeiot@users.noreply.github.com> Date: Tue, 23 Apr 2024 10:50:41 +0300 Subject: [PATCH 767/967] Improve Vodafone Station empty/unavailable phone number detection (#115696) Vodafone Sercomm H300S model incorrectly reports phone_unavailable1/phone_unavailable2 flags. --- homeassistant/components/vodafone_station/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/vodafone_station/sensor.py b/homeassistant/components/vodafone_station/sensor.py index 937c0220cbf..2a08a9b2ebe 100644 --- a/homeassistant/components/vodafone_station/sensor.py +++ b/homeassistant/components/vodafone_station/sensor.py @@ -107,12 +107,12 @@ SENSOR_TYPES: Final = ( VodafoneStationEntityDescription( key="phone_num1", translation_key="phone_num1", - is_suitable=lambda info: info["phone_unavailable1"] == "0", + is_suitable=lambda info: info["phone_num1"] != "", ), VodafoneStationEntityDescription( key="phone_num2", translation_key="phone_num2", - is_suitable=lambda info: info["phone_unavailable2"] == "0", + is_suitable=lambda info: info["phone_num2"] != "", ), VodafoneStationEntityDescription( key="sys_uptime", From e90d76b18de75d80c132cc3338b1ad44ed6f101c Mon Sep 17 00:00:00 2001 From: Martijn van der Pol Date: Tue, 23 Apr 2024 09:55:58 +0200 Subject: [PATCH 768/967] Don't raise errors when using datetime objects in `as_datetime` Jinja function/filter (#109062) * add support for datetime objects to as_datetime * change import of datetime.date --------- Co-authored-by: Erik Montnemery --- homeassistant/helpers/template.py | 14 ++++++++++---- tests/helpers/test_template.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 16379c1d05c..a1ba1279292 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -9,7 +9,7 @@ import collections.abc from collections.abc import Callable, Generator, Iterable from contextlib import AbstractContextManager, suppress from contextvars import ContextVar -from datetime import datetime, timedelta +from datetime import date, datetime, time, timedelta from functools import cache, lru_cache, partial, wraps import json import logging @@ -2001,12 +2001,12 @@ def square_root(value, default=_SENTINEL): def timestamp_custom(value, date_format=DATE_STR_FORMAT, local=True, default=_SENTINEL): """Filter to convert given timestamp to format.""" try: - date = dt_util.utc_from_timestamp(value) + result = dt_util.utc_from_timestamp(value) if local: - date = dt_util.as_local(date) + result = dt_util.as_local(result) - return date.strftime(date_format) + return result.strftime(date_format) except (ValueError, TypeError): # If timestamp can't be converted if default is _SENTINEL: @@ -2048,6 +2048,12 @@ def forgiving_as_timestamp(value, default=_SENTINEL): def as_datetime(value: Any, default: Any = _SENTINEL) -> Any: """Filter and to convert a time string or UNIX timestamp to datetime object.""" + # Return datetime.datetime object without changes + if type(value) is datetime: + return value + # Add midnight to datetime.date object + if type(value) is date: + return datetime.combine(value, time(0, 0, 0)) try: # Check for a valid UNIX timestamp string, int or float timestamp = float(value) diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 524b8f47dfe..ec5b76964f7 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -1198,6 +1198,35 @@ def test_as_datetime_from_timestamp( ) +@pytest.mark.parametrize( + ("input", "output"), + [ + ( + "{% set dt = as_datetime('2024-01-01 16:00:00-08:00') %}", + "2024-01-01 16:00:00-08:00", + ), + ( + "{% set dt = as_datetime('2024-01-29').date() %}", + "2024-01-29 00:00:00", + ), + ], +) +def test_as_datetime_from_datetime( + hass: HomeAssistant, input: str, output: str +) -> None: + """Test using datetime.datetime or datetime.date objects as input.""" + + assert ( + template.Template(f"{input}{{{{ dt | as_datetime }}}}", hass).async_render() + == output + ) + + assert ( + template.Template(f"{input}{{{{ as_datetime(dt) }}}}", hass).async_render() + == output + ) + + @pytest.mark.parametrize( ("input", "default", "output"), [ From 640dc56c51b31ea5a34c8ba7d2eacf2ded19401b Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 23 Apr 2024 10:28:46 +0200 Subject: [PATCH 769/967] Deprecate modbus:restart service (#115754) --- homeassistant/components/modbus/modbus.py | 13 +++++++++++++ homeassistant/components/modbus/strings.json | 4 ++++ 2 files changed, 17 insertions(+) diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index 0d1848e0d8e..bd7eed8235c 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -34,6 +34,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType @@ -234,6 +235,18 @@ async def async_modbus_setup( async def async_restart_hub(service: ServiceCall) -> None: """Restart Modbus hub.""" + async_create_issue( + hass, + DOMAIN, + "deprecated_restart", + breaks_in_ha_version="2024.11.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_restart", + ) + _LOGGER.warning( + "`modbus.restart`: is deprecated and will be removed in version 2024.11" + ) async_dispatcher_send(hass, SIGNAL_START_ENTITY) hub = hub_collect[service.data[ATTR_HUB]] await hub.async_restart() diff --git a/homeassistant/components/modbus/strings.json b/homeassistant/components/modbus/strings.json index 72d7a3ec5f1..f89f9a97d52 100644 --- a/homeassistant/components/modbus/strings.json +++ b/homeassistant/components/modbus/strings.json @@ -97,6 +97,10 @@ "no_entities": { "title": "Modbus {sub_1} contain no entities, entry not loaded.", "description": "Please add at least one entity to Modbus {sub_1} in your configuration.yaml file and restart Home Assistant to fix this issue." + }, + "deprecated_restart": { + "title": "`modbus.restart` is being removed", + "description": "Please use reload yaml via the developer tools in the UI instead of via the `modbus.restart` service." } } } From 9cdf7b435a4ef3f1fa2aa9ffa1c4db6260470f5f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 23 Apr 2024 11:06:29 +0200 Subject: [PATCH 770/967] Add uv version to wheels cache key [ci] (#116021) --- .github/workflows/ci.yaml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 62daa6863d9..5d38b0480b7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -33,7 +33,7 @@ on: type: boolean env: - CACHE_VERSION: 7 + CACHE_VERSION: 8 UV_CACHE_VERSION: 1 MYPY_CACHE_VERSION: 8 HA_SHORT_VERSION: "2024.5" @@ -452,8 +452,10 @@ jobs: check-latest: true - name: Generate partial uv restore key id: generate-uv-key - run: >- - echo "key=uv-${{ env.UV_CACHE_VERSION }}-${{ + run: | + uv_version=$(cat requirements_test.txt | grep uv | cut -d '=' -f 3) + echo "version=${uv_version}" >> $GITHUB_OUTPUT + echo "key=uv-${{ env.UV_CACHE_VERSION }}-${uv_version}-${{ env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT - name: Restore base Python virtual environment id: cache-venv @@ -473,7 +475,9 @@ jobs: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ steps.generate-uv-key.outputs.key }} restore-keys: | - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-uv-${{ env.UV_CACHE_VERSION }}-${{ env.HA_SHORT_VERSION }}- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-uv-${{ + env.UV_CACHE_VERSION }}-${{ steps.generate-uv-key.outputs.version }}-${{ + env.HA_SHORT_VERSION }}- - name: Install additional OS dependencies if: steps.cache-venv.outputs.cache-hit != 'true' run: | From 85203aeb28fa8a31cfb905eec55e32dc505358be Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 23 Apr 2024 21:23:28 +1200 Subject: [PATCH 771/967] Bump aioesphomeapi to 24.3.0 (#116004) --- 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 0e9a2bdc87f..cde44fa3231 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -15,7 +15,7 @@ "iot_class": "local_push", "loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"], "requirements": [ - "aioesphomeapi==24.2.0", + "aioesphomeapi==24.3.0", "esphome-dashboard-api==1.2.3", "bleak-esphome==1.0.0" ], diff --git a/requirements_all.txt b/requirements_all.txt index 058e8102e18..b2ffc771db8 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.2.0 +aioesphomeapi==24.3.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 879f2b9123e..7b45f2d56d4 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.2.0 +aioesphomeapi==24.3.0 # homeassistant.components.flo aioflo==2021.11.0 From 2977ec48720d28ba75120ad462687c368e724bce Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 23 Apr 2024 11:54:19 +0200 Subject: [PATCH 772/967] Add event platform to Lutron (#109121) * Add event platform to Lutron * Add event platform to Lutron * Fix * Fix * Fix * Add deprecation note * Fix * Fix * Update homeassistant/components/lutron/event.py * Update homeassistant/components/lutron/event.py * Fix --- .coveragerc | 1 + homeassistant/components/lutron/__init__.py | 74 +------------ homeassistant/components/lutron/event.py | 109 +++++++++++++++++++ homeassistant/components/lutron/strings.json | 15 +++ 4 files changed, 131 insertions(+), 68 deletions(-) create mode 100644 homeassistant/components/lutron/event.py diff --git a/.coveragerc b/.coveragerc index f6368de7d89..e4fe305a3bf 100644 --- a/.coveragerc +++ b/.coveragerc @@ -741,6 +741,7 @@ omit = homeassistant/components/lutron/binary_sensor.py homeassistant/components/lutron/cover.py homeassistant/components/lutron/entity.py + homeassistant/components/lutron/event.py homeassistant/components/lutron/fan.py homeassistant/components/lutron/light.py homeassistant/components/lutron/switch.py diff --git a/homeassistant/components/lutron/__init__.py b/homeassistant/components/lutron/__init__.py index 517eb4c8350..828182547c2 100644 --- a/homeassistant/components/lutron/__init__.py +++ b/homeassistant/components/lutron/__init__.py @@ -3,31 +3,25 @@ from dataclasses import dataclass import logging -from pylutron import Button, Keypad, Led, Lutron, LutronEvent, OccupancyGroup, Output +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 ( - ATTR_ID, - CONF_HOST, - CONF_PASSWORD, - CONF_USERNAME, - Platform, -) +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.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 homeassistant.util import slugify from .const import DOMAIN PLATFORMS = [ Platform.BINARY_SENSOR, Platform.COVER, + Platform.EVENT, Platform.FAN, Platform.LIGHT, Platform.SCENE, @@ -105,69 +99,13 @@ async def async_setup(hass: HomeAssistant, base_config: ConfigType) -> bool: return True -class LutronButton: - """Representation of a button on a Lutron keypad. - - This is responsible for firing events as keypad buttons are pressed - (and possibly released, depending on the button type). It is not - represented as an entity; it simply fires events. - """ - - def __init__( - self, hass: HomeAssistant, area_name: str, keypad: Keypad, button: Button - ) -> None: - """Register callback for activity on the button.""" - name = f"{keypad.name}: {button.name}" - if button.name == "Unknown Button": - name += f" {button.number}" - self._hass = hass - self._has_release_event = ( - button.button_type is not None and "RaiseLower" in button.button_type - ) - self._id = slugify(name) - self._keypad = keypad - self._area_name = area_name - self._button_name = button.name - self._button = button - self._event = "lutron_event" - self._full_id = slugify(f"{area_name} {name}") - self._uuid = button.uuid - - button.subscribe(self.button_callback, None) - - def button_callback( - self, _button: Button, _context: None, event: LutronEvent, _params: dict - ) -> None: - """Fire an event about a button being pressed or released.""" - # Events per button type: - # RaiseLower -> pressed/released - # SingleAction -> single - action = None - if self._has_release_event: - if event == Button.Event.PRESSED: - action = "pressed" - else: - action = "released" - elif event == Button.Event.PRESSED: - action = "single" - - if action: - data = { - ATTR_ID: self._id, - ATTR_ACTION: action, - ATTR_FULL_ID: self._full_id, - ATTR_UUID: self._uuid, - } - self._hass.bus.fire(self._event, data) - - @dataclass(slots=True, kw_only=True) class LutronData: """Storage class for platform global data.""" client: Lutron binary_sensors: list[tuple[str, OccupancyGroup]] - buttons: list[LutronButton] + buttons: list[tuple[str, Keypad, Button]] covers: list[tuple[str, Output]] fans: list[tuple[str, Output]] lights: list[tuple[str, Output]] @@ -273,8 +211,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b led.legacy_uuid, entry_data.client.guid, ) - - entry_data.buttons.append(LutronButton(hass, area.name, keypad, button)) + if button.button_type: + entry_data.buttons.append((area.name, keypad, button)) if area.occupancy_group is not None: entry_data.binary_sensors.append((area.name, area.occupancy_group)) platform = Platform.BINARY_SENSOR diff --git a/homeassistant/components/lutron/event.py b/homeassistant/components/lutron/event.py new file mode 100644 index 00000000000..710f942a006 --- /dev/null +++ b/homeassistant/components/lutron/event.py @@ -0,0 +1,109 @@ +"""Support for Lutron events.""" + +from enum import StrEnum + +from pylutron import Button, Keypad, Lutron, LutronEvent + +from homeassistant.components.event import EventEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ID +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util import slugify + +from . import ATTR_ACTION, ATTR_FULL_ID, ATTR_UUID, DOMAIN, LutronData +from .entity import LutronKeypad + + +class LutronEventType(StrEnum): + """Lutron event types.""" + + SINGLE_PRESS = "single_press" + PRESS = "press" + RELEASE = "release" + + +LEGACY_EVENT_TYPES: dict[LutronEventType, str] = { + LutronEventType.SINGLE_PRESS: "single", + LutronEventType.PRESS: "pressed", + LutronEventType.RELEASE: "released", +} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Lutron event platform.""" + entry_data: LutronData = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities( + LutronEventEntity(area_name, keypad, button, entry_data.client) + for area_name, keypad, button in entry_data.buttons + ) + + +class LutronEventEntity(LutronKeypad, EventEntity): + """Representation of a Lutron keypad button.""" + + _attr_translation_key = "button" + + def __init__( + self, + area_name: str, + keypad: Keypad, + button: Button, + controller: Lutron, + ) -> None: + """Initialize the button.""" + super().__init__(area_name, button, controller, keypad) + if (name := button.name) == "Unknown Button": + name += f" {button.number}" + self._attr_name = name + self._has_release_event = ( + button.button_type is not None and "RaiseLower" in button.button_type + ) + if self._has_release_event: + self._attr_event_types = [LutronEventType.PRESS, LutronEventType.RELEASE] + else: + self._attr_event_types = [LutronEventType.SINGLE_PRESS] + + self._full_id = slugify(f"{area_name} {name}") + self._id = slugify(name) + + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + await super().async_added_to_hass() + self._lutron_device.subscribe(self.handle_event, None) + + async def async_will_remove_from_hass(self) -> None: + """Unregister callbacks.""" + await super().async_will_remove_from_hass() + # Temporary solution until https://github.com/thecynic/pylutron/pull/93 gets merged + self._lutron_device._subscribers.remove((self.handle_event, None)) # pylint: disable=protected-access + + @callback + def handle_event( + self, button: Button, _context: None, event: LutronEvent, _params: dict + ) -> None: + """Handle received event.""" + action: LutronEventType | None = None + if self._has_release_event: + if event == Button.Event.PRESSED: + action = LutronEventType.PRESS + else: + action = LutronEventType.RELEASE + elif event == Button.Event.PRESSED: + action = LutronEventType.SINGLE_PRESS + + if action: + data = { + ATTR_ID: self._id, + ATTR_ACTION: LEGACY_EVENT_TYPES[action], + ATTR_FULL_ID: self._full_id, + ATTR_UUID: button.uuid, + } + self.hass.bus.fire("lutron_event", data) + self._trigger_event(action) + self.async_write_ha_state() diff --git a/homeassistant/components/lutron/strings.json b/homeassistant/components/lutron/strings.json index efa0a35d81a..0212c8845d5 100644 --- a/homeassistant/components/lutron/strings.json +++ b/homeassistant/components/lutron/strings.json @@ -22,6 +22,21 @@ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" } }, + "entity": { + "event": { + "button": { + "state_attributes": { + "event_type": { + "state": { + "single_press": "Single press", + "press": "Press", + "release": "Release" + } + } + } + } + } + }, "issues": { "deprecated_yaml_import_issue_cannot_connect": { "title": "The Lutron YAML configuration import cannot connect to server", From fd14695d26de06d7d82f7e39c3777e3d3c3b085a Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Tue, 23 Apr 2024 13:16:55 +0200 Subject: [PATCH 773/967] Bump deebot-client to 7.0.0 (#116025) --- homeassistant/components/ecovacs/event.py | 6 ++---- homeassistant/components/ecovacs/manifest.json | 2 +- homeassistant/components/ecovacs/select.py | 10 +++++----- homeassistant/components/ecovacs/util.py | 9 +++++++++ homeassistant/components/ecovacs/vacuum.py | 5 +++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ecovacs/test_event.py | 2 +- 8 files changed, 23 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/ecovacs/event.py b/homeassistant/components/ecovacs/event.py index daac4a626ae..fb4c25c7559 100644 --- a/homeassistant/components/ecovacs/event.py +++ b/homeassistant/components/ecovacs/event.py @@ -13,6 +13,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN from .controller import EcovacsController from .entity import EcovacsEntity +from .util import get_name_key async def async_setup_entry( @@ -54,10 +55,7 @@ class EcovacsLastJobEventEntity( # we trigger only on job done return - event_type = event.status.name.lower() - if event.status == CleanJobStatus.MANUAL_STOPPED: - event_type = "manually_stopped" - + event_type = get_name_key(event.status) self._trigger_event(event_type) self.async_write_ha_state() diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index 52753e6eb39..2e088024215 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.9", "deebot-client==6.0.2"] + "requirements": ["py-sucks==0.9.9", "deebot-client==7.0.0"] } diff --git a/homeassistant/components/ecovacs/select.py b/homeassistant/components/ecovacs/select.py index 8a3def54e28..01d4c5aae6b 100644 --- a/homeassistant/components/ecovacs/select.py +++ b/homeassistant/components/ecovacs/select.py @@ -22,7 +22,7 @@ from .entity import ( EcovacsDescriptionEntity, EventT, ) -from .util import get_supported_entitites +from .util import get_name_key, get_supported_entitites @dataclass(kw_only=True, frozen=True) @@ -41,8 +41,8 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSelectEntityDescription, ...] = ( EcovacsSelectEntityDescription[WaterInfoEvent]( device_capabilities=VacuumCapabilities, capability_fn=lambda caps: caps.water, - current_option_fn=lambda e: e.amount.display_name, - options_fn=lambda water: [amount.display_name for amount in water.types], + current_option_fn=lambda e: get_name_key(e.amount), + options_fn=lambda water: [get_name_key(amount) for amount in water.types], key="water_amount", translation_key="water_amount", entity_category=EntityCategory.CONFIG, @@ -50,8 +50,8 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSelectEntityDescription, ...] = ( EcovacsSelectEntityDescription[WorkModeEvent]( device_capabilities=VacuumCapabilities, capability_fn=lambda caps: caps.clean.work_mode, - current_option_fn=lambda e: e.mode.display_name, - options_fn=lambda cap: [mode.display_name for mode in cap.types], + current_option_fn=lambda e: get_name_key(e.mode), + options_fn=lambda cap: [get_name_key(mode) for mode in cap.types], key="work_mode", translation_key="work_mode", entity_registry_enabled_default=False, diff --git a/homeassistant/components/ecovacs/util.py b/homeassistant/components/ecovacs/util.py index 14e69cd4b61..ab5db58c579 100644 --- a/homeassistant/components/ecovacs/util.py +++ b/homeassistant/components/ecovacs/util.py @@ -2,12 +2,15 @@ from __future__ import annotations +from enum import Enum import random import string from typing import TYPE_CHECKING from deebot_client.capabilities import Capabilities +from homeassistant.core import callback + from .entity import ( EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity, @@ -38,3 +41,9 @@ def get_supported_entitites( if isinstance(device.capabilities, description.device_capabilities) if (capability := description.capability_fn(device.capabilities)) ] + + +@callback +def get_name_key(enum: Enum) -> str: + """Return the lower case name of the enum.""" + return enum.name.lower() diff --git a/homeassistant/components/ecovacs/vacuum.py b/homeassistant/components/ecovacs/vacuum.py index d5016ab683d..0e990645d7c 100644 --- a/homeassistant/components/ecovacs/vacuum.py +++ b/homeassistant/components/ecovacs/vacuum.py @@ -33,6 +33,7 @@ from homeassistant.util import slugify from .const import DOMAIN from .controller import EcovacsController from .entity import EcovacsEntity +from .util import get_name_key _LOGGER = logging.getLogger(__name__) @@ -242,7 +243,7 @@ class EcovacsVacuum( self._rooms: list[Room] = [] self._attr_fan_speed_list = [ - level.display_name for level in capabilities.fan_speed.types + get_name_key(level) for level in capabilities.fan_speed.types ] async def async_added_to_hass(self) -> None: @@ -254,7 +255,7 @@ class EcovacsVacuum( self.async_write_ha_state() async def on_fan_speed(event: FanSpeedEvent) -> None: - self._attr_fan_speed = event.speed.display_name + self._attr_fan_speed = get_name_key(event.speed) self.async_write_ha_state() async def on_rooms(event: RoomsEvent) -> None: diff --git a/requirements_all.txt b/requirements_all.txt index b2ffc771db8..a5d370fce8b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -694,7 +694,7 @@ debugpy==1.8.1 # decora==0.6 # homeassistant.components.ecovacs -deebot-client==6.0.2 +deebot-client==7.0.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7b45f2d56d4..35c005fe4d3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -572,7 +572,7 @@ dbus-fast==2.21.1 debugpy==1.8.1 # homeassistant.components.ecovacs -deebot-client==6.0.2 +deebot-client==7.0.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/tests/components/ecovacs/test_event.py b/tests/components/ecovacs/test_event.py index 0e7adaad954..104a3bfc69e 100644 --- a/tests/components/ecovacs/test_event.py +++ b/tests/components/ecovacs/test_event.py @@ -76,7 +76,7 @@ async def test_last_job( await notify_and_wait( hass, event_bus, - ReportStatsEvent(0, 1, "spotArea", "3", CleanJobStatus.MANUAL_STOPPED, [1]), + ReportStatsEvent(0, 1, "spotArea", "3", CleanJobStatus.MANUALLY_STOPPED, [1]), ) assert (state := hass.states.get(state.entity_id)) From b8918d7d17440178edd8ede02c0bf9e0acb84f8c Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Tue, 23 Apr 2024 14:18:49 +0200 Subject: [PATCH 774/967] Add number platform to Husqvarna Automower (#115125) * Add number platform to Husqvarna Automower * use fixture to enable by default * replace state test with snapshot test * make property in entity description * send value as integer * give the exists functions something to do --- .../husqvarna_automower/__init__.py | 1 + .../components/husqvarna_automower/icons.json | 5 + .../components/husqvarna_automower/number.py | 95 +++++++++++++++++++ .../husqvarna_automower/strings.json | 5 + .../snapshots/test_number.ambr | 56 +++++++++++ .../husqvarna_automower/test_number.py | 77 +++++++++++++++ 6 files changed, 239 insertions(+) create mode 100644 homeassistant/components/husqvarna_automower/number.py create mode 100644 tests/components/husqvarna_automower/snapshots/test_number.ambr create mode 100644 tests/components/husqvarna_automower/test_number.py diff --git a/homeassistant/components/husqvarna_automower/__init__.py b/homeassistant/components/husqvarna_automower/__init__.py index 03ab02429bb..fe6f6978014 100644 --- a/homeassistant/components/husqvarna_automower/__init__.py +++ b/homeassistant/components/husqvarna_automower/__init__.py @@ -21,6 +21,7 @@ PLATFORMS: list[Platform] = [ Platform.BINARY_SENSOR, Platform.DEVICE_TRACKER, Platform.LAWN_MOWER, + Platform.NUMBER, Platform.SELECT, Platform.SENSOR, Platform.SWITCH, diff --git a/homeassistant/components/husqvarna_automower/icons.json b/homeassistant/components/husqvarna_automower/icons.json index ec11ef92d08..2ecbf9c198a 100644 --- a/homeassistant/components/husqvarna_automower/icons.json +++ b/homeassistant/components/husqvarna_automower/icons.json @@ -8,6 +8,11 @@ "default": "mdi:debug-step-into" } }, + "number": { + "cutting_height": { + "default": "mdi:grass" + } + }, "select": { "headlight_mode": { "default": "mdi:car-light-high" diff --git a/homeassistant/components/husqvarna_automower/number.py b/homeassistant/components/husqvarna_automower/number.py new file mode 100644 index 00000000000..8745b93479d --- /dev/null +++ b/homeassistant/components/husqvarna_automower/number.py @@ -0,0 +1,95 @@ +"""Creates the number entities for the mower.""" + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +import logging +from typing import Any + +from aioautomower.exceptions import ApiException +from aioautomower.model import MowerAttributes +from aioautomower.session import AutomowerSession + +from homeassistant.components.number import NumberEntity, NumberEntityDescription +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 .coordinator import AutomowerDataUpdateCoordinator +from .entity import AutomowerBaseEntity + +_LOGGER = logging.getLogger(__name__) + + +@dataclass(frozen=True, kw_only=True) +class AutomowerNumberEntityDescription(NumberEntityDescription): + """Describes Automower number entity.""" + + exists_fn: Callable[[MowerAttributes], bool] = lambda _: True + value_fn: Callable[[MowerAttributes], int] + set_value_fn: Callable[[AutomowerSession, str, float], Awaitable[Any]] + + +NUMBER_TYPES: tuple[AutomowerNumberEntityDescription, ...] = ( + AutomowerNumberEntityDescription( + key="cutting_height", + translation_key="cutting_height", + entity_registry_enabled_default=False, + entity_category=EntityCategory.CONFIG, + native_min_value=1, + native_max_value=9, + exists_fn=lambda data: data.cutting_height is not None, + value_fn=lambda data: data.cutting_height, + set_value_fn=lambda session, mower_id, cheight: session.set_cutting_height( + mower_id, int(cheight) + ), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up number platform.""" + coordinator: AutomowerDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + AutomowerNumberEntity(mower_id, coordinator, description) + for mower_id in coordinator.data + for description in NUMBER_TYPES + if description.exists_fn(coordinator.data[mower_id]) + ) + + +class AutomowerNumberEntity(AutomowerBaseEntity, NumberEntity): + """Defining the AutomowerNumberEntity with AutomowerNumberEntityDescription.""" + + entity_description: AutomowerNumberEntityDescription + + def __init__( + self, + mower_id: str, + coordinator: AutomowerDataUpdateCoordinator, + description: AutomowerNumberEntityDescription, + ) -> None: + """Set up AutomowerNumberEntity.""" + super().__init__(mower_id, coordinator) + self.entity_description = description + self._attr_unique_id = f"{mower_id}_{description.key}" + + @property + def native_value(self) -> float: + """Return the state of the number.""" + return self.entity_description.value_fn(self.mower_attributes) + + async def async_set_native_value(self, value: float) -> None: + """Change to new number value.""" + try: + await self.entity_description.set_value_fn( + self.coordinator.api, self.mower_id, value + ) + except ApiException as exception: + raise HomeAssistantError( + f"Command couldn't be sent to the command queue: {exception}" + ) from exception diff --git a/homeassistant/components/husqvarna_automower/strings.json b/homeassistant/components/husqvarna_automower/strings.json index 0a2d3685c6e..b4c1c97cd68 100644 --- a/homeassistant/components/husqvarna_automower/strings.json +++ b/homeassistant/components/husqvarna_automower/strings.json @@ -37,6 +37,11 @@ "name": "Returning to dock" } }, + "number": { + "cutting_height": { + "name": "Cutting height" + } + }, "select": { "headlight_mode": { "name": "Headlight mode", diff --git a/tests/components/husqvarna_automower/snapshots/test_number.ambr b/tests/components/husqvarna_automower/snapshots/test_number.ambr new file mode 100644 index 00000000000..a5479345bd1 --- /dev/null +++ b/tests/components/husqvarna_automower/snapshots/test_number.ambr @@ -0,0 +1,56 @@ +# serializer version: 1 +# name: test_snapshot_number[number.test_mower_1_cutting_height-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 9, + 'min': 1, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.test_mower_1_cutting_height', + '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': 'Cutting height', + 'platform': 'husqvarna_automower', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cutting_height', + 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_cutting_height', + 'unit_of_measurement': None, + }) +# --- +# name: test_snapshot_number[number.test_mower_1_cutting_height-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Mower 1 Cutting height', + 'max': 9, + 'min': 1, + 'mode': , + 'step': 1.0, + }), + 'context': , + 'entity_id': 'number.test_mower_1_cutting_height', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4', + }) +# --- diff --git a/tests/components/husqvarna_automower/test_number.py b/tests/components/husqvarna_automower/test_number.py new file mode 100644 index 00000000000..abf56df1c0b --- /dev/null +++ b/tests/components/husqvarna_automower/test_number.py @@ -0,0 +1,77 @@ +"""Tests for number platform.""" + +from unittest.mock import AsyncMock, patch + +from aioautomower.exceptions import ApiException +import pytest +from syrupy 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 setup_integration + +from tests.common import MockConfigEntry + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_number_commands( + hass: HomeAssistant, + mock_automower_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test number commands.""" + entity_id = "number.test_mower_1_cutting_height" + await setup_integration(hass, mock_config_entry) + await hass.services.async_call( + domain="number", + service="set_value", + target={"entity_id": entity_id}, + service_data={"value": "3"}, + blocking=True, + ) + mocked_method = mock_automower_client.set_cutting_height + assert len(mocked_method.mock_calls) == 1 + + mocked_method.side_effect = ApiException("Test error") + with pytest.raises(HomeAssistantError) as exc_info: + await hass.services.async_call( + domain="number", + service="set_value", + target={"entity_id": entity_id}, + service_data={"value": "3"}, + blocking=True, + ) + assert ( + str(exc_info.value) + == "Command couldn't be sent to the command queue: Test error" + ) + assert len(mocked_method.mock_calls) == 2 + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_snapshot_number( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_automower_client: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test states of the number entity.""" + with patch( + "homeassistant.components.husqvarna_automower.PLATFORMS", + [Platform.NUMBER], + ): + await setup_integration(hass, mock_config_entry) + entity_entries = er.async_entries_for_config_entry( + entity_registry, mock_config_entry.entry_id + ) + + assert entity_entries + for entity_entry in entity_entries: + assert hass.states.get(entity_entry.entity_id) == snapshot( + name=f"{entity_entry.entity_id}-state" + ) + assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry") From 2c651e190f0387d91e4cd1d8052940c4c4749714 Mon Sep 17 00:00:00 2001 From: Arie Catsman <120491684+catsmanac@users.noreply.github.com> Date: Tue, 23 Apr 2024 14:26:53 +0200 Subject: [PATCH 775/967] Add additional zeroconf discovery coverage and logging to enphase_envoy (#114405) * add debug info to zeroconf for enphase_envoy * Implement review feedback, lost space Co-authored-by: Charles Garwood * review feedback textual changes. * implement review feedbackw.py Co-authored-by: J. Nick Koston * Add some more zeroconf tests and valid jwt * review feedback assert abort reason and keyerror for serialnumber * Review feedback config flow test ends with abort or create_entry * Review feedback optimize resource usage * Cover new code in test. * Use caplog for debug COV --------- Co-authored-by: Charles Garwood Co-authored-by: J. Nick Koston --- .../components/enphase_envoy/config_flow.py | 18 ++ tests/components/enphase_envoy/conftest.py | 6 +- .../enphase_envoy/test_config_flow.py | 239 +++++++++++++++++- 3 files changed, 261 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/enphase_envoy/config_flow.py b/homeassistant/components/enphase_envoy/config_flow.py index 13894d423d6..5f859d16142 100644 --- a/homeassistant/components/enphase_envoy/config_flow.py +++ b/homeassistant/components/enphase_envoy/config_flow.py @@ -89,6 +89,14 @@ class EnphaseConfigFlow(ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> ConfigFlowResult: """Handle a flow initialized by zeroconf discovery.""" + if _LOGGER.isEnabledFor(logging.DEBUG): + current_hosts = self._async_current_hosts() + _LOGGER.debug( + "Zeroconf ip %s processing %s, current hosts: %s", + discovery_info.ip_address.version, + discovery_info.host, + current_hosts, + ) if discovery_info.ip_address.version != 4: return self.async_abort(reason="not_ipv4_address") serial = discovery_info.properties["serialnum"] @@ -96,17 +104,27 @@ class EnphaseConfigFlow(ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(serial) self.ip_address = discovery_info.host self._abort_if_unique_id_configured({CONF_HOST: self.ip_address}) + _LOGGER.debug( + "Zeroconf ip %s, fw %s, no existing entry with serial %s", + self.ip_address, + self.protovers, + serial, + ) for entry in self._async_current_entries(include_ignore=False): if ( entry.unique_id is None and CONF_HOST in entry.data and entry.data[CONF_HOST] == self.ip_address ): + _LOGGER.debug( + "Zeroconf update envoy with this ip and blank serial in unique_id", + ) title = f"{ENVOY} {serial}" if entry.title == ENVOY else ENVOY return self.async_update_reload_and_abort( entry, title=title, unique_id=serial, reason="already_configured" ) + _LOGGER.debug("Zeroconf ip %s to step user", self.ip_address) return await self.async_step_user() async def async_step_reauth( diff --git a/tests/components/enphase_envoy/conftest.py b/tests/components/enphase_envoy/conftest.py index 40d409aea8e..4d50f026c55 100644 --- a/tests/components/enphase_envoy/conftest.py +++ b/tests/components/enphase_envoy/conftest.py @@ -2,6 +2,7 @@ from unittest.mock import AsyncMock, Mock, patch +import jwt from pyenphase import ( Envoy, EnvoyData, @@ -368,7 +369,10 @@ def mock_authenticate(): @pytest.fixture(name="mock_auth") def mock_auth(serial_number): """Define a mocked EnvoyAuth fixture.""" - return EnvoyTokenAuth("127.0.0.1", token="abc", envoy_serial=serial_number) + token = jwt.encode( + payload={"name": "envoy", "exp": 1907837780}, key="secret", algorithm="HS256" + ) + return EnvoyTokenAuth("127.0.0.1", token=token, envoy_serial=serial_number) @pytest.fixture(name="mock_setup") diff --git a/tests/components/enphase_envoy/test_config_flow.py b/tests/components/enphase_envoy/test_config_flow.py index 7af0cd584a4..2709087a543 100644 --- a/tests/components/enphase_envoy/test_config_flow.py +++ b/tests/components/enphase_envoy/test_config_flow.py @@ -1,6 +1,7 @@ """Test the Enphase Envoy config flow.""" from ipaddress import ip_address +import logging from unittest.mock import AsyncMock from pyenphase import EnvoyAuthenticationError, EnvoyError @@ -13,6 +14,10 @@ from homeassistant.components.enphase_envoy.const import DOMAIN, PLATFORMS from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from tests.common import MockConfigEntry + +_LOGGER = logging.getLogger(__name__) + async def test_form(hass: HomeAssistant, config, setup_enphase_envoy) -> None: """Test we get the form.""" @@ -324,9 +329,13 @@ async def test_form_host_already_exists( async def test_zeroconf_serial_already_exists( - hass: HomeAssistant, config_entry, setup_enphase_envoy + hass: HomeAssistant, + config_entry, + setup_enphase_envoy, + caplog: pytest.LogCaptureFixture, ) -> None: """Test serial number already exists from zeroconf.""" + _LOGGER.setLevel(logging.DEBUG) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, @@ -345,6 +354,7 @@ async def test_zeroconf_serial_already_exists( assert result["reason"] == "already_configured" assert config_entry.data["host"] == "4.4.4.4" + assert "Zeroconf ip 4 processing 4.4.4.4, current hosts: {'1.1.1.1'}" in caplog.text async def test_zeroconf_serial_already_exists_ignores_ipv6( @@ -397,6 +407,233 @@ async def test_zeroconf_host_already_exists( assert config_entry.title == "Envoy 1234" +async def test_zero_conf_while_form( + hass: HomeAssistant, config_entry, setup_enphase_envoy +) -> None: + """Test zeroconf while form is active.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + ip_address=ip_address("1.1.1.1"), + ip_addresses=[ip_address("1.1.1.1")], + hostname="mock_hostname", + name="mock_name", + port=None, + properties={"serialnum": "1234", "protovers": "7.0.1"}, + type="mock_type", + ), + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert config_entry.data["host"] == "1.1.1.1" + assert config_entry.unique_id == "1234" + assert config_entry.title == "Envoy 1234" + + +async def test_zero_conf_second_envoy_while_form( + hass: HomeAssistant, config_entry, setup_enphase_envoy +) -> None: + """Test zeroconf while form is active.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + ip_address=ip_address("4.4.4.4"), + ip_addresses=[ip_address("4.4.4.4")], + hostname="mock_hostname", + name="mock_name", + port=None, + properties={"serialnum": "4321", "protovers": "7.0.1"}, + type="mock_type", + ), + ) + await hass.async_block_till_done() + assert result2["type"] is FlowResultType.FORM + assert config_entry.data["host"] == "1.1.1.1" + assert config_entry.unique_id == "1234" + assert config_entry.title == "Envoy 1234" + + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + "host": "4.4.4.4", + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + assert result3["type"] is FlowResultType.CREATE_ENTRY + assert result3["title"] == "Envoy 4321" + assert result3["result"].unique_id == "4321" + + result4 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + assert result4["type"] is FlowResultType.ABORT + + +async def test_zero_conf_malformed_serial_property( + hass: HomeAssistant, config_entry, setup_enphase_envoy +) -> None: + """Test malformed zeroconf properties.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + + with pytest.raises(KeyError) as ex: + await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + ip_address=ip_address("1.1.1.1"), + ip_addresses=[ip_address("1.1.1.1")], + hostname="mock_hostname", + name="mock_name", + port=None, + properties={"serilnum": "1234", "protovers": "7.1.2"}, + type="mock_type", + ), + ) + await hass.async_block_till_done() + assert "serialnum" in str(ex.value) + + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + assert result3["type"] is FlowResultType.ABORT + + +async def test_zero_conf_malformed_serial( + hass: HomeAssistant, config_entry, setup_enphase_envoy +) -> None: + """Test malformed zeroconf properties.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + ip_address=ip_address("1.1.1.1"), + ip_addresses=[ip_address("1.1.1.1")], + hostname="mock_hostname", + name="mock_name", + port=None, + properties={"serialnum": "12%4", "protovers": "7.1.2"}, + type="mock_type", + ), + ) + await hass.async_block_till_done() + assert result2["type"] is FlowResultType.FORM + + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + assert result3["type"] is FlowResultType.CREATE_ENTRY + assert result3["title"] == "Envoy 12%4" + + +async def test_zero_conf_malformed_fw_property( + hass: HomeAssistant, config_entry, setup_enphase_envoy +) -> None: + """Test malformed zeroconf property.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + ip_address=ip_address("1.1.1.1"), + ip_addresses=[ip_address("1.1.1.1")], + hostname="mock_hostname", + name="mock_name", + port=None, + properties={"serialnum": "1234", "protvers": "7.1.2"}, + type="mock_type", + ), + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert config_entry.data["host"] == "1.1.1.1" + assert config_entry.unique_id == "1234" + assert config_entry.title == "Envoy 1234" + + +async def test_zero_conf_old_blank_entry( + hass: HomeAssistant, setup_enphase_envoy +) -> None: + """Test re-using old blank entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + "host": "1.1.1.1", + "username": "", + "password": "", + "name": "unknown", + }, + unique_id=None, + title="Envoy", + ) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + ip_address=ip_address("1.1.1.1"), + ip_addresses=[ip_address("1.1.1.1"), ip_address("1.1.1.2")], + hostname="mock_hostname", + name="mock_name", + port=None, + properties={"serialnum": "1234", "protovers": "7.1.2"}, + type="mock_type", + ), + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + assert entry.data["host"] == "1.1.1.1" + assert entry.unique_id == "1234" + assert entry.title == "Envoy 1234" + + async def test_reauth(hass: HomeAssistant, config_entry, setup_enphase_envoy) -> None: """Test we reauth auth.""" result = await hass.config_entries.flow.async_init( From fced9eb4b5b713ab9425ca83d9d20d45252b42ce Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Tue, 23 Apr 2024 14:33:05 +0200 Subject: [PATCH 776/967] Use location name on self hosted Ecovacs config entries (#115294) --- homeassistant/components/ecovacs/config_flow.py | 2 +- homeassistant/components/ecovacs/controller.py | 5 +++-- homeassistant/components/ecovacs/util.py | 8 ++++++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/ecovacs/config_flow.py b/homeassistant/components/ecovacs/config_flow.py index a1ea19144b0..4a421113f5f 100644 --- a/homeassistant/components/ecovacs/config_flow.py +++ b/homeassistant/components/ecovacs/config_flow.py @@ -71,7 +71,7 @@ async def _validate_input( if errors: return errors - device_id = get_client_device_id() + device_id = get_client_device_id(hass, rest_url is not None) country = user_input[CONF_COUNTRY] rest_config = create_rest_config( aiohttp_client.async_get_clientsession(hass), diff --git a/homeassistant/components/ecovacs/controller.py b/homeassistant/components/ecovacs/controller.py index 5defcdf861f..6b6fe3128dd 100644 --- a/homeassistant/components/ecovacs/controller.py +++ b/homeassistant/components/ecovacs/controller.py @@ -43,7 +43,8 @@ class EcovacsController: self._hass = hass self._devices: list[Device] = [] self.legacy_devices: list[VacBot] = [] - self._device_id = get_client_device_id() + rest_url = config.get(CONF_OVERRIDE_REST_URL) + self._device_id = get_client_device_id(hass, rest_url is not None) country = config[CONF_COUNTRY] self._continent = get_continent(country) @@ -52,7 +53,7 @@ class EcovacsController: aiohttp_client.async_get_clientsession(self._hass), device_id=self._device_id, alpha_2_country=country, - override_rest_url=config.get(CONF_OVERRIDE_REST_URL), + override_rest_url=rest_url, ), config[CONF_USERNAME], md5(config[CONF_PASSWORD]), diff --git a/homeassistant/components/ecovacs/util.py b/homeassistant/components/ecovacs/util.py index ab5db58c579..9d692bbbb8f 100644 --- a/homeassistant/components/ecovacs/util.py +++ b/homeassistant/components/ecovacs/util.py @@ -9,7 +9,8 @@ from typing import TYPE_CHECKING from deebot_client.capabilities import Capabilities -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback +from homeassistant.util import slugify from .entity import ( EcovacsCapabilityEntityDescription, @@ -21,8 +22,11 @@ if TYPE_CHECKING: from .controller import EcovacsController -def get_client_device_id() -> str: +def get_client_device_id(hass: HomeAssistant, self_hosted: bool) -> str: """Get client device id.""" + if self_hosted: + return f"HA-{slugify(hass.config.location_name)}" + return "".join( random.choice(string.ascii_uppercase + string.digits) for _ in range(8) ) From d367bc63f087d06c1994bbd57ae1bbd9862feb57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A5le=20Stor=C3=B8=20Hauknes?= Date: Tue, 23 Apr 2024 15:53:31 +0200 Subject: [PATCH 777/967] Fix KeyError error when fetching sensors (Airthings) (#115844) --- homeassistant/components/airthings/sensor.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/airthings/sensor.py b/homeassistant/components/airthings/sensor.py index fc91d816aca..f0a3dc5be8f 100644 --- a/homeassistant/components/airthings/sensor.py +++ b/homeassistant/components/airthings/sensor.py @@ -157,3 +157,11 @@ class AirthingsHeaterEnergySensor( def native_value(self) -> StateType: """Return the value reported by the sensor.""" return self.coordinator.data[self._id].sensors[self.entity_description.key] # type: ignore[no-any-return] + + @property + def available(self) -> bool: + """Check if device and sensor is available in data.""" + return ( + super().available + and self.entity_description.key in self.coordinator.data[self._id].sensors + ) From a0314cddd4794793e4e7c81fe30fc8bd982526e8 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 23 Apr 2024 16:02:16 +0200 Subject: [PATCH 778/967] Fix invalid tuple annotations (#116035) --- homeassistant/components/imap/coordinator.py | 8 ++++---- tests/components/group/test_init.py | 6 +++--- tests/components/mqtt/test_common.py | 2 +- tests/components/mqtt/test_light_json.py | 4 ++-- tests/test_exceptions.py | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/imap/coordinator.py b/homeassistant/components/imap/coordinator.py index 53d24044b53..c0123b89ee4 100644 --- a/homeassistant/components/imap/coordinator.py +++ b/homeassistant/components/imap/coordinator.py @@ -125,13 +125,13 @@ class ImapMessage: return str(part.get_payload()) @property - def headers(self) -> dict[str, tuple[str,]]: + def headers(self) -> dict[str, tuple[str, ...]]: """Get the email headers.""" - header_base: dict[str, tuple[str,]] = {} + header_base: dict[str, tuple[str, ...]] = {} for key, value in self.email_message.items(): - header_instances: tuple[str,] = (str(value),) + header_instances: tuple[str, ...] = (str(value),) if header_base.setdefault(key, header_instances) != header_instances: - header_base[key] += header_instances # type: ignore[assignment] + header_base[key] += header_instances return header_base @property diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py index 9c2f14f5d74..d3f2747933e 100644 --- a/tests/components/group/test_init.py +++ b/tests/components/group/test_init.py @@ -663,9 +663,9 @@ async def test_is_on(hass: HomeAssistant) -> None: ) async def test_is_on_and_state_mixed_domains( hass: HomeAssistant, - domains: tuple[str,], - states_old: tuple[str,], - states_new: tuple[str,], + domains: tuple[str, ...], + states_old: tuple[str, ...], + states_new: tuple[str, ...], state_ison_group_old: tuple[str, bool], state_ison_group_new: tuple[str, bool], ) -> None: diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index 9dc52871529..e9c3b57777f 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -83,7 +83,7 @@ def help_all_subscribe_calls(mqtt_client_mock: MqttMockPahoClient) -> list[Any]: def help_custom_config( mqtt_entity_domain: str, mqtt_base_config: ConfigType, - mqtt_entity_configs: Iterable[ConfigType,], + mqtt_entity_configs: Iterable[ConfigType], ) -> ConfigType: """Tweak a default config for parametrization. diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index ff1b308ef70..739240a352c 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -236,7 +236,7 @@ async def test_warning_if_color_mode_flags_are_used( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture, - color_modes: tuple[str,], + color_modes: tuple[str, ...], ) -> None: """Test warnings deprecated config keys without supported color modes defined.""" with patch( @@ -278,7 +278,7 @@ async def test_warning_on_discovery_if_color_mode_flags_are_used( mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture, config: dict[str, Any], - color_modes: tuple[str,], + color_modes: tuple[str, ...], ) -> None: """Test warnings deprecated config keys with discovery.""" with patch( diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 5e113d3ba10..9d556b55b15 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -102,7 +102,7 @@ def test_template_message(arg: str | Exception, expected: str) -> None: ) async def test_home_assistant_error( hass: HomeAssistant, - exception_args: tuple[Any,], + exception_args: tuple[Any, ...], exception_kwargs: dict[str, Any], args_base_class: tuple[Any], message: str, From 1649957e5cd0b46b285cec49682645f32c516912 Mon Sep 17 00:00:00 2001 From: Spacetech Date: Tue, 23 Apr 2024 07:39:11 -0700 Subject: [PATCH 779/967] Expose dynamic range status in Onkyo media player (#109099) Expose HDR status in Onkyo media player Co-authored-by: Erik Montnemery --- homeassistant/components/onkyo/media_player.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py index c0503e6e850..7575443c793 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -442,6 +442,7 @@ class OnkyoDevice(MediaPlayerEntity): "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: From 90efe5ac9082516d7b3214c4a05db7b06cc15a96 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Tue, 23 Apr 2024 16:44:37 +0200 Subject: [PATCH 780/967] Velbus Cover: Assume state for VMBxBL modules (#109213) --- homeassistant/components/velbus/cover.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/velbus/cover.py b/homeassistant/components/velbus/cover.py index f37de104659..823d682d339 100644 --- a/homeassistant/components/velbus/cover.py +++ b/homeassistant/components/velbus/cover.py @@ -34,6 +34,7 @@ class VelbusCover(VelbusEntity, CoverEntity): """Representation a Velbus cover.""" _channel: VelbusBlind + _assumed_closed: bool def __init__(self, channel: VelbusBlind) -> None: """Initialize the cover.""" @@ -51,11 +52,16 @@ class VelbusCover(VelbusEntity, CoverEntity): | CoverEntityFeature.CLOSE | CoverEntityFeature.STOP ) + self._attr_assumed_state = True + # guess the state to get the open/closed icons somewhat working + self._assumed_closed = False @property def is_closed(self) -> bool | None: """Return if the cover is closed.""" - return self._channel.is_closed() + if self._channel.support_position(): + return self._channel.is_closed() + return self._assumed_closed @property def is_opening(self) -> bool: @@ -83,11 +89,13 @@ class VelbusCover(VelbusEntity, CoverEntity): async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" await self._channel.open() + self._assumed_closed = False @api_call async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" await self._channel.close() + self._assumed_closed = True @api_call async def async_stop_cover(self, **kwargs: Any) -> None: From 5e250d8a76a88f1fcc91fa4a12b926f3c1ce0f43 Mon Sep 17 00:00:00 2001 From: Volker Stolz Date: Tue, 23 Apr 2024 17:13:25 +0200 Subject: [PATCH 781/967] Augment SyntaxError raised during dependency collection with offending filename (#109204) * Capture parsing exception when collecting dependencies and augment with offending filename. Whereas before any syntax error in some component-file would result in an opaque SyntaxError without pointing out the file, now the result will show as: ``` File "/usr/local/Cellar/python@3.11/3.11.7_1/Frameworks/Python.framework/Versions/3.11/lib/python3.11/multiprocessing/pool.py", line 873, in next raise value SyntaxError: Can't parse file homeassistant/components/your/file.py ``` * tweak * D'oh, had pre-commit hook still off. --------- Co-authored-by: Erik Montnemery --- script/hassfest/dependencies.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/script/hassfest/dependencies.py b/script/hassfest/dependencies.py index 6fe7700cb3f..1547bc1e829 100644 --- a/script/hassfest/dependencies.py +++ b/script/hassfest/dependencies.py @@ -32,7 +32,11 @@ class ImportCollector(ast.NodeVisitor): self._cur_fil_dir = fil.relative_to(self.integration.path) self.referenced[self._cur_fil_dir] = set() - self.visit(ast.parse(fil.read_text())) + try: + self.visit(ast.parse(fil.read_text())) + except SyntaxError as e: + e.add_note(f"File: {fil}") + raise self._cur_fil_dir = None def _add_reference(self, reference_domain: str) -> None: From 14e19c6d9cd6388af8b60575ff67aa27fe6d3972 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 23 Apr 2024 17:32:21 +0200 Subject: [PATCH 782/967] Remove unnecessary type ignores (#116036) --- homeassistant/components/alexa/intent.py | 8 +++++--- homeassistant/components/automation/logbook.py | 12 ++++++++++-- homeassistant/components/evohome/__init__.py | 8 ++++---- homeassistant/components/feedreader/__init__.py | 2 +- homeassistant/components/geniushub/water_heater.py | 4 ++-- homeassistant/components/group/notify.py | 4 ++-- homeassistant/components/input_text/__init__.py | 2 +- 7 files changed, 25 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/alexa/intent.py b/homeassistant/components/alexa/intent.py index fdf72ccce28..217d5dccc25 100644 --- a/homeassistant/components/alexa/intent.py +++ b/homeassistant/components/alexa/intent.py @@ -1,5 +1,6 @@ """Support for Alexa skill service end point.""" +from collections.abc import Callable, Coroutine import enum import logging from typing import Any @@ -16,7 +17,9 @@ from .const import DOMAIN, SYN_RESOLUTION_MATCH _LOGGER = logging.getLogger(__name__) -HANDLERS = Registry() # type: ignore[var-annotated] +HANDLERS: Registry[ + str, Callable[[HomeAssistant, dict[str, Any]], Coroutine[Any, Any, dict[str, Any]]] +] = Registry() INTENTS_API_ENDPOINT = "/api/alexa" @@ -129,8 +132,7 @@ async def async_handle_message( if not (handler := HANDLERS.get(req_type)): raise UnknownRequest(f"Received unknown request {req_type}") - response: dict[str, Any] = await handler(hass, message) - return response + return await handler(hass, message) @HANDLERS.register("SessionEndedRequest") diff --git a/homeassistant/components/automation/logbook.py b/homeassistant/components/automation/logbook.py index 7b9c8cf5809..33ed586f901 100644 --- a/homeassistant/components/automation/logbook.py +++ b/homeassistant/components/automation/logbook.py @@ -1,5 +1,8 @@ """Describe logbook events.""" +from collections.abc import Callable +from typing import Any + from homeassistant.components.logbook import ( LOGBOOK_ENTRY_CONTEXT_ID, LOGBOOK_ENTRY_ENTITY_ID, @@ -16,11 +19,16 @@ from .const import DOMAIN @callback -def async_describe_events(hass: HomeAssistant, async_describe_event): # type: ignore[no-untyped-def] +def async_describe_events( + hass: HomeAssistant, + async_describe_event: Callable[ + [str, str, Callable[[LazyEventPartialState], dict[str, Any]]], None + ], +) -> None: """Describe logbook events.""" @callback - def async_describe_logbook_event(event: LazyEventPartialState): # type: ignore[no-untyped-def] + def async_describe_logbook_event(event: LazyEventPartialState) -> dict[str, Any]: """Describe a logbook event.""" data = event.data message = "triggered" diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 49920d79ff3..4564e863e42 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -33,7 +33,7 @@ from evohomeasync2.schema.const import ( SZ_TIMING_MODE, SZ_UNTIL, ) -import voluptuous as vol # type: ignore[import-untyped] +import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, @@ -462,7 +462,7 @@ class EvoBroker: self.client.access_token_expires # type: ignore[arg-type] ) - app_storage = { + app_storage: dict[str, Any] = { CONF_USERNAME: self.client.username, REFRESH_TOKEN: self.client.refresh_token, ACCESS_TOKEN: self.client.access_token, @@ -470,11 +470,11 @@ class EvoBroker: } if self.client_v1: - app_storage[USER_DATA] = { # type: ignore[assignment] + 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] = {} # type: ignore[assignment] + app_storage[USER_DATA] = {} await self._store.async_save(app_storage) diff --git a/homeassistant/components/feedreader/__init__.py b/homeassistant/components/feedreader/__init__.py index 0a16e986d0b..2b0c6b77559 100644 --- a/homeassistant/components/feedreader/__init__.py +++ b/homeassistant/components/feedreader/__init__.py @@ -117,7 +117,7 @@ class FeedManager: def _update(self) -> struct_time | None: """Update the feed and publish new entries to the event bus.""" _LOGGER.debug("Fetching new data from feed %s", self._url) - self._feed: feedparser.FeedParserDict = feedparser.parse( # type: ignore[no-redef] + self._feed = feedparser.parse( self._url, etag=None if not self._feed else self._feed.get("etag"), modified=None if not self._feed else self._feed.get("modified"), diff --git a/homeassistant/components/geniushub/water_heater.py b/homeassistant/components/geniushub/water_heater.py index 6c3b5223ef9..f17560ebc62 100644 --- a/homeassistant/components/geniushub/water_heater.py +++ b/homeassistant/components/geniushub/water_heater.py @@ -75,9 +75,9 @@ class GeniusWaterHeater(GeniusHeatingZone, WaterHeaterEntity): return list(HA_OPMODE_TO_GH) @property - def current_operation(self) -> str: + def current_operation(self) -> str | None: """Return the current operation mode.""" - return GH_STATE_TO_HA[self._zone.data["mode"]] # type: ignore[return-value] + return GH_STATE_TO_HA[self._zone.data["mode"]] async def async_set_operation_mode(self, operation_mode: str) -> None: """Set a new operation mode for this boiler.""" diff --git a/homeassistant/components/group/notify.py b/homeassistant/components/group/notify.py index bad3d7944d3..425dcf5a914 100644 --- a/homeassistant/components/group/notify.py +++ b/homeassistant/components/group/notify.py @@ -34,12 +34,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def add_defaults( - input_data: dict[str, Any], default_data: dict[str, Any] + input_data: dict[str, Any], default_data: Mapping[str, Any] ) -> dict[str, Any]: """Deep update a dictionary with default values.""" for key, val in default_data.items(): if isinstance(val, Mapping): - input_data[key] = add_defaults(input_data.get(key, {}), val) # type: ignore[arg-type] + input_data[key] = add_defaults(input_data.get(key, {}), val) elif key not in input_data: input_data[key] = val return input_data diff --git a/homeassistant/components/input_text/__init__.py b/homeassistant/components/input_text/__init__.py index 52788066ba2..55b43ee8a1e 100644 --- a/homeassistant/components/input_text/__init__.py +++ b/homeassistant/components/input_text/__init__.py @@ -264,7 +264,7 @@ class InputText(collection.CollectionEntity, RestoreEntity): return state = await self.async_get_last_state() - value: str | None = state and state.state # type: ignore[assignment] + value = state.state if state else None # Check against None because value can be 0 if value is not None and self._minimum <= len(value) <= self._maximum: From 8257af1b22de2910dc769ae7b6dac66771a0cc57 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 23 Apr 2024 17:33:36 +0200 Subject: [PATCH 783/967] Improve energy typing (#116034) --- homeassistant/components/energy/sensor.py | 14 ++++++-------- homeassistant/components/energy/websocket_api.py | 11 +++++------ 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/energy/sensor.py b/homeassistant/components/energy/sensor.py index 37930e31af0..147d8f3e26a 100644 --- a/homeassistant/components/energy/sensor.py +++ b/homeassistant/components/energy/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio -from collections.abc import Callable +from collections.abc import Callable, Mapping import copy from dataclasses import dataclass import logging @@ -167,8 +167,7 @@ class SensorManager: if adapter.flow_type is None: self._process_sensor_data( adapter, - # Opting out of the type complexity because can't get it to work - energy_source, # type: ignore[arg-type] + energy_source, to_add, to_remove, ) @@ -177,8 +176,7 @@ class SensorManager: for flow in energy_source[adapter.flow_type]: # type: ignore[typeddict-item] self._process_sensor_data( adapter, - # Opting out of the type complexity because can't get it to work - flow, # type: ignore[arg-type] + flow, to_add, to_remove, ) @@ -189,7 +187,7 @@ class SensorManager: def _process_sensor_data( self, adapter: SourceAdapter, - config: dict, + config: Mapping[str, Any], to_add: list[EnergyCostSensor], to_remove: dict[tuple[str, str | None, str], EnergyCostSensor], ) -> None: @@ -241,7 +239,7 @@ class EnergyCostSensor(SensorEntity): def __init__( self, adapter: SourceAdapter, - config: dict, + config: Mapping[str, Any], ) -> None: """Initialize the sensor.""" super().__init__() @@ -456,7 +454,7 @@ class EnergyCostSensor(SensorEntity): await super().async_will_remove_from_hass() @callback - def update_config(self, config: dict) -> None: + def update_config(self, config: Mapping[str, Any]) -> None: """Update the config.""" self._config = config diff --git a/homeassistant/components/energy/websocket_api.py b/homeassistant/components/energy/websocket_api.py index 2dd45a8be4d..2b5b71d3e2f 100644 --- a/homeassistant/components/energy/websocket_api.py +++ b/homeassistant/components/energy/websocket_api.py @@ -31,7 +31,7 @@ from .data import ( EnergyPreferencesUpdate, async_get_manager, ) -from .types import EnergyPlatform, GetSolarForecastType +from .types import EnergyPlatform, GetSolarForecastType, SolarForecastType from .validate import async_validate EnergyWebSocketCommandHandler = Callable[ @@ -203,19 +203,18 @@ async def ws_solar_forecast( for source in manager.data["energy_sources"]: if ( source["type"] != "solar" - or source.get("config_entry_solar_forecast") is None + or (solar_forecast := source.get("config_entry_solar_forecast")) is None ): continue - # typing is not catching the above guard for config_entry_solar_forecast being none - for config_entry in source["config_entry_solar_forecast"]: # type: ignore[union-attr] - config_entries[config_entry] = None + for entry in solar_forecast: + config_entries[entry] = None if not config_entries: connection.send_result(msg["id"], {}) return - forecasts = {} + forecasts: dict[str, SolarForecastType] = {} forecast_platforms = await async_get_energy_platforms(hass) From d4b801af3251ad0315a9967339ca0ff21eb8f320 Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Tue, 23 Apr 2024 17:39:29 +0200 Subject: [PATCH 784/967] Use snapshot test helper in Husqvarna Automower (#116039) --- .../husqvarna_automower/test_binary_sensor.py | 12 +++--------- .../husqvarna_automower/test_device_tracker.py | 13 +++---------- tests/components/husqvarna_automower/test_number.py | 13 +++---------- tests/components/husqvarna_automower/test_sensor.py | 12 +++--------- tests/components/husqvarna_automower/test_switch.py | 12 +++--------- 5 files changed, 15 insertions(+), 47 deletions(-) diff --git a/tests/components/husqvarna_automower/test_binary_sensor.py b/tests/components/husqvarna_automower/test_binary_sensor.py index 144dc734025..5500b547853 100644 --- a/tests/components/husqvarna_automower/test_binary_sensor.py +++ b/tests/components/husqvarna_automower/test_binary_sensor.py @@ -20,6 +20,7 @@ from tests.common import ( MockConfigEntry, async_fire_time_changed, load_json_value_fixture, + snapshot_platform, ) @@ -71,13 +72,6 @@ async def test_snapshot_binary_sensor( [Platform.BINARY_SENSOR], ): await setup_integration(hass, mock_config_entry) - entity_entries = er.async_entries_for_config_entry( - entity_registry, mock_config_entry.entry_id + await snapshot_platform( + hass, entity_registry, snapshot, mock_config_entry.entry_id ) - - assert entity_entries - for entity_entry in entity_entries: - assert hass.states.get(entity_entry.entity_id) == snapshot( - name=f"{entity_entry.entity_id}-state" - ) - assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry") diff --git a/tests/components/husqvarna_automower/test_device_tracker.py b/tests/components/husqvarna_automower/test_device_tracker.py index d9cab0d5074..015be201ccc 100644 --- a/tests/components/husqvarna_automower/test_device_tracker.py +++ b/tests/components/husqvarna_automower/test_device_tracker.py @@ -10,7 +10,7 @@ from homeassistant.helpers import entity_registry as er from . import setup_integration -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, snapshot_platform async def test_device_tracker_snapshot( @@ -26,13 +26,6 @@ async def test_device_tracker_snapshot( [Platform.DEVICE_TRACKER], ): await setup_integration(hass, mock_config_entry) - entity_entries = er.async_entries_for_config_entry( - entity_registry, mock_config_entry.entry_id + await snapshot_platform( + hass, entity_registry, snapshot, mock_config_entry.entry_id ) - - assert entity_entries - for entity_entry in entity_entries: - assert hass.states.get(entity_entry.entity_id) == snapshot( - name=f"{entity_entry.entity_id}-state" - ) - assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry") diff --git a/tests/components/husqvarna_automower/test_number.py b/tests/components/husqvarna_automower/test_number.py index abf56df1c0b..b66f1965151 100644 --- a/tests/components/husqvarna_automower/test_number.py +++ b/tests/components/husqvarna_automower/test_number.py @@ -13,7 +13,7 @@ from homeassistant.helpers import entity_registry as er from . import setup_integration -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, snapshot_platform @pytest.mark.usefixtures("entity_registry_enabled_by_default") @@ -65,13 +65,6 @@ async def test_snapshot_number( [Platform.NUMBER], ): await setup_integration(hass, mock_config_entry) - entity_entries = er.async_entries_for_config_entry( - entity_registry, mock_config_entry.entry_id + await snapshot_platform( + hass, entity_registry, snapshot, mock_config_entry.entry_id ) - - assert entity_entries - for entity_entry in entity_entries: - assert hass.states.get(entity_entry.entity_id) == snapshot( - name=f"{entity_entry.entity_id}-state" - ) - assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry") diff --git a/tests/components/husqvarna_automower/test_sensor.py b/tests/components/husqvarna_automower/test_sensor.py index 5d304330aca..f54ce9c6275 100644 --- a/tests/components/husqvarna_automower/test_sensor.py +++ b/tests/components/husqvarna_automower/test_sensor.py @@ -21,6 +21,7 @@ from tests.common import ( MockConfigEntry, async_fire_time_changed, load_json_value_fixture, + snapshot_platform, ) @@ -132,13 +133,6 @@ async def test_sensor( [Platform.SENSOR], ): await setup_integration(hass, mock_config_entry) - entity_entries = er.async_entries_for_config_entry( - entity_registry, mock_config_entry.entry_id + await snapshot_platform( + hass, entity_registry, snapshot, mock_config_entry.entry_id ) - - assert entity_entries - for entity_entry in entity_entries: - assert hass.states.get(entity_entry.entity_id) == snapshot( - name=f"{entity_entry.entity_id}-state" - ) - assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry") diff --git a/tests/components/husqvarna_automower/test_switch.py b/tests/components/husqvarna_automower/test_switch.py index 8dbb5450db1..aab1128a746 100644 --- a/tests/components/husqvarna_automower/test_switch.py +++ b/tests/components/husqvarna_automower/test_switch.py @@ -23,6 +23,7 @@ from tests.common import ( MockConfigEntry, async_fire_time_changed, load_json_value_fixture, + snapshot_platform, ) @@ -106,13 +107,6 @@ async def test_switch( [Platform.SWITCH], ): await setup_integration(hass, mock_config_entry) - entity_entries = er.async_entries_for_config_entry( - entity_registry, mock_config_entry.entry_id + await snapshot_platform( + hass, entity_registry, snapshot, mock_config_entry.entry_id ) - - assert entity_entries - for entity_entry in entity_entries: - assert hass.states.get(entity_entry.entity_id) == snapshot( - name=f"{entity_entry.entity_id}-state" - ) - assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry") From 10228ee1a2bd1bfb41eb4f74b880d9dcce45b3da Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 23 Apr 2024 17:39:44 +0200 Subject: [PATCH 785/967] Bump python-fritzhome to 0.6.11 (#115904) --- .../components/fritzbox/coordinator.py | 26 +++++++++++-------- .../components/fritzbox/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/fritzbox/coordinator.py b/homeassistant/components/fritzbox/coordinator.py index 06454fa912a..54af8fbdacd 100644 --- a/homeassistant/components/fritzbox/coordinator.py +++ b/homeassistant/components/fritzbox/coordinator.py @@ -82,9 +82,9 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat def _update_fritz_devices(self) -> FritzboxCoordinatorData: """Update all fritzbox device data.""" try: - self.fritz.update_devices() + self.fritz.update_devices(ignore_removed=False) if self.has_templates: - self.fritz.update_templates() + self.fritz.update_templates(ignore_removed=False) except RequestConnectionError as ex: raise UpdateFailed from ex except HTTPError: @@ -93,9 +93,9 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat self.fritz.login() except LoginError as ex: raise ConfigEntryAuthFailed from ex - self.fritz.update_devices() + self.fritz.update_devices(ignore_removed=False) if self.has_templates: - self.fritz.update_templates() + self.fritz.update_templates(ignore_removed=False) devices = self.fritz.get_devices() device_data = {} @@ -124,14 +124,18 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat self.new_devices = device_data.keys() - self.data.devices.keys() self.new_templates = template_data.keys() - self.data.templates.keys() - if ( - self.data.devices.keys() - device_data.keys() - or self.data.templates.keys() - template_data.keys() - ): - self.cleanup_removed_devices(list(device_data) + list(template_data)) - return FritzboxCoordinatorData(devices=device_data, templates=template_data) async def _async_update_data(self) -> FritzboxCoordinatorData: """Fetch all device data.""" - return await self.hass.async_add_executor_job(self._update_fritz_devices) + new_data = await self.hass.async_add_executor_job(self._update_fritz_devices) + + if ( + self.data.devices.keys() - new_data.devices.keys() + or self.data.templates.keys() - new_data.templates.keys() + ): + self.cleanup_removed_devices( + list(new_data.devices) + list(new_data.templates) + ) + + return new_data diff --git a/homeassistant/components/fritzbox/manifest.json b/homeassistant/components/fritzbox/manifest.json index 5d41f8c12dc..de2e9e0200a 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.10"], + "requirements": ["pyfritzhome==0.6.11"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:fritzbox:1" diff --git a/requirements_all.txt b/requirements_all.txt index a5d370fce8b..240606435ba 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1836,7 +1836,7 @@ pyforked-daapd==0.1.14 pyfreedompro==1.1.0 # homeassistant.components.fritzbox -pyfritzhome==0.6.10 +pyfritzhome==0.6.11 # homeassistant.components.ifttt pyfttt==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 35c005fe4d3..51161b1afd3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1432,7 +1432,7 @@ pyforked-daapd==0.1.14 pyfreedompro==1.1.0 # homeassistant.components.fritzbox -pyfritzhome==0.6.10 +pyfritzhome==0.6.11 # homeassistant.components.ifttt pyfttt==0.3 From 3b678896d966da540d229050ae5ed7d24e6ad906 Mon Sep 17 00:00:00 2001 From: David Knowles Date: Tue, 23 Apr 2024 12:08:07 -0400 Subject: [PATCH 786/967] Remove platform schema from Hydrawise (#116032) --- .../components/hydrawise/binary_sensor.py | 26 -------------- homeassistant/components/hydrawise/sensor.py | 28 +-------------- homeassistant/components/hydrawise/switch.py | 36 +------------------ 3 files changed, 2 insertions(+), 88 deletions(-) diff --git a/homeassistant/components/hydrawise/binary_sensor.py b/homeassistant/components/hydrawise/binary_sensor.py index e75cf56ac75..a93976b12e0 100644 --- a/homeassistant/components/hydrawise/binary_sensor.py +++ b/homeassistant/components/hydrawise/binary_sensor.py @@ -3,20 +3,15 @@ from __future__ import annotations from pydrawise.schema import Zone -import voluptuous as vol from homeassistant.components.binary_sensor import ( - PLATFORM_SCHEMA, BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_MONITORED_CONDITIONS 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 .const import DOMAIN from .coordinator import HydrawiseDataUpdateCoordinator @@ -39,27 +34,6 @@ BINARY_SENSOR_KEYS: list[str] = [ desc.key for desc in (BINARY_SENSOR_STATUS, *BINARY_SENSOR_TYPES) ] -# Deprecated since Home Assistant 2023.10.0 -# Can be removed completely in 2024.4.0 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_MONITORED_CONDITIONS, default=BINARY_SENSOR_KEYS): vol.All( - cv.ensure_list, [vol.In(BINARY_SENSOR_KEYS)] - ) - } -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up a sensor for a Hydrawise device.""" - # We don't need to trigger import flow from here as it's triggered from `__init__.py` - return # pragma: no cover - async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/hydrawise/sensor.py b/homeassistant/components/hydrawise/sensor.py index eedeb4a07bc..84e9f979878 100644 --- a/homeassistant/components/hydrawise/sensor.py +++ b/homeassistant/components/hydrawise/sensor.py @@ -5,20 +5,16 @@ from __future__ import annotations from datetime import datetime from pydrawise.schema import Zone -import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_MONITORED_CONDITIONS, UnitOfTime +from homeassistant.const import UnitOfTime 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.util import dt as dt_util from .const import DOMAIN @@ -39,32 +35,10 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ) SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] - -# Deprecated since Home Assistant 2023.10.0 -# Can be removed completely in 2024.4.0 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_KEYS): vol.All( - cv.ensure_list, [vol.In(SENSOR_KEYS)] - ) - } -) - TWO_YEAR_SECONDS = 60 * 60 * 24 * 365 * 2 WATERING_TIME_ICON = "mdi:water-pump" -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up a sensor for a Hydrawise device.""" - # We don't need to trigger import flow from here as it's triggered from `__init__.py` - return # pragma: no cover - - async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, diff --git a/homeassistant/components/hydrawise/switch.py b/homeassistant/components/hydrawise/switch.py index 49106a5938a..2dc459e7dd4 100644 --- a/homeassistant/components/hydrawise/switch.py +++ b/homeassistant/components/hydrawise/switch.py @@ -6,28 +6,18 @@ from datetime import timedelta from typing import Any from pydrawise.schema import Zone -import voluptuous as vol from homeassistant.components.switch import ( - PLATFORM_SCHEMA, SwitchDeviceClass, SwitchEntity, SwitchEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_MONITORED_CONDITIONS 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.util import dt as dt_util -from .const import ( - ALLOWED_WATERING_TIME, - CONF_WATERING_TIME, - DEFAULT_WATERING_TIME, - DOMAIN, -) +from .const import DEFAULT_WATERING_TIME, DOMAIN from .coordinator import HydrawiseDataUpdateCoordinator from .entity import HydrawiseEntity @@ -46,30 +36,6 @@ SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = ( SWITCH_KEYS: list[str] = [desc.key for desc in SWITCH_TYPES] -# Deprecated since Home Assistant 2023.10.0 -# Can be removed completely in 2024.4.0 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_MONITORED_CONDITIONS, default=SWITCH_KEYS): vol.All( - cv.ensure_list, [vol.In(SWITCH_KEYS)] - ), - vol.Optional( - CONF_WATERING_TIME, default=DEFAULT_WATERING_TIME.total_seconds() // 60 - ): vol.All(vol.In(ALLOWED_WATERING_TIME)), - } -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up a sensor for a Hydrawise device.""" - # We don't need to trigger import flow from here as it's triggered from `__init__.py` - return # pragma: no cover - async def async_setup_entry( hass: HomeAssistant, From d4ecf30b6a2696433b20e7e6c93fe23c2fda693f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 23 Apr 2024 18:35:53 +0200 Subject: [PATCH 787/967] Include libgammu-dev in devcontainer (#115983) --- Dockerfile.dev | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile.dev b/Dockerfile.dev index e60456f7b1f..507cc9a7bb2 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -22,6 +22,7 @@ RUN \ libavcodec-dev \ libavdevice-dev \ libavutil-dev \ + libgammu-dev \ libswscale-dev \ libswresample-dev \ libavfilter-dev \ From d8aa1cd8b51f1d6a141fe1333c6b4c81f4db35c1 Mon Sep 17 00:00:00 2001 From: Tucker Kern Date: Tue, 23 Apr 2024 11:11:40 -0600 Subject: [PATCH 788/967] Add fan preset translations and icons to BAF (#109944) --- homeassistant/components/baf/const.py | 2 +- homeassistant/components/baf/fan.py | 1 + homeassistant/components/baf/icons.json | 15 +++++++++++++++ homeassistant/components/baf/strings.json | 11 +++++++++++ 4 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/baf/icons.json diff --git a/homeassistant/components/baf/const.py b/homeassistant/components/baf/const.py index 9876d7ffec3..4d5020bdf02 100644 --- a/homeassistant/components/baf/const.py +++ b/homeassistant/components/baf/const.py @@ -9,7 +9,7 @@ QUERY_INTERVAL = 300 RUN_TIMEOUT = 20 -PRESET_MODE_AUTO = "Auto" +PRESET_MODE_AUTO = "auto" SPEED_COUNT = 7 SPEED_RANGE = (1, SPEED_COUNT) diff --git a/homeassistant/components/baf/fan.py b/homeassistant/components/baf/fan.py index 15c6519747d..6c90e2a53cb 100644 --- a/homeassistant/components/baf/fan.py +++ b/homeassistant/components/baf/fan.py @@ -48,6 +48,7 @@ class BAFFan(BAFEntity, FanEntity): _attr_preset_modes = [PRESET_MODE_AUTO] _attr_speed_count = SPEED_COUNT _attr_name = None + _attr_translation_key = "baf" @callback def _async_update_attrs(self) -> None: diff --git a/homeassistant/components/baf/icons.json b/homeassistant/components/baf/icons.json new file mode 100644 index 00000000000..c91c4cde86a --- /dev/null +++ b/homeassistant/components/baf/icons.json @@ -0,0 +1,15 @@ +{ + "entity": { + "fan": { + "baf": { + "state_attributes": { + "preset_mode": { + "state": { + "auto": "mdi:fan-auto" + } + } + } + } + } + } +} diff --git a/homeassistant/components/baf/strings.json b/homeassistant/components/baf/strings.json index 5143b519d27..e2f02a6095e 100644 --- a/homeassistant/components/baf/strings.json +++ b/homeassistant/components/baf/strings.json @@ -26,6 +26,17 @@ "name": "Auto comfort" } }, + "fan": { + "baf": { + "state_attributes": { + "preset_mode": { + "state": { + "auto": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::auto%]" + } + } + } + } + }, "number": { "comfort_min_speed": { "name": "Auto Comfort Minimum Speed" From cc9eab4c78a77cd4004c96fb15b93edd03367481 Mon Sep 17 00:00:00 2001 From: Jim Date: Tue, 23 Apr 2024 18:32:09 +0100 Subject: [PATCH 789/967] Allow plain text messages in telegram_bot (#110051) * Add new plain_text parser Passing None in the parse_mode kwargs on the various bot methods actually means that no parser is used. * Add new plain text parser option to services.yaml --------- Co-authored-by: Erik Montnemery --- homeassistant/components/telegram_bot/__init__.py | 2 ++ homeassistant/components/telegram_bot/services.yaml | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index 897fd6a9bac..f672ae1547f 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -122,6 +122,7 @@ EVENT_TELEGRAM_SENT = "telegram_sent" PARSER_HTML = "html" PARSER_MD = "markdown" PARSER_MD2 = "markdownv2" +PARSER_PLAIN_TEXT = "plain_text" DEFAULT_TRUSTED_NETWORKS = [ip_network("149.154.160.0/20"), ip_network("91.108.4.0/22")] @@ -524,6 +525,7 @@ class TelegramNotificationService: PARSER_HTML: ParseMode.HTML, PARSER_MD: ParseMode.MARKDOWN, PARSER_MD2: ParseMode.MARKDOWN_V2, + PARSER_PLAIN_TEXT: None, } self._parse_mode = self._parsers.get(parser) self.bot = bot diff --git a/homeassistant/components/telegram_bot/services.yaml b/homeassistant/components/telegram_bot/services.yaml index 1587f754508..d2195c1d6ce 100644 --- a/homeassistant/components/telegram_bot/services.yaml +++ b/homeassistant/components/telegram_bot/services.yaml @@ -22,6 +22,7 @@ send_message: - "html" - "markdown" - "markdownv2" + - "plain_text" disable_notification: selector: boolean: @@ -94,6 +95,7 @@ send_photo: - "html" - "markdown" - "markdownv2" + - "plain_text" disable_notification: selector: boolean: @@ -229,6 +231,7 @@ send_animation: - "html" - "markdown" - "markdownv2" + - "plain_text" disable_notification: selector: boolean: @@ -300,6 +303,7 @@ send_video: - "html" - "markdown" - "markdownv2" + - "plain_text" disable_notification: selector: boolean: @@ -435,6 +439,7 @@ send_document: - "html" - "markdown" - "markdownv2" + - "plain_text" disable_notification: selector: boolean: @@ -587,6 +592,7 @@ edit_message: - "html" - "markdown" - "markdownv2" + - "plain_text" disable_web_page_preview: selector: boolean: From 0ed48c844d1ae3e809f3615b5687617d501d90a7 Mon Sep 17 00:00:00 2001 From: Markus Jacobsen Date: Tue, 23 Apr 2024 21:06:06 +0200 Subject: [PATCH 790/967] Bump mozart-api to 3.4.1.8.5 (#113745) --- .../components/bang_olufsen/__init__.py | 27 ++++++++++++------- .../components/bang_olufsen/manifest.json | 2 +- .../components/bang_olufsen/media_player.py | 4 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 23 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/bang_olufsen/__init__.py b/homeassistant/components/bang_olufsen/__init__.py index 2488c2e64f5..07b9d0befe1 100644 --- a/homeassistant/components/bang_olufsen/__init__.py +++ b/homeassistant/components/bang_olufsen/__init__.py @@ -4,7 +4,11 @@ from __future__ import annotations from dataclasses import dataclass -from aiohttp.client_exceptions import ClientConnectorError +from aiohttp.client_exceptions import ( + ClientConnectorError, + ClientOSError, + ServerTimeoutError, +) from mozart_api.exceptions import ApiException from mozart_api.mozart_client import MozartClient @@ -44,12 +48,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: model=entry.data[CONF_MODEL], ) - client = MozartClient(host=entry.data[CONF_HOST], websocket_reconnect=True) + client = MozartClient(host=entry.data[CONF_HOST]) - # Check connection and try to initialize it. + # Check API and WebSocket connection try: - await client.get_battery_state(_request_timeout=3) - except (ApiException, ClientConnectorError, TimeoutError) as error: + await client.check_device_connection(True) + except* ( + ClientConnectorError, + ClientOSError, + ServerTimeoutError, + ApiException, + TimeoutError, + ) as error: await client.close_api_client() raise ConfigEntryNotReady(f"Unable to connect to {entry.title}") from error @@ -61,11 +71,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: client, ) - # Check and start WebSocket connection - if not await client.connect_notifications(remote_control=True): - raise ConfigEntryNotReady( - f"Unable to connect to {entry.title} WebSocket notification channel" - ) + # Start WebSocket connection + await client.connect_notifications(remote_control=True, reconnect=True) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/bang_olufsen/manifest.json b/homeassistant/components/bang_olufsen/manifest.json index 3c920a99d7f..f2b31293227 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.2.1.150.6"], + "requirements": ["mozart-api==3.4.1.8.5"], "zeroconf": ["_bangolufsen._tcp.local."] } diff --git a/homeassistant/components/bang_olufsen/media_player.py b/homeassistant/components/bang_olufsen/media_player.py index 935c057efc8..9f55790d711 100644 --- a/homeassistant/components/bang_olufsen/media_player.py +++ b/homeassistant/components/bang_olufsen/media_player.py @@ -363,7 +363,9 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity): def is_volume_muted(self) -> bool | None: """Boolean if volume is currently muted.""" if self._volume.muted and self._volume.muted.muted: - return self._volume.muted.muted + # The any return here is side effect of pydantic v2 compatibility + # This will be fixed in the future. + return self._volume.muted.muted # type: ignore[no-any-return] return None @property diff --git a/requirements_all.txt b/requirements_all.txt index 240606435ba..f212a8675e8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1332,7 +1332,7 @@ motionblindsble==0.0.9 motioneye-client==0.3.14 # homeassistant.components.bang_olufsen -mozart-api==3.2.1.150.6 +mozart-api==3.4.1.8.5 # homeassistant.components.mullvad mullvad-api==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 51161b1afd3..8f318a24b5e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1074,7 +1074,7 @@ motionblindsble==0.0.9 motioneye-client==0.3.14 # homeassistant.components.bang_olufsen -mozart-api==3.2.1.150.6 +mozart-api==3.4.1.8.5 # homeassistant.components.mullvad mullvad-api==1.0.0 From 61cf7e851b7dd5555b63a9d986706eed4da37200 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 23 Apr 2024 21:13:32 +0200 Subject: [PATCH 791/967] Update pipdeptree to 2.17.0 (#116049) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index e42a94091ad..10812a87c6e 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -16,7 +16,7 @@ pre-commit==3.7.0 pydantic==1.10.12 pylint==3.1.0 pylint-per-file-ignores==1.3.2 -pipdeptree==2.16.1 +pipdeptree==2.17.0 pytest-asyncio==0.23.6 pytest-aiohttp==1.0.5 pytest-cov==5.0.0 From 991e479dacdf00fff033d89ecb9179531a7b38ec Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 23 Apr 2024 21:26:00 +0200 Subject: [PATCH 792/967] Update coverage to 7.5.0 (#116048) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 10812a87c6e..233c8c1534e 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.1.0 -coverage==7.4.4 +coverage==7.5.0 freezegun==1.4.0 mock-open==1.4.0 mypy-dev==1.10.0a3 From 46ec8a85b60b63a62e70e5c4f52bd9a5cf4b4244 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 23 Apr 2024 21:31:08 +0200 Subject: [PATCH 793/967] Pass the job type when setting up homekit state change listeners (#116038) --- homeassistant/components/homekit/accessories.py | 8 +++++++- homeassistant/components/homekit/type_cameras.py | 3 +++ homeassistant/components/homekit/type_covers.py | 9 ++++++++- homeassistant/components/homekit/type_humidifiers.py | 9 ++++++++- 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index f2e1a26b3de..40e86efe6a9 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -46,6 +46,7 @@ from homeassistant.core import ( Context, Event, EventStateChangedData, + HassJobType, HomeAssistant, State, callback as ha_callback, @@ -436,7 +437,10 @@ class HomeAccessory(Accessory): # type: ignore[misc] self._update_available_from_state(state) self._subscriptions.append( async_track_state_change_event( - self.hass, [self.entity_id], self.async_update_event_state_callback + self.hass, + [self.entity_id], + self.async_update_event_state_callback, + job_type=HassJobType.Callback, ) ) @@ -456,6 +460,7 @@ class HomeAccessory(Accessory): # type: ignore[misc] self.hass, [self.linked_battery_sensor], self.async_update_linked_battery_callback, + job_type=HassJobType.Callback, ) ) elif state is not None: @@ -468,6 +473,7 @@ class HomeAccessory(Accessory): # type: ignore[misc] self.hass, [self.linked_battery_charging_sensor], self.async_update_linked_battery_charging_callback, + job_type=HassJobType.Callback, ) ) elif battery_charging_state is None and state is not None: diff --git a/homeassistant/components/homekit/type_cameras.py b/homeassistant/components/homekit/type_cameras.py index d14fef8eabf..4f05bfbd687 100644 --- a/homeassistant/components/homekit/type_cameras.py +++ b/homeassistant/components/homekit/type_cameras.py @@ -20,6 +20,7 @@ from homeassistant.const import STATE_ON from homeassistant.core import ( Event, EventStateChangedData, + HassJobType, HomeAssistant, State, callback, @@ -272,6 +273,7 @@ class Camera(HomeAccessory, PyhapCamera): # type: ignore[misc] self.hass, [self.linked_motion_sensor], self._async_update_motion_state_event, + job_type=HassJobType.Callback, ) ) @@ -282,6 +284,7 @@ class Camera(HomeAccessory, PyhapCamera): # type: ignore[misc] self.hass, [self.linked_doorbell_sensor], self._async_update_doorbell_state_event, + job_type=HassJobType.Callback, ) ) diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index d14713b5f05..29dda418665 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -34,7 +34,13 @@ from homeassistant.const import ( STATE_OPEN, STATE_OPENING, ) -from homeassistant.core import Event, EventStateChangedData, State, callback +from homeassistant.core import ( + Event, + EventStateChangedData, + HassJobType, + State, + callback, +) from homeassistant.helpers.event import async_track_state_change_event from .accessories import TYPES, HomeAccessory @@ -136,6 +142,7 @@ class GarageDoorOpener(HomeAccessory): self.hass, [self.linked_obstruction_sensor], self._async_update_obstruction_event, + job_type=HassJobType.Callback, ) ) diff --git a/homeassistant/components/homekit/type_humidifiers.py b/homeassistant/components/homekit/type_humidifiers.py index 1fca441e800..5bdf5950f18 100644 --- a/homeassistant/components/homekit/type_humidifiers.py +++ b/homeassistant/components/homekit/type_humidifiers.py @@ -25,7 +25,13 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_ON, ) -from homeassistant.core import Event, EventStateChangedData, State, callback +from homeassistant.core import ( + Event, + EventStateChangedData, + HassJobType, + State, + callback, +) from homeassistant.helpers.event import async_track_state_change_event from .accessories import TYPES, HomeAccessory @@ -184,6 +190,7 @@ class HumidifierDehumidifier(HomeAccessory): self.hass, [self.linked_humidity_sensor], self.async_update_current_humidity_event, + job_type=HassJobType.Callback, ) ) From 3e0a45eee267b923db76715f2d238c586e32ba56 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 23 Apr 2024 21:36:36 +0200 Subject: [PATCH 794/967] Update requests_mock to 1.12.1 (#116050) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 233c8c1534e..5470bc2a49d 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -29,7 +29,7 @@ pytest-unordered==0.6.0 pytest-picked==0.5.0 pytest-xdist==3.5.0 pytest==8.1.1 -requests-mock==1.11.0 +requests-mock==1.12.1 respx==0.21.0 syrupy==4.6.1 tqdm==4.66.2 From 8bf3c87336f9d04602c0bff3feb5021eedae2ff0 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 23 Apr 2024 21:45:20 +0200 Subject: [PATCH 795/967] Breakout heartbeat monitor and poe command queue in UniFi (#112529) * Split out entity helper functionality to own class * Split out heartbeat to own class * Break out poe command * Make more parts private * Make more things private and simplify naming * Sort initialize * Fix ruff --- .../components/unifi/device_tracker.py | 12 +- .../components/unifi/hub/entity_helper.py | 156 ++++++++++++++++++ homeassistant/components/unifi/hub/hub.py | 110 ++++-------- homeassistant/components/unifi/sensor.py | 4 +- homeassistant/components/unifi/switch.py | 2 +- 5 files changed, 193 insertions(+), 91 deletions(-) create mode 100644 homeassistant/components/unifi/hub/entity_helper.py diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index a41d1942536..dc48b9c31fe 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -240,7 +240,7 @@ class UnifiScannerEntity(UnifiEntity[HandlerT, ApiItemT], ScannerEntity): self._ignore_events = False self._is_connected = description.is_connected_fn(self.hub, self._obj_id) if self.is_connected: - self.hub.async_heartbeat( + self.hub.update_heartbeat( self.unique_id, dt_util.utcnow() + description.heartbeat_timedelta_fn(self.hub, self._obj_id), @@ -301,12 +301,12 @@ class UnifiScannerEntity(UnifiEntity[HandlerT, ApiItemT], ScannerEntity): # From unifi.entity.async_signal_reachable_callback # Controller connection state has changed and entity is unavailable # Cancel heartbeat - self.hub.async_heartbeat(self.unique_id) + self.hub.remove_heartbeat(self.unique_id) return if is_connected := description.is_connected_fn(self.hub, self._obj_id): self._is_connected = is_connected - self.hub.async_heartbeat( + self.hub.update_heartbeat( self.unique_id, dt_util.utcnow() + description.heartbeat_timedelta_fn(self.hub, self._obj_id), @@ -319,12 +319,12 @@ class UnifiScannerEntity(UnifiEntity[HandlerT, ApiItemT], ScannerEntity): return if event.key in self._event_is_on: - self.hub.async_heartbeat(self.unique_id) + self.hub.remove_heartbeat(self.unique_id) self._is_connected = True self.async_write_ha_state() return - self.hub.async_heartbeat( + self.hub.update_heartbeat( self.unique_id, dt_util.utcnow() + self.entity_description.heartbeat_timedelta_fn(self.hub, self._obj_id), @@ -344,7 +344,7 @@ class UnifiScannerEntity(UnifiEntity[HandlerT, ApiItemT], ScannerEntity): async def async_will_remove_from_hass(self) -> None: """Disconnect object when removed.""" await super().async_will_remove_from_hass() - self.hub.async_heartbeat(self.unique_id) + self.hub.remove_heartbeat(self.unique_id) @property def extra_state_attributes(self) -> Mapping[str, Any] | None: diff --git a/homeassistant/components/unifi/hub/entity_helper.py b/homeassistant/components/unifi/hub/entity_helper.py new file mode 100644 index 00000000000..c4bcf237386 --- /dev/null +++ b/homeassistant/components/unifi/hub/entity_helper.py @@ -0,0 +1,156 @@ +"""UniFi Network entity helper.""" + +from __future__ import annotations + +from datetime import datetime, timedelta + +import aiounifi +from aiounifi.models.device import DeviceSetPoePortModeRequest + +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_call_later, async_track_time_interval +import homeassistant.util.dt as dt_util + + +class UnifiEntityHelper: + """UniFi Network integration handling platforms for entity registration.""" + + def __init__(self, hass: HomeAssistant, api: aiounifi.Controller) -> None: + """Initialize the UniFi entity loader.""" + self.hass = hass + self.api = api + + self._device_command = UnifiDeviceCommand(hass, api) + self._heartbeat = UnifiEntityHeartbeat(hass) + + @callback + def reset(self) -> None: + """Cancel timers.""" + self._device_command.reset() + self._heartbeat.reset() + + @callback + def initialize(self) -> None: + """Initialize entity helper.""" + self._heartbeat.initialize() + + @property + def signal_heartbeat(self) -> str: + """Event to signal new heartbeat missed.""" + return self._heartbeat.signal + + @callback + def update_heartbeat(self, unique_id: str, heartbeat_expire_time: datetime) -> None: + """Update device time in heartbeat monitor.""" + self._heartbeat.update(unique_id, heartbeat_expire_time) + + @callback + def remove_heartbeat(self, unique_id: str) -> None: + """Update device time in heartbeat monitor.""" + self._heartbeat.remove(unique_id) + + @callback + def queue_poe_port_command( + self, device_id: str, port_idx: int, poe_mode: str + ) -> None: + """Queue commands to execute them together per device.""" + self._device_command.queue_poe_command(device_id, port_idx, poe_mode) + + +class UnifiEntityHeartbeat: + """UniFi entity heartbeat monitor.""" + + CHECK_HEARTBEAT_INTERVAL = timedelta(seconds=1) + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the heartbeat monitor.""" + self.hass = hass + + self._cancel_heartbeat_check: CALLBACK_TYPE | None = None + self._heartbeat_time: dict[str, datetime] = {} + + @callback + def reset(self) -> None: + """Cancel timers.""" + if self._cancel_heartbeat_check: + self._cancel_heartbeat_check() + self._cancel_heartbeat_check = None + + @callback + def initialize(self) -> None: + """Initialize heartbeat monitor.""" + self._cancel_heartbeat_check = async_track_time_interval( + self.hass, self._check_for_stale, self.CHECK_HEARTBEAT_INTERVAL + ) + + @property + def signal(self) -> str: + """Event to signal new heartbeat missed.""" + return "unifi-heartbeat-missed" + + @callback + def update(self, unique_id: str, heartbeat_expire_time: datetime) -> None: + """Update device time in heartbeat monitor.""" + self._heartbeat_time[unique_id] = heartbeat_expire_time + + @callback + def remove(self, unique_id: str) -> None: + """Remove device from heartbeat monitor.""" + self._heartbeat_time.pop(unique_id, None) + + @callback + def _check_for_stale(self, *_: datetime) -> None: + """Check for any devices scheduled to be marked disconnected.""" + now = dt_util.utcnow() + + unique_ids_to_remove = [] + for unique_id, heartbeat_expire_time in self._heartbeat_time.items(): + if now > heartbeat_expire_time: + async_dispatcher_send(self.hass, f"{self.signal}_{unique_id}") + unique_ids_to_remove.append(unique_id) + + for unique_id in unique_ids_to_remove: + del self._heartbeat_time[unique_id] + + +class UnifiDeviceCommand: + """UniFi Device command helper class.""" + + COMMAND_DELAY = 5 + + def __init__(self, hass: HomeAssistant, api: aiounifi.Controller) -> None: + """Initialize device command helper.""" + self.hass = hass + self.api = api + + self._command_queue: dict[str, dict[int, str]] = {} + self._cancel_command: CALLBACK_TYPE | None = None + + @callback + def reset(self) -> None: + """Cancel timers.""" + if self._cancel_command: + self._cancel_command() + self._cancel_command = None + + @callback + def queue_poe_command(self, device_id: str, port_idx: int, poe_mode: str) -> None: + """Queue commands to execute them together per device.""" + self.reset() + + device_queue = self._command_queue.setdefault(device_id, {}) + device_queue[port_idx] = poe_mode + + async def _command(now: datetime) -> None: + """Execute previously queued commands.""" + queue = self._command_queue.copy() + self._command_queue.clear() + for device_id, device_commands in queue.items(): + device = self.api.devices[device_id] + commands = list(device_commands.items()) + await self.api.request( + DeviceSetPoePortModeRequest.create(device, targets=commands) + ) + + self._cancel_command = async_call_later(self.hass, self.COMMAND_DELAY, _command) diff --git a/homeassistant/components/unifi/hub/hub.py b/homeassistant/components/unifi/hub/hub.py index df91584f267..f8c1f2517a2 100644 --- a/homeassistant/components/unifi/hub/hub.py +++ b/homeassistant/components/unifi/hub/hub.py @@ -2,13 +2,12 @@ from __future__ import annotations -from datetime import datetime, timedelta +from datetime import datetime import aiounifi -from aiounifi.models.device import DeviceSetPoePortModeRequest from homeassistant.config_entries import ConfigEntry -from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import ( DeviceEntry, @@ -16,16 +15,13 @@ from homeassistant.helpers.device_registry import ( DeviceInfo, ) from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.event import async_call_later, async_track_time_interval -import homeassistant.util.dt as dt_util from ..const import ATTR_MANUFACTURER, CONF_SITE_ID, DOMAIN as UNIFI_DOMAIN, PLATFORMS from .config import UnifiConfig +from .entity_helper import UnifiEntityHelper from .entity_loader import UnifiEntityLoader from .websocket import UnifiWebsocket -CHECK_HEARTBEAT_INTERVAL = timedelta(seconds=1) - class UnifiHub: """Manages a single UniFi Network instance.""" @@ -38,17 +34,12 @@ class UnifiHub: self.api = api self.config = UnifiConfig.from_config_entry(config_entry) self.entity_loader = UnifiEntityLoader(self) + self._entity_helper = UnifiEntityHelper(hass, api) self.websocket = UnifiWebsocket(hass, api, self.signal_reachable) self.site = config_entry.data[CONF_SITE_ID] self.is_admin = False - self._cancel_heartbeat_check: CALLBACK_TYPE | None = None - self._heartbeat_time: dict[str, datetime] = {} - - self.poe_command_queue: dict[str, dict[int, str]] = {} - self._cancel_poe_command: CALLBACK_TYPE | None = None - @callback @staticmethod def get_hub(hass: HomeAssistant, config_entry: ConfigEntry) -> UnifiHub: @@ -61,6 +52,28 @@ class UnifiHub: """Websocket connection state.""" return self.websocket.available + @property + def signal_heartbeat_missed(self) -> str: + """Event to signal new heartbeat missed.""" + return self._entity_helper.signal_heartbeat + + @callback + def update_heartbeat(self, unique_id: str, heartbeat_expire_time: datetime) -> None: + """Update device time in heartbeat monitor.""" + self._entity_helper.update_heartbeat(unique_id, heartbeat_expire_time) + + @callback + def remove_heartbeat(self, unique_id: str) -> None: + """Update device time in heartbeat monitor.""" + self._entity_helper.remove_heartbeat(unique_id) + + @callback + def queue_poe_port_command( + self, device_id: str, port_idx: int, poe_mode: str + ) -> None: + """Queue commands to execute them together per device.""" + self._entity_helper.queue_poe_port_command(device_id, port_idx, poe_mode) + @property def signal_reachable(self) -> str: """Integration specific event to signal a change in connection status.""" @@ -71,77 +84,16 @@ class UnifiHub: """Event specific per UniFi entry to signal new options.""" return f"unifi-options-{self.config.entry.entry_id}" - @property - def signal_heartbeat_missed(self) -> str: - """Event specific per UniFi device tracker to signal new heartbeat missed.""" - return "unifi-heartbeat-missed" - async def initialize(self) -> None: """Set up a UniFi Network instance.""" await self.entity_loader.initialize() + self._entity_helper.initialize() assert self.config.entry.unique_id is not None self.is_admin = self.api.sites[self.config.entry.unique_id].role == "admin" self.config.entry.add_update_listener(self.async_config_entry_updated) - self._cancel_heartbeat_check = async_track_time_interval( - self.hass, self._async_check_for_stale, CHECK_HEARTBEAT_INTERVAL - ) - - @callback - def async_heartbeat( - self, unique_id: str, heartbeat_expire_time: datetime | None = None - ) -> None: - """Signal when a device has fresh home state.""" - if heartbeat_expire_time is not None: - self._heartbeat_time[unique_id] = heartbeat_expire_time - return - - if unique_id in self._heartbeat_time: - del self._heartbeat_time[unique_id] - - @callback - def _async_check_for_stale(self, *_: datetime) -> None: - """Check for any devices scheduled to be marked disconnected.""" - now = dt_util.utcnow() - - unique_ids_to_remove = [] - for unique_id, heartbeat_expire_time in self._heartbeat_time.items(): - if now > heartbeat_expire_time: - async_dispatcher_send( - self.hass, f"{self.signal_heartbeat_missed}_{unique_id}" - ) - unique_ids_to_remove.append(unique_id) - - for unique_id in unique_ids_to_remove: - del self._heartbeat_time[unique_id] - - @callback - def async_queue_poe_port_command( - self, device_id: str, port_idx: int, poe_mode: str - ) -> None: - """Queue commands to execute them together per device.""" - if self._cancel_poe_command: - self._cancel_poe_command() - self._cancel_poe_command = None - - device_queue = self.poe_command_queue.setdefault(device_id, {}) - device_queue[port_idx] = poe_mode - - async def async_execute_command(now: datetime) -> None: - """Execute previously queued commands.""" - queue = self.poe_command_queue.copy() - self.poe_command_queue.clear() - for device_id, device_commands in queue.items(): - device = self.api.devices[device_id] - commands = list(device_commands.items()) - await self.api.request( - DeviceSetPoePortModeRequest.create(device, targets=commands) - ) - - self._cancel_poe_command = async_call_later(self.hass, 5, async_execute_command) - @property def device_info(self) -> DeviceInfo: """UniFi Network device info.""" @@ -205,12 +157,6 @@ class UnifiHub: if not unload_ok: return False - if self._cancel_heartbeat_check: - self._cancel_heartbeat_check() - self._cancel_heartbeat_check = None - - if self._cancel_poe_command: - self._cancel_poe_command() - self._cancel_poe_command = None + self._entity_helper.reset() return True diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index cec87b36416..17b3cae93fd 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -460,7 +460,7 @@ class UnifiSensorEntity(UnifiEntity[HandlerT, ApiItemT], SensorEntity): if description.is_connected_fn is not None: # Send heartbeat if client is connected if description.is_connected_fn(self.hub, self._obj_id): - self.hub.async_heartbeat( + self.hub.update_heartbeat( self._attr_unique_id, dt_util.utcnow() + self.hub.config.option_detection_time, ) @@ -485,4 +485,4 @@ class UnifiSensorEntity(UnifiEntity[HandlerT, ApiItemT], SensorEntity): if self.entity_description.is_connected_fn is not None: # Remove heartbeat registration - self.hub.async_heartbeat(self._attr_unique_id) + self.hub.remove_heartbeat(self._attr_unique_id) diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index 6e073a655a5..45357dd67d2 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -147,7 +147,7 @@ async def async_poe_port_control_fn(hub: UnifiHub, obj_id: str, target: bool) -> port = hub.api.ports[obj_id] on_state = "auto" if port.raw["poe_caps"] != 8 else "passthrough" state = on_state if target else "off" - hub.async_queue_poe_port_command(mac, int(index), state) + hub.queue_poe_port_command(mac, int(index), state) async def async_port_forward_control_fn( From bb2bd086bc12785a98b643f1c2e4c1f9bccb635b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 23 Apr 2024 21:52:55 +0200 Subject: [PATCH 796/967] Add missing adapter data to Bluetooth config entry titles (#115930) --- homeassistant/components/bluetooth/__init__.py | 5 +++++ .../components/bluetooth/config_flow.py | 10 +--------- homeassistant/components/bluetooth/util.py | 18 +++++++++++++++++- tests/components/bluetooth/test_config_flow.py | 8 ++------ tests/components/bluetooth/test_init.py | 13 +++++++++++++ 5 files changed, 38 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 560fb0663a8..4768d58379a 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -86,6 +86,7 @@ from .manager import HomeAssistantBluetoothManager from .match import BluetoothCallbackMatcher, IntegrationMatcher from .models import BluetoothCallback, BluetoothChange from .storage import BluetoothStorage +from .util import adapter_title if TYPE_CHECKING: from homeassistant.helpers.typing import ConfigType @@ -332,6 +333,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) from err adapters = await manager.async_get_bluetooth_adapters() details = adapters[adapter] + if entry.title == address: + hass.config_entries.async_update_entry( + entry, title=adapter_title(adapter, details) + ) slots: int = details.get(ADAPTER_CONNECTION_SLOTS) or DEFAULT_CONNECTION_SLOTS entry.async_on_unload(async_register_scanner(hass, scanner, connection_slots=slots)) await async_update_device(hass, entry, adapter, details) diff --git a/homeassistant/components/bluetooth/config_flow.py b/homeassistant/components/bluetooth/config_flow.py index 87038d48151..90d2624fb0f 100644 --- a/homeassistant/components/bluetooth/config_flow.py +++ b/homeassistant/components/bluetooth/config_flow.py @@ -12,7 +12,6 @@ from bluetooth_adapters import ( AdapterDetails, adapter_human_name, adapter_model, - adapter_unique_name, get_adapters, ) import voluptuous as vol @@ -28,6 +27,7 @@ from homeassistant.helpers.typing import DiscoveryInfoType from . import models from .const import CONF_ADAPTER, CONF_DETAILS, CONF_PASSIVE, DOMAIN +from .util import adapter_title OPTIONS_SCHEMA = vol.Schema( { @@ -47,14 +47,6 @@ def adapter_display_info(adapter: str, details: AdapterDetails) -> str: return f"{name} {manufacturer} {model}" -def adapter_title(adapter: str, details: AdapterDetails) -> str: - """Return the adapter title.""" - unique_name = adapter_unique_name(adapter, details[ADAPTER_ADDRESS]) - model = adapter_model(details) - manufacturer = details[ADAPTER_MANUFACTURER] or "Unknown" - return f"{manufacturer} {model} ({unique_name})" - - class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): """Config flow for Bluetooth.""" diff --git a/homeassistant/components/bluetooth/util.py b/homeassistant/components/bluetooth/util.py index 0faac9a8613..8c7ad13294a 100644 --- a/homeassistant/components/bluetooth/util.py +++ b/homeassistant/components/bluetooth/util.py @@ -2,7 +2,14 @@ from __future__ import annotations -from bluetooth_adapters import BluetoothAdapters +from bluetooth_adapters import ( + ADAPTER_ADDRESS, + ADAPTER_MANUFACTURER, + ADAPTER_PRODUCT, + AdapterDetails, + BluetoothAdapters, + adapter_unique_name, +) from bluetooth_data_tools import monotonic_time_coarse from homeassistant.core import callback @@ -69,3 +76,12 @@ def async_load_history_from_system( connectable_loaded_history[address] = service_info return all_loaded_history, connectable_loaded_history + + +@callback +def adapter_title(adapter: str, details: AdapterDetails) -> str: + """Return the adapter title.""" + unique_name = adapter_unique_name(adapter, details[ADAPTER_ADDRESS]) + model = details.get(ADAPTER_PRODUCT, "Unknown") + manufacturer = details[ADAPTER_MANUFACTURER] or "Unknown" + return f"{manufacturer} {model} ({unique_name})" diff --git a/tests/components/bluetooth/test_config_flow.py b/tests/components/bluetooth/test_config_flow.py index d044be76e6d..33474280ec4 100644 --- a/tests/components/bluetooth/test_config_flow.py +++ b/tests/components/bluetooth/test_config_flow.py @@ -99,9 +99,7 @@ async def test_async_step_user_linux_one_adapter( result["flow_id"], user_input={} ) assert result2["type"] is FlowResultType.CREATE_ENTRY - assert ( - result2["title"] == "ACME Bluetooth Adapter 5.0 (cc01:aa01) (00:00:00:00:00:01)" - ) + assert result2["title"] == "ACME Bluetooth Adapter 5.0 (00:00:00:00:00:01)" assert result2["data"] == {} assert len(mock_setup_entry.mock_calls) == 1 @@ -144,9 +142,7 @@ async def test_async_step_user_linux_two_adapters( result["flow_id"], user_input={CONF_ADAPTER: "hci1"} ) assert result2["type"] is FlowResultType.CREATE_ENTRY - assert ( - result2["title"] == "ACME Bluetooth Adapter 5.0 (cc01:aa01) (00:00:00:00:00:02)" - ) + assert result2["title"] == "ACME Bluetooth Adapter 5.0 (00:00:00:00:00:02)" assert result2["data"] == {} assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 82fa0341966..8c26745d541 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -3173,3 +3173,16 @@ async def test_haos_9_or_later( registry = async_get_issue_registry(hass) issue = registry.async_get_issue(DOMAIN, "haos_outdated") assert issue is None + + +async def test_title_updated_if_mac_address( + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, one_adapter: None +) -> None: + """Test the title is updated if it is the mac address.""" + entry = MockConfigEntry( + domain="bluetooth", title="00:00:00:00:00:01", unique_id="00:00:00:00:00:01" + ) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.title == "ACME Bluetooth Adapter 5.0 (00:00:00:00:00:01)" From b5bd25d4fb1e1e7f68a5799b9d73c87c4c59361d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 23 Apr 2024 21:54:13 +0200 Subject: [PATCH 797/967] Add entity translations to totalconnect (#115950) --- .../totalconnect/alarm_control_panel.py | 23 ++- .../components/totalconnect/binary_sensor.py | 9 +- .../components/totalconnect/entity.py | 10 +- .../components/totalconnect/strings.json | 7 + .../snapshots/test_alarm_control_panel.ambr | 12 +- .../snapshots/test_binary_sensor.ambr | 174 +++++++++--------- .../totalconnect/test_binary_sensor.py | 14 +- 7 files changed, 128 insertions(+), 121 deletions(-) diff --git a/homeassistant/components/totalconnect/alarm_control_panel.py b/homeassistant/components/totalconnect/alarm_control_panel.py index b0ad2f19069..1de9db1d319 100644 --- a/homeassistant/components/totalconnect/alarm_control_panel.py +++ b/homeassistant/components/totalconnect/alarm_control_panel.py @@ -92,17 +92,17 @@ class TotalConnectAlarm(TotalConnectLocationEntity, AlarmControlPanelEntity): Add _# for partition 2 and beyond. """ if partition_id == 1: - self._attr_name = self.device.name + self._attr_name = None self._attr_unique_id = str(location.location_id) else: - self._attr_name = f"{self.device.name} partition {partition_id}" + self._attr_translation_key = "partition" + self._attr_translation_placeholders = {"partition_id": str(partition_id)} self._attr_unique_id = f"{location.location_id}_{partition_id}" @property def state(self) -> str | None: """Return the state of the device.""" attr = { - "location_name": self.name, "location_id": self._location.location_id, "partition": self._partition_id, "ac_loss": self._location.ac_loss, @@ -112,6 +112,11 @@ class TotalConnectAlarm(TotalConnectLocationEntity, AlarmControlPanelEntity): "triggered_zone": None, } + if self._partition_id == 1: + attr["location_name"] = self.device.name + else: + attr["location_name"] = f"{self.device.name} partition {self._partition_id}" + state: str | None = None if self._partition.arming_state.is_disarmed(): state = STATE_ALARM_DISARMED @@ -152,7 +157,7 @@ class TotalConnectAlarm(TotalConnectLocationEntity, AlarmControlPanelEntity): ) from error except BadResultCodeError as error: raise HomeAssistantError( - f"TotalConnect failed to disarm {self.name}." + f"TotalConnect failed to disarm {self.device.name}." ) from error await self.coordinator.async_request_refresh() @@ -171,7 +176,7 @@ class TotalConnectAlarm(TotalConnectLocationEntity, AlarmControlPanelEntity): ) from error except BadResultCodeError as error: raise HomeAssistantError( - f"TotalConnect failed to arm home {self.name}." + f"TotalConnect failed to arm home {self.device.name}." ) from error await self.coordinator.async_request_refresh() @@ -190,7 +195,7 @@ class TotalConnectAlarm(TotalConnectLocationEntity, AlarmControlPanelEntity): ) from error except BadResultCodeError as error: raise HomeAssistantError( - f"TotalConnect failed to arm away {self.name}." + f"TotalConnect failed to arm away {self.device.name}." ) from error await self.coordinator.async_request_refresh() @@ -209,7 +214,7 @@ class TotalConnectAlarm(TotalConnectLocationEntity, AlarmControlPanelEntity): ) from error except BadResultCodeError as error: raise HomeAssistantError( - f"TotalConnect failed to arm night {self.name}." + f"TotalConnect failed to arm night {self.device.name}." ) from error await self.coordinator.async_request_refresh() @@ -228,7 +233,7 @@ class TotalConnectAlarm(TotalConnectLocationEntity, AlarmControlPanelEntity): ) from error except BadResultCodeError as error: raise HomeAssistantError( - f"TotalConnect failed to arm home instant {self.name}." + f"TotalConnect failed to arm home instant {self.device.name}." ) from error await self.coordinator.async_request_refresh() @@ -247,7 +252,7 @@ class TotalConnectAlarm(TotalConnectLocationEntity, AlarmControlPanelEntity): ) from error except BadResultCodeError as error: raise HomeAssistantError( - f"TotalConnect failed to arm away instant {self.name}." + f"TotalConnect failed to arm away instant {self.device.name}." ) from error await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/totalconnect/binary_sensor.py b/homeassistant/components/totalconnect/binary_sensor.py index 9ff25e07d03..85461805124 100644 --- a/homeassistant/components/totalconnect/binary_sensor.py +++ b/homeassistant/components/totalconnect/binary_sensor.py @@ -54,7 +54,7 @@ def get_security_zone_device_class(zone: TotalConnectZone) -> BinarySensorDevice SECURITY_BINARY_SENSOR = TotalConnectZoneBinarySensorEntityDescription( key=ZONE, - name="", + name=None, device_class_fn=get_security_zone_device_class, is_on_fn=lambda zone: zone.is_faulted() or zone.is_triggered(), ) @@ -64,14 +64,12 @@ NO_BUTTON_BINARY_SENSORS: tuple[TotalConnectZoneBinarySensorEntityDescription, . key=LOW_BATTERY, device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, - name=" low battery", is_on_fn=lambda zone: zone.is_low_battery(), ), TotalConnectZoneBinarySensorEntityDescription( key=TAMPER, device_class=BinarySensorDeviceClass.TAMPER, entity_category=EntityCategory.DIAGNOSTIC, - name=f" {TAMPER}", is_on_fn=lambda zone: zone.is_tampered(), ), ) @@ -89,21 +87,18 @@ LOCATION_BINARY_SENSORS: tuple[TotalConnectAlarmBinarySensorEntityDescription, . key=LOW_BATTERY, device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, - name=" low battery", is_on_fn=lambda location: location.is_low_battery(), ), TotalConnectAlarmBinarySensorEntityDescription( key=TAMPER, device_class=BinarySensorDeviceClass.TAMPER, entity_category=EntityCategory.DIAGNOSTIC, - name=f" {TAMPER}", is_on_fn=lambda location: location.is_cover_tampered(), ), TotalConnectAlarmBinarySensorEntityDescription( key=POWER, device_class=BinarySensorDeviceClass.POWER, entity_category=EntityCategory.DIAGNOSTIC, - name=f" {POWER}", is_on_fn=lambda location: location.is_ac_loss(), ), ) @@ -161,7 +156,6 @@ class TotalConnectZoneBinarySensor(TotalConnectZoneEntity, BinarySensorEntity): """Initialize the TotalConnect status.""" super().__init__(coordinator, zone, location_id, entity_description.key) self.entity_description = entity_description - self._attr_name = f"{zone.description}{entity_description.name}" self._attr_extra_state_attributes = { "zone_id": zone.zoneid, "location_id": location_id, @@ -195,7 +189,6 @@ class TotalConnectAlarmBinarySensor(TotalConnectLocationEntity, BinarySensorEnti """Initialize the TotalConnect alarm device binary sensor.""" super().__init__(coordinator, location) self.entity_description = entity_description - self._attr_name = f"{self.device.name}{entity_description.name}" self._attr_unique_id = f"{location.location_id}_{entity_description.key}" self._attr_extra_state_attributes = { "location_id": location.location_id, diff --git a/homeassistant/components/totalconnect/entity.py b/homeassistant/components/totalconnect/entity.py index deef0c5aa2a..a18ffc14df5 100644 --- a/homeassistant/components/totalconnect/entity.py +++ b/homeassistant/components/totalconnect/entity.py @@ -12,6 +12,8 @@ from . import DOMAIN, TotalConnectDataUpdateCoordinator class TotalConnectEntity(CoordinatorEntity[TotalConnectDataUpdateCoordinator]): """Represent a TotalConnect entity.""" + _attr_has_entity_name = True + class TotalConnectLocationEntity(TotalConnectEntity): """Represent a TotalConnect location.""" @@ -24,11 +26,11 @@ class TotalConnectLocationEntity(TotalConnectEntity): """Initialize the TotalConnect location.""" super().__init__(coordinator) self._location = location - self.device = location.devices[location.security_device_id] + self.device = device = location.devices[location.security_device_id] self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, self.device.serial_number)}, - name=self.device.name, - serial_number=self.device.serial_number, + identifiers={(DOMAIN, device.serial_number)}, + name=device.name, + serial_number=device.serial_number, ) diff --git a/homeassistant/components/totalconnect/strings.json b/homeassistant/components/totalconnect/strings.json index 922962c9866..03656b60084 100644 --- a/homeassistant/components/totalconnect/strings.json +++ b/homeassistant/components/totalconnect/strings.json @@ -49,5 +49,12 @@ "name": "Arm home instant", "description": "Arms Home with zero entry delay." } + }, + "entity": { + "alarm_control_panel": { + "partition": { + "name": "Partition {partition_id}" + } + } } } diff --git a/tests/components/totalconnect/snapshots/test_alarm_control_panel.ambr b/tests/components/totalconnect/snapshots/test_alarm_control_panel.ambr index 4dc6b576ba3..8261cd74859 100644 --- a/tests/components/totalconnect/snapshots/test_alarm_control_panel.ambr +++ b/tests/components/totalconnect/snapshots/test_alarm_control_panel.ambr @@ -12,7 +12,7 @@ 'domain': 'alarm_control_panel', 'entity_category': None, 'entity_id': 'alarm_control_panel.test', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -23,7 +23,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'test', + 'original_name': None, 'platform': 'totalconnect', 'previous_unique_id': None, 'supported_features': , @@ -70,7 +70,7 @@ 'domain': 'alarm_control_panel', 'entity_category': None, 'entity_id': 'alarm_control_panel.test_partition_2', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -81,11 +81,11 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'test partition 2', + 'original_name': 'Partition 2', 'platform': 'totalconnect', 'previous_unique_id': None, 'supported_features': , - 'translation_key': None, + 'translation_key': 'partition', 'unique_id': '123456_2', 'unit_of_measurement': None, }) @@ -98,7 +98,7 @@ 'code_arm_required': True, 'code_format': None, 'cover_tampered': False, - 'friendly_name': 'test partition 2', + 'friendly_name': 'test Partition 2', 'location_id': '123456', 'location_name': 'test partition 2', 'low_battery': False, diff --git a/tests/components/totalconnect/snapshots/test_binary_sensor.ambr b/tests/components/totalconnect/snapshots/test_binary_sensor.ambr index a79f609488d..54089c6f192 100644 --- a/tests/components/totalconnect/snapshots/test_binary_sensor.ambr +++ b/tests/components/totalconnect/snapshots/test_binary_sensor.ambr @@ -12,7 +12,7 @@ 'domain': 'binary_sensor', 'entity_category': None, 'entity_id': 'binary_sensor.fire', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -23,7 +23,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Fire', + 'original_name': None, 'platform': 'totalconnect', 'previous_unique_id': None, 'supported_features': 0, @@ -49,7 +49,7 @@ 'state': 'off', }) # --- -# name: test_entity_registry[binary_sensor.fire_low_battery-entry] +# name: test_entity_registry[binary_sensor.fire_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -61,8 +61,8 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.fire_low_battery', - 'has_entity_name': False, + 'entity_id': 'binary_sensor.fire_battery', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -73,7 +73,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Fire low battery', + 'original_name': 'Battery', 'platform': 'totalconnect', 'previous_unique_id': None, 'supported_features': 0, @@ -82,17 +82,17 @@ 'unit_of_measurement': None, }) # --- -# name: test_entity_registry[binary_sensor.fire_low_battery-state] +# name: test_entity_registry[binary_sensor.fire_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', - 'friendly_name': 'Fire low battery', + 'friendly_name': 'Fire Battery', 'location_id': '123456', 'partition': '1', 'zone_id': '2', }), 'context': , - 'entity_id': 'binary_sensor.fire_low_battery', + 'entity_id': 'binary_sensor.fire_battery', 'last_changed': , 'last_reported': , 'last_updated': , @@ -112,7 +112,7 @@ 'domain': 'binary_sensor', 'entity_category': , 'entity_id': 'binary_sensor.fire_tamper', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -123,7 +123,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Fire tamper', + 'original_name': 'Tamper', 'platform': 'totalconnect', 'previous_unique_id': None, 'supported_features': 0, @@ -136,7 +136,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'tamper', - 'friendly_name': 'Fire tamper', + 'friendly_name': 'Fire Tamper', 'location_id': '123456', 'partition': '1', 'zone_id': '2', @@ -162,7 +162,7 @@ 'domain': 'binary_sensor', 'entity_category': None, 'entity_id': 'binary_sensor.gas', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -173,7 +173,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Gas', + 'original_name': None, 'platform': 'totalconnect', 'previous_unique_id': None, 'supported_features': 0, @@ -199,7 +199,7 @@ 'state': 'off', }) # --- -# name: test_entity_registry[binary_sensor.gas_low_battery-entry] +# name: test_entity_registry[binary_sensor.gas_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -211,8 +211,8 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.gas_low_battery', - 'has_entity_name': False, + 'entity_id': 'binary_sensor.gas_battery', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -223,7 +223,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Gas low battery', + 'original_name': 'Battery', 'platform': 'totalconnect', 'previous_unique_id': None, 'supported_features': 0, @@ -232,17 +232,17 @@ 'unit_of_measurement': None, }) # --- -# name: test_entity_registry[binary_sensor.gas_low_battery-state] +# name: test_entity_registry[binary_sensor.gas_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', - 'friendly_name': 'Gas low battery', + 'friendly_name': 'Gas Battery', 'location_id': '123456', 'partition': '1', 'zone_id': '3', }), 'context': , - 'entity_id': 'binary_sensor.gas_low_battery', + 'entity_id': 'binary_sensor.gas_battery', 'last_changed': , 'last_reported': , 'last_updated': , @@ -262,7 +262,7 @@ 'domain': 'binary_sensor', 'entity_category': , 'entity_id': 'binary_sensor.gas_tamper', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -273,7 +273,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Gas tamper', + 'original_name': 'Tamper', 'platform': 'totalconnect', 'previous_unique_id': None, 'supported_features': 0, @@ -286,7 +286,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'tamper', - 'friendly_name': 'Gas tamper', + 'friendly_name': 'Gas Tamper', 'location_id': '123456', 'partition': '1', 'zone_id': '3', @@ -312,7 +312,7 @@ 'domain': 'binary_sensor', 'entity_category': None, 'entity_id': 'binary_sensor.medical', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -323,7 +323,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Medical', + 'original_name': None, 'platform': 'totalconnect', 'previous_unique_id': None, 'supported_features': 0, @@ -362,7 +362,7 @@ 'domain': 'binary_sensor', 'entity_category': None, 'entity_id': 'binary_sensor.motion', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -373,7 +373,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Motion', + 'original_name': None, 'platform': 'totalconnect', 'previous_unique_id': None, 'supported_features': 0, @@ -399,7 +399,7 @@ 'state': 'off', }) # --- -# name: test_entity_registry[binary_sensor.motion_low_battery-entry] +# name: test_entity_registry[binary_sensor.motion_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -411,8 +411,8 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.motion_low_battery', - 'has_entity_name': False, + 'entity_id': 'binary_sensor.motion_battery', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -423,7 +423,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Motion low battery', + 'original_name': 'Battery', 'platform': 'totalconnect', 'previous_unique_id': None, 'supported_features': 0, @@ -432,17 +432,17 @@ 'unit_of_measurement': None, }) # --- -# name: test_entity_registry[binary_sensor.motion_low_battery-state] +# name: test_entity_registry[binary_sensor.motion_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', - 'friendly_name': 'Motion low battery', + 'friendly_name': 'Motion Battery', 'location_id': '123456', 'partition': '1', 'zone_id': '4', }), 'context': , - 'entity_id': 'binary_sensor.motion_low_battery', + 'entity_id': 'binary_sensor.motion_battery', 'last_changed': , 'last_reported': , 'last_updated': , @@ -462,7 +462,7 @@ 'domain': 'binary_sensor', 'entity_category': , 'entity_id': 'binary_sensor.motion_tamper', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -473,7 +473,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Motion tamper', + 'original_name': 'Tamper', 'platform': 'totalconnect', 'previous_unique_id': None, 'supported_features': 0, @@ -486,7 +486,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'tamper', - 'friendly_name': 'Motion tamper', + 'friendly_name': 'Motion Tamper', 'location_id': '123456', 'partition': '1', 'zone_id': '4', @@ -512,7 +512,7 @@ 'domain': 'binary_sensor', 'entity_category': None, 'entity_id': 'binary_sensor.security', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -523,7 +523,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Security', + 'original_name': None, 'platform': 'totalconnect', 'previous_unique_id': None, 'supported_features': 0, @@ -549,7 +549,7 @@ 'state': 'on', }) # --- -# name: test_entity_registry[binary_sensor.security_low_battery-entry] +# name: test_entity_registry[binary_sensor.security_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -561,8 +561,8 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.security_low_battery', - 'has_entity_name': False, + 'entity_id': 'binary_sensor.security_battery', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -573,7 +573,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Security low battery', + 'original_name': 'Battery', 'platform': 'totalconnect', 'previous_unique_id': None, 'supported_features': 0, @@ -582,17 +582,17 @@ 'unit_of_measurement': None, }) # --- -# name: test_entity_registry[binary_sensor.security_low_battery-state] +# name: test_entity_registry[binary_sensor.security_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', - 'friendly_name': 'Security low battery', + 'friendly_name': 'Security Battery', 'location_id': '123456', 'partition': '1', 'zone_id': '1', }), 'context': , - 'entity_id': 'binary_sensor.security_low_battery', + 'entity_id': 'binary_sensor.security_battery', 'last_changed': , 'last_reported': , 'last_updated': , @@ -612,7 +612,7 @@ 'domain': 'binary_sensor', 'entity_category': , 'entity_id': 'binary_sensor.security_tamper', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -623,7 +623,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Security tamper', + 'original_name': 'Tamper', 'platform': 'totalconnect', 'previous_unique_id': None, 'supported_features': 0, @@ -636,7 +636,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'tamper', - 'friendly_name': 'Security tamper', + 'friendly_name': 'Security Tamper', 'location_id': '123456', 'partition': '1', 'zone_id': '1', @@ -662,7 +662,7 @@ 'domain': 'binary_sensor', 'entity_category': None, 'entity_id': 'binary_sensor.temperature', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -673,7 +673,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Temperature', + 'original_name': None, 'platform': 'totalconnect', 'previous_unique_id': None, 'supported_features': 0, @@ -699,7 +699,7 @@ 'state': 'off', }) # --- -# name: test_entity_registry[binary_sensor.temperature_low_battery-entry] +# name: test_entity_registry[binary_sensor.temperature_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -711,8 +711,8 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.temperature_low_battery', - 'has_entity_name': False, + 'entity_id': 'binary_sensor.temperature_battery', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -723,7 +723,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Temperature low battery', + 'original_name': 'Battery', 'platform': 'totalconnect', 'previous_unique_id': None, 'supported_features': 0, @@ -732,17 +732,17 @@ 'unit_of_measurement': None, }) # --- -# name: test_entity_registry[binary_sensor.temperature_low_battery-state] +# name: test_entity_registry[binary_sensor.temperature_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', - 'friendly_name': 'Temperature low battery', + 'friendly_name': 'Temperature Battery', 'location_id': '123456', 'partition': '1', 'zone_id': 7, }), 'context': , - 'entity_id': 'binary_sensor.temperature_low_battery', + 'entity_id': 'binary_sensor.temperature_battery', 'last_changed': , 'last_reported': , 'last_updated': , @@ -762,7 +762,7 @@ 'domain': 'binary_sensor', 'entity_category': , 'entity_id': 'binary_sensor.temperature_tamper', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -773,7 +773,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Temperature tamper', + 'original_name': 'Tamper', 'platform': 'totalconnect', 'previous_unique_id': None, 'supported_features': 0, @@ -786,7 +786,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'tamper', - 'friendly_name': 'Temperature tamper', + 'friendly_name': 'Temperature Tamper', 'location_id': '123456', 'partition': '1', 'zone_id': 7, @@ -799,7 +799,7 @@ 'state': 'off', }) # --- -# name: test_entity_registry[binary_sensor.test_low_battery-entry] +# name: test_entity_registry[binary_sensor.test_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -811,8 +811,8 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.test_low_battery', - 'has_entity_name': False, + 'entity_id': 'binary_sensor.test_battery', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -823,7 +823,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'test low battery', + 'original_name': 'Battery', 'platform': 'totalconnect', 'previous_unique_id': None, 'supported_features': 0, @@ -832,15 +832,15 @@ 'unit_of_measurement': None, }) # --- -# name: test_entity_registry[binary_sensor.test_low_battery-state] +# name: test_entity_registry[binary_sensor.test_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', - 'friendly_name': 'test low battery', + 'friendly_name': 'test Battery', 'location_id': '123456', }), 'context': , - 'entity_id': 'binary_sensor.test_low_battery', + 'entity_id': 'binary_sensor.test_battery', 'last_changed': , 'last_reported': , 'last_updated': , @@ -860,7 +860,7 @@ 'domain': 'binary_sensor', 'entity_category': , 'entity_id': 'binary_sensor.test_power', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -871,7 +871,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'test power', + 'original_name': 'Power', 'platform': 'totalconnect', 'previous_unique_id': None, 'supported_features': 0, @@ -884,7 +884,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power', - 'friendly_name': 'test power', + 'friendly_name': 'test Power', 'location_id': '123456', }), 'context': , @@ -908,7 +908,7 @@ 'domain': 'binary_sensor', 'entity_category': , 'entity_id': 'binary_sensor.test_tamper', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -919,7 +919,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'test tamper', + 'original_name': 'Tamper', 'platform': 'totalconnect', 'previous_unique_id': None, 'supported_features': 0, @@ -932,7 +932,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'tamper', - 'friendly_name': 'test tamper', + 'friendly_name': 'test Tamper', 'location_id': '123456', }), 'context': , @@ -956,7 +956,7 @@ 'domain': 'binary_sensor', 'entity_category': None, 'entity_id': 'binary_sensor.unknown', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -967,7 +967,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Unknown', + 'original_name': None, 'platform': 'totalconnect', 'previous_unique_id': None, 'supported_features': 0, @@ -993,7 +993,7 @@ 'state': 'off', }) # --- -# name: test_entity_registry[binary_sensor.unknown_low_battery-entry] +# name: test_entity_registry[binary_sensor.unknown_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1005,8 +1005,8 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.unknown_low_battery', - 'has_entity_name': False, + 'entity_id': 'binary_sensor.unknown_battery', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -1017,7 +1017,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Unknown low battery', + 'original_name': 'Battery', 'platform': 'totalconnect', 'previous_unique_id': None, 'supported_features': 0, @@ -1026,17 +1026,17 @@ 'unit_of_measurement': None, }) # --- -# name: test_entity_registry[binary_sensor.unknown_low_battery-state] +# name: test_entity_registry[binary_sensor.unknown_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', - 'friendly_name': 'Unknown low battery', + 'friendly_name': 'Unknown Battery', 'location_id': '123456', 'partition': '1', 'zone_id': '6', }), 'context': , - 'entity_id': 'binary_sensor.unknown_low_battery', + 'entity_id': 'binary_sensor.unknown_battery', 'last_changed': , 'last_reported': , 'last_updated': , @@ -1056,7 +1056,7 @@ 'domain': 'binary_sensor', 'entity_category': , 'entity_id': 'binary_sensor.unknown_tamper', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -1067,7 +1067,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Unknown tamper', + 'original_name': 'Tamper', 'platform': 'totalconnect', 'previous_unique_id': None, 'supported_features': 0, @@ -1080,7 +1080,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'tamper', - 'friendly_name': 'Unknown tamper', + 'friendly_name': 'Unknown Tamper', 'location_id': '123456', 'partition': '1', 'zone_id': '6', diff --git a/tests/components/totalconnect/test_binary_sensor.py b/tests/components/totalconnect/test_binary_sensor.py index 1a8a65391f5..dc433129ac8 100644 --- a/tests/components/totalconnect/test_binary_sensor.py +++ b/tests/components/totalconnect/test_binary_sensor.py @@ -17,9 +17,9 @@ from .common import RESPONSE_DISARMED, ZONE_NORMAL, setup_platform from tests.common import snapshot_platform ZONE_ENTITY_ID = "binary_sensor.security" -ZONE_LOW_BATTERY_ID = "binary_sensor.security_low_battery" +ZONE_LOW_BATTERY_ID = "binary_sensor.security_battery" ZONE_TAMPER_ID = "binary_sensor.security_tamper" -PANEL_BATTERY_ID = "binary_sensor.test_low_battery" +PANEL_BATTERY_ID = "binary_sensor.test_battery" PANEL_TAMPER_ID = "binary_sensor.test_tamper" PANEL_POWER_ID = "binary_sensor.test_power" @@ -49,7 +49,7 @@ async def test_state_and_attributes(hass: HomeAssistant) -> None: ) assert state.attributes.get("device_class") == BinarySensorDeviceClass.DOOR - state = hass.states.get(f"{ZONE_ENTITY_ID}_low_battery") + state = hass.states.get(f"{ZONE_ENTITY_ID}_battery") assert state.state == STATE_OFF state = hass.states.get(f"{ZONE_ENTITY_ID}_tamper") assert state.state == STATE_OFF @@ -58,7 +58,7 @@ async def test_state_and_attributes(hass: HomeAssistant) -> None: state = hass.states.get("binary_sensor.fire") assert state.state == STATE_OFF assert state.attributes.get("device_class") == BinarySensorDeviceClass.SMOKE - state = hass.states.get("binary_sensor.fire_low_battery") + state = hass.states.get("binary_sensor.fire_battery") assert state.state == STATE_ON state = hass.states.get("binary_sensor.fire_tamper") assert state.state == STATE_OFF @@ -67,7 +67,7 @@ async def test_state_and_attributes(hass: HomeAssistant) -> None: state = hass.states.get("binary_sensor.gas") assert state.state == STATE_OFF assert state.attributes.get("device_class") == BinarySensorDeviceClass.GAS - state = hass.states.get("binary_sensor.gas_low_battery") + state = hass.states.get("binary_sensor.gas_battery") assert state.state == STATE_OFF state = hass.states.get("binary_sensor.gas_tamper") assert state.state == STATE_ON @@ -76,7 +76,7 @@ async def test_state_and_attributes(hass: HomeAssistant) -> None: state = hass.states.get("binary_sensor.unknown") assert state.state == STATE_OFF assert state.attributes.get("device_class") == BinarySensorDeviceClass.DOOR - state = hass.states.get("binary_sensor.unknown_low_battery") + state = hass.states.get("binary_sensor.unknown_battery") assert state.state == STATE_OFF state = hass.states.get("binary_sensor.unknown_tamper") assert state.state == STATE_OFF @@ -85,7 +85,7 @@ async def test_state_and_attributes(hass: HomeAssistant) -> None: state = hass.states.get("binary_sensor.temperature") assert state.state == STATE_OFF assert state.attributes.get("device_class") == BinarySensorDeviceClass.PROBLEM - state = hass.states.get("binary_sensor.temperature_low_battery") + state = hass.states.get("binary_sensor.temperature_battery") assert state.state == STATE_OFF state = hass.states.get("binary_sensor.temperature_tamper") assert state.state == STATE_OFF From d08bb96d00f0377180e09152f3d30c95f5cbf7c2 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 23 Apr 2024 21:59:03 +0200 Subject: [PATCH 798/967] Deprecate Unify Circuit integration (#115528) Co-authored-by: TheJulianJES --- homeassistant/components/circuit/__init__.py | 12 ++++++++++++ homeassistant/components/circuit/strings.json | 8 ++++++++ 2 files changed, 20 insertions(+) create mode 100644 homeassistant/components/circuit/strings.json diff --git a/homeassistant/components/circuit/__init__.py b/homeassistant/components/circuit/__init__.py index f71babad3d5..7e7d0eda76e 100644 --- a/homeassistant/components/circuit/__init__.py +++ b/homeassistant/components/circuit/__init__.py @@ -5,6 +5,7 @@ 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" @@ -26,6 +27,17 @@ CONFIG_SCHEMA = vol.Schema( 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: diff --git a/homeassistant/components/circuit/strings.json b/homeassistant/components/circuit/strings.json new file mode 100644 index 00000000000..b9cb852d5b9 --- /dev/null +++ b/homeassistant/components/circuit/strings.json @@ -0,0 +1,8 @@ +{ + "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." + } + } +} From fd08b7281ec06ceb1d48f4916601c4d30de78ef0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 23 Apr 2024 22:07:16 +0200 Subject: [PATCH 799/967] Convert solaredge to asyncio with aiosolaredge (#115599) --- CODEOWNERS | 4 +-- .../components/solaredge/__init__.py | 14 ++++---- .../components/solaredge/config_flow.py | 19 +++++----- .../components/solaredge/coordinator.py | 35 ++++++++----------- .../components/solaredge/manifest.json | 6 ++-- homeassistant/components/solaredge/sensor.py | 6 ++-- requirements_all.txt | 6 ++-- requirements_test_all.txt | 6 ++-- .../components/solaredge/test_config_flow.py | 15 ++++---- .../components/solaredge/test_coordinator.py | 18 +++++----- 10 files changed, 65 insertions(+), 64 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 5dcf4b3df81..c8a391fd7dc 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1286,8 +1286,8 @@ build.json @home-assistant/supervisor /tests/components/snmp/ @nmaggioni /homeassistant/components/snooz/ @AustinBrunkhorst /tests/components/snooz/ @AustinBrunkhorst -/homeassistant/components/solaredge/ @frenck -/tests/components/solaredge/ @frenck +/homeassistant/components/solaredge/ @frenck @bdraco +/tests/components/solaredge/ @frenck @bdraco /homeassistant/components/solaredge_local/ @drobtravels @scheric /homeassistant/components/solarlog/ @Ernst79 /tests/components/solarlog/ @Ernst79 diff --git a/homeassistant/components/solaredge/__init__.py b/homeassistant/components/solaredge/__init__.py index 69e02c1875c..64f76372e91 100644 --- a/homeassistant/components/solaredge/__init__.py +++ b/homeassistant/components/solaredge/__init__.py @@ -4,13 +4,14 @@ from __future__ import annotations import socket -from requests.exceptions import ConnectTimeout, HTTPError -from solaredge import Solaredge +from aiohttp import ClientError +from aiosolaredge import SolarEdge from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from .const import CONF_SITE_ID, DATA_API_CLIENT, DOMAIN, LOGGER @@ -22,13 +23,12 @@ PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up SolarEdge from a config entry.""" - api = Solaredge(entry.data[CONF_API_KEY]) + session = async_get_clientsession(hass) + api = SolarEdge(entry.data[CONF_API_KEY], session) try: - response = await hass.async_add_executor_job( - api.get_details, entry.data[CONF_SITE_ID] - ) - except (ConnectTimeout, HTTPError, socket.gaierror) as ex: + response = await api.get_details(entry.data[CONF_SITE_ID]) + except (TimeoutError, ClientError, socket.gaierror) as ex: LOGGER.error("Could not retrieve details from SolarEdge API") raise ConfigEntryNotReady from ex diff --git a/homeassistant/components/solaredge/config_flow.py b/homeassistant/components/solaredge/config_flow.py index b75af866549..6235e22400f 100644 --- a/homeassistant/components/solaredge/config_flow.py +++ b/homeassistant/components/solaredge/config_flow.py @@ -2,15 +2,17 @@ from __future__ import annotations +import socket from typing import Any -from requests.exceptions import ConnectTimeout, HTTPError -import solaredge +from aiohttp import ClientError +import aiosolaredge import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_KEY, CONF_NAME from homeassistant.core import callback +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import slugify from .const import CONF_SITE_ID, DEFAULT_NAME, DOMAIN @@ -38,15 +40,16 @@ class SolarEdgeConfigFlow(ConfigFlow, domain=DOMAIN): """Return True if site_id exists in configuration.""" return site_id in self._async_current_site_ids() - def _check_site(self, site_id: str, api_key: str) -> bool: + async def _async_check_site(self, site_id: str, api_key: str) -> bool: """Check if we can connect to the soleredge api service.""" - api = solaredge.Solaredge(api_key) + session = async_get_clientsession(self.hass) + api = aiosolaredge.SolarEdge(api_key, session) try: - response = api.get_details(site_id) + response = await api.get_details(site_id) if response["details"]["status"].lower() != "active": self._errors[CONF_SITE_ID] = "site_not_active" return False - except (ConnectTimeout, HTTPError): + except (TimeoutError, ClientError, socket.gaierror): self._errors[CONF_SITE_ID] = "could_not_connect" return False except KeyError: @@ -66,9 +69,7 @@ class SolarEdgeConfigFlow(ConfigFlow, domain=DOMAIN): else: site = user_input[CONF_SITE_ID] api = user_input[CONF_API_KEY] - can_connect = await self.hass.async_add_executor_job( - self._check_site, site, api - ) + can_connect = await self._async_check_site(site, api) if can_connect: return self.async_create_entry( title=name, data={CONF_SITE_ID: site, CONF_API_KEY: api} diff --git a/homeassistant/components/solaredge/coordinator.py b/homeassistant/components/solaredge/coordinator.py index d2da99820d7..0c264c1c514 100644 --- a/homeassistant/components/solaredge/coordinator.py +++ b/homeassistant/components/solaredge/coordinator.py @@ -6,7 +6,7 @@ from abc import ABC, abstractmethod from datetime import date, datetime, timedelta from typing import Any -from solaredge import Solaredge +from aiosolaredge import SolarEdge from stringcase import snakecase from homeassistant.core import HomeAssistant, callback @@ -27,7 +27,7 @@ class SolarEdgeDataService(ABC): coordinator: DataUpdateCoordinator[None] - def __init__(self, hass: HomeAssistant, api: Solaredge, site_id: str) -> None: + def __init__(self, hass: HomeAssistant, api: SolarEdge, site_id: str) -> None: """Initialize the data object.""" self.api = api self.site_id = site_id @@ -54,12 +54,8 @@ class SolarEdgeDataService(ABC): """Update interval.""" @abstractmethod - def update(self) -> None: - """Update data in executor.""" - async def async_update_data(self) -> None: """Update data.""" - await self.hass.async_add_executor_job(self.update) class SolarEdgeOverviewDataService(SolarEdgeDataService): @@ -70,10 +66,10 @@ class SolarEdgeOverviewDataService(SolarEdgeDataService): """Update interval.""" return OVERVIEW_UPDATE_DELAY - def update(self) -> None: + async def async_update_data(self) -> None: """Update the data from the SolarEdge Monitoring API.""" try: - data = self.api.get_overview(self.site_id) + data = await self.api.get_overview(self.site_id) overview = data["overview"] except KeyError as ex: raise UpdateFailed("Missing overview data, skipping update") from ex @@ -113,11 +109,11 @@ class SolarEdgeDetailsDataService(SolarEdgeDataService): """Update interval.""" return DETAILS_UPDATE_DELAY - def update(self) -> None: + async def async_update_data(self) -> None: """Update the data from the SolarEdge Monitoring API.""" try: - data = self.api.get_details(self.site_id) + data = await self.api.get_details(self.site_id) details = data["details"] except KeyError as ex: raise UpdateFailed("Missing details data, skipping update") from ex @@ -157,10 +153,10 @@ class SolarEdgeInventoryDataService(SolarEdgeDataService): """Update interval.""" return INVENTORY_UPDATE_DELAY - def update(self) -> None: + async def async_update_data(self) -> None: """Update the data from the SolarEdge Monitoring API.""" try: - data = self.api.get_inventory(self.site_id) + data = await self.api.get_inventory(self.site_id) inventory = data["Inventory"] except KeyError as ex: raise UpdateFailed("Missing inventory data, skipping update") from ex @@ -178,7 +174,7 @@ class SolarEdgeInventoryDataService(SolarEdgeDataService): class SolarEdgeEnergyDetailsService(SolarEdgeDataService): """Get and update the latest power flow data.""" - def __init__(self, hass: HomeAssistant, api: Solaredge, site_id: str) -> None: + def __init__(self, hass: HomeAssistant, api: SolarEdge, site_id: str) -> None: """Initialize the power flow data service.""" super().__init__(hass, api, site_id) @@ -189,17 +185,16 @@ class SolarEdgeEnergyDetailsService(SolarEdgeDataService): """Update interval.""" return ENERGY_DETAILS_DELAY - def update(self) -> None: + async def async_update_data(self) -> None: """Update the data from the SolarEdge Monitoring API.""" try: now = datetime.now() today = date.today() midnight = datetime.combine(today, datetime.min.time()) - data = self.api.get_energy_details( + data = await self.api.get_energy_details( self.site_id, midnight, - now.strftime("%Y-%m-%d %H:%M:%S"), - meters=None, + now, time_unit="DAY", ) energy_details = data["energyDetails"] @@ -239,7 +234,7 @@ class SolarEdgeEnergyDetailsService(SolarEdgeDataService): class SolarEdgePowerFlowDataService(SolarEdgeDataService): """Get and update the latest power flow data.""" - def __init__(self, hass: HomeAssistant, api: Solaredge, site_id: str) -> None: + def __init__(self, hass: HomeAssistant, api: SolarEdge, site_id: str) -> None: """Initialize the power flow data service.""" super().__init__(hass, api, site_id) @@ -250,10 +245,10 @@ class SolarEdgePowerFlowDataService(SolarEdgeDataService): """Update interval.""" return POWER_FLOW_UPDATE_DELAY - def update(self) -> None: + async def async_update_data(self) -> None: """Update the data from the SolarEdge Monitoring API.""" try: - data = self.api.get_current_power_flow(self.site_id) + data = await self.api.get_current_power_flow(self.site_id) power_flow = data["siteCurrentPowerFlow"] except KeyError as ex: raise UpdateFailed("Missing power flow data, skipping update") from ex diff --git a/homeassistant/components/solaredge/manifest.json b/homeassistant/components/solaredge/manifest.json index 22759b1be7c..02f96c0211f 100644 --- a/homeassistant/components/solaredge/manifest.json +++ b/homeassistant/components/solaredge/manifest.json @@ -1,7 +1,7 @@ { "domain": "solaredge", "name": "SolarEdge", - "codeowners": ["@frenck"], + "codeowners": ["@frenck", "@bdraco"], "config_flow": true, "dhcp": [ { @@ -12,6 +12,6 @@ "documentation": "https://www.home-assistant.io/integrations/solaredge", "integration_type": "device", "iot_class": "cloud_polling", - "loggers": ["solaredge"], - "requirements": ["solaredge==0.0.2", "stringcase==1.2.0"] + "loggers": ["aiosolaredge"], + "requirements": ["aiosolaredge==0.2.0", "stringcase==1.2.0"] } diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py index 5ec65a3b9a5..b3345d5dc86 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -5,7 +5,7 @@ from __future__ import annotations from dataclasses import dataclass from typing import Any -from solaredge import Solaredge +from aiosolaredge import SolarEdge from homeassistant.components.sensor import ( SensorDeviceClass, @@ -205,7 +205,7 @@ async def async_setup_entry( ) -> None: """Add an solarEdge entry.""" # Add the needed sensors to hass - api: Solaredge = hass.data[DOMAIN][entry.entry_id][DATA_API_CLIENT] + api: SolarEdge = hass.data[DOMAIN][entry.entry_id][DATA_API_CLIENT] sensor_factory = SolarEdgeSensorFactory(hass, entry.data[CONF_SITE_ID], api) for service in sensor_factory.all_services: @@ -223,7 +223,7 @@ async def async_setup_entry( class SolarEdgeSensorFactory: """Factory which creates sensors based on the sensor_key.""" - def __init__(self, hass: HomeAssistant, site_id: str, api: Solaredge) -> None: + def __init__(self, hass: HomeAssistant, site_id: str, api: SolarEdge) -> None: """Initialize the factory.""" details = SolarEdgeDetailsDataService(hass, api, site_id) diff --git a/requirements_all.txt b/requirements_all.txt index f212a8675e8..d51dc0225ed 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -367,6 +367,9 @@ aioskybell==22.7.0 # homeassistant.components.slimproto aioslimproto==3.0.0 +# homeassistant.components.solaredge +aiosolaredge==0.2.0 + # homeassistant.components.steamist aiosteamist==0.3.2 @@ -2574,9 +2577,6 @@ soco==0.30.3 # homeassistant.components.solaredge_local solaredge-local==0.2.3 -# homeassistant.components.solaredge -solaredge==0.0.2 - # homeassistant.components.solax solax==3.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8f318a24b5e..7b04e7cf037 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -340,6 +340,9 @@ aioskybell==22.7.0 # homeassistant.components.slimproto aioslimproto==3.0.0 +# homeassistant.components.solaredge +aiosolaredge==0.2.0 + # homeassistant.components.steamist aiosteamist==0.3.2 @@ -1990,9 +1993,6 @@ snapcast==2.3.6 # homeassistant.components.sonos soco==0.30.3 -# homeassistant.components.solaredge -solaredge==0.0.2 - # homeassistant.components.solax solax==3.1.0 diff --git a/tests/components/solaredge/test_config_flow.py b/tests/components/solaredge/test_config_flow.py index 9ff605a871d..759a4d6b421 100644 --- a/tests/components/solaredge/test_config_flow.py +++ b/tests/components/solaredge/test_config_flow.py @@ -1,9 +1,9 @@ """Tests for the SolarEdge config flow.""" -from unittest.mock import Mock, patch +from unittest.mock import AsyncMock, Mock, patch +from aiohttp import ClientError import pytest -from requests.exceptions import ConnectTimeout, HTTPError from homeassistant.components.solaredge.const import CONF_SITE_ID, DEFAULT_NAME, DOMAIN from homeassistant.config_entries import SOURCE_IGNORE, SOURCE_USER @@ -22,8 +22,11 @@ API_KEY = "a1b2c3d4e5f6g7h8" def mock_controller(): """Mock a successful Solaredge API.""" api = Mock() - api.get_details.return_value = {"details": {"status": "active"}} - with patch("solaredge.Solaredge", return_value=api): + api.get_details = AsyncMock(return_value={"details": {"status": "active"}}) + with patch( + "homeassistant.components.solaredge.config_flow.aiosolaredge.SolarEdge", + return_value=api, + ): yield api @@ -117,7 +120,7 @@ async def test_asserts(hass: HomeAssistant, test_api: Mock) -> None: assert result.get("errors") == {CONF_SITE_ID: "invalid_api_key"} # test with ConnectionTimeout - test_api.get_details.side_effect = ConnectTimeout() + test_api.get_details = AsyncMock(side_effect=TimeoutError()) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, @@ -127,7 +130,7 @@ async def test_asserts(hass: HomeAssistant, test_api: Mock) -> None: assert result.get("errors") == {CONF_SITE_ID: "could_not_connect"} # test with HTTPError - test_api.get_details.side_effect = HTTPError() + test_api.get_details = AsyncMock(side_effect=ClientError()) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, diff --git a/tests/components/solaredge/test_coordinator.py b/tests/components/solaredge/test_coordinator.py index b1496d18d93..7a6b3af1cde 100644 --- a/tests/components/solaredge/test_coordinator.py +++ b/tests/components/solaredge/test_coordinator.py @@ -1,6 +1,6 @@ """Tests for the SolarEdge coordinator services.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, patch from freezegun.api import FrozenDateTimeFactory import pytest @@ -25,7 +25,7 @@ def enable_all_entities(entity_registry_enabled_by_default): """Make sure all entities are enabled.""" -@patch("homeassistant.components.solaredge.Solaredge") +@patch("homeassistant.components.solaredge.SolarEdge") async def test_solaredgeoverviewdataservice_energy_values_validity( mock_solaredge, hass: HomeAssistant, freezer: FrozenDateTimeFactory ) -> None: @@ -35,7 +35,9 @@ async def test_solaredgeoverviewdataservice_energy_values_validity( title=DEFAULT_NAME, data={CONF_NAME: DEFAULT_NAME, CONF_SITE_ID: SITE_ID, CONF_API_KEY: API_KEY}, ) - mock_solaredge().get_details.return_value = {"details": {"status": "active"}} + mock_solaredge().get_details = AsyncMock( + return_value={"details": {"status": "active"}} + ) mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) @@ -50,7 +52,7 @@ async def test_solaredgeoverviewdataservice_energy_values_validity( "currentPower": {"power": 0.0}, } } - mock_solaredge().get_overview.return_value = mock_overview_data + mock_solaredge().get_overview = AsyncMock(return_value=mock_overview_data) freezer.tick(OVERVIEW_UPDATE_DELAY) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) @@ -60,7 +62,7 @@ async def test_solaredgeoverviewdataservice_energy_values_validity( # Invalid energy values, lifeTimeData energy is lower than last year, month or day. mock_overview_data["overview"]["lifeTimeData"]["energy"] = 0 - mock_solaredge().get_overview.return_value = mock_overview_data + mock_solaredge().get_overview = AsyncMock(return_value=mock_overview_data) freezer.tick(OVERVIEW_UPDATE_DELAY) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) @@ -71,7 +73,7 @@ async def test_solaredgeoverviewdataservice_energy_values_validity( # New valid energy values update mock_overview_data["overview"]["lifeTimeData"]["energy"] = 100001 - mock_solaredge().get_overview.return_value = mock_overview_data + mock_solaredge().get_overview = AsyncMock(return_value=mock_overview_data) freezer.tick(OVERVIEW_UPDATE_DELAY) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) @@ -82,7 +84,7 @@ async def test_solaredgeoverviewdataservice_energy_values_validity( # Invalid energy values, lastYearData energy is lower than last month or day. mock_overview_data["overview"]["lastYearData"]["energy"] = 0 - mock_solaredge().get_overview.return_value = mock_overview_data + mock_solaredge().get_overview = AsyncMock(return_value=mock_overview_data) freezer.tick(OVERVIEW_UPDATE_DELAY) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) @@ -100,7 +102,7 @@ async def test_solaredgeoverviewdataservice_energy_values_validity( mock_overview_data["overview"]["lastYearData"]["energy"] = 0.0 mock_overview_data["overview"]["lastMonthData"]["energy"] = 0.0 mock_overview_data["overview"]["lastDayData"]["energy"] = 0.0 - mock_solaredge().get_overview.return_value = mock_overview_data + mock_solaredge().get_overview = AsyncMock(return_value=mock_overview_data) freezer.tick(OVERVIEW_UPDATE_DELAY) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) From a33aacfcaa109257ea5c83d39531a1efa9485e4a Mon Sep 17 00:00:00 2001 From: Nalin Mahajan Date: Tue, 23 Apr 2024 15:10:16 -0500 Subject: [PATCH 800/967] Add Retry for C4 API due to flakiness (#113857) Co-authored-by: nalin29 --- homeassistant/components/control4/__init__.py | 26 ++++++++++++++----- homeassistant/components/control4/const.py | 2 ++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/control4/__init__.py b/homeassistant/components/control4/__init__.py index b8d195fcb05..4b24ac6bf77 100644 --- a/homeassistant/components/control4/__init__.py +++ b/homeassistant/components/control4/__init__.py @@ -30,6 +30,7 @@ from homeassistant.helpers.update_coordinator import ( ) from .const import ( + API_RETRY_TIMES, CONF_ACCOUNT, CONF_CONFIG_LISTENER, CONF_CONTROLLER_UNIQUE_ID, @@ -47,6 +48,18 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.LIGHT, Platform.MEDIA_PLAYER] +async def call_c4_api_retry(func, *func_args): + """Call C4 API function and retry on failure.""" + for i in range(API_RETRY_TIMES): + try: + output = await func(*func_args) + return output + except client_exceptions.ClientError as exception: + _LOGGER.error("Error connecting to Control4 account API: %s", exception) + if i == API_RETRY_TIMES - 1: + raise ConfigEntryNotReady(exception) from exception + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Control4 from a config entry.""" hass.data.setdefault(DOMAIN, {}) @@ -74,18 +87,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: controller_unique_id = config[CONF_CONTROLLER_UNIQUE_ID] entry_data[CONF_CONTROLLER_UNIQUE_ID] = controller_unique_id - director_token_dict = await account.getDirectorBearerToken(controller_unique_id) - director_session = aiohttp_client.async_get_clientsession(hass, verify_ssl=False) + director_token_dict = await call_c4_api_retry( + account.getDirectorBearerToken, controller_unique_id + ) + director_session = aiohttp_client.async_get_clientsession(hass, verify_ssl=False) director = C4Director( config[CONF_HOST], director_token_dict[CONF_TOKEN], director_session ) entry_data[CONF_DIRECTOR] = director - # Add Control4 controller to device registry - controller_href = (await account.getAccountControllers())["href"] - entry_data[CONF_DIRECTOR_SW_VERSION] = await account.getControllerOSVersion( - controller_href + controller_href = (await call_c4_api_retry(account.getAccountControllers))["href"] + entry_data[CONF_DIRECTOR_SW_VERSION] = await call_c4_api_retry( + account.getControllerOSVersion, controller_href ) _, model, mac_address = controller_unique_id.split("_", 3) diff --git a/homeassistant/components/control4/const.py b/homeassistant/components/control4/const.py index f8d939e1ac5..57074c00108 100644 --- a/homeassistant/components/control4/const.py +++ b/homeassistant/components/control4/const.py @@ -5,6 +5,8 @@ DOMAIN = "control4" DEFAULT_SCAN_INTERVAL = 5 MIN_SCAN_INTERVAL = 1 +API_RETRY_TIMES = 5 + CONF_ACCOUNT = "account" CONF_DIRECTOR = "director" CONF_DIRECTOR_SW_VERSION = "director_sw_version" From f249a9ba4bdf54f561108a58297b28ded1ce1b1c Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Wed, 24 Apr 2024 06:11:41 +1000 Subject: [PATCH 801/967] Add API scope checks to Teslemetry (#113640) --- .../components/teslemetry/__init__.py | 8 +++-- .../components/teslemetry/climate.py | 23 +++++++++++++- homeassistant/components/teslemetry/entity.py | 7 ++++- homeassistant/components/teslemetry/models.py | 2 ++ tests/components/teslemetry/conftest.py | 18 ++++++++++- tests/components/teslemetry/const.py | 18 +++++++++++ tests/components/teslemetry/test_climate.py | 31 +++++++++++++++++-- 7 files changed, 99 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/teslemetry/__init__.py b/homeassistant/components/teslemetry/__init__.py index 084d51ff31b..45fd1eee327 100644 --- a/homeassistant/components/teslemetry/__init__.py +++ b/homeassistant/components/teslemetry/__init__.py @@ -4,6 +4,7 @@ import asyncio from typing import Final from tesla_fleet_api import EnergySpecific, Teslemetry, VehicleSpecific +from tesla_fleet_api.const import Scope from tesla_fleet_api.exceptions import ( InvalidToken, SubscriptionRequired, @@ -37,6 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: access_token=access_token, ) try: + scopes = (await teslemetry.metadata())["scopes"] products = (await teslemetry.products())["response"] except InvalidToken as e: raise ConfigEntryAuthFailed from e @@ -49,7 +51,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: vehicles: list[TeslemetryVehicleData] = [] energysites: list[TeslemetryEnergyData] = [] for product in products: - if "vin" in product: + if "vin" in product and Scope.VEHICLE_DEVICE_DATA in scopes: vin = product["vin"] api = VehicleSpecific(teslemetry.vehicle, vin) coordinator = TeslemetryVehicleDataCoordinator(hass, api) @@ -60,7 +62,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: vin=vin, ) ) - elif "energy_site_id" in product: + elif "energy_site_id" in product and Scope.ENERGY_DEVICE_DATA in scopes: site_id = product["energy_site_id"] api = EnergySpecific(teslemetry.energy, site_id) energysites.append( @@ -86,7 +88,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Setup Platforms hass.data.setdefault(DOMAIN, {})[entry.entry_id] = TeslemetryData( - vehicles, energysites + vehicles, energysites, scopes ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/teslemetry/climate.py b/homeassistant/components/teslemetry/climate.py index 0835785d194..4c1c05570ab 100644 --- a/homeassistant/components/teslemetry/climate.py +++ b/homeassistant/components/teslemetry/climate.py @@ -4,6 +4,8 @@ from __future__ import annotations from typing import Any +from tesla_fleet_api.const import Scope + from homeassistant.components.climate import ( ClimateEntity, ClimateEntityFeature, @@ -17,6 +19,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN, TeslemetryClimateSide from .context import handle_command from .entity import TeslemetryVehicleEntity +from .models import TeslemetryVehicleData async def async_setup_entry( @@ -26,7 +29,7 @@ async def async_setup_entry( data = hass.data[DOMAIN][entry.entry_id] async_add_entities( - TeslemetryClimateEntity(vehicle, TeslemetryClimateSide.DRIVER) + TeslemetryClimateEntity(vehicle, TeslemetryClimateSide.DRIVER, data.scopes) for vehicle in data.vehicles ) @@ -48,6 +51,22 @@ class TeslemetryClimateEntity(TeslemetryVehicleEntity, ClimateEntity): _attr_preset_modes = ["off", "keep", "dog", "camp"] _enable_turn_on_off_backwards_compatibility = False + def __init__( + self, + data: TeslemetryVehicleData, + side: TeslemetryClimateSide, + scopes: Scope, + ) -> None: + """Initialize the climate.""" + self.scoped = Scope.VEHICLE_CMDS in scopes + if not self.scoped: + self._attr_supported_features = ClimateEntityFeature(0) + + super().__init__( + data, + side, + ) + @property def hvac_mode(self) -> HVACMode | None: """Return hvac operation ie. heat, cool mode.""" @@ -82,6 +101,7 @@ class TeslemetryClimateEntity(TeslemetryVehicleEntity, ClimateEntity): async def async_turn_on(self) -> None: """Set the climate state to on.""" + self.raise_for_scope() with handle_command(): await self.wake_up_if_asleep() await self.api.auto_conditioning_start() @@ -89,6 +109,7 @@ class TeslemetryClimateEntity(TeslemetryVehicleEntity, ClimateEntity): async def async_turn_off(self) -> None: """Set the climate state to off.""" + self.raise_for_scope() with handle_command(): await self.wake_up_if_asleep() await self.api.auto_conditioning_stop() diff --git a/homeassistant/components/teslemetry/entity.py b/homeassistant/components/teslemetry/entity.py index eda3d26f341..d67a1bd1770 100644 --- a/homeassistant/components/teslemetry/entity.py +++ b/homeassistant/components/teslemetry/entity.py @@ -5,7 +5,7 @@ from typing import Any from tesla_fleet_api.exceptions import TeslaFleetError -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -83,6 +83,11 @@ class TeslemetryVehicleEntity(CoordinatorEntity[TeslemetryVehicleDataCoordinator self.coordinator.data[key] = value self.async_write_ha_state() + def raise_for_scope(self): + """Raise an error if a scope is not available.""" + if not self.scoped: + raise ServiceValidationError("Missing required scope") + class TeslemetryEnergyEntity(CoordinatorEntity[TeslemetryEnergyDataCoordinator]): """Parent class for Teslemetry Energy Entities.""" diff --git a/homeassistant/components/teslemetry/models.py b/homeassistant/components/teslemetry/models.py index d6f15e2e932..615156e6fdc 100644 --- a/homeassistant/components/teslemetry/models.py +++ b/homeassistant/components/teslemetry/models.py @@ -6,6 +6,7 @@ import asyncio from dataclasses import dataclass from tesla_fleet_api import EnergySpecific, VehicleSpecific +from tesla_fleet_api.const import Scope from .coordinator import ( TeslemetryEnergyDataCoordinator, @@ -19,6 +20,7 @@ class TeslemetryData: vehicles: list[TeslemetryVehicleData] energysites: list[TeslemetryEnergyData] + scopes: list[Scope] @dataclass diff --git a/tests/components/teslemetry/conftest.py b/tests/components/teslemetry/conftest.py index f252787b37c..9040ec96a03 100644 --- a/tests/components/teslemetry/conftest.py +++ b/tests/components/teslemetry/conftest.py @@ -7,7 +7,23 @@ from unittest.mock import patch import pytest -from .const import LIVE_STATUS, PRODUCTS, RESPONSE_OK, VEHICLE_DATA, WAKE_UP_ONLINE +from .const import ( + LIVE_STATUS, + METADATA, + PRODUCTS, + RESPONSE_OK, + VEHICLE_DATA, + WAKE_UP_ONLINE, +) + + +@pytest.fixture(autouse=True) +def mock_metadata(): + """Mock Tesla Fleet Api metadata method.""" + with patch( + "homeassistant.components.teslemetry.Teslemetry.metadata", return_value=METADATA + ) as mock_products: + yield mock_products @pytest.fixture(autouse=True) diff --git a/tests/components/teslemetry/const.py b/tests/components/teslemetry/const.py index 776cc231a5c..96e9ead8912 100644 --- a/tests/components/teslemetry/const.py +++ b/tests/components/teslemetry/const.py @@ -16,3 +16,21 @@ VEHICLE_DATA_ALT = load_json_object_fixture("vehicle_data_alt.json", DOMAIN) LIVE_STATUS = load_json_object_fixture("live_status.json", DOMAIN) RESPONSE_OK = {"response": {}, "error": None} + +METADATA = { + "region": "NA", + "scopes": [ + "openid", + "offline_access", + "user_data", + "vehicle_device_data", + "vehicle_cmds", + "vehicle_charging_cmds", + "energy_device_data", + "energy_cmds", + ], +} +METADATA_NOSCOPE = { + "region": "NA", + "scopes": ["openid", "offline_access", "vehicle_device_data"], +} diff --git a/tests/components/teslemetry/test_climate.py b/tests/components/teslemetry/test_climate.py index e83e9d648cd..a05bc07b305 100644 --- a/tests/components/teslemetry/test_climate.py +++ b/tests/components/teslemetry/test_climate.py @@ -22,11 +22,11 @@ from homeassistant.components.climate import ( from homeassistant.components.teslemetry.coordinator import SYNC_INTERVAL from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import entity_registry as er from . import assert_entities, setup_platform -from .const import WAKE_UP_ASLEEP, WAKE_UP_ONLINE +from .const import METADATA_NOSCOPE, WAKE_UP_ASLEEP, WAKE_UP_ONLINE from tests.common import async_fire_time_changed @@ -176,3 +176,30 @@ async def test_asleep_or_offline( ) await hass.async_block_till_done() mock_wake_up.assert_called_once() + + +async def test_climate_noscope( + hass: HomeAssistant, + mock_metadata, +) -> None: + """Tests that the climate entity is correct.""" + mock_metadata.return_value = METADATA_NOSCOPE + + await setup_platform(hass, [Platform.CLIMATE]) + entity_id = "climate.test_climate" + + with pytest.raises(ServiceValidationError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: [entity_id], ATTR_HVAC_MODE: HVACMode.HEAT_COOL}, + blocking=True, + ) + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: [entity_id], ATTR_TEMPERATURE: 20}, + blocking=True, + ) From 2c7a1ddb1d79973f6a0321615f04fe180ace39f5 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Tue, 23 Apr 2024 22:13:09 +0200 Subject: [PATCH 802/967] Bump plugwise to v0.37.2 (#115989) --- homeassistant/components/plugwise/__init__.py | 10 ++++++++- .../components/plugwise/binary_sensor.py | 4 ++-- .../components/plugwise/manifest.json | 2 +- .../components/plugwise/strings.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../anna_heatpump_heating/all_data.json | 2 +- .../m_anna_heatpump_cooling/all_data.json | 2 +- .../m_anna_heatpump_idle/all_data.json | 2 +- .../fixtures/stretch_v31/all_data.json | 1 - tests/components/plugwise/test_init.py | 21 +++++++++++++++---- 11 files changed, 35 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/plugwise/__init__.py b/homeassistant/components/plugwise/__init__.py index 28389ffa357..3140e518688 100644 --- a/homeassistant/components/plugwise/__init__.py +++ b/homeassistant/components/plugwise/__init__.py @@ -49,8 +49,16 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: def async_migrate_entity_entry(entry: er.RegistryEntry) -> dict[str, Any] | None: """Migrate Plugwise entity entries. - - Migrates unique ID from old relay switches to the new unique ID + - Migrates old unique ID's from old binary_sensors and switches to the new unique ID's """ + if entry.domain == Platform.BINARY_SENSOR and entry.unique_id.endswith( + "-slave_boiler_state" + ): + return { + "new_unique_id": entry.unique_id.replace( + "-slave_boiler_state", "-secondary_boiler_state" + ) + } if entry.domain == Platform.SWITCH and entry.unique_id.endswith("-plug"): return {"new_unique_id": entry.unique_id.replace("-plug", "-relay")} diff --git a/homeassistant/components/plugwise/binary_sensor.py b/homeassistant/components/plugwise/binary_sensor.py index d32ae94160f..01ebc736dbe 100644 --- a/homeassistant/components/plugwise/binary_sensor.py +++ b/homeassistant/components/plugwise/binary_sensor.py @@ -64,8 +64,8 @@ BINARY_SENSORS: tuple[PlugwiseBinarySensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, ), PlugwiseBinarySensorEntityDescription( - key="slave_boiler_state", - translation_key="slave_boiler_state", + key="secondary_boiler_state", + translation_key="secondary_boiler_state", entity_category=EntityCategory.DIAGNOSTIC, ), PlugwiseBinarySensorEntityDescription( diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index 888f813760a..1eb1cf6e8b6 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.1"], + "requirements": ["plugwise==0.37.2"], "zeroconf": ["_plugwise._tcp.local."] } diff --git a/homeassistant/components/plugwise/strings.json b/homeassistant/components/plugwise/strings.json index 7d26f5a624c..ef2d6458441 100644 --- a/homeassistant/components/plugwise/strings.json +++ b/homeassistant/components/plugwise/strings.json @@ -48,7 +48,7 @@ "cooling_state": { "name": "[%key:component::climate::entity_component::_::state_attributes::hvac_action::state::cooling%]" }, - "slave_boiler_state": { + "secondary_boiler_state": { "name": "Secondary boiler state" }, "plugwise_notification": { diff --git a/requirements_all.txt b/requirements_all.txt index d51dc0225ed..b2c21c1239d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1548,7 +1548,7 @@ plexauth==0.0.6 plexwebsocket==0.0.14 # homeassistant.components.plugwise -plugwise==0.37.1 +plugwise==0.37.2 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7b04e7cf037..664467ea0a5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1225,7 +1225,7 @@ plexauth==0.0.6 plexwebsocket==0.0.14 # homeassistant.components.plugwise -plugwise==0.37.1 +plugwise==0.37.2 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 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 d655f95c79b..d496edb4149 100644 --- a/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json +++ b/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json @@ -25,7 +25,7 @@ "dhw_state": false, "flame_state": false, "heating_state": true, - "slave_boiler_state": false + "secondary_boiler_state": false }, "dev_class": "heater_central", "location": "a57efe5f145f498c9be62a9b63626fbf", 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 92c95f6c5a9..ef7af8a362b 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 @@ -25,7 +25,7 @@ "dhw_state": false, "flame_state": false, "heating_state": false, - "slave_boiler_state": false + "secondary_boiler_state": false }, "dev_class": "heater_central", "location": "a57efe5f145f498c9be62a9b63626fbf", 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 be400b9bc98..8f2e6a75f3f 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 @@ -25,7 +25,7 @@ "dhw_state": false, "flame_state": false, "heating_state": false, - "slave_boiler_state": false + "secondary_boiler_state": false }, "dev_class": "heater_central", "location": "a57efe5f145f498c9be62a9b63626fbf", diff --git a/tests/components/plugwise/fixtures/stretch_v31/all_data.json b/tests/components/plugwise/fixtures/stretch_v31/all_data.json index f42cde65b39..a875324fc13 100644 --- a/tests/components/plugwise/fixtures/stretch_v31/all_data.json +++ b/tests/components/plugwise/fixtures/stretch_v31/all_data.json @@ -136,7 +136,6 @@ "gateway": { "gateway_id": "0000aaaa0000aaaa0000aaaa0000aa00", "item_count": 83, - "notifications": {}, "smile_name": "Stretch" } } diff --git a/tests/components/plugwise/test_init.py b/tests/components/plugwise/test_init.py index 4eb0b2cb56a..b206b36be89 100644 --- a/tests/components/plugwise/test_init.py +++ b/tests/components/plugwise/test_init.py @@ -12,9 +12,8 @@ from plugwise.exceptions import ( import pytest from homeassistant.components.plugwise.const import DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -22,6 +21,9 @@ from tests.common import MockConfigEntry HEATER_ID = "1cbf783bb11e4a7c8a6843dee3a86927" # Opentherm device_id for migration PLUG_ID = "cd0ddb54ef694e11ac18ed1cbce5dbbd" # VCR device_id for migration +SECONDARY_ID = ( + "1cbf783bb11e4a7c8a6843dee3a86927" # Heater_central device_id for migration +) async def test_load_unload_config_entry( @@ -77,7 +79,7 @@ async def test_gateway_config_entry_not_ready( [ ( { - "domain": SENSOR_DOMAIN, + "domain": Platform.SENSOR, "platform": DOMAIN, "unique_id": f"{HEATER_ID}-outdoor_temperature", "suggested_object_id": f"{HEATER_ID}-outdoor_temperature", @@ -118,7 +120,18 @@ async def test_migrate_unique_id_temperature( [ ( { - "domain": SWITCH_DOMAIN, + "domain": Platform.BINARY_SENSOR, + "platform": DOMAIN, + "unique_id": f"{SECONDARY_ID}-slave_boiler_state", + "suggested_object_id": f"{SECONDARY_ID}-slave_boiler_state", + "disabled_by": None, + }, + f"{SECONDARY_ID}-slave_boiler_state", + f"{SECONDARY_ID}-secondary_boiler_state", + ), + ( + { + "domain": Platform.SWITCH, "platform": DOMAIN, "unique_id": f"{PLUG_ID}-plug", "suggested_object_id": f"{PLUG_ID}-plug", From f1fa33483e499344ffca230f17ecd357f45eda3a Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Tue, 23 Apr 2024 22:23:44 +0200 Subject: [PATCH 803/967] Bump aioautomower to 2024.4.3 (#114500) --- .../husqvarna_automower/device_tracker.py | 6 ++++ .../husqvarna_automower/manifest.json | 2 +- .../components/husqvarna_automower/select.py | 7 +++-- .../components/husqvarna_automower/sensor.py | 3 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../snapshots/test_diagnostics.ambr | 4 ++- .../snapshots/test_sensor.ambr | 2 +- .../husqvarna_automower/test_sensor.py | 28 +++++++++++++++++-- 9 files changed, 45 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/device_tracker.py b/homeassistant/components/husqvarna_automower/device_tracker.py index a32fd8758bd..780d1da76fb 100644 --- a/homeassistant/components/husqvarna_automower/device_tracker.py +++ b/homeassistant/components/husqvarna_automower/device_tracker.py @@ -1,5 +1,7 @@ """Creates the device tracker entity for the mower.""" +from typing import TYPE_CHECKING + from homeassistant.components.device_tracker import SourceType, TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -44,9 +46,13 @@ class AutomowerDeviceTrackerEntity(AutomowerBaseEntity, TrackerEntity): @property def latitude(self) -> float: """Return latitude value of the device.""" + if TYPE_CHECKING: + assert self.mower_attributes.positions is not None return self.mower_attributes.positions[0].latitude @property def longitude(self) -> float: """Return longitude value of the device.""" + if TYPE_CHECKING: + assert self.mower_attributes.positions is not None return self.mower_attributes.positions[0].longitude diff --git a/homeassistant/components/husqvarna_automower/manifest.json b/homeassistant/components/husqvarna_automower/manifest.json index e4536ee594d..147c6dfb6d5 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.3.4"] + "requirements": ["aioautomower==2024.4.3"] } diff --git a/homeassistant/components/husqvarna_automower/select.py b/homeassistant/components/husqvarna_automower/select.py index e4376a1bca5..67aac4a2046 100644 --- a/homeassistant/components/husqvarna_automower/select.py +++ b/homeassistant/components/husqvarna_automower/select.py @@ -1,6 +1,7 @@ """Creates a select entity for the headlight of the mower.""" import logging +from typing import cast from aioautomower.exceptions import ApiException from aioautomower.model import HeadlightModes @@ -58,12 +59,14 @@ class AutomowerSelectEntity(AutomowerControlEntity, SelectEntity): @property def current_option(self) -> str: """Return the current option for the entity.""" - return self.mower_attributes.headlight.mode.lower() + return cast(HeadlightModes, self.mower_attributes.headlight.mode).lower() async def async_select_option(self, option: str) -> None: """Change the selected option.""" try: - await self.coordinator.api.set_headlight_mode(self.mower_id, option.upper()) + await self.coordinator.api.set_headlight_mode( + self.mower_id, cast(HeadlightModes, option.upper()) + ) except ApiException as exception: raise HomeAssistantError( f"Command couldn't be sent to the command queue: {exception}" diff --git a/homeassistant/components/husqvarna_automower/sensor.py b/homeassistant/components/husqvarna_automower/sensor.py index 10aec9b1536..6840708ed42 100644 --- a/homeassistant/components/husqvarna_automower/sensor.py +++ b/homeassistant/components/husqvarna_automower/sensor.py @@ -18,7 +18,6 @@ from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfLength, UnitOf 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 .coordinator import AutomowerDataUpdateCoordinator @@ -298,7 +297,7 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = ( key="next_start_timestamp", translation_key="next_start_timestamp", device_class=SensorDeviceClass.TIMESTAMP, - value_fn=lambda data: dt_util.as_local(data.planner.next_start_datetime), + value_fn=lambda data: data.planner.next_start_datetime, ), AutomowerSensorEntityDescription( key="error", diff --git a/requirements_all.txt b/requirements_all.txt index b2c21c1239d..df688e6e00f 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.3.4 +aioautomower==2024.4.3 # homeassistant.components.azure_devops aioazuredevops==2.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 664467ea0a5..60e54a81780 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.3.4 +aioautomower==2024.4.3 # homeassistant.components.azure_devops aioazuredevops==2.0.0 diff --git a/tests/components/husqvarna_automower/snapshots/test_diagnostics.ambr b/tests/components/husqvarna_automower/snapshots/test_diagnostics.ambr index aea65005fc4..ee951986062 100644 --- a/tests/components/husqvarna_automower/snapshots/test_diagnostics.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_diagnostics.ambr @@ -50,12 +50,14 @@ 'activity': 'PARKED_IN_CS', 'error_code': 0, 'error_datetime': None, + 'error_datetime_naive': None, 'error_key': None, 'mode': 'MAIN_AREA', 'state': 'RESTRICTED', }), 'planner': dict({ - 'next_start_datetime': '2023-06-05T19:00:00', + 'next_start_datetime': '2023-06-05T19:00:00+00:00', + 'next_start_datetime_naive': '2023-06-05T19:00:00', 'override': dict({ 'action': 'NOT_ACTIVE', }), diff --git a/tests/components/husqvarna_automower/snapshots/test_sensor.ambr b/tests/components/husqvarna_automower/snapshots/test_sensor.ambr index fda9c900240..7d4533afe72 100644 --- a/tests/components/husqvarna_automower/snapshots/test_sensor.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_sensor.ambr @@ -548,7 +548,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2023-06-06T02:00:00+00:00', + 'state': '2023-06-05T19:00:00+00:00', }) # --- # name: test_sensor[sensor.test_mower_1_number_of_charging_cycles-entry] diff --git a/tests/components/husqvarna_automower/test_sensor.py b/tests/components/husqvarna_automower/test_sensor.py index f54ce9c6275..2c0661f82cb 100644 --- a/tests/components/husqvarna_automower/test_sensor.py +++ b/tests/components/husqvarna_automower/test_sensor.py @@ -10,7 +10,7 @@ from syrupy import SnapshotAssertion from homeassistant.components.husqvarna_automower.const import DOMAIN from homeassistant.components.husqvarna_automower.coordinator import SCAN_INTERVAL -from homeassistant.const import Platform +from homeassistant.const import STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -46,7 +46,7 @@ async def test_sensor_unknown_states( async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get("sensor.test_mower_1_mode") - assert state.state == "unknown" + assert state.state == STATE_UNKNOWN async def test_cutting_blade_usage_time_sensor( @@ -63,6 +63,30 @@ async def test_cutting_blade_usage_time_sensor( assert state.state == "0.034" +async def test_next_start_sensor( + hass: HomeAssistant, + mock_automower_client: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test if this sensor is only added, if data is available.""" + await setup_integration(hass, mock_config_entry) + state = hass.states.get("sensor.test_mower_1_next_start") + assert state is not None + assert state.state == "2023-06-05T19:00:00+00:00" + + values = mower_list_to_dictionary_dataclass( + load_json_value_fixture("mower.json", DOMAIN) + ) + values[TEST_MOWER_ID].planner.next_start_datetime = 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("sensor.test_mower_1_next_start") + assert state.state == STATE_UNKNOWN + + @pytest.mark.parametrize( ("sensor_to_test"), [ From 8f1761343ea418d477614ad65295c14fbb88bf82 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 23 Apr 2024 22:24:36 +0200 Subject: [PATCH 804/967] Only work out job type once when setting up dispatcher (#116030) --- homeassistant/helpers/dispatcher.py | 13 +++++++++++-- homeassistant/util/logging.py | 30 ++++++++++++++++++----------- 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/homeassistant/helpers/dispatcher.py b/homeassistant/helpers/dispatcher.py index c1194c7da01..52d57e9cf08 100644 --- a/homeassistant/helpers/dispatcher.py +++ b/homeassistant/helpers/dispatcher.py @@ -7,7 +7,12 @@ from functools import partial import logging from typing import Any, TypeVarTuple, overload -from homeassistant.core import HassJob, HomeAssistant, callback +from homeassistant.core import ( + HassJob, + HomeAssistant, + callback, + get_hassjob_callable_job_type, +) from homeassistant.loader import bind_hass from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.logging import catch_log_exception @@ -161,9 +166,13 @@ def _generate_job( signal: SignalType[*_Ts] | str, target: Callable[[*_Ts], Any] | Callable[..., Any] ) -> HassJob[..., None | Coroutine[Any, Any, None]]: """Generate a HassJob for a signal and target.""" + job_type = get_hassjob_callable_job_type(target) return HassJob( - catch_log_exception(target, partial(_format_err, signal, target)), + catch_log_exception( + target, partial(_format_err, signal, target), job_type=job_type + ), f"dispatcher {signal}", + job_type=job_type, ) diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index 8709186face..ab163578846 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -2,7 +2,6 @@ from __future__ import annotations -import asyncio from collections.abc import Callable, Coroutine from functools import partial, wraps import inspect @@ -12,7 +11,12 @@ import queue import traceback from typing import Any, TypeVar, TypeVarTuple, cast, overload -from homeassistant.core import HomeAssistant, callback, is_callback +from homeassistant.core import ( + HassJobType, + HomeAssistant, + callback, + get_hassjob_callable_job_type, +) _T = TypeVar("_T") _Ts = TypeVarTuple("_Ts") @@ -129,34 +133,38 @@ def _callback_wrapper( @overload def catch_log_exception( - func: Callable[[*_Ts], Coroutine[Any, Any, Any]], format_err: Callable[[*_Ts], Any] + func: Callable[[*_Ts], Coroutine[Any, Any, Any]], + format_err: Callable[[*_Ts], Any], + job_type: HassJobType | None = None, ) -> Callable[[*_Ts], Coroutine[Any, Any, None]]: ... @overload def catch_log_exception( - func: Callable[[*_Ts], Any], format_err: Callable[[*_Ts], Any] + func: Callable[[*_Ts], Any], + format_err: Callable[[*_Ts], Any], + job_type: HassJobType | None = None, ) -> Callable[[*_Ts], None] | Callable[[*_Ts], Coroutine[Any, Any, None]]: ... def catch_log_exception( - func: Callable[[*_Ts], Any], format_err: Callable[[*_Ts], Any] + func: Callable[[*_Ts], Any], + format_err: Callable[[*_Ts], Any], + job_type: HassJobType | None = None, ) -> Callable[[*_Ts], None] | Callable[[*_Ts], Coroutine[Any, Any, None]]: """Decorate a function func to catch and log exceptions. If func is a coroutine function, a coroutine function will be returned. If func is a callback, a callback will be returned. """ - # Check for partials to properly determine if coroutine function - check_func = func - while isinstance(check_func, partial): - check_func = check_func.func # type: ignore[unreachable] # false positive + if job_type is None: + job_type = get_hassjob_callable_job_type(func) - if asyncio.iscoroutinefunction(check_func): + if job_type is HassJobType.Coroutinefunction: async_func = cast(Callable[[*_Ts], Coroutine[Any, Any, None]], func) return wraps(async_func)(partial(_async_wrapper, async_func, format_err)) # type: ignore[return-value] - if is_callback(check_func): + if job_type is HassJobType.Callback: return wraps(func)(partial(_callback_wrapper, func, format_err)) # type: ignore[return-value] return wraps(func)(partial(_sync_wrapper, func, format_err)) # type: ignore[return-value] From 0c583bb1d902f2bbbb7eac4b5dfcef7818182043 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 23 Apr 2024 22:25:35 +0200 Subject: [PATCH 805/967] Fix ruff complaints in control4 (#116058) --- homeassistant/components/control4/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/control4/__init__.py b/homeassistant/components/control4/__init__.py index 4b24ac6bf77..86a13de1ac8 100644 --- a/homeassistant/components/control4/__init__.py +++ b/homeassistant/components/control4/__init__.py @@ -52,8 +52,7 @@ async def call_c4_api_retry(func, *func_args): """Call C4 API function and retry on failure.""" for i in range(API_RETRY_TIMES): try: - output = await func(*func_args) - return output + return await func(*func_args) except client_exceptions.ClientError as exception: _LOGGER.error("Error connecting to Control4 account API: %s", exception) if i == API_RETRY_TIMES - 1: From 31d11b2362af06e9083edef633703af050834bd6 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 23 Apr 2024 22:26:01 +0200 Subject: [PATCH 806/967] Add re-auth flow for MQTT broker username and password (#116011) Co-authored-by: J. Nick Koston --- homeassistant/components/mqtt/client.py | 8 ++ homeassistant/components/mqtt/config_flow.py | 61 +++++++++++- homeassistant/components/mqtt/strings.json | 14 +++ tests/components/mqtt/test_config_flow.py | 97 ++++++++++++++++++++ tests/components/mqtt/test_init.py | 18 ++++ 5 files changed, 197 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 9a344e13023..133991ade16 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -895,10 +895,18 @@ class MQTT: import paho.mqtt.client as mqtt if result_code != mqtt.CONNACK_ACCEPTED: + if result_code in ( + mqtt.CONNACK_REFUSED_BAD_USERNAME_PASSWORD, + mqtt.CONNACK_REFUSED_NOT_AUTHORIZED, + ): + self._should_reconnect = False + self.hass.async_create_task(self.async_disconnect()) + self.config_entry.async_start_reauth(self.hass) _LOGGER.error( "Unable to connect to the MQTT broker: %s", mqtt.connack_string(result_code), ) + self._async_connection_result(False) return self.connected = True diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 5bf0c9c1879..8168b997fa6 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections import OrderedDict -from collections.abc import Callable +from collections.abc import Callable, Mapping import queue from ssl import PROTOCOL_TLS_CLIENT, SSLContext, SSLError from types import MappingProxyType @@ -158,13 +158,23 @@ CERT_UPLOAD_SELECTOR = FileSelector( ) KEY_UPLOAD_SELECTOR = FileSelector(FileSelectorConfig(accept=".key,application/pkcs8")) +REAUTH_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): TEXT_SELECTOR, + vol.Required(CONF_PASSWORD): PASSWORD_SELECTOR, + } +) +PWD_NOT_CHANGED = "__**password_not_changed**__" + class FlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 1 + entry: ConfigEntry | None _hassio_discovery: dict[str, Any] | None = None + _reauth_config_entry: ConfigEntry | None = None @staticmethod @callback @@ -183,6 +193,55 @@ class FlowHandler(ConfigFlow, domain=DOMAIN): return await self.async_step_broker() + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: + """Handle re-authentication with Aladdin Connect.""" + + self.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 re-authentication with MQTT broker.""" + errors: dict[str, str] = {} + + assert self.entry is not None + if user_input: + password_changed = ( + user_password := user_input[CONF_PASSWORD] + ) != PWD_NOT_CHANGED + entry_password = self.entry.data.get(CONF_PASSWORD) + password = user_password if password_changed else entry_password + new_entry_data = { + **self.entry.data, + CONF_USERNAME: user_input.get(CONF_USERNAME), + CONF_PASSWORD: password, + } + if await self.hass.async_add_executor_job( + try_connection, + new_entry_data, + ): + return self.async_update_reload_and_abort( + self.entry, data=new_entry_data + ) + + errors["base"] = "invalid_auth" + + schema = self.add_suggested_values_to_schema( + REAUTH_SCHEMA, + { + CONF_USERNAME: self.entry.data.get(CONF_USERNAME), + CONF_PASSWORD: PWD_NOT_CHANGED, + }, + ) + return self.async_show_form( + step_id="reauth_confirm", + data_schema=schema, + errors=errors, + ) + async def async_step_broker( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 2bd47db63bc..fc5f0bc4970 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -68,10 +68,23 @@ "data_description": { "discovery": "Option to enable MQTT automatic discovery." } + }, + "reauth_confirm": { + "title": "Re-authentication required with the MQTT broker", + "description": "The MQTT broker reported an authentication error. Please confirm the brokers correct usernname and password.", + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + }, + "data_description": { + "username": "[%key:component::mqtt::config::step::broker::data_description::username%]", + "password": "[%key:component::mqtt::config::step::broker::data_description::password%]" + } } }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" }, "error": { @@ -84,6 +97,7 @@ "bad_client_cert_key": "Client certificate and private key are not a valid pair", "bad_ws_headers": "Supply valid HTTP headers as a JSON object", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "invalid_inclusion": "The client certificate and private key must be configurered together" } }, diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index bbba791137a..56d19506a66 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -14,6 +14,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import mqtt from homeassistant.components.hassio import HassioServiceInfo +from homeassistant.components.mqtt.config_flow import PWD_NOT_CHANGED from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -1060,6 +1061,102 @@ async def test_skipping_advanced_options( assert result["step_id"] == step_id +@pytest.mark.parametrize( + ("test_input", "user_input", "new_password"), + [ + ( + { + mqtt.CONF_BROKER: "test-broker", + mqtt.CONF_USERNAME: "username", + mqtt.CONF_PASSWORD: "verysecret", + }, + { + mqtt.CONF_USERNAME: "username", + mqtt.CONF_PASSWORD: "newpassword", + }, + "newpassword", + ), + ( + { + mqtt.CONF_BROKER: "test-broker", + mqtt.CONF_USERNAME: "username", + mqtt.CONF_PASSWORD: "verysecret", + }, + { + mqtt.CONF_USERNAME: "username", + mqtt.CONF_PASSWORD: PWD_NOT_CHANGED, + }, + "verysecret", + ), + ], +) +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, + test_input: dict[str, Any], + user_input: dict[str, Any], + new_password: str, +) -> None: + """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() + + # Start reauth flow + config_entry.async_start_reauth(hass) + await hass.async_block_till_done() + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + result = flows[0] + assert result["step_id"] == "reauth_confirm" + assert result["context"]["source"] == "reauth" + + # Show the form + result = await hass.config_entries.flow.async_init( + mqtt.DOMAIN, + context={ + "source": config_entries.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"]) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + # Simulate re-auth fails + mock_try_connection.return_value = False + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=user_input + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "invalid_auth"} + + # Simulate re-auth succeeds + mock_try_connection.return_value = True + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=user_input + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + assert len(hass.config_entries.async_entries()) == 1 + assert config_entry.data.get(mqtt.CONF_PASSWORD) == new_password + await hass.async_block_till_done() + + async def test_options_user_connection_fails( hass: HomeAssistant, mock_try_connection_time_out: MagicMock ) -> None: diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 7bb43568b30..9d135b89f36 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -2046,6 +2046,24 @@ 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, + mqtt_mock_entry: MqttMockHAClientGenerator, + mqtt_client_mock: MqttMockPahoClient, + return_code: int, +) -> None: + """Test re-auth is triggered if authentication is failing.""" + await mqtt_mock_entry() + # 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) + 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, From a22c221722216338d23f1a47bd3716f8518cd390 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 23 Apr 2024 22:28:31 +0200 Subject: [PATCH 807/967] Rename bus._async_fire to bus.async_fire_internal (#116027) --- .../components/automation/__init__.py | 5 ++- homeassistant/core.py | 39 +++++++++++-------- homeassistant/data_entry_flow.py | 2 +- homeassistant/helpers/script.py | 2 +- homeassistant/setup.py | 4 +- 5 files changed, 32 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 89a2817e236..fa242ac1557 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -707,7 +707,10 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity): @callback def started_action() -> None: - self.hass.bus.async_fire( + # This is always a callback from a coro so there is no + # risk of this running in a thread which allows us to use + # async_fire_internal + self.hass.bus.async_fire_internal( EVENT_AUTOMATION_TRIGGERED, event_data, context=trigger_context ) diff --git a/homeassistant/core.py b/homeassistant/core.py index 8471d2c4dcc..01329806e61 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -506,8 +506,8 @@ class HomeAssistant: setattr(self.loop, "_thread_ident", threading.get_ident()) self.set_state(CoreState.starting) - self.bus.async_fire(EVENT_CORE_CONFIG_UPDATE) - self.bus.async_fire(EVENT_HOMEASSISTANT_START) + self.bus.async_fire_internal(EVENT_CORE_CONFIG_UPDATE) + self.bus.async_fire_internal(EVENT_HOMEASSISTANT_START) if not self._tasks: pending: set[asyncio.Future[Any]] | None = None @@ -540,8 +540,8 @@ class HomeAssistant: return self.set_state(CoreState.running) - self.bus.async_fire(EVENT_CORE_CONFIG_UPDATE) - self.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + self.bus.async_fire_internal(EVENT_CORE_CONFIG_UPDATE) + self.bus.async_fire_internal(EVENT_HOMEASSISTANT_STARTED) def add_job( self, target: Callable[[*_Ts], Any] | Coroutine[Any, Any, Any], *args: *_Ts @@ -1115,7 +1115,7 @@ class HomeAssistant: self.exit_code = exit_code self.set_state(CoreState.stopping) - self.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + self.bus.async_fire_internal(EVENT_HOMEASSISTANT_STOP) try: async with self.timeout.async_timeout(STOP_STAGE_SHUTDOWN_TIMEOUT): await self.async_block_till_done() @@ -1128,7 +1128,7 @@ class HomeAssistant: # Stage 3 - Final write self.set_state(CoreState.final_write) - self.bus.async_fire(EVENT_HOMEASSISTANT_FINAL_WRITE) + self.bus.async_fire_internal(EVENT_HOMEASSISTANT_FINAL_WRITE) try: async with self.timeout.async_timeout(FINAL_WRITE_STAGE_SHUTDOWN_TIMEOUT): await self.async_block_till_done() @@ -1141,7 +1141,7 @@ class HomeAssistant: # Stage 4 - Close self.set_state(CoreState.not_running) - self.bus.async_fire(EVENT_HOMEASSISTANT_CLOSE) + self.bus.async_fire_internal(EVENT_HOMEASSISTANT_CLOSE) # Make a copy of running_tasks since a task can finish # while we are awaiting canceled tasks to get their result @@ -1390,7 +1390,7 @@ class _OneTimeListener(Generic[_DataT]): return f"<_OneTimeListener {self.listener_job.target}>" -# Empty list, used by EventBus._async_fire +# Empty list, used by EventBus.async_fire_internal EMPTY_LIST: list[Any] = [] @@ -1455,10 +1455,12 @@ class EventBus: raise MaxLengthExceeded( event_type, "event_type", MAX_LENGTH_EVENT_EVENT_TYPE ) - return self._async_fire(event_type, event_data, origin, context, time_fired) + return self.async_fire_internal( + event_type, event_data, origin, context, time_fired + ) @callback - def _async_fire( + def async_fire_internal( self, event_type: EventType[_DataT] | str, event_data: _DataT | None = None, @@ -1466,7 +1468,12 @@ class EventBus: context: Context | None = None, time_fired: float | None = None, ) -> None: - """Fire an event. + """Fire an event, for internal use only. + + This method is intended to only be used by core internally + and should not be considered a stable API. We will make + breaking change to this function in the future and it + should not be used in integrations. This method must be run in the event loop. """ @@ -2112,7 +2119,7 @@ class StateMachine: "old_state": old_state, "new_state": None, } - self._bus._async_fire( # pylint: disable=protected-access + self._bus.async_fire_internal( EVENT_STATE_CHANGED, state_changed_data, context=context, @@ -2225,7 +2232,7 @@ class StateMachine: # mypy does not understand this is only possible if old_state is not None old_last_reported = old_state.last_reported # type: ignore[union-attr] old_state.last_reported = now # type: ignore[union-attr] - self._bus._async_fire( # pylint: disable=protected-access + self._bus.async_fire_internal( EVENT_STATE_REPORTED, { "entity_id": entity_id, @@ -2268,7 +2275,7 @@ class StateMachine: "old_state": old_state, "new_state": state, } - self._bus._async_fire( # pylint: disable=protected-access + self._bus.async_fire_internal( EVENT_STATE_CHANGED, state_changed_data, context=context, @@ -2622,7 +2629,7 @@ class ServiceRegistry: domain, service, processed_data, context, return_response ) - self._hass.bus._async_fire( # pylint: disable=protected-access + self._hass.bus.async_fire_internal( EVENT_CALL_SERVICE, { ATTR_DOMAIN: domain, @@ -2948,7 +2955,7 @@ class Config: self._update(source=ConfigSource.STORAGE, **kwargs) await self._async_store() - self.hass.bus.async_fire(EVENT_CORE_CONFIG_UPDATE, kwargs) + self.hass.bus.async_fire_internal(EVENT_CORE_CONFIG_UPDATE, kwargs) _raise_issue_if_historic_currency(self.hass, self.currency) _raise_issue_if_no_country(self.hass, self.country) diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 7e7019681af..f628879a7fd 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -442,7 +442,7 @@ class FlowManager(abc.ABC, Generic[_FlowResultT, _HandlerT]): ) ): # Tell frontend to reload the flow state. - self.hass.bus.async_fire( + self.hass.bus.async_fire_internal( EVENT_DATA_ENTRY_FLOW_PROGRESSED, {"handler": flow.handler, "flow_id": flow_id, "refresh": True}, ) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 62c781ae629..d925bf215ab 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -784,7 +784,7 @@ class _ScriptRun: ) trace_set_result(event=self._action[CONF_EVENT], event_data=event_data) - self._hass.bus.async_fire( + self._hass.bus.async_fire_internal( self._action[CONF_EVENT], event_data, context=self._context ) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 5772fce6955..fab70e31d9d 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -459,7 +459,9 @@ async def _async_setup_component( # Cleanup hass.data[DATA_SETUP].pop(domain, None) - hass.bus.async_fire(EVENT_COMPONENT_LOADED, EventComponentLoaded(component=domain)) + hass.bus.async_fire_internal( + EVENT_COMPONENT_LOADED, EventComponentLoaded(component=domain) + ) return True From a45040af145fcd10f252c8b4637ad3442a119e48 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 23 Apr 2024 22:30:20 +0200 Subject: [PATCH 808/967] Add entity translations to 17track (#116022) --- .../components/seventeentrack/icons.json | 30 +++++++++ .../components/seventeentrack/sensor.py | 51 ++++++++------- .../components/seventeentrack/strings.json | 28 ++++++++ .../components/seventeentrack/test_sensor.py | 64 +++++-------------- 4 files changed, 101 insertions(+), 72 deletions(-) create mode 100644 homeassistant/components/seventeentrack/icons.json diff --git a/homeassistant/components/seventeentrack/icons.json b/homeassistant/components/seventeentrack/icons.json new file mode 100644 index 00000000000..05323a69743 --- /dev/null +++ b/homeassistant/components/seventeentrack/icons.json @@ -0,0 +1,30 @@ +{ + "entity": { + "sensor": { + "not_found": { + "default": "mdi:package" + }, + "in_transit": { + "default": "mdi:package" + }, + "expired": { + "default": "mdi:package" + }, + "ready_to_be_picked_up": { + "default": "mdi:package" + }, + "undelivered": { + "default": "mdi:package" + }, + "delivered": { + "default": "mdi:package" + }, + "returned": { + "default": "mdi:package" + }, + "package": { + "default": "mdi:package" + } + } + } +} diff --git a/homeassistant/components/seventeentrack/sensor.py b/homeassistant/components/seventeentrack/sensor.py index cbad01d0b0a..acc8471c030 100644 --- a/homeassistant/components/seventeentrack/sensor.py +++ b/homeassistant/components/seventeentrack/sensor.py @@ -18,6 +18,7 @@ from homeassistant.const import ( from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_validation as cv, entity_registry as er +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 @@ -38,7 +39,6 @@ from .const import ( CONF_SHOW_ARCHIVED, CONF_SHOW_DELIVERED, DOMAIN, - ENTITY_ID_TEMPLATE, LOGGER, NOTIFICATION_DELIVERED_MESSAGE, NOTIFICATION_DELIVERED_TITLE, @@ -150,7 +150,7 @@ async def async_setup_entry( ) async_add_entities( - SeventeenTrackSummarySensor(status, summary_data["status_name"], coordinator) + SeventeenTrackSummarySensor(status, coordinator) for status, summary_data in coordinator.data.summary.items() ) @@ -161,26 +161,37 @@ async def async_setup_entry( ) -class SeventeenTrackSummarySensor( - CoordinatorEntity[SeventeenTrackCoordinator], SensorEntity -): - """Define a summary sensor.""" +class SeventeenTrackSensor(CoordinatorEntity[SeventeenTrackCoordinator], SensorEntity): + """Define a 17Track sensor.""" _attr_attribution = ATTRIBUTION - _attr_icon = "mdi:package" + _attr_has_entity_name = True + + def __init__(self, coordinator: SeventeenTrackCoordinator) -> None: + """Initialize the sensor.""" + super().__init__(coordinator) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, coordinator.account_id)}, + entry_type=DeviceEntryType.SERVICE, + name="17Track", + ) + + +class SeventeenTrackSummarySensor(SeventeenTrackSensor): + """Define a summary sensor.""" + _attr_native_unit_of_measurement = "packages" def __init__( self, status: str, - status_name: str, coordinator: SeventeenTrackCoordinator, ) -> None: """Initialize the sensor.""" super().__init__(coordinator) self._status = status - self._attr_name = f"Seventeentrack Packages {status_name}" - self._attr_unique_id = f"summary_{coordinator.account_id}_{self._status}" + self._attr_translation_key = status + self._attr_unique_id = f"summary_{coordinator.account_id}_{status}" @property def available(self) -> bool: @@ -211,13 +222,10 @@ class SeventeenTrackSummarySensor( } -class SeventeenTrackPackageSensor( - CoordinatorEntity[SeventeenTrackCoordinator], SensorEntity -): +class SeventeenTrackPackageSensor(SeventeenTrackSensor): """Define an individual package sensor.""" - _attr_attribution = ATTRIBUTION - _attr_icon = "mdi:package" + _attr_translation_key = "package" def __init__( self, @@ -228,24 +236,19 @@ class SeventeenTrackPackageSensor( super().__init__(coordinator) self._tracking_number = tracking_number self._previous_status = coordinator.data.live_packages[tracking_number].status - self.entity_id = ENTITY_ID_TEMPLATE.format(tracking_number) self._attr_unique_id = UNIQUE_ID_TEMPLATE.format( coordinator.account_id, tracking_number ) + package = coordinator.data.live_packages[tracking_number] + if not (name := package.friendly_name): + name = tracking_number + self._attr_translation_placeholders = {"name": name} @property def available(self) -> bool: """Return whether the entity is available.""" return self._tracking_number in self.coordinator.data.live_packages - @property - def name(self) -> str: - """Return the name.""" - package = self.coordinator.data.live_packages.get(self._tracking_number) - if package is None or not (name := package.friendly_name): - name = self._tracking_number - return f"Seventeentrack Package: {name}" - @property def native_value(self) -> StateType: """Return the state.""" diff --git a/homeassistant/components/seventeentrack/strings.json b/homeassistant/components/seventeentrack/strings.json index 39ddb5ef8ef..8d91f926d50 100644 --- a/homeassistant/components/seventeentrack/strings.json +++ b/homeassistant/components/seventeentrack/strings.json @@ -38,5 +38,33 @@ "title": "The 17Track YAML configuration import request failed due to invalid authentication", "description": "Configuring 17Track using YAML is being removed but there were invalid credentials provided while importing your existing configuration.\nSetup will not proceed.\n\nVerify that your 17Track credentials are correct and restart Home Assistant to attempt the import again.\n\nAlternatively, you may remove the 17Track configuration from your YAML configuration entirely, restart Home Assistant, and add the 17Track integration manually." } + }, + "entity": { + "sensor": { + "not_found": { + "name": "Not found" + }, + "in_transit": { + "name": "In transit" + }, + "expired": { + "name": "Expired" + }, + "ready_to_be_picked_up": { + "name": "Ready to be picked up" + }, + "undelivered": { + "name": "Undelivered" + }, + "delivered": { + "name": "Delivered" + }, + "returned": { + "name": "Returned" + }, + "package": { + "name": "Package {name}" + } + } } } diff --git a/tests/components/seventeentrack/test_sensor.py b/tests/components/seventeentrack/test_sensor.py index 27de64ca89f..31fc5deec24 100644 --- a/tests/components/seventeentrack/test_sensor.py +++ b/tests/components/seventeentrack/test_sensor.py @@ -75,7 +75,7 @@ async def test_add_package( mock_seventeentrack.return_value.profile.packages.return_value = [package] await init_integration(hass, mock_config_entry) - assert hass.states.get("sensor.seventeentrack_package_456") + assert hass.states.get("sensor.17track_package_friendly_name_1") assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH + 1 package2 = get_package( @@ -89,7 +89,7 @@ async def test_add_package( await goto_future(hass, freezer) - assert hass.states.get("sensor.seventeentrack_package_789") is not None + assert hass.states.get("sensor.17track_package_friendly_name_1") is not None assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH + 2 @@ -103,9 +103,9 @@ async def test_add_package_default_friendly_name( mock_seventeentrack.return_value.profile.packages.return_value = [package] await init_integration(hass, mock_config_entry) - state_456 = hass.states.get("sensor.seventeentrack_package_456") + state_456 = hass.states.get("sensor.17track_package_456") assert state_456 is not None - assert state_456.attributes["friendly_name"] == "Seventeentrack Package: 456" + assert state_456.attributes["friendly_name"] == "17Track Package 456" assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH + 1 @@ -132,16 +132,16 @@ async def test_remove_package( await init_integration(hass, mock_config_entry) - assert hass.states.get("sensor.seventeentrack_package_456") is not None - assert hass.states.get("sensor.seventeentrack_package_789") is not None + assert hass.states.get("sensor.17track_package_friendly_name_1") is not None + assert hass.states.get("sensor.17track_package_friendly_name_2") is not None assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH + 2 mock_seventeentrack.return_value.profile.packages.return_value = [package2] await goto_future(hass, freezer) - assert hass.states.get("sensor.seventeentrack_package_456") is None - assert hass.states.get("sensor.seventeentrack_package_789") is not None + assert hass.states.get("sensor.17track_package_friendly_name_1") is None + assert hass.states.get("sensor.17track_package_friendly_name_2") is not None assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH + 1 @@ -157,35 +157,7 @@ async def test_package_error( mock_seventeentrack.return_value.profile.summary.return_value = {} await init_integration(hass, mock_config_entry) - assert hass.states.get("sensor.seventeentrack_package_456") is None - - -async def test_friendly_name_changed( - hass: HomeAssistant, - freezer: FrozenDateTimeFactory, - mock_seventeentrack: AsyncMock, - mock_config_entry: MockConfigEntry, -) -> None: - """Test friendly name change.""" - package = get_package() - mock_seventeentrack.return_value.profile.packages.return_value = [package] - - await init_integration(hass, mock_config_entry) - - assert hass.states.get("sensor.seventeentrack_package_456") is not None - assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH + 1 - - package = get_package(friendly_name="friendly name 2") - mock_seventeentrack.return_value.profile.packages.return_value = [package] - - await goto_future(hass, freezer) - - assert hass.states.get("sensor.seventeentrack_package_456") is not None - entity = hass.data["entity_components"]["sensor"].get_entity( - "sensor.seventeentrack_package_456" - ) - assert entity.name == "Seventeentrack Package: friendly name 2" - assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH + 1 + assert hass.states.get("sensor.17track_package_friendly_name_1") is None async def test_delivered_not_shown( @@ -204,7 +176,7 @@ async def test_delivered_not_shown( await init_integration(hass, mock_config_entry_with_default_options) await goto_future(hass, freezer) - assert hass.states.get("sensor.seventeentrack_package_456") is None + assert hass.states.get("sensor.17track_package_friendly_name_1") is None persistent_notification_mock.create.assert_called() @@ -222,7 +194,7 @@ async def test_delivered_shown( ) as persistent_notification_mock: await init_integration(hass, mock_config_entry) - assert hass.states.get("sensor.seventeentrack_package_456") is not None + assert hass.states.get("sensor.17track_package_friendly_name_1") is not None assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH + 1 persistent_notification_mock.create.assert_not_called() @@ -239,7 +211,7 @@ async def test_becomes_delivered_not_shown_notification( await init_integration(hass, mock_config_entry_with_default_options) - assert hass.states.get("sensor.seventeentrack_package_456") is not None + assert hass.states.get("sensor.17track_package_friendly_name_1") is not None assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH + 1 package_delivered = get_package(status=40) @@ -268,9 +240,7 @@ async def test_summary_correctly_updated( assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH + 1 - state_ready_picked = hass.states.get( - "sensor.seventeentrack_packages_ready_to_be_picked_up" - ) + state_ready_picked = hass.states.get("sensor.17track_ready_to_be_picked_up") assert state_ready_picked is not None assert len(state_ready_picked.attributes["packages"]) == 1 @@ -283,9 +253,7 @@ async def test_summary_correctly_updated( for state in hass.states.async_all(): assert state.state == "1" - state_ready_picked = hass.states.get( - "sensor.seventeentrack_packages_ready_to_be_picked_up" - ) + state_ready_picked = hass.states.get("sensor.17track_ready_to_be_picked_up") assert state_ready_picked is not None assert len(state_ready_picked.attributes["packages"]) == 0 @@ -323,9 +291,9 @@ async def test_utc_timestamp( await init_integration(hass, mock_config_entry) - assert hass.states.get("sensor.seventeentrack_package_456") is not None + assert hass.states.get("sensor.17track_package_friendly_name_1") is not None assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH + 1 - state_456 = hass.states.get("sensor.seventeentrack_package_456") + state_456 = hass.states.get("sensor.17track_package_friendly_name_1") assert state_456 is not None assert str(state_456.attributes.get("timestamp")) == "2020-08-10 03:32:00+00:00" From 0f60b404dfbf84cf5b32fa66a4ee633ca4c95e50 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 23 Apr 2024 22:50:31 +0200 Subject: [PATCH 809/967] Fix husqvarna_automower typing (#116060) --- .../components/husqvarna_automower/number.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/number.py b/homeassistant/components/husqvarna_automower/number.py index 8745b93479d..e2e617b427b 100644 --- a/homeassistant/components/husqvarna_automower/number.py +++ b/homeassistant/components/husqvarna_automower/number.py @@ -3,7 +3,7 @@ from collections.abc import Awaitable, Callable from dataclasses import dataclass import logging -from typing import Any +from typing import TYPE_CHECKING, Any from aioautomower.exceptions import ApiException from aioautomower.model import MowerAttributes @@ -12,7 +12,7 @@ from aioautomower.session import AutomowerSession from homeassistant.components.number import NumberEntity, NumberEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -32,6 +32,15 @@ class AutomowerNumberEntityDescription(NumberEntityDescription): set_value_fn: Callable[[AutomowerSession, str, float], Awaitable[Any]] +@callback +def _async_get_cutting_height(data: MowerAttributes) -> int: + """Return the cutting height.""" + if TYPE_CHECKING: + # Sensor does not get created if it is None + assert data.cutting_height is not None + return data.cutting_height + + NUMBER_TYPES: tuple[AutomowerNumberEntityDescription, ...] = ( AutomowerNumberEntityDescription( key="cutting_height", @@ -41,7 +50,7 @@ NUMBER_TYPES: tuple[AutomowerNumberEntityDescription, ...] = ( native_min_value=1, native_max_value=9, exists_fn=lambda data: data.cutting_height is not None, - value_fn=lambda data: data.cutting_height, + value_fn=_async_get_cutting_height, set_value_fn=lambda session, mower_id, cheight: session.set_cutting_height( mower_id, int(cheight) ), From 8d2813fb8b0f83a2c69b41e821ef0f1b3c222713 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 23 Apr 2024 22:53:13 +0200 Subject: [PATCH 810/967] Migrate legacy Ecobee notify service (#115592) * Migrate legacy Ecobee notify service * Correct comment * Update homeassistant/components/ecobee/notify.py Co-authored-by: Joost Lekkerkerker * Use version to check latest entry being used * Use 6 months of deprecation * Add repair flow tests * Only allow migrate_notify fix flow * Simplify repair flow * Use ecobee data to refrence entry * Make entry attrubute puiblic * Use hass.data ro retrieve entry. * Only register issue when legacy service when it is use * Remove backslash * Use ws_client.send_json_auto_id * Cleanup * Import domain from notify integration * Apply suggestions from code review Co-authored-by: Joost Lekkerkerker * Update dependencies * Use Issue_registry fixture * remove `update_before_add` flag * Update homeassistant/components/ecobee/notify.py Co-authored-by: Joost Lekkerkerker * Update homeassistant/components/ecobee/notify.py * Update tests/components/ecobee/conftest.py Co-authored-by: Joost Lekkerkerker * Fix typo and import --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/ecobee/__init__.py | 6 +- homeassistant/components/ecobee/const.py | 1 + homeassistant/components/ecobee/manifest.json | 1 + homeassistant/components/ecobee/notify.py | 57 ++++++++++++- homeassistant/components/ecobee/repairs.py | 37 +++++++++ homeassistant/components/ecobee/strings.json | 13 +++ tests/components/ecobee/common.py | 10 ++- tests/components/ecobee/conftest.py | 9 ++- tests/components/ecobee/test_notify.py | 57 +++++++++++++ tests/components/ecobee/test_repairs.py | 79 +++++++++++++++++++ 10 files changed, 259 insertions(+), 11 deletions(-) create mode 100644 homeassistant/components/ecobee/repairs.py create mode 100644 tests/components/ecobee/test_notify.py create mode 100644 tests/components/ecobee/test_repairs.py diff --git a/homeassistant/components/ecobee/__init__.py b/homeassistant/components/ecobee/__init__.py index 8083d0efcb4..6f032fbaae9 100644 --- a/homeassistant/components/ecobee/__init__.py +++ b/homeassistant/components/ecobee/__init__.py @@ -73,6 +73,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + # The legacy Ecobee notify.notify service is deprecated + # was with HA Core 2024.5.0 and will be removed with HA core 2024.11.0 hass.async_create_task( discovery.async_load_platform( hass, @@ -97,7 +99,7 @@ class EcobeeData: ) -> None: """Initialize the Ecobee data object.""" self._hass = hass - self._entry = entry + self.entry = entry self.ecobee = Ecobee( config={ECOBEE_API_KEY: api_key, ECOBEE_REFRESH_TOKEN: refresh_token} ) @@ -117,7 +119,7 @@ class EcobeeData: _LOGGER.debug("Refreshing ecobee tokens and updating config entry") if await self._hass.async_add_executor_job(self.ecobee.refresh_tokens): self._hass.config_entries.async_update_entry( - self._entry, + self.entry, data={ CONF_API_KEY: self.ecobee.config[ECOBEE_API_KEY], CONF_REFRESH_TOKEN: self.ecobee.config[ECOBEE_REFRESH_TOKEN], diff --git a/homeassistant/components/ecobee/const.py b/homeassistant/components/ecobee/const.py index e20acb5cfca..0eed0ab67f9 100644 --- a/homeassistant/components/ecobee/const.py +++ b/homeassistant/components/ecobee/const.py @@ -46,6 +46,7 @@ PLATFORMS = [ Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.HUMIDIFIER, + Platform.NOTIFY, Platform.NUMBER, Platform.SENSOR, Platform.WEATHER, diff --git a/homeassistant/components/ecobee/manifest.json b/homeassistant/components/ecobee/manifest.json index f3f5b59a36f..7e461230600 100644 --- a/homeassistant/components/ecobee/manifest.json +++ b/homeassistant/components/ecobee/manifest.json @@ -3,6 +3,7 @@ "name": "ecobee", "codeowners": [], "config_flow": true, + "dependencies": ["http", "repairs"], "documentation": "https://www.home-assistant.io/integrations/ecobee", "homekit": { "models": ["EB", "ecobee*"] diff --git a/homeassistant/components/ecobee/notify.py b/homeassistant/components/ecobee/notify.py index b2f6ccb05c8..787130c403f 100644 --- a/homeassistant/components/ecobee/notify.py +++ b/homeassistant/components/ecobee/notify.py @@ -2,11 +2,23 @@ from __future__ import annotations -from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService +from functools import partial +from typing import Any + +from homeassistant.components.notify import ( + ATTR_TARGET, + BaseNotificationService, + NotifyEntity, +) +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 Ecobee, EcobeeData from .const import DOMAIN +from .entity import EcobeeBaseEntity +from .repairs import migrate_notify_issue def get_service( @@ -18,18 +30,25 @@ def get_service( if discovery_info is None: return None - data = hass.data[DOMAIN] + data: EcobeeData = hass.data[DOMAIN] return EcobeeNotificationService(data.ecobee) class EcobeeNotificationService(BaseNotificationService): """Implement the notification service for the Ecobee thermostat.""" - def __init__(self, ecobee): + def __init__(self, ecobee: Ecobee) -> None: """Initialize the service.""" self.ecobee = ecobee - def send_message(self, message="", **kwargs): + async def async_send_message(self, message: str = "", **kwargs: Any) -> None: + """Send a message and raise issue.""" + migrate_notify_issue(self.hass) + await self.hass.async_add_executor_job( + partial(self.send_message, message, **kwargs) + ) + + def send_message(self, message: str = "", **kwargs: Any) -> None: """Send a message.""" targets = kwargs.get(ATTR_TARGET) @@ -39,3 +58,33 @@ class EcobeeNotificationService(BaseNotificationService): for target in targets: thermostat_index = int(target) self.ecobee.send_message(thermostat_index, message) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the ecobee thermostat.""" + data: EcobeeData = hass.data[DOMAIN] + async_add_entities( + EcobeeNotifyEntity(data, index) for index in range(len(data.ecobee.thermostats)) + ) + + +class EcobeeNotifyEntity(EcobeeBaseEntity, NotifyEntity): + """Implement the notification entity for the Ecobee thermostat.""" + + _attr_name = None + _attr_has_entity_name = True + + def __init__(self, data: EcobeeData, thermostat_index: int) -> None: + """Initialize the thermostat.""" + super().__init__(data, thermostat_index) + self._attr_unique_id = ( + f"{self.thermostat["identifier"]}_notify_{thermostat_index}" + ) + + def send_message(self, message: str) -> None: + """Send a message.""" + self.data.ecobee.send_message(self.thermostat_index, message) diff --git a/homeassistant/components/ecobee/repairs.py b/homeassistant/components/ecobee/repairs.py new file mode 100644 index 00000000000..66474730b2f --- /dev/null +++ b/homeassistant/components/ecobee/repairs.py @@ -0,0 +1,37 @@ +"""Repairs support for Ecobee.""" + +from __future__ import annotations + +from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN +from homeassistant.components.repairs import RepairsFlow +from homeassistant.components.repairs.issue_handler import ConfirmRepairFlow +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import issue_registry as ir + +from .const import DOMAIN + + +@callback +def migrate_notify_issue(hass: HomeAssistant) -> None: + """Ensure an issue is registered.""" + ir.async_create_issue( + hass, + DOMAIN, + "migrate_notify", + breaks_in_ha_version="2024.11.0", + issue_domain=NOTIFY_DOMAIN, + is_fixable=True, + is_persistent=True, + translation_key="migrate_notify", + severity=ir.IssueSeverity.WARNING, + ) + + +async def async_create_fix_flow( + hass: HomeAssistant, + issue_id: str, + data: dict[str, str | int | float | None] | None, +) -> RepairsFlow: + """Create flow.""" + assert issue_id == "migrate_notify" + return ConfirmRepairFlow() diff --git a/homeassistant/components/ecobee/strings.json b/homeassistant/components/ecobee/strings.json index b1d1df65417..1d64b6d6b94 100644 --- a/homeassistant/components/ecobee/strings.json +++ b/homeassistant/components/ecobee/strings.json @@ -163,5 +163,18 @@ } } } + }, + "issues": { + "migrate_notify": { + "title": "Migration of Ecobee notify service", + "fix_flow": { + "step": { + "confirm": { + "description": "The Ecobee `notify` service has been migrated. A new `notify` entity per Thermostat is available now.\n\nUpdate any automations to use the new `notify.send_message` exposed by these new entities. When this is done, fix this issue and restart Home Assistant.", + "title": "Disable legacy Ecobee notify service" + } + } + } + } } } diff --git a/tests/components/ecobee/common.py b/tests/components/ecobee/common.py index 60f17c3618d..423b0eee320 100644 --- a/tests/components/ecobee/common.py +++ b/tests/components/ecobee/common.py @@ -4,14 +4,19 @@ from unittest.mock import patch from homeassistant.components.ecobee.const import CONF_REFRESH_TOKEN, DOMAIN from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry -async def setup_platform(hass, platform) -> MockConfigEntry: +async def setup_platform( + hass: HomeAssistant, + platform: str, +) -> MockConfigEntry: """Set up the ecobee platform.""" mock_entry = MockConfigEntry( + title=DOMAIN, domain=DOMAIN, data={ CONF_API_KEY: "ABC123", @@ -22,7 +27,6 @@ async def setup_platform(hass, platform) -> MockConfigEntry: with patch("homeassistant.components.ecobee.const.PLATFORMS", [platform]): assert await async_setup_component(hass, DOMAIN, {}) - - await hass.async_block_till_done() + await hass.async_block_till_done() return mock_entry diff --git a/tests/components/ecobee/conftest.py b/tests/components/ecobee/conftest.py index 952c2f3fba3..27d5a949c58 100644 --- a/tests/components/ecobee/conftest.py +++ b/tests/components/ecobee/conftest.py @@ -1,12 +1,13 @@ """Fixtures for tests.""" +from collections.abc import Generator from unittest.mock import MagicMock, patch import pytest from homeassistant.components.ecobee import ECOBEE_API_KEY, ECOBEE_REFRESH_TOKEN -from tests.common import load_fixture +from tests.common import load_fixture, load_json_object_fixture @pytest.fixture(autouse=True) @@ -23,11 +24,15 @@ def requests_mock_fixture(requests_mock): @pytest.fixture -def mock_ecobee(): +def mock_ecobee() -> Generator[None, MagicMock]: """Mock an Ecobee object.""" ecobee = MagicMock() ecobee.request_pin.return_value = True ecobee.refresh_tokens.return_value = True + ecobee.thermostats = load_json_object_fixture("ecobee-data.json", "ecobee")[ + "thermostatList" + ] + ecobee.get_thermostat = lambda index: ecobee.thermostats[index] ecobee.config = {ECOBEE_API_KEY: "mocked_key", ECOBEE_REFRESH_TOKEN: "mocked_token"} with patch("homeassistant.components.ecobee.Ecobee", return_value=ecobee): diff --git a/tests/components/ecobee/test_notify.py b/tests/components/ecobee/test_notify.py new file mode 100644 index 00000000000..c66f04c752a --- /dev/null +++ b/tests/components/ecobee/test_notify.py @@ -0,0 +1,57 @@ +"""Test Ecobee notify service.""" + +from unittest.mock import MagicMock + +from homeassistant.components.ecobee import DOMAIN +from homeassistant.components.notify import ( + DOMAIN as NOTIFY_DOMAIN, + SERVICE_SEND_MESSAGE, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir + +from .common import setup_platform + +THERMOSTAT_ID = 0 + + +async def test_notify_entity_service( + hass: HomeAssistant, + mock_ecobee: MagicMock, +) -> None: + """Test the notify entity service.""" + await setup_platform(hass, NOTIFY_DOMAIN) + + entity_id = "notify.ecobee" + state = hass.states.get(entity_id) + assert state is not None + assert hass.services.has_service(NOTIFY_DOMAIN, SERVICE_SEND_MESSAGE) + await hass.services.async_call( + NOTIFY_DOMAIN, + SERVICE_SEND_MESSAGE, + service_data={"entity_id": entity_id, "message": "It is too cold!"}, + blocking=True, + ) + await hass.async_block_till_done() + mock_ecobee.send_message.assert_called_with(THERMOSTAT_ID, "It is too cold!") + + +async def test_legacy_notify_service( + hass: HomeAssistant, + mock_ecobee: MagicMock, + issue_registry: ir.IssueRegistry, +) -> None: + """Test the legacy notify service.""" + await setup_platform(hass, NOTIFY_DOMAIN) + + assert hass.services.has_service(NOTIFY_DOMAIN, DOMAIN) + await hass.services.async_call( + NOTIFY_DOMAIN, + DOMAIN, + service_data={"message": "It is too cold!", "target": THERMOSTAT_ID}, + blocking=True, + ) + await hass.async_block_till_done() + mock_ecobee.send_message.assert_called_with(THERMOSTAT_ID, "It is too cold!") + mock_ecobee.send_message.reset_mock() + assert len(issue_registry.issues) == 1 diff --git a/tests/components/ecobee/test_repairs.py b/tests/components/ecobee/test_repairs.py new file mode 100644 index 00000000000..19fdc6f7bba --- /dev/null +++ b/tests/components/ecobee/test_repairs.py @@ -0,0 +1,79 @@ +"""Test repairs for Ecobee integration.""" + +from http import HTTPStatus +from unittest.mock import MagicMock + +from homeassistant.components.ecobee import DOMAIN +from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN +from homeassistant.components.repairs.issue_handler import ( + async_process_repairs_platforms, +) +from homeassistant.components.repairs.websocket_api import ( + RepairsFlowIndexView, + RepairsFlowResourceView, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir + +from .common import setup_platform + +from tests.typing import ClientSessionGenerator + +THERMOSTAT_ID = 0 + + +async def test_ecobee_repair_flow( + hass: HomeAssistant, + mock_ecobee: MagicMock, + hass_client: ClientSessionGenerator, + issue_registry: ir.IssueRegistry, +) -> None: + """Test the ecobee notify service repair flow is triggered.""" + await setup_platform(hass, NOTIFY_DOMAIN) + await async_process_repairs_platforms(hass) + + http_client = await hass_client() + + # Simulate legacy service being used + assert hass.services.has_service(NOTIFY_DOMAIN, DOMAIN) + await hass.services.async_call( + NOTIFY_DOMAIN, + DOMAIN, + service_data={"message": "It is too cold!", "target": THERMOSTAT_ID}, + blocking=True, + ) + await hass.async_block_till_done() + mock_ecobee.send_message.assert_called_with(THERMOSTAT_ID, "It is too cold!") + mock_ecobee.send_message.reset_mock() + + # Assert the issue is present + assert issue_registry.async_get_issue( + domain=DOMAIN, + issue_id="migrate_notify", + ) + assert len(issue_registry.issues) == 1 + + url = RepairsFlowIndexView.url + resp = await http_client.post( + url, json={"handler": DOMAIN, "issue_id": "migrate_notify"} + ) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data["step_id"] == "confirm" + + url = RepairsFlowResourceView.url.format(flow_id=flow_id) + resp = await http_client.post(url) + assert resp.status == HTTPStatus.OK + data = await resp.json() + assert data["type"] == "create_entry" + # Test confirm step in repair flow + await hass.async_block_till_done() + + # Assert the issue is no longer present + assert not issue_registry.async_get_issue( + domain=DOMAIN, + issue_id="migrate_notify", + ) + assert len(issue_registry.issues) == 0 From 72ed16c3e08a4311a5dbe5a46d3f6bacecee394d Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 23 Apr 2024 23:20:34 +0200 Subject: [PATCH 811/967] Update quality scale mqtt integration to platinum (#116059) --- homeassistant/components/mqtt/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/manifest.json b/homeassistant/components/mqtt/manifest.json index 5f923868270..34370c82507 100644 --- a/homeassistant/components/mqtt/manifest.json +++ b/homeassistant/components/mqtt/manifest.json @@ -6,6 +6,6 @@ "dependencies": ["file_upload", "http"], "documentation": "https://www.home-assistant.io/integrations/mqtt", "iot_class": "local_push", - "quality_scale": "gold", + "quality_scale": "platinum", "requirements": ["paho-mqtt==1.6.1"] } From 35db2e41015b8d861e92f3da191b36f3c1cb3810 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 23 Apr 2024 23:42:17 +0200 Subject: [PATCH 812/967] Complete test coverage for Tankerkonig (#115920) * complete tests * update snapshots after rebase --- .coveragerc | 5 -- tests/components/tankerkoenig/conftest.py | 24 +------ tests/components/tankerkoenig/const.py | 48 ++++++++++++++ .../snapshots/test_binary_sensor.ambr | 9 +++ .../tankerkoenig/snapshots/test_sensor.ambr | 52 +++++++++++++++ .../tankerkoenig/test_binary_sensor.py | 25 +++++++ .../tankerkoenig/test_config_flow.py | 22 +++++-- .../tankerkoenig/test_coordinator.py | 45 ++++++++++++- tests/components/tankerkoenig/test_sensor.py | 65 +++++++++++++++++++ 9 files changed, 262 insertions(+), 33 deletions(-) create mode 100644 tests/components/tankerkoenig/snapshots/test_binary_sensor.ambr create mode 100644 tests/components/tankerkoenig/snapshots/test_sensor.ambr create mode 100644 tests/components/tankerkoenig/test_binary_sensor.py create mode 100644 tests/components/tankerkoenig/test_sensor.py diff --git a/.coveragerc b/.coveragerc index e4fe305a3bf..9eb32f7cda8 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1408,11 +1408,6 @@ omit = homeassistant/components/tado/water_heater.py homeassistant/components/tami4/button.py homeassistant/components/tank_utility/sensor.py - homeassistant/components/tankerkoenig/__init__.py - homeassistant/components/tankerkoenig/binary_sensor.py - homeassistant/components/tankerkoenig/coordinator.py - homeassistant/components/tankerkoenig/entity.py - homeassistant/components/tankerkoenig/sensor.py homeassistant/components/tapsaff/binary_sensor.py homeassistant/components/tautulli/__init__.py homeassistant/components/tautulli/coordinator.py diff --git a/tests/components/tankerkoenig/conftest.py b/tests/components/tankerkoenig/conftest.py index 4400082a45f..1a3dcb6f991 100644 --- a/tests/components/tankerkoenig/conftest.py +++ b/tests/components/tankerkoenig/conftest.py @@ -6,20 +6,11 @@ from unittest.mock import AsyncMock, patch import pytest from homeassistant.components.tankerkoenig import DOMAIN -from homeassistant.components.tankerkoenig.const import CONF_FUEL_TYPES, CONF_STATIONS -from homeassistant.const import ( - CONF_API_KEY, - CONF_LATITUDE, - CONF_LOCATION, - CONF_LONGITUDE, - CONF_NAME, - CONF_RADIUS, - CONF_SHOW_ON_MAP, -) +from homeassistant.const import CONF_SHOW_ON_MAP from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from .const import NEARBY_STATIONS, PRICES, STATION +from .const import CONFIG_DATA, NEARBY_STATIONS, PRICES, STATION from tests.common import MockConfigEntry @@ -55,16 +46,7 @@ async def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry: options={ CONF_SHOW_ON_MAP: True, }, - data={ - CONF_NAME: "Home", - CONF_API_KEY: "269534f6-xxxx-xxxx-xxxx-yyyyzzzzxxxx", - CONF_FUEL_TYPES: ["e5"], - CONF_LOCATION: {CONF_LATITUDE: 51.0, CONF_LONGITUDE: 13.0}, - CONF_RADIUS: 2.0, - CONF_STATIONS: [ - "3bcd61da-xxxx-xxxx-xxxx-19d5523a7ae8", - ], - }, + data=CONFIG_DATA, ) diff --git a/tests/components/tankerkoenig/const.py b/tests/components/tankerkoenig/const.py index 9ec64eb79a9..2c28753a7f3 100644 --- a/tests/components/tankerkoenig/const.py +++ b/tests/components/tankerkoenig/const.py @@ -2,6 +2,16 @@ from aiotankerkoenig import PriceInfo, Station, Status +from homeassistant.components.tankerkoenig.const import CONF_FUEL_TYPES, CONF_STATIONS +from homeassistant.const import ( + CONF_API_KEY, + CONF_LATITUDE, + CONF_LOCATION, + CONF_LONGITUDE, + CONF_NAME, + CONF_RADIUS, +) + NEARBY_STATIONS = [ Station( id="3bcd61da-xxxx-xxxx-xxxx-19d5523a7ae8", @@ -49,6 +59,25 @@ STATION = Station( state="xxXX", ) +STATION_MISSING_FUELTYPE = Station( + id="3bcd61da-xxxx-xxxx-xxxx-19d5523a7ae8", + name="Station ABC", + brand="Station", + street="Somewhere Street", + house_number="1", + post_code=1234, + place="Somewhere", + opening_times=[], + overrides=[], + whole_day=True, + is_open=True, + e5=1.719, + e10=1.659, + lat=51.1, + lng=13.1, + state="xxXX", +) + PRICES = { "3bcd61da-xxxx-xxxx-xxxx-19d5523a7ae8": PriceInfo( status=Status.OPEN, @@ -57,3 +86,22 @@ PRICES = { diesel=1.659, ), } + +PRICES_MISSING_FUELTYPE = { + "3bcd61da-xxxx-xxxx-xxxx-19d5523a7ae8": PriceInfo( + status=Status.OPEN, + e5=1.719, + e10=1.659, + ), +} + +CONFIG_DATA = { + CONF_NAME: "Home", + CONF_API_KEY: "269534f6-xxxx-xxxx-xxxx-yyyyzzzzxxxx", + CONF_FUEL_TYPES: ["e5"], + CONF_LOCATION: {CONF_LATITUDE: 51.0, CONF_LONGITUDE: 13.0}, + CONF_RADIUS: 2.0, + CONF_STATIONS: [ + "3bcd61da-xxxx-xxxx-xxxx-19d5523a7ae8", + ], +} diff --git a/tests/components/tankerkoenig/snapshots/test_binary_sensor.ambr b/tests/components/tankerkoenig/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..6b454820b05 --- /dev/null +++ b/tests/components/tankerkoenig/snapshots/test_binary_sensor.ambr @@ -0,0 +1,9 @@ +# serializer version: 1 +# name: test_binary_sensor + ReadOnlyDict({ + 'device_class': 'door', + 'friendly_name': 'Station Somewhere Street 1 Status', + 'latitude': 51.1, + 'longitude': 13.1, + }) +# --- diff --git a/tests/components/tankerkoenig/snapshots/test_sensor.ambr b/tests/components/tankerkoenig/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..ec9a72e141d --- /dev/null +++ b/tests/components/tankerkoenig/snapshots/test_sensor.ambr @@ -0,0 +1,52 @@ +# serializer version: 1 +# name: test_sensor + ReadOnlyDict({ + 'attribution': 'Data provided by https://www.tankerkoenig.de', + 'brand': 'Station', + 'city': 'Somewhere', + 'friendly_name': 'Station Somewhere Street 1 Super E10', + 'fuel_type': , + 'house_number': '1', + 'latitude': 51.1, + 'longitude': 13.1, + 'postcode': 1234, + 'state_class': , + 'station_name': 'Station ABC', + 'street': 'Somewhere Street', + 'unit_of_measurement': '€', + }) +# --- +# name: test_sensor.1 + ReadOnlyDict({ + 'attribution': 'Data provided by https://www.tankerkoenig.de', + 'brand': 'Station', + 'city': 'Somewhere', + 'friendly_name': 'Station Somewhere Street 1 Super', + 'fuel_type': , + 'house_number': '1', + 'latitude': 51.1, + 'longitude': 13.1, + 'postcode': 1234, + 'state_class': , + 'station_name': 'Station ABC', + 'street': 'Somewhere Street', + 'unit_of_measurement': '€', + }) +# --- +# name: test_sensor.2 + ReadOnlyDict({ + 'attribution': 'Data provided by https://www.tankerkoenig.de', + 'brand': 'Station', + 'city': 'Somewhere', + 'friendly_name': 'Station Somewhere Street 1 Diesel', + 'fuel_type': , + 'house_number': '1', + 'latitude': 51.1, + 'longitude': 13.1, + 'postcode': 1234, + 'state_class': , + 'station_name': 'Station ABC', + 'street': 'Somewhere Street', + 'unit_of_measurement': '€', + }) +# --- diff --git a/tests/components/tankerkoenig/test_binary_sensor.py b/tests/components/tankerkoenig/test_binary_sensor.py new file mode 100644 index 00000000000..c103f2d26ff --- /dev/null +++ b/tests/components/tankerkoenig/test_binary_sensor.py @@ -0,0 +1,25 @@ +"""Tests for the Tankerkoening integration.""" + +from __future__ import annotations + +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.const import STATE_ON +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +@pytest.mark.usefixtures("setup_integration") +async def test_binary_sensor( + hass: HomeAssistant, + config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test the tankerkoenig binary sensors.""" + + state = hass.states.get("binary_sensor.station_somewhere_street_1_status") + assert state + assert state.state == STATE_ON + assert state.attributes == snapshot diff --git a/tests/components/tankerkoenig/test_config_flow.py b/tests/components/tankerkoenig/test_config_flow.py index b255491cb31..022b49fd3f8 100644 --- a/tests/components/tankerkoenig/test_config_flow.py +++ b/tests/components/tankerkoenig/test_config_flow.py @@ -1,6 +1,6 @@ """Tests for Tankerkoenig config flow.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, patch from aiotankerkoenig.exceptions import TankerkoenigInvalidKeyError @@ -21,6 +21,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.setup import async_setup_component from .const import NEARBY_STATIONS @@ -208,7 +209,7 @@ async def test_reauth(hass: HomeAssistant, config_entry: MockConfigEntry) -> Non assert entry.data[CONF_API_KEY] == "269534f6-aaaa-bbbb-cccc-yyyyzzzzxxxx" -async def test_options_flow(hass: HomeAssistant) -> None: +async def test_options_flow(hass: HomeAssistant, tankerkoenig: AsyncMock) -> None: """Test options flow.""" mock_config = MockConfigEntry( @@ -218,10 +219,17 @@ async def test_options_flow(hass: HomeAssistant) -> None: unique_id=f"{DOMAIN}_{MOCK_USER_DATA[CONF_LOCATION][CONF_LATITUDE]}_{MOCK_USER_DATA[CONF_LOCATION][CONF_LONGITUDE]}", ) mock_config.add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() - with patch( - "homeassistant.components.tankerkoenig.config_flow.Tankerkoenig.nearby_stations", - return_value=NEARBY_STATIONS, + with ( + patch( + "homeassistant.components.tankerkoenig.config_flow.Tankerkoenig.nearby_stations", + return_value=NEARBY_STATIONS, + ), + patch( + "homeassistant.config_entries.ConfigEntries.async_reload" + ) as mock_async_reload, ): result = await hass.config_entries.options.async_init(mock_config.entry_id) assert result["type"] is FlowResultType.FORM @@ -237,6 +245,10 @@ async def test_options_flow(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.CREATE_ENTRY assert not mock_config.options[CONF_SHOW_ON_MAP] + await hass.async_block_till_done() + + assert mock_async_reload.call_count == 1 + async def test_options_flow_error(hass: HomeAssistant) -> None: """Test options flow.""" diff --git a/tests/components/tankerkoenig/test_coordinator.py b/tests/components/tankerkoenig/test_coordinator.py index 1e8991f3f9c..3ba0dc31c5f 100644 --- a/tests/components/tankerkoenig/test_coordinator.py +++ b/tests/components/tankerkoenig/test_coordinator.py @@ -15,14 +15,20 @@ import pytest from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.components.tankerkoenig.const import DEFAULT_SCAN_INTERVAL, DOMAIN +from homeassistant.components.tankerkoenig.const import ( + CONF_STATIONS, + DEFAULT_SCAN_INTERVAL, + DOMAIN, +) from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import ATTR_ID, STATE_UNAVAILABLE +from homeassistant.const import ATTR_ID, CONF_SHOW_ON_MAP, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant 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 .const import CONFIG_DATA + from tests.common import MockConfigEntry, async_fire_time_changed @@ -190,3 +196,38 @@ async def test_automatic_registry_cleanup( len(dr.async_entries_for_config_entry(device_registry, config_entry.entry_id)) == 1 ) + + +async def test_many_stations_warning( + hass: HomeAssistant, tankerkoenig: AsyncMock, caplog: pytest.LogCaptureFixture +) -> None: + """Test the warning about morethan 10 selected stations.""" + mock_config = MockConfigEntry( + domain=DOMAIN, + data={ + **CONFIG_DATA, + CONF_STATIONS: [ + "3bcd61da-xxxx-xxxx-xxxx-19d5523a7ae8", + "36b4b812-xxxx-xxxx-xxxx-c51735325858", + "54e2b642-xxxx-xxxx-xxxx-87cd4e9867f1", + "11b5c130-xxxx-xxxx-xxxx-856b8489b528", + "a9137924-xxxx-xxxx-xxxx-7029d7eb073f", + "57c6d275-xxxx-xxxx-xxxx-7f6ad9e6d638", + "bbc3c3a2-xxxx-xxxx-xxxx-840cc3d496b6", + "1db63dd9-xxxx-xxxx-xxxx-a889b53cbc65", + "18d7262e-xxxx-xxxx-xxxx-4a61ad302e14", + "a8041aa3-xxxx-xxxx-xxxx-7c6b180e5a40", + "739aa0eb-xxxx-xxxx-xxxx-a3d7b6c8a42f", + "9ad9fb26-xxxx-xxxx-xxxx-84e6a02b3096", + "74267867-xxxx-xxxx-xxxx-74ce3d45882c", + "86657222-xxxx-xxxx-xxxx-a2b795ab3cf9", + ], + }, + options={CONF_SHOW_ON_MAP: True}, + unique_id="51.0_13.0", + ) + mock_config.add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + assert "Found more than 10 stations to check" in caplog.text diff --git a/tests/components/tankerkoenig/test_sensor.py b/tests/components/tankerkoenig/test_sensor.py new file mode 100644 index 00000000000..788c1de7021 --- /dev/null +++ b/tests/components/tankerkoenig/test_sensor.py @@ -0,0 +1,65 @@ +"""Tests for the Tankerkoening integration.""" + +from __future__ import annotations + +from unittest.mock import AsyncMock + +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.components.tankerkoenig import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from .const import PRICES_MISSING_FUELTYPE, STATION_MISSING_FUELTYPE + +from tests.common import MockConfigEntry + + +@pytest.mark.usefixtures("setup_integration") +async def test_sensor( + hass: HomeAssistant, + tankerkoenig: AsyncMock, + config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test the tankerkoenig sensors.""" + + state = hass.states.get("sensor.station_somewhere_street_1_super_e10") + assert state + assert state.state == "1.659" + assert state.attributes == snapshot + + state = hass.states.get("sensor.station_somewhere_street_1_super") + assert state + assert state.state == "1.719" + assert state.attributes == snapshot + + state = hass.states.get("sensor.station_somewhere_street_1_diesel") + assert state + assert state.state == "1.659" + assert state.attributes == snapshot + + +async def test_sensor_missing_fueltype( + hass: HomeAssistant, + tankerkoenig: AsyncMock, + config_entry: MockConfigEntry, +) -> None: + """Test the tankerkoenig sensors.""" + tankerkoenig.station_details.return_value = STATION_MISSING_FUELTYPE + tankerkoenig.prices.return_value = PRICES_MISSING_FUELTYPE + + config_entry.add_to_hass(hass) + + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + state = hass.states.get("sensor.station_somewhere_street_1_super_e10") + assert state + + state = hass.states.get("sensor.station_somewhere_street_1_super") + assert state + + state = hass.states.get("sensor.station_somewhere_street_1_diesel") + assert not state From 62dadc47ff442ceea8f741c0eb054175cb1e2a34 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Apr 2024 00:02:31 +0200 Subject: [PATCH 813/967] Bump github/codeql-action from 3.25.1 to 3.25.2 (#116016) 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 6a366a7ab8d..d1393c97462 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,11 +24,11 @@ jobs: uses: actions/checkout@v4.1.3 - name: Initialize CodeQL - uses: github/codeql-action/init@v3.25.1 + uses: github/codeql-action/init@v3.25.2 with: languages: python - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3.25.1 + uses: github/codeql-action/analyze@v3.25.2 with: category: "/language:python" From f9c2cd73f555d8e9e98f327d8cee6e86ce367a04 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 24 Apr 2024 02:19:18 +0200 Subject: [PATCH 814/967] Fix non-thread-safe operations in media_extractor (#116065) --- homeassistant/components/media_extractor/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_extractor/__init__.py b/homeassistant/components/media_extractor/__init__.py index 139acf06cf6..56b768c26a2 100644 --- a/homeassistant/components/media_extractor/__init__.py +++ b/homeassistant/components/media_extractor/__init__.py @@ -55,7 +55,7 @@ CONFIG_SCHEMA = vol.Schema( ) -def setup(hass: HomeAssistant, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the media extractor service.""" async def extract_media_url(call: ServiceCall) -> ServiceResponse: @@ -114,7 +114,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: supports_response=SupportsResponse.ONLY, ) - hass.services.register( + hass.services.async_register( DOMAIN, SERVICE_PLAY_MEDIA, play_media, From e3016b131a28ffb44144afc37ce0f1770e899fba Mon Sep 17 00:00:00 2001 From: David Friedland Date: Tue, 23 Apr 2024 18:22:03 -0700 Subject: [PATCH 815/967] Add Event support to ESPHome components (#116061) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .../components/esphome/entry_data.py | 5 +- homeassistant/components/esphome/event.py | 48 +++++++++++++++++++ tests/components/esphome/test_event.py | 38 +++++++++++++++ 3 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/esphome/event.py create mode 100644 tests/components/esphome/test_event.py diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index a840fc3a17e..7316c09cc5e 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -24,6 +24,8 @@ from aioesphomeapi import ( DeviceInfo, EntityInfo, EntityState, + Event, + EventInfo, FanInfo, LightInfo, LockInfo, @@ -70,6 +72,7 @@ INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], Platform] = { CoverInfo: Platform.COVER, DateInfo: Platform.DATE, DateTimeInfo: Platform.DATETIME, + EventInfo: Platform.EVENT, FanInfo: Platform.FAN, LightInfo: Platform.LIGHT, LockInfo: Platform.LOCK, @@ -345,7 +348,7 @@ class RuntimeEntryData: if ( current_state == state and subscription_key not in stale_state - and state_type is not CameraState + and state_type not in (CameraState, Event) and not ( state_type is SensorState and (platform_info := self.info.get(SensorInfo)) diff --git a/homeassistant/components/esphome/event.py b/homeassistant/components/esphome/event.py new file mode 100644 index 00000000000..3c7331beba0 --- /dev/null +++ b/homeassistant/components/esphome/event.py @@ -0,0 +1,48 @@ +"""Support for ESPHome event components.""" + +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 + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, 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): + """An event implementation for ESPHome.""" + + @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 + if event_types := static_info.event_types: + self._attr_event_types = event_types + self._attr_device_class = try_parse_enum( + EventDeviceClass, static_info.device_class + ) + + @callback + def _on_state_update(self) -> None: + self._update_state_from_entry_data() + self._trigger_event(self._state.event_type) + self.async_write_ha_state() diff --git a/tests/components/esphome/test_event.py b/tests/components/esphome/test_event.py new file mode 100644 index 00000000000..c17dc4d98a9 --- /dev/null +++ b/tests/components/esphome/test_event.py @@ -0,0 +1,38 @@ +"""Test ESPHome Events.""" + +from aioesphomeapi import APIClient, Event, EventInfo +import pytest + +from homeassistant.components.event import EventDeviceClass +from homeassistant.core import HomeAssistant + + +@pytest.mark.freeze_time("2024-04-24 00:00:00+00:00") +async def test_generic_event_entity( + hass: HomeAssistant, + mock_client: APIClient, + mock_generic_device_entry, +) -> None: + """Test a generic event entity.""" + entity_info = [ + EventInfo( + object_id="myevent", + key=1, + name="my event", + unique_id="my_event", + event_types=["type1", "type2"], + device_class=EventDeviceClass.BUTTON, + ) + ] + states = [Event(key=1, event_type="type1")] + 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("event.test_myevent") + assert state is not None + assert state.state == "2024-04-24T00:00:00.000+00:00" + assert state.attributes["event_type"] == "type1" From f2336a5a3abff6c6ed17baf17c5c931631ec5420 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 24 Apr 2024 03:31:44 +0200 Subject: [PATCH 816/967] Fix non-thread-safe operation in harmony (#116070) Fix unsafe thread operation in harmony https://github.com/home-assistant/core/actions/runs/8808429751/job/24177716644?pr=116066 --- homeassistant/components/harmony/entity.py | 13 +++++++++---- homeassistant/components/harmony/remote.py | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/harmony/entity.py b/homeassistant/components/harmony/entity.py index 99b5744e0ed..8bfa9fbad4d 100644 --- a/homeassistant/components/harmony/entity.py +++ b/homeassistant/components/harmony/entity.py @@ -6,6 +6,7 @@ from collections.abc import Callable from datetime import datetime import logging +from homeassistant.core import callback from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_call_later @@ -38,7 +39,7 @@ class HarmonyEntity(Entity): _LOGGER.debug("%s: connected to the HUB", self._data.name) self.async_write_ha_state() - self._clear_disconnection_delay() + self._async_clear_disconnection_delay() async def async_got_disconnected(self, _: str | None = None) -> None: """Notification that we're disconnected from the HUB.""" @@ -46,15 +47,19 @@ class HarmonyEntity(Entity): # We're going to wait for 10 seconds before announcing we're # unavailable, this to allow a reconnection to happen. self._unsub_mark_disconnected = async_call_later( - self.hass, TIME_MARK_DISCONNECTED, self._mark_disconnected_if_unavailable + self.hass, + TIME_MARK_DISCONNECTED, + self._async_mark_disconnected_if_unavailable, ) - def _clear_disconnection_delay(self) -> None: + @callback + def _async_clear_disconnection_delay(self) -> None: if self._unsub_mark_disconnected: self._unsub_mark_disconnected() self._unsub_mark_disconnected = None - def _mark_disconnected_if_unavailable(self, _: datetime) -> None: + @callback + def _async_mark_disconnected_if_unavailable(self, _: datetime) -> None: self._unsub_mark_disconnected = None if not self.available: # Still disconnected. Let the state engine know. diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index c6b2e9be718..0c9bdcb9c6e 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -138,7 +138,7 @@ class HarmonyRemote(HarmonyEntity, RemoteEntity, RestoreEntity): _LOGGER.debug("%s: Harmony Hub added", self._data.name) - self.async_on_remove(self._clear_disconnection_delay) + self.async_on_remove(self._async_clear_disconnection_delay) self._setup_callbacks() self.async_on_remove( From b1b8b8ba00c5e2a55e1b65fd1c434cb2d89659ed Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 24 Apr 2024 03:32:07 +0200 Subject: [PATCH 817/967] Fix non-thread-safe operations in wake_on_lan (#116069) Fix unsafe thread operations in wake_on_lan https://github.com/home-assistant/core/actions/runs/8808429751/job/24177715837?pr=116066 --- homeassistant/components/wake_on_lan/switch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/wake_on_lan/switch.py b/homeassistant/components/wake_on_lan/switch.py index a0b54fd8db0..e5c3a055310 100644 --- a/homeassistant/components/wake_on_lan/switch.py +++ b/homeassistant/components/wake_on_lan/switch.py @@ -129,7 +129,7 @@ class WolSwitch(SwitchEntity): if self._attr_assumed_state: self._state = True - self.async_write_ha_state() + self.schedule_update_ha_state() def turn_off(self, **kwargs: Any) -> None: """Turn the device off if an off action is present.""" @@ -138,7 +138,7 @@ class WolSwitch(SwitchEntity): if self._attr_assumed_state: self._state = False - self.async_write_ha_state() + self.schedule_update_ha_state() def update(self) -> None: """Check if device is on and update the state. Only called if assumed state is false.""" From 9d54aa205be26e172e89ed40d5be520ecf8c1caf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 24 Apr 2024 03:33:19 +0200 Subject: [PATCH 818/967] Fix non-thread-safe operations in html5 (#116068) Fix non thread-safe calls in html5 https://github.com/home-assistant/core/actions/runs/8808425552/job/24177668764?pr=116055 --- homeassistant/components/html5/notify.py | 4 +- tests/components/html5/test_notify.py | 232 ++++++++++++----------- 2 files changed, 122 insertions(+), 114 deletions(-) diff --git a/homeassistant/components/html5/notify.py b/homeassistant/components/html5/notify.py index 782340dffa6..6049f8e2434 100644 --- a/homeassistant/components/html5/notify.py +++ b/homeassistant/components/html5/notify.py @@ -165,7 +165,7 @@ HTML5_SHOWNOTIFICATION_PARAMETERS = ( ) -def get_service( +async def async_get_service( hass: HomeAssistant, config: ConfigType, discovery_info: DiscoveryInfoType | None = None, @@ -173,7 +173,7 @@ def get_service( """Get the HTML5 push notification service.""" json_path = hass.config.path(REGISTRATIONS_FILE) - registrations = _load_config(json_path) + registrations = await hass.async_add_executor_job(_load_config, json_path) vapid_pub_key = config[ATTR_VAPID_PUB_KEY] vapid_prv_key = config[ATTR_VAPID_PRV_KEY] diff --git a/tests/components/html5/test_notify.py b/tests/components/html5/test_notify.py index 6763708cc38..ec14b38cd69 100644 --- a/tests/components/html5/test_notify.py +++ b/tests/components/html5/test_notify.py @@ -2,7 +2,7 @@ from http import HTTPStatus import json -from unittest.mock import MagicMock, mock_open, patch +from unittest.mock import mock_open, patch from aiohttp.hdrs import AUTHORIZATION @@ -83,166 +83,174 @@ async def mock_client(hass, hass_client, registrations=None): return await hass_client() -class TestHtml5Notify: - """Tests for HTML5 notify platform.""" +async def test_get_service_with_no_json(hass: HomeAssistant): + """Test empty json file.""" + await async_setup_component(hass, "http", {}) + m = mock_open() + with patch("homeassistant.util.json.open", m, create=True): + service = await html5.async_get_service(hass, VAPID_CONF) - def test_get_service_with_no_json(self): - """Test empty json file.""" - hass = MagicMock() + assert service is not None - m = mock_open() - with patch("homeassistant.util.json.open", m, create=True): - service = html5.get_service(hass, VAPID_CONF) - assert service is not None +@patch("homeassistant.components.html5.notify.WebPusher") +async def test_dismissing_message(mock_wp, hass: HomeAssistant): + """Test dismissing message.""" + await async_setup_component(hass, "http", {}) + mock_wp().send().status_code = 201 - @patch("homeassistant.components.html5.notify.WebPusher") - def test_dismissing_message(self, mock_wp): - """Test dismissing message.""" - hass = MagicMock() - mock_wp().send().status_code = 201 + data = {"device": SUBSCRIPTION_1} - data = {"device": SUBSCRIPTION_1} + m = mock_open(read_data=json.dumps(data)) + with patch("homeassistant.util.json.open", m, create=True): + service = await html5.async_get_service(hass, VAPID_CONF) + service.hass = hass - m = mock_open(read_data=json.dumps(data)) - with patch("homeassistant.util.json.open", m, create=True): - service = html5.get_service(hass, VAPID_CONF) + assert service is not None - assert service is not None + await service.async_dismiss(target=["device", "non_existing"], data={"tag": "test"}) - service.dismiss(target=["device", "non_existing"], data={"tag": "test"}) + assert len(mock_wp.mock_calls) == 4 - assert len(mock_wp.mock_calls) == 4 + # WebPusher constructor + assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_1["subscription"] - # WebPusher constructor - assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_1["subscription"] + # Call to send + payload = json.loads(mock_wp.mock_calls[3][2]["data"]) - # Call to send - payload = json.loads(mock_wp.mock_calls[3][2]["data"]) + assert payload["dismiss"] is True + assert payload["tag"] == "test" - assert payload["dismiss"] is True - assert payload["tag"] == "test" - @patch("homeassistant.components.html5.notify.WebPusher") - def test_sending_message(self, mock_wp): - """Test sending message.""" - hass = MagicMock() - mock_wp().send().status_code = 201 +@patch("homeassistant.components.html5.notify.WebPusher") +async def test_sending_message(mock_wp, hass: HomeAssistant): + """Test sending message.""" + await async_setup_component(hass, "http", {}) + mock_wp().send().status_code = 201 - data = {"device": SUBSCRIPTION_1} + data = {"device": SUBSCRIPTION_1} - m = mock_open(read_data=json.dumps(data)) - with patch("homeassistant.util.json.open", m, create=True): - service = html5.get_service(hass, VAPID_CONF) + m = mock_open(read_data=json.dumps(data)) + with patch("homeassistant.util.json.open", m, create=True): + service = await html5.async_get_service(hass, VAPID_CONF) + service.hass = hass - assert service is not None + assert service is not None - service.send_message( - "Hello", target=["device", "non_existing"], data={"icon": "beer.png"} - ) + await service.async_send_message( + "Hello", target=["device", "non_existing"], data={"icon": "beer.png"} + ) - assert len(mock_wp.mock_calls) == 4 + assert len(mock_wp.mock_calls) == 4 - # WebPusher constructor - assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_1["subscription"] + # WebPusher constructor + assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_1["subscription"] - # Call to send - payload = json.loads(mock_wp.mock_calls[3][2]["data"]) + # Call to send + payload = json.loads(mock_wp.mock_calls[3][2]["data"]) - assert payload["body"] == "Hello" - assert payload["icon"] == "beer.png" + assert payload["body"] == "Hello" + assert payload["icon"] == "beer.png" - @patch("homeassistant.components.html5.notify.WebPusher") - def test_fcm_key_include(self, mock_wp): - """Test if the FCM header is included.""" - hass = MagicMock() - mock_wp().send().status_code = 201 - data = {"chrome": SUBSCRIPTION_5} +@patch("homeassistant.components.html5.notify.WebPusher") +async def test_fcm_key_include(mock_wp, hass: HomeAssistant): + """Test if the FCM header is included.""" + await async_setup_component(hass, "http", {}) + mock_wp().send().status_code = 201 - m = mock_open(read_data=json.dumps(data)) - with patch("homeassistant.util.json.open", m, create=True): - service = html5.get_service(hass, VAPID_CONF) + data = {"chrome": SUBSCRIPTION_5} - assert service is not None + m = mock_open(read_data=json.dumps(data)) + with patch("homeassistant.util.json.open", m, create=True): + service = await html5.async_get_service(hass, VAPID_CONF) + service.hass = hass - service.send_message("Hello", target=["chrome"]) + assert service is not None - assert len(mock_wp.mock_calls) == 4 - # WebPusher constructor - assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_5["subscription"] + await service.async_send_message("Hello", target=["chrome"]) - # Get the keys passed to the WebPusher's send method - assert mock_wp.mock_calls[3][2]["headers"]["Authorization"] is not None + assert len(mock_wp.mock_calls) == 4 + # WebPusher constructor + assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_5["subscription"] - @patch("homeassistant.components.html5.notify.WebPusher") - def test_fcm_send_with_unknown_priority(self, mock_wp): - """Test if the gcm_key is only included for GCM endpoints.""" - hass = MagicMock() - mock_wp().send().status_code = 201 + # Get the keys passed to the WebPusher's send method + assert mock_wp.mock_calls[3][2]["headers"]["Authorization"] is not None - data = {"chrome": SUBSCRIPTION_5} - m = mock_open(read_data=json.dumps(data)) - with patch("homeassistant.util.json.open", m, create=True): - service = html5.get_service(hass, VAPID_CONF) +@patch("homeassistant.components.html5.notify.WebPusher") +async def test_fcm_send_with_unknown_priority(mock_wp, hass: HomeAssistant): + """Test if the gcm_key is only included for GCM endpoints.""" + await async_setup_component(hass, "http", {}) + mock_wp().send().status_code = 201 - assert service is not None + data = {"chrome": SUBSCRIPTION_5} - service.send_message("Hello", target=["chrome"], priority="undefined") + m = mock_open(read_data=json.dumps(data)) + with patch("homeassistant.util.json.open", m, create=True): + service = await html5.async_get_service(hass, VAPID_CONF) + service.hass = hass - assert len(mock_wp.mock_calls) == 4 - # WebPusher constructor - assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_5["subscription"] + assert service is not None - # Get the keys passed to the WebPusher's send method - assert mock_wp.mock_calls[3][2]["headers"]["priority"] == "normal" + await service.async_send_message("Hello", target=["chrome"], priority="undefined") - @patch("homeassistant.components.html5.notify.WebPusher") - def test_fcm_no_targets(self, mock_wp): - """Test if the gcm_key is only included for GCM endpoints.""" - hass = MagicMock() - mock_wp().send().status_code = 201 + assert len(mock_wp.mock_calls) == 4 + # WebPusher constructor + assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_5["subscription"] - data = {"chrome": SUBSCRIPTION_5} + # Get the keys passed to the WebPusher's send method + assert mock_wp.mock_calls[3][2]["headers"]["priority"] == "normal" - m = mock_open(read_data=json.dumps(data)) - with patch("homeassistant.util.json.open", m, create=True): - service = html5.get_service(hass, VAPID_CONF) - assert service is not None +@patch("homeassistant.components.html5.notify.WebPusher") +async def test_fcm_no_targets(mock_wp, hass: HomeAssistant): + """Test if the gcm_key is only included for GCM endpoints.""" + await async_setup_component(hass, "http", {}) + mock_wp().send().status_code = 201 - service.send_message("Hello") + data = {"chrome": SUBSCRIPTION_5} - assert len(mock_wp.mock_calls) == 4 - # WebPusher constructor - assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_5["subscription"] + m = mock_open(read_data=json.dumps(data)) + with patch("homeassistant.util.json.open", m, create=True): + service = await html5.async_get_service(hass, VAPID_CONF) + service.hass = hass - # Get the keys passed to the WebPusher's send method - assert mock_wp.mock_calls[3][2]["headers"]["priority"] == "normal" + assert service is not None - @patch("homeassistant.components.html5.notify.WebPusher") - def test_fcm_additional_data(self, mock_wp): - """Test if the gcm_key is only included for GCM endpoints.""" - hass = MagicMock() - mock_wp().send().status_code = 201 + await service.async_send_message("Hello") - data = {"chrome": SUBSCRIPTION_5} + assert len(mock_wp.mock_calls) == 4 + # WebPusher constructor + assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_5["subscription"] - m = mock_open(read_data=json.dumps(data)) - with patch("homeassistant.util.json.open", m, create=True): - service = html5.get_service(hass, VAPID_CONF) + # Get the keys passed to the WebPusher's send method + assert mock_wp.mock_calls[3][2]["headers"]["priority"] == "normal" - assert service is not None - service.send_message("Hello", data={"mykey": "myvalue"}) +@patch("homeassistant.components.html5.notify.WebPusher") +async def test_fcm_additional_data(mock_wp, hass: HomeAssistant): + """Test if the gcm_key is only included for GCM endpoints.""" + await async_setup_component(hass, "http", {}) + mock_wp().send().status_code = 201 - assert len(mock_wp.mock_calls) == 4 - # WebPusher constructor - assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_5["subscription"] + data = {"chrome": SUBSCRIPTION_5} - # Get the keys passed to the WebPusher's send method - assert mock_wp.mock_calls[3][2]["headers"]["priority"] == "normal" + m = mock_open(read_data=json.dumps(data)) + with patch("homeassistant.util.json.open", m, create=True): + service = await html5.async_get_service(hass, VAPID_CONF) + service.hass = hass + + assert service is not None + + await service.async_send_message("Hello", data={"mykey": "myvalue"}) + + assert len(mock_wp.mock_calls) == 4 + # WebPusher constructor + assert mock_wp.mock_calls[2][1][0] == SUBSCRIPTION_5["subscription"] + + # Get the keys passed to the WebPusher's send method + assert mock_wp.mock_calls[3][2]["headers"]["priority"] == "normal" async def test_registering_new_device_view( From 53a179088fe2b04804ac1f547333f2f65aea551b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 24 Apr 2024 03:36:05 +0200 Subject: [PATCH 819/967] Add debug mode to catch unsafe thread operations using core helpers (#115390) * adjust * adjust * fixes * one more * test * debug * move to config * cover * Update homeassistant/core.py * set debug from RuntimeConfig * reduce * fix message * raise * Update homeassistant/core.py * Update homeassistant/core.py * no flood check for raise * cover --- homeassistant/bootstrap.py | 2 ++ homeassistant/config.py | 5 ++++ homeassistant/const.py | 1 + homeassistant/core.py | 20 +++++++++++++- homeassistant/helpers/dispatcher.py | 3 +++ homeassistant/helpers/entity.py | 2 ++ homeassistant/helpers/frame.py | 25 +++++++++++++---- homeassistant/helpers/template.py | 2 ++ homeassistant/runner.py | 1 + homeassistant/util/async_.py | 3 +-- tests/helpers/test_dispatcher.py | 21 +++++++++++++++ tests/helpers/test_entity.py | 21 +++++++++++++++ tests/helpers/test_frame.py | 42 +++++++++++++++++++++++++++++ tests/helpers/test_template.py | 17 ++++++++++++ tests/test_bootstrap.py | 16 ++++++++++- tests/test_config.py | 2 ++ tests/test_core.py | 20 ++++++++++++++ tests/util/test_async.py | 4 ++- 18 files changed, 197 insertions(+), 10 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index afb364e6d2f..10ba0392f15 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -253,6 +253,8 @@ async def async_setup_hass( runtime_config.log_no_color, ) + if runtime_config.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 diff --git a/homeassistant/config.py b/homeassistant/config.py index 61b346944fa..abb29f6a1a1 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -39,6 +39,7 @@ from .const import ( CONF_CUSTOMIZE, CONF_CUSTOMIZE_DOMAIN, CONF_CUSTOMIZE_GLOB, + CONF_DEBUG, CONF_ELEVATION, CONF_EXTERNAL_URL, CONF_ID, @@ -391,6 +392,7 @@ CORE_CONFIG_SCHEMA = vol.All( vol.Optional(CONF_CURRENCY): _validate_currency, vol.Optional(CONF_COUNTRY): cv.country, vol.Optional(CONF_LANGUAGE): cv.language, + vol.Optional(CONF_DEBUG): cv.boolean, } ), _filter_bad_internal_external_urls, @@ -899,6 +901,9 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: dict) -> Non if key in config: setattr(hac, attr, config[key]) + 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/homeassistant/const.py b/homeassistant/const.py index 58a1c92ea72..ba83eca58d8 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -296,6 +296,7 @@ CONF_WHILE: Final = "while" CONF_WHITELIST: Final = "whitelist" CONF_ALLOWLIST_EXTERNAL_DIRS: Final = "allowlist_external_dirs" LEGACY_CONF_WHITELIST_EXTERNAL_DIRS: Final = "whitelist_external_dirs" +CONF_DEBUG: Final = "debug" CONF_XY: Final = "xy" CONF_ZONE: Final = "zone" diff --git a/homeassistant/core.py b/homeassistant/core.py index 01329806e61..75460ea5759 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -429,6 +429,20 @@ class HomeAssistant: max_workers=1, thread_name_prefix="ImportExecutor" ) + def verify_event_loop_thread(self, what: str) -> None: + """Report and raise if we are not running in the event loop thread.""" + if ( + loop_thread_ident := self.loop.__dict__.get("_thread_ident") + ) and loop_thread_ident != threading.get_ident(): + 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", + error_if_core=True, + error_if_integration=True, + ) + @property def _active_tasks(self) -> set[asyncio.Future[Any]]: """Return all active tasks. @@ -503,7 +517,6 @@ class HomeAssistant: This method is a coroutine. """ _LOGGER.info("Starting Home Assistant") - setattr(self.loop, "_thread_ident", threading.get_ident()) self.set_state(CoreState.starting) self.bus.async_fire_internal(EVENT_CORE_CONFIG_UPDATE) @@ -1451,6 +1464,9 @@ class EventBus: This method must be run in the event loop. """ + if self._hass.config.debug: + self._hass.verify_event_loop_thread("async_fire") + if len(event_type) > MAX_LENGTH_EVENT_EVENT_TYPE: raise MaxLengthExceeded( event_type, "event_type", MAX_LENGTH_EVENT_EVENT_TYPE @@ -2749,6 +2765,7 @@ class Config: self.elevation: int = 0 """Elevation (always in meters regardless of the unit system).""" + self.debug: bool = False self.location_name: str = "Home" self.time_zone: str = "UTC" self.units: UnitSystem = METRIC_SYSTEM @@ -2889,6 +2906,7 @@ class Config: "country": self.country, "language": self.language, "safe_mode": self.safe_mode, + "debug": self.debug, } def set_time_zone(self, time_zone_str: str) -> None: diff --git a/homeassistant/helpers/dispatcher.py b/homeassistant/helpers/dispatcher.py index 52d57e9cf08..aa8176a1b83 100644 --- a/homeassistant/helpers/dispatcher.py +++ b/homeassistant/helpers/dispatcher.py @@ -199,6 +199,9 @@ def async_dispatcher_send( This method must be run in the event loop. """ + if hass.config.debug: + hass.verify_event_loop_thread("async_dispatcher_send") + if (maybe_dispatchers := hass.data.get(DATA_DISPATCHER)) is None: return dispatchers: _DispatcherDataType[*_Ts] = maybe_dispatchers diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 086def8a8be..40b145727a1 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -971,6 +971,8 @@ class Entity( """Write the state to the state machine.""" if self.hass is None: raise RuntimeError(f"Attribute hass is None for {self}") + if self.hass.config.debug: + self.hass.verify_event_loop_thread("async_write_ha_state") # The check for self.platform guards against integrations not using an # EntityComponent and can be removed in HA Core 2024.1 diff --git a/homeassistant/helpers/frame.py b/homeassistant/helpers/frame.py index d86fec3de43..068a12c0598 100644 --- a/homeassistant/helpers/frame.py +++ b/homeassistant/helpers/frame.py @@ -136,6 +136,7 @@ def report( error_if_core: bool = True, level: int = logging.WARNING, log_custom_component_only: bool = False, + error_if_integration: bool = False, ) -> None: """Report incorrect usage. @@ -153,14 +154,19 @@ def report( _LOGGER.warning(msg, stack_info=True) return - if not log_custom_component_only or integration_frame.custom_integration: - _report_integration(what, integration_frame, level) + if ( + error_if_integration + or not log_custom_component_only + or integration_frame.custom_integration + ): + _report_integration(what, integration_frame, level, error_if_integration) def _report_integration( what: str, integration_frame: IntegrationFrame, level: int = logging.WARNING, + error: bool = False, ) -> None: """Report incorrect usage in an integration. @@ -168,7 +174,7 @@ def _report_integration( """ # Keep track of integrations already reported to prevent flooding key = f"{integration_frame.filename}:{integration_frame.line_number}" - if key in _REPORTED_INTEGRATIONS: + if not error and key in _REPORTED_INTEGRATIONS: return _REPORTED_INTEGRATIONS.add(key) @@ -180,11 +186,11 @@ def _report_integration( integration_domain=integration_frame.integration, module=integration_frame.module, ) - + integration_type = "custom " if integration_frame.custom_integration else "" _LOGGER.log( level, "Detected that %sintegration '%s' %s at %s, line %s: %s, please %s", - "custom " if integration_frame.custom_integration else "", + integration_type, integration_frame.integration, what, integration_frame.relative_filename, @@ -192,6 +198,15 @@ def _report_integration( integration_frame.line, report_issue, ) + if not error: + return + raise RuntimeError( + f"Detected that {integration_type}integration " + f"'{integration_frame.integration}' {what} at " + f"{integration_frame.relative_filename}, line " + f"{integration_frame.line_number}: {integration_frame.line}. " + f"Please {report_issue}." + ) def warn_use(func: _CallableT, what: str) -> _CallableT: diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index a1ba1279292..24baab96a4e 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -695,6 +695,8 @@ class Template: **kwargs: Any, ) -> RenderInfo: """Render the template and collect an entity filter.""" + if self.hass and self.hass.config.debug: + self.hass.verify_event_loop_thread("async_render_to_info") self._renders += 1 assert self.hass and _render_info.get() is None diff --git a/homeassistant/runner.py b/homeassistant/runner.py index f036c7d6322..4e2326d4ea7 100644 --- a/homeassistant/runner.py +++ b/homeassistant/runner.py @@ -107,6 +107,7 @@ class HassEventLoopPolicy(asyncio.DefaultEventLoopPolicy): def new_event_loop(self) -> asyncio.AbstractEventLoop: """Get the event loop.""" loop: asyncio.AbstractEventLoop = super().new_event_loop() + setattr(loop, "_thread_ident", threading.get_ident()) loop.set_exception_handler(_async_loop_exception_handler) if self.debug: loop.set_debug(True) diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py index 0cf9fc992c5..19c20207e1d 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -52,8 +52,7 @@ def run_callback_threadsafe( Return a concurrent.futures.Future to access the result. """ - ident = loop.__dict__.get("_thread_ident") - if ident is not None and ident == threading.get_ident(): + if (ident := loop.__dict__.get("_thread_ident")) and ident == threading.get_ident(): raise RuntimeError("Cannot be called from within the event loop") future: concurrent.futures.Future[_T] = concurrent.futures.Future() diff --git a/tests/helpers/test_dispatcher.py b/tests/helpers/test_dispatcher.py index 149231a9368..d9a79cc6a7a 100644 --- a/tests/helpers/test_dispatcher.py +++ b/tests/helpers/test_dispatcher.py @@ -239,3 +239,24 @@ async def test_dispatcher_add_dispatcher(hass: HomeAssistant) -> None: async_dispatcher_send(hass, "test", 5) assert calls == [3, 4, 4, 5, 5] + + +async def test_thread_safety_checks(hass: HomeAssistant) -> None: + """Test dispatcher thread safety checks.""" + hass.config.debug = True + calls = [] + + @callback + def _dispatcher(data): + calls.append(data) + + async_dispatcher_connect(hass, "test", _dispatcher) + + with pytest.raises( + RuntimeError, + match="Detected code that calls async_dispatcher_send from a thread.", + ): + await hass.async_add_executor_job(async_dispatcher_send, hass, "test", 3) + + async_dispatcher_send(hass, "test", 4) + assert calls == [4] diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 690592a850b..349c065f9b5 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -2594,3 +2594,24 @@ async def test_get_hassjob_type(hass: HomeAssistant) -> None: assert ent_1.get_hassjob_type("update") is HassJobType.Executor assert ent_1.get_hassjob_type("async_update") is HassJobType.Coroutinefunction assert ent_1.get_hassjob_type("update_callback") is HassJobType.Callback + + +async def test_async_write_ha_state_thread_safety(hass: HomeAssistant) -> None: + """Test async_write_ha_state thread safety.""" + hass.config.debug = True + + ent = entity.Entity() + ent.entity_id = "test.any" + ent.hass = hass + ent.async_write_ha_state() + assert hass.states.get(ent.entity_id) + + ent2 = entity.Entity() + ent2.entity_id = "test.any2" + ent2.hass = hass + with pytest.raises( + RuntimeError, + match="Detected code that calls async_write_ha_state from a thread.", + ): + await hass.async_add_executor_job(ent2.async_write_ha_state) + assert not hass.states.get(ent2.entity_id) diff --git a/tests/helpers/test_frame.py b/tests/helpers/test_frame.py index fe215264f59..904bed965c8 100644 --- a/tests/helpers/test_frame.py +++ b/tests/helpers/test_frame.py @@ -205,3 +205,45 @@ async def test_report_missing_integration_frame( frame.report(what, error_if_core=False, log_custom_component_only=True) assert caplog.text == "" + + +@pytest.mark.parametrize("run_count", [1, 2]) +# Run this twice to make sure the flood check does not +# kick in when error_if_integration=True +async def test_report_error_if_integration( + caplog: pytest.LogCaptureFixture, run_count: int +) -> None: + """Test RuntimeError is raised if error_if_integration is set.""" + 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.get_current_frame", + return_value=frames, + ), + pytest.raises( + RuntimeError, + match=( + "Detected that integration 'hue' did a bad" + " thing at homeassistant/components/hue/light.py" + ), + ), + ): + frame.report("did a bad thing", error_if_integration=True) diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index ec5b76964f7..f55a94d7283 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -5757,3 +5757,20 @@ async def test_label_areas( info = render_to_info(hass, f"{{{{ '{label.name}' | label_areas }}}}") assert_result_info(info, [master_bedroom.id]) assert info.rate_limit is None + + +async def test_template_thread_safety_checks(hass: HomeAssistant) -> None: + """Test template thread safety checks.""" + hass.states.async_set("sensor.test", "23") + template_str = "{{ states('sensor.test') }}" + template_obj = template.Template(template_str, None) + template_obj.hass = hass + hass.config.debug = True + + with pytest.raises( + RuntimeError, + match="Detected code that calls async_render_to_info from a thread.", + ): + await hass.async_add_executor_job(template_obj.async_render_to_info) + + assert template_obj.async_render_to_info().result() == 23 diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 12eb52c06f4..6b96fb43d1f 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -13,7 +13,7 @@ import pytest from homeassistant import bootstrap, loader, runner import homeassistant.config as config_util from homeassistant.config_entries import HANDLERS, ConfigEntry -from homeassistant.const import SIGNAL_BOOTSTRAP_INTEGRATIONS +from homeassistant.const import CONF_DEBUG, SIGNAL_BOOTSTRAP_INTEGRATIONS from homeassistant.core import CoreState, HomeAssistant, async_get_hass, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -112,6 +112,16 @@ async def test_empty_setup(hass: HomeAssistant) -> None: assert domain in hass.config.components, domain +@pytest.mark.parametrize("load_registries", [False]) +async def test_config_does_not_turn_off_debug(hass: HomeAssistant) -> None: + """Test that config does not turn off debug if its turned on by runtime config.""" + # Mock that its turned on from RuntimeConfig + hass.config.debug = True + + await bootstrap.async_from_config_dict({CONF_DEBUG: False}, hass) + assert hass.config.debug is True + + @pytest.mark.parametrize("load_registries", [False]) async def test_preload_translations(hass: HomeAssistant) -> None: """Test translations are preloaded for all frontend deps and base platforms.""" @@ -599,6 +609,7 @@ async def test_setup_hass( log_no_color=log_no_color, skip_pip=True, recovery_mode=False, + debug=True, ), ) @@ -619,6 +630,9 @@ async def test_setup_hass( assert len(mock_ensure_config_exists.mock_calls) == 1 assert len(mock_process_ha_config_upgrade.mock_calls) == 1 + # debug in RuntimeConfig should set it it in hass.config + assert hass.config.debug is True + assert hass == async_get_hass() diff --git a/tests/test_config.py b/tests/test_config.py index defd6a1018b..58529fb0057 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -857,6 +857,7 @@ async def test_loading_configuration(hass: HomeAssistant) -> None: "internal_url": "http://example.local", "media_dirs": {"mymedia": "/usr"}, "legacy_templates": True, + "debug": True, "currency": "EUR", "country": "SE", "language": "sv", @@ -877,6 +878,7 @@ async def test_loading_configuration(hass: HomeAssistant) -> None: 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" assert hass.config.language == "sv" diff --git a/tests/test_core.py b/tests/test_core.py index 30665619fcd..2f5276eec87 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1990,6 +1990,7 @@ async def test_config_as_dict() -> None: "country": None, "language": "en", "safe_mode": False, + "debug": False, } assert expected == config.as_dict() @@ -3439,3 +3440,22 @@ async def test_top_level_components(hass: HomeAssistant) -> None: hass.config.components.remove("homeassistant.scene") with pytest.raises(NotImplementedError): hass.config.components.discard("homeassistant") + + +async def test_debug_mode_defaults_to_off(hass: HomeAssistant) -> None: + """Test debug mode defaults to off.""" + assert not hass.config.debug + + +async def test_async_fire_thread_safety(hass: HomeAssistant) -> None: + """Test async_fire thread safety.""" + hass.config.debug = True + + events = async_capture_events(hass, "test_event") + hass.bus.async_fire("test_event") + with pytest.raises( + RuntimeError, match="Detected code that calls async_fire from a thread." + ): + await hass.async_add_executor_job(hass.bus.async_fire, "test_event") + + assert len(events) == 1 diff --git a/tests/util/test_async.py b/tests/util/test_async.py index 157becc4b01..ac927b1375a 100644 --- a/tests/util/test_async.py +++ b/tests/util/test_async.py @@ -76,7 +76,8 @@ async def test_run_callback_threadsafe(hass: HomeAssistant) -> None: nonlocal it_ran it_ran = True - assert hasync.run_callback_threadsafe(hass.loop, callback) + with patch.dict(hass.loop.__dict__, {"_thread_ident": -1}): + assert hasync.run_callback_threadsafe(hass.loop, callback) assert it_ran is False # Verify that async_block_till_done will flush @@ -95,6 +96,7 @@ async def test_callback_is_always_scheduled(hass: HomeAssistant) -> None: hasync.shutdown_run_callback_threadsafe(hass.loop) with ( + patch.dict(hass.loop.__dict__, {"_thread_ident": -1}), patch.object(hass.loop, "call_soon_threadsafe") as mock_call_soon_threadsafe, pytest.raises(RuntimeError), ): From 4a59ee978cbb2eff56bf50254950e8b68baf06f8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 24 Apr 2024 06:41:55 +0200 Subject: [PATCH 820/967] Always do thread safety checks when calling async_fire (#116055) --- homeassistant/core.py | 18 ++++++++++-------- tests/test_core.py | 2 -- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 75460ea5759..189dc2f9d8a 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1407,6 +1407,12 @@ class _OneTimeListener(Generic[_DataT]): EMPTY_LIST: list[Any] = [] +def _verify_event_type_length_or_raise(event_type: EventType[_DataT] | str) -> None: + """Verify the length of the event type and raise if too long.""" + if len(event_type) > MAX_LENGTH_EVENT_EVENT_TYPE: + raise MaxLengthExceeded(event_type, "event_type", MAX_LENGTH_EVENT_EVENT_TYPE) + + class EventBus: """Allow the firing of and listening for events.""" @@ -1447,8 +1453,9 @@ class EventBus: context: Context | None = None, ) -> None: """Fire an event.""" + _verify_event_type_length_or_raise(event_type) self._hass.loop.call_soon_threadsafe( - self.async_fire, event_type, event_data, origin, context + self.async_fire_internal, event_type, event_data, origin, context ) @callback @@ -1464,13 +1471,8 @@ class EventBus: This method must be run in the event loop. """ - if self._hass.config.debug: - self._hass.verify_event_loop_thread("async_fire") - - if len(event_type) > MAX_LENGTH_EVENT_EVENT_TYPE: - raise MaxLengthExceeded( - event_type, "event_type", MAX_LENGTH_EVENT_EVENT_TYPE - ) + _verify_event_type_length_or_raise(event_type) + self._hass.verify_event_loop_thread("async_fire") return self.async_fire_internal( event_type, event_data, origin, context, time_fired ) diff --git a/tests/test_core.py b/tests/test_core.py index 2f5276eec87..6bab89bca85 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -3449,8 +3449,6 @@ async def test_debug_mode_defaults_to_off(hass: HomeAssistant) -> None: async def test_async_fire_thread_safety(hass: HomeAssistant) -> None: """Test async_fire thread safety.""" - hass.config.debug = True - events = async_capture_events(hass, "test_event") hass.bus.async_fire("test_event") with pytest.raises( From b37f7b1ff0a6f9e28ad154ef210cb19e51e02572 Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Wed, 24 Apr 2024 07:23:24 +0200 Subject: [PATCH 821/967] Enable Ruff RUF019 (#115396) * Enable Ruff RUF019 * fix tado tests * review comments --- homeassistant/components/bluesound/media_player.py | 2 +- homeassistant/components/isy994/light.py | 7 ++----- homeassistant/components/tado/__init__.py | 4 ++-- homeassistant/components/velbus/__init__.py | 2 +- homeassistant/components/xiaomi_miio/remote.py | 4 ++-- homeassistant/helpers/config_validation.py | 4 ++-- pyproject.toml | 1 + 7 files changed, 11 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index cb6f013dbf8..6c63067a1c1 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -934,7 +934,7 @@ class BluesoundPlayer(MediaPlayerEntity): selected_source = items[0] url = f"Play?url={selected_source['url']}&preset_id&image={selected_source['image']}" - if "is_raw_url" in selected_source and selected_source["is_raw_url"]: + if selected_source.get("is_raw_url"): url = selected_source["url"] return await self.send_bluesound_command(url) diff --git a/homeassistant/components/isy994/light.py b/homeassistant/components/isy994/light.py index 69701534840..b9b269d9ca3 100644 --- a/homeassistant/components/isy994/light.py +++ b/homeassistant/components/isy994/light.py @@ -114,8 +114,5 @@ class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity): if not (last_state := await self.async_get_last_state()): return - if ( - ATTR_LAST_BRIGHTNESS in last_state.attributes - and last_state.attributes[ATTR_LAST_BRIGHTNESS] - ): - self._last_brightness = last_state.attributes[ATTR_LAST_BRIGHTNESS] + if last_brightness := last_state.attributes.get(ATTR_LAST_BRIGHTNESS): + self._last_brightness = last_brightness diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index 5ab7a6f67b8..8f69ccdaffb 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -221,7 +221,7 @@ class TadoConnector: # Errors are planned to be converted to exceptions # in PyTado library, so this can be removed - if "errors" in mobile_devices and mobile_devices["errors"]: + if isinstance(mobile_devices, dict) and mobile_devices.get("errors"): _LOGGER.error( "Error for home ID %s while updating mobile devices: %s", self.home_id, @@ -256,7 +256,7 @@ class TadoConnector: # Errors are planned to be converted to exceptions # in PyTado library, so this can be removed - if "errors" in devices and devices["errors"]: + if isinstance(devices, dict) and devices.get("errors"): _LOGGER.error( "Error for home ID %s while updating devices: %s", self.home_id, diff --git a/homeassistant/components/velbus/__init__.py b/homeassistant/components/velbus/__init__.py index ea03c4b15f1..479b7f02024 100644 --- a/homeassistant/components/velbus/__init__.py +++ b/homeassistant/components/velbus/__init__.py @@ -145,7 +145,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Handle a clear cache service call.""" # clear the cache with suppress(FileNotFoundError): - if CONF_ADDRESS in call.data and call.data[CONF_ADDRESS]: + if call.data.get(CONF_ADDRESS): await hass.async_add_executor_job( os.unlink, hass.config.path( diff --git a/homeassistant/components/xiaomi_miio/remote.py b/homeassistant/components/xiaomi_miio/remote.py index cd3b3192520..5baaf614b01 100644 --- a/homeassistant/components/xiaomi_miio/remote.py +++ b/homeassistant/components/xiaomi_miio/remote.py @@ -138,8 +138,8 @@ async def async_setup_platform( message = await hass.async_add_executor_job(device.read, slot) _LOGGER.debug("Message received from device: '%s'", message) - if "code" in message and message["code"]: - log_msg = "Received command is: {}".format(message["code"]) + if code := message.get("code"): + log_msg = f"Received command is: {code}" _LOGGER.info(log_msg) persistent_notification.async_create( hass, log_msg, title="Xiaomi Miio Remote" diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 38287eb6722..bf20a2d7f5f 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1106,7 +1106,7 @@ def empty_config_schema(domain: str) -> Callable[[dict], dict]: """Return a config schema which logs if there are configuration parameters.""" def validator(config: dict) -> dict: - if domain in config and config[domain]: + if config_domain := config.get(domain): get_integration_logger(__name__).error( ( "The %s integration does not support any configuration parameters, " @@ -1114,7 +1114,7 @@ def empty_config_schema(domain: str) -> Callable[[dict], dict]: "configuration." ), domain, - config[domain], + config_domain, ) return config diff --git a/pyproject.toml b/pyproject.toml index d3487d50a17..7e3038f6ee2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -705,6 +705,7 @@ select = [ "RUF006", # Store a reference to the return value of asyncio.create_task "RUF013", # PEP 484 prohibits implicit Optional "RUF018", # Avoid assignment expressions in assert statements + "RUF019", # Unnecessary key check before dictionary access # "RUF100", # Unused `noqa` directive; temporarily every now and then to clean them up "S102", # Use of exec detected "S103", # bad-file-permissions From f115525137765b1fae8e3b5106bb7c62fd2f27f4 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Wed, 24 Apr 2024 07:51:02 +0200 Subject: [PATCH 822/967] Migrate KNX notify service to entity platform (#115665) --- homeassistant/components/knx/__init__.py | 6 +- homeassistant/components/knx/manifest.json | 2 +- homeassistant/components/knx/notify.py | 59 ++++++++-- homeassistant/components/knx/repairs.py | 36 ++++++ homeassistant/components/knx/schema.py | 1 + homeassistant/components/knx/strings.json | 13 +++ tests/components/knx/test_notify.py | 129 +++++++++++++++------ tests/components/knx/test_repairs.py | 84 ++++++++++++++ 8 files changed, 281 insertions(+), 49 deletions(-) create mode 100644 homeassistant/components/knx/repairs.py create mode 100644 tests/components/knx/test_repairs.py diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index c84d53d6039..da68dc36a6d 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -197,11 +197,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: [ platform for platform in SUPPORTED_PLATFORMS - if platform in config and platform not in (Platform.SENSOR, Platform.NOTIFY) + if platform in config and platform is not Platform.SENSOR ], ) - # set up notify platform, no entry support for notify component yet + # set up notify service for backwards compatibility - remove 2024.11 if NotifySchema.PLATFORM in config: hass.async_create_task( discovery.async_load_platform( @@ -232,7 +232,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: platform for platform in SUPPORTED_PLATFORMS if platform in hass.data[DATA_KNX_CONFIG] - and platform not in (Platform.SENSOR, Platform.NOTIFY) + and platform is not Platform.SENSOR ], ], ) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index af0c6b8d01c..77f3db3f9f3 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", "repairs", "websocket_api"], "documentation": "https://www.home-assistant.io/integrations/knx", "integration_type": "hub", "iot_class": "local_push", diff --git a/homeassistant/components/knx/notify.py b/homeassistant/components/knx/notify.py index 74ae86dc5d0..e208e4fd646 100644 --- a/homeassistant/components/knx/notify.py +++ b/homeassistant/components/knx/notify.py @@ -1,4 +1,4 @@ -"""Support for KNX/IP notification services.""" +"""Support for KNX/IP notifications.""" from __future__ import annotations @@ -7,13 +7,16 @@ from typing import Any from xknx import XKNX from xknx.devices import Notification as XknxNotification -from homeassistant.components.notify import BaseNotificationService -from homeassistant.const import CONF_NAME, CONF_TYPE +from homeassistant import config_entries +from homeassistant.components.notify import BaseNotificationService, NotifyEntity +from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, CONF_TYPE, Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS -from .schema import NotifySchema +from .knx_entity import KnxEntity +from .repairs import migrate_notify_issue async def async_get_service( @@ -25,16 +28,11 @@ async def async_get_service( if discovery_info is None: return None - if platform_config := hass.data[DATA_KNX_CONFIG].get(NotifySchema.PLATFORM): + if platform_config := hass.data[DATA_KNX_CONFIG].get(Platform.NOTIFY): xknx: XKNX = hass.data[DOMAIN].xknx notification_devices = [ - XknxNotification( - xknx, - name=device_config[CONF_NAME], - group_address=device_config[KNX_ADDRESS], - value_type=device_config[CONF_TYPE], - ) + _create_notification_instance(xknx, device_config) for device_config in platform_config ] return KNXNotificationService(notification_devices) @@ -59,6 +57,7 @@ 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) if "target" in kwargs: await self._async_send_to_device(message, kwargs["target"]) else: @@ -74,3 +73,41 @@ class KNXNotificationService(BaseNotificationService): for device in self.devices: if device.name in names: await device.set(message) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up notify(s) for KNX platform.""" + xknx: XKNX = hass.data[DOMAIN].xknx + config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.NOTIFY] + + async_add_entities(KNXNotify(xknx, entity_config) for entity_config in config) + + +def _create_notification_instance(xknx: XKNX, config: ConfigType) -> XknxNotification: + """Return a KNX Notification to be used within XKNX.""" + return XknxNotification( + xknx, + name=config[CONF_NAME], + group_address=config[KNX_ADDRESS], + value_type=config[CONF_TYPE], + ) + + +class KNXNotify(NotifyEntity, KnxEntity): + """Representation of a KNX notification entity.""" + + _device: XknxNotification + + def __init__(self, xknx: XKNX, config: ConfigType) -> None: + """Initialize a KNX notification.""" + super().__init__(_create_notification_instance(xknx, config)) + self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) + self._attr_unique_id = str(self._device.remote_value.group_address) + + async def async_send_message(self, message: str) -> None: + """Send a notification to knx bus.""" + await self._device.set(message) diff --git a/homeassistant/components/knx/repairs.py b/homeassistant/components/knx/repairs.py new file mode 100644 index 00000000000..f0a92850d36 --- /dev/null +++ b/homeassistant/components/knx/repairs.py @@ -0,0 +1,36 @@ +"""Repairs support for KNX.""" + +from __future__ import annotations + +from homeassistant.components.repairs import ConfirmRepairFlow, RepairsFlow +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import issue_registry as ir + +from .const import DOMAIN + + +@callback +def migrate_notify_issue(hass: HomeAssistant) -> None: + """Create issue for notify service deprecation.""" + ir.async_create_issue( + hass, + DOMAIN, + "migrate_notify", + breaks_in_ha_version="2024.11.0", + issue_domain=Platform.NOTIFY.value, + is_fixable=True, + is_persistent=True, + translation_key="migrate_notify", + severity=ir.IssueSeverity.WARNING, + ) + + +async def async_create_fix_flow( + hass: HomeAssistant, + issue_id: str, + data: dict[str, str | int | float | None] | None, +) -> RepairsFlow: + """Create flow.""" + assert issue_id == "migrate_notify" + return ConfirmRepairFlow() diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index 39670b4f92b..462605c3985 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -750,6 +750,7 @@ class NotifySchema(KNXPlatformSchema): vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_TYPE, default="latin_1"): string_type_validator, vol.Required(KNX_ADDRESS): ga_validator, + vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, } ) diff --git a/homeassistant/components/knx/strings.json b/homeassistant/components/knx/strings.json index 39b96dddf8f..a69ba106ffd 100644 --- a/homeassistant/components/knx/strings.json +++ b/homeassistant/components/knx/strings.json @@ -384,5 +384,18 @@ "name": "[%key:common::action::reload%]", "description": "Reloads the KNX integration." } + }, + "issues": { + "migrate_notify": { + "title": "Migration of KNX notify service", + "fix_flow": { + "step": { + "confirm": { + "description": "The KNX `notify` service has been migrated. New `notify` entities are available now.\n\nUpdate any automations to use the new `notify.send_message` exposed by these new entities. When this is done, fix this issue and restart Home Assistant.", + "title": "Disable legacy KNX notify service" + } + } + } + } } } diff --git a/tests/components/knx/test_notify.py b/tests/components/knx/test_notify.py index d843c460c34..94f2d579fc8 100644 --- a/tests/components/knx/test_notify.py +++ b/tests/components/knx/test_notify.py @@ -1,5 +1,6 @@ """Test KNX notify.""" +from homeassistant.components import notify from homeassistant.components.knx.const import KNX_ADDRESS from homeassistant.components.knx.schema import NotifySchema from homeassistant.const import CONF_NAME, CONF_TYPE @@ -8,7 +9,9 @@ from homeassistant.core import HomeAssistant from .conftest import KNXTestKit -async def test_notify_simple(hass: HomeAssistant, knx: KNXTestKit) -> None: +async def test_legacy_notify_service_simple( + hass: HomeAssistant, knx: KNXTestKit +) -> None: """Test KNX notify can send to one device.""" await knx.setup_integration( { @@ -26,22 +29,7 @@ async def test_notify_simple(hass: HomeAssistant, knx: KNXTestKit) -> None: await knx.assert_write( "1/0/0", - ( - 0x49, - 0x20, - 0x6C, - 0x6F, - 0x76, - 0x65, - 0x20, - 0x4B, - 0x4E, - 0x58, - 0x0, - 0x0, - 0x0, - 0x0, - ), + (73, 32, 108, 111, 118, 101, 32, 75, 78, 88, 0, 0, 0, 0), ) await hass.services.async_call( @@ -56,26 +44,11 @@ async def test_notify_simple(hass: HomeAssistant, knx: KNXTestKit) -> None: await knx.assert_write( "1/0/0", - ( - 0x49, - 0x20, - 0x6C, - 0x6F, - 0x76, - 0x65, - 0x20, - 0x4B, - 0x4E, - 0x58, - 0x2C, - 0x20, - 0x62, - 0x75, - ), + (73, 32, 108, 111, 118, 101, 32, 75, 78, 88, 44, 32, 98, 117), ) -async def test_notify_multiple_sends_to_all_with_different_encodings( +async def test_legacy_notify_service_multiple_sends_to_all_with_different_encodings( hass: HomeAssistant, knx: KNXTestKit ) -> None: """Test KNX notify `type` configuration.""" @@ -110,3 +83,91 @@ async def test_notify_multiple_sends_to_all_with_different_encodings( "1/0/1", (71, 228, 110, 115, 101, 102, 252, 223, 99, 104, 101, 110, 0, 0), ) + + +async def test_notify_simple(hass: HomeAssistant, knx: KNXTestKit) -> None: + """Test KNX notify can send to one device.""" + await knx.setup_integration( + { + NotifySchema.PLATFORM: { + CONF_NAME: "test", + KNX_ADDRESS: "1/0/0", + } + } + ) + + await hass.services.async_call( + notify.DOMAIN, + notify.SERVICE_SEND_MESSAGE, + { + "entity_id": "notify.test", + notify.ATTR_MESSAGE: "I love KNX", + }, + ) + await knx.assert_write( + "1/0/0", + (73, 32, 108, 111, 118, 101, 32, 75, 78, 88, 0, 0, 0, 0), + ) + + await hass.services.async_call( + notify.DOMAIN, + notify.SERVICE_SEND_MESSAGE, + { + "entity_id": "notify.test", + notify.ATTR_MESSAGE: "I love KNX, but this text is too long for KNX, poor KNX", + }, + ) + await knx.assert_write( + "1/0/0", + (73, 32, 108, 111, 118, 101, 32, 75, 78, 88, 44, 32, 98, 117), + ) + + +async def test_notify_multiple_sends_with_different_encodings( + hass: HomeAssistant, knx: KNXTestKit +) -> None: + """Test KNX notify `type` configuration.""" + await knx.setup_integration( + { + NotifySchema.PLATFORM: [ + { + CONF_NAME: "ASCII", + KNX_ADDRESS: "1/0/0", + CONF_TYPE: "string", + }, + { + CONF_NAME: "Latin-1", + KNX_ADDRESS: "1/0/1", + CONF_TYPE: "latin_1", + }, + ] + } + ) + message = {notify.ATTR_MESSAGE: "Gänsefüßchen"} + + await hass.services.async_call( + notify.DOMAIN, + notify.SERVICE_SEND_MESSAGE, + { + "entity_id": "notify.ascii", + **message, + }, + ) + await knx.assert_write( + "1/0/0", + # "G?nsef??chen" + (71, 63, 110, 115, 101, 102, 63, 63, 99, 104, 101, 110, 0, 0), + ) + + await hass.services.async_call( + notify.DOMAIN, + notify.SERVICE_SEND_MESSAGE, + { + "entity_id": "notify.latin_1", + **message, + }, + ) + await knx.assert_write( + "1/0/1", + (71, 228, 110, 115, 101, 102, 252, 223, 99, 104, 101, 110, 0, 0), + ) diff --git a/tests/components/knx/test_repairs.py b/tests/components/knx/test_repairs.py new file mode 100644 index 00000000000..4ad06e0addb --- /dev/null +++ b/tests/components/knx/test_repairs.py @@ -0,0 +1,84 @@ +"""Test repairs for KNX integration.""" + +from http import HTTPStatus + +from homeassistant.components.knx.const import DOMAIN, KNX_ADDRESS +from homeassistant.components.knx.schema import NotifySchema +from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN +from homeassistant.components.repairs.websocket_api import ( + RepairsFlowIndexView, + RepairsFlowResourceView, +) +from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant +import homeassistant.helpers.issue_registry as ir + +from .conftest import KNXTestKit + +from tests.typing import ClientSessionGenerator + + +async def test_knx_notify_service_issue( + hass: HomeAssistant, + knx: KNXTestKit, + hass_client: ClientSessionGenerator, + issue_registry: ir.IssueRegistry, +) -> None: + """Test the legacy notify service still works before migration and repair flow is triggered.""" + await knx.setup_integration( + { + NotifySchema.PLATFORM: { + CONF_NAME: "test", + KNX_ADDRESS: "1/0/0", + } + } + ) + http_client = await hass_client() + + # Assert no issue is present + assert len(issue_registry.issues) == 0 + + # Simulate legacy service being used + assert hass.services.has_service(NOTIFY_DOMAIN, NOTIFY_DOMAIN) + await hass.services.async_call( + NOTIFY_DOMAIN, + NOTIFY_DOMAIN, + service_data={"message": "It is too cold!", "target": "test"}, + blocking=True, + ) + await knx.assert_write( + "1/0/0", + (73, 116, 32, 105, 115, 32, 116, 111, 111, 32, 99, 111, 108, 100), + ) + + # Assert the issue is present + assert len(issue_registry.issues) == 1 + assert issue_registry.async_get_issue( + domain=DOMAIN, + issue_id="migrate_notify", + ) + + # Test confirm step in repair flow + resp = await http_client.post( + RepairsFlowIndexView.url, + json={"handler": DOMAIN, "issue_id": "migrate_notify"}, + ) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data["step_id"] == "confirm" + + resp = await http_client.post( + RepairsFlowResourceView.url.format(flow_id=flow_id), + ) + assert resp.status == HTTPStatus.OK + data = await resp.json() + assert data["type"] == "create_entry" + + # Assert the issue is no longer present + assert not issue_registry.async_get_issue( + domain=DOMAIN, + issue_id="migrate_notify", + ) + assert len(issue_registry.issues) == 0 From d8cca482b3726f2ad9f0914f1efbc14754d90a70 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 24 Apr 2024 07:52:14 +0200 Subject: [PATCH 823/967] Add reconfigure flow to AVM Fritz!Tools (#116057) add reconfigure flow --- homeassistant/components/fritz/config_flow.py | 84 ++++++++- homeassistant/components/fritz/strings.json | 16 +- tests/components/fritz/test_config_flow.py | 177 +++++++++++++++++- 3 files changed, 270 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/fritz/config_flow.py b/homeassistant/components/fritz/config_flow.py index 1cfa3af39fb..fdafd486b29 100644 --- a/homeassistant/components/fritz/config_flow.py +++ b/homeassistant/components/fritz/config_flow.py @@ -138,6 +138,12 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): }, ) + def _determine_port(self, user_input: dict[str, Any]) -> int: + """Determine port from user_input.""" + if port := user_input.get(CONF_PORT): + return int(port) + return DEFAULT_HTTPS_PORT if user_input[CONF_SSL] else DEFAULT_HTTP_PORT + async def async_step_ssdp( self, discovery_info: ssdp.SsdpServiceInfo ) -> ConfigFlowResult: @@ -189,7 +195,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): self._username = user_input[CONF_USERNAME] self._password = user_input[CONF_PASSWORD] self._use_tls = user_input[CONF_SSL] - self._port = DEFAULT_HTTPS_PORT if self._use_tls else DEFAULT_HTTP_PORT + self._port = self._determine_port(user_input) error = await self.hass.async_add_executor_job(self.fritz_tools_init) @@ -252,10 +258,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): self._password = user_input[CONF_PASSWORD] self._use_tls = user_input[CONF_SSL] - if (port := user_input.get(CONF_PORT)) is None: - self._port = DEFAULT_HTTPS_PORT if self._use_tls else DEFAULT_HTTP_PORT - else: - self._port = port + self._port = self._determine_port(user_input) if not (error := await self.hass.async_add_executor_job(self.fritz_tools_init)): self._name = self._model @@ -329,6 +332,77 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): await self.hass.config_entries.async_reload(self._entry.entry_id) return self.async_abort(reason="reauth_successful") + async def async_step_reconfigure(self, _: Mapping[str, Any]) -> ConfigFlowResult: + """Handle reconfigure flow .""" + self._entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + assert self._entry + self._host = self._entry.data[CONF_HOST] + self._port = self._entry.data[CONF_PORT] + self._username = self._entry.data[CONF_USERNAME] + self._password = self._entry.data[CONF_PASSWORD] + self._use_tls = self._entry.data.get(CONF_SSL, DEFAULT_SSL) + + return await self.async_step_reconfigure_confirm() + + def _show_setup_form_reconfigure_confirm( + self, user_input: dict[str, Any], errors: dict[str, str] | None = None + ) -> ConfigFlowResult: + """Show the reconfigure form to the user.""" + advanced_data_schema = {} + if self.show_advanced_options: + advanced_data_schema = { + vol.Optional(CONF_PORT, default=user_input[CONF_PORT]): vol.Coerce(int), + } + + return self.async_show_form( + step_id="reconfigure_confirm", + data_schema=vol.Schema( + { + vol.Required(CONF_HOST, default=user_input[CONF_HOST]): str, + **advanced_data_schema, + vol.Required(CONF_SSL, default=user_input[CONF_SSL]): bool, + } + ), + description_placeholders={"host": self._host}, + errors=errors or {}, + ) + + async def async_step_reconfigure_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle reconfigure flow.""" + if user_input is None: + return self._show_setup_form_reconfigure_confirm( + { + CONF_HOST: self._host, + CONF_PORT: self._port, + CONF_SSL: self._use_tls, + } + ) + + self._host = user_input[CONF_HOST] + self._use_tls = user_input[CONF_SSL] + self._port = self._determine_port(user_input) + + if error := await self.hass.async_add_executor_job(self.fritz_tools_init): + return self._show_setup_form_reconfigure_confirm( + user_input={**user_input, CONF_PORT: self._port}, errors={"base": error} + ) + + assert isinstance(self._entry, ConfigEntry) + self.hass.config_entries.async_update_entry( + self._entry, + data={ + CONF_HOST: self._host, + CONF_PASSWORD: self._password, + CONF_PORT: self._port, + CONF_USERNAME: self._username, + CONF_SSL: self._use_tls, + }, + ) + await self.hass.config_entries.async_reload(self._entry.entry_id) + return self.async_abort(reason="reconfigure_successful") + class FritzBoxToolsOptionsFlowHandler(OptionsFlowWithConfigEntry): """Handle an options flow.""" diff --git a/homeassistant/components/fritz/strings.json b/homeassistant/components/fritz/strings.json index 4899edb6938..a96c3b8ac28 100644 --- a/homeassistant/components/fritz/strings.json +++ b/homeassistant/components/fritz/strings.json @@ -18,6 +18,19 @@ "password": "[%key:common::config_flow::data::password%]" } }, + "reconfigure_confirm": { + "title": "Updating FRITZ!Box Tools - configuration", + "description": "Update FRITZ!Box Tools configuration for: {host}.", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]", + "ssl": "[%key:common::config_flow::data::ssl%]" + }, + "data_description": { + "host": "The hostname or IP address of your FRITZ!Box router.", + "port": "Leave it empty to use the default port." + } + }, "user": { "title": "[%key:component::fritz::config::step::confirm::title%]", "description": "Set up FRITZ!Box Tools to control your FRITZ!Box.\nMinimum needed: username, password.", @@ -38,7 +51,8 @@ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "ignore_ip6_link_local": "IPv6 link local address is not supported.", - "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%]" }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", diff --git a/tests/components/fritz/test_config_flow.py b/tests/components/fritz/test_config_flow.py index 64bf3cd9064..f87fbe722cd 100644 --- a/tests/components/fritz/test_config_flow.py +++ b/tests/components/fritz/test_config_flow.py @@ -23,7 +23,12 @@ from homeassistant.components.fritz.const import ( FRITZ_AUTH_EXCEPTIONS, ) from homeassistant.components.ssdp import ATTR_UPNP_UDN -from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_SSDP, SOURCE_USER +from homeassistant.config_entries import ( + SOURCE_REAUTH, + SOURCE_RECONFIGURE, + SOURCE_SSDP, + SOURCE_USER, +) from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -405,6 +410,176 @@ async def test_reauth_not_successful( assert result["errors"]["base"] == error +@pytest.mark.parametrize( + ("show_advanced_options", "user_input", "expected_config"), + [ + ( + True, + {CONF_HOST: "host_a", CONF_PORT: 49000, CONF_SSL: False}, + {CONF_HOST: "host_a", CONF_PORT: 49000, CONF_SSL: False}, + ), + ( + True, + {CONF_HOST: "host_a", CONF_PORT: 49443, CONF_SSL: True}, + {CONF_HOST: "host_a", CONF_PORT: 49443, CONF_SSL: True}, + ), + ( + True, + {CONF_HOST: "host_a", CONF_PORT: 12345, CONF_SSL: True}, + {CONF_HOST: "host_a", CONF_PORT: 12345, CONF_SSL: True}, + ), + ( + False, + {CONF_HOST: "host_b", CONF_SSL: False}, + {CONF_HOST: "host_b", CONF_PORT: 49000, CONF_SSL: False}, + ), + ( + False, + {CONF_HOST: "host_b", CONF_SSL: True}, + {CONF_HOST: "host_b", CONF_PORT: 49443, CONF_SSL: True}, + ), + ], +) +async def test_reconfigure_successful( + hass: HomeAssistant, + fc_class_mock, + mock_get_source_ip, + show_advanced_options: bool, + user_input: dict, + expected_config: dict, +) -> None: + """Test starting a reconfigure flow.""" + + mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + mock_config.add_to_hass(hass) + + with ( + patch( + "homeassistant.components.fritz.config_flow.FritzConnection", + side_effect=fc_class_mock, + ), + patch( + "homeassistant.components.fritz.common.FritzBoxTools._update_device_info", + return_value=MOCK_FIRMWARE_INFO, + ), + patch( + "homeassistant.components.fritz.async_setup_entry", + ) as mock_setup_entry, + patch( + "requests.get", + ) as mock_request_get, + patch( + "requests.post", + ) as mock_request_post, + ): + mock_request_get.return_value.status_code = 200 + mock_request_get.return_value.content = MOCK_REQUEST + mock_request_post.return_value.status_code = 200 + mock_request_post.return_value.text = MOCK_REQUEST + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_RECONFIGURE, + "entry_id": mock_config.entry_id, + "show_advanced_options": show_advanced_options, + }, + data=mock_config.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=user_input, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert mock_config.data == { + **expected_config, + CONF_USERNAME: "fake_user", + CONF_PASSWORD: "fake_pass", + } + + assert mock_setup_entry.called + + +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.""" + + mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + mock_config.add_to_hass(hass) + + with ( + patch( + "homeassistant.components.fritz.config_flow.FritzConnection", + side_effect=[FritzConnectionException, fc_class_mock], + ), + patch( + "homeassistant.components.fritz.common.FritzBoxTools._update_device_info", + return_value=MOCK_FIRMWARE_INFO, + ), + patch( + "homeassistant.components.fritz.async_setup_entry", + ), + patch( + "requests.get", + ) as mock_request_get, + patch( + "requests.post", + ) as mock_request_post, + ): + mock_request_get.return_value.status_code = 200 + mock_request_get.return_value.content = MOCK_REQUEST + mock_request_post.return_value.status_code = 200 + mock_request_post.return_value.text = MOCK_REQUEST + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_RECONFIGURE, "entry_id": mock_config.entry_id}, + data=mock_config.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: "fake_host", + CONF_SSL: False, + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure_confirm" + assert result["errors"]["base"] == ERROR_CANNOT_CONNECT + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "fake_host", + CONF_SSL: False, + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert mock_config.data == { + CONF_HOST: "fake_host", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + CONF_PORT: 49000, + CONF_SSL: False, + } + + async def test_ssdp_already_configured( hass: HomeAssistant, fc_class_mock, mock_get_source_ip ) -> None: From 44208a5be0eed3429fb03a5606f16556fb65f683 Mon Sep 17 00:00:00 2001 From: osohotwateriot <102795312+osohotwateriot@users.noreply.github.com> Date: Wed, 24 Apr 2024 09:19:26 +0300 Subject: [PATCH 824/967] Add OSO Energy sensors (#108226) * Add OSO Energy sensors * Fix comments * Fixes after review * Fix sensor names and translations * Fixes after review * Fix validation errors * Fixes after review * Remove profile sensor --- .coveragerc | 1 + .../components/osoenergy/__init__.py | 24 ++- homeassistant/components/osoenergy/sensor.py | 151 ++++++++++++++++++ .../components/osoenergy/strings.json | 49 +++++- .../components/osoenergy/water_heater.py | 52 +++--- 5 files changed, 241 insertions(+), 36 deletions(-) create mode 100644 homeassistant/components/osoenergy/sensor.py diff --git a/.coveragerc b/.coveragerc index 9eb32f7cda8..6f382bcb780 100644 --- a/.coveragerc +++ b/.coveragerc @@ -986,6 +986,7 @@ omit = homeassistant/components/orvibo/switch.py homeassistant/components/osoenergy/__init__.py homeassistant/components/osoenergy/const.py + homeassistant/components/osoenergy/sensor.py homeassistant/components/osoenergy/water_heater.py homeassistant/components/osramlightify/light.py homeassistant/components/otp/sensor.py diff --git a/homeassistant/components/osoenergy/__init__.py b/homeassistant/components/osoenergy/__init__.py index 48ea01e8bb8..20ff22cea23 100644 --- a/homeassistant/components/osoenergy/__init__.py +++ b/homeassistant/components/osoenergy/__init__.py @@ -16,18 +16,25 @@ 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 -_T = TypeVar( - "_T", OSOEnergyBinarySensorData, OSOEnergySensorData, OSOEnergyWaterHeaterData +_OSOEnergyT = TypeVar( + "_OSOEnergyT", + OSOEnergyBinarySensorData, + OSOEnergySensorData, + OSOEnergyWaterHeaterData, ) +MANUFACTURER = "OSO Energy" PLATFORMS = [ + Platform.SENSOR, Platform.WATER_HEATER, ] PLATFORM_LOOKUP = { + Platform.SENSOR: "sensor", Platform.WATER_HEATER: "water_heater", } @@ -70,13 +77,18 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -class OSOEnergyEntity(Entity, Generic[_T]): +class OSOEnergyEntity(Entity, Generic[_OSOEnergyT]): """Initiate OSO Energy Base Class.""" _attr_has_entity_name = True - def __init__(self, osoenergy: OSOEnergy, osoenergy_device: _T) -> None: + def __init__(self, osoenergy: OSOEnergy, entity_data: _OSOEnergyT) -> None: """Initialize the instance.""" self.osoenergy = osoenergy - self.device = osoenergy_device - self._attr_unique_id = osoenergy_device.device_id + 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 new file mode 100644 index 00000000000..0be6ad83281 --- /dev/null +++ b/homeassistant/components/osoenergy/sensor.py @@ -0,0 +1,151 @@ +"""Support for OSO Energy sensors.""" + +from collections.abc import Callable +from dataclasses import dataclass + +from apyosoenergyapi import OSOEnergy +from apyosoenergyapi.helper.const import OSOEnergySensorData + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import UnitOfEnergy, UnitOfPower, UnitOfVolume +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 + + +@dataclass(frozen=True, kw_only=True) +class OSOEnergySensorEntityDescription(SensorEntityDescription): + """Class describing OSO Energy heater sensor entities.""" + + value_fn: Callable[[OSOEnergy], StateType] + + +SENSOR_TYPES: dict[str, OSOEnergySensorEntityDescription] = { + "heater_mode": OSOEnergySensorEntityDescription( + key="heater_mode", + translation_key="heater_mode", + device_class=SensorDeviceClass.ENUM, + options=[ + "auto", + "manual", + "off", + "legionella", + "powersave", + "extraenergy", + "voltage", + "ffr", + ], + value_fn=lambda entity_data: entity_data.state.lower(), + ), + "optimization_mode": OSOEnergySensorEntityDescription( + key="optimization_mode", + translation_key="optimization_mode", + device_class=SensorDeviceClass.ENUM, + options=["off", "oso", "gridcompany", "smartcompany", "advanced"], + value_fn=lambda entity_data: entity_data.state.lower(), + ), + "power_load": OSOEnergySensorEntityDescription( + key="power_load", + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.KILO_WATT, + value_fn=lambda entity_data: entity_data.state, + ), + "tapping_capacity": OSOEnergySensorEntityDescription( + key="tapping_capacity", + translation_key="tapping_capacity", + device_class=SensorDeviceClass.ENERGY, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + value_fn=lambda entity_data: entity_data.state, + ), + "capacity_mixed_water_40": OSOEnergySensorEntityDescription( + key="capacity_mixed_water_40", + translation_key="capacity_mixed_water_40", + device_class=SensorDeviceClass.VOLUME, + native_unit_of_measurement=UnitOfVolume.LITERS, + value_fn=lambda entity_data: entity_data.state, + ), + "v40_min": OSOEnergySensorEntityDescription( + key="v40_min", + translation_key="v40_min", + device_class=SensorDeviceClass.VOLUME, + native_unit_of_measurement=UnitOfVolume.LITERS, + value_fn=lambda entity_data: entity_data.state, + ), + "v40_level_min": OSOEnergySensorEntityDescription( + key="v40_level_min", + translation_key="v40_level_min", + device_class=SensorDeviceClass.VOLUME, + native_unit_of_measurement=UnitOfVolume.LITERS, + value_fn=lambda entity_data: entity_data.state, + ), + "v40_level_max": OSOEnergySensorEntityDescription( + key="v40_level_max", + translation_key="v40_level_max", + device_class=SensorDeviceClass.VOLUME, + native_unit_of_measurement=UnitOfVolume.LITERS, + value_fn=lambda entity_data: entity_data.state, + ), + "volume": OSOEnergySensorEntityDescription( + key="volume", + device_class=SensorDeviceClass.VOLUME, + native_unit_of_measurement=UnitOfVolume.LITERS, + value_fn=lambda entity_data: entity_data.state, + ), +} + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up OSO Energy sensor.""" + osoenergy = hass.data[DOMAIN][entry.entry_id] + devices = osoenergy.session.device_list.get("sensor") + entities = [] + if devices: + for dev in devices: + sensor_type = dev.osoEnergyType.lower() + if sensor_type in SENSOR_TYPES: + entities.append( + OSOEnergySensor(osoenergy, SENSOR_TYPES[sensor_type], dev) + ) + + async_add_entities(entities, True) + + +class OSOEnergySensor(OSOEnergyEntity[OSOEnergySensorData], SensorEntity): + """OSO Energy Sensor Entity.""" + + entity_description: OSOEnergySensorEntityDescription + + def __init__( + self, + instance: OSOEnergy, + description: OSOEnergySensorEntityDescription, + entity_data: OSOEnergySensorData, + ) -> None: + """Initialize the OSO Energy sensor.""" + super().__init__(instance, entity_data) + + device_id = entity_data.device_id + self._attr_unique_id = f"{device_id}_{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.entity_data) + + async def async_update(self) -> None: + """Update all data for OSO Energy.""" + await self.osoenergy.session.update_data() + self.entity_data = await self.osoenergy.sensor.get_sensor(self.entity_data) diff --git a/homeassistant/components/osoenergy/strings.json b/homeassistant/components/osoenergy/strings.json index a45482bf030..5313f1d6565 100644 --- a/homeassistant/components/osoenergy/strings.json +++ b/homeassistant/components/osoenergy/strings.json @@ -17,13 +17,56 @@ } }, "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%]" + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } + }, + "entity": { + "sensor": { + "tapping_capacity": { + "name": "Tapping capacity" + }, + "capacity_mixed_water_40": { + "name": "Capacity mixed water 40°C" + }, + "v40_min": { + "name": "Mixed water at 40°C" + }, + "v40_level_min": { + "name": "Minimum level of mixed water at 40°C" + }, + "v40_level_max": { + "name": "Maximum level of mixed water at 40°C" + }, + "heater_mode": { + "name": "Heater mode", + "state": { + "auto": "Auto", + "extraenergy": "Extra energy", + "ffr": "Fast frequency reserve", + "legionella": "Legionella", + "manual": "Manual", + "off": "Off", + "powersave": "Power save", + "voltage": "Voltage" + } + }, + "optimization_mode": { + "name": "Optimization mode", + "state": { + "advanced": "Advanced", + "gridcompany": "Grid company", + "off": "Off", + "oso": "OSO", + "smartcompany": "Smart company" + } + }, + "profile": { + "name": "Profile local" + } + } } } diff --git a/homeassistant/components/osoenergy/water_heater.py b/homeassistant/components/osoenergy/water_heater.py index eaf54a9f9a4..b7fb2ba16e6 100644 --- a/homeassistant/components/osoenergy/water_heater.py +++ b/homeassistant/components/osoenergy/water_heater.py @@ -2,6 +2,7 @@ from typing import Any +from apyosoenergyapi import OSOEnergy from apyosoenergyapi.helper.const import OSOEnergyWaterHeaterData from homeassistant.components.water_heater import ( @@ -15,7 +16,6 @@ from homeassistant.components.water_heater import ( 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.entity_platform import AddEntitiesCallback from . import OSOEnergyEntity @@ -34,9 +34,6 @@ CURRENT_OPERATION_MAP: dict[str, Any] = { "extraenergy": STATE_HIGH_DEMAND, }, } -HEATER_MIN_TEMP = 10 -HEATER_MAX_TEMP = 80 -MANUFACTURER = "OSO Energy" async def async_setup_entry( @@ -59,30 +56,29 @@ class OSOEnergyWaterHeater( _attr_supported_features = WaterHeaterEntityFeature.TARGET_TEMPERATURE _attr_temperature_unit = UnitOfTemperature.CELSIUS - @property - def device_info(self) -> DeviceInfo: - """Return device information.""" - return DeviceInfo( - identifiers={(DOMAIN, self.device.device_id)}, - manufacturer=MANUFACTURER, - model=self.device.device_type, - name=self.device.device_name, - ) + def __init__( + self, + instance: OSOEnergy, + entity_data: OSOEnergyWaterHeaterData, + ) -> None: + """Initialize the OSO Energy water heater.""" + super().__init__(instance, entity_data) + self._attr_unique_id = entity_data.device_id @property def available(self) -> bool: """Return if the device is available.""" - return self.device.available + return self.entity_data.available @property def current_operation(self) -> str: """Return current operation.""" - status = self.device.current_operation + status = self.entity_data.current_operation if status == "off": return STATE_OFF - optimization_mode = self.device.optimization_mode.lower() - heater_mode = self.device.heater_mode.lower() + optimization_mode = self.entity_data.optimization_mode.lower() + heater_mode = self.entity_data.heater_mode.lower() if optimization_mode in CURRENT_OPERATION_MAP: return CURRENT_OPERATION_MAP[optimization_mode].get( heater_mode, STATE_ELECTRIC @@ -93,49 +89,51 @@ class OSOEnergyWaterHeater( @property def current_temperature(self) -> float: """Return the current temperature of the heater.""" - return self.device.current_temperature + return self.entity_data.current_temperature @property def target_temperature(self) -> float: """Return the temperature we try to reach.""" - return self.device.target_temperature + return self.entity_data.target_temperature @property def target_temperature_high(self) -> float: """Return the temperature we try to reach.""" - return self.device.target_temperature_high + return self.entity_data.target_temperature_high @property def target_temperature_low(self) -> float: """Return the temperature we try to reach.""" - return self.device.target_temperature_low + return self.entity_data.target_temperature_low @property def min_temp(self) -> float: """Return the minimum temperature.""" - return self.device.min_temperature + return self.entity_data.min_temperature @property def max_temp(self) -> float: """Return the maximum temperature.""" - return self.device.max_temperature + return self.entity_data.max_temperature async def async_turn_on(self, **kwargs) -> None: """Turn on hotwater.""" - await self.osoenergy.hotwater.turn_on(self.device, True) + await self.osoenergy.hotwater.turn_on(self.entity_data, True) async def async_turn_off(self, **kwargs) -> None: """Turn off hotwater.""" - await self.osoenergy.hotwater.turn_off(self.device, True) + await self.osoenergy.hotwater.turn_off(self.entity_data, True) async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" target_temperature = int(kwargs.get("temperature", self.target_temperature)) profile = [target_temperature] * 24 - await self.osoenergy.hotwater.set_profile(self.device, profile) + await self.osoenergy.hotwater.set_profile(self.entity_data, profile) async def async_update(self) -> None: """Update all Node data from Hive.""" await self.osoenergy.session.update_data() - self.device = await self.osoenergy.hotwater.get_water_heater(self.device) + self.entity_data = await self.osoenergy.hotwater.get_water_heater( + self.entity_data + ) From 474a1a3d94d8d8da9edc62ec9209a7c00e7e0cf5 Mon Sep 17 00:00:00 2001 From: Richard Kroegel <42204099+rikroe@users.noreply.github.com> Date: Wed, 24 Apr 2024 09:46:55 +0200 Subject: [PATCH 825/967] Use display_precision if suggested_display_precision is None (#110270) Co-authored-by: Richard Co-authored-by: Erik Montnemery --- homeassistant/components/sensor/__init__.py | 16 ++++++++------ tests/components/sensor/test_init.py | 24 +++++++++++++++++++++ 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 1d06e1a24c4..ad6b3454ea9 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -747,13 +747,15 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): return value - def _suggested_precision_or_none(self) -> int | None: - """Return suggested display precision, or None if not set.""" + def _display_precision_or_none(self) -> int | None: + """Return display precision, or None if not set.""" assert self.registry_entry - if (sensor_options := self.registry_entry.options.get(DOMAIN)) and ( - precision := sensor_options.get("suggested_display_precision") - ) is not None: - return cast(int, precision) + if not (sensor_options := self.registry_entry.options.get(DOMAIN)): + return None + + for option in ("display_precision", "suggested_display_precision"): + if (precision := sensor_options.get(option)) is not None: + return cast(int, precision) return None def _update_suggested_precision(self) -> None: @@ -835,7 +837,7 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): Called when the entity registry entry has been updated and before the sensor is added to the state machine. """ - self._sensor_option_display_precision = self._suggested_precision_or_none() + self._sensor_option_display_precision = self._display_precision_or_none() assert self.registry_entry if ( sensor_options := self.registry_entry.options.get(f"{DOMAIN}.private") diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 9e8e401ea46..74fd81188cd 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -1146,6 +1146,14 @@ async def test_unit_conversion_priority_precision( suggested_display_precision=suggested_precision, suggested_unit_of_measurement=suggested_unit, ) + entity4 = MockSensor( + name="Test", + device_class=device_class, + native_unit_of_measurement=native_unit, + native_value=str(native_value), + suggested_display_precision=None, + unique_id="very_unique_4", + ) setup_test_component_platform( hass, sensor.DOMAIN, @@ -1154,6 +1162,7 @@ async def test_unit_conversion_priority_precision( entity1, entity2, entity3, + entity4, ], ) @@ -1230,6 +1239,21 @@ async def test_unit_conversion_priority_precision( round(custom_state, 4) ) + # Set a display_precision without having suggested_display_precision + entity_registry.async_update_entity_options( + entity4.entity_id, + "sensor", + {"display_precision": 4}, + ) + entry4 = entity_registry.async_get(entity4.entity_id) + assert "suggested_display_precision" not in entry4.options["sensor"] + assert entry4.options["sensor"]["display_precision"] == 4 + await hass.async_block_till_done() + state = hass.states.get(entity4.entity_id) + assert float(async_rounded_state(hass, entity4.entity_id, state)) == pytest.approx( + round(automatic_state, 4) + ) + @pytest.mark.parametrize( ( From ec377ce6657f01fcb9fb9b2b285a7f3459879272 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 24 Apr 2024 09:49:10 +0200 Subject: [PATCH 826/967] Bump deebot-client to 7.1.0 (#116082) --- 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 2e088024215..aad04d9ec87 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.9", "deebot-client==7.0.0"] + "requirements": ["py-sucks==0.9.9", "deebot-client==7.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index df688e6e00f..42f716f58e3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -697,7 +697,7 @@ debugpy==1.8.1 # decora==0.6 # homeassistant.components.ecovacs -deebot-client==7.0.0 +deebot-client==7.1.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 60e54a81780..11533275029 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -575,7 +575,7 @@ dbus-fast==2.21.1 debugpy==1.8.1 # homeassistant.components.ecovacs -deebot-client==7.0.0 +deebot-client==7.1.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns From b520efb87ae422faba8d2fcddecd201865fe1df0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 24 Apr 2024 09:56:59 +0200 Subject: [PATCH 827/967] Small speed up to async_track_event (#116083) --- homeassistant/helpers/event.py | 36 +++++++++++++--------------------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 7fae0976686..5cffe992c0d 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -3,11 +3,12 @@ from __future__ import annotations import asyncio +from collections import defaultdict from collections.abc import Callable, Coroutine, Iterable, Mapping, Sequence import copy from dataclasses import dataclass from datetime import datetime, timedelta -import functools as ft +from functools import partial, wraps import logging from random import randint import time @@ -161,7 +162,7 @@ def threaded_listener_factory( ) -> Callable[Concatenate[HomeAssistant, _P], CALLBACK_TYPE]: """Convert an async event helper to a threaded one.""" - @ft.wraps(async_factory) + @wraps(async_factory) def factory( hass: HomeAssistant, *args: _P.args, **kwargs: _P.kwargs ) -> CALLBACK_TYPE: @@ -170,7 +171,7 @@ def threaded_listener_factory( raise TypeError("First parameter needs to be a hass instance") async_remove = run_callback_threadsafe( - hass.loop, ft.partial(async_factory, hass, *args, **kwargs) + hass.loop, partial(async_factory, hass, *args, **kwargs) ).result() def remove() -> None: @@ -409,19 +410,16 @@ def _async_track_event( return _remove_empty_listener hass_data = hass.data - callbacks_key = tracker.callbacks_key - - callbacks: dict[str, list[HassJob[[Event[_TypedDictT]], Any]]] | None - if not (callbacks := hass_data.get(callbacks_key)): - callbacks = hass_data[callbacks_key] = {} + callbacks: defaultdict[str, list[HassJob[[Event[_TypedDictT]], Any]]] | None + if not (callbacks := hass_data.get(tracker.callbacks_key)): + callbacks = hass_data[tracker.callbacks_key] = defaultdict(list) listeners_key = tracker.listeners_key - - if listeners_key not in hass_data: - hass_data[listeners_key] = hass.bus.async_listen( + if tracker.listeners_key not in hass_data: + hass_data[tracker.listeners_key] = hass.bus.async_listen( tracker.event_type, - ft.partial(tracker.dispatcher_callable, hass, callbacks), - event_filter=ft.partial(tracker.filter_callable, hass, callbacks), + partial(tracker.dispatcher_callable, hass, callbacks), + event_filter=partial(tracker.filter_callable, hass, callbacks), ) job = HassJob(action, f"track {tracker.event_type} event {keys}", job_type=job_type) @@ -432,19 +430,13 @@ def _async_track_event( # here because this function gets called ~20000 times # during startup, and we want to avoid the overhead of # creating empty lists and throwing them away. - if callback_list := callbacks.get(keys): - callback_list.append(job) - else: - callbacks[keys] = [job] + callbacks[keys].append(job) keys = [keys] else: for key in keys: - if callback_list := callbacks.get(key): - callback_list.append(job) - else: - callbacks[key] = [job] + callbacks[key].append(job) - return ft.partial(_remove_listener, hass, listeners_key, keys, job, callbacks) + return partial(_remove_listener, hass, listeners_key, keys, job, callbacks) @callback From a4829330f6cdb3159d2ec905120ea284ae1c86c8 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 24 Apr 2024 09:57:38 +0200 Subject: [PATCH 828/967] Add strict connection for cloud (#115814) Co-authored-by: Martin Hjelmare --- homeassistant/components/cloud/__init__.py | 100 +++++- homeassistant/components/cloud/client.py | 1 + homeassistant/components/cloud/const.py | 1 + homeassistant/components/cloud/http_api.py | 6 +- homeassistant/components/cloud/icons.json | 1 + homeassistant/components/cloud/manifest.json | 2 +- homeassistant/components/cloud/prefs.py | 18 +- homeassistant/components/cloud/strings.json | 12 + homeassistant/components/cloud/util.py | 15 + homeassistant/components/http/__init__.py | 9 +- homeassistant/components/http/auth.py | 100 ++++-- homeassistant/components/http/const.py | 2 + homeassistant/helpers/network.py | 12 +- script/hassfest/dependencies.py | 1 + tests/components/cloud/test_client.py | 2 + tests/components/cloud/test_http_api.py | 5 + tests/components/cloud/test_init.py | 84 ++++- tests/components/cloud/test_prefs.py | 25 +- .../cloud/test_strict_connection.py | 294 ++++++++++++++++++ tests/helpers/test_network.py | 12 + 20 files changed, 644 insertions(+), 58 deletions(-) create mode 100644 homeassistant/components/cloud/util.py create mode 100644 tests/components/cloud/test_strict_connection.py diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 80f9d9f9368..2552fe4bf5c 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -7,11 +7,14 @@ from collections.abc import Awaitable, Callable from datetime import datetime, timedelta from enum import Enum from typing import cast +from urllib.parse import quote_plus, urljoin from hass_nabucasa import Cloud import voluptuous as vol -from homeassistant.components import alexa, google_assistant +from homeassistant.components import alexa, google_assistant, http +from homeassistant.components.auth import STRICT_CONNECTION_URL +from homeassistant.components.http.auth import async_sign_path from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry from homeassistant.const import ( CONF_DESCRIPTION, @@ -21,8 +24,21 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, Platform, ) -from homeassistant.core import Event, HassJob, HomeAssistant, ServiceCall, callback -from homeassistant.exceptions import HomeAssistantError +from homeassistant.core import ( + Event, + HassJob, + HomeAssistant, + ServiceCall, + ServiceResponse, + SupportsResponse, + callback, +) +from homeassistant.exceptions import ( + HomeAssistantError, + ServiceValidationError, + Unauthorized, + UnknownUser, +) from homeassistant.helpers import config_validation as cv, entityfilter from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.discovery import async_load_platform @@ -31,6 +47,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.network import NoURLAvailableError, get_url from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass @@ -265,18 +282,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown) _remote_handle_prefs_updated(cloud) - - async def _service_handler(service: ServiceCall) -> None: - """Handle service for cloud.""" - if service.service == SERVICE_REMOTE_CONNECT: - await prefs.async_update(remote_enabled=True) - elif service.service == SERVICE_REMOTE_DISCONNECT: - await prefs.async_update(remote_enabled=False) - - async_register_admin_service(hass, DOMAIN, SERVICE_REMOTE_CONNECT, _service_handler) - async_register_admin_service( - hass, DOMAIN, SERVICE_REMOTE_DISCONNECT, _service_handler - ) + _setup_services(hass, prefs) async def async_startup_repairs(_: datetime) -> None: """Create repair issues after startup.""" @@ -395,3 +401,67 @@ 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) + + +@callback +def _setup_services(hass: HomeAssistant, prefs: CloudPreferences) -> None: + """Set up services for cloud component.""" + + async def _service_handler(service: ServiceCall) -> None: + """Handle service for cloud.""" + if service.service == SERVICE_REMOTE_CONNECT: + await prefs.async_update(remote_enabled=True) + elif service.service == SERVICE_REMOTE_DISCONNECT: + await prefs.async_update(remote_enabled=False) + + async_register_admin_service(hass, DOMAIN, SERVICE_REMOTE_CONNECT, _service_handler) + async_register_admin_service( + hass, DOMAIN, SERVICE_REMOTE_DISCONNECT, _service_handler + ) + + async def create_temporary_strict_connection_url( + call: ServiceCall, + ) -> ServiceResponse: + """Create a strict connection url and return it.""" + # Copied form homeassistant/helpers/service.py#_async_admin_handler + # as the helper supports no responses yet + if call.context.user_id: + user = await hass.auth.async_get_user(call.context.user_id) + if user is None: + raise UnknownUser(context=call.context) + if not user.is_admin: + raise Unauthorized(context=call.context) + + if prefs.strict_connection is http.const.StrictConnectionMode.DISABLED: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="strict_connection_not_enabled", + ) + + try: + url = get_url(hass, require_cloud=True) + except NoURLAvailableError as ex: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="no_url_available", + ) from ex + + path = async_sign_path( + hass, + STRICT_CONNECTION_URL, + timedelta(hours=1), + use_content_user=True, + ) + url = urljoin(url, path) + + return { + "url": f"https://login.home-assistant.io?u={quote_plus(url)}", + "direct_url": url, + } + + hass.services.async_register( + DOMAIN, + "create_temporary_strict_connection_url", + create_temporary_strict_connection_url, + supports_response=SupportsResponse.ONLY, + ) diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index 01c8de77156..c4d1c1dec60 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -250,6 +250,7 @@ class CloudClient(Interface): "enabled": self._prefs.remote_enabled, "instance_domain": self.cloud.remote.instance_domain, "alias": self.cloud.remote.alias, + "strict_connection": self._prefs.strict_connection, }, "version": HA_VERSION, "instance_id": self.prefs.instance_id, diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index 2c58dd57340..8b68eefc443 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -33,6 +33,7 @@ PREF_GOOGLE_SETTINGS_VERSION = "google_settings_version" PREF_TTS_DEFAULT_VOICE = "tts_default_voice" PREF_GOOGLE_CONNECTED = "google_connected" PREF_REMOTE_ALLOW_REMOTE_ENABLE = "remote_allow_remote_enable" +PREF_STRICT_CONNECTION = "strict_connection" DEFAULT_TTS_DEFAULT_VOICE = ("en-US", "JennyNeural") DEFAULT_DISABLE_2FA = False DEFAULT_ALEXA_REPORT_STATE = True diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index b577e9de0d4..29185191a20 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -19,7 +19,7 @@ from hass_nabucasa.const import STATE_DISCONNECTED from hass_nabucasa.voice import TTS_VOICES import voluptuous as vol -from homeassistant.components import websocket_api +from homeassistant.components import http, websocket_api from homeassistant.components.alexa import ( entities as alexa_entities, errors as alexa_errors, @@ -46,6 +46,7 @@ from .const import ( PREF_GOOGLE_REPORT_STATE, PREF_GOOGLE_SECURE_DEVICES_PIN, PREF_REMOTE_ALLOW_REMOTE_ENABLE, + PREF_STRICT_CONNECTION, PREF_TTS_DEFAULT_VOICE, REQUEST_TIMEOUT, ) @@ -452,6 +453,9 @@ def validate_language_voice(value: tuple[str, str]) -> tuple[str, str]: vol.Coerce(tuple), validate_language_voice ), vol.Optional(PREF_REMOTE_ALLOW_REMOTE_ENABLE): bool, + vol.Optional(PREF_STRICT_CONNECTION): vol.Coerce( + http.const.StrictConnectionMode + ), } ) @websocket_api.async_response diff --git a/homeassistant/components/cloud/icons.json b/homeassistant/components/cloud/icons.json index 06ee7eb2f19..1a8593388b4 100644 --- a/homeassistant/components/cloud/icons.json +++ b/homeassistant/components/cloud/icons.json @@ -1,5 +1,6 @@ { "services": { + "create_temporary_strict_connection_url": "mdi:login-variant", "remote_connect": "mdi:cloud", "remote_disconnect": "mdi:cloud-off" } diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index 49a3fc0bf5c..0d2ee546ad8 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Cloud", "after_dependencies": ["assist_pipeline", "google_assistant", "alexa"], "codeowners": ["@home-assistant/cloud"], - "dependencies": ["http", "repairs", "webhook"], + "dependencies": ["auth", "http", "repairs", "webhook"], "documentation": "https://www.home-assistant.io/integrations/cloud", "integration_type": "system", "iot_class": "cloud_push", diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index af4e68194d6..9fce615128b 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -10,7 +10,7 @@ from hass_nabucasa.voice import MAP_VOICE from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.auth.models import User -from homeassistant.components import webhook +from homeassistant.components import http, webhook from homeassistant.components.google_assistant.http import ( async_get_users as async_get_google_assistant_users, ) @@ -44,6 +44,7 @@ from .const import ( PREF_INSTANCE_ID, PREF_REMOTE_ALLOW_REMOTE_ENABLE, PREF_REMOTE_DOMAIN, + PREF_STRICT_CONNECTION, PREF_TTS_DEFAULT_VOICE, PREF_USERNAME, ) @@ -176,6 +177,7 @@ class CloudPreferences: google_settings_version: int | UndefinedType = UNDEFINED, google_connected: bool | UndefinedType = UNDEFINED, remote_allow_remote_enable: bool | UndefinedType = UNDEFINED, + strict_connection: http.const.StrictConnectionMode | UndefinedType = UNDEFINED, ) -> None: """Update user preferences.""" prefs = {**self._prefs} @@ -195,6 +197,7 @@ class CloudPreferences: (PREF_REMOTE_DOMAIN, remote_domain), (PREF_GOOGLE_CONNECTED, google_connected), (PREF_REMOTE_ALLOW_REMOTE_ENABLE, remote_allow_remote_enable), + (PREF_STRICT_CONNECTION, strict_connection), ): if value is not UNDEFINED: prefs[key] = value @@ -242,6 +245,7 @@ class CloudPreferences: PREF_GOOGLE_SECURE_DEVICES_PIN: self.google_secure_devices_pin, PREF_REMOTE_ALLOW_REMOTE_ENABLE: self.remote_allow_remote_enable, PREF_TTS_DEFAULT_VOICE: self.tts_default_voice, + PREF_STRICT_CONNECTION: self.strict_connection, } @property @@ -358,6 +362,17 @@ class CloudPreferences: """ return self._prefs.get(PREF_TTS_DEFAULT_VOICE, DEFAULT_TTS_DEFAULT_VOICE) # type: ignore[no-any-return] + @property + def strict_connection(self) -> http.const.StrictConnectionMode: + """Return the strict connection mode.""" + mode = self._prefs.get( + PREF_STRICT_CONNECTION, http.const.StrictConnectionMode.DISABLED + ) + + if not isinstance(mode, http.const.StrictConnectionMode): + mode = http.const.StrictConnectionMode(mode) + return mode # type: ignore[no-any-return] + async def get_cloud_user(self) -> str: """Return ID of Home Assistant Cloud system user.""" user = await self._load_cloud_user() @@ -415,4 +430,5 @@ class CloudPreferences: PREF_REMOTE_DOMAIN: None, PREF_REMOTE_ALLOW_REMOTE_ENABLE: True, PREF_USERNAME: username, + PREF_STRICT_CONNECTION: http.const.StrictConnectionMode.DISABLED, } diff --git a/homeassistant/components/cloud/strings.json b/homeassistant/components/cloud/strings.json index 16a82a27c1a..1fec87235da 100644 --- a/homeassistant/components/cloud/strings.json +++ b/homeassistant/components/cloud/strings.json @@ -5,6 +5,14 @@ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" } }, + "exceptions": { + "strict_connection_not_enabled": { + "message": "Strict connection is not enabled for cloud requests" + }, + "no_url_available": { + "message": "No cloud URL available.\nPlease mark sure you have a working Remote UI." + } + }, "system_health": { "info": { "can_reach_cert_server": "Reach Certificate Server", @@ -73,6 +81,10 @@ } }, "services": { + "create_temporary_strict_connection_url": { + "name": "Create a temporary strict connection URL", + "description": "Create a temporary strict connection URL, which can be used to login on another device." + }, "remote_connect": { "name": "Remote connect", "description": "Makes the instance UI accessible from outside of the local network by using Home Assistant Cloud." diff --git a/homeassistant/components/cloud/util.py b/homeassistant/components/cloud/util.py new file mode 100644 index 00000000000..3e055851fff --- /dev/null +++ b/homeassistant/components/cloud/util.py @@ -0,0 +1,15 @@ +"""Cloud util functions.""" + +from hass_nabucasa import Cloud + +from homeassistant.components import http +from homeassistant.core import HomeAssistant + +from .client import CloudClient +from .const import DOMAIN + + +def get_strict_connection_mode(hass: HomeAssistant) -> http.const.StrictConnectionMode: + """Get the strict connection mode.""" + cloud: Cloud[CloudClient] = hass.data[DOMAIN] + return cloud.client.prefs.strict_connection diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index f9532b90ce6..83601599d88 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -69,6 +69,7 @@ from homeassistant.util.json import json_loads from .auth import async_setup_auth, async_sign_path from .ban import setup_bans from .const import ( # noqa: F401 + DOMAIN, KEY_HASS_REFRESH_TOKEN_ID, KEY_HASS_USER, StrictConnectionMode, @@ -82,8 +83,6 @@ from .security_filter import setup_security_filter from .static import CACHE_HEADERS, CachingStaticResource from .web_runner import HomeAssistantTCPSite -DOMAIN: Final = "http" - CONF_SERVER_HOST: Final = "server_host" CONF_SERVER_PORT: Final = "server_port" CONF_BASE_URL: Final = "base_url" @@ -149,7 +148,7 @@ HTTP_SCHEMA: Final = vol.All( vol.Optional(CONF_USE_X_FRAME_OPTIONS, default=True): cv.boolean, vol.Optional( CONF_STRICT_CONNECTION, default=StrictConnectionMode.DISABLED - ): vol.In([e.value for e in StrictConnectionMode]), + ): vol.Coerce(StrictConnectionMode), } ), ) @@ -628,7 +627,9 @@ def _setup_services(hass: HomeAssistant, conf: ConfData) -> None: ) try: - url = get_url(hass, prefer_external=True, allow_internal=False) + url = get_url( + hass, prefer_external=True, allow_internal=False, allow_cloud=False + ) except NoURLAvailableError as ex: raise ServiceValidationError( translation_domain=DOMAIN, diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 1eb74289089..889c9e76367 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -25,6 +25,7 @@ from homeassistant.auth.const import GROUP_ID_READ_ONLY from homeassistant.auth.models import User from homeassistant.components import websocket_api from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import singleton from homeassistant.helpers.http import current_request from homeassistant.helpers.json import json_bytes from homeassistant.helpers.network import is_cloud_connection @@ -32,6 +33,7 @@ from homeassistant.helpers.storage import Store from homeassistant.util.network import is_local from .const import ( + DOMAIN, KEY_AUTHENTICATED, KEY_HASS_REFRESH_TOKEN_ID, KEY_HASS_USER, @@ -50,8 +52,9 @@ STORAGE_VERSION = 1 STORAGE_KEY = "http.auth" CONTENT_USER_NAME = "Home Assistant Content" STRICT_CONNECTION_EXCLUDED_PATH = "/api/webhook/" +STRICT_CONNECTION_STATIC_PAGE_NAME = "strict_connection_static_page.html" STRICT_CONNECTION_STATIC_PAGE = os.path.join( - os.path.dirname(__file__), "strict_connection_static_page.html" + os.path.dirname(__file__), STRICT_CONNECTION_STATIC_PAGE_NAME ) @@ -156,16 +159,10 @@ async def async_setup_auth( await store.async_save(data) hass.data[STORAGE_KEY] = refresh_token.id - strict_connection_static_file_content = None + if strict_connection_mode_non_cloud is StrictConnectionMode.STATIC_PAGE: - - def read_static_page() -> str: - with open(STRICT_CONNECTION_STATIC_PAGE, encoding="utf-8") as file: - return file.read() - - strict_connection_static_file_content = await hass.async_add_executor_job( - read_static_page - ) + # Load the static page content on setup + await _read_strict_connection_static_page(hass) @callback def async_validate_auth_header(request: Request) -> bool: @@ -255,21 +252,36 @@ async def async_setup_auth( authenticated = True auth_type = "signed request" - if ( - not authenticated - and strict_connection_mode_non_cloud is not StrictConnectionMode.DISABLED - and not request.path.startswith(STRICT_CONNECTION_EXCLUDED_PATH) - and not await hass.auth.session.async_validate_request_for_strict_connection_session( - request - ) - and ( - resp := _async_perform_action_on_non_local( - request, strict_connection_static_file_content - ) - ) - is not None + if not authenticated and not request.path.startswith( + STRICT_CONNECTION_EXCLUDED_PATH ): - return resp + strict_connection_mode = strict_connection_mode_non_cloud + strict_connection_func = ( + _async_perform_strict_connection_action_on_non_local + ) + if is_cloud_connection(hass): + from homeassistant.components.cloud.util import ( # pylint: disable=import-outside-toplevel + get_strict_connection_mode, + ) + + strict_connection_mode = get_strict_connection_mode(hass) + strict_connection_func = _async_perform_strict_connection_action + + if ( + strict_connection_mode is not StrictConnectionMode.DISABLED + and not await hass.auth.session.async_validate_request_for_strict_connection_session( + request + ) + and ( + resp := await strict_connection_func( + hass, + request, + strict_connection_mode is StrictConnectionMode.STATIC_PAGE, + ) + ) + is not None + ): + return resp if authenticated and _LOGGER.isEnabledFor(logging.DEBUG): _LOGGER.debug( @@ -286,17 +298,17 @@ async def async_setup_auth( app.middlewares.append(auth_middleware) -@callback -def _async_perform_action_on_non_local( +async def _async_perform_strict_connection_action_on_non_local( + hass: HomeAssistant, request: Request, - strict_connection_static_file_content: str | None, + static_page: bool, ) -> StreamResponse | None: """Perform strict connection mode action if the request is not local. The function does the following: - Try to get the IP address of the request. If it fails, assume it's not local - If the request is local, return None (allow the request to continue) - - If strict_connection_static_file_content is set, return a response with the content + - If static_page is True, return a response with the content - Otherwise close the connection and raise an exception """ try: @@ -308,10 +320,25 @@ def _async_perform_action_on_non_local( if ip_address_ and is_local(ip_address_): return None - _LOGGER.debug("Perform strict connection action for %s", ip_address_) - if strict_connection_static_file_content: + return await _async_perform_strict_connection_action(hass, request, static_page) + + +async def _async_perform_strict_connection_action( + hass: HomeAssistant, + request: Request, + static_page: bool, +) -> StreamResponse | None: + """Perform strict connection mode action. + + The function does the following: + - If static_page is True, return a response with the content + - Otherwise close the connection and raise an exception + """ + + _LOGGER.debug("Perform strict connection action for %s", request.remote) + if static_page: return Response( - text=strict_connection_static_file_content, + text=await _read_strict_connection_static_page(hass), content_type="text/html", status=HTTPStatus.IM_A_TEAPOT, ) @@ -322,3 +349,14 @@ def _async_perform_action_on_non_local( # We need to raise an exception to stop processing the request raise HTTPBadRequest + + +@singleton.singleton(f"{DOMAIN}_{STRICT_CONNECTION_STATIC_PAGE_NAME}") +async def _read_strict_connection_static_page(hass: HomeAssistant) -> str: + """Read the strict connection static page from disk via executor.""" + + def read_static_page() -> str: + with open(STRICT_CONNECTION_STATIC_PAGE, encoding="utf-8") as file: + return file.read() + + return await hass.async_add_executor_job(read_static_page) diff --git a/homeassistant/components/http/const.py b/homeassistant/components/http/const.py index d02416c531b..662596288c7 100644 --- a/homeassistant/components/http/const.py +++ b/homeassistant/components/http/const.py @@ -5,6 +5,8 @@ from typing import Final from homeassistant.helpers.http import KEY_AUTHENTICATED, KEY_HASS # noqa: F401 +DOMAIN: Final = "http" + KEY_HASS_USER: Final = "hass_user" KEY_HASS_REFRESH_TOKEN_ID: Final = "hass_refresh_token_id" diff --git a/homeassistant/helpers/network.py b/homeassistant/helpers/network.py index 6e8fa8dc3a3..d5891973e40 100644 --- a/homeassistant/helpers/network.py +++ b/homeassistant/helpers/network.py @@ -122,6 +122,7 @@ def get_url( require_current_request: bool = False, require_ssl: bool = False, require_standard_port: bool = False, + require_cloud: bool = False, allow_internal: bool = True, allow_external: bool = True, allow_cloud: bool = True, @@ -145,7 +146,7 @@ def get_url( # Try finding an URL in the order specified for url_type in order: - if allow_internal and url_type == TYPE_URL_INTERNAL: + if allow_internal and url_type == TYPE_URL_INTERNAL and not require_cloud: with suppress(NoURLAvailableError): return _get_internal_url( hass, @@ -155,7 +156,7 @@ def get_url( require_standard_port=require_standard_port, ) - if allow_external and url_type == TYPE_URL_EXTERNAL: + if require_cloud or (allow_external and url_type == TYPE_URL_EXTERNAL): with suppress(NoURLAvailableError): return _get_external_url( hass, @@ -165,7 +166,10 @@ def get_url( require_current_request=require_current_request, require_ssl=require_ssl, require_standard_port=require_standard_port, + require_cloud=require_cloud, ) + if require_cloud: + raise NoURLAvailableError # For current request, we accept loopback interfaces (e.g., 127.0.0.1), # the Supervisor hostname and localhost transparently @@ -263,8 +267,12 @@ def _get_external_url( require_current_request: bool = False, require_ssl: bool = False, require_standard_port: bool = False, + require_cloud: bool = False, ) -> str: """Get external URL of this instance.""" + if require_cloud: + return _get_cloud_url(hass, require_current_request=require_current_request) + if prefer_cloud and allow_cloud: with suppress(NoURLAvailableError): return _get_cloud_url(hass) diff --git a/script/hassfest/dependencies.py b/script/hassfest/dependencies.py index 1547bc1e829..d4eb135a265 100644 --- a/script/hassfest/dependencies.py +++ b/script/hassfest/dependencies.py @@ -152,6 +152,7 @@ IGNORE_VIOLATIONS = { ("demo", "manual"), # This would be a circular dep ("http", "network"), + ("http", "cloud"), # This would be a circular dep ("zha", "homeassistant_hardware"), ("zha", "homeassistant_sky_connect"), diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index 5e15aa32b6f..bcddc32f107 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -24,6 +24,7 @@ from homeassistant.components.homeassistant.exposed_entities import ( ExposedEntities, async_expose_entity, ) +from homeassistant.components.http.const import StrictConnectionMode from homeassistant.const import CONTENT_TYPE_JSON, __version__ as HA_VERSION from homeassistant.core import HomeAssistant, State from homeassistant.helpers import entity_registry as er @@ -387,6 +388,7 @@ async def test_cloud_connection_info(hass: HomeAssistant) -> None: "connected": False, "enabled": False, "instance_domain": None, + "strict_connection": StrictConnectionMode.DISABLED, }, "version": HA_VERSION, } diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 5ee9af88681..d9d2b5c6742 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -19,6 +19,7 @@ from homeassistant.components.assist_pipeline.pipeline import STORAGE_KEY from homeassistant.components.cloud.const import DEFAULT_EXPOSED_DOMAINS, DOMAIN from homeassistant.components.google_assistant.helpers import GoogleEntity from homeassistant.components.homeassistant import exposed_entities +from homeassistant.components.http.const import StrictConnectionMode from homeassistant.components.websocket_api import ERR_INVALID_FORMAT from homeassistant.core import HomeAssistant, State from homeassistant.helpers import entity_registry as er @@ -782,6 +783,7 @@ async def test_websocket_status( "google_report_state": True, "remote_allow_remote_enable": True, "remote_enabled": False, + "strict_connection": "disabled", "tts_default_voice": ["en-US", "JennyNeural"], }, "alexa_entities": { @@ -901,6 +903,7 @@ async def test_websocket_update_preferences( assert cloud.client.prefs.alexa_enabled assert cloud.client.prefs.google_secure_devices_pin is None assert cloud.client.prefs.remote_allow_remote_enable is True + assert cloud.client.prefs.strict_connection is StrictConnectionMode.DISABLED client = await hass_ws_client(hass) @@ -912,6 +915,7 @@ async def test_websocket_update_preferences( "google_secure_devices_pin": "1234", "tts_default_voice": ["en-GB", "RyanNeural"], "remote_allow_remote_enable": False, + "strict_connection": StrictConnectionMode.DROP_CONNECTION, } ) response = await client.receive_json() @@ -922,6 +926,7 @@ async def test_websocket_update_preferences( assert cloud.client.prefs.google_secure_devices_pin == "1234" assert cloud.client.prefs.remote_allow_remote_enable is False assert cloud.client.prefs.tts_default_voice == ("en-GB", "RyanNeural") + assert cloud.client.prefs.strict_connection is StrictConnectionMode.DROP_CONNECTION @pytest.mark.parametrize( diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index 9cc1324ebc1..98f9a54c04b 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -3,6 +3,7 @@ from collections.abc import Callable, Coroutine from typing import Any from unittest.mock import MagicMock, patch +from urllib.parse import quote_plus from hass_nabucasa import Cloud import pytest @@ -13,11 +14,16 @@ 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 ( + DOMAIN, + PREF_CLOUDHOOKS, + PREF_STRICT_CONNECTION, +) from homeassistant.components.cloud.prefs import STORAGE_KEY +from homeassistant.components.http.const import StrictConnectionMode from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import Context, HomeAssistant -from homeassistant.exceptions import Unauthorized +from homeassistant.exceptions import ServiceValidationError, Unauthorized from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, MockUser @@ -295,3 +301,77 @@ async def test_cloud_logout( await hass.async_block_till_done() assert cloud.is_logged_in is False + + +async def test_service_create_temporary_strict_connection_url_strict_connection_disabled( + hass: HomeAssistant, +) -> None: + """Test service create_temporary_strict_connection_url with strict_connection not enabled.""" + mock_config_entry = MockConfigEntry(domain=DOMAIN) + mock_config_entry.add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, {"cloud": {}}) + await hass.async_block_till_done() + with pytest.raises( + ServiceValidationError, + match="Strict connection is not enabled for cloud requests", + ): + await hass.services.async_call( + cloud.DOMAIN, + "create_temporary_strict_connection_url", + blocking=True, + return_response=True, + ) + + +@pytest.mark.parametrize( + ("mode"), + [ + StrictConnectionMode.DROP_CONNECTION, + StrictConnectionMode.STATIC_PAGE, + ], +) +async def test_service_create_temporary_strict_connection( + hass: HomeAssistant, + set_cloud_prefs: Callable[[dict[str, Any]], Coroutine[Any, Any, None]], + mode: StrictConnectionMode, +) -> None: + """Test service create_temporary_strict_connection_url.""" + mock_config_entry = MockConfigEntry(domain=DOMAIN) + mock_config_entry.add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, {"cloud": {}}) + await hass.async_block_till_done() + + await set_cloud_prefs( + { + PREF_STRICT_CONNECTION: mode, + } + ) + + # No cloud url set + with pytest.raises(ServiceValidationError, match="No cloud URL available"): + await hass.services.async_call( + cloud.DOMAIN, + "create_temporary_strict_connection_url", + blocking=True, + return_response=True, + ) + + # Patch cloud url + url = "https://example.com" + with patch( + "homeassistant.helpers.network._get_cloud_url", + return_value=url, + ): + response = await hass.services.async_call( + cloud.DOMAIN, + "create_temporary_strict_connection_url", + blocking=True, + return_response=True, + ) + assert isinstance(response, dict) + direct_url_prefix = f"{url}/auth/strict_connection/temp_token?authSig=" + assert response.pop("direct_url").startswith(direct_url_prefix) + assert response.pop("url").startswith( + f"https://login.home-assistant.io?u={quote_plus(direct_url_prefix)}" + ) + assert response == {} # No more keys in response diff --git a/tests/components/cloud/test_prefs.py b/tests/components/cloud/test_prefs.py index 9b0fa4c01d7..1ed2e1d524f 100644 --- a/tests/components/cloud/test_prefs.py +++ b/tests/components/cloud/test_prefs.py @@ -6,8 +6,13 @@ from unittest.mock import ANY, MagicMock, patch import pytest from homeassistant.auth.const import GROUP_ID_ADMIN -from homeassistant.components.cloud.const import DOMAIN, PREF_TTS_DEFAULT_VOICE +from homeassistant.components.cloud.const import ( + DOMAIN, + PREF_STRICT_CONNECTION, + PREF_TTS_DEFAULT_VOICE, +) from homeassistant.components.cloud.prefs import STORAGE_KEY, CloudPreferences +from homeassistant.components.http.const import StrictConnectionMode from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -174,3 +179,21 @@ async def test_tts_default_voice_legacy_gender( await hass.async_block_till_done() assert cloud.client.prefs.tts_default_voice == (expected_language, voice) + + +@pytest.mark.parametrize("mode", list(StrictConnectionMode)) +async def test_strict_connection_convertion( + hass: HomeAssistant, + cloud: MagicMock, + hass_storage: dict[str, Any], + mode: StrictConnectionMode, +) -> None: + """Test strict connection string value will be converted to the enum.""" + hass_storage[STORAGE_KEY] = { + "version": 1, + "data": {PREF_STRICT_CONNECTION: mode.value}, + } + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + + assert cloud.client.prefs.strict_connection is mode diff --git a/tests/components/cloud/test_strict_connection.py b/tests/components/cloud/test_strict_connection.py new file mode 100644 index 00000000000..844096ab0eb --- /dev/null +++ b/tests/components/cloud/test_strict_connection.py @@ -0,0 +1,294 @@ +"""Test strict connection mode for cloud.""" + +from collections.abc import Awaitable, Callable, Coroutine, Generator +from contextlib import contextmanager +from datetime import timedelta +from http import HTTPStatus +from typing import Any +from unittest.mock import MagicMock, Mock, patch + +from aiohttp import ServerDisconnectedError, web +from aiohttp.test_utils import TestClient +from aiohttp_session import get_session +import pytest +from yarl import URL + +from homeassistant.auth.models import RefreshToken +from homeassistant.auth.session import SESSION_ID, TEMP_TIMEOUT +from homeassistant.components.cloud.const import PREF_STRICT_CONNECTION +from homeassistant.components.http import KEY_HASS +from homeassistant.components.http.auth import ( + STRICT_CONNECTION_STATIC_PAGE, + async_setup_auth, + async_sign_path, +) +from homeassistant.components.http.const import KEY_AUTHENTICATED, StrictConnectionMode +from homeassistant.components.http.session import COOKIE_NAME, PREFIXED_COOKIE_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.network import is_cloud_connection +from homeassistant.setup import async_setup_component +from homeassistant.util.dt import utcnow + +from tests.common import async_fire_time_changed +from tests.typing import ClientSessionGenerator + + +@pytest.fixture +async def refresh_token(hass: HomeAssistant, hass_access_token: str) -> RefreshToken: + """Return a refresh token.""" + refresh_token = hass.auth.async_validate_access_token(hass_access_token) + assert refresh_token + session = hass.auth.session + assert session._strict_connection_sessions == {} + assert session._temp_sessions == {} + return refresh_token + + +@contextmanager +def simulate_cloud_request() -> Generator[None, None, None]: + """Simulate a cloud request.""" + with patch( + "hass_nabucasa.remote.is_cloud_request", Mock(get=Mock(return_value=True)) + ): + yield + + +@pytest.fixture +def app_strict_connection( + hass: HomeAssistant, refresh_token: RefreshToken +) -> web.Application: + """Fixture to set up a web.Application.""" + + async def handler(request): + """Return if request was authenticated.""" + return web.json_response(data={"authenticated": request[KEY_AUTHENTICATED]}) + + app = web.Application() + app[KEY_HASS] = hass + app.router.add_get("/", handler) + + async def set_cookie(request: web.Request) -> web.Response: + hass = request.app[KEY_HASS] + # Clear all sessions + hass.auth.session._temp_sessions.clear() + hass.auth.session._strict_connection_sessions.clear() + + if request.query["token"] == "refresh": + await hass.auth.session.async_create_session(request, refresh_token) + else: + await hass.auth.session.async_create_temp_unauthorized_session(request) + session = await get_session(request) + return web.Response(text=session[SESSION_ID]) + + app.router.add_get("/test/cookie", set_cookie) + return app + + +@pytest.fixture(name="client") +async def set_up_fixture( + hass: HomeAssistant, + aiohttp_client: ClientSessionGenerator, + app_strict_connection: web.Application, + cloud: MagicMock, + socket_enabled: None, +) -> TestClient: + """Set up the fixture.""" + + await async_setup_auth(hass, app_strict_connection, StrictConnectionMode.DISABLED) + assert await async_setup_component(hass, "cloud", {"cloud": {}}) + await hass.async_block_till_done() + return await aiohttp_client(app_strict_connection) + + +@pytest.mark.parametrize( + "strict_connection_mode", [e.value for e in StrictConnectionMode] +) +async def test_strict_connection_cloud_authenticated_requests( + hass: HomeAssistant, + client: TestClient, + hass_access_token: str, + set_cloud_prefs: Callable[[dict[str, Any]], Coroutine[Any, Any, None]], + refresh_token: RefreshToken, + strict_connection_mode: StrictConnectionMode, +) -> None: + """Test authenticated requests with strict connection.""" + assert hass.auth.session._strict_connection_sessions == {} + + signed_path = async_sign_path( + hass, "/", timedelta(seconds=5), refresh_token_id=refresh_token.id + ) + + await set_cloud_prefs( + { + PREF_STRICT_CONNECTION: strict_connection_mode, + } + ) + + with simulate_cloud_request(): + assert is_cloud_connection(hass) + req = await client.get( + "/", headers={"Authorization": f"Bearer {hass_access_token}"} + ) + assert req.status == HTTPStatus.OK + assert await req.json() == {"authenticated": True} + req = await client.get(signed_path) + assert req.status == HTTPStatus.OK + assert await req.json() == {"authenticated": True} + + +async def _test_strict_connection_cloud_enabled_external_unauthenticated_requests( + hass: HomeAssistant, + client: TestClient, + perform_unauthenticated_request: Callable[ + [HomeAssistant, TestClient], Awaitable[None] + ], + _: RefreshToken, +) -> None: + """Test external unauthenticated requests with strict connection cloud enabled.""" + with simulate_cloud_request(): + assert is_cloud_connection(hass) + await perform_unauthenticated_request(hass, client) + + +async def _test_strict_connection_cloud_enabled_external_unauthenticated_requests_refresh_token( + hass: HomeAssistant, + client: TestClient, + perform_unauthenticated_request: Callable[ + [HomeAssistant, TestClient], Awaitable[None] + ], + refresh_token: RefreshToken, +) -> None: + """Test external unauthenticated requests with strict connection cloud enabled and refresh token cookie.""" + session = hass.auth.session + + # set strict connection cookie with refresh token + session_id = await _modify_cookie_for_cloud(client, "refresh") + assert session._strict_connection_sessions == {session_id: refresh_token.id} + with simulate_cloud_request(): + assert is_cloud_connection(hass) + req = await client.get("/") + assert req.status == HTTPStatus.OK + assert await req.json() == {"authenticated": False} + + # Invalidate refresh token, which should also invalidate session + hass.auth.async_remove_refresh_token(refresh_token) + assert session._strict_connection_sessions == {} + + await perform_unauthenticated_request(hass, client) + + +async def _test_strict_connection_cloud_enabled_external_unauthenticated_requests_temp_session( + hass: HomeAssistant, + client: TestClient, + perform_unauthenticated_request: Callable[ + [HomeAssistant, TestClient], Awaitable[None] + ], + _: RefreshToken, +) -> None: + """Test external unauthenticated requests with strict connection cloud enabled and temp cookie.""" + session = hass.auth.session + + # set strict connection cookie with temp session + assert session._temp_sessions == {} + session_id = await _modify_cookie_for_cloud(client, "temp") + assert session_id in session._temp_sessions + with simulate_cloud_request(): + assert is_cloud_connection(hass) + resp = await client.get("/") + assert resp.status == HTTPStatus.OK + assert await resp.json() == {"authenticated": False} + + async_fire_time_changed(hass, utcnow() + TEMP_TIMEOUT + timedelta(minutes=1)) + await hass.async_block_till_done(wait_background_tasks=True) + assert session._temp_sessions == {} + + await perform_unauthenticated_request(hass, client) + + +async def _drop_connection_unauthorized_request( + _: HomeAssistant, client: TestClient +) -> None: + with pytest.raises(ServerDisconnectedError): + # unauthorized requests should raise ServerDisconnectedError + await client.get("/") + + +async def _static_page_unauthorized_request( + hass: HomeAssistant, client: TestClient +) -> None: + req = await client.get("/") + assert req.status == HTTPStatus.IM_A_TEAPOT + + def read_static_page() -> str: + with open(STRICT_CONNECTION_STATIC_PAGE, encoding="utf-8") as file: + return file.read() + + assert await req.text() == await hass.async_add_executor_job(read_static_page) + + +@pytest.mark.parametrize( + "test_func", + [ + _test_strict_connection_cloud_enabled_external_unauthenticated_requests, + _test_strict_connection_cloud_enabled_external_unauthenticated_requests_refresh_token, + _test_strict_connection_cloud_enabled_external_unauthenticated_requests_temp_session, + ], + ids=[ + "no cookie", + "refresh token cookie", + "temp session cookie", + ], +) +@pytest.mark.parametrize( + ("strict_connection_mode", "request_func"), + [ + (StrictConnectionMode.DROP_CONNECTION, _drop_connection_unauthorized_request), + (StrictConnectionMode.STATIC_PAGE, _static_page_unauthorized_request), + ], + ids=["drop connection", "static page"], +) +async def test_strict_connection_cloud_external_unauthenticated_requests( + hass: HomeAssistant, + client: TestClient, + refresh_token: RefreshToken, + set_cloud_prefs: Callable[[dict[str, Any]], Coroutine[Any, Any, None]], + test_func: Callable[ + [ + HomeAssistant, + TestClient, + Callable[[HomeAssistant, TestClient], Awaitable[None]], + RefreshToken, + ], + Awaitable[None], + ], + strict_connection_mode: StrictConnectionMode, + request_func: Callable[[HomeAssistant, TestClient], Awaitable[None]], +) -> None: + """Test external unauthenticated requests with strict connection cloud.""" + await set_cloud_prefs( + { + PREF_STRICT_CONNECTION: strict_connection_mode, + } + ) + + await test_func( + hass, + client, + request_func, + refresh_token, + ) + + +async def _modify_cookie_for_cloud(client: TestClient, token_type: str) -> str: + """Modify cookie for cloud.""" + # Cloud cookie has set secure=true and will not set on unsecure connection + # As we test with unsecure connection, we need to set it manually + # We get the session via http and modify the cookie name to the secure one + session_id = await (await client.get(f"/test/cookie?token={token_type}")).text() + cookie_jar = client.session.cookie_jar + localhost = URL("http://127.0.0.1") + cookie = cookie_jar.filter_cookies(localhost)[COOKIE_NAME].value + assert cookie + cookie_jar.clear() + cookie_jar.update_cookies({PREFIXED_COOKIE_NAME: cookie}, localhost) + return session_id diff --git a/tests/helpers/test_network.py b/tests/helpers/test_network.py index caffebf094e..3c9594bca38 100644 --- a/tests/helpers/test_network.py +++ b/tests/helpers/test_network.py @@ -362,6 +362,18 @@ async def test_get_url_external(hass: HomeAssistant) -> None: with pytest.raises(NoURLAvailableError): _get_external_url(hass, require_current_request=True, require_ssl=True) + with pytest.raises(NoURLAvailableError): + _get_external_url(hass, require_cloud=True) + + with patch( + "homeassistant.components.cloud.async_remote_ui_url", + return_value="https://example.nabu.casa", + ): + hass.config.components.add("cloud") + assert ( + _get_external_url(hass, require_cloud=True) == "https://example.nabu.casa" + ) + async def test_get_cloud_url(hass: HomeAssistant) -> None: """Test getting an instance URL when the user has set an external URL.""" From 6f2a2ba46e5220fb98df88b64a8ca7d445327b5e Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Wed, 24 Apr 2024 10:00:18 +0200 Subject: [PATCH 829/967] Bump plugwise to v0.37.3 (#116081) --- 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 1eb1cf6e8b6..ada7d2d2533 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.2"], + "requirements": ["plugwise==0.37.3"], "zeroconf": ["_plugwise._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 42f716f58e3..9469fa4f8f1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1548,7 +1548,7 @@ plexauth==0.0.6 plexwebsocket==0.0.14 # homeassistant.components.plugwise -plugwise==0.37.2 +plugwise==0.37.3 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 11533275029..7159b90ed09 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1225,7 +1225,7 @@ plexauth==0.0.6 plexwebsocket==0.0.14 # homeassistant.components.plugwise -plugwise==0.37.2 +plugwise==0.37.3 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 From 79b488981239b5f673ba12bc5d808c9c4c2973b9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 24 Apr 2024 10:05:52 +0200 Subject: [PATCH 830/967] Always do thread safety checks when writing state for custom components (#116044) --- homeassistant/helpers/entity.py | 25 +++++++++++++++++++++---- tests/helpers/test_entity.py | 26 ++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 40b145727a1..cf21882eec8 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -521,6 +521,7 @@ 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 @@ -967,8 +968,8 @@ class Entity( self._async_write_ha_state() @callback - def async_write_ha_state(self) -> None: - """Write the state to the state machine.""" + def _async_verify_state_writable(self) -> None: + """Verify the entity is in a writable state.""" if self.hass is None: raise RuntimeError(f"Attribute hass is None for {self}") if self.hass.config.debug: @@ -995,6 +996,18 @@ class Entity( f"No entity id specified for entity {self.name}" ) + @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() + 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 self._is_custom_component or self.hass.config.debug: + self.hass.verify_event_loop_thread("async_write_ha_state") self._async_write_ha_state() def _stringify_state(self, available: bool) -> str: @@ -1221,7 +1234,9 @@ class Entity( f"Entity {self.entity_id} schedule update ha state", ) else: - self.hass.loop.call_soon_threadsafe(self.async_write_ha_state) + self.hass.loop.call_soon_threadsafe( + self._async_write_ha_state_from_call_soon_threadsafe + ) @callback def async_schedule_update_ha_state(self, force_refresh: bool = False) -> None: @@ -1426,10 +1441,12 @@ class Entity( Not to be extended by integrations. """ + is_custom_component = "custom_components" in type(self).__module__ entity_info: EntityInfo = { "domain": self.platform.platform_name, - "custom_component": "custom_components" in type(self).__module__, + "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/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 349c065f9b5..a80674e0f76 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -2615,3 +2615,29 @@ async def test_async_write_ha_state_thread_safety(hass: HomeAssistant) -> None: ): await hass.async_add_executor_job(ent2.async_write_ha_state) assert not hass.states.get(ent2.entity_id) + + +async def test_async_write_ha_state_thread_safety_custom_component( + hass: HomeAssistant, +) -> None: + """Test async_write_ha_state thread safe for custom components.""" + + ent = entity.Entity() + ent._is_custom_component = True + ent.entity_id = "test.any" + ent.hass = hass + ent.platform = MockEntityPlatform(hass, domain="test") + ent.async_write_ha_state() + 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") + with pytest.raises( + RuntimeError, + match="Detected code that calls async_write_ha_state from a thread.", + ): + await hass.async_add_executor_job(ent2.async_write_ha_state) + assert not hass.states.get(ent2.entity_id) From c4340f6f5f76021752db41537ad52752feef4fba Mon Sep 17 00:00:00 2001 From: Gage Benne Date: Wed, 24 Apr 2024 04:16:35 -0400 Subject: [PATCH 831/967] Ecobee preset mode icon translations (#116072) --- homeassistant/components/ecobee/climate.py | 72 ++++++++++++---------- tests/components/ecobee/test_climate.py | 2 +- 2 files changed, 42 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index e341f4176ad..11675c0bf61 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -12,7 +12,10 @@ from homeassistant.components.climate import ( ATTR_TARGET_TEMP_LOW, FAN_AUTO, FAN_ON, + PRESET_AWAY, + PRESET_HOME, PRESET_NONE, + PRESET_SLEEP, ClimateEntity, ClimateEntityFeature, HVACAction, @@ -60,9 +63,6 @@ PRESET_TEMPERATURE = "temp" PRESET_VACATION = "vacation" PRESET_HOLD_NEXT_TRANSITION = "next_transition" PRESET_HOLD_INDEFINITE = "indefinite" -AWAY_MODE = "awayMode" -PRESET_HOME = "home" -PRESET_SLEEP = "sleep" HAS_HEAT_PUMP = "hasHeatPump" DEFAULT_MIN_HUMIDITY = 15 @@ -103,6 +103,13 @@ ECOBEE_HVAC_ACTION_TO_HASS = { "compWaterHeater": None, } +ECOBEE_TO_HASS_PRESET = { + "Away": PRESET_AWAY, + "Home": PRESET_HOME, + "Sleep": PRESET_SLEEP, +} +HASS_TO_ECOBEE_PRESET = {v: k for k, v in ECOBEE_TO_HASS_PRESET.items()} + PRESET_TO_ECOBEE_HOLD = { PRESET_HOLD_NEXT_TRANSITION: "nextTransition", PRESET_HOLD_INDEFINITE: "indefinite", @@ -348,10 +355,6 @@ class Thermostat(ClimateEntity): self._attr_hvac_modes.insert(0, HVACMode.HEAT_COOL) self._attr_hvac_modes.append(HVACMode.OFF) - self._preset_modes = { - comfort["climateRef"]: comfort["name"] - for comfort in self.thermostat["program"]["climates"] - } self.update_without_throttle = False async def async_update(self) -> None: @@ -474,7 +477,7 @@ class Thermostat(ClimateEntity): return self.thermostat["runtime"]["desiredFanMode"] @property - def preset_mode(self): + def preset_mode(self) -> str | None: """Return current preset mode.""" events = self.thermostat["events"] for event in events: @@ -487,8 +490,8 @@ class Thermostat(ClimateEntity): ): return PRESET_AWAY_INDEFINITELY - if event["holdClimateRef"] in self._preset_modes: - return self._preset_modes[event["holdClimateRef"]] + if name := self.comfort_settings.get(event["holdClimateRef"]): + return ECOBEE_TO_HASS_PRESET.get(name, name) # Any hold not based on a climate is a temp hold return PRESET_TEMPERATURE @@ -499,7 +502,12 @@ class Thermostat(ClimateEntity): self.vacation = event["name"] return PRESET_VACATION - return self._preset_modes[self.thermostat["program"]["currentClimateRef"]] + if name := self.comfort_settings.get( + self.thermostat["program"]["currentClimateRef"] + ): + return ECOBEE_TO_HASS_PRESET.get(name, name) + + return None @property def hvac_mode(self): @@ -545,14 +553,14 @@ class Thermostat(ClimateEntity): return HVACAction.IDLE @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return device specific state attributes.""" status = self.thermostat["equipmentStatus"] return { "fan": self.fan, - "climate_mode": self._preset_modes[ + "climate_mode": self.comfort_settings.get( self.thermostat["program"]["currentClimateRef"] - ], + ), "equipment_running": status, "fan_min_on_time": self.settings["fanMinOnTime"], } @@ -577,6 +585,8 @@ class Thermostat(ClimateEntity): def set_preset_mode(self, preset_mode: str) -> None: """Activate a preset.""" + preset_mode = HASS_TO_ECOBEE_PRESET.get(preset_mode, preset_mode) + if preset_mode == self.preset_mode: return @@ -605,25 +615,14 @@ class Thermostat(ClimateEntity): elif preset_mode == PRESET_NONE: self.data.ecobee.resume_program(self.thermostat_index) - elif preset_mode in self.preset_modes: - climate_ref = None - - for comfort in self.thermostat["program"]["climates"]: - if comfort["name"] == preset_mode: - climate_ref = comfort["climateRef"] + else: + for climate_ref, name in self.comfort_settings.items(): + if name == preset_mode: + preset_mode = climate_ref break - - if climate_ref is not None: - self.data.ecobee.set_climate_hold( - self.thermostat_index, - climate_ref, - self.hold_preference(), - self.hold_hours(), - ) else: _LOGGER.warning("Received unknown preset mode: %s", preset_mode) - else: self.data.ecobee.set_climate_hold( self.thermostat_index, preset_mode, @@ -632,11 +631,22 @@ class Thermostat(ClimateEntity): ) @property - def preset_modes(self): + def preset_modes(self) -> list[str] | None: """Return available preset modes.""" # Return presets provided by the ecobee API, and an indefinite away # preset which we handle separately in set_preset_mode(). - return [*self._preset_modes.values(), PRESET_AWAY_INDEFINITELY] + return [ + ECOBEE_TO_HASS_PRESET.get(name, name) + for name in self.comfort_settings.values() + ] + [PRESET_AWAY_INDEFINITELY] + + @property + def comfort_settings(self) -> dict[str, str]: + """Return ecobee API comfort settings.""" + return { + comfort["climateRef"]: comfort["name"] + for comfort in self.thermostat["program"]["climates"] + } def set_auto_temp_hold(self, heat_temp, cool_temp): """Set temperature hold in auto mode.""" diff --git a/tests/components/ecobee/test_climate.py b/tests/components/ecobee/test_climate.py index 7ea9950e2d4..46ca77025cc 100644 --- a/tests/components/ecobee/test_climate.py +++ b/tests/components/ecobee/test_climate.py @@ -441,7 +441,7 @@ async def test_preset_indefinite_away(ecobee_fixture, thermostat) -> None: """Test indefinite away showing correctly, and not as temporary away.""" ecobee_fixture["program"]["currentClimateRef"] = "away" ecobee_fixture["events"][0]["holdClimateRef"] = "away" - assert thermostat.preset_mode == "Away" + assert thermostat.preset_mode == "away" ecobee_fixture["events"][0]["endDate"] = "2999-01-01" assert thermostat.preset_mode == PRESET_AWAY_INDEFINITELY From 102b34123c2cf523126e4b92bc9d24536a831f76 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Wed, 24 Apr 2024 10:17:01 +0200 Subject: [PATCH 832/967] Bump zha-quirks to 0.0.115 (#116071) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 7741673557d..9b7788ff129 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -24,7 +24,7 @@ "bellows==0.38.1", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.114", + "zha-quirks==0.0.115", "zigpy-deconz==0.23.1", "zigpy==0.63.5", "zigpy-xbee==0.20.1", diff --git a/requirements_all.txt b/requirements_all.txt index 9469fa4f8f1..b8b11172a91 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2938,7 +2938,7 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha-quirks==0.0.114 +zha-quirks==0.0.115 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7159b90ed09..f5df78f7bcf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2285,7 +2285,7 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha-quirks==0.0.114 +zha-quirks==0.0.115 # homeassistant.components.zha zigpy-deconz==0.23.1 From 07d68eacfa77053c5be1211cec939792701cd0b5 Mon Sep 17 00:00:00 2001 From: Simon Roberts Date: Wed, 24 Apr 2024 18:24:49 +1000 Subject: [PATCH 833/967] Fix iotawatt warnings about "Detected new cycle for sensor.{sensorname}_wh_last" (#115909) * Bump ha-iotawattpy to 0.1.2 * Remove energy energy-over-period sensors: they cause issue for HA --------- Co-authored-by: Stefan Agner --- homeassistant/components/iotawatt/coordinator.py | 1 + homeassistant/components/iotawatt/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/iotawatt/coordinator.py b/homeassistant/components/iotawatt/coordinator.py index e741c7a5a27..4f9ac1f94b7 100644 --- a/homeassistant/components/iotawatt/coordinator.py +++ b/homeassistant/components/iotawatt/coordinator.py @@ -63,6 +63,7 @@ class IotawattUpdater(DataUpdateCoordinator): self.entry.data.get(CONF_USERNAME), self.entry.data.get(CONF_PASSWORD), integratedInterval="d", + includeNonTotalSensors=False, ) try: is_authenticated = await api.connect() diff --git a/homeassistant/components/iotawatt/manifest.json b/homeassistant/components/iotawatt/manifest.json index 5beaa1e318c..5fd178389d9 100644 --- a/homeassistant/components/iotawatt/manifest.json +++ b/homeassistant/components/iotawatt/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/iotawatt", "iot_class": "local_polling", "loggers": ["iotawattpy"], - "requirements": ["ha-iotawattpy==0.1.1"] + "requirements": ["ha-iotawattpy==0.1.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index b8b11172a91..5a7ce85328a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1026,7 +1026,7 @@ ha-av==10.1.1 ha-ffmpeg==3.2.0 # homeassistant.components.iotawatt -ha-iotawattpy==0.1.1 +ha-iotawattpy==0.1.2 # homeassistant.components.philips_js ha-philipsjs==3.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f5df78f7bcf..4831f441286 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -840,7 +840,7 @@ ha-av==10.1.1 ha-ffmpeg==3.2.0 # homeassistant.components.iotawatt -ha-iotawattpy==0.1.1 +ha-iotawattpy==0.1.2 # homeassistant.components.philips_js ha-philipsjs==3.1.1 From bcc2dd99b28b8094b3e123dd5bc56d949ee6fb2a Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 24 Apr 2024 10:29:59 +0200 Subject: [PATCH 834/967] Rename strict connection static page to guard page (#116085) --- homeassistant/components/http/auth.py | 40 +++++++++---------- homeassistant/components/http/const.py | 2 +- ...html => strict_connection_guard_page.html} | 0 tests/components/cloud/test_init.py | 2 +- .../cloud/test_strict_connection.py | 12 +++--- tests/components/http/test_auth.py | 12 +++--- tests/components/http/test_init.py | 2 +- 7 files changed, 35 insertions(+), 35 deletions(-) rename homeassistant/components/http/{strict_connection_static_page.html => strict_connection_guard_page.html} (100%) diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 889c9e76367..58dae21d2a6 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -52,9 +52,9 @@ STORAGE_VERSION = 1 STORAGE_KEY = "http.auth" CONTENT_USER_NAME = "Home Assistant Content" STRICT_CONNECTION_EXCLUDED_PATH = "/api/webhook/" -STRICT_CONNECTION_STATIC_PAGE_NAME = "strict_connection_static_page.html" -STRICT_CONNECTION_STATIC_PAGE = os.path.join( - os.path.dirname(__file__), STRICT_CONNECTION_STATIC_PAGE_NAME +STRICT_CONNECTION_GUARD_PAGE_NAME = "strict_connection_guard_page.html" +STRICT_CONNECTION_GUARD_PAGE = os.path.join( + os.path.dirname(__file__), STRICT_CONNECTION_GUARD_PAGE_NAME ) @@ -160,9 +160,9 @@ async def async_setup_auth( hass.data[STORAGE_KEY] = refresh_token.id - if strict_connection_mode_non_cloud is StrictConnectionMode.STATIC_PAGE: - # Load the static page content on setup - await _read_strict_connection_static_page(hass) + if strict_connection_mode_non_cloud is StrictConnectionMode.GUARD_PAGE: + # Load the guard page content on setup + await _read_strict_connection_guard_page(hass) @callback def async_validate_auth_header(request: Request) -> bool: @@ -276,7 +276,7 @@ async def async_setup_auth( resp := await strict_connection_func( hass, request, - strict_connection_mode is StrictConnectionMode.STATIC_PAGE, + strict_connection_mode is StrictConnectionMode.GUARD_PAGE, ) ) is not None @@ -301,14 +301,14 @@ async def async_setup_auth( async def _async_perform_strict_connection_action_on_non_local( hass: HomeAssistant, request: Request, - static_page: bool, + guard_page: bool, ) -> StreamResponse | None: """Perform strict connection mode action if the request is not local. The function does the following: - Try to get the IP address of the request. If it fails, assume it's not local - If the request is local, return None (allow the request to continue) - - If static_page is True, return a response with the content + - If guard_page is True, return a response with the content - Otherwise close the connection and raise an exception """ try: @@ -320,25 +320,25 @@ async def _async_perform_strict_connection_action_on_non_local( if ip_address_ and is_local(ip_address_): return None - return await _async_perform_strict_connection_action(hass, request, static_page) + return await _async_perform_strict_connection_action(hass, request, guard_page) async def _async_perform_strict_connection_action( hass: HomeAssistant, request: Request, - static_page: bool, + guard_page: bool, ) -> StreamResponse | None: """Perform strict connection mode action. The function does the following: - - If static_page is True, return a response with the content + - If guard_page is True, return a response with the content - Otherwise close the connection and raise an exception """ _LOGGER.debug("Perform strict connection action for %s", request.remote) - if static_page: + if guard_page: return Response( - text=await _read_strict_connection_static_page(hass), + text=await _read_strict_connection_guard_page(hass), content_type="text/html", status=HTTPStatus.IM_A_TEAPOT, ) @@ -351,12 +351,12 @@ async def _async_perform_strict_connection_action( raise HTTPBadRequest -@singleton.singleton(f"{DOMAIN}_{STRICT_CONNECTION_STATIC_PAGE_NAME}") -async def _read_strict_connection_static_page(hass: HomeAssistant) -> str: - """Read the strict connection static page from disk via executor.""" +@singleton.singleton(f"{DOMAIN}_{STRICT_CONNECTION_GUARD_PAGE_NAME}") +async def _read_strict_connection_guard_page(hass: HomeAssistant) -> str: + """Read the strict connection guard page from disk via executor.""" - def read_static_page() -> str: - with open(STRICT_CONNECTION_STATIC_PAGE, encoding="utf-8") as file: + def read_guard_page() -> str: + with open(STRICT_CONNECTION_GUARD_PAGE, encoding="utf-8") as file: return file.read() - return await hass.async_add_executor_job(read_static_page) + return await hass.async_add_executor_job(read_guard_page) diff --git a/homeassistant/components/http/const.py b/homeassistant/components/http/const.py index 662596288c7..4a15e310b11 100644 --- a/homeassistant/components/http/const.py +++ b/homeassistant/components/http/const.py @@ -15,5 +15,5 @@ class StrictConnectionMode(StrEnum): """Enum for strict connection mode.""" DISABLED = "disabled" - STATIC_PAGE = "static_page" + GUARD_PAGE = "guard_page" DROP_CONNECTION = "drop_connection" diff --git a/homeassistant/components/http/strict_connection_static_page.html b/homeassistant/components/http/strict_connection_guard_page.html similarity index 100% rename from homeassistant/components/http/strict_connection_static_page.html rename to homeassistant/components/http/strict_connection_guard_page.html diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index 98f9a54c04b..bc4526975da 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -327,7 +327,7 @@ async def test_service_create_temporary_strict_connection_url_strict_connection_ ("mode"), [ StrictConnectionMode.DROP_CONNECTION, - StrictConnectionMode.STATIC_PAGE, + StrictConnectionMode.GUARD_PAGE, ], ) async def test_service_create_temporary_strict_connection( diff --git a/tests/components/cloud/test_strict_connection.py b/tests/components/cloud/test_strict_connection.py index 844096ab0eb..f275bc4d2dd 100644 --- a/tests/components/cloud/test_strict_connection.py +++ b/tests/components/cloud/test_strict_connection.py @@ -18,7 +18,7 @@ from homeassistant.auth.session import SESSION_ID, TEMP_TIMEOUT from homeassistant.components.cloud.const import PREF_STRICT_CONNECTION from homeassistant.components.http import KEY_HASS from homeassistant.components.http.auth import ( - STRICT_CONNECTION_STATIC_PAGE, + STRICT_CONNECTION_GUARD_PAGE, async_setup_auth, async_sign_path, ) @@ -213,17 +213,17 @@ async def _drop_connection_unauthorized_request( await client.get("/") -async def _static_page_unauthorized_request( +async def _guard_page_unauthorized_request( hass: HomeAssistant, client: TestClient ) -> None: req = await client.get("/") assert req.status == HTTPStatus.IM_A_TEAPOT - def read_static_page() -> str: - with open(STRICT_CONNECTION_STATIC_PAGE, encoding="utf-8") as file: + def read_guard_page() -> str: + with open(STRICT_CONNECTION_GUARD_PAGE, encoding="utf-8") as file: return file.read() - assert await req.text() == await hass.async_add_executor_job(read_static_page) + assert await req.text() == await hass.async_add_executor_job(read_guard_page) @pytest.mark.parametrize( @@ -243,7 +243,7 @@ async def _static_page_unauthorized_request( ("strict_connection_mode", "request_func"), [ (StrictConnectionMode.DROP_CONNECTION, _drop_connection_unauthorized_request), - (StrictConnectionMode.STATIC_PAGE, _static_page_unauthorized_request), + (StrictConnectionMode.GUARD_PAGE, _guard_page_unauthorized_request), ], ids=["drop connection", "static page"], ) diff --git a/tests/components/http/test_auth.py b/tests/components/http/test_auth.py index f0f87e58173..afff8294f0c 100644 --- a/tests/components/http/test_auth.py +++ b/tests/components/http/test_auth.py @@ -30,7 +30,7 @@ from homeassistant.components.http.auth import ( DATA_SIGN_SECRET, SIGN_QUERY_PARAM, STORAGE_KEY, - STRICT_CONNECTION_STATIC_PAGE, + STRICT_CONNECTION_GUARD_PAGE, async_setup_auth, async_sign_path, async_user_not_allowed_do_auth, @@ -879,17 +879,17 @@ async def _drop_connection_unauthorized_request( await client.get("/") -async def _static_page_unauthorized_request( +async def _guard_page_unauthorized_request( hass: HomeAssistant, client: TestClient ) -> None: req = await client.get("/") assert req.status == HTTPStatus.IM_A_TEAPOT - def read_static_page() -> str: - with open(STRICT_CONNECTION_STATIC_PAGE, encoding="utf-8") as file: + def read_guard_page() -> str: + with open(STRICT_CONNECTION_GUARD_PAGE, encoding="utf-8") as file: return file.read() - assert await req.text() == await hass.async_add_executor_job(read_static_page) + assert await req.text() == await hass.async_add_executor_job(read_guard_page) @pytest.mark.parametrize( @@ -909,7 +909,7 @@ async def _static_page_unauthorized_request( ("strict_connection_mode", "request_func"), [ (StrictConnectionMode.DROP_CONNECTION, _drop_connection_unauthorized_request), - (StrictConnectionMode.STATIC_PAGE, _static_page_unauthorized_request), + (StrictConnectionMode.GUARD_PAGE, _guard_page_unauthorized_request), ], ids=["drop connection", "static page"], ) diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index b84da595ab1..b554737e7b3 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -548,7 +548,7 @@ async def test_service_create_temporary_strict_connection_url_strict_connection_ ("mode"), [ StrictConnectionMode.DROP_CONNECTION, - StrictConnectionMode.STATIC_PAGE, + StrictConnectionMode.GUARD_PAGE, ], ) async def test_service_create_temporary_strict_connection( From 5bded2a52dd0b816759248ecbac8a0e5535351d8 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 24 Apr 2024 10:30:57 +0200 Subject: [PATCH 835/967] Fix accuweather system_health after data change (#116063) --- .../components/accuweather/system_health.py | 2 +- tests/components/accuweather/test_system_health.py | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/accuweather/system_health.py b/homeassistant/components/accuweather/system_health.py index 607a557f333..f47828cb5a3 100644 --- a/homeassistant/components/accuweather/system_health.py +++ b/homeassistant/components/accuweather/system_health.py @@ -24,7 +24,7 @@ async def system_health_info(hass: HomeAssistant) -> dict[str, Any]: """Get info for the info page.""" remaining_requests = list(hass.data[DOMAIN].values())[ 0 - ].accuweather.requests_remaining + ].coordinator_observation.accuweather.requests_remaining return { "can_reach_server": system_health.async_check_can_reach_url(hass, ENDPOINT), diff --git a/tests/components/accuweather/test_system_health.py b/tests/components/accuweather/test_system_health.py index 6321071eaa5..562c572c830 100644 --- a/tests/components/accuweather/test_system_health.py +++ b/tests/components/accuweather/test_system_health.py @@ -5,6 +5,7 @@ from unittest.mock import Mock from aiohttp import ClientError +from homeassistant.components.accuweather import AccuWeatherData from homeassistant.components.accuweather.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -23,8 +24,10 @@ async def test_accuweather_system_health( await hass.async_block_till_done() hass.data[DOMAIN] = {} - hass.data[DOMAIN]["0123xyz"] = {} - hass.data[DOMAIN]["0123xyz"] = Mock(accuweather=Mock(requests_remaining="42")) + hass.data[DOMAIN]["0123xyz"] = AccuWeatherData( + coordinator_observation=Mock(accuweather=Mock(requests_remaining="42")), + coordinator_daily_forecast=Mock(), + ) info = await get_system_health_info(hass, DOMAIN) @@ -48,8 +51,10 @@ async def test_accuweather_system_health_fail( await hass.async_block_till_done() hass.data[DOMAIN] = {} - hass.data[DOMAIN]["0123xyz"] = {} - hass.data[DOMAIN]["0123xyz"] = Mock(accuweather=Mock(requests_remaining="0")) + hass.data[DOMAIN]["0123xyz"] = AccuWeatherData( + coordinator_observation=Mock(accuweather=Mock(requests_remaining="0")), + coordinator_daily_forecast=Mock(), + ) info = await get_system_health_info(hass, DOMAIN) From e0b58c3f450d774f2678748bbcdeb2982f77aa0c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 24 Apr 2024 10:41:11 +0200 Subject: [PATCH 836/967] Move thread safety check in async_register/async_remove (#116077) --- homeassistant/core.py | 44 +++++++++++++++++++++++++++++++++++++++---- tests/test_core.py | 23 ++++++++++++++++++++++ 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 189dc2f9d8a..a3150adc221 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -2456,7 +2456,7 @@ class ServiceRegistry: """ run_callback_threadsafe( self._hass.loop, - self.async_register, + self._async_register, domain, service, service_func, @@ -2484,6 +2484,33 @@ class ServiceRegistry: Schema is called to coerce and validate the service data. + This method must be run in the event loop. + """ + self._hass.verify_event_loop_thread("async_register") + self._async_register( + domain, service, service_func, schema, supports_response, job_type + ) + + @callback + def _async_register( + self, + domain: str, + service: str, + service_func: Callable[ + [ServiceCall], + Coroutine[Any, Any, ServiceResponse | EntityServiceResponse] + | ServiceResponse + | EntityServiceResponse + | None, + ], + schema: vol.Schema | None = None, + supports_response: SupportsResponse = SupportsResponse.NONE, + job_type: HassJobType | None = None, + ) -> None: + """Register a service. + + Schema is called to coerce and validate the service data. + This method must be run in the event loop. """ domain = domain.lower() @@ -2502,20 +2529,29 @@ class ServiceRegistry: else: self._services[domain] = {service: service_obj} - self._hass.bus.async_fire( + self._hass.bus.async_fire_internal( EVENT_SERVICE_REGISTERED, {ATTR_DOMAIN: domain, ATTR_SERVICE: service} ) def remove(self, domain: str, service: str) -> None: """Remove a registered service from service handler.""" run_callback_threadsafe( - self._hass.loop, self.async_remove, domain, service + self._hass.loop, self._async_remove, domain, service ).result() @callback def async_remove(self, domain: str, service: str) -> None: """Remove a registered service from service handler. + This method must be run in the event loop. + """ + self._hass.verify_event_loop_thread("async_remove") + self._async_remove(domain, service) + + @callback + def _async_remove(self, domain: str, service: str) -> None: + """Remove a registered service from service handler. + This method must be run in the event loop. """ domain = domain.lower() @@ -2530,7 +2566,7 @@ class ServiceRegistry: if not self._services[domain]: self._services.pop(domain) - self._hass.bus.async_fire( + self._hass.bus.async_fire_internal( EVENT_SERVICE_REMOVED, {ATTR_DOMAIN: domain, ATTR_SERVICE: service} ) diff --git a/tests/test_core.py b/tests/test_core.py index 6bab89bca85..a553d5bbbed 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -3457,3 +3457,26 @@ async def test_async_fire_thread_safety(hass: HomeAssistant) -> None: await hass.async_add_executor_job(hass.bus.async_fire, "test_event") assert len(events) == 1 + + +async def test_async_register_thread_safety(hass: HomeAssistant) -> None: + """Test async_register thread safety.""" + with pytest.raises( + RuntimeError, match="Detected code that calls async_register from a thread." + ): + await hass.async_add_executor_job( + hass.services.async_register, + "test_domain", + "test_service", + lambda call: None, + ) + + +async def test_async_remove_thread_safety(hass: HomeAssistant) -> None: + """Test async_remove thread safety.""" + with pytest.raises( + RuntimeError, match="Detected code that calls async_remove from a thread." + ): + await hass.async_add_executor_job( + hass.services.async_remove, "test_domain", "test_service" + ) From 1120246194affc07e7564fee03d83b18d2d4140d Mon Sep 17 00:00:00 2001 From: rlippmann <70883373+rlippmann@users.noreply.github.com> Date: Wed, 24 Apr 2024 05:13:07 -0400 Subject: [PATCH 837/967] Deprecate relative_time() in favor of time_since() and time_until() (#111177) * add time_since/time_until. add deprecation of relative_time * fix merge conflicts * Apply suggestions from code review * Update homeassistant/helpers/template.py * Update homeassistant/helpers/template.py * Update homeassistant/helpers/template.py --------- Co-authored-by: Erik Montnemery --- .../components/homeassistant/strings.json | 4 + homeassistant/helpers/template.py | 74 ++++ homeassistant/util/dt.py | 82 +++-- tests/helpers/test_template.py | 334 +++++++++++++++++- tests/util/test_dt.py | 67 ++++ 5 files changed, 539 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/homeassistant/strings.json b/homeassistant/components/homeassistant/strings.json index 09b2f17c947..5cdd47d8be4 100644 --- a/homeassistant/components/homeassistant/strings.json +++ b/homeassistant/components/homeassistant/strings.json @@ -56,6 +56,10 @@ "config_entry_reauth": { "title": "[%key:common::config_flow::title::reauth%]", "description": "Reauthentication is needed" + }, + "template_function_relative_time_deprecated": { + "title": "The {relative_time} template function is deprecated", + "description": "The {relative_time} template function is deprecated in Home Assistant. Please use the {time_since} or {time_until} template functions instead." } }, "system_health": { diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 24baab96a4e..335d6842548 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -59,6 +59,7 @@ from homeassistant.const import ( UnitOfLength, ) from homeassistant.core import ( + DOMAIN as HA_DOMAIN, Context, HomeAssistant, State, @@ -2480,6 +2481,29 @@ def relative_time(hass: HomeAssistant, value: Any) -> Any: If the input are not a datetime object the input will be returned unmodified. """ + + def warn_relative_time_deprecated() -> None: + ir = issue_registry.async_get(hass) + issue_id = "template_function_relative_time_deprecated" + if ir.async_get_issue(HA_DOMAIN, issue_id): + return + issue_registry.async_create_issue( + hass, + HA_DOMAIN, + issue_id, + breaks_in_ha_version="2024.11.0", + is_fixable=False, + severity=issue_registry.IssueSeverity.WARNING, + translation_key=issue_id, + translation_placeholders={ + "relative_time": "relative_time()", + "time_since": "time_since()", + "time_until": "time_until()", + }, + ) + _LOGGER.warning("Template function 'relative_time' is deprecated") + + warn_relative_time_deprecated() if (render_info := _render_info.get()) is not None: render_info.has_time = True @@ -2492,6 +2516,50 @@ def relative_time(hass: HomeAssistant, value: Any) -> Any: return dt_util.get_age(value) +def time_since(hass: HomeAssistant, value: Any | datetime, precision: int = 1) -> Any: + """Take a datetime and return its "age" as a string. + + The age can be in seconds, minutes, hours, days, months and year. + + precision is the number of units to return, with the last unit rounded. + + If the value not a datetime object the input will be returned unmodified. + """ + if (render_info := _render_info.get()) is not None: + render_info.has_time = True + + if not isinstance(value, datetime): + return value + if not value.tzinfo: + value = dt_util.as_local(value) + if dt_util.now() < value: + return value + + return dt_util.get_age(value, precision) + + +def time_until(hass: HomeAssistant, value: Any | datetime, precision: int = 1) -> Any: + """Take a datetime and return the amount of time until that time as a string. + + The time until can be in seconds, minutes, hours, days, months and years. + + precision is the number of units to return, with the last unit rounded. + + If the value not a datetime object the input will be returned unmodified. + """ + if (render_info := _render_info.get()) is not None: + render_info.has_time = True + + if not isinstance(value, datetime): + return value + if not value.tzinfo: + value = dt_util.as_local(value) + if dt_util.now() > value: + return value + + return dt_util.get_time_remaining(value, precision) + + def urlencode(value): """Urlencode dictionary and return as UTF-8 string.""" return urllib_urlencode(value).encode("utf-8") @@ -2890,6 +2958,8 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): "floor_id", "floor_name", "relative_time", + "time_since", + "time_until", "today_at", "label_id", "label_name", @@ -2946,6 +3016,10 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.globals["now"] = hassfunction(now) self.globals["relative_time"] = hassfunction(relative_time) self.filters["relative_time"] = self.globals["relative_time"] + self.globals["time_since"] = hassfunction(time_since) + self.filters["time_since"] = self.globals["time_since"] + self.globals["time_until"] = hassfunction(time_until) + self.filters["time_until"] = self.globals["time_until"] self.globals["today_at"] = hassfunction(today_at) self.filters["today_at"] = self.globals["today_at"] diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index 2f2b415144f..923838a48a5 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -286,36 +286,78 @@ def parse_time(time_str: str) -> dt.time | None: return None -def get_age(date: dt.datetime) -> str: - """Take a datetime and return its "age" as a string. - - The age can be in second, minute, hour, day, month or year. Only the - biggest unit is considered, e.g. if it's 2 days and 3 hours, "2 days" will - be returned. - Make sure date is not in the future, or else it won't work. - """ +def _get_timestring(timediff: float, precision: int = 1) -> str: + """Return a string representation of a time diff.""" def formatn(number: int, unit: str) -> str: """Add "unit" if it's plural.""" if number == 1: - return f"1 {unit}" - return f"{number:d} {unit}s" + return f"1 {unit} " + return f"{number:d} {unit}s " + + if timediff == 0.0: + return "0 seconds" + + units = ("year", "month", "day", "hour", "minute", "second") + + factors = (365 * 24 * 60 * 60, 30 * 24 * 60 * 60, 24 * 60 * 60, 60 * 60, 60, 1) + + result_string: str = "" + current_precision = 0 + + for i, current_factor in enumerate(factors): + selected_unit = units[i] + if timediff < current_factor: + continue + current_precision = current_precision + 1 + if current_precision == precision: + return ( + result_string + formatn(round(timediff / current_factor), selected_unit) + ).rstrip() + curr_diff = int(timediff // current_factor) + result_string += formatn(curr_diff, selected_unit) + timediff -= (curr_diff) * current_factor + + return result_string.rstrip() + + +def get_age(date: dt.datetime, precision: int = 1) -> str: + """Take a datetime and return its "age" as a string. + + The age can be in second, minute, hour, day, month and year. + + depth number of units will be returned, with the last unit rounded + + The date must be in the past or a ValueException will be raised. + """ delta = (now() - date).total_seconds() + rounded_delta = round(delta) - units = ["second", "minute", "hour", "day", "month"] - factors = [60, 60, 24, 30, 12] - selected_unit = "year" + if rounded_delta < 0: + raise ValueError("Time value is in the future") + return _get_timestring(rounded_delta, precision) - for i, next_factor in enumerate(factors): - if rounded_delta < next_factor: - selected_unit = units[i] - break - delta /= next_factor - rounded_delta = round(delta) - return formatn(rounded_delta, selected_unit) +def get_time_remaining(date: dt.datetime, precision: int = 1) -> str: + """Take a datetime and return its "age" as a string. + + The age can be in second, minute, hour, day, month and year. + + depth number of units will be returned, with the last unit rounded + + The date must be in the future or a ValueException will be raised. + """ + + delta = (date - now()).total_seconds() + + rounded_delta = round(delta) + + if rounded_delta < 0: + raise ValueError("Time value is in the past") + + return _get_timestring(rounded_delta, precision) def parse_time_expression(parameter: Any, min_value: int, max_value: int) -> list[int]: diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index f55a94d7283..d134570d119 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -31,7 +31,7 @@ from homeassistant.const import ( UnitOfTemperature, UnitOfVolume, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant from homeassistant.exceptions import TemplateError from homeassistant.helpers import ( area_registry as ar, @@ -2240,6 +2240,7 @@ def test_relative_time(mock_is_safe, hass: HomeAssistant) -> None: """Test relative_time method.""" hass.config.set_time_zone("UTC") now = datetime.strptime("2000-01-01 10:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z") + issue_registry = ir.async_get(hass) relative_time_template = ( '{{relative_time(strptime("2000-01-01 09:00:00", "%Y-%m-%d %H:%M:%S"))}}' ) @@ -2249,7 +2250,9 @@ def test_relative_time(mock_is_safe, hass: HomeAssistant) -> None: hass, ).async_render() assert result == "1 hour" - + assert issue_registry.async_get_issue( + HA_DOMAIN, "template_function_relative_time_deprecated" + ) result = template.Template( ( "{{" @@ -2308,6 +2311,333 @@ def test_relative_time(mock_is_safe, hass: HomeAssistant) -> None: assert info.has_time is True +@patch( + "homeassistant.helpers.template.TemplateEnvironment.is_safe_callable", + return_value=True, +) +def test_time_since(mock_is_safe, hass: HomeAssistant) -> None: + """Test time_since method.""" + hass.config.set_time_zone("UTC") + now = datetime.strptime("2000-01-01 10:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z") + time_since_template = ( + '{{time_since(strptime("2000-01-01 09:00:00", "%Y-%m-%d %H:%M:%S"))}}' + ) + with freeze_time(now): + result = template.Template( + time_since_template, + hass, + ).async_render() + assert result == "1 hour" + + result = template.Template( + ( + "{{" + " time_since(" + " strptime(" + ' "2000-01-01 09:00:00 +01:00",' + ' "%Y-%m-%d %H:%M:%S %z"' + " )" + " )" + "}}" + ), + hass, + ).async_render() + assert result == "2 hours" + + result = template.Template( + ( + "{{" + " time_since(" + " strptime(" + ' "2000-01-01 03:00:00 -06:00",' + ' "%Y-%m-%d %H:%M:%S %z"' + " )" + " )" + "}}" + ), + hass, + ).async_render() + assert result == "1 hour" + + result1 = str( + template.strptime("2000-01-01 11:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z") + ) + result2 = template.Template( + ( + "{{" + " time_since(" + " strptime(" + ' "2000-01-01 11:00:00 +00:00",' + ' "%Y-%m-%d %H:%M:%S %z"),' + " precision = 2" + " )" + "}}" + ), + hass, + ).async_render() + assert result1 == result2 + + result = template.Template( + ( + "{{" + " time_since(" + " strptime(" + ' "2000-01-01 09:05:00 +01:00",' + ' "%Y-%m-%d %H:%M:%S %z"),' + " precision=2" + " )" + "}}" + ), + hass, + ).async_render() + assert result == "1 hour 55 minutes" + + result = template.Template( + ( + "{{" + " time_since(" + " strptime(" + ' "2000-01-01 02:05:27 -06:00",' + ' "%Y-%m-%d %H:%M:%S %z"),' + " precision = 3" + " )" + "}}" + ), + hass, + ).async_render() + assert result == "1 hour 54 minutes 33 seconds" + result = template.Template( + ( + "{{" + " time_since(" + " strptime(" + ' "2000-01-01 02:05:27 -06:00",' + ' "%Y-%m-%d %H:%M:%S %z")' + " )" + "}}" + ), + hass, + ).async_render() + assert result == "2 hours" + result = template.Template( + ( + "{{" + " time_since(" + " strptime(" + ' "1999-02-01 02:05:27 -06:00",' + ' "%Y-%m-%d %H:%M:%S %z"),' + " precision = 0" + " )" + "}}" + ), + hass, + ).async_render() + assert result == "11 months 4 days 1 hour 54 minutes 33 seconds" + result = template.Template( + ( + "{{" + " time_since(" + " strptime(" + ' "1999-02-01 02:05:27 -06:00",' + ' "%Y-%m-%d %H:%M:%S %z")' + " )" + "}}" + ), + hass, + ).async_render() + assert result == "11 months" + result1 = str( + template.strptime("2000-01-01 11:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z") + ) + result2 = template.Template( + ( + "{{" + " time_since(" + " strptime(" + ' "2000-01-01 11:00:00 +00:00",' + ' "%Y-%m-%d %H:%M:%S %z"),' + " precision=3" + " )" + "}}" + ), + hass, + ).async_render() + assert result1 == result2 + + result = template.Template( + '{{time_since("string")}}', + hass, + ).async_render() + assert result == "string" + + info = template.Template(time_since_template, hass).async_render_to_info() + assert info.has_time is True + + +@patch( + "homeassistant.helpers.template.TemplateEnvironment.is_safe_callable", + return_value=True, +) +def test_time_until(mock_is_safe, hass: HomeAssistant) -> None: + """Test time_until method.""" + hass.config.set_time_zone("UTC") + now = datetime.strptime("2000-01-01 10:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z") + time_until_template = ( + '{{time_until(strptime("2000-01-01 11:00:00", "%Y-%m-%d %H:%M:%S"))}}' + ) + with freeze_time(now): + result = template.Template( + time_until_template, + hass, + ).async_render() + assert result == "1 hour" + + result = template.Template( + ( + "{{" + " time_until(" + " strptime(" + ' "2000-01-01 13:00:00 +01:00",' + ' "%Y-%m-%d %H:%M:%S %z"' + " )" + " )" + "}}" + ), + hass, + ).async_render() + assert result == "2 hours" + + result = template.Template( + ( + "{{" + " time_until(" + " strptime(" + ' "2000-01-01 05:00:00 -06:00",' + ' "%Y-%m-%d %H:%M:%S %z"' + " )" + " )" + "}}" + ), + hass, + ).async_render() + assert result == "1 hour" + + result1 = str( + template.strptime("2000-01-01 09:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z") + ) + result2 = template.Template( + ( + "{{" + " time_until(" + " strptime(" + ' "2000-01-01 09:00:00 +00:00",' + ' "%Y-%m-%d %H:%M:%S %z"),' + " precision = 2" + " )" + "}}" + ), + hass, + ).async_render() + assert result1 == result2 + + result = template.Template( + ( + "{{" + " time_until(" + " strptime(" + ' "2000-01-01 12:05:00 +01:00",' + ' "%Y-%m-%d %H:%M:%S %z"),' + " precision=2" + " )" + "}}" + ), + hass, + ).async_render() + assert result == "1 hour 5 minutes" + + result = template.Template( + ( + "{{" + " time_until(" + " strptime(" + ' "2000-01-01 05:54:33 -06:00",' + ' "%Y-%m-%d %H:%M:%S %z"),' + " precision = 3" + " )" + "}}" + ), + hass, + ).async_render() + assert result == "1 hour 54 minutes 33 seconds" + result = template.Template( + ( + "{{" + " time_until(" + " strptime(" + ' "2000-01-01 05:54:33 -06:00",' + ' "%Y-%m-%d %H:%M:%S %z")' + " )" + "}}" + ), + hass, + ).async_render() + assert result == "2 hours" + result = template.Template( + ( + "{{" + " time_until(" + " strptime(" + ' "2001-02-01 05:54:33 -06:00",' + ' "%Y-%m-%d %H:%M:%S %z"),' + " precision = 0" + " )" + "}}" + ), + hass, + ).async_render() + assert result == "1 year 1 month 2 days 1 hour 54 minutes 33 seconds" + result = template.Template( + ( + "{{" + " time_until(" + " strptime(" + ' "2001-02-01 05:54:33 -06:00",' + ' "%Y-%m-%d %H:%M:%S %z"),' + " precision = 4" + " )" + "}}" + ), + hass, + ).async_render() + assert result == "1 year 1 month 2 days 2 hours" + result1 = str( + template.strptime("2000-01-01 09:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z") + ) + result2 = template.Template( + ( + "{{" + " time_until(" + " strptime(" + ' "2000-01-01 09:00:00 +00:00",' + ' "%Y-%m-%d %H:%M:%S %z"),' + " precision=3" + " )" + "}}" + ), + hass, + ).async_render() + assert result1 == result2 + + result = template.Template( + '{{time_until("string")}}', + hass, + ).async_render() + assert result == "string" + + info = template.Template(time_until_template, hass).async_render_to_info() + assert info.has_time is True + + @patch( "homeassistant.helpers.template.TemplateEnvironment.is_safe_callable", return_value=True, diff --git a/tests/util/test_dt.py b/tests/util/test_dt.py index 7ed8154f033..215524c426b 100644 --- a/tests/util/test_dt.py +++ b/tests/util/test_dt.py @@ -178,12 +178,18 @@ def test_get_age() -> None: """Test get_age.""" diff = dt_util.now() - timedelta(seconds=0) assert dt_util.get_age(diff) == "0 seconds" + assert dt_util.get_age(diff, precision=2) == "0 seconds" diff = dt_util.now() - timedelta(seconds=1) assert dt_util.get_age(diff) == "1 second" + assert dt_util.get_age(diff, precision=2) == "1 second" + + diff = dt_util.now() + timedelta(seconds=1) + pytest.raises(ValueError, dt_util.get_age, diff) diff = dt_util.now() - timedelta(seconds=30) assert dt_util.get_age(diff) == "30 seconds" + diff = dt_util.now() + timedelta(seconds=30) diff = dt_util.now() - timedelta(minutes=5) assert dt_util.get_age(diff) == "5 minutes" @@ -196,20 +202,81 @@ def test_get_age() -> None: diff = dt_util.now() - timedelta(minutes=320) assert dt_util.get_age(diff) == "5 hours" + assert dt_util.get_age(diff, precision=2) == "5 hours 20 minutes" + assert dt_util.get_age(diff, precision=3) == "5 hours 20 minutes" diff = dt_util.now() - timedelta(minutes=1.6 * 60 * 24) assert dt_util.get_age(diff) == "2 days" + assert dt_util.get_age(diff, precision=2) == "1 day 14 hours" + assert dt_util.get_age(diff, precision=3) == "1 day 14 hours 24 minutes" + diff = dt_util.now() + timedelta(minutes=1.6 * 60 * 24) + pytest.raises(ValueError, dt_util.get_age, diff) diff = dt_util.now() - timedelta(minutes=2 * 60 * 24) assert dt_util.get_age(diff) == "2 days" diff = dt_util.now() - timedelta(minutes=32 * 60 * 24) assert dt_util.get_age(diff) == "1 month" + assert dt_util.get_age(diff, precision=10) == "1 month 2 days" + + diff = dt_util.now() - timedelta(minutes=32 * 60 * 24 + 1) + assert dt_util.get_age(diff, precision=3) == "1 month 2 days 1 minute" diff = dt_util.now() - timedelta(minutes=365 * 60 * 24) assert dt_util.get_age(diff) == "1 year" +def test_time_remaining() -> None: + """Test get_age.""" + diff = dt_util.now() + timedelta(seconds=0) + assert dt_util.get_time_remaining(diff) == "0 seconds" + assert dt_util.get_time_remaining(diff) == "0 seconds" + assert dt_util.get_time_remaining(diff, precision=2) == "0 seconds" + + diff = dt_util.now() + timedelta(seconds=1) + assert dt_util.get_time_remaining(diff) == "1 second" + + diff = dt_util.now() - timedelta(seconds=1) + pytest.raises(ValueError, dt_util.get_time_remaining, diff) + + diff = dt_util.now() + timedelta(seconds=30) + assert dt_util.get_time_remaining(diff) == "30 seconds" + + diff = dt_util.now() + timedelta(minutes=5) + assert dt_util.get_time_remaining(diff) == "5 minutes" + + diff = dt_util.now() + timedelta(minutes=1) + assert dt_util.get_time_remaining(diff) == "1 minute" + + diff = dt_util.now() + timedelta(minutes=300) + assert dt_util.get_time_remaining(diff) == "5 hours" + + diff = dt_util.now() + timedelta(minutes=320) + assert dt_util.get_time_remaining(diff) == "5 hours" + assert dt_util.get_time_remaining(diff, precision=2) == "5 hours 20 minutes" + assert dt_util.get_time_remaining(diff, precision=3) == "5 hours 20 minutes" + + diff = dt_util.now() + timedelta(minutes=1.6 * 60 * 24) + assert dt_util.get_time_remaining(diff) == "2 days" + assert dt_util.get_time_remaining(diff, precision=2) == "1 day 14 hours" + assert dt_util.get_time_remaining(diff, precision=3) == "1 day 14 hours 24 minutes" + diff = dt_util.now() - timedelta(minutes=1.6 * 60 * 24) + pytest.raises(ValueError, dt_util.get_time_remaining, diff) + + diff = dt_util.now() + timedelta(minutes=2 * 60 * 24) + assert dt_util.get_time_remaining(diff) == "2 days" + + diff = dt_util.now() + timedelta(minutes=32 * 60 * 24) + assert dt_util.get_time_remaining(diff) == "1 month" + assert dt_util.get_time_remaining(diff, precision=10) == "1 month 2 days" + + diff = dt_util.now() + timedelta(minutes=32 * 60 * 24 + 1) + assert dt_util.get_time_remaining(diff, precision=3) == "1 month 2 days 1 minute" + + diff = dt_util.now() + timedelta(minutes=365 * 60 * 24) + assert dt_util.get_time_remaining(diff) == "1 year" + + def test_parse_time_expression() -> None: """Test parse_time_expression.""" assert list(range(60)) == dt_util.parse_time_expression("*", 0, 59) From e9e401ae2929591ba4fceeec8b17f9f5302c9b5d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 24 Apr 2024 11:26:48 +0200 Subject: [PATCH 838/967] Migrate discovery debouncer callback to async_fire_internal (#116078) --- homeassistant/config_entries.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index bf576b517d3..0637e5f7c87 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1405,7 +1405,9 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager[ConfigFlowResult]): @callback def _async_discovery(self) -> None: """Handle discovery.""" - self.hass.bus.async_fire(EVENT_FLOW_DISCOVERED) + # async_fire_internal is used here because this is only + # called from the Debouncer so we know the usage is safe + self.hass.bus.async_fire_internal(EVENT_FLOW_DISCOVERED) persistent_notification.async_create( self.hass, title="New devices discovered", From e3984cd50ae4d13f5f8c51567cfa65a5b49b19f9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 24 Apr 2024 12:06:52 +0200 Subject: [PATCH 839/967] Temporary CI workaround for broken microsoft ubuntu repo (#116091) --- .github/workflows/ci.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5d38b0480b7..320c2e8d280 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -481,7 +481,7 @@ jobs: - name: Install additional OS dependencies if: steps.cache-venv.outputs.cache-hit != 'true' run: | - sudo apt-get update + sudo apt-get update || true sudo apt-get -y install \ bluez \ ffmpeg \ @@ -694,7 +694,7 @@ jobs: steps: - name: Install additional OS dependencies run: | - sudo apt-get update + sudo apt-get update || true sudo apt-get -y install \ bluez \ ffmpeg \ @@ -754,7 +754,7 @@ jobs: steps: - name: Install additional OS dependencies run: | - sudo apt-get update + sudo apt-get update || true sudo apt-get -y install \ bluez \ ffmpeg \ @@ -869,7 +869,7 @@ jobs: steps: - name: Install additional OS dependencies run: | - sudo apt-get update + sudo apt-get update || true sudo apt-get -y install \ bluez \ ffmpeg \ @@ -991,7 +991,7 @@ jobs: steps: - name: Install additional OS dependencies run: | - sudo apt-get update + sudo apt-get update || true sudo apt-get -y install \ bluez \ ffmpeg \ @@ -1132,7 +1132,7 @@ jobs: steps: - name: Install additional OS dependencies run: | - sudo apt-get update + sudo apt-get update || true sudo apt-get -y install \ bluez \ ffmpeg \ From df12789e0802adea8830f58d0c2586cfe88a3a8c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 24 Apr 2024 12:46:16 +0200 Subject: [PATCH 840/967] Remove duplicate async_write_ha_state thread safety check (#116086) --- homeassistant/helpers/entity.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index cf21882eec8..a91b4c32d21 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -972,8 +972,6 @@ class Entity( """Verify the entity is in a writable state.""" if self.hass is None: raise RuntimeError(f"Attribute hass is None for {self}") - if self.hass.config.debug: - self.hass.verify_event_loop_thread("async_write_ha_state") # The check for self.platform guards against integrations not using an # EntityComponent and can be removed in HA Core 2024.1 From d17e9bfc99756803bb4b816a65fad3ae07aed686 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 24 Apr 2024 12:55:09 +0200 Subject: [PATCH 841/967] Enable debug mode if asyncio debug is on at startup (#116084) --- homeassistant/bootstrap.py | 4 +++- tests/test_bootstrap.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 10ba0392f15..cbc808eb0fa 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -253,8 +253,9 @@ async def async_setup_hass( runtime_config.log_no_color, ) - if runtime_config.debug: + 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 @@ -318,6 +319,7 @@ async def async_setup_hass( hass = core.HomeAssistant(old_config.config_dir) if old_logging: hass.data[DATA_LOGGING] = old_logging + hass.config.debug = old_config.debug hass.config.skip_pip = old_config.skip_pip hass.config.skip_pip_packages = old_config.skip_pip_packages hass.config.internal_url = old_config.internal_url diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 6b96fb43d1f..2e35e4ffddb 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -122,6 +122,38 @@ async def test_config_does_not_turn_off_debug(hass: HomeAssistant) -> None: assert hass.config.debug is True +@pytest.mark.parametrize("hass_config", [{"frontend": {}}]) +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, + mock_ensure_config_exists: AsyncMock, + mock_process_ha_config_upgrade: Mock, +) -> None: + """Test that asyncio debug turns on hass debug.""" + asyncio.get_running_loop().set_debug(True) + + verbose = Mock() + log_rotate_days = Mock() + log_file = Mock() + log_no_color = Mock() + + hass = await bootstrap.async_setup_hass( + runner.RuntimeConfig( + config_dir=get_test_config_dir(), + verbose=verbose, + log_rotate_days=log_rotate_days, + log_file=log_file, + log_no_color=log_no_color, + skip_pip=True, + recovery_mode=False, + ), + ) + + assert hass.config.debug is True + + @pytest.mark.parametrize("load_registries", [False]) async def test_preload_translations(hass: HomeAssistant) -> None: """Test translations are preloaded for all frontend deps and base platforms.""" From 9fcb774252db4a0c7cd18276924a4f0d24570cb2 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 24 Apr 2024 13:06:14 +0200 Subject: [PATCH 842/967] Add reconfigure flow to AVM Fritz!SmartHome (#116047) --- .../components/fritzbox/config_flow.py | 41 ++++++++++ .../components/fritzbox/strings.json | 12 ++- tests/components/fritzbox/test_config_flow.py | 81 ++++++++++++++++++- 3 files changed, 132 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fritzbox/config_flow.py b/homeassistant/components/fritzbox/config_flow.py index c89415fa7ee..62f189b542f 100644 --- a/homeassistant/components/fritzbox/config_flow.py +++ b/homeassistant/components/fritzbox/config_flow.py @@ -221,3 +221,44 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN): description_placeholders={"name": self._name}, errors=errors, ) + + 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"]) + assert entry is not None + self._entry = entry + self._name = self._entry.data[CONF_HOST] + self._host = self._entry.data[CONF_HOST] + self._username = self._entry.data[CONF_USERNAME] + self._password = self._entry.data[CONF_PASSWORD] + + 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 user_input is not None: + self._host = user_input[CONF_HOST] + + result = await self.hass.async_add_executor_job(self._try_connect) + + if result == RESULT_SUCCESS: + await self._update_entry() + return self.async_abort(reason="reconfigure_successful") + errors["base"] = result + + return self.async_show_form( + step_id="reconfigure_confirm", + data_schema=vol.Schema( + { + vol.Required(CONF_HOST, default=self._host): str, + } + ), + description_placeholders={"name": self._name}, + errors=errors, + ) diff --git a/homeassistant/components/fritzbox/strings.json b/homeassistant/components/fritzbox/strings.json index f4d2fe3670e..755cc97d7d8 100644 --- a/homeassistant/components/fritzbox/strings.json +++ b/homeassistant/components/fritzbox/strings.json @@ -26,6 +26,15 @@ "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]" } + }, + "reconfigure_confirm": { + "description": "Update your configuration information for {name}.", + "data": { + "host": "[%key:common::config_flow::data::host%]" + }, + "data_description": { + "host": "The hostname or IP address of your FRITZ!Box router." + } } }, "abort": { @@ -34,7 +43,8 @@ "ignore_ip6_link_local": "IPv6 link local address is not supported.", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", "not_supported": "Connected to AVM FRITZ!Box but it's unable to control Smart Home devices.", - "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%]" }, "error": { "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" diff --git a/tests/components/fritzbox/test_config_flow.py b/tests/components/fritzbox/test_config_flow.py index 53a4f1c5205..72d36a8ab63 100644 --- a/tests/components/fritzbox/test_config_flow.py +++ b/tests/components/fritzbox/test_config_flow.py @@ -12,7 +12,12 @@ from requests.exceptions import HTTPError from homeassistant.components import ssdp from homeassistant.components.fritzbox.const import DOMAIN from homeassistant.components.ssdp import ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_UDN -from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_SSDP, SOURCE_USER +from homeassistant.config_entries import ( + SOURCE_REAUTH, + SOURCE_RECONFIGURE, + SOURCE_SSDP, + SOURCE_USER, +) from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -202,6 +207,80 @@ async def test_reauth_not_successful(hass: HomeAssistant, fritz: Mock) -> None: assert result["reason"] == "no_devices_found" +async def test_reconfigure_success(hass: HomeAssistant, fritz: Mock) -> None: + """Test starting a reconfigure flow.""" + mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + mock_config.add_to_hass(hass) + + assert mock_config.data[CONF_HOST] == "10.0.0.1" + assert mock_config.data[CONF_USERNAME] == "fake_user" + assert mock_config.data[CONF_PASSWORD] == "fake_pass" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_RECONFIGURE, "entry_id": mock_config.entry_id}, + data=mock_config.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: "new_host", + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert mock_config.data[CONF_HOST] == "new_host" + assert mock_config.data[CONF_USERNAME] == "fake_user" + assert mock_config.data[CONF_PASSWORD] == "fake_pass" + + +async def test_reconfigure_failed(hass: HomeAssistant, fritz: Mock) -> None: + """Test starting a reconfigure flow with failure.""" + fritz().login.side_effect = [OSError("Boom"), None] + + mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + mock_config.add_to_hass(hass) + + assert mock_config.data[CONF_HOST] == "10.0.0.1" + assert mock_config.data[CONF_USERNAME] == "fake_user" + assert mock_config.data[CONF_PASSWORD] == "fake_pass" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_RECONFIGURE, "entry_id": mock_config.entry_id}, + data=mock_config.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: "new_host", + }, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure_confirm" + assert result["errors"]["base"] == "no_devices_found" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "new_host", + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert mock_config.data[CONF_HOST] == "new_host" + assert mock_config.data[CONF_USERNAME] == "fake_user" + assert mock_config.data[CONF_PASSWORD] == "fake_pass" + + @pytest.mark.parametrize( ("test_data", "expected_result"), [ From a752f8e7d7b369fc756e5ac3bb7cf35f6c591bd1 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 24 Apr 2024 13:17:01 +0200 Subject: [PATCH 843/967] Remove microsoft apt package list before update (#116097) --- .github/workflows/ci.yaml | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 320c2e8d280..115c1a932ea 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -481,7 +481,8 @@ jobs: - name: Install additional OS dependencies if: steps.cache-venv.outputs.cache-hit != 'true' run: | - sudo apt-get update || true + sudo rm /etc/apt/sources.list.d/microsoft-prod.list + sudo apt-get update sudo apt-get -y install \ bluez \ ffmpeg \ @@ -694,7 +695,8 @@ jobs: steps: - name: Install additional OS dependencies run: | - sudo apt-get update || true + sudo rm /etc/apt/sources.list.d/microsoft-prod.list + sudo apt-get update sudo apt-get -y install \ bluez \ ffmpeg \ @@ -754,7 +756,8 @@ jobs: steps: - name: Install additional OS dependencies run: | - sudo apt-get update || true + sudo rm /etc/apt/sources.list.d/microsoft-prod.list + sudo apt-get update sudo apt-get -y install \ bluez \ ffmpeg \ @@ -869,7 +872,8 @@ jobs: steps: - name: Install additional OS dependencies run: | - sudo apt-get update || true + sudo rm /etc/apt/sources.list.d/microsoft-prod.list + sudo apt-get update sudo apt-get -y install \ bluez \ ffmpeg \ @@ -991,7 +995,8 @@ jobs: steps: - name: Install additional OS dependencies run: | - sudo apt-get update || true + sudo rm /etc/apt/sources.list.d/microsoft-prod.list + sudo apt-get update sudo apt-get -y install \ bluez \ ffmpeg \ @@ -1132,7 +1137,8 @@ jobs: steps: - name: Install additional OS dependencies run: | - sudo apt-get update || true + sudo rm /etc/apt/sources.list.d/microsoft-prod.list + sudo apt-get update sudo apt-get -y install \ bluez \ ffmpeg \ From bfed682abe50161f1326fd3b4b85912319a81da2 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 24 Apr 2024 13:18:09 +0200 Subject: [PATCH 844/967] =?UTF-8?q?Mark=20Tankerkoenig=20as=20Platinum=20?= =?UTF-8?q?=F0=9F=8F=86=20integration=20(#115917)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- homeassistant/components/tankerkoenig/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/tankerkoenig/manifest.json b/homeassistant/components/tankerkoenig/manifest.json index 4570d0e5649..c754094655d 100644 --- a/homeassistant/components/tankerkoenig/manifest.json +++ b/homeassistant/components/tankerkoenig/manifest.json @@ -6,5 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/tankerkoenig", "iot_class": "cloud_polling", "loggers": ["aiotankerkoenig"], + "quality_scale": "platinum", "requirements": ["aiotankerkoenig==0.4.1"] } From 5aa61cb6d593a4d4e4dfbe438ae2686726ad8acd Mon Sep 17 00:00:00 2001 From: Andy Date: Wed, 24 Apr 2024 13:19:50 +0200 Subject: [PATCH 845/967] Extend options for ecovacs lifespans (#116094) Co-authored-by: Robert Resch --- homeassistant/components/ecovacs/const.py | 2 + homeassistant/components/ecovacs/icons.json | 12 + homeassistant/components/ecovacs/strings.json | 12 + .../ecovacs/snapshots/test_button.ambr | 92 +++ .../ecovacs/snapshots/test_sensor.ambr | 630 +++++++++++++++++- tests/components/ecovacs/test_button.py | 22 +- tests/components/ecovacs/test_init.py | 2 +- tests/components/ecovacs/test_sensor.py | 28 + 8 files changed, 772 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/ecovacs/const.py b/homeassistant/components/ecovacs/const.py index e5ef0760182..6b77404e935 100644 --- a/homeassistant/components/ecovacs/const.py +++ b/homeassistant/components/ecovacs/const.py @@ -12,8 +12,10 @@ CONF_OVERRIDE_MQTT_URL = "override_mqtt_url" CONF_VERIFY_MQTT_CERTIFICATE = "verify_mqtt_certificate" SUPPORTED_LIFESPANS = ( + LifeSpan.BLADE, LifeSpan.BRUSH, LifeSpan.FILTER, + LifeSpan.LENS_BRUSH, LifeSpan.SIDE_BRUSH, ) diff --git a/homeassistant/components/ecovacs/icons.json b/homeassistant/components/ecovacs/icons.json index 2e2d897c455..44c577104dd 100644 --- a/homeassistant/components/ecovacs/icons.json +++ b/homeassistant/components/ecovacs/icons.json @@ -12,12 +12,18 @@ "relocate": { "default": "mdi:map-marker-question" }, + "reset_lifespan_blade": { + "default": "mdi:saw-blade" + }, "reset_lifespan_brush": { "default": "mdi:broom" }, "reset_lifespan_filter": { "default": "mdi:air-filter" }, + "reset_lifespan_lens_brush": { + "default": "mdi:broom" + }, "reset_lifespan_side_brush": { "default": "mdi:broom" } @@ -42,12 +48,18 @@ "error": { "default": "mdi:alert-circle" }, + "lifespan_blade": { + "default": "mdi:saw-blade" + }, "lifespan_brush": { "default": "mdi:broom" }, "lifespan_filter": { "default": "mdi:air-filter" }, + "lifespan_lens_brush": { + "default": "mdi:broom" + }, "lifespan_side_brush": { "default": "mdi:broom" }, diff --git a/homeassistant/components/ecovacs/strings.json b/homeassistant/components/ecovacs/strings.json index 50afd21deb3..bb27bd6941d 100644 --- a/homeassistant/components/ecovacs/strings.json +++ b/homeassistant/components/ecovacs/strings.json @@ -46,12 +46,18 @@ "relocate": { "name": "Relocate" }, + "reset_lifespan_blade": { + "name": "Reset blade lifespan" + }, "reset_lifespan_brush": { "name": "Reset main brush lifespan" }, "reset_lifespan_filter": { "name": "Reset filter lifespan" }, + "reset_lifespan_lens_brush": { + "name": "Reset lens brush lifespan" + }, "reset_lifespan_side_brush": { "name": "Reset side brushes lifespan" } @@ -92,12 +98,18 @@ } } }, + "lifespan_blade": { + "name": "Blade lifespan" + }, "lifespan_brush": { "name": "Main brush lifespan" }, "lifespan_filter": { "name": "Filter lifespan" }, + "lifespan_lens_brush": { + "name": "Lens brush lifespan" + }, "lifespan_side_brush": { "name": "Side brushes lifespan" }, diff --git a/tests/components/ecovacs/snapshots/test_button.ambr b/tests/components/ecovacs/snapshots/test_button.ambr index 816551f7e6a..d250a60a35f 100644 --- a/tests/components/ecovacs/snapshots/test_button.ambr +++ b/tests/components/ecovacs/snapshots/test_button.ambr @@ -1,4 +1,96 @@ # serializer version: 1 +# name: test_buttons[5xu9h3][button.goat_g1_reset_blade_lifespan:entity-registry] + 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.goat_g1_reset_blade_lifespan', + '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': 'Reset blade lifespan', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'reset_lifespan_blade', + 'unique_id': '8516fbb1-17f1-4194-0000000_reset_lifespan_blade', + 'unit_of_measurement': None, + }) +# --- +# name: test_buttons[5xu9h3][button.goat_g1_reset_blade_lifespan:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Goat G1 Reset blade lifespan', + }), + 'context': , + 'entity_id': 'button.goat_g1_reset_blade_lifespan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-01-01T00:00:00+00:00', + }) +# --- +# name: test_buttons[5xu9h3][button.goat_g1_reset_lens_brush_lifespan:entity-registry] + 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.goat_g1_reset_lens_brush_lifespan', + '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': 'Reset lens brush lifespan', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'reset_lifespan_lens_brush', + 'unique_id': '8516fbb1-17f1-4194-0000000_reset_lifespan_lens_brush', + 'unit_of_measurement': None, + }) +# --- +# name: test_buttons[5xu9h3][button.goat_g1_reset_lens_brush_lifespan:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Goat G1 Reset lens brush lifespan', + }), + 'context': , + 'entity_id': 'button.goat_g1_reset_lens_brush_lifespan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-01-01T00:00:00+00:00', + }) +# --- # name: test_buttons[yna5x1][button.ozmo_950_relocate:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/ecovacs/snapshots/test_sensor.ambr b/tests/components/ecovacs/snapshots/test_sensor.ambr index b35310158f2..e2cee3d410f 100644 --- a/tests/components/ecovacs/snapshots/test_sensor.ambr +++ b/tests/components/ecovacs/snapshots/test_sensor.ambr @@ -1,5 +1,583 @@ # serializer version: 1 -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_area_cleaned:entity-registry] +# name: test_sensors[5xu9h3][sensor.goat_g1_area_cleaned:entity-registry] + 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.goat_g1_area_cleaned', + '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': 'Area cleaned', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'stats_area', + 'unique_id': '8516fbb1-17f1-4194-0000000_stats_area', + 'unit_of_measurement': 'm²', + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_area_cleaned:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Goat G1 Area cleaned', + 'unit_of_measurement': 'm²', + }), + 'context': , + 'entity_id': 'sensor.goat_g1_area_cleaned', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10', + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_battery:entity-registry] + 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.goat_g1_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': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '8516fbb1-17f1-4194-0000000_battery_level', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_battery:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Goat G1 Battery', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.goat_g1_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_blade_lifespan:entity-registry] + 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.goat_g1_blade_lifespan', + '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': 'Blade lifespan', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifespan_blade', + 'unique_id': '8516fbb1-17f1-4194-0000000_lifespan_blade', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_blade_lifespan:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Goat G1 Blade lifespan', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.goat_g1_blade_lifespan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_cleaning_duration:entity-registry] + 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.goat_g1_cleaning_duration', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Cleaning duration', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'stats_time', + 'unique_id': '8516fbb1-17f1-4194-0000000_stats_time', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_cleaning_duration:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Goat G1 Cleaning duration', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.goat_g1_cleaning_duration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.0', + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_error:entity-registry] + 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.goat_g1_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': 'Error', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'error', + 'unique_id': '8516fbb1-17f1-4194-0000000_error', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_error:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'description': 'NoError: Robot is operational', + 'friendly_name': 'Goat G1 Error', + }), + 'context': , + 'entity_id': 'sensor.goat_g1_error', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_ip_address:entity-registry] + 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.goat_g1_ip_address', + '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': 'IP address', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'network_ip', + 'unique_id': '8516fbb1-17f1-4194-0000000_network_ip', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_ip_address:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Goat G1 IP address', + }), + 'context': , + 'entity_id': 'sensor.goat_g1_ip_address', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '192.168.0.10', + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_lens_brush_lifespan:entity-registry] + 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.goat_g1_lens_brush_lifespan', + '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': 'Lens brush lifespan', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifespan_lens_brush', + 'unique_id': '8516fbb1-17f1-4194-0000000_lifespan_lens_brush', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_lens_brush_lifespan:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Goat G1 Lens brush lifespan', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.goat_g1_lens_brush_lifespan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_total_area_cleaned:entity-registry] + 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.goat_g1_total_area_cleaned', + '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 area cleaned', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'total_stats_area', + 'unique_id': '8516fbb1-17f1-4194-0000000_total_stats_area', + 'unit_of_measurement': 'm²', + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_total_area_cleaned:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Goat G1 Total area cleaned', + 'state_class': , + 'unit_of_measurement': 'm²', + }), + 'context': , + 'entity_id': 'sensor.goat_g1_total_area_cleaned', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '60', + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_total_cleaning_duration:entity-registry] + 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.goat_g1_total_cleaning_duration', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total cleaning duration', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'total_stats_time', + 'unique_id': '8516fbb1-17f1-4194-0000000_total_stats_time', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_total_cleaning_duration:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Goat G1 Total cleaning duration', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.goat_g1_total_cleaning_duration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '40.000', + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_total_cleanings:entity-registry] + 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.goat_g1_total_cleanings', + '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 cleanings', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'total_stats_cleanings', + 'unique_id': '8516fbb1-17f1-4194-0000000_total_stats_cleanings', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_total_cleanings:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Goat G1 Total cleanings', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.goat_g1_total_cleanings', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '123', + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_wi_fi_rssi:entity-registry] + 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.goat_g1_wi_fi_rssi', + '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': 'Wi-Fi RSSI', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'network_rssi', + 'unique_id': '8516fbb1-17f1-4194-0000000_network_rssi', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_wi_fi_rssi:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Goat G1 Wi-Fi RSSI', + }), + 'context': , + 'entity_id': 'sensor.goat_g1_wi_fi_rssi', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-62', + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_wi_fi_ssid:entity-registry] + 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.goat_g1_wi_fi_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': 'Wi-Fi SSID', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'network_ssid', + 'unique_id': '8516fbb1-17f1-4194-0000000_network_ssid', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[5xu9h3][sensor.goat_g1_wi_fi_ssid:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Goat G1 Wi-Fi SSID', + }), + 'context': , + 'entity_id': 'sensor.goat_g1_wi_fi_ssid', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Testnetwork', + }) +# --- +# name: test_sensors[yna5x1][sensor.ozmo_950_area_cleaned:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -32,7 +610,7 @@ 'unit_of_measurement': 'm²', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_area_cleaned:state] +# name: test_sensors[yna5x1][sensor.ozmo_950_area_cleaned:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Ozmo 950 Area cleaned', @@ -46,7 +624,7 @@ 'state': '10', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_battery:entity-registry] +# name: test_sensors[yna5x1][sensor.ozmo_950_battery:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -79,7 +657,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_battery:state] +# name: test_sensors[yna5x1][sensor.ozmo_950_battery:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', @@ -94,7 +672,7 @@ 'state': '100', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_cleaning_duration:entity-registry] +# name: test_sensors[yna5x1][sensor.ozmo_950_cleaning_duration:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -130,7 +708,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_cleaning_duration:state] +# name: test_sensors[yna5x1][sensor.ozmo_950_cleaning_duration:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -145,7 +723,7 @@ 'state': '5.0', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_error:entity-registry] +# name: test_sensors[yna5x1][sensor.ozmo_950_error:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -178,7 +756,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_error:state] +# name: test_sensors[yna5x1][sensor.ozmo_950_error:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'description': 'NoError: Robot is operational', @@ -192,7 +770,7 @@ 'state': '0', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_filter_lifespan:entity-registry] +# name: test_sensors[yna5x1][sensor.ozmo_950_filter_lifespan:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -225,7 +803,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_filter_lifespan:state] +# name: test_sensors[yna5x1][sensor.ozmo_950_filter_lifespan:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Ozmo 950 Filter lifespan', @@ -239,7 +817,7 @@ 'state': '56', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_ip_address:entity-registry] +# name: test_sensors[yna5x1][sensor.ozmo_950_ip_address:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -272,7 +850,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_ip_address:state] +# name: test_sensors[yna5x1][sensor.ozmo_950_ip_address:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Ozmo 950 IP address', @@ -285,7 +863,7 @@ 'state': '192.168.0.10', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_main_brush_lifespan:entity-registry] +# name: test_sensors[yna5x1][sensor.ozmo_950_main_brush_lifespan:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -318,7 +896,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_main_brush_lifespan:state] +# name: test_sensors[yna5x1][sensor.ozmo_950_main_brush_lifespan:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Ozmo 950 Main brush lifespan', @@ -332,7 +910,7 @@ 'state': '80', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_side_brushes_lifespan:entity-registry] +# name: test_sensors[yna5x1][sensor.ozmo_950_side_brushes_lifespan:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -365,7 +943,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_side_brushes_lifespan:state] +# name: test_sensors[yna5x1][sensor.ozmo_950_side_brushes_lifespan:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Ozmo 950 Side brushes lifespan', @@ -379,7 +957,7 @@ 'state': '40', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_total_area_cleaned:entity-registry] +# name: test_sensors[yna5x1][sensor.ozmo_950_total_area_cleaned:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -414,7 +992,7 @@ 'unit_of_measurement': 'm²', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_total_area_cleaned:state] +# name: test_sensors[yna5x1][sensor.ozmo_950_total_area_cleaned:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Ozmo 950 Total area cleaned', @@ -429,7 +1007,7 @@ 'state': '60', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_total_cleaning_duration:entity-registry] +# name: test_sensors[yna5x1][sensor.ozmo_950_total_cleaning_duration:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -467,7 +1045,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_total_cleaning_duration:state] +# name: test_sensors[yna5x1][sensor.ozmo_950_total_cleaning_duration:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -483,7 +1061,7 @@ 'state': '40.000', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_total_cleanings:entity-registry] +# name: test_sensors[yna5x1][sensor.ozmo_950_total_cleanings:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -518,7 +1096,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_total_cleanings:state] +# name: test_sensors[yna5x1][sensor.ozmo_950_total_cleanings:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Ozmo 950 Total cleanings', @@ -532,7 +1110,7 @@ 'state': '123', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_wi_fi_rssi:entity-registry] +# name: test_sensors[yna5x1][sensor.ozmo_950_wi_fi_rssi:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -565,7 +1143,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_wi_fi_rssi:state] +# name: test_sensors[yna5x1][sensor.ozmo_950_wi_fi_rssi:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Ozmo 950 Wi-Fi RSSI', @@ -578,7 +1156,7 @@ 'state': '-62', }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_wi_fi_ssid:entity-registry] +# name: test_sensors[yna5x1][sensor.ozmo_950_wi_fi_ssid:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -611,7 +1189,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_sensors[yna5x1-entity_ids0][sensor.ozmo_950_wi_fi_ssid:state] +# name: test_sensors[yna5x1][sensor.ozmo_950_wi_fi_ssid:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Ozmo 950 Wi-Fi SSID', diff --git a/tests/components/ecovacs/test_button.py b/tests/components/ecovacs/test_button.py index 8e583e6342b..277983eb0c5 100644 --- a/tests/components/ecovacs/test_button.py +++ b/tests/components/ecovacs/test_button.py @@ -48,8 +48,21 @@ def platforms() -> Platform | list[Platform]: ), ], ), + ( + "5xu9h3", + [ + ( + "button.goat_g1_reset_blade_lifespan", + ResetLifeSpan(LifeSpan.BLADE), + ), + ( + "button.goat_g1_reset_lens_brush_lifespan", + ResetLifeSpan(LifeSpan.LENS_BRUSH), + ), + ], + ), ], - ids=["yna5x1"], + ids=["yna5x1", "5xu9h3"], ) async def test_buttons( hass: HomeAssistant, @@ -98,6 +111,13 @@ async def test_buttons( "button.ozmo_950_reset_side_brushes_lifespan", ], ), + ( + "5xu9h3", + [ + "button.goat_g1_reset_blade_lifespan", + "button.goat_g1_reset_lens_brush_lifespan", + ], + ), ], ) async def test_disabled_by_default_buttons( diff --git a/tests/components/ecovacs/test_init.py b/tests/components/ecovacs/test_init.py index 7780b86d714..c27da2196b1 100644 --- a/tests/components/ecovacs/test_init.py +++ b/tests/components/ecovacs/test_init.py @@ -122,7 +122,7 @@ async def test_devices_in_dr( ("device_fixture", "entities"), [ ("yna5x1", 26), - ("5xu9h3", 20), + ("5xu9h3", 24), ], ) async def test_all_entities_loaded( diff --git a/tests/components/ecovacs/test_sensor.py b/tests/components/ecovacs/test_sensor.py index 7ff4ab3f009..5b8bf18e1d8 100644 --- a/tests/components/ecovacs/test_sensor.py +++ b/tests/components/ecovacs/test_sensor.py @@ -69,7 +69,25 @@ async def notify_events(hass: HomeAssistant, event_bus: EventBus): "sensor.ozmo_950_error", ], ), + ( + "5xu9h3", + [ + "sensor.goat_g1_area_cleaned", + "sensor.goat_g1_cleaning_duration", + "sensor.goat_g1_total_area_cleaned", + "sensor.goat_g1_total_cleaning_duration", + "sensor.goat_g1_total_cleanings", + "sensor.goat_g1_battery", + "sensor.goat_g1_ip_address", + "sensor.goat_g1_wi_fi_rssi", + "sensor.goat_g1_wi_fi_ssid", + "sensor.goat_g1_blade_lifespan", + "sensor.goat_g1_lens_brush_lifespan", + "sensor.goat_g1_error", + ], + ), ], + ids=["yna5x1", "5xu9h3"], ) async def test_sensors( hass: HomeAssistant, @@ -111,7 +129,17 @@ async def test_sensors( "sensor.ozmo_950_wi_fi_ssid", ], ), + ( + "5xu9h3", + [ + "sensor.goat_g1_error", + "sensor.goat_g1_ip_address", + "sensor.goat_g1_wi_fi_rssi", + "sensor.goat_g1_wi_fi_ssid", + ], + ), ], + ids=["yna5x1", "5xu9h3"], ) async def test_disabled_by_default_sensors( hass: HomeAssistant, entity_registry: er.EntityRegistry, entity_ids: list[str] From 18132916fad71d1c317dc37be68b4c4641b0905e Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 24 Apr 2024 13:29:42 +0200 Subject: [PATCH 846/967] Mask current password in MQTT option flow (#116098) * Mask current password in MQTT option flow * Update docstr * Typo --- homeassistant/components/mqtt/config_flow.py | 49 ++++++++++++++------ tests/components/mqtt/test_config_flow.py | 6 +-- 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 8168b997fa6..1a7dfbbc507 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -167,6 +167,29 @@ REAUTH_SCHEMA = vol.Schema( PWD_NOT_CHANGED = "__**password_not_changed**__" +@callback +def update_password_from_user_input( + entry_password: str | None, user_input: dict[str, Any] +) -> dict[str, Any]: + """Update the password if the entry has been updated. + + As we want to avoid reflecting the stored password in the UI, + we replace the suggested value in the UI with a sentitel, + and we change it back here if it was changed. + """ + substituted_used_data = dict(user_input) + # Take out the password submitted + user_password: str | None = substituted_used_data.pop(CONF_PASSWORD, None) + # Only add the password if it has changed. + # If the sentinel password is submitted, we replace that with our current + # password from the config entry data. + password_changed = user_password is not None and user_password != PWD_NOT_CHANGED + password = user_password if password_changed else entry_password + if password is not None: + substituted_used_data[CONF_PASSWORD] = password + return substituted_used_data + + class FlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow.""" @@ -209,16 +232,10 @@ class FlowHandler(ConfigFlow, domain=DOMAIN): assert self.entry is not None if user_input: - password_changed = ( - user_password := user_input[CONF_PASSWORD] - ) != PWD_NOT_CHANGED - entry_password = self.entry.data.get(CONF_PASSWORD) - password = user_password if password_changed else entry_password - new_entry_data = { - **self.entry.data, - CONF_USERNAME: user_input.get(CONF_USERNAME), - CONF_PASSWORD: password, - } + substituted_used_data = update_password_from_user_input( + self.entry.data.get(CONF_PASSWORD), user_input + ) + new_entry_data = {**self.entry.data, **substituted_used_data} if await self.hass.async_add_executor_job( try_connection, new_entry_data, @@ -350,13 +367,17 @@ class MQTTOptionsFlowHandler(OptionsFlow): validated_user_input, errors, ): + self.broker_config.update( + update_password_from_user_input( + self.config_entry.data.get(CONF_PASSWORD), validated_user_input + ), + ) can_connect = await self.hass.async_add_executor_job( try_connection, - validated_user_input, + self.broker_config, ) if can_connect: - self.broker_config.update(validated_user_input) return await self.async_step_options() errors["base"] = "cannot_connect" @@ -657,7 +678,9 @@ async def async_get_broker_settings( current_broker = current_config.get(CONF_BROKER) current_port = current_config.get(CONF_PORT, DEFAULT_PORT) current_user = current_config.get(CONF_USERNAME) - current_pass = current_config.get(CONF_PASSWORD) + # Return the sentinel password to avoid exposure + current_entry_pass = current_config.get(CONF_PASSWORD) + current_pass = PWD_NOT_CHANGED if current_entry_pass else None # Treat the previous post as an update of the current settings # (if there was a basic broker setup step) diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 56d19506a66..422ec84c091 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -902,7 +902,7 @@ async def test_option_flow_default_suggested_values( } suggested = { mqtt.CONF_USERNAME: "user", - mqtt.CONF_PASSWORD: "pass", + mqtt.CONF_PASSWORD: PWD_NOT_CHANGED, } for key, value in defaults.items(): assert get_default(result["data_schema"].schema, key) == value @@ -964,7 +964,7 @@ async def test_option_flow_default_suggested_values( } suggested = { mqtt.CONF_USERNAME: "us3r", - mqtt.CONF_PASSWORD: "p4ss", + mqtt.CONF_PASSWORD: PWD_NOT_CHANGED, } for key, value in defaults.items(): assert get_default(result["data_schema"].schema, key) == value @@ -1329,7 +1329,7 @@ async def test_try_connection_with_advanced_parameters( } suggested = { mqtt.CONF_USERNAME: "user", - mqtt.CONF_PASSWORD: "pass", + mqtt.CONF_PASSWORD: PWD_NOT_CHANGED, mqtt.CONF_TLS_INSECURE: True, mqtt.CONF_PROTOCOL: "3.1.1", mqtt.CONF_TRANSPORT: "websockets", From 0e0b543dec2c5be7ae3f2dcfbec4e5485c1c3875 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Wed, 24 Apr 2024 21:30:22 +1000 Subject: [PATCH 847/967] Deprecate speed limit lock in Tessie (#113848) --- homeassistant/components/tessie/lock.py | 65 +++++++++++++++++++- homeassistant/components/tessie/strings.json | 35 +++++++++++ tests/components/tessie/test_lock.py | 55 ++++++++++++++--- 3 files changed, 145 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/tessie/lock.py b/homeassistant/components/tessie/lock.py index 09402055ee8..1e5653744fb 100644 --- a/homeassistant/components/tessie/lock.py +++ b/homeassistant/components/tessie/lock.py @@ -12,10 +12,14 @@ from tessie_api import ( unlock, ) +from homeassistant.components.automation import automations_with_entity from homeassistant.components.lock import ATTR_CODE, LockEntity +from homeassistant.components.script import scripts_with_entity from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers import entity_registry as er, issue_registry as ir from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN, TessieChargeCableLockStates @@ -29,11 +33,46 @@ async def async_setup_entry( """Set up the Tessie sensor platform from a config entry.""" data = hass.data[DOMAIN][entry.entry_id] - async_add_entities( + entities = [ klass(vehicle.state_coordinator) - for klass in (TessieLockEntity, TessieCableLockEntity, TessieSpeedLimitEntity) + for klass in (TessieLockEntity, TessieCableLockEntity) for vehicle in data - ) + ] + + ent_reg = er.async_get(hass) + + for vehicle in data: + entity_id = ent_reg.async_get_entity_id( + Platform.LOCK, + DOMAIN, + f"{vehicle.state_coordinator.vin}-vehicle_state_speed_limit_mode_active", + ) + if entity_id: + entity_entry = ent_reg.async_get(entity_id) + assert entity_entry + if entity_entry.disabled: + ent_reg.async_remove(entity_id) + else: + entities.append(TessieSpeedLimitEntity(vehicle.state_coordinator)) + + entity_automations = automations_with_entity(hass, entity_id) + entity_scripts = scripts_with_entity(hass, entity_id) + for item in entity_automations + entity_scripts: + ir.async_create_issue( + hass, + DOMAIN, + f"deprecated_speed_limit_{entity_id}_{item}", + breaks_in_ha_version="2024.11.0", + is_fixable=True, + is_persistent=False, + severity=ir.IssueSeverity.WARNING, + translation_key="deprecated_speed_limit_entity", + translation_placeholders={ + "entity": entity_id, + "info": item, + }, + ) + async_add_entities(entities) class TessieLockEntity(TessieEntity, LockEntity): @@ -81,6 +120,16 @@ class TessieSpeedLimitEntity(TessieEntity, LockEntity): async def async_lock(self, **kwargs: Any) -> None: """Enable speed limit with pin.""" + ir.async_create_issue( + self.coordinator.hass, + DOMAIN, + "deprecated_speed_limit_locked", + breaks_in_ha_version="2024.11.0", + is_fixable=True, + is_persistent=False, + severity=ir.IssueSeverity.WARNING, + translation_key="deprecated_speed_limit_locked", + ) code: str | None = kwargs.get(ATTR_CODE) if code: await self.run(enable_speed_limit, pin=code) @@ -88,6 +137,16 @@ class TessieSpeedLimitEntity(TessieEntity, LockEntity): async def async_unlock(self, **kwargs: Any) -> None: """Disable speed limit with pin.""" + ir.async_create_issue( + self.coordinator.hass, + DOMAIN, + "deprecated_speed_limit_unlocked", + breaks_in_ha_version="2024.11.0", + is_fixable=True, + is_persistent=False, + severity=ir.IssueSeverity.WARNING, + translation_key="deprecated_speed_limit_unlocked", + ) code: str | None = kwargs.get(ATTR_CODE) if code: await self.run(disable_speed_limit, pin=code) diff --git a/homeassistant/components/tessie/strings.json b/homeassistant/components/tessie/strings.json index 8e1e47f934f..ea75660ddb7 100644 --- a/homeassistant/components/tessie/strings.json +++ b/homeassistant/components/tessie/strings.json @@ -410,5 +410,40 @@ "no_cable": { "message": "Insert cable to lock" } + }, + "issues": { + "deprecated_speed_limit_entity": { + "title": "Detected Tessie speed limit lock entity usage", + "fix_flow": { + "step": { + "confirm": { + "title": "[%key:component::tessie::issues::deprecated_speed_limit_entity::title%]", + "description": "The Tessie integration's speed limit lock entity has been deprecated and will be remove in 2024.11.0.\nHome Assistant detected that entity `{entity}` is being used in `{info}`\n\nYou should remove the speed limit lock entity from `{info}` then click submit to fix this issue." + } + } + } + }, + "deprecated_speed_limit_locked": { + "title": "Detected Tessie speed limit lock entity locked", + "fix_flow": { + "step": { + "confirm": { + "title": "[%key:component::tessie::issues::deprecated_speed_limit_locked::title%]", + "description": "The Tessie integration's speed limit lock entity has been deprecated and will be remove in 2024.11.0.\n\nPlease remove this entity from any automation or script, disable the entity then click submit to fix this issue." + } + } + } + }, + "deprecated_speed_limit_unlocked": { + "title": "Detected Tessie speed limit lock entity unlocked", + "fix_flow": { + "step": { + "confirm": { + "title": "[%key:component::tessie::issues::deprecated_speed_limit_unlocked::title%]", + "description": "The Tessie integration's speed limit lock entity has been deprecated and will be remove in 2024.11.0.\n\nPlease remove this entity from any automation or script, disable the entity then click submit to fix this issue." + } + } + } + } } } diff --git a/tests/components/tessie/test_lock.py b/tests/components/tessie/test_lock.py index ca921583d97..0371b592f07 100644 --- a/tests/components/tessie/test_lock.py +++ b/tests/components/tessie/test_lock.py @@ -15,8 +15,9 @@ from homeassistant.const import ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED, Pl from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.issue_registry import async_get as async_get_issue_registry -from .common import assert_entities, setup_platform +from .common import DOMAIN, assert_entities, setup_platform async def test_locks( @@ -24,6 +25,17 @@ async def test_locks( ) -> None: """Tests that the lock entity is correct.""" + # Create the deprecated speed limit lock entity + entity_registry.async_get_or_create( + LOCK_DOMAIN, + DOMAIN, + "VINVINVIN-vehicle_state_speed_limit_mode_active", + original_name="Charge cable lock", + has_entity_name=True, + translation_key="vehicle_state_speed_limit_mode_active", + disabled_by=er.RegistryEntryDisabler.INTEGRATION, + ) + entry = await setup_platform(hass, [Platform.LOCK]) assert_entities(hass, entry.entry_id, entity_registry, snapshot) @@ -72,19 +84,47 @@ async def test_locks( assert hass.states.get(entity_id).state == STATE_UNLOCKED mock_run.assert_called_once() + +async def test_speed_limit_lock( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: + """Tests that the deprecated speed limit lock entity is correct.""" + + issue_registry = async_get_issue_registry(hass) + + # Create the deprecated speed limit lock entity + entity = entity_registry.async_get_or_create( + LOCK_DOMAIN, + DOMAIN, + "VINVINVIN-vehicle_state_speed_limit_mode_active", + original_name="Charge cable lock", + has_entity_name=True, + translation_key="vehicle_state_speed_limit_mode_active", + ) + + with patch( + "homeassistant.components.tessie.lock.automations_with_entity", + return_value=["item"], + ): + await setup_platform(hass, [Platform.LOCK]) + assert issue_registry.async_get_issue( + DOMAIN, f"deprecated_speed_limit_{entity.entity_id}_item" + ) + # Test lock set value functions - entity_id = "lock.test_speed_limit" with patch( "homeassistant.components.tessie.lock.enable_speed_limit" ) as mock_enable_speed_limit: await hass.services.async_call( LOCK_DOMAIN, SERVICE_LOCK, - {ATTR_ENTITY_ID: [entity_id], ATTR_CODE: "1234"}, + {ATTR_ENTITY_ID: [entity.entity_id], ATTR_CODE: "1234"}, blocking=True, ) - assert hass.states.get(entity_id).state == STATE_LOCKED + assert hass.states.get(entity.entity_id).state == STATE_LOCKED mock_enable_speed_limit.assert_called_once() + # Assert issue has been raised in the issue register + assert issue_registry.async_get_issue(DOMAIN, "deprecated_speed_limit_locked") with patch( "homeassistant.components.tessie.lock.disable_speed_limit" @@ -92,16 +132,17 @@ async def test_locks( await hass.services.async_call( LOCK_DOMAIN, SERVICE_UNLOCK, - {ATTR_ENTITY_ID: [entity_id], ATTR_CODE: "1234"}, + {ATTR_ENTITY_ID: [entity.entity_id], ATTR_CODE: "1234"}, blocking=True, ) - assert hass.states.get(entity_id).state == STATE_UNLOCKED + assert hass.states.get(entity.entity_id).state == STATE_UNLOCKED mock_disable_speed_limit.assert_called_once() + assert issue_registry.async_get_issue(DOMAIN, "deprecated_speed_limit_unlocked") with pytest.raises(ServiceValidationError): await hass.services.async_call( LOCK_DOMAIN, SERVICE_UNLOCK, - {ATTR_ENTITY_ID: [entity_id], ATTR_CODE: "abc"}, + {ATTR_ENTITY_ID: [entity.entity_id], ATTR_CODE: "abc"}, blocking=True, ) From 24a1f0712fdc93150f39104c8b4b286495286f2b Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Wed, 24 Apr 2024 08:03:40 -0400 Subject: [PATCH 848/967] Fix Sonos music library play problems (#113429) --- .../components/sonos/media_browser.py | 50 +++++- .../components/sonos/media_player.py | 31 +++- tests/components/sonos/conftest.py | 106 +++++++++++++ tests/components/sonos/test_media_player.py | 146 +++++++++++++++--- 4 files changed, 302 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/sonos/media_browser.py b/homeassistant/components/sonos/media_browser.py index b6fc250ab23..eeadd7db232 100644 --- a/homeassistant/components/sonos/media_browser.py +++ b/homeassistant/components/sonos/media_browser.py @@ -199,9 +199,15 @@ def build_item_response( payload["search_type"] == MediaType.ALBUM and media[0].item_class == "object.item.audioItem.musicTrack" ): - item = get_media(media_library, payload["idstring"], SONOS_ALBUM_ARTIST) + idstring = payload["idstring"] + if idstring.startswith("A:ALBUMARTIST/"): + search_type = SONOS_ALBUM_ARTIST + elif idstring.startswith("A:ALBUM/"): + search_type = SONOS_ALBUM + item = get_media(media_library, idstring, search_type) + title = getattr(item, "title", None) - thumbnail = get_thumbnail_url(SONOS_ALBUM_ARTIST, payload["idstring"]) + thumbnail = get_thumbnail_url(search_type, payload["idstring"]) if not title: try: @@ -493,8 +499,9 @@ def get_content_id(item: DidlObject) -> str: def get_media( media_library: MusicLibrary, item_id: str, search_type: str -) -> MusicServiceItem: - """Fetch media/album.""" +) -> MusicServiceItem | None: + """Fetch a single media/album.""" + _LOGGER.debug("get_media item_id [%s], search_type [%s]", item_id, search_type) search_type = MEDIA_TYPES_TO_SONOS.get(search_type, search_type) if search_type == "playlists": @@ -513,9 +520,38 @@ def get_media( if not item_id.startswith("A:ALBUM") and search_type == SONOS_ALBUM: item_id = "A:ALBUMARTIST/" + "/".join(item_id.split("/")[2:]) - search_term = urllib.parse.unquote(item_id.split("/")[-1]) - matches = media_library.get_music_library_information( - search_type, search_term=search_term, full_album_art_uri=True + if item_id.startswith("A:ALBUM/") or search_type == "tracks": + search_term = urllib.parse.unquote(item_id.split("/")[-1]) + matches = media_library.get_music_library_information( + search_type, search_term=search_term, full_album_art_uri=True + ) + else: + # When requesting media by album_artist, composer, genre use the browse interface + # to navigate the hierarchy. This occurs when invoked from media browser or service + # calls + # Example: A:ALBUMARTIST/Neil Young/Greatest Hits - get specific album + # Example: A:ALBUMARTIST/Neil Young - get all albums + # Others: composer, genre + # A:// + splits = item_id.split("/") + title = urllib.parse.unquote(splits[2]) if len(splits) > 2 else None + browse_id_string = splits[0] + "/" + splits[1] + matches = media_library.browse_by_idstring( + search_type, browse_id_string, full_album_art_uri=True + ) + if title: + result = next( + (item for item in matches if (title == item.title)), + None, + ) + matches = [result] + + _LOGGER.debug( + "get_media search_type [%s] item_id [%s] matches [%d]", + search_type, + item_id, + len(matches), ) if len(matches) > 0: return matches[0] + return None diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 581bdaad37d..35c6be3fa6b 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -7,7 +7,7 @@ from functools import partial import logging from typing import Any -from soco import alarms +from soco import SoCo, alarms from soco.core import ( MUSIC_SRC_LINE_IN, MUSIC_SRC_RADIO, @@ -15,6 +15,7 @@ from soco.core import ( PLAY_MODES, ) from soco.data_structures import DidlFavorite +from soco.ms_data_structures import MusicServiceItem from sonos_websocket.exception import SonosWebsocketError import voluptuous as vol @@ -549,6 +550,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): self, media_type: MediaType | str, media_id: str, is_radio: bool, **kwargs: Any ) -> None: """Wrap sync calls to async_play_media.""" + _LOGGER.debug("_play_media media_type %s media_id %s", media_type, media_id) enqueue = kwargs.get(ATTR_MEDIA_ENQUEUE, MediaPlayerEnqueue.REPLACE) if media_type == "favorite_item_id": @@ -645,10 +647,35 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): _LOGGER.error('Could not find "%s" in the library', media_id) return - soco.play_uri(item.get_uri()) + self._play_media_queue(soco, item, enqueue) else: _LOGGER.error('Sonos does not support a media type of "%s"', media_type) + def _play_media_queue( + self, soco: SoCo, item: MusicServiceItem, enqueue: MediaPlayerEnqueue + ): + """Manage adding, replacing, playing items onto the sonos queue.""" + _LOGGER.debug( + "_play_media_queue item_id [%s] title [%s] enqueue [%s]", + item.item_id, + item.title, + enqueue, + ) + if enqueue == MediaPlayerEnqueue.REPLACE: + soco.clear_queue() + + if enqueue in (MediaPlayerEnqueue.ADD, MediaPlayerEnqueue.REPLACE): + soco.add_to_queue(item, timeout=LONG_SERVICE_TIMEOUT) + if enqueue == MediaPlayerEnqueue.REPLACE: + soco.play_from_queue(0) + else: + pos = (self.media.queue_position or 0) + 1 + new_pos = soco.add_to_queue( + item, position=pos, timeout=LONG_SERVICE_TIMEOUT + ) + if enqueue == MediaPlayerEnqueue.PLAY: + soco.play_from_queue(new_pos - 1) + @soco_error() def set_sleep_timer(self, sleep_time: int) -> None: """Set the timer on the player.""" diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 218ca90a26b..0eb9b497fbd 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -203,6 +203,7 @@ class SoCoMockFactory: my_speaker_info["zone_name"] = name 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.avTransport = SonosMockService("AVTransport", ip_address) mock_soco.renderingControl = SonosMockService("RenderingControl", ip_address) @@ -303,11 +304,116 @@ def config_fixture(): return {DOMAIN: {MP_DOMAIN: {CONF_HOSTS: ["192.168.42.2"]}}} +class MockMusicServiceItem: + """Mocks a Soco MusicServiceItem.""" + + def __init__(self, title: str, item_id: str, parent_id: str, item_class: str): + """Initialize the mock item.""" + self.title = title + self.item_id = item_id + self.item_class = item_class + self.parent_id = parent_id + + +def mock_browse_by_idstring( + search_type: str, idstring: str, start=0, max_items=100, full_album_art_uri=False +) -> list[MockMusicServiceItem]: + """Mock the call to browse_by_id_string.""" + if search_type == "album_artists" and idstring == "A:ALBUMARTIST/Beatles": + return [ + MockMusicServiceItem( + "All", + idstring + "/", + idstring, + "object.container.playlistContainer.sameArtist", + ), + MockMusicServiceItem( + "A Hard Day's Night", + "A:ALBUMARTIST/Beatles/A%20Hard%20Day's%20Night", + idstring, + "object.container.album.musicAlbum", + ), + MockMusicServiceItem( + "Abbey Road", + "A:ALBUMARTIST/Beatles/Abbey%20Road", + idstring, + "object.container.album.musicAlbum", + ), + ] + # browse_by_id_string works with URL encoded or decoded strings + if search_type == "genres" and idstring in ( + "A:GENRE/Classic%20Rock", + "A:GENRE/Classic Rock", + ): + return [ + MockMusicServiceItem( + "All", + "A:GENRE/Classic%20Rock/", + "A:GENRE/Classic%20Rock", + "object.container.albumlist", + ), + MockMusicServiceItem( + "Bruce Springsteen", + "A:GENRE/Classic%20Rock/Bruce%20Springsteen", + "A:GENRE/Classic%20Rock", + "object.container.person.musicArtist", + ), + MockMusicServiceItem( + "Cream", + "A:GENRE/Classic%20Rock/Cream", + "A:GENRE/Classic%20Rock", + "object.container.person.musicArtist", + ), + ] + if search_type == "composers" and idstring in ( + "A:COMPOSER/Carlos%20Santana", + "A:COMPOSER/Carlos Santana", + ): + return [ + MockMusicServiceItem( + "All", + "A:COMPOSER/Carlos%20Santana/", + "A:COMPOSER/Carlos%20Santana", + "object.container.playlistContainer.sameArtist", + ), + MockMusicServiceItem( + "Between Good And Evil", + "A:COMPOSER/Carlos%20Santana/Between%20Good%20And%20Evil", + "A:COMPOSER/Carlos%20Santana", + "object.container.album.musicAlbum", + ), + MockMusicServiceItem( + "Sacred Fire", + "A:COMPOSER/Carlos%20Santana/Sacred%20Fire", + "A:COMPOSER/Carlos%20Santana", + "object.container.album.musicAlbum", + ), + ] + return [] + + +def mock_get_music_library_information( + search_type: str, search_term: str, full_album_art_uri: bool = True +) -> list[MockMusicServiceItem]: + """Mock the call to get music library information.""" + if search_type == "albums" and search_term == "Abbey Road": + return [ + MockMusicServiceItem( + "Abbey Road", + "A:ALBUM/Abbey%20Road", + "A:ALBUM", + "object.container.album.musicAlbum", + ) + ] + + @pytest.fixture(name="music_library") def music_library_fixture(): """Create music_library fixture.""" music_library = MagicMock() music_library.get_sonos_favorites.return_value.update_id = 1 + music_library.browse_by_idstring = mock_browse_by_idstring + music_library.get_music_library_information = mock_get_music_library_information return music_library diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index c181520b85d..976d3480429 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -7,7 +7,10 @@ import pytest from homeassistant.components.media_player import ( DOMAIN as MP_DOMAIN, SERVICE_PLAY_MEDIA, + MediaPlayerEnqueue, ) +from homeassistant.components.media_player.const import ATTR_MEDIA_ENQUEUE +from homeassistant.components.sonos.media_player import LONG_SERVICE_TIMEOUT from homeassistant.const import STATE_IDLE from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import ( @@ -16,7 +19,7 @@ from homeassistant.helpers.device_registry import ( DeviceRegistry, ) -from .conftest import SoCoMockFactory +from .conftest import MockMusicServiceItem, SoCoMockFactory async def test_device_registry( @@ -65,35 +68,134 @@ async def test_entity_basic( assert attributes["volume_level"] == 0.19 -class _MockMusicServiceItem: - """Mocks a Soco MusicServiceItem.""" - - def __init__( - self, - title: str, - item_id: str, - parent_id: str, - item_class: str, - ) -> None: - """Initialize the mock item.""" - self.title = title - self.item_id = item_id - self.item_class = item_class - self.parent_id = parent_id - - def get_uri(self) -> str: - """Return URI.""" - return self.item_id.replace("S://", "x-file-cifs://") +@pytest.mark.parametrize( + ("media_content_type", "media_content_id", "enqueue", "test_result"), + [ + ( + "artist", + "A:ALBUMARTIST/Beatles", + MediaPlayerEnqueue.REPLACE, + { + "title": "All", + "item_id": "A:ALBUMARTIST/Beatles/", + "clear_queue": 1, + "position": None, + "play": 1, + "play_pos": 0, + }, + ), + ( + "genre", + "A:GENRE/Classic%20Rock", + MediaPlayerEnqueue.ADD, + { + "title": "All", + "item_id": "A:GENRE/Classic%20Rock/", + "clear_queue": 0, + "position": None, + "play": 0, + "play_pos": 0, + }, + ), + ( + "album", + "A:ALBUM/Abbey%20Road", + MediaPlayerEnqueue.NEXT, + { + "title": "Abbey Road", + "item_id": "A:ALBUM/Abbey%20Road", + "clear_queue": 0, + "position": 1, + "play": 0, + "play_pos": 0, + }, + ), + ( + "composer", + "A:COMPOSER/Carlos%20Santana", + MediaPlayerEnqueue.PLAY, + { + "title": "All", + "item_id": "A:COMPOSER/Carlos%20Santana/", + "clear_queue": 0, + "position": 1, + "play": 1, + "play_pos": 9, + }, + ), + ( + "artist", + "A:ALBUMARTIST/Beatles/Abbey%20Road", + MediaPlayerEnqueue.REPLACE, + { + "title": "Abbey Road", + "item_id": "A:ALBUMARTIST/Beatles/Abbey%20Road", + "clear_queue": 1, + "position": None, + "play": 1, + "play_pos": 0, + }, + ), + ], +) +async def test_play_media_library( + hass: HomeAssistant, + soco_factory: SoCoMockFactory, + async_autosetup_sonos, + media_content_type, + media_content_id, + enqueue, + test_result, +) -> None: + """Test playing local library with a variety of options.""" + sock_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": media_content_type, + "media_content_id": media_content_id, + ATTR_MEDIA_ENQUEUE: enqueue, + }, + blocking=True, + ) + assert sock_mock.clear_queue.call_count == test_result["clear_queue"] + assert sock_mock.add_to_queue.call_count == 1 + assert ( + sock_mock.add_to_queue.call_args_list[0].args[0].title == test_result["title"] + ) + assert ( + sock_mock.add_to_queue.call_args_list[0].args[0].item_id + == test_result["item_id"] + ) + if test_result["position"] is not None: + assert ( + sock_mock.add_to_queue.call_args_list[0].kwargs["position"] + == test_result["position"] + ) + else: + assert "position" not in sock_mock.add_to_queue.call_args_list[0].kwargs + assert ( + sock_mock.add_to_queue.call_args_list[0].kwargs["timeout"] + == LONG_SERVICE_TIMEOUT + ) + assert sock_mock.play_from_queue.call_count == test_result["play"] + if test_result["play"] != 0: + assert ( + sock_mock.play_from_queue.call_args_list[0].args[0] + == test_result["play_pos"] + ) _mock_playlists = [ - _MockMusicServiceItem( + MockMusicServiceItem( "playlist1", "S://192.168.1.68/music/iTunes/iTunes%20Music%20Library.xml#GUID_1", "A:PLAYLISTS", "object.container.playlistContainer", ), - _MockMusicServiceItem( + MockMusicServiceItem( "playlist2", "S://192.168.1.68/music/iTunes/iTunes%20Music%20Library.xml#GUID_2", "A:PLAYLISTS", From 1f4585cc9ea484553e8d02865c1d622817a7a129 Mon Sep 17 00:00:00 2001 From: Shai Ungar Date: Wed, 24 Apr 2024 15:29:13 +0300 Subject: [PATCH 849/967] Add service to 17track to get packages (#116067) * Add service to 17track * Add service to 17track change to select selector add snapshot test * Add service to 17track use strings for the selector * Add service to 17track fix test --- .../components/seventeentrack/__init__.py | 77 +++++++++++++++++-- .../components/seventeentrack/const.py | 5 ++ .../components/seventeentrack/coordinator.py | 10 +-- .../components/seventeentrack/icons.json | 3 + .../components/seventeentrack/services.yaml | 20 +++++ .../components/seventeentrack/strings.json | 29 +++++++ .../snapshots/test_services.ambr | 53 +++++++++++++ .../seventeentrack/test_services.py | 76 ++++++++++++++++++ 8 files changed, 262 insertions(+), 11 deletions(-) create mode 100644 homeassistant/components/seventeentrack/services.yaml create mode 100644 tests/components/seventeentrack/snapshots/test_services.ambr create mode 100644 tests/components/seventeentrack/test_services.py diff --git a/homeassistant/components/seventeentrack/__init__.py b/homeassistant/components/seventeentrack/__init__.py index 1f9879cdcbc..40c9c8d58d1 100644 --- a/homeassistant/components/seventeentrack/__init__.py +++ b/homeassistant/components/seventeentrack/__init__.py @@ -4,16 +4,81 @@ from py17track import Client as SeventeenTrackClient from py17track.errors import SeventeenTrackError from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform -from homeassistant.core import HomeAssistant +from homeassistant.const import ( + ATTR_FRIENDLY_NAME, + ATTR_LOCATION, + CONF_PASSWORD, + CONF_USERNAME, + Platform, +) +from homeassistant.core import ( + HomeAssistant, + ServiceCall, + ServiceResponse, + SupportsResponse, +) from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import ConfigType +from homeassistant.util import slugify -from .const import DOMAIN +from .const import ( + ATTR_CONFIG_ENTRY_ID, + ATTR_INFO_TEXT, + ATTR_PACKAGE_STATE, + ATTR_STATUS, + ATTR_TIMESTAMP, + ATTR_TRACKING_NUMBER, + DOMAIN, + SERVICE_GET_PACKAGES, +) from .coordinator import SeventeenTrackCoordinator PLATFORMS: list[Platform] = [Platform.SENSOR] +CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the 17Track component.""" + + async def get_packages(call: ServiceCall) -> ServiceResponse: + """Get packages from 17Track.""" + config_entry_id = call.data[ATTR_CONFIG_ENTRY_ID] + package_states = call.data.get(ATTR_PACKAGE_STATE, []) + seventeen_coordinator: SeventeenTrackCoordinator = hass.data[DOMAIN][ + config_entry_id + ] + live_packages = sorted( + await seventeen_coordinator.client.profile.packages( + show_archived=seventeen_coordinator.show_archived + ) + ) + + return { + "packages": [ + { + ATTR_TRACKING_NUMBER: package.tracking_number, + ATTR_LOCATION: package.location, + ATTR_STATUS: package.status, + ATTR_TIMESTAMP: package.timestamp, + ATTR_INFO_TEXT: package.info_text, + ATTR_FRIENDLY_NAME: package.friendly_name, + } + for package in live_packages + if slugify(package.status) in package_states or package_states == [] + ] + } + + hass.services.async_register( + DOMAIN, + SERVICE_GET_PACKAGES, + get_packages, + supports_response=SupportsResponse.ONLY, + ) + return True + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up 17Track from a config entry.""" @@ -26,10 +91,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except SeventeenTrackError as err: raise ConfigEntryNotReady from err - coordinator = SeventeenTrackCoordinator(hass, client) + seventeen_coordinator = SeventeenTrackCoordinator(hass, client) - await coordinator.async_config_entry_first_refresh() + await seventeen_coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = seventeen_coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/seventeentrack/const.py b/homeassistant/components/seventeentrack/const.py index fc7ca7b2e7f..39932d31935 100644 --- a/homeassistant/components/seventeentrack/const.py +++ b/homeassistant/components/seventeentrack/const.py @@ -40,3 +40,8 @@ NOTIFICATION_DELIVERED_MESSAGE = ( ) VALUE_DELIVERED = "Delivered" + +SERVICE_GET_PACKAGES = "get_packages" + +ATTR_PACKAGE_STATE = "package_state" +ATTR_CONFIG_ENTRY_ID = "config_entry_id" diff --git a/homeassistant/components/seventeentrack/coordinator.py b/homeassistant/components/seventeentrack/coordinator.py index 84bdf1e1359..4da4969ed92 100644 --- a/homeassistant/components/seventeentrack/coordinator.py +++ b/homeassistant/components/seventeentrack/coordinator.py @@ -45,19 +45,19 @@ class SeventeenTrackCoordinator(DataUpdateCoordinator[SeventeenTrackData]): self.show_delivered = self.config_entry.options[CONF_SHOW_DELIVERED] self.account_id = client.profile.account_id - self._show_archived = self.config_entry.options[CONF_SHOW_ARCHIVED] - self._client = client + self.show_archived = self.config_entry.options[CONF_SHOW_ARCHIVED] + self.client = client async def _async_update_data(self) -> SeventeenTrackData: """Fetch data from 17Track API.""" try: - summary = await self._client.profile.summary( - show_archived=self._show_archived + summary = await self.client.profile.summary( + show_archived=self.show_archived ) live_packages = set( - await self._client.profile.packages(show_archived=self._show_archived) + await self.client.profile.packages(show_archived=self.show_archived) ) except SeventeenTrackError as err: diff --git a/homeassistant/components/seventeentrack/icons.json b/homeassistant/components/seventeentrack/icons.json index 05323a69743..78ca65edc4d 100644 --- a/homeassistant/components/seventeentrack/icons.json +++ b/homeassistant/components/seventeentrack/icons.json @@ -26,5 +26,8 @@ "default": "mdi:package" } } + }, + "services": { + "get_packages": "mdi:package" } } diff --git a/homeassistant/components/seventeentrack/services.yaml b/homeassistant/components/seventeentrack/services.yaml new file mode 100644 index 00000000000..41cb66ada5f --- /dev/null +++ b/homeassistant/components/seventeentrack/services.yaml @@ -0,0 +1,20 @@ +get_packages: + fields: + package_state: + selector: + select: + multiple: true + options: + - "not_found" + - "in_transit" + - "expired" + - "ready_to_be_picked_up" + - "undelivered" + - "delivered" + - "returned" + translation_key: package_state + config_entry_id: + required: true + selector: + config_entry: + integration: seventeentrack diff --git a/homeassistant/components/seventeentrack/strings.json b/homeassistant/components/seventeentrack/strings.json index 8d91f926d50..626af29e856 100644 --- a/homeassistant/components/seventeentrack/strings.json +++ b/homeassistant/components/seventeentrack/strings.json @@ -66,5 +66,34 @@ "name": "Package {name}" } } + }, + "services": { + "get_packages": { + "name": "Get packages", + "description": "Get packages from 17Track", + "fields": { + "package_state": { + "name": "Package states", + "description": "Only return packages with the specified states. Returns all packages if not specified." + }, + "config_entry_id": { + "name": "17Track service", + "description": "The packages will be retrieved for the selected service." + } + } + } + }, + "selector": { + "package_state": { + "options": { + "not_found": "[%key:component::seventeentrack::entity::sensor::not_found::name%]", + "in_transit": "[%key:component::seventeentrack::entity::sensor::in_transit::name%]", + "expired": "[%key:component::seventeentrack::entity::sensor::expired::name%]", + "ready_to_be_picked_up": "[%key:component::seventeentrack::entity::sensor::ready_to_be_picked_up::name%]", + "undelivered": "[%key:component::seventeentrack::entity::sensor::undelivered::name%]", + "delivered": "[%key:component::seventeentrack::entity::sensor::delivered::name%]", + "returned": "[%key:component::seventeentrack::entity::sensor::returned::name%]" + } + } } } diff --git a/tests/components/seventeentrack/snapshots/test_services.ambr b/tests/components/seventeentrack/snapshots/test_services.ambr new file mode 100644 index 00000000000..185a1d44fe0 --- /dev/null +++ b/tests/components/seventeentrack/snapshots/test_services.ambr @@ -0,0 +1,53 @@ +# serializer version: 1 +# name: test_get_all_packages + dict({ + 'packages': list([ + dict({ + 'friendly_name': 'friendly name 3', + 'info_text': 'info text 1', + 'location': 'location 1', + 'status': 'Expired', + 'timestamp': datetime.datetime(2020, 8, 10, 10, 32, tzinfo=), + 'tracking_number': '123', + }), + dict({ + 'friendly_name': 'friendly name 1', + 'info_text': 'info text 1', + 'location': 'location 1', + 'status': 'In Transit', + 'timestamp': datetime.datetime(2020, 8, 10, 10, 32, tzinfo=), + 'tracking_number': '456', + }), + dict({ + 'friendly_name': 'friendly name 2', + 'info_text': 'info text 1', + 'location': 'location 1', + 'status': 'Delivered', + 'timestamp': datetime.datetime(2020, 8, 10, 10, 32, tzinfo=), + 'tracking_number': '789', + }), + ]), + }) +# --- +# name: test_get_packages_from_list + dict({ + 'packages': list([ + dict({ + 'friendly_name': 'friendly name 1', + 'info_text': 'info text 1', + 'location': 'location 1', + 'status': 'In Transit', + 'timestamp': datetime.datetime(2020, 8, 10, 10, 32, tzinfo=), + 'tracking_number': '456', + }), + dict({ + 'friendly_name': 'friendly name 2', + 'info_text': 'info text 1', + 'location': 'location 1', + 'status': 'Delivered', + 'timestamp': datetime.datetime(2020, 8, 10, 10, 32, tzinfo=), + 'tracking_number': '789', + }), + ]), + }) +# --- diff --git a/tests/components/seventeentrack/test_services.py b/tests/components/seventeentrack/test_services.py new file mode 100644 index 00000000000..cbd7132bf67 --- /dev/null +++ b/tests/components/seventeentrack/test_services.py @@ -0,0 +1,76 @@ +"""Tests for the seventeentrack service.""" + +from unittest.mock import AsyncMock + +from syrupy import SnapshotAssertion + +from homeassistant.components.seventeentrack import DOMAIN, SERVICE_GET_PACKAGES +from homeassistant.core import HomeAssistant, SupportsResponse + +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( + hass: HomeAssistant, + mock_seventeentrack: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Ensure service returns only the packages in the list.""" + await _mock_packages(mock_seventeentrack) + await init_integration(hass, mock_config_entry) + service_response = await hass.services.async_call( + DOMAIN, + SERVICE_GET_PACKAGES, + { + "config_entry_id": mock_config_entry.entry_id, + "package_state": ["in_transit", "delivered"], + }, + blocking=True, + return_response=SupportsResponse.ONLY, + ) + + assert service_response == snapshot + + +async def test_get_all_packages( + hass: HomeAssistant, + mock_seventeentrack: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Ensure service returns all packages when non provided.""" + await _mock_packages(mock_seventeentrack) + await init_integration(hass, mock_config_entry) + service_response = await hass.services.async_call( + DOMAIN, + SERVICE_GET_PACKAGES, + { + "config_entry_id": mock_config_entry.entry_id, + }, + blocking=True, + return_response=SupportsResponse.ONLY, + ) + + assert service_response == snapshot + + +async def _mock_packages(mock_seventeentrack): + package1 = get_package(status=10) + package2 = get_package( + tracking_number="789", + friendly_name="friendly name 2", + status=40, + ) + package3 = get_package( + tracking_number="123", + friendly_name="friendly name 3", + status=20, + ) + mock_seventeentrack.return_value.profile.packages.return_value = [ + package1, + package2, + package3, + ] From 350ca48d4c10b2105e1e3513da7137498dd6ad83 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 24 Apr 2024 15:12:29 +0200 Subject: [PATCH 850/967] Return specific group state if there is one (#115866) * Return specific group state if there is one * Refactor * Additional test cases * Refactor * Break out if more than one on state * tweaks * Remove log, add comment * add comment * Apply suggestions from code review Co-authored-by: J. Nick Koston * Refactor and improve comments * Refactor to class method * More filtering * Apply suggestions from code review * Only active if not excluded * Do not use a set * Apply suggestions from code review Co-authored-by: Erik Montnemery --------- Co-authored-by: J. Nick Koston Co-authored-by: Erik Montnemery --- homeassistant/components/group/entity.py | 95 ++++++++++++++++++---- homeassistant/components/group/registry.py | 14 +++- tests/components/group/test_init.py | 24 +++++- 3 files changed, 109 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/group/entity.py b/homeassistant/components/group/entity.py index a8fd9027984..5ac913dde8d 100644 --- a/homeassistant/components/group/entity.py +++ b/homeassistant/components/group/entity.py @@ -8,7 +8,7 @@ from collections.abc import Callable, Collection, Mapping import logging from typing import Any -from homeassistant.const import ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, STATE_ON +from homeassistant.const import ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, STATE_OFF, STATE_ON from homeassistant.core import ( CALLBACK_TYPE, Event, @@ -131,6 +131,9 @@ class Group(Entity): _unrecorded_attributes = frozenset({ATTR_ENTITY_ID, ATTR_ORDER, ATTR_AUTO}) _attr_should_poll = False + # In case there is only one active domain we use specific ON or OFF + # values, if all ON or OFF states are equal + single_active_domain: str | None tracking: tuple[str, ...] trackable: tuple[str, ...] @@ -287,6 +290,7 @@ class Group(Entity): if not entity_ids: self.tracking = () self.trackable = () + self.single_active_domain = None return registry: GroupIntegrationRegistry = self.hass.data[REG_KEY] @@ -294,12 +298,22 @@ class Group(Entity): tracking: list[str] = [] trackable: list[str] = [] + self.single_active_domain = None + multiple_domains: bool = False for ent_id in entity_ids: ent_id_lower = ent_id.lower() domain = split_entity_id(ent_id_lower)[0] tracking.append(ent_id_lower) - if domain not in excluded_domains: - trackable.append(ent_id_lower) + if domain in excluded_domains: + continue + + trackable.append(ent_id_lower) + + if not multiple_domains and self.single_active_domain is None: + self.single_active_domain = domain + if self.single_active_domain != domain: + multiple_domains = True + self.single_active_domain = None self.trackable = tuple(trackable) self.tracking = tuple(tracking) @@ -395,10 +409,36 @@ class Group(Entity): self._on_off[entity_id] = state in registry.on_off_mapping else: entity_on_state = registry.on_states_by_domain[domain] - if domain in registry.on_states_by_domain: - self._on_states.update(entity_on_state) + self._on_states.update(entity_on_state) self._on_off[entity_id] = state in entity_on_state + def _detect_specific_on_off_state(self, group_is_on: bool) -> set[str]: + """Check if a specific ON or OFF state is possible.""" + # In case the group contains entities of the same domain with the same ON + # or an OFF state (one or more domains), we want to use that specific state. + # If we have more then one ON or OFF state we default to STATE_ON or STATE_OFF. + registry: GroupIntegrationRegistry = self.hass.data[REG_KEY] + active_on_states: set[str] = set() + active_off_states: set[str] = set() + for entity_id in self.trackable: + if (state := self.hass.states.get(entity_id)) is None: + continue + current_state = state.state + if ( + group_is_on + and (domain_on_states := registry.on_states_by_domain.get(state.domain)) + and current_state in domain_on_states + ): + active_on_states.add(current_state) + # If we have more than one on state, the group state + # will result in STATE_ON and we can stop checking + if len(active_on_states) > 1: + break + elif current_state in registry.off_on_mapping: + active_off_states.add(current_state) + + return active_on_states if group_is_on else active_off_states + @callback def _async_update_group_state(self, tr_state: State | None = None) -> None: """Update group state. @@ -425,27 +465,48 @@ class Group(Entity): elif tr_state.attributes.get(ATTR_ASSUMED_STATE): self._assumed_state = True - num_on_states = len(self._on_states) + # If we do not have an on state for any domains + # we use None (which will be STATE_UNKNOWN) + if (num_on_states := len(self._on_states)) == 0: + self._state = None + return + + group_is_on = self.mode(self._on_off.values()) + # If all the entity domains we are tracking # have the same on state we use this state # and its hass.data[REG_KEY].on_off_mapping to off if num_on_states == 1: - on_state = list(self._on_states)[0] - # If we do not have an on state for any domains - # we use None (which will be STATE_UNKNOWN) - elif num_on_states == 0: - self._state = None - return + on_state = next(iter(self._on_states)) # If the entity domains have more than one - # on state, we use STATE_ON/STATE_OFF - else: + # on state, we use STATE_ON/STATE_OFF, unless there is + # only one specific `on` state in use for one specific domain + elif self.single_active_domain and num_on_states: + active_on_states = self._detect_specific_on_off_state(True) + on_state = ( + list(active_on_states)[0] if len(active_on_states) == 1 else STATE_ON + ) + elif group_is_on: on_state = STATE_ON - group_is_on = self.mode(self._on_off.values()) if group_is_on: self._state = on_state + return + + registry: GroupIntegrationRegistry = self.hass.data[REG_KEY] + if ( + active_domain := self.single_active_domain + ) and active_domain in registry.off_state_by_domain: + # If there is only one domain used, + # then we return the off state for that domain.s + self._state = registry.off_state_by_domain[active_domain] else: - registry: GroupIntegrationRegistry = self.hass.data[REG_KEY] - self._state = registry.on_off_mapping[on_state] + active_off_states = self._detect_specific_on_off_state(False) + # If there is one off state in use then we return that specific state, + # also if there a multiple domains involved, e.g. + # person and device_tracker, with a shared state. + self._state = ( + list(active_off_states)[0] if len(active_off_states) == 1 else STATE_OFF + ) def async_get_component(hass: HomeAssistant) -> EntityComponent[Group]: diff --git a/homeassistant/components/group/registry.py b/homeassistant/components/group/registry.py index 6cdb929d60c..474448db68a 100644 --- a/homeassistant/components/group/registry.py +++ b/homeassistant/components/group/registry.py @@ -49,9 +49,12 @@ class GroupIntegrationRegistry: def __init__(self) -> None: """Imitialize registry.""" - self.on_off_mapping: dict[str, str] = {STATE_ON: STATE_OFF} + self.on_off_mapping: dict[str, dict[str | None, str]] = { + STATE_ON: {None: STATE_OFF} + } self.off_on_mapping: dict[str, str] = {STATE_OFF: STATE_ON} self.on_states_by_domain: dict[str, set[str]] = {} + self.off_state_by_domain: dict[str, str] = {} self.exclude_domains: set[str] = set() def exclude_domain(self) -> None: @@ -60,11 +63,14 @@ class GroupIntegrationRegistry: def on_off_states(self, on_states: set, off_state: str) -> None: """Register on and off states for the current domain.""" + domain = current_domain.get() for on_state in on_states: if on_state not in self.on_off_mapping: - self.on_off_mapping[on_state] = off_state - + self.on_off_mapping[on_state] = {domain: off_state} + else: + self.on_off_mapping[on_state][domain] = off_state if len(on_states) == 1 and off_state not in self.off_on_mapping: self.off_on_mapping[off_state] = list(on_states)[0] - self.on_states_by_domain[current_domain.get()] = set(on_states) + self.on_states_by_domain[domain] = set(on_states) + self.off_state_by_domain[domain] = off_state diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py index d3f2747933e..b9cdfcb1590 100644 --- a/tests/components/group/test_init.py +++ b/tests/components/group/test_init.py @@ -9,7 +9,7 @@ from unittest.mock import patch import pytest -from homeassistant.components import group +from homeassistant.components import group, vacuum from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_FRIENDLY_NAME, @@ -659,6 +659,24 @@ async def test_is_on(hass: HomeAssistant) -> None: (STATE_ON, True), (STATE_OFF, False), ), + ( + ("vacuum", "vacuum"), + # Cleaning is the only on state + (vacuum.STATE_DOCKED, vacuum.STATE_CLEANING), + # Returning is the only on state + (vacuum.STATE_RETURNING, vacuum.STATE_PAUSED), + (vacuum.STATE_CLEANING, True), + (vacuum.STATE_RETURNING, True), + ), + ( + ("vacuum", "vacuum"), + # Multiple on states, so group state will be STATE_ON + (vacuum.STATE_RETURNING, vacuum.STATE_CLEANING), + # Only off states, so group state will be off + (vacuum.STATE_PAUSED, vacuum.STATE_IDLE), + (STATE_ON, True), + (STATE_OFF, False), + ), ], ) async def test_is_on_and_state_mixed_domains( @@ -1220,7 +1238,7 @@ async def test_group_climate_all_cool(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert hass.states.get("group.group_zero").state == STATE_ON + assert hass.states.get("group.group_zero").state == "cool" async def test_group_climate_all_off(hass: HomeAssistant) -> None: @@ -1334,7 +1352,7 @@ async def test_group_vacuum_on(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert hass.states.get("group.group_zero").state == STATE_ON + assert hass.states.get("group.group_zero").state == "cleaning" async def test_device_tracker_not_home(hass: HomeAssistant) -> None: From 70b358bca1adda27fa2e234f477e375d11b6cc66 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 24 Apr 2024 15:13:33 +0200 Subject: [PATCH 851/967] Always reload after a successful reauth flow (#116026) * Always reload after a succesfull reauth-flow * Add test, fix CI failures * Add kwarg to prevent reloading and tests * Do not reload entry for bond if it exists * Remove mocks on internals * Rename kwarg to always_reload * Update tests/components/weatherflow_cloud/test_config_flow.py * Update tests/components/homeworks/test_config_flow.py * Update tests/components/homeworks/test_config_flow.py * Rename to option to reload_even_if_entry_is_unchanged --- homeassistant/components/bond/config_flow.py | 5 +- .../components/homeworks/config_flow.py | 5 +- .../weatherflow_cloud/config_flow.py | 1 + homeassistant/config_entries.py | 3 +- tests/test_config_entries.py | 96 ++++++++++++++++--- 5 files changed, 92 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/bond/config_flow.py b/homeassistant/components/bond/config_flow.py index 45170a0404f..a12d3057258 100644 --- a/homeassistant/components/bond/config_flow.py +++ b/homeassistant/components/bond/config_flow.py @@ -113,7 +113,10 @@ class BondConfigFlow(ConfigFlow, domain=DOMAIN): ): updates[CONF_ACCESS_TOKEN] = token return self.async_update_reload_and_abort( - entry, data={**entry.data, **updates}, reason="already_configured" + entry, + data={**entry.data, **updates}, + reason="already_configured", + reload_even_if_entry_is_unchanged=False, ) self._discovered = {CONF_HOST: host, CONF_NAME: bond_id} diff --git a/homeassistant/components/homeworks/config_flow.py b/homeassistant/components/homeworks/config_flow.py index b9515c306d6..f447860c53f 100644 --- a/homeassistant/components/homeworks/config_flow.py +++ b/homeassistant/components/homeworks/config_flow.py @@ -690,7 +690,10 @@ class HomeworksConfigFlowHandler(ConfigFlow, domain=DOMAIN): CONF_PORT: user_input[CONF_PORT], } return self.async_update_reload_and_abort( - entry, options=new_options, reason="reconfigure_successful" + entry, + options=new_options, + reason="reconfigure_successful", + reload_even_if_entry_is_unchanged=False, ) return self.async_show_form( diff --git a/homeassistant/components/weatherflow_cloud/config_flow.py b/homeassistant/components/weatherflow_cloud/config_flow.py index 4c905a8451e..e8972c320ed 100644 --- a/homeassistant/components/weatherflow_cloud/config_flow.py +++ b/homeassistant/components/weatherflow_cloud/config_flow.py @@ -50,6 +50,7 @@ class WeatherFlowCloudConfigFlow(ConfigFlow, domain=DOMAIN): existing_entry, data={CONF_API_TOKEN: api_token}, reason="reauth_successful", + reload_even_if_entry_is_unchanged=False, ) return self.async_show_form( diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 0637e5f7c87..056814bbc4d 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -2399,6 +2399,7 @@ class ConfigFlow(ConfigEntryBaseFlow): data: Mapping[str, Any] | UndefinedType = UNDEFINED, options: Mapping[str, Any] | UndefinedType = UNDEFINED, reason: str = "reauth_successful", + reload_even_if_entry_is_unchanged: bool = True, ) -> ConfigFlowResult: """Update config entry, reload config entry and finish config flow.""" result = self.hass.config_entries.async_update_entry( @@ -2408,7 +2409,7 @@ class ConfigFlow(ConfigEntryBaseFlow): data=data, options=options, ) - if result: + if reload_even_if_entry_is_unchanged or result: self.hass.config_entries.async_schedule_reload(entry.entry_id) return self.async_abort(reason=reason) diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 63dea5ea735..68f770631ed 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -4504,24 +4504,86 @@ def test_raise_trying_to_add_same_config_entry_twice( assert f"An entry with the id {entry.entry_id} already exists" in caplog.text +@pytest.mark.parametrize( + ( + "title", + "unique_id", + "data_vendor", + "options_vendor", + "kwargs", + "calls_entry_load_unload", + ), + [ + ( + ("Test", "Updated title"), + ("1234", "5678"), + ("data", "data2"), + ("options", "options2"), + {}, + (2, 1), + ), + ( + ("Test", "Test"), + ("1234", "1234"), + ("data", "data"), + ("options", "options"), + {}, + (2, 1), + ), + ( + ("Test", "Updated title"), + ("1234", "5678"), + ("data", "data2"), + ("options", "options2"), + {"reload_even_if_entry_is_unchanged": True}, + (2, 1), + ), + ( + ("Test", "Test"), + ("1234", "1234"), + ("data", "data"), + ("options", "options"), + {"reload_even_if_entry_is_unchanged": False}, + (1, 0), + ), + ], + ids=[ + "changed_entry_default", + "unchanged_entry_default", + "changed_entry_explicit_reload", + "changed_entry_no_reload", + ], +) async def test_update_entry_and_reload( - hass: HomeAssistant, manager: config_entries.ConfigEntries + hass: HomeAssistant, + manager: config_entries.ConfigEntries, + title: tuple[str, str], + unique_id: tuple[str, str], + data_vendor: tuple[str, str], + options_vendor: tuple[str, str], + kwargs: dict[str, Any], + calls_entry_load_unload: tuple[int, int], ) -> None: """Test updating an entry and reloading.""" entry = MockConfigEntry( domain="comp", - unique_id="1234", - title="Test", - data={"vendor": "data"}, - options={"vendor": "options"}, + unique_id=unique_id[0], + title=title[0], + data={"vendor": data_vendor[0]}, + options={"vendor": options_vendor[0]}, ) entry.add_to_hass(hass) - mock_integration( - hass, MockModule("comp", async_setup_entry=AsyncMock(return_value=True)) + comp = MockModule( + "comp", + async_setup_entry=AsyncMock(return_value=True), + async_unload_entry=AsyncMock(return_value=True), ) + mock_integration(hass, comp) mock_platform(hass, "comp.config_flow", None) + await hass.config_entries.async_setup(entry.entry_id) + class MockFlowHandler(config_entries.ConfigFlow): """Define a mock flow handler.""" @@ -4531,23 +4593,27 @@ async def test_update_entry_and_reload( """Mock Reauth.""" return self.async_update_reload_and_abort( entry=entry, - unique_id="5678", - title="Updated Title", - data={"vendor": "data2"}, - options={"vendor": "options2"}, + unique_id=unique_id[1], + title=title[1], + data={"vendor": data_vendor[1]}, + options={"vendor": options_vendor[1]}, + **kwargs, ) with patch.dict(config_entries.HANDLERS, {"comp": MockFlowHandler}): task = await manager.flow.async_init("comp", context={"source": "reauth"}) await hass.async_block_till_done() - assert entry.title == "Updated Title" - assert entry.unique_id == "5678" - assert entry.data == {"vendor": "data2"} - assert entry.options == {"vendor": "options2"} + assert entry.title == title[1] + assert entry.unique_id == unique_id[1] + assert entry.data == {"vendor": data_vendor[1]} + assert entry.options == {"vendor": options_vendor[1]} assert entry.state == config_entries.ConfigEntryState.LOADED assert task["type"] == FlowResultType.ABORT assert task["reason"] == "reauth_successful" + # Assert entry was reloaded + assert len(comp.async_setup_entry.mock_calls) == calls_entry_load_unload[0] + assert len(comp.async_unload_entry.mock_calls) == calls_entry_load_unload[1] @pytest.mark.parametrize("unique_id", [["blah", "bleh"], {"key": "value"}]) From ea96ac37b7d63791e834c7882d58fd9dddb9a2b3 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 24 Apr 2024 15:29:51 +0200 Subject: [PATCH 852/967] Update frontend to 20240424.1 (#116103) --- 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 d711314cabb..ad63bdbed84 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==20240404.2"] + "requirements": ["home-assistant-frontend==20240424.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 50c17024b01..74c4d185847 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ habluetooth==2.8.0 hass-nabucasa==0.78.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240404.2 +home-assistant-frontend==20240424.1 home-assistant-intents==2024.4.3 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 5a7ce85328a..76b37b60e62 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1078,7 +1078,7 @@ hole==0.8.0 holidays==0.47 # homeassistant.components.frontend -home-assistant-frontend==20240404.2 +home-assistant-frontend==20240424.1 # homeassistant.components.conversation home-assistant-intents==2024.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4831f441286..82465f7a5c6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -880,7 +880,7 @@ hole==0.8.0 holidays==0.47 # homeassistant.components.frontend -home-assistant-frontend==20240404.2 +home-assistant-frontend==20240424.1 # homeassistant.components.conversation home-assistant-intents==2024.4.3 From 74cea2ecaedc5ddf5b61f2188e16936d1186c2d2 Mon Sep 17 00:00:00 2001 From: mletenay Date: Wed, 24 Apr 2024 15:31:29 +0200 Subject: [PATCH 853/967] Update goodwe library to 0.3.2 (#115309) --- 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 03575f9f4e2..6f1bdd2b449 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.2.32"] + "requirements": ["goodwe==0.3.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 76b37b60e62..256c5c3500e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -952,7 +952,7 @@ glances-api==0.6.0 goalzero==0.2.2 # homeassistant.components.goodwe -goodwe==0.2.32 +goodwe==0.3.2 # homeassistant.components.google_mail # homeassistant.components.google_tasks diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 82465f7a5c6..63a3563ebaf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -781,7 +781,7 @@ glances-api==0.6.0 goalzero==0.2.2 # homeassistant.components.goodwe -goodwe==0.2.32 +goodwe==0.3.2 # homeassistant.components.google_mail # homeassistant.components.google_tasks From 220dc1f125a1f3c91f66e6172341a316f0488856 Mon Sep 17 00:00:00 2001 From: Manuel Dipolt Date: Wed, 24 Apr 2024 15:59:09 +0200 Subject: [PATCH 854/967] Add binary sensor platform to romy integration (#112998) * wip * poc working, reworked to a binary sensor list * Update homeassistant/components/romy/binary_sensor.py Co-authored-by: Joost Lekkerkerker * Update homeassistant/components/romy/binary_sensor.py Co-authored-by: Joost Lekkerkerker * Update homeassistant/components/romy/binary_sensor.py Co-authored-by: Joost Lekkerkerker * Update homeassistant/components/romy/binary_sensor.py Co-authored-by: Joost Lekkerkerker * Update homeassistant/components/romy/binary_sensor.py Co-authored-by: Joost Lekkerkerker * code review changes, adjust translation key names * code review clean up: removed unecessary RomyBinarySensorEntityDescription * code review changes: translation names * code review changes, put DeviceInfo into RomyEntity * code review change: change docked icon to type plug * code review change: move CoordinatorEntity to the base class * code review changes: sensors disabled per default * code review: icons.json added * code review changes: sensors enabled per default again * checkout main entity.py * type hinting changes * Update homeassistant/components/romy/binary_sensor.py --------- Co-authored-by: Joost Lekkerkerker --- .coveragerc | 1 + .../components/romy/binary_sensor.py | 73 +++++++++++++++++++ homeassistant/components/romy/const.py | 2 +- homeassistant/components/romy/icons.json | 20 +++++ homeassistant/components/romy/strings.json | 14 ++++ 5 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/romy/binary_sensor.py create mode 100644 homeassistant/components/romy/icons.json diff --git a/.coveragerc b/.coveragerc index 6f382bcb780..ca2cce2719f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1158,6 +1158,7 @@ omit = homeassistant/components/roborock/coordinator.py homeassistant/components/rocketchat/notify.py homeassistant/components/romy/__init__.py + homeassistant/components/romy/binary_sensor.py homeassistant/components/romy/coordinator.py homeassistant/components/romy/entity.py homeassistant/components/romy/vacuum.py diff --git a/homeassistant/components/romy/binary_sensor.py b/homeassistant/components/romy/binary_sensor.py new file mode 100644 index 00000000000..263c5840e5f --- /dev/null +++ b/homeassistant/components/romy/binary_sensor.py @@ -0,0 +1,73 @@ +"""Checking binary status values from your ROMY.""" + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + 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 RomyVacuumCoordinator +from .entity import RomyEntity + +BINARY_SENSORS: list[BinarySensorEntityDescription] = [ + BinarySensorEntityDescription( + key="dustbin", + translation_key="dustbin_present", + ), + BinarySensorEntityDescription( + key="dock", + translation_key="docked", + device_class=BinarySensorDeviceClass.PLUG, + ), + BinarySensorEntityDescription( + key="water_tank", + translation_key="water_tank_present", + device_class=BinarySensorDeviceClass.MOISTURE, + ), + BinarySensorEntityDescription( + key="water_tank_empty", + translation_key="water_tank_empty", + device_class=BinarySensorDeviceClass.PROBLEM, + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up ROMY vacuum cleaner.""" + + coordinator: RomyVacuumCoordinator = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities( + RomyBinarySensor(coordinator, entity_description) + for entity_description in BINARY_SENSORS + if entity_description.key in coordinator.romy.binary_sensors + ) + + +class RomyBinarySensor(RomyEntity, BinarySensorEntity): + """RomyBinarySensor Class.""" + + entity_description: BinarySensorEntityDescription + + def __init__( + self, + coordinator: RomyVacuumCoordinator, + entity_description: BinarySensorEntityDescription, + ) -> None: + """Initialize ROMYs StatusSensor.""" + super().__init__(coordinator) + self._attr_unique_id = f"{entity_description.key}_{self.romy.unique_id}" + self.entity_description = entity_description + + @property + def is_on(self) -> bool: + """Return the value of the sensor.""" + return bool(self.romy.binary_sensors[self.entity_description.key]) diff --git a/homeassistant/components/romy/const.py b/homeassistant/components/romy/const.py index 5d42380902b..0fa039e8d1b 100644 --- a/homeassistant/components/romy/const.py +++ b/homeassistant/components/romy/const.py @@ -6,6 +6,6 @@ import logging from homeassistant.const import Platform DOMAIN = "romy" -PLATFORMS = [Platform.VACUUM] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.VACUUM] UPDATE_INTERVAL = timedelta(seconds=5) LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/romy/icons.json b/homeassistant/components/romy/icons.json new file mode 100644 index 00000000000..c27b36af64c --- /dev/null +++ b/homeassistant/components/romy/icons.json @@ -0,0 +1,20 @@ +{ + "entity": { + "binary_sensor": { + "water_tank_empty": { + "default": "mdi:cup-outline", + "state": { + "off": "mdi:cup-water", + "on": "mdi:cup-outline" + } + }, + "dustbin_present": { + "default": "mdi:basket-check", + "state": { + "off": "mdi:basket-remove", + "on": "mdi:basket-check" + } + } + } + } +} diff --git a/homeassistant/components/romy/strings.json b/homeassistant/components/romy/strings.json index 26dc60a2e84..f4bc4d191ff 100644 --- a/homeassistant/components/romy/strings.json +++ b/homeassistant/components/romy/strings.json @@ -46,6 +46,20 @@ } } } + }, + "binary_sensor": { + "dustbin_present": { + "name": "Dustbin present" + }, + "docked": { + "name": "Robot docked" + }, + "water_tank_present": { + "name": "Watertank present" + }, + "water_tank_empty": { + "name": "Watertank empty" + } } } } From d0f5e40b197c41e66a0d9b457bb5714d11c02ced Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 24 Apr 2024 16:14:44 +0200 Subject: [PATCH 855/967] Refactor ESPHome manager to avoid sending signals in tests (#116033) --- .../components/esphome/entry_data.py | 30 +++++++++---- homeassistant/components/esphome/update.py | 8 +--- tests/components/esphome/conftest.py | 11 +++-- tests/components/esphome/test_update.py | 45 ++++++++----------- 4 files changed, 48 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 7316c09cc5e..41b18c9b88c 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -49,9 +49,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.storage import Store -from homeassistant.util.signal_type import SignalType from .const import DOMAIN from .dashboard import async_get_dashboard @@ -126,6 +124,9 @@ class RuntimeEntryData: default_factory=dict ) device_update_subscriptions: set[CALLBACK_TYPE] = field(default_factory=set) + static_info_update_subscriptions: set[Callable[[list[EntityInfo]], None]] = field( + default_factory=set + ) loaded_platforms: set[Platform] = field(default_factory=set) platform_load_lock: asyncio.Lock = field(default_factory=asyncio.Lock) _storage_contents: StoreData | None = None @@ -154,11 +155,6 @@ class RuntimeEntryData: "_", " " ) - @property - def signal_static_info_updated(self) -> SignalType[list[EntityInfo]]: - """Return the signal to listen to for updates on static info.""" - return SignalType(f"esphome_{self.entry_id}_on_list") - @callback def async_register_static_info_callback( self, @@ -303,8 +299,9 @@ class RuntimeEntryData: for callback_ in callbacks_: callback_(entity_infos) - # Then send dispatcher event - async_dispatcher_send(hass, self.signal_static_info_updated, infos) + # Finally update static info subscriptions + for callback_ in self.static_info_update_subscriptions: + callback_(infos) @callback def async_subscribe_device_updated(self, callback_: CALLBACK_TYPE) -> CALLBACK_TYPE: @@ -317,6 +314,21 @@ class RuntimeEntryData: """Unsubscribe to device updates.""" self.device_update_subscriptions.remove(callback_) + @callback + def async_subscribe_static_info_updated( + self, callback_: Callable[[list[EntityInfo]], None] + ) -> CALLBACK_TYPE: + """Subscribe to static info updates.""" + self.static_info_update_subscriptions.add(callback_) + return partial(self._async_unsubscribe_static_info_updated, callback_) + + @callback + def _async_unsubscribe_static_info_updated( + self, callback_: Callable[[list[EntityInfo]], None] + ) -> None: + """Unsubscribe to static info updates.""" + self.static_info_update_subscriptions.remove(callback_) + @callback def async_subscribe_state_update( self, diff --git a/homeassistant/components/esphome/update.py b/homeassistant/components/esphome/update.py index 3e5a82bbd0b..b16a6e798b7 100644 --- a/homeassistant/components/esphome/update.py +++ b/homeassistant/components/esphome/update.py @@ -17,7 +17,6 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr 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.update_coordinator import CoordinatorEntity @@ -149,14 +148,9 @@ class ESPHomeUpdateEntity(CoordinatorEntity[ESPHomeDashboard], UpdateEntity): async def async_added_to_hass(self) -> None: """Handle entity added to Home Assistant.""" await super().async_added_to_hass() - hass = self.hass entry_data = self._entry_data self.async_on_remove( - async_dispatcher_connect( - hass, - entry_data.signal_static_info_updated, - self._handle_device_update, - ) + entry_data.async_subscribe_static_info_updated(self._handle_device_update) ) self.async_on_remove( entry_data.async_subscribe_device_updated(self._handle_device_update) diff --git a/tests/components/esphome/conftest.py b/tests/components/esphome/conftest.py index e23f020991d..f71b4196be6 100644 --- a/tests/components/esphome/conftest.py +++ b/tests/components/esphome/conftest.py @@ -181,7 +181,9 @@ async def mock_dashboard(hass): class MockESPHomeDevice: """Mock an esphome device.""" - def __init__(self, entry: MockConfigEntry, client: APIClient) -> None: + def __init__( + self, entry: MockConfigEntry, client: APIClient, device_info: DeviceInfo + ) -> None: """Init the mock.""" self.entry = entry self.client = client @@ -193,6 +195,7 @@ class MockESPHomeDevice: self.home_assistant_state_subscription_callback: Callable[ [str, str | None], None ] + self.device_info = device_info def set_state_callback(self, state_callback: Callable[[EntityState], None]) -> None: """Set the state callback.""" @@ -274,8 +277,6 @@ async def _mock_generic_device_entry( ) entry.add_to_hass(hass) - mock_device = MockESPHomeDevice(entry, mock_client) - default_device_info = { "name": "test", "friendly_name": "Test", @@ -284,6 +285,8 @@ async def _mock_generic_device_entry( } device_info = DeviceInfo(**(default_device_info | mock_device_info)) + mock_device = MockESPHomeDevice(entry, mock_client, device_info) + def _subscribe_states(callback: Callable[[EntityState], None]) -> None: """Subscribe to state.""" mock_device.set_state_callback(callback) @@ -302,7 +305,7 @@ async def _mock_generic_device_entry( """Subscribe to home assistant states.""" mock_device.set_home_assistant_state_subscription_callback(on_state_sub) - mock_client.device_info = AsyncMock(return_value=device_info) + mock_client.device_info = AsyncMock(return_value=mock_device.device_info) mock_client.subscribe_voice_assistant = Mock() mock_client.list_entities_services = AsyncMock( return_value=mock_list_entities_services diff --git a/tests/components/esphome/test_update.py b/tests/components/esphome/test_update.py index 959ad12876d..b3deb2f33ee 100644 --- a/tests/components/esphome/test_update.py +++ b/tests/components/esphome/test_update.py @@ -1,7 +1,6 @@ """Test ESPHome update entities.""" from collections.abc import Awaitable, Callable -import dataclasses from unittest.mock import Mock, patch from aioesphomeapi import APIClient, EntityInfo, EntityState, UserService @@ -18,7 +17,6 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.dispatcher import async_dispatcher_send from .conftest import MockESPHomeDevice @@ -176,9 +174,11 @@ async def test_update_entity( async def test_update_static_info( hass: HomeAssistant, - stub_reconnect, - mock_config_entry, - mock_device_info, + mock_client: APIClient, + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], mock_dashboard, ) -> None: """Test ESPHome update entity.""" @@ -190,32 +190,25 @@ async def test_update_static_info( ] await async_get_dashboard(hass).async_refresh() - signal_static_info_updated = f"esphome_{mock_config_entry.entry_id}_on_list" - runtime_data = Mock( - available=True, - device_info=mock_device_info, - signal_static_info_updated=signal_static_info_updated, + mock_device: MockESPHomeDevice = await mock_esphome_device( + mock_client=mock_client, + entity_info=[], + user_service=[], + states=[], ) - with patch( - "homeassistant.components.esphome.update.DomainData.get_entry_data", - return_value=runtime_data, - ): - assert await hass.config_entries.async_forward_entry_setup( - mock_config_entry, "update" - ) - - state = hass.states.get("update.none_firmware") + state = hass.states.get("update.test_firmware") assert state is not None - assert state.state == "on" + assert state.state == STATE_ON - runtime_data.device_info = dataclasses.replace( - runtime_data.device_info, esphome_version="1.2.3" - ) - async_dispatcher_send(hass, signal_static_info_updated, []) + object.__setattr__(mock_device.device_info, "esphome_version", "1.2.3") + await mock_device.mock_disconnect(True) + await mock_device.mock_connect() - state = hass.states.get("update.none_firmware") - assert state.state == "off" + await hass.async_block_till_done(wait_background_tasks=True) + + state = hass.states.get("update.test_firmware") + assert state.state == STATE_OFF @pytest.mark.parametrize( From c9ff618ef0f747ff9923c25e5dea1c5594dafd02 Mon Sep 17 00:00:00 2001 From: nyangogo <7449028+miawgogo@users.noreply.github.com> Date: Wed, 24 Apr 2024 15:19:44 +0100 Subject: [PATCH 856/967] Add nfandroidtv type checking and allow for strings to be passed to the image and icon data (#108652) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * nfandroidtv - add type checking and allow for strings to be passed to the image and icon data * nfandroidtv - wrong argument name * nfandroidtv - put the icon in the wrong varible 🙃 * nfandroidtv - raise ServiceValidationError instead of logging --------- Co-authored-by: nyangogo <7449028+ioangogo@users.noreply.github.com> --- .../components/nfandroidtv/notify.py | 58 ++++++++++++++----- .../components/nfandroidtv/strings.json | 8 +++ 2 files changed, 52 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/nfandroidtv/notify.py b/homeassistant/components/nfandroidtv/notify.py index dd42a0ab10b..dd6b15400d9 100644 --- a/homeassistant/components/nfandroidtv/notify.py +++ b/homeassistant/components/nfandroidtv/notify.py @@ -19,6 +19,7 @@ from homeassistant.components.notify import ( ) from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -44,6 +45,7 @@ from .const import ( ATTR_POSITION, ATTR_TRANSPARENCY, DEFAULT_TIMEOUT, + DOMAIN, ) _LOGGER = logging.getLogger(__name__) @@ -133,21 +135,49 @@ class NFAndroidTVNotificationService(BaseNotificationService): "Invalid interrupt-value: %s", data.get(ATTR_INTERRUPT) ) if imagedata := data.get(ATTR_IMAGE): - image_file = self.load_file( - url=imagedata.get(ATTR_IMAGE_URL), - local_path=imagedata.get(ATTR_IMAGE_PATH), - username=imagedata.get(ATTR_IMAGE_USERNAME), - password=imagedata.get(ATTR_IMAGE_PASSWORD), - auth=imagedata.get(ATTR_IMAGE_AUTH), - ) + if isinstance(imagedata, str): + image_file = ( + self.load_file(url=imagedata) + if imagedata.startswith("http") + else self.load_file(local_path=imagedata) + ) + elif isinstance(imagedata, dict): + image_file = self.load_file( + url=imagedata.get(ATTR_IMAGE_URL), + local_path=imagedata.get(ATTR_IMAGE_PATH), + username=imagedata.get(ATTR_IMAGE_USERNAME), + password=imagedata.get(ATTR_IMAGE_PASSWORD), + auth=imagedata.get(ATTR_IMAGE_AUTH), + ) + else: + raise ServiceValidationError( + "Invalid image provided", + translation_domain=DOMAIN, + translation_key="invalid_notification_image", + translation_placeholders={"type": type(imagedata).__name__}, + ) if icondata := data.get(ATTR_ICON): - icon = self.load_file( - url=icondata.get(ATTR_ICON_URL), - local_path=icondata.get(ATTR_ICON_PATH), - username=icondata.get(ATTR_ICON_USERNAME), - password=icondata.get(ATTR_ICON_PASSWORD), - auth=icondata.get(ATTR_ICON_AUTH), - ) + if isinstance(icondata, str): + icondata = ( + self.load_file(url=icondata) + if icondata.startswith("http") + else self.load_file(local_path=icondata) + ) + elif isinstance(icondata, dict): + icon = self.load_file( + url=icondata.get(ATTR_ICON_URL), + local_path=icondata.get(ATTR_ICON_PATH), + username=icondata.get(ATTR_ICON_USERNAME), + password=icondata.get(ATTR_ICON_PASSWORD), + auth=icondata.get(ATTR_ICON_AUTH), + ) + else: + raise ServiceValidationError( + "Invalid Icon provided", + translation_domain=DOMAIN, + translation_key="invalid_notification_icon", + translation_placeholders={"type": type(icondata).__name__}, + ) self.notify.send( message, title=title, diff --git a/homeassistant/components/nfandroidtv/strings.json b/homeassistant/components/nfandroidtv/strings.json index cde02327712..e73fc68d66a 100644 --- a/homeassistant/components/nfandroidtv/strings.json +++ b/homeassistant/components/nfandroidtv/strings.json @@ -1,4 +1,12 @@ { + "exceptions": { + "invalid_notification_icon": { + "message": "Invalid icon data provided. Got {type}" + }, + "invalid_notification_image": { + "message": "Invalid image data provided. Got {type}" + } + }, "config": { "step": { "user": { From bbaa0c16cc0547b485396d793229a6daf6d97b80 Mon Sep 17 00:00:00 2001 From: Arie Catsman <120491684+catsmanac@users.noreply.github.com> Date: Wed, 24 Apr 2024 16:33:14 +0200 Subject: [PATCH 857/967] Cancel timer on enphase_envoy config entry unload (#111406) * lingeringtimer * Add async_cleanup to enphase_envoy_coordinator and call from unload_entry --- homeassistant/components/enphase_envoy/__init__.py | 2 ++ homeassistant/components/enphase_envoy/coordinator.py | 6 ++++++ tests/components/enphase_envoy/conftest.py | 2 +- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/enphase_envoy/__init__.py b/homeassistant/components/enphase_envoy/__init__.py index 2407f807eb7..2cdba43453e 100644 --- a/homeassistant/components/enphase_envoy/__init__.py +++ b/homeassistant/components/enphase_envoy/__init__.py @@ -46,6 +46,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + await coordinator.async_cleanup() unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/enphase_envoy/coordinator.py b/homeassistant/components/enphase_envoy/coordinator.py index a508d5127d6..c0852fca807 100644 --- a/homeassistant/components/enphase_envoy/coordinator.py +++ b/homeassistant/components/enphase_envoy/coordinator.py @@ -159,3 +159,9 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): return envoy_data.raw raise RuntimeError("Unreachable code in _async_update_data") # pragma: no cover + + async def async_cleanup(self) -> None: + """Cleanup coordinator.""" + if self._cancel_token_refresh: + self._cancel_token_refresh() + self._cancel_token_refresh = None diff --git a/tests/components/enphase_envoy/conftest.py b/tests/components/enphase_envoy/conftest.py index 4d50f026c55..965af3b40fc 100644 --- a/tests/components/enphase_envoy/conftest.py +++ b/tests/components/enphase_envoy/conftest.py @@ -343,7 +343,7 @@ def mock_envoy_fixture( @pytest.fixture(name="setup_enphase_envoy") -async def setup_enphase_envoy_fixture(hass, config, mock_envoy): +async def setup_enphase_envoy_fixture(hass: HomeAssistant, config, mock_envoy): """Define a fixture to set up Enphase Envoy.""" with ( patch( From 169b9b0bfe65aaec56d3d3c111e5ddcc2cf04218 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Wed, 24 Apr 2024 09:47:03 -0500 Subject: [PATCH 858/967] Fix removing suggested_display_precision from entity registry (#110671) * Fix removing suggested_display_precision from entity registry * Fix tests * Update homeassistant/components/sensor/__init__.py --------- Co-authored-by: Erik --- homeassistant/components/sensor/__init__.py | 5 --- tests/components/sensor/test_init.py | 35 +++++++++++++++++++++ 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index ad6b3454ea9..a955e861c20 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -786,11 +786,6 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): ratio_log = floor(ratio_log) if ratio_log > 0 else ceil(ratio_log) display_precision = max(0, display_precision + ratio_log) - if display_precision is None and ( - DOMAIN not in self.registry_entry.options - or "suggested_display_precision" not in self.registry_entry.options - ): - return sensor_options: Mapping[str, Any] = self.registry_entry.options.get(DOMAIN, {}) if ( "suggested_display_precision" in sensor_options diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 74fd81188cd..079984476b0 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -1618,6 +1618,41 @@ async def test_suggested_precision_option_update( } +async def test_suggested_precision_option_removal( + hass: HomeAssistant, +) -> None: + """Test suggested precision stored in the registry is removed.""" + + entity_registry = er.async_get(hass) + + # Pre-register entities + entry = entity_registry.async_get_or_create("sensor", "test", "very_unique") + entity_registry.async_update_entity_options( + entry.entity_id, + "sensor", + { + "suggested_display_precision": 1, + }, + ) + + entity0 = MockSensor( + name="Test", + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=UnitOfTime.HOURS, + native_value="1.5", + suggested_display_precision=None, + unique_id="very_unique", + ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) + + assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) + await hass.async_block_till_done() + + # Assert the suggested precision is no longer stored in the registry + entry = entity_registry.async_get(entity0.entity_id) + assert entry.options.get("sensor", {}).get("suggested_display_precision") is None + + @pytest.mark.parametrize( ( "unit_system", From e47e62cbbf45e717f82e2240be48288114a0f6fd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 24 Apr 2024 16:58:46 +0200 Subject: [PATCH 859/967] Reduce duplicate code in enphase_envoy (#116107) Also converts a coro to a callback function since nothing was being awaited --- homeassistant/components/enphase_envoy/__init__.py | 4 ++-- homeassistant/components/enphase_envoy/coordinator.py | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/enphase_envoy/__init__.py b/homeassistant/components/enphase_envoy/__init__.py index 2cdba43453e..322f909437a 100644 --- a/homeassistant/components/enphase_envoy/__init__.py +++ b/homeassistant/components/enphase_envoy/__init__.py @@ -46,8 +46,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - coordinator = hass.data[DOMAIN][entry.entry_id] - await coordinator.async_cleanup() + coordinator: EnphaseUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator.async_cancel_token_refresh() unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/enphase_envoy/coordinator.py b/homeassistant/components/enphase_envoy/coordinator.py index c0852fca807..04f93098ad9 100644 --- a/homeassistant/components/enphase_envoy/coordinator.py +++ b/homeassistant/components/enphase_envoy/coordinator.py @@ -83,9 +83,7 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): def _async_mark_setup_complete(self) -> None: """Mark setup as complete and setup token refresh if needed.""" self._setup_complete = True - if self._cancel_token_refresh: - self._cancel_token_refresh() - self._cancel_token_refresh = None + self.async_cancel_token_refresh() if not isinstance(self.envoy.auth, EnvoyTokenAuth): return self._cancel_token_refresh = async_track_time_interval( @@ -160,8 +158,9 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): raise RuntimeError("Unreachable code in _async_update_data") # pragma: no cover - async def async_cleanup(self) -> None: - """Cleanup coordinator.""" + @callback + def async_cancel_token_refresh(self) -> None: + """Cancel token refresh.""" if self._cancel_token_refresh: self._cancel_token_refresh() self._cancel_token_refresh = None From 380f192c93d87287782d68e1f7b7e8a6565eea14 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 24 Apr 2024 11:06:24 -0400 Subject: [PATCH 860/967] Expose the SkyConnect integration with a firmware config/options flow (#115363) Co-authored-by: Stefan Agner Co-authored-by: Martin Hjelmare Co-authored-by: Erik --- .../homeassistant_sky_connect/__init__.py | 117 +- .../homeassistant_sky_connect/config_flow.py | 630 +++++++++- .../homeassistant_sky_connect/const.py | 11 + .../homeassistant_sky_connect/hardware.py | 2 +- .../homeassistant_sky_connect/manifest.json | 2 +- .../homeassistant_sky_connect/strings.json | 128 +- .../homeassistant_sky_connect/util.py | 140 ++- .../zha/repairs/wrong_silabs_firmware.py | 9 +- homeassistant/generated/integrations.json | 5 + script/hassfest/dependencies.py | 1 + .../test_config_flow.py | 1096 ++++++++++++----- .../test_config_flow_failures.py | 920 ++++++++++++++ .../test_hardware.py | 22 +- .../homeassistant_sky_connect/test_init.py | 393 +----- .../homeassistant_sky_connect/test_util.py | 203 +++ tests/components/usb/__init__.py | 16 + tests/components/zha/test_repairs.py | 10 +- 17 files changed, 2943 insertions(+), 762 deletions(-) create mode 100644 tests/components/homeassistant_sky_connect/test_config_flow_failures.py create mode 100644 tests/components/homeassistant_sky_connect/test_util.py diff --git a/homeassistant/components/homeassistant_sky_connect/__init__.py b/homeassistant/components/homeassistant_sky_connect/__init__.py index a85a1161792..fc02f31f263 100644 --- a/homeassistant/components/homeassistant_sky_connect/__init__.py +++ b/homeassistant/components/homeassistant_sky_connect/__init__.py @@ -2,87 +2,62 @@ from __future__ import annotations -from homeassistant.components import usb -from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import ( - check_multi_pan_addon, - get_zigbee_socket, - multi_pan_addon_using_device, -) -from homeassistant.config_entries import SOURCE_HARDWARE, ConfigEntry -from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError -from homeassistant.helpers import discovery_flow +import logging -from .const import DOMAIN -from .util import get_hardware_variant, get_usb_service_info +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from .util import guess_firmware_type -async def _async_usb_scan_done(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Finish Home Assistant SkyConnect config entry setup.""" - matcher = usb.USBCallbackMatcher( - domain=DOMAIN, - vid=entry.data["vid"].upper(), - pid=entry.data["pid"].upper(), - serial_number=entry.data["serial_number"].lower(), - manufacturer=entry.data["manufacturer"].lower(), - description=entry.data["description"].lower(), - ) - - if not usb.async_is_plugged_in(hass, matcher): - # The USB dongle is not plugged in, remove the config entry - hass.async_create_task( - hass.config_entries.async_remove(entry.entry_id), eager_start=True - ) - return - - usb_dev = entry.data["device"] - # The call to get_serial_by_id can be removed in HA Core 2024.1 - dev_path = await hass.async_add_executor_job(usb.get_serial_by_id, usb_dev) - - if not await multi_pan_addon_using_device(hass, dev_path): - usb_info = get_usb_service_info(entry) - await hass.config_entries.flow.async_init( - "zha", - context={"source": "usb"}, - data=usb_info, - ) - return - - hw_variant = get_hardware_variant(entry) - hw_discovery_data = { - "name": f"{hw_variant.short_name} Multiprotocol", - "port": { - "path": get_zigbee_socket(), - }, - "radio_type": "ezsp", - } - discovery_flow.async_create_flow( - hass, - "zha", - context={"source": SOURCE_HARDWARE}, - data=hw_discovery_data, - ) +_LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a Home Assistant SkyConnect config entry.""" - - try: - await check_multi_pan_addon(hass) - except HomeAssistantError as err: - raise ConfigEntryNotReady from err - - @callback - def async_usb_scan_done() -> None: - """Handle usb discovery started.""" - hass.async_create_task(_async_usb_scan_done(hass, entry), eager_start=True) - - unsub_usb = usb.async_register_initial_scan_callback(hass, async_usb_scan_done) - entry.async_on_unload(unsub_usb) - return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" return True + + +async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Migrate old entry.""" + + _LOGGER.debug( + "Migrating from version %s:%s", config_entry.version, config_entry.minor_version + ) + + if config_entry.version == 1: + if config_entry.minor_version == 1: + # Add-on startup with type service get started before Core, always (e.g. the + # Multi-Protocol add-on). Probing the firmware would interfere with the add-on, + # so we can't safely probe here. Instead, we must make an educated guess! + firmware_guess = await guess_firmware_type( + hass, config_entry.data["device"] + ) + + new_data = {**config_entry.data} + new_data["firmware"] = firmware_guess.firmware_type.value + + # Copy `description` to `product` + new_data["product"] = new_data["description"] + + hass.config_entries.async_update_entry( + config_entry, + data=new_data, + version=1, + minor_version=2, + ) + + _LOGGER.debug( + "Migration to version %s.%s successful", + config_entry.version, + config_entry.minor_version, + ) + + return True + + # This means the user has downgraded from a future version + return False diff --git a/homeassistant/components/homeassistant_sky_connect/config_flow.py b/homeassistant/components/homeassistant_sky_connect/config_flow.py index 3a3d32c2888..6ffb2783165 100644 --- a/homeassistant/components/homeassistant_sky_connect/config_flow.py +++ b/homeassistant/components/homeassistant_sky_connect/config_flow.py @@ -2,29 +2,498 @@ from __future__ import annotations +from abc import ABC, abstractmethod +import asyncio +import logging from typing import Any +from universal_silabs_flasher.const import ApplicationType + from homeassistant.components import usb +from homeassistant.components.hassio import ( + AddonError, + AddonInfo, + AddonManager, + AddonState, + is_hassio, +) from homeassistant.components.homeassistant_hardware import silabs_multiprotocol_addon -from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult +from homeassistant.components.zha.repairs.wrong_silabs_firmware import ( + probe_silabs_firmware_type, +) +from homeassistant.config_entries import ( + ConfigEntry, + ConfigEntryBaseFlow, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, + OptionsFlowWithConfigEntry, +) from homeassistant.core import callback +from homeassistant.data_entry_flow import AbortFlow -from .const import DOMAIN, HardwareVariant -from .util import get_hardware_variant, get_usb_service_info +from .const import DOCS_WEB_FLASHER_URL, DOMAIN, ZHA_DOMAIN, HardwareVariant +from .util import ( + get_hardware_variant, + get_otbr_addon_manager, + get_usb_service_info, + get_zha_device_path, + get_zigbee_flasher_addon_manager, +) + +_LOGGER = logging.getLogger(__name__) + +STEP_PICK_FIRMWARE_THREAD = "pick_firmware_thread" +STEP_PICK_FIRMWARE_ZIGBEE = "pick_firmware_zigbee" -class HomeAssistantSkyConnectConfigFlow(ConfigFlow, domain=DOMAIN): +class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC): + """Base flow to install firmware.""" + + _failed_addon_name: str + _failed_addon_reason: str + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Instantiate base flow.""" + super().__init__(*args, **kwargs) + + self._usb_info: usb.UsbServiceInfo | None = None + self._hw_variant: HardwareVariant | None = None + self._probed_firmware_type: ApplicationType | None = None + + self.addon_install_task: asyncio.Task | None = None + self.addon_start_task: asyncio.Task | None = None + self.addon_uninstall_task: asyncio.Task | None = None + + def _get_translation_placeholders(self) -> dict[str, str]: + """Shared translation placeholders.""" + placeholders = { + "model": ( + self._hw_variant.full_name + if self._hw_variant is not None + else "unknown" + ), + "firmware_type": ( + self._probed_firmware_type.value + if self._probed_firmware_type is not None + else "unknown" + ), + "docs_web_flasher_url": DOCS_WEB_FLASHER_URL, + } + + self.context["title_placeholders"] = placeholders + + return placeholders + + async def _async_set_addon_config( + self, config: dict, addon_manager: AddonManager + ) -> None: + """Set add-on config.""" + try: + await addon_manager.async_set_addon_options(config) + except AddonError as err: + _LOGGER.error(err) + raise AbortFlow( + "addon_set_config_failed", + description_placeholders=self._get_translation_placeholders(), + ) from err + + async def _async_get_addon_info(self, addon_manager: AddonManager) -> AddonInfo: + """Return add-on info.""" + try: + addon_info = await addon_manager.async_get_addon_info() + except AddonError as err: + _LOGGER.error(err) + raise AbortFlow( + "addon_info_failed", + description_placeholders={ + **self._get_translation_placeholders(), + "addon_name": addon_manager.addon_name, + }, + ) from err + + return addon_info + + async def async_step_pick_firmware( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Pick Thread or Zigbee firmware.""" + assert self._usb_info is not None + + self._probed_firmware_type = await probe_silabs_firmware_type( + self._usb_info.device, + probe_methods=( + # We probe in order of frequency: Zigbee, Thread, then multi-PAN + ApplicationType.GECKO_BOOTLOADER, + ApplicationType.EZSP, + ApplicationType.SPINEL, + ApplicationType.CPC, + ), + ) + + if self._probed_firmware_type not in ( + ApplicationType.EZSP, + ApplicationType.SPINEL, + ApplicationType.CPC, + ): + return self.async_abort( + reason="unsupported_firmware", + description_placeholders=self._get_translation_placeholders(), + ) + + return self.async_show_menu( + step_id="pick_firmware", + menu_options=[ + STEP_PICK_FIRMWARE_THREAD, + STEP_PICK_FIRMWARE_ZIGBEE, + ], + description_placeholders=self._get_translation_placeholders(), + ) + + async def async_step_pick_firmware_zigbee( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Pick Zigbee firmware.""" + # Allow the stick to be used with ZHA without flashing + if self._probed_firmware_type == ApplicationType.EZSP: + return await self.async_step_confirm_zigbee() + + if not is_hassio(self.hass): + return self.async_abort( + reason="not_hassio", + description_placeholders=self._get_translation_placeholders(), + ) + + # Only flash new firmware if we need to + fw_flasher_manager = get_zigbee_flasher_addon_manager(self.hass) + addon_info = await self._async_get_addon_info(fw_flasher_manager) + + if addon_info.state == AddonState.NOT_INSTALLED: + return await self.async_step_install_zigbee_flasher_addon() + + if addon_info.state == AddonState.NOT_RUNNING: + return await self.async_step_run_zigbee_flasher_addon() + + # If the addon is already installed and running, fail + return self.async_abort( + reason="addon_already_running", + description_placeholders={ + **self._get_translation_placeholders(), + "addon_name": fw_flasher_manager.addon_name, + }, + ) + + async def async_step_install_zigbee_flasher_addon( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Show progress dialog for installing the Zigbee flasher addon.""" + return await self._install_addon( + get_zigbee_flasher_addon_manager(self.hass), + "install_zigbee_flasher_addon", + "run_zigbee_flasher_addon", + ) + + async def _install_addon( + self, + addon_manager: silabs_multiprotocol_addon.WaitingAddonManager, + step_id: str, + next_step_id: str, + ) -> ConfigFlowResult: + """Show progress dialog for installing an addon.""" + addon_info = await self._async_get_addon_info(addon_manager) + + _LOGGER.debug("Flasher addon state: %s", addon_info) + + if not self.addon_install_task: + self.addon_install_task = self.hass.async_create_task( + addon_manager.async_install_addon_waiting(), + "Addon install", + ) + + if not self.addon_install_task.done(): + return self.async_show_progress( + step_id=step_id, + progress_action="install_addon", + description_placeholders={ + **self._get_translation_placeholders(), + "addon_name": addon_manager.addon_name, + }, + progress_task=self.addon_install_task, + ) + + try: + await self.addon_install_task + except AddonError as err: + _LOGGER.error(err) + self._failed_addon_name = addon_manager.addon_name + self._failed_addon_reason = "addon_install_failed" + return self.async_show_progress_done(next_step_id="addon_operation_failed") + finally: + self.addon_install_task = None + + return self.async_show_progress_done(next_step_id=next_step_id) + + async def async_step_addon_operation_failed( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Abort when add-on installation or start failed.""" + return self.async_abort( + reason=self._failed_addon_reason, + description_placeholders={ + **self._get_translation_placeholders(), + "addon_name": self._failed_addon_name, + }, + ) + + async def async_step_run_zigbee_flasher_addon( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Configure the flasher addon to point to the SkyConnect and run it.""" + fw_flasher_manager = get_zigbee_flasher_addon_manager(self.hass) + addon_info = await self._async_get_addon_info(fw_flasher_manager) + + assert self._usb_info is not None + new_addon_config = { + **addon_info.options, + "device": self._usb_info.device, + "baudrate": 115200, + "bootloader_baudrate": 115200, + "flow_control": True, + } + + _LOGGER.debug("Reconfiguring flasher addon with %s", new_addon_config) + await self._async_set_addon_config(new_addon_config, fw_flasher_manager) + + if not self.addon_start_task: + + async def start_and_wait_until_done() -> None: + await fw_flasher_manager.async_start_addon_waiting() + # Now that the addon is running, wait for it to finish + await fw_flasher_manager.async_wait_until_addon_state( + AddonState.NOT_RUNNING + ) + + self.addon_start_task = self.hass.async_create_task( + start_and_wait_until_done() + ) + + if not self.addon_start_task.done(): + return self.async_show_progress( + step_id="run_zigbee_flasher_addon", + progress_action="run_zigbee_flasher_addon", + description_placeholders={ + **self._get_translation_placeholders(), + "addon_name": fw_flasher_manager.addon_name, + }, + progress_task=self.addon_start_task, + ) + + try: + await self.addon_start_task + except (AddonError, AbortFlow) as err: + _LOGGER.error(err) + self._failed_addon_name = fw_flasher_manager.addon_name + self._failed_addon_reason = "addon_start_failed" + return self.async_show_progress_done(next_step_id="addon_operation_failed") + finally: + self.addon_start_task = None + + return self.async_show_progress_done( + next_step_id="uninstall_zigbee_flasher_addon" + ) + + async def async_step_uninstall_zigbee_flasher_addon( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Uninstall the flasher addon.""" + fw_flasher_manager = get_zigbee_flasher_addon_manager(self.hass) + + if not self.addon_uninstall_task: + _LOGGER.debug("Uninstalling flasher addon") + self.addon_uninstall_task = self.hass.async_create_task( + fw_flasher_manager.async_uninstall_addon_waiting() + ) + + if not self.addon_uninstall_task.done(): + return self.async_show_progress( + step_id="uninstall_zigbee_flasher_addon", + progress_action="uninstall_zigbee_flasher_addon", + description_placeholders={ + **self._get_translation_placeholders(), + "addon_name": fw_flasher_manager.addon_name, + }, + progress_task=self.addon_uninstall_task, + ) + + try: + await self.addon_uninstall_task + except (AddonError, AbortFlow) as err: + _LOGGER.error(err) + # The uninstall failing isn't critical so we can just continue + finally: + self.addon_uninstall_task = None + + return self.async_show_progress_done(next_step_id="confirm_zigbee") + + async def async_step_confirm_zigbee( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Confirm Zigbee setup.""" + assert self._usb_info is not None + assert self._hw_variant is not None + self._probed_firmware_type = ApplicationType.EZSP + + if user_input is not None: + await self.hass.config_entries.flow.async_init( + ZHA_DOMAIN, + context={"source": "hardware"}, + data={ + "name": self._hw_variant.full_name, + "port": { + "path": self._usb_info.device, + "baudrate": 115200, + "flow_control": "hardware", + }, + "radio_type": "ezsp", + }, + ) + + return self._async_flow_finished() + + return self.async_show_form( + step_id="confirm_zigbee", + description_placeholders=self._get_translation_placeholders(), + ) + + async def async_step_pick_firmware_thread( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Pick Thread firmware.""" + # We install the OTBR addon no matter what, since it is required to use Thread + if not is_hassio(self.hass): + return self.async_abort( + reason="not_hassio_thread", + description_placeholders=self._get_translation_placeholders(), + ) + + otbr_manager = get_otbr_addon_manager(self.hass) + addon_info = await self._async_get_addon_info(otbr_manager) + + if addon_info.state == AddonState.NOT_INSTALLED: + return await self.async_step_install_otbr_addon() + + if addon_info.state == AddonState.NOT_RUNNING: + return await self.async_step_start_otbr_addon() + + # If the addon is already installed and running, fail + return self.async_abort( + reason="otbr_addon_already_running", + description_placeholders={ + **self._get_translation_placeholders(), + "addon_name": otbr_manager.addon_name, + }, + ) + + async def async_step_install_otbr_addon( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Show progress dialog for installing the OTBR addon.""" + return await self._install_addon( + get_otbr_addon_manager(self.hass), "install_otbr_addon", "start_otbr_addon" + ) + + async def async_step_start_otbr_addon( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Configure OTBR to point to the SkyConnect and run the addon.""" + otbr_manager = get_otbr_addon_manager(self.hass) + addon_info = await self._async_get_addon_info(otbr_manager) + + assert self._usb_info is not None + new_addon_config = { + **addon_info.options, + "device": self._usb_info.device, + "baudrate": 460800, + "flow_control": True, + "autoflash_firmware": True, + } + + _LOGGER.debug("Reconfiguring OTBR addon with %s", new_addon_config) + await self._async_set_addon_config(new_addon_config, otbr_manager) + + if not self.addon_start_task: + self.addon_start_task = self.hass.async_create_task( + otbr_manager.async_start_addon_waiting() + ) + + if not self.addon_start_task.done(): + return self.async_show_progress( + step_id="start_otbr_addon", + progress_action="start_otbr_addon", + description_placeholders={ + **self._get_translation_placeholders(), + "addon_name": otbr_manager.addon_name, + }, + progress_task=self.addon_start_task, + ) + + try: + await self.addon_start_task + except (AddonError, AbortFlow) as err: + _LOGGER.error(err) + self._failed_addon_name = otbr_manager.addon_name + self._failed_addon_reason = "addon_start_failed" + return self.async_show_progress_done(next_step_id="addon_operation_failed") + finally: + self.addon_start_task = None + + return self.async_show_progress_done(next_step_id="confirm_otbr") + + async def async_step_confirm_otbr( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Confirm OTBR setup.""" + assert self._usb_info is not None + assert self._hw_variant is not None + + self._probed_firmware_type = ApplicationType.SPINEL + + if user_input is not None: + # OTBR discovery is done automatically via hassio + return self._async_flow_finished() + + return self.async_show_form( + step_id="confirm_otbr", + description_placeholders=self._get_translation_placeholders(), + ) + + @abstractmethod + def _async_flow_finished(self) -> ConfigFlowResult: + """Finish the flow.""" + # This should be implemented by a subclass + raise NotImplementedError + + +class HomeAssistantSkyConnectConfigFlow( + BaseFirmwareInstallFlow, ConfigFlow, domain=DOMAIN +): """Handle a config flow for Home Assistant SkyConnect.""" VERSION = 1 + MINOR_VERSION = 2 @staticmethod @callback def async_get_options_flow( config_entry: ConfigEntry, - ) -> HomeAssistantSkyConnectOptionsFlow: + ) -> OptionsFlow: """Return the options flow.""" - return HomeAssistantSkyConnectOptionsFlow(config_entry) + firmware_type = ApplicationType(config_entry.data["firmware"]) + + if firmware_type is ApplicationType.CPC: + return HomeAssistantSkyConnectMultiPanOptionsFlowHandler(config_entry) + + return HomeAssistantSkyConnectOptionsFlowHandler(config_entry) async def async_step_usb( self, discovery_info: usb.UsbServiceInfo @@ -37,27 +506,62 @@ class HomeAssistantSkyConnectConfigFlow(ConfigFlow, domain=DOMAIN): manufacturer = discovery_info.manufacturer description = discovery_info.description unique_id = f"{vid}:{pid}_{serial_number}_{manufacturer}_{description}" + if await self.async_set_unique_id(unique_id): self._abort_if_unique_id_configured(updates={"device": device}) + discovery_info.device = await self.hass.async_add_executor_job( + usb.get_serial_by_id, discovery_info.device + ) + + self._usb_info = discovery_info + assert description is not None - hw_variant = HardwareVariant.from_usb_product_name(description) + self._hw_variant = HardwareVariant.from_usb_product_name(description) + + return await self.async_step_confirm() + + async def async_step_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Confirm a discovery.""" + self._set_confirm_only() + + # Without confirmation, discovery can automatically progress into parts of the + # config flow logic that interacts with hardware. + if user_input is not None: + return await self.async_step_pick_firmware() + + return self.async_show_form( + step_id="confirm", + description_placeholders=self._get_translation_placeholders(), + ) + + def _async_flow_finished(self) -> ConfigFlowResult: + """Create the config entry.""" + assert self._usb_info is not None + assert self._hw_variant is not None + assert self._probed_firmware_type is not None return self.async_create_entry( - title=hw_variant.full_name, + title=self._hw_variant.full_name, data={ - "device": device, - "vid": vid, - "pid": pid, - "serial_number": serial_number, - "manufacturer": manufacturer, - "description": description, + "vid": self._usb_info.vid, + "pid": self._usb_info.pid, + "serial_number": self._usb_info.serial_number, + "manufacturer": self._usb_info.manufacturer, + "description": self._usb_info.description, # For backwards compatibility + "product": self._usb_info.description, + "device": self._usb_info.device, + "firmware": self._probed_firmware_type.value, }, ) -class HomeAssistantSkyConnectOptionsFlow(silabs_multiprotocol_addon.OptionsFlowHandler): - """Handle an option flow for Home Assistant SkyConnect.""" +class HomeAssistantSkyConnectMultiPanOptionsFlowHandler( + silabs_multiprotocol_addon.OptionsFlowHandler +): + """Multi-PAN options flow for Home Assistant SkyConnect.""" async def _async_serial_port_settings( self, @@ -92,3 +596,97 @@ class HomeAssistantSkyConnectOptionsFlow(silabs_multiprotocol_addon.OptionsFlowH def _hardware_name(self) -> str: """Return the name of the hardware.""" return self._hw_variant.full_name + + +class HomeAssistantSkyConnectOptionsFlowHandler( + BaseFirmwareInstallFlow, OptionsFlowWithConfigEntry +): + """Zigbee and Thread options flow handlers.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Instantiate options flow.""" + super().__init__(*args, **kwargs) + + self._usb_info = get_usb_service_info(self.config_entry) + self._probed_firmware_type = ApplicationType(self.config_entry.data["firmware"]) + self._hw_variant = HardwareVariant.from_usb_product_name( + self.config_entry.data["product"] + ) + + # Make `context` a regular dictionary + self.context = {} + + # Regenerate the translation placeholders + self._get_translation_placeholders() + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Manage the options flow.""" + # Don't probe the running firmware, we load it from the config entry + return self.async_show_menu( + step_id="pick_firmware", + menu_options=[ + STEP_PICK_FIRMWARE_THREAD, + STEP_PICK_FIRMWARE_ZIGBEE, + ], + description_placeholders=self._get_translation_placeholders(), + ) + + async def async_step_pick_firmware_zigbee( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Pick Zigbee firmware.""" + assert self._usb_info is not None + + if is_hassio(self.hass): + otbr_manager = get_otbr_addon_manager(self.hass) + otbr_addon_info = await self._async_get_addon_info(otbr_manager) + + if ( + otbr_addon_info.state != AddonState.NOT_INSTALLED + and otbr_addon_info.options.get("device") == self._usb_info.device + ): + raise AbortFlow( + "otbr_still_using_stick", + description_placeholders=self._get_translation_placeholders(), + ) + + return await super().async_step_pick_firmware_zigbee(user_input) + + async def async_step_pick_firmware_thread( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Pick Thread firmware.""" + assert self._usb_info is not None + + zha_entries = self.hass.config_entries.async_entries( + ZHA_DOMAIN, + include_ignore=False, + include_disabled=True, + ) + + if zha_entries and get_zha_device_path(zha_entries[0]) == self._usb_info.device: + raise AbortFlow( + "zha_still_using_stick", + description_placeholders=self._get_translation_placeholders(), + ) + + return await super().async_step_pick_firmware_thread(user_input) + + def _async_flow_finished(self) -> ConfigFlowResult: + """Create the config entry.""" + assert self._usb_info is not None + assert self._hw_variant is not None + assert self._probed_firmware_type is not None + + self.hass.config_entries.async_update_entry( + entry=self.config_entry, + data={ + **self.config_entry.data, + "firmware": self._probed_firmware_type.value, + }, + options=self.config_entry.options, + ) + + return self.async_create_entry(title="", data={}) diff --git a/homeassistant/components/homeassistant_sky_connect/const.py b/homeassistant/components/homeassistant_sky_connect/const.py index 1dd1471c470..1d6c16dc528 100644 --- a/homeassistant/components/homeassistant_sky_connect/const.py +++ b/homeassistant/components/homeassistant_sky_connect/const.py @@ -5,6 +5,17 @@ import enum from typing import Self DOMAIN = "homeassistant_sky_connect" +ZHA_DOMAIN = "zha" + +DOCS_WEB_FLASHER_URL = "https://skyconnect.home-assistant.io/firmware-update/" + +OTBR_ADDON_NAME = "OpenThread Border Router" +OTBR_ADDON_MANAGER_DATA = "openthread_border_router" +OTBR_ADDON_SLUG = "core_openthread_border_router" + +ZIGBEE_FLASHER_ADDON_NAME = "Silicon Labs Flasher" +ZIGBEE_FLASHER_ADDON_MANAGER_DATA = "silabs_flasher" +ZIGBEE_FLASHER_ADDON_SLUG = "core_silabs_flasher" @dataclasses.dataclass(frozen=True) diff --git a/homeassistant/components/homeassistant_sky_connect/hardware.py b/homeassistant/components/homeassistant_sky_connect/hardware.py index a9abeb27737..2872077111a 100644 --- a/homeassistant/components/homeassistant_sky_connect/hardware.py +++ b/homeassistant/components/homeassistant_sky_connect/hardware.py @@ -25,7 +25,7 @@ def async_info(hass: HomeAssistant) -> list[HardwareInfo]: pid=entry.data["pid"], serial_number=entry.data["serial_number"], manufacturer=entry.data["manufacturer"], - description=entry.data["description"], + description=entry.data["product"], ), name=get_hardware_variant(entry).full_name, url=DOCUMENTATION_URL, diff --git a/homeassistant/components/homeassistant_sky_connect/manifest.json b/homeassistant/components/homeassistant_sky_connect/manifest.json index f56fd24de61..c90ea2c075f 100644 --- a/homeassistant/components/homeassistant_sky_connect/manifest.json +++ b/homeassistant/components/homeassistant_sky_connect/manifest.json @@ -5,7 +5,7 @@ "config_flow": true, "dependencies": ["hardware", "usb", "homeassistant_hardware"], "documentation": "https://www.home-assistant.io/integrations/homeassistant_sky_connect", - "integration_type": "hardware", + "integration_type": "device", "usb": [ { "vid": "10C4", diff --git a/homeassistant/components/homeassistant_sky_connect/strings.json b/homeassistant/components/homeassistant_sky_connect/strings.json index 825649ef0d3..792406dcb02 100644 --- a/homeassistant/components/homeassistant_sky_connect/strings.json +++ b/homeassistant/components/homeassistant_sky_connect/strings.json @@ -57,6 +57,50 @@ "start_flasher_addon": { "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::start_flasher_addon::title%]", "description": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::start_flasher_addon::description%]" + }, + "confirm": { + "title": "[%key:component::homeassistant_sky_connect::config::step::confirm::title%]", + "description": "[%key:component::homeassistant_sky_connect::config::step::confirm::description%]" + }, + "pick_firmware": { + "title": "[%key:component::homeassistant_sky_connect::config::step::pick_firmware::title%]", + "description": "[%key:component::homeassistant_sky_connect::config::step::pick_firmware::description%]", + "menu_options": { + "pick_firmware_thread": "[%key:component::homeassistant_sky_connect::config::step::pick_firmware::menu_options::pick_firmware_thread%]", + "pick_firmware_zigbee": "[%key:component::homeassistant_sky_connect::config::step::pick_firmware::menu_options::pick_firmware_zigbee%]" + } + }, + "install_zigbee_flasher_addon": { + "title": "[%key:component::homeassistant_sky_connect::config::step::install_zigbee_flasher_addon::title%]", + "description": "[%key:component::homeassistant_sky_connect::config::step::install_zigbee_flasher_addon::description%]" + }, + "run_zigbee_flasher_addon": { + "title": "[%key:component::homeassistant_sky_connect::config::step::run_zigbee_flasher_addon::title%]", + "description": "[%key:component::homeassistant_sky_connect::config::step::run_zigbee_flasher_addon::description%]" + }, + "zigbee_flasher_failed": { + "title": "[%key:component::homeassistant_sky_connect::config::step::zigbee_flasher_failed::title%]", + "description": "[%key:component::homeassistant_sky_connect::config::step::zigbee_flasher_failed::description%]" + }, + "confirm_zigbee": { + "title": "[%key:component::homeassistant_sky_connect::config::step::confirm_zigbee::title%]", + "description": "[%key:component::homeassistant_sky_connect::config::step::confirm_zigbee::description%]" + }, + "install_otbr_addon": { + "title": "[%key:component::homeassistant_sky_connect::config::step::install_otbr_addon::title%]", + "description": "[%key:component::homeassistant_sky_connect::config::step::install_otbr_addon::description%]" + }, + "start_otbr_addon": { + "title": "[%key:component::homeassistant_sky_connect::config::step::start_otbr_addon::title%]", + "description": "[%key:component::homeassistant_sky_connect::config::step::start_otbr_addon::description%]" + }, + "otbr_failed": { + "title": "[%key:component::homeassistant_sky_connect::config::step::otbr_failed::title%]", + "description": "[%key:component::homeassistant_sky_connect::config::step::otbr_failed::description%]" + }, + "confirm_otbr": { + "title": "[%key:component::homeassistant_sky_connect::config::step::confirm_otbr::title%]", + "description": "[%key:component::homeassistant_sky_connect::config::step::confirm_otbr::description%]" } }, "error": { @@ -68,12 +112,92 @@ "addon_already_running": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_already_running%]", "addon_set_config_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_set_config_failed%]", "addon_start_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_start_failed%]", + "zha_migration_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::zha_migration_failed%]", "not_hassio": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::not_hassio%]", - "zha_migration_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::zha_migration_failed%]" + "not_hassio_thread": "[%key:component::homeassistant_sky_connect::config::abort::not_hassio_thread%]", + "otbr_addon_already_running": "[%key:component::homeassistant_sky_connect::config::abort::otbr_addon_already_running%]", + "zha_still_using_stick": "This {model} is in use by the Zigbee Home Automation integration. Please migrate your Zigbee network to another adapter or delete the integration and try again.", + "otbr_still_using_stick": "This {model} is in use by the OpenThread Border Router add-on. If you use the Thread network, make sure you have alternative border routers. Uninstall the add-on and try again." }, "progress": { "install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]", - "start_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]" + "start_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]", + "start_otbr_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]", + "install_zigbee_flasher_addon": "[%key:component::homeassistant_sky_connect::config::progress::install_zigbee_flasher_addon%]", + "run_zigbee_flasher_addon": "[%key:component::homeassistant_sky_connect::config::progress::run_zigbee_flasher_addon%]", + "uninstall_zigbee_flasher_addon": "[%key:component::homeassistant_sky_connect::config::progress::uninstall_zigbee_flasher_addon%]" + } + }, + "config": { + "flow_title": "{model}", + "step": { + "confirm": { + "title": "Set up the {model}", + "description": "The {model} can be used as either a Thread border router or a Zigbee coordinator. In the next step, you will choose which firmware will be configured." + }, + "pick_firmware": { + "title": "Pick your firmware", + "description": "The {model} can be used as a Thread border router or a Zigbee coordinator.", + "menu_options": { + "pick_firmware_thread": "Use as a Thread border router", + "pick_firmware_zigbee": "Use as a Zigbee coordinator" + } + }, + "install_zigbee_flasher_addon": { + "title": "Installing flasher", + "description": "Installing the Silicon Labs Flasher add-on." + }, + "run_zigbee_flasher_addon": { + "title": "Installing Zigbee firmware", + "description": "Installing Zigbee firmware. This will take about a minute." + }, + "uninstall_zigbee_flasher_addon": { + "title": "Removing flasher", + "description": "Removing the Silicon Labs Flasher add-on." + }, + "zigbee_flasher_failed": { + "title": "Zigbee installation failed", + "description": "The Zigbee firmware installation process was unsuccessful. Ensure no other software is trying to communicate with the {model} and try again." + }, + "confirm_zigbee": { + "title": "Zigbee setup complete", + "description": "Your {model} is now a Zigbee coordinator and will be shown as discovered by the Zigbee Home Automation integration once you exit." + }, + "install_otbr_addon": { + "title": "Installing OpenThread Border Router add-on", + "description": "The OpenThread Border Router (OTBR) add-on is being installed." + }, + "start_otbr_addon": { + "title": "Starting OpenThread Border Router add-on", + "description": "The OpenThread Border Router (OTBR) add-on is now starting." + }, + "otbr_failed": { + "title": "Failed to setup OpenThread Border Router", + "description": "The OpenThread Border Router add-on installation was unsuccessful. Ensure no other software is trying to communicate with the {model}, you have access to the internet and can install other add-ons, and try again. Check the Supervisor logs if the problem persists." + }, + "confirm_otbr": { + "title": "OpenThread Border Router setup complete", + "description": "Your {model} is now an OpenThread Border Router and will show up in the Thread integration once you exit." + } + }, + "abort": { + "addon_info_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_info_failed%]", + "addon_install_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_install_failed%]", + "addon_already_running": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_already_running%]", + "addon_set_config_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_set_config_failed%]", + "addon_start_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_start_failed%]", + "zha_migration_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::zha_migration_failed%]", + "not_hassio": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::not_hassio%]", + "not_hassio_thread": "The OpenThread Border Router addon can only be installed with Home Assistant OS. If you would like to use the {model} as an Thread border router, please flash the firmware manually using the [web flasher]({docs_web_flasher_url}) and set up OpenThread Border Router to communicate with it.", + "otbr_addon_already_running": "The OpenThread Border Router add-on is already running, it cannot be installed again." + }, + "progress": { + "install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]", + "start_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]", + "start_otbr_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]", + "install_zigbee_flasher_addon": "The Silicon Labs Flasher addon is installed, this may take a few minutes.", + "run_zigbee_flasher_addon": "Please wait while Zigbee firmware is installed to your {model}, this will take a few minutes. Do not make any changes to your hardware or software until this finishes.", + "uninstall_zigbee_flasher_addon": "The Silicon Labs Flasher addon is being removed." } } } diff --git a/homeassistant/components/homeassistant_sky_connect/util.py b/homeassistant/components/homeassistant_sky_connect/util.py index e1de1d3b442..f242416fa9a 100644 --- a/homeassistant/components/homeassistant_sky_connect/util.py +++ b/homeassistant/components/homeassistant_sky_connect/util.py @@ -2,10 +2,35 @@ from __future__ import annotations -from homeassistant.components import usb -from homeassistant.config_entries import ConfigEntry +from collections import defaultdict +from dataclasses import dataclass +import logging +from typing import cast -from .const import HardwareVariant +from universal_silabs_flasher.const import ApplicationType + +from homeassistant.components import usb +from homeassistant.components.hassio import AddonError, AddonState, is_hassio +from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import ( + WaitingAddonManager, + get_multiprotocol_addon_manager, +) +from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.singleton import singleton + +from .const import ( + OTBR_ADDON_MANAGER_DATA, + OTBR_ADDON_NAME, + OTBR_ADDON_SLUG, + ZHA_DOMAIN, + ZIGBEE_FLASHER_ADDON_MANAGER_DATA, + ZIGBEE_FLASHER_ADDON_NAME, + ZIGBEE_FLASHER_ADDON_SLUG, + HardwareVariant, +) + +_LOGGER = logging.getLogger(__name__) def get_usb_service_info(config_entry: ConfigEntry) -> usb.UsbServiceInfo: @@ -16,10 +41,115 @@ def get_usb_service_info(config_entry: ConfigEntry) -> usb.UsbServiceInfo: pid=config_entry.data["pid"], serial_number=config_entry.data["serial_number"], manufacturer=config_entry.data["manufacturer"], - description=config_entry.data["description"], + description=config_entry.data["product"], ) def get_hardware_variant(config_entry: ConfigEntry) -> HardwareVariant: """Get the hardware variant from the config entry.""" - return HardwareVariant.from_usb_product_name(config_entry.data["description"]) + return HardwareVariant.from_usb_product_name(config_entry.data["product"]) + + +def get_zha_device_path(config_entry: ConfigEntry) -> str: + """Get the device path from a ZHA config entry.""" + return cast(str, config_entry.data["device"]["path"]) + + +@singleton(OTBR_ADDON_MANAGER_DATA) +@callback +def get_otbr_addon_manager(hass: HomeAssistant) -> WaitingAddonManager: + """Get the OTBR add-on manager.""" + return WaitingAddonManager( + hass, + _LOGGER, + OTBR_ADDON_NAME, + OTBR_ADDON_SLUG, + ) + + +@singleton(ZIGBEE_FLASHER_ADDON_MANAGER_DATA) +@callback +def get_zigbee_flasher_addon_manager(hass: HomeAssistant) -> WaitingAddonManager: + """Get the flasher add-on manager.""" + return WaitingAddonManager( + hass, + _LOGGER, + ZIGBEE_FLASHER_ADDON_NAME, + ZIGBEE_FLASHER_ADDON_SLUG, + ) + + +@dataclass(slots=True, kw_only=True) +class FirmwareGuess: + """Firmware guess.""" + + is_running: bool + firmware_type: ApplicationType + source: str + + +async def guess_firmware_type(hass: HomeAssistant, device_path: str) -> FirmwareGuess: + """Guess the firmware type based on installed addons and other integrations.""" + device_guesses: defaultdict[str | None, list[FirmwareGuess]] = defaultdict(list) + + for zha_config_entry in hass.config_entries.async_entries(ZHA_DOMAIN): + zha_path = get_zha_device_path(zha_config_entry) + device_guesses[zha_path].append( + FirmwareGuess( + is_running=(zha_config_entry.state == ConfigEntryState.LOADED), + firmware_type=ApplicationType.EZSP, + source="zha", + ) + ) + + if is_hassio(hass): + otbr_addon_manager = get_otbr_addon_manager(hass) + + try: + otbr_addon_info = await otbr_addon_manager.async_get_addon_info() + except AddonError: + pass + else: + if otbr_addon_info.state != AddonState.NOT_INSTALLED: + otbr_path = otbr_addon_info.options.get("device") + device_guesses[otbr_path].append( + FirmwareGuess( + is_running=(otbr_addon_info.state == AddonState.RUNNING), + firmware_type=ApplicationType.SPINEL, + source="otbr", + ) + ) + + multipan_addon_manager = await get_multiprotocol_addon_manager(hass) + + try: + multipan_addon_info = await multipan_addon_manager.async_get_addon_info() + except AddonError: + pass + else: + if multipan_addon_info.state != AddonState.NOT_INSTALLED: + multipan_path = multipan_addon_info.options.get("device") + device_guesses[multipan_path].append( + FirmwareGuess( + is_running=(multipan_addon_info.state == AddonState.RUNNING), + firmware_type=ApplicationType.CPC, + source="multiprotocol", + ) + ) + + # Fall back to EZSP if we can't guess the firmware type + if device_path not in device_guesses: + return FirmwareGuess( + is_running=False, firmware_type=ApplicationType.EZSP, source="unknown" + ) + + # Prioritizes guesses that were pulled from a running addon or integration but keep + # the sort order we defined above + guesses = sorted( + device_guesses[device_path], + key=lambda guess: guess.is_running, + ) + + assert guesses + + return guesses[-1] diff --git a/homeassistant/components/zha/repairs/wrong_silabs_firmware.py b/homeassistant/components/zha/repairs/wrong_silabs_firmware.py index 5b1f85e1a29..4ee10c7bb93 100644 --- a/homeassistant/components/zha/repairs/wrong_silabs_firmware.py +++ b/homeassistant/components/zha/repairs/wrong_silabs_firmware.py @@ -74,9 +74,14 @@ def _detect_radio_hardware(hass: HomeAssistant, device: str) -> HardwareType: return HardwareType.OTHER -async def probe_silabs_firmware_type(device: str) -> ApplicationType | None: +async def probe_silabs_firmware_type( + device: str, *, probe_methods: ApplicationType | None = None +) -> ApplicationType | None: """Probe the running firmware on a Silabs device.""" - flasher = Flasher(device=device) + flasher = Flasher( + device=device, + **({"probe_methods": probe_methods} if probe_methods else {}), + ) try: await flasher.probe_app_type() diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index e6a103989d1..cf5f352f22c 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -2565,6 +2565,11 @@ "integration_type": "virtual", "supported_by": "netatmo" }, + "homeassistant_sky_connect": { + "name": "Home Assistant SkyConnect", + "integration_type": "device", + "config_flow": true + }, "homematic": { "name": "Homematic", "integrations": { diff --git a/script/hassfest/dependencies.py b/script/hassfest/dependencies.py index d4eb135a265..66796d4dd0d 100644 --- a/script/hassfest/dependencies.py +++ b/script/hassfest/dependencies.py @@ -157,6 +157,7 @@ IGNORE_VIOLATIONS = { ("zha", "homeassistant_hardware"), ("zha", "homeassistant_sky_connect"), ("zha", "homeassistant_yellow"), + ("homeassistant_sky_connect", "zha"), # This should become a helper method that integrations can submit data to ("websocket_api", "lovelace"), ("websocket_api", "shopping_list"), diff --git a/tests/components/homeassistant_sky_connect/test_config_flow.py b/tests/components/homeassistant_sky_connect/test_config_flow.py index 9647cef4721..c34e3ebe186 100644 --- a/tests/components/homeassistant_sky_connect/test_config_flow.py +++ b/tests/components/homeassistant_sky_connect/test_config_flow.py @@ -1,23 +1,31 @@ """Test the Home Assistant SkyConnect config flow.""" -from collections.abc import Generator -import copy -from unittest.mock import Mock, patch +import asyncio +from collections.abc import Awaitable, Callable +from typing import Any +from unittest.mock import AsyncMock, Mock, call, patch import pytest +from universal_silabs_flasher.const import ApplicationType -from homeassistant.components import homeassistant_sky_connect, usb +from homeassistant.components import usb +from homeassistant.components.hassio.addon_manager import AddonInfo, AddonState +from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import ( + get_multiprotocol_addon_manager, +) +from homeassistant.components.homeassistant_sky_connect.config_flow import ( + STEP_PICK_FIRMWARE_THREAD, + STEP_PICK_FIRMWARE_ZIGBEE, +) from homeassistant.components.homeassistant_sky_connect.const import DOMAIN -from homeassistant.components.zha import ( - CONF_DEVICE_PATH, - DOMAIN as ZHA_DOMAIN, - RadioType, +from homeassistant.components.homeassistant_sky_connect.util import ( + get_otbr_addon_manager, + get_zigbee_flasher_addon_manager, ) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry, MockModule, mock_integration +from tests.common import MockConfigEntry USB_DATA_SKY = usb.UsbServiceInfo( device="/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_9e2adbd75b8beb119fe564a0f320645d-if00-port0", @@ -38,340 +46,840 @@ USB_DATA_ZBT1 = usb.UsbServiceInfo( ) -@pytest.fixture(autouse=True) -def config_flow_handler(hass: HomeAssistant) -> Generator[None, None, None]: - """Fixture for a test config flow.""" - with patch( - "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.WaitingAddonManager.async_wait_until_addon_state" - ): - yield +def delayed_side_effect() -> Callable[..., Awaitable[None]]: + """Slows down eager tasks by delaying for an event loop tick.""" + + async def side_effect(*args: Any, **kwargs: Any) -> None: + await asyncio.sleep(0) + + return side_effect @pytest.mark.parametrize( - ("usb_data", "title"), + ("usb_data", "model"), [ (USB_DATA_SKY, "Home Assistant SkyConnect"), (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), ], ) -async def test_config_flow( - usb_data: usb.UsbServiceInfo, title: str, hass: HomeAssistant +async def test_config_flow_zigbee( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant ) -> None: """Test the config flow for SkyConnect.""" - with patch( - "homeassistant.components.homeassistant_sky_connect.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "usb"}, data=usb_data - ) - - expected_data = { - "device": usb_data.device, - "vid": usb_data.vid, - "pid": usb_data.pid, - "serial_number": usb_data.serial_number, - "manufacturer": usb_data.manufacturer, - "description": usb_data.description, - } - - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == title - assert result["data"] == expected_data - assert result["options"] == {} - assert len(mock_setup_entry.mock_calls) == 1 - - config_entry = hass.config_entries.async_entries(DOMAIN)[0] - assert config_entry.data == expected_data - assert config_entry.options == {} - assert config_entry.title == title - assert ( - config_entry.unique_id - == f"{usb_data.vid}:{usb_data.pid}_{usb_data.serial_number}_{usb_data.manufacturer}_{usb_data.description}" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=usb_data ) + # First step is confirmation, we haven't probed the firmware yet + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "confirm" + assert result["description_placeholders"]["firmware_type"] == "unknown" + assert result["description_placeholders"]["model"] == model -@pytest.mark.parametrize( - ("usb_data", "title"), - [ - (USB_DATA_SKY, "Home Assistant SkyConnect"), - (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), - ], -) -async def test_config_flow_multiple_entries( - usb_data: usb.UsbServiceInfo, title: str, hass: HomeAssistant -) -> None: - """Test multiple entries are allowed.""" - # Setup an existing config entry - config_entry = MockConfigEntry( - data={}, - domain=DOMAIN, - options={}, - title=title, - unique_id=f"{usb_data.vid}:{usb_data.pid}_{usb_data.serial_number}_{usb_data.manufacturer}_{usb_data.description}", - ) - config_entry.add_to_hass(hass) - - usb_data = copy.copy(usb_data) - usb_data.serial_number = "bla_serial_number_2" - + # Next, we probe the firmware with patch( - "homeassistant.components.homeassistant_sky_connect.async_setup_entry", - return_value=True, + "homeassistant.components.homeassistant_sky_connect.config_flow.probe_silabs_firmware_type", + return_value=ApplicationType.SPINEL, # Ensure we re-install it ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "usb"}, data=usb_data + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} ) - assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "pick_firmware" + assert result["description_placeholders"]["firmware_type"] == "spinel" - -@pytest.mark.parametrize( - ("usb_data", "title"), - [ - (USB_DATA_SKY, "Home Assistant SkyConnect"), - (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), - ], -) -async def test_config_flow_update_device( - usb_data: usb.UsbServiceInfo, title: str, hass: HomeAssistant -) -> None: - """Test updating device path.""" - # Setup an existing config entry - config_entry = MockConfigEntry( - data={}, - domain=DOMAIN, - options={}, - title=title, - unique_id=f"{usb_data.vid}:{usb_data.pid}_{usb_data.serial_number}_{usb_data.manufacturer}_{usb_data.description}", + # Set up Zigbee firmware + mock_flasher_manager = Mock(spec_set=get_zigbee_flasher_addon_manager(hass)) + mock_flasher_manager.async_install_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + mock_flasher_manager.async_start_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + mock_flasher_manager.async_uninstall_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() ) - config_entry.add_to_hass(hass) - - usb_data = copy.copy(usb_data) - usb_data.device = "bla_device_2" - - with patch( - "homeassistant.components.homeassistant_sky_connect.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - assert await hass.config_entries.async_setup(config_entry.entry_id) - assert len(mock_setup_entry.mock_calls) == 1 with ( patch( - "homeassistant.components.homeassistant_sky_connect.async_setup_entry", - return_value=True, - ) as mock_setup_entry, + "homeassistant.components.homeassistant_sky_connect.config_flow.get_zigbee_flasher_addon_manager", + return_value=mock_flasher_manager, + ), patch( - "homeassistant.components.homeassistant_sky_connect.async_unload_entry", - wraps=homeassistant_sky_connect.async_unload_entry, - ) as mock_unload_entry, + "homeassistant.components.homeassistant_sky_connect.config_flow.is_hassio", + return_value=True, + ), ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "usb"}, data=usb_data + mock_flasher_manager.addon_name = "Silicon Labs Flasher" + mock_flasher_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={}, + state=AddonState.NOT_INSTALLED, + update_available=False, + version=None, ) - await hass.async_block_till_done() - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "already_configured" - assert len(mock_setup_entry.mock_calls) == 1 - assert len(mock_unload_entry.mock_calls) == 1 + # Pick the menu option: we are now installing the addon + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_ZIGBEE}, + ) + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["progress_action"] == "install_addon" + assert result["step_id"] == "install_zigbee_flasher_addon" + await hass.async_block_till_done(wait_background_tasks=True) -@pytest.mark.parametrize( - ("usb_data", "title"), - [ - (USB_DATA_SKY, "Home Assistant SkyConnect"), - (USB_DATA_ZBT1, "Home Assistant ZBT-1"), - ], -) -async def test_option_flow_install_multi_pan_addon( - usb_data: usb.UsbServiceInfo, - title: str, - hass: HomeAssistant, - addon_store_info, - addon_info, - install_addon, - set_addon_options, - start_addon, -) -> None: - """Test installing the multi pan addon.""" - assert await async_setup_component(hass, "usb", {}) - mock_integration(hass, MockModule("hassio")) + # Progress the flow, we are now configuring the addon and running it + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "run_zigbee_flasher_addon" + assert result["progress_action"] == "run_zigbee_flasher_addon" + assert mock_flasher_manager.async_set_addon_options.mock_calls == [ + call( + { + "device": usb_data.device, + "baudrate": 115200, + "bootloader_baudrate": 115200, + "flow_control": True, + } + ) + ] - # Setup the config entry - config_entry = MockConfigEntry( - data={ - "device": usb_data.device, - "vid": usb_data.vid, - "pid": usb_data.pid, - "serial_number": usb_data.serial_number, - "manufacturer": usb_data.manufacturer, - "description": usb_data.description, - }, - domain=DOMAIN, - options={}, - title=title, - unique_id=f"{usb_data.vid}:{usb_data.pid}_{usb_data.serial_number}_{usb_data.manufacturer}_{usb_data.description}", - ) - config_entry.add_to_hass(hass) + await hass.async_block_till_done(wait_background_tasks=True) - with patch( - "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", - side_effect=Mock(return_value=True), - ): - result = await hass.config_entries.options.async_init(config_entry.entry_id) + # Progress the flow, we are now uninstalling the addon + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "uninstall_zigbee_flasher_addon" + assert result["progress_action"] == "uninstall_zigbee_flasher_addon" + + await hass.async_block_till_done(wait_background_tasks=True) + + # We are finally done with the addon + assert mock_flasher_manager.async_uninstall_addon_waiting.mock_calls == [call()] + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "addon_not_installed" + assert result["step_id"] == "confirm_zigbee" - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - "enable_multi_pan": True, - }, + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} ) - assert result["type"] is FlowResultType.SHOW_PROGRESS - assert result["step_id"] == "install_addon" - assert result["progress_action"] == "install_addon" - - await hass.async_block_till_done() - install_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") - - result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] is FlowResultType.SHOW_PROGRESS - assert result["step_id"] == "start_addon" - set_addon_options.assert_called_once_with( - hass, - "core_silabs_multiprotocol", - { - "options": { - "autoflash_firmware": True, - "device": usb_data.device, - "baudrate": "115200", - "flow_control": True, - } - }, - ) - - await hass.async_block_till_done() - start_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") - - result = await hass.config_entries.options.async_configure(result["flow_id"]) assert result["type"] is FlowResultType.CREATE_ENTRY + config_entry = result["result"] + assert config_entry.data == { + "firmware": "ezsp", + "device": usb_data.device, + "manufacturer": usb_data.manufacturer, + "pid": usb_data.pid, + "description": usb_data.description, + "product": usb_data.description, + "serial_number": usb_data.serial_number, + "vid": usb_data.vid, + } -def mock_detect_radio_type(radio_type=RadioType.ezsp, ret=True): - """Mock `detect_radio_type` that just sets the appropriate attributes.""" - - async def detect(self): - self.radio_type = radio_type - self.device_settings = radio_type.controller.SCHEMA_DEVICE( - {CONF_DEVICE_PATH: self.device_path} - ) - - return ret - - return detect + # Ensure a ZHA discovery flow has been created + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + zha_flow = flows[0] + assert zha_flow["handler"] == "zha" + assert zha_flow["context"]["source"] == "hardware" + assert zha_flow["step_id"] == "confirm" @pytest.mark.parametrize( - ("usb_data", "title"), + ("usb_data", "model"), [ (USB_DATA_SKY, "Home Assistant SkyConnect"), (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), ], ) -@patch( - "homeassistant.components.zha.radio_manager.ZhaRadioManager.detect_radio_type", - mock_detect_radio_type(), -) -async def test_option_flow_install_multi_pan_addon_zha( - usb_data: usb.UsbServiceInfo, - title: str, - hass: HomeAssistant, - addon_store_info, - addon_info, - install_addon, - set_addon_options, - start_addon, +async def test_config_flow_zigbee_skip_step_if_installed( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant ) -> None: - """Test installing the multi pan addon when a zha config entry exists.""" - assert await async_setup_component(hass, "usb", {}) - mock_integration(hass, MockModule("hassio")) + """Test the config flow for SkyConnect, skip installing the addon if necessary.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=usb_data + ) - # Setup the config entry - config_entry = MockConfigEntry( - data={ - "device": usb_data.device, - "vid": usb_data.vid, - "pid": usb_data.pid, - "serial_number": usb_data.serial_number, - "manufacturer": usb_data.manufacturer, - "description": usb_data.description, - }, - domain=DOMAIN, + # First step is confirmation, we haven't probed the firmware yet + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "confirm" + assert result["description_placeholders"]["firmware_type"] == "unknown" + assert result["description_placeholders"]["model"] == model + + # Next, we probe the firmware + with patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.probe_silabs_firmware_type", + return_value=ApplicationType.SPINEL, # Ensure we re-install it + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "pick_firmware" + assert result["description_placeholders"]["firmware_type"] == "spinel" + + # Set up Zigbee firmware + mock_flasher_manager = Mock(spec_set=get_zigbee_flasher_addon_manager(hass)) + mock_flasher_manager.async_start_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + mock_flasher_manager.async_uninstall_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.get_zigbee_flasher_addon_manager", + return_value=mock_flasher_manager, + ), + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.is_hassio", + return_value=True, + ), + ): + mock_flasher_manager.addon_name = "Silicon Labs Flasher" + mock_flasher_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={ + "device": "", + "baudrate": 115200, + "bootloader_baudrate": 115200, + "flow_control": True, + }, + state=AddonState.NOT_RUNNING, + update_available=False, + version="1.2.3", + ) + + # Pick the menu option: we skip installation, instead we directly run it + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_ZIGBEE}, + ) + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "run_zigbee_flasher_addon" + assert result["progress_action"] == "run_zigbee_flasher_addon" + assert mock_flasher_manager.async_set_addon_options.mock_calls == [ + call( + { + "device": usb_data.device, + "baudrate": 115200, + "bootloader_baudrate": 115200, + "flow_control": True, + } + ) + ] + + # Uninstall the addon + await hass.async_block_till_done(wait_background_tasks=True) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + # Done + await hass.async_block_till_done(wait_background_tasks=True) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "confirm_zigbee" + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_SKY, "Home Assistant SkyConnect"), + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_config_flow_thread( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test the config flow for SkyConnect.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=usb_data + ) + + # First step is confirmation, we haven't probed the firmware yet + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "confirm" + assert result["description_placeholders"]["firmware_type"] == "unknown" + assert result["description_placeholders"]["model"] == model + + # Next, we probe the firmware + with patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.probe_silabs_firmware_type", + return_value=ApplicationType.EZSP, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "pick_firmware" + assert result["description_placeholders"]["firmware_type"] == "ezsp" + + # Set up Thread firmware + mock_otbr_manager = Mock(spec_set=get_otbr_addon_manager(hass)) + mock_otbr_manager.async_install_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + mock_otbr_manager.async_start_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.get_otbr_addon_manager", + return_value=mock_otbr_manager, + ), + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.is_hassio", + return_value=True, + ), + ): + mock_otbr_manager.addon_name = "OpenThread Border Router" + mock_otbr_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={}, + state=AddonState.NOT_INSTALLED, + update_available=False, + version=None, + ) + + # Pick the menu option + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_THREAD}, + ) + + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["progress_action"] == "install_addon" + assert result["step_id"] == "install_otbr_addon" + + await hass.async_block_till_done(wait_background_tasks=True) + + mock_otbr_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={ + "device": "", + "baudrate": 460800, + "flow_control": True, + "autoflash_firmware": True, + }, + state=AddonState.NOT_RUNNING, + update_available=False, + version="1.2.3", + ) + + # Progress the flow, it is now configuring the addon and running it + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_otbr_addon" + assert result["progress_action"] == "start_otbr_addon" + + assert mock_otbr_manager.async_set_addon_options.mock_calls == [ + call( + { + "device": usb_data.device, + "baudrate": 460800, + "flow_control": True, + "autoflash_firmware": True, + } + ) + ] + + await hass.async_block_till_done(wait_background_tasks=True) + + # The addon is now running + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "confirm_otbr" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + + config_entry = result["result"] + assert config_entry.data == { + "firmware": "spinel", + "device": usb_data.device, + "manufacturer": usb_data.manufacturer, + "pid": usb_data.pid, + "description": usb_data.description, + "product": usb_data.description, + "serial_number": usb_data.serial_number, + "vid": usb_data.vid, + } + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_SKY, "Home Assistant SkyConnect"), + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_config_flow_thread_addon_already_installed( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test the Thread config flow for SkyConnect, addon is already installed.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=usb_data + ) + + with patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.probe_silabs_firmware_type", + return_value=ApplicationType.EZSP, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + mock_otbr_manager = Mock(spec_set=get_otbr_addon_manager(hass)) + mock_otbr_manager.addon_name = "OpenThread Border Router" + mock_otbr_manager.async_install_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + mock_otbr_manager.async_start_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + mock_otbr_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, options={}, - title=title, - unique_id=f"{usb_data.vid}:{usb_data.pid}_{usb_data.serial_number}_{usb_data.manufacturer}_{usb_data.description}", + state=AddonState.NOT_RUNNING, + update_available=False, + version=None, + ) + + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.get_otbr_addon_manager", + return_value=mock_otbr_manager, + ), + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.is_hassio", + return_value=True, + ), + ): + # Pick the menu option + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_THREAD}, + ) + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_otbr_addon" + assert result["progress_action"] == "start_otbr_addon" + + assert mock_otbr_manager.async_set_addon_options.mock_calls == [ + call( + { + "device": usb_data.device, + "baudrate": 460800, + "flow_control": True, + "autoflash_firmware": True, + } + ) + ] + + await hass.async_block_till_done(wait_background_tasks=True) + + # The addon is now running + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "confirm_otbr" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_config_flow_zigbee_not_hassio( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test when the stick is used with a non-hassio setup.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=usb_data + ) + + with patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.probe_silabs_firmware_type", + return_value=ApplicationType.EZSP, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.is_hassio", + return_value=False, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_ZIGBEE}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "confirm_zigbee" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + + config_entry = result["result"] + assert config_entry.data == { + "firmware": "ezsp", + "device": usb_data.device, + "manufacturer": usb_data.manufacturer, + "pid": usb_data.pid, + "description": usb_data.description, + "product": usb_data.description, + "serial_number": usb_data.serial_number, + "vid": usb_data.vid, + } + + # Ensure a ZHA discovery flow has been created + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + zha_flow = flows[0] + assert zha_flow["handler"] == "zha" + assert zha_flow["context"]["source"] == "hardware" + assert zha_flow["step_id"] == "confirm" + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_SKY, "Home Assistant SkyConnect"), + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_options_flow_zigbee_to_thread( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test the options flow for SkyConnect, migrating Zigbee to Thread.""" + config_entry = MockConfigEntry( + domain="homeassistant_sky_connect", + data={ + "firmware": "ezsp", + "device": usb_data.device, + "manufacturer": usb_data.manufacturer, + "pid": usb_data.pid, + "description": usb_data.description, + "product": usb_data.description, + "serial_number": usb_data.serial_number, + "vid": usb_data.vid, + }, + version=1, + minor_version=2, ) config_entry.add_to_hass(hass) - zha_config_entry = MockConfigEntry( - data={"device": {"path": usb_data.device}, "radio_type": "ezsp"}, - domain=ZHA_DOMAIN, - options={}, - title="Yellow", - ) - zha_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) - with patch( - "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", - side_effect=Mock(return_value=True), + # First step is confirmation + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "pick_firmware" + assert result["description_placeholders"]["firmware_type"] == "ezsp" + assert result["description_placeholders"]["model"] == model + + # Pick Thread + mock_otbr_manager = Mock(spec_set=get_otbr_addon_manager(hass)) + mock_otbr_manager.async_install_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + mock_otbr_manager.async_start_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.get_otbr_addon_manager", + return_value=mock_otbr_manager, + ), + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.is_hassio", + return_value=True, + ), + ): + mock_otbr_manager.addon_name = "OpenThread Border Router" + mock_otbr_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={}, + state=AddonState.NOT_INSTALLED, + update_available=False, + version=None, + ) + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_THREAD}, + ) + + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["progress_action"] == "install_addon" + assert result["step_id"] == "install_otbr_addon" + + await hass.async_block_till_done(wait_background_tasks=True) + + mock_otbr_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={ + "device": "", + "baudrate": 460800, + "flow_control": True, + "autoflash_firmware": True, + }, + state=AddonState.NOT_RUNNING, + update_available=False, + version="1.2.3", + ) + + # Progress the flow, it is now configuring the addon and running it + result = await hass.config_entries.options.async_configure(result["flow_id"]) + + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_otbr_addon" + assert result["progress_action"] == "start_otbr_addon" + + assert mock_otbr_manager.async_set_addon_options.mock_calls == [ + call( + { + "device": usb_data.device, + "baudrate": 460800, + "flow_control": True, + "autoflash_firmware": True, + } + ) + ] + + await hass.async_block_till_done(wait_background_tasks=True) + + # The addon is now running + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "confirm_otbr" + + # We are now done + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + + # The firmware type has been updated + assert config_entry.data["firmware"] == "spinel" + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_SKY, "Home Assistant SkyConnect"), + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_options_flow_thread_to_zigbee( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test the options flow for SkyConnect, migrating Thread to Zigbee.""" + config_entry = MockConfigEntry( + domain="homeassistant_sky_connect", + data={ + "firmware": "spinel", + "device": usb_data.device, + "manufacturer": usb_data.manufacturer, + "pid": usb_data.pid, + "description": usb_data.description, + "product": usb_data.description, + "serial_number": usb_data.serial_number, + "vid": usb_data.vid, + }, + version=1, + minor_version=2, + ) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + + # First step is confirmation + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "pick_firmware" + assert result["description_placeholders"]["firmware_type"] == "spinel" + assert result["description_placeholders"]["model"] == model + + # Set up Zigbee firmware + mock_flasher_manager = Mock(spec_set=get_zigbee_flasher_addon_manager(hass)) + mock_flasher_manager.async_install_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + mock_flasher_manager.async_start_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + mock_flasher_manager.async_uninstall_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + + # OTBR is not installed + mock_otbr_manager = Mock(spec_set=get_otbr_addon_manager(hass)) + mock_otbr_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={}, + state=AddonState.NOT_INSTALLED, + update_available=False, + version=None, + ) + + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.get_zigbee_flasher_addon_manager", + return_value=mock_flasher_manager, + ), + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.get_otbr_addon_manager", + return_value=mock_otbr_manager, + ), + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.is_hassio", + return_value=True, + ), + ): + mock_flasher_manager.addon_name = "Silicon Labs Flasher" + mock_flasher_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={}, + state=AddonState.NOT_INSTALLED, + update_available=False, + version=None, + ) + + # Pick the menu option: we are now installing the addon + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_ZIGBEE}, + ) + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["progress_action"] == "install_addon" + assert result["step_id"] == "install_zigbee_flasher_addon" + + await hass.async_block_till_done(wait_background_tasks=True) + + # Progress the flow, we are now configuring the addon and running it + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "run_zigbee_flasher_addon" + assert result["progress_action"] == "run_zigbee_flasher_addon" + assert mock_flasher_manager.async_set_addon_options.mock_calls == [ + call( + { + "device": usb_data.device, + "baudrate": 115200, + "bootloader_baudrate": 115200, + "flow_control": True, + } + ) + ] + + await hass.async_block_till_done(wait_background_tasks=True) + + # Progress the flow, we are now uninstalling the addon + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "uninstall_zigbee_flasher_addon" + assert result["progress_action"] == "uninstall_zigbee_flasher_addon" + + await hass.async_block_till_done(wait_background_tasks=True) + + # We are finally done with the addon + assert mock_flasher_manager.async_uninstall_addon_waiting.mock_calls == [call()] + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "confirm_zigbee" + + # We are now done + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + + # The firmware type has been updated + assert config_entry.data["firmware"] == "ezsp" + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_SKY, "Home Assistant SkyConnect"), + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_options_flow_multipan_uninstall( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test options flow for when multi-PAN firmware is installed.""" + config_entry = MockConfigEntry( + domain="homeassistant_sky_connect", + data={ + "firmware": "cpc", + "device": usb_data.device, + "manufacturer": usb_data.manufacturer, + "pid": usb_data.pid, + "product": usb_data.description, + "serial_number": usb_data.serial_number, + "vid": usb_data.vid, + }, + version=1, + minor_version=2, + ) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + + # Multi-PAN addon is running + mock_multipan_manager = Mock(spec_set=await get_multiprotocol_addon_manager(hass)) + mock_multipan_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={"device": usb_data.device}, + state=AddonState.RUNNING, + update_available=False, + version="1.0.0", + ) + + with ( + patch( + "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.get_multiprotocol_addon_manager", + return_value=mock_multipan_manager, + ), + patch( + "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", + return_value=True, + ), ): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "addon_not_installed" - - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - "enable_multi_pan": True, - }, - ) - assert result["type"] is FlowResultType.SHOW_PROGRESS - assert result["step_id"] == "install_addon" - assert result["progress_action"] == "install_addon" - - await hass.async_block_till_done() - install_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") - - result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] is FlowResultType.SHOW_PROGRESS - assert result["step_id"] == "start_addon" - set_addon_options.assert_called_once_with( - hass, - "core_silabs_multiprotocol", - { - "options": { - "autoflash_firmware": True, - "device": usb_data.device, - "baudrate": "115200", - "flow_control": True, - } - }, - ) - # Check the ZHA config entry data is updated - assert zha_config_entry.data == { - "device": { - "path": "socket://core-silabs-multiprotocol:9999", - "baudrate": 115200, - "flow_control": None, - }, - "radio_type": "ezsp", - } - - await hass.async_block_till_done() - start_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") - - result = await hass.config_entries.options.async_configure(result["flow_id"]) - assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "addon_menu" + assert "uninstall_addon" in result["menu_options"] diff --git a/tests/components/homeassistant_sky_connect/test_config_flow_failures.py b/tests/components/homeassistant_sky_connect/test_config_flow_failures.py new file mode 100644 index 00000000000..128c812272f --- /dev/null +++ b/tests/components/homeassistant_sky_connect/test_config_flow_failures.py @@ -0,0 +1,920 @@ +"""Test the Home Assistant SkyConnect config flow failure cases.""" + +from unittest.mock import AsyncMock, Mock, patch + +import pytest +from universal_silabs_flasher.const import ApplicationType + +from homeassistant.components import usb +from homeassistant.components.hassio.addon_manager import ( + AddonError, + AddonInfo, + AddonState, +) +from homeassistant.components.homeassistant_sky_connect.config_flow import ( + STEP_PICK_FIRMWARE_THREAD, + STEP_PICK_FIRMWARE_ZIGBEE, +) +from homeassistant.components.homeassistant_sky_connect.const import DOMAIN +from homeassistant.components.homeassistant_sky_connect.util import ( + get_otbr_addon_manager, + get_zigbee_flasher_addon_manager, +) +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from .test_config_flow import USB_DATA_ZBT1, delayed_side_effect + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_config_flow_cannot_probe_firmware( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test failure case when firmware cannot be probed.""" + + with patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.probe_silabs_firmware_type", + return_value=None, + ): + # Start the flow + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=usb_data + ) + + # Probing fails + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "unsupported_firmware" + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_config_flow_zigbee_not_hassio_wrong_firmware( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test when the stick is used with a non-hassio setup but the firmware is bad.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=usb_data + ) + + with patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.probe_silabs_firmware_type", + return_value=ApplicationType.SPINEL, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.is_hassio", + return_value=False, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_ZIGBEE}, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "not_hassio" + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_config_flow_zigbee_flasher_addon_already_running( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test failure case when flasher addon is already running.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=usb_data + ) + + with patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.probe_silabs_firmware_type", + return_value=ApplicationType.SPINEL, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + mock_flasher_manager = Mock(spec_set=get_zigbee_flasher_addon_manager(hass)) + mock_flasher_manager.addon_name = "Silicon Labs Flasher" + mock_flasher_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={}, + state=AddonState.RUNNING, + update_available=False, + version="1.0.0", + ) + + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.get_zigbee_flasher_addon_manager", + return_value=mock_flasher_manager, + ), + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.is_hassio", + return_value=True, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_ZIGBEE}, + ) + + # Cannot get addon info + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "addon_already_running" + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_config_flow_zigbee_flasher_addon_info_fails( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test failure case when flasher addon cannot be installed.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=usb_data + ) + + with patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.probe_silabs_firmware_type", + return_value=ApplicationType.SPINEL, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + mock_flasher_manager = Mock(spec_set=get_zigbee_flasher_addon_manager(hass)) + mock_flasher_manager.addon_name = "Silicon Labs Flasher" + mock_flasher_manager.async_get_addon_info.side_effect = AddonError() + + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.get_zigbee_flasher_addon_manager", + return_value=mock_flasher_manager, + ), + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.is_hassio", + return_value=True, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_ZIGBEE}, + ) + + # Cannot get addon info + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "addon_info_failed" + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_config_flow_zigbee_flasher_addon_install_fails( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test failure case when flasher addon cannot be installed.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=usb_data + ) + + with patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.probe_silabs_firmware_type", + return_value=ApplicationType.SPINEL, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + mock_flasher_manager = Mock(spec_set=get_zigbee_flasher_addon_manager(hass)) + mock_flasher_manager.addon_name = "Silicon Labs Flasher" + mock_flasher_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={}, + state=AddonState.NOT_INSTALLED, + update_available=False, + version=None, + ) + mock_flasher_manager.async_install_addon_waiting = AsyncMock( + side_effect=AddonError() + ) + + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.get_zigbee_flasher_addon_manager", + return_value=mock_flasher_manager, + ), + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.is_hassio", + return_value=True, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_ZIGBEE}, + ) + + # Cannot install addon + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "addon_install_failed" + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_config_flow_zigbee_flasher_addon_set_config_fails( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test failure case when flasher addon cannot be configured.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=usb_data + ) + + with patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.probe_silabs_firmware_type", + return_value=ApplicationType.SPINEL, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + mock_flasher_manager = Mock(spec_set=get_zigbee_flasher_addon_manager(hass)) + mock_flasher_manager.addon_name = "Silicon Labs Flasher" + mock_flasher_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={}, + state=AddonState.NOT_INSTALLED, + update_available=False, + version=None, + ) + mock_flasher_manager.async_install_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + mock_flasher_manager.async_set_addon_options = AsyncMock(side_effect=AddonError()) + + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.get_zigbee_flasher_addon_manager", + return_value=mock_flasher_manager, + ), + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.is_hassio", + return_value=True, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_ZIGBEE}, + ) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + await hass.async_block_till_done(wait_background_tasks=True) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "addon_set_config_failed" + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_config_flow_zigbee_flasher_run_fails( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test failure case when flasher addon fails to run.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=usb_data + ) + + with patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.probe_silabs_firmware_type", + return_value=ApplicationType.SPINEL, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + mock_flasher_manager = Mock(spec_set=get_zigbee_flasher_addon_manager(hass)) + mock_flasher_manager.addon_name = "Silicon Labs Flasher" + mock_flasher_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={}, + state=AddonState.NOT_INSTALLED, + update_available=False, + version=None, + ) + mock_flasher_manager.async_install_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + mock_flasher_manager.async_start_addon_waiting = AsyncMock(side_effect=AddonError()) + + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.get_zigbee_flasher_addon_manager", + return_value=mock_flasher_manager, + ), + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.is_hassio", + return_value=True, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_ZIGBEE}, + ) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + await hass.async_block_till_done(wait_background_tasks=True) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "addon_start_failed" + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_config_flow_zigbee_flasher_uninstall_fails( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test failure case when flasher addon uninstall fails.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=usb_data + ) + + with patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.probe_silabs_firmware_type", + return_value=ApplicationType.SPINEL, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + mock_flasher_manager = Mock(spec_set=get_zigbee_flasher_addon_manager(hass)) + mock_flasher_manager.addon_name = "Silicon Labs Flasher" + mock_flasher_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={}, + state=AddonState.NOT_INSTALLED, + update_available=False, + version=None, + ) + mock_flasher_manager.async_install_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + mock_flasher_manager.async_start_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + mock_flasher_manager.async_uninstall_addon_waiting = AsyncMock( + side_effect=AddonError() + ) + + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.get_zigbee_flasher_addon_manager", + return_value=mock_flasher_manager, + ), + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.is_hassio", + return_value=True, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_ZIGBEE}, + ) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + await hass.async_block_till_done(wait_background_tasks=True) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + await hass.async_block_till_done(wait_background_tasks=True) + + # Uninstall failure isn't critical + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "confirm_zigbee" + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_config_flow_thread_not_hassio( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test when the stick is used with a non-hassio setup and Thread is selected.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=usb_data + ) + + with patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.probe_silabs_firmware_type", + return_value=ApplicationType.EZSP, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.is_hassio", + return_value=False, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_THREAD}, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "not_hassio_thread" + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_config_flow_thread_addon_info_fails( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test failure case when flasher addon cannot be installed.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=usb_data + ) + + with patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.probe_silabs_firmware_type", + return_value=ApplicationType.EZSP, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + mock_otbr_manager = Mock(spec_set=get_otbr_addon_manager(hass)) + mock_otbr_manager.addon_name = "OpenThread Border Router" + mock_otbr_manager.async_get_addon_info.side_effect = AddonError() + + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.get_otbr_addon_manager", + return_value=mock_otbr_manager, + ), + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.is_hassio", + return_value=True, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_THREAD}, + ) + + # Cannot get addon info + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "addon_info_failed" + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_config_flow_thread_addon_already_running( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test failure case when the Thread addon is already running.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=usb_data + ) + + with patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.probe_silabs_firmware_type", + return_value=ApplicationType.EZSP, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + mock_otbr_manager = Mock(spec_set=get_otbr_addon_manager(hass)) + mock_otbr_manager.addon_name = "OpenThread Border Router" + mock_otbr_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={}, + state=AddonState.RUNNING, + update_available=False, + version="1.0.0", + ) + mock_otbr_manager.async_install_addon_waiting = AsyncMock(side_effect=AddonError()) + + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.get_otbr_addon_manager", + return_value=mock_otbr_manager, + ), + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.is_hassio", + return_value=True, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_THREAD}, + ) + + # Cannot install addon + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "otbr_addon_already_running" + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_config_flow_thread_addon_install_fails( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test failure case when flasher addon cannot be installed.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=usb_data + ) + + with patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.probe_silabs_firmware_type", + return_value=ApplicationType.EZSP, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + mock_otbr_manager = Mock(spec_set=get_otbr_addon_manager(hass)) + mock_otbr_manager.addon_name = "OpenThread Border Router" + mock_otbr_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={}, + state=AddonState.NOT_INSTALLED, + update_available=False, + version=None, + ) + mock_otbr_manager.async_install_addon_waiting = AsyncMock(side_effect=AddonError()) + + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.get_otbr_addon_manager", + return_value=mock_otbr_manager, + ), + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.is_hassio", + return_value=True, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_THREAD}, + ) + + # Cannot install addon + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "addon_install_failed" + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_config_flow_thread_addon_set_config_fails( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test failure case when flasher addon cannot be configured.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=usb_data + ) + + with patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.probe_silabs_firmware_type", + return_value=ApplicationType.EZSP, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + mock_otbr_manager = Mock(spec_set=get_otbr_addon_manager(hass)) + mock_otbr_manager.addon_name = "OpenThread Border Router" + mock_otbr_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={}, + state=AddonState.NOT_INSTALLED, + update_available=False, + version=None, + ) + mock_otbr_manager.async_install_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + mock_otbr_manager.async_set_addon_options = AsyncMock(side_effect=AddonError()) + + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.get_otbr_addon_manager", + return_value=mock_otbr_manager, + ), + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.is_hassio", + return_value=True, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_THREAD}, + ) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + await hass.async_block_till_done(wait_background_tasks=True) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "addon_set_config_failed" + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_config_flow_thread_flasher_run_fails( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test failure case when flasher addon fails to run.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=usb_data + ) + + with patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.probe_silabs_firmware_type", + return_value=ApplicationType.EZSP, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + mock_otbr_manager = Mock(spec_set=get_otbr_addon_manager(hass)) + mock_otbr_manager.addon_name = "OpenThread Border Router" + mock_otbr_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={}, + state=AddonState.NOT_INSTALLED, + update_available=False, + version=None, + ) + mock_otbr_manager.async_install_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + mock_otbr_manager.async_start_addon_waiting = AsyncMock(side_effect=AddonError()) + + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.get_otbr_addon_manager", + return_value=mock_otbr_manager, + ), + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.is_hassio", + return_value=True, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_THREAD}, + ) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + await hass.async_block_till_done(wait_background_tasks=True) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "addon_start_failed" + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_config_flow_thread_flasher_uninstall_fails( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test failure case when flasher addon uninstall fails.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=usb_data + ) + + with patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.probe_silabs_firmware_type", + return_value=ApplicationType.EZSP, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + mock_otbr_manager = Mock(spec_set=get_otbr_addon_manager(hass)) + mock_otbr_manager.addon_name = "OpenThread Border Router" + mock_otbr_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={}, + state=AddonState.NOT_INSTALLED, + update_available=False, + version=None, + ) + mock_otbr_manager.async_install_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + mock_otbr_manager.async_start_addon_waiting = AsyncMock( + side_effect=delayed_side_effect() + ) + mock_otbr_manager.async_uninstall_addon_waiting = AsyncMock( + side_effect=AddonError() + ) + + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.get_otbr_addon_manager", + return_value=mock_otbr_manager, + ), + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.is_hassio", + return_value=True, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_THREAD}, + ) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + await hass.async_block_till_done(wait_background_tasks=True) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + await hass.async_block_till_done(wait_background_tasks=True) + + # Uninstall failure isn't critical + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "confirm_otbr" + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_options_flow_zigbee_to_thread_zha_configured( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test the options flow migration failure, ZHA using the stick.""" + config_entry = MockConfigEntry( + domain="homeassistant_sky_connect", + data={ + "firmware": "ezsp", + "device": usb_data.device, + "manufacturer": usb_data.manufacturer, + "pid": usb_data.pid, + "description": usb_data.description, + "product": usb_data.description, + "serial_number": usb_data.serial_number, + "vid": usb_data.vid, + }, + version=1, + minor_version=2, + ) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + + # Set up ZHA as well + zha_config_entry = MockConfigEntry( + domain="zha", + data={"device": {"path": usb_data.device}}, + ) + zha_config_entry.add_to_hass(hass) + + # Confirm options flow + result = await hass.config_entries.options.async_init(config_entry.entry_id) + + # Pick Thread + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_THREAD}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "zha_still_using_stick" + + +@pytest.mark.parametrize( + ("usb_data", "model"), + [ + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_options_flow_thread_to_zigbee_otbr_configured( + usb_data: usb.UsbServiceInfo, model: str, hass: HomeAssistant +) -> None: + """Test the options flow migration failure, OTBR still using the stick.""" + config_entry = MockConfigEntry( + domain="homeassistant_sky_connect", + data={ + "firmware": "spinel", + "device": usb_data.device, + "manufacturer": usb_data.manufacturer, + "pid": usb_data.pid, + "description": usb_data.description, + "product": usb_data.description, + "serial_number": usb_data.serial_number, + "vid": usb_data.vid, + }, + version=1, + minor_version=2, + ) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + + # Confirm options flow + result = await hass.config_entries.options.async_init(config_entry.entry_id) + + # Pick Zigbee + mock_otbr_manager = Mock(spec_set=get_otbr_addon_manager(hass)) + mock_otbr_manager.addon_name = "OpenThread Border Router" + mock_otbr_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={"device": usb_data.device}, + state=AddonState.RUNNING, + update_available=False, + version="1.0.0", + ) + + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.get_otbr_addon_manager", + return_value=mock_otbr_manager, + ), + patch( + "homeassistant.components.homeassistant_sky_connect.config_flow.is_hassio", + return_value=True, + ), + ): + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"next_step_id": STEP_PICK_FIRMWARE_ZIGBEE}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "otbr_still_using_stick" diff --git a/tests/components/homeassistant_sky_connect/test_hardware.py b/tests/components/homeassistant_sky_connect/test_hardware.py index 6b283378045..888ed27a3c0 100644 --- a/tests/components/homeassistant_sky_connect/test_hardware.py +++ b/tests/components/homeassistant_sky_connect/test_hardware.py @@ -1,7 +1,5 @@ """Test the Home Assistant SkyConnect hardware platform.""" -from unittest.mock import patch - from homeassistant.components.homeassistant_sky_connect.const import DOMAIN from homeassistant.core import EVENT_HOMEASSISTANT_STARTED, HomeAssistant from homeassistant.setup import async_setup_component @@ -15,7 +13,8 @@ CONFIG_ENTRY_DATA = { "pid": "EA60", "serial_number": "9e2adbd75b8beb119fe564a0f320645d", "manufacturer": "Nabu Casa", - "description": "SkyConnect v1.0", + "product": "SkyConnect v1.0", + "firmware": "ezsp", } CONFIG_ENTRY_DATA_2 = { @@ -24,7 +23,8 @@ CONFIG_ENTRY_DATA_2 = { "pid": "EA60", "serial_number": "9e2adbd75b8beb119fe564a0f320645d", "manufacturer": "Nabu Casa", - "description": "Home Assistant Connect ZBT-1", + "product": "Home Assistant Connect ZBT-1", + "firmware": "ezsp", } @@ -42,22 +42,24 @@ async def test_hardware_info( options={}, title="Home Assistant SkyConnect", unique_id="unique_1", + version=1, + minor_version=2, ) config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + config_entry_2 = MockConfigEntry( data=CONFIG_ENTRY_DATA_2, domain=DOMAIN, options={}, title="Home Assistant Connect ZBT-1", unique_id="unique_2", + version=1, + minor_version=2, ) config_entry_2.add_to_hass(hass) - with patch( - "homeassistant.components.homeassistant_sky_connect.usb.async_is_plugged_in", - return_value=True, - ): - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + + assert await hass.config_entries.async_setup(config_entry_2.entry_id) client = await hass_ws_client(hass) diff --git a/tests/components/homeassistant_sky_connect/test_init.py b/tests/components/homeassistant_sky_connect/test_init.py index a6dd5100d7e..88b57f2dd64 100644 --- a/tests/components/homeassistant_sky_connect/test_init.py +++ b/tests/components/homeassistant_sky_connect/test_init.py @@ -1,377 +1,56 @@ """Test the Home Assistant SkyConnect integration.""" -from collections.abc import Generator -from typing import Any -from unittest.mock import MagicMock, Mock, patch +from unittest.mock import patch -import pytest +from universal_silabs_flasher.const import ApplicationType -from homeassistant.components import zha -from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.homeassistant_sky_connect.const import DOMAIN -from homeassistant.config_entries import ConfigEntryState -from homeassistant.core import EVENT_HOMEASSISTANT_STARTED, HomeAssistant -from homeassistant.setup import async_setup_component +from homeassistant.components.homeassistant_sky_connect.util import FirmwareGuess +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry -CONFIG_ENTRY_DATA = { - "device": "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_9e2adbd75b8beb119fe564a0f320645d-if00-port0", - "vid": "10C4", - "pid": "EA60", - "serial_number": "3c0ed67c628beb11b1cd64a0f320645d", - "manufacturer": "Nabu Casa", - "description": "SkyConnect v1.0", -} +async def test_config_entry_migration_v2(hass: HomeAssistant) -> None: + """Test migrating config entries from v1 to v2 format.""" -@pytest.fixture(autouse=True) -def disable_usb_probing() -> Generator[None, None, None]: - """Disallow touching of system USB devices during unit tests.""" - with patch("homeassistant.components.usb.comports", return_value=[]): - yield - - -@pytest.fixture -def mock_zha_config_flow_setup() -> Generator[None, None, None]: - """Mock the radio connection and probing of the ZHA config flow.""" - - def mock_probe(config: dict[str, Any]) -> None: - # The radio probing will return the correct baudrate - return {**config, "baudrate": 115200} - - mock_connect_app = MagicMock() - mock_connect_app.__aenter__.return_value.backups.backups = [] - - with ( - patch( - "bellows.zigbee.application.ControllerApplication.probe", - side_effect=mock_probe, - ), - patch( - "homeassistant.components.zha.radio_manager.ZhaRadioManager.connect_zigpy_app", - return_value=mock_connect_app, - ), - ): - yield - - -@pytest.mark.parametrize( - ("onboarded", "num_entries", "num_flows"), [(False, 1, 0), (True, 0, 1)] -) -async def test_setup_entry( - mock_zha_config_flow_setup, - hass: HomeAssistant, - addon_store_info, - onboarded, - num_entries, - num_flows, -) -> None: - """Test setup of a config entry, including setup of zha.""" - assert await async_setup_component(hass, "usb", {}) - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - - # Setup the config entry config_entry = MockConfigEntry( - data=CONFIG_ENTRY_DATA, domain=DOMAIN, - options={}, - title="Home Assistant SkyConnect", - ) - config_entry.add_to_hass(hass) - with ( - patch( - "homeassistant.components.homeassistant_sky_connect.usb.async_is_plugged_in", - return_value=True, - ) as mock_is_plugged_in, - patch( - "homeassistant.components.onboarding.async_is_onboarded", - return_value=onboarded, - ), - ): - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - assert len(mock_is_plugged_in.mock_calls) == 1 - - matcher = mock_is_plugged_in.mock_calls[0].args[1] - assert matcher["vid"].isupper() - assert matcher["pid"].isupper() - assert matcher["serial_number"].islower() - assert matcher["manufacturer"].islower() - assert matcher["description"].islower() - - # Finish setting up ZHA - if num_entries > 0: - zha_flows = hass.config_entries.flow.async_progress_by_handler("zha") - assert len(zha_flows) == 1 - assert zha_flows[0]["step_id"] == "choose_formation_strategy" - - await hass.config_entries.flow.async_configure( - zha_flows[0]["flow_id"], - user_input={"next_step_id": zha.config_flow.FORMATION_REUSE_SETTINGS}, - ) - await hass.async_block_till_done() - - assert len(hass.config_entries.flow.async_progress_by_handler("zha")) == num_flows - assert len(hass.config_entries.async_entries("zha")) == num_entries - - -async def test_setup_zha( - mock_zha_config_flow_setup, hass: HomeAssistant, addon_store_info -) -> None: - """Test zha gets the right config.""" - assert await async_setup_component(hass, "usb", {}) - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - - # Setup the config entry - config_entry = MockConfigEntry( - data=CONFIG_ENTRY_DATA, - domain=DOMAIN, - options={}, - title="Home Assistant SkyConnect", - ) - config_entry.add_to_hass(hass) - with ( - patch( - "homeassistant.components.homeassistant_sky_connect.usb.async_is_plugged_in", - return_value=True, - ) as mock_is_plugged_in, - patch( - "homeassistant.components.onboarding.async_is_onboarded", return_value=False - ), - ): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - assert len(mock_is_plugged_in.mock_calls) == 1 - - zha_flows = hass.config_entries.flow.async_progress_by_handler("zha") - assert len(zha_flows) == 1 - assert zha_flows[0]["step_id"] == "choose_formation_strategy" - - # Finish setting up ZHA - await hass.config_entries.flow.async_configure( - zha_flows[0]["flow_id"], - user_input={"next_step_id": zha.config_flow.FORMATION_REUSE_SETTINGS}, - ) - await hass.async_block_till_done() - - config_entry = hass.config_entries.async_entries("zha")[0] - assert config_entry.data == { - "device": { - "baudrate": 115200, - "flow_control": None, - "path": CONFIG_ENTRY_DATA["device"], + unique_id="some_unique_id", + data={ + "device": "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_9e2adbd75b8beb119fe564a0f320645d-if00-port0", + "vid": "10C4", + "pid": "EA60", + "serial_number": "3c0ed67c628beb11b1cd64a0f320645d", + "manufacturer": "Nabu Casa", + "description": "SkyConnect v1.0", }, - "radio_type": "ezsp", - } - assert config_entry.options == {} - assert config_entry.title == CONFIG_ENTRY_DATA["description"] - - -async def test_setup_zha_multipan( - hass: HomeAssistant, addon_info, addon_running -) -> None: - """Test zha gets the right config.""" - assert await async_setup_component(hass, "usb", {}) - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - - addon_info.return_value["options"]["device"] = CONFIG_ENTRY_DATA["device"] - - # Setup the config entry - config_entry = MockConfigEntry( - data=CONFIG_ENTRY_DATA, - domain=DOMAIN, - options={}, - title="Home Assistant SkyConnect", + version=1, ) + config_entry.add_to_hass(hass) - with ( - patch( - "homeassistant.components.homeassistant_sky_connect.usb.async_is_plugged_in", - return_value=True, - ) as mock_is_plugged_in, - patch( - "homeassistant.components.onboarding.async_is_onboarded", return_value=False - ), - patch( - "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", - side_effect=Mock(return_value=True), - ), - ): - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - assert len(mock_is_plugged_in.mock_calls) == 1 - # Finish setting up ZHA - zha_flows = hass.config_entries.flow.async_progress_by_handler("zha") - assert len(zha_flows) == 1 - assert zha_flows[0]["step_id"] == "choose_formation_strategy" - - await hass.config_entries.flow.async_configure( - zha_flows[0]["flow_id"], - user_input={"next_step_id": zha.config_flow.FORMATION_REUSE_SETTINGS}, - ) - await hass.async_block_till_done() - - config_entry = hass.config_entries.async_entries("zha")[0] - assert config_entry.data == { - "device": { - "baudrate": 115200, - "flow_control": None, - "path": "socket://core-silabs-multiprotocol:9999", - }, - "radio_type": "ezsp", - } - assert config_entry.options == {} - assert config_entry.title == "SkyConnect Multiprotocol" - - -async def test_setup_zha_multipan_other_device( - mock_zha_config_flow_setup, hass: HomeAssistant, addon_info, addon_running -) -> None: - """Test zha gets the right config.""" - assert await async_setup_component(hass, "usb", {}) - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - - addon_info.return_value["options"]["device"] = "/dev/not_our_sky_connect" - - # Setup the config entry - config_entry = MockConfigEntry( - data=CONFIG_ENTRY_DATA, - domain=DOMAIN, - options={}, - title="Home Assistant Yellow", - ) - config_entry.add_to_hass(hass) - with ( - patch( - "homeassistant.components.homeassistant_sky_connect.usb.async_is_plugged_in", - return_value=True, - ) as mock_is_plugged_in, - patch( - "homeassistant.components.onboarding.async_is_onboarded", return_value=False - ), - patch( - "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", - side_effect=Mock(return_value=True), - ), - ): - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - assert len(mock_is_plugged_in.mock_calls) == 1 - - # Finish setting up ZHA - zha_flows = hass.config_entries.flow.async_progress_by_handler("zha") - assert len(zha_flows) == 1 - assert zha_flows[0]["step_id"] == "choose_formation_strategy" - - await hass.config_entries.flow.async_configure( - zha_flows[0]["flow_id"], - user_input={"next_step_id": zha.config_flow.FORMATION_REUSE_SETTINGS}, - ) - await hass.async_block_till_done() - - config_entry = hass.config_entries.async_entries("zha")[0] - assert config_entry.data == { - "device": { - "baudrate": 115200, - "flow_control": None, - "path": CONFIG_ENTRY_DATA["device"], - }, - "radio_type": "ezsp", - } - assert config_entry.options == {} - assert config_entry.title == CONFIG_ENTRY_DATA["description"] - - -async def test_setup_entry_wait_usb(hass: HomeAssistant) -> None: - """Test setup of a config entry when the dongle is not plugged in.""" - # Setup the config entry - config_entry = MockConfigEntry( - data=CONFIG_ENTRY_DATA, - domain=DOMAIN, - options={}, - title="Home Assistant SkyConnect", - ) - config_entry.add_to_hass(hass) with patch( - "homeassistant.components.homeassistant_sky_connect.usb.async_is_plugged_in", - return_value=False, - ) as mock_is_plugged_in: + "homeassistant.components.homeassistant_sky_connect.guess_firmware_type", + return_value=FirmwareGuess( + is_running=True, + firmware_type=ApplicationType.SPINEL, + source="otbr", + ), + ): await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state is ConfigEntryState.LOADED - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - # USB discovery starts, config entry should be removed - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await hass.async_block_till_done() - assert len(mock_is_plugged_in.mock_calls) == 1 - assert len(hass.config_entries.async_entries(DOMAIN)) == 0 + assert config_entry.version == 1 + assert config_entry.minor_version == 2 + assert config_entry.data == { + "description": "SkyConnect v1.0", + "device": "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_9e2adbd75b8beb119fe564a0f320645d-if00-port0", + "vid": "10C4", + "pid": "EA60", + "serial_number": "3c0ed67c628beb11b1cd64a0f320645d", + "manufacturer": "Nabu Casa", + "product": "SkyConnect v1.0", # `description` has been copied to `product` + "firmware": "spinel", # new key + } -async def test_setup_entry_addon_info_fails( - hass: HomeAssistant, addon_store_info -) -> None: - """Test setup of a config entry when fetching addon info fails.""" - assert await async_setup_component(hass, "usb", {}) - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - - addon_store_info.side_effect = HassioAPIError("Boom") - - # Setup the config entry - config_entry = MockConfigEntry( - data=CONFIG_ENTRY_DATA, - domain=DOMAIN, - options={}, - title="Home Assistant SkyConnect", - ) - config_entry.add_to_hass(hass) - with ( - patch( - "homeassistant.components.homeassistant_sky_connect.usb.async_is_plugged_in", - return_value=True, - ), - patch( - "homeassistant.components.onboarding.async_is_onboarded", return_value=False - ), - patch( - "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", - side_effect=Mock(return_value=True), - ), - ): - assert not 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_setup_entry_addon_not_running( - hass: HomeAssistant, addon_installed, start_addon -) -> None: - """Test the addon is started if it is not running.""" - assert await async_setup_component(hass, "usb", {}) - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - - # Setup the config entry - config_entry = MockConfigEntry( - data=CONFIG_ENTRY_DATA, - domain=DOMAIN, - options={}, - title="Home Assistant SkyConnect", - ) - config_entry.add_to_hass(hass) - with ( - patch( - "homeassistant.components.homeassistant_sky_connect.usb.async_is_plugged_in", - return_value=True, - ), - patch( - "homeassistant.components.onboarding.async_is_onboarded", return_value=False - ), - patch( - "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", - side_effect=Mock(return_value=True), - ), - ): - assert not await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - assert config_entry.state is ConfigEntryState.SETUP_RETRY - start_addon.assert_called_once() + await hass.config_entries.async_unload(config_entry.entry_id) diff --git a/tests/components/homeassistant_sky_connect/test_util.py b/tests/components/homeassistant_sky_connect/test_util.py new file mode 100644 index 00000000000..12ba352eb16 --- /dev/null +++ b/tests/components/homeassistant_sky_connect/test_util.py @@ -0,0 +1,203 @@ +"""Test SkyConnect utilities.""" + +from unittest.mock import AsyncMock, patch + +from universal_silabs_flasher.const import ApplicationType + +from homeassistant.components.hassio import AddonError, AddonInfo, AddonState +from homeassistant.components.homeassistant_sky_connect.const import ( + DOMAIN, + HardwareVariant, +) +from homeassistant.components.homeassistant_sky_connect.util import ( + FirmwareGuess, + get_hardware_variant, + get_usb_service_info, + get_zha_device_path, + guess_firmware_type, +) +from homeassistant.components.usb import UsbServiceInfo +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +SKYCONNECT_CONFIG_ENTRY = MockConfigEntry( + domain=DOMAIN, + unique_id="some_unique_id", + data={ + "device": "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_9e2adbd75b8beb119fe564a0f320645d-if00-port0", + "vid": "10C4", + "pid": "EA60", + "serial_number": "3c0ed67c628beb11b1cd64a0f320645d", + "manufacturer": "Nabu Casa", + "product": "SkyConnect v1.0", + "firmware": "ezsp", + }, + version=2, +) + +CONNECT_ZBT1_CONFIG_ENTRY = MockConfigEntry( + domain=DOMAIN, + unique_id="some_unique_id", + data={ + "device": "/dev/serial/by-id/usb-Nabu_Casa_Home_Assistant_Connect_ZBT-1_9e2adbd75b8beb119fe564a0f320645d-if00-port0", + "vid": "10C4", + "pid": "EA60", + "serial_number": "3c0ed67c628beb11b1cd64a0f320645d", + "manufacturer": "Nabu Casa", + "product": "Home Assistant Connect ZBT-1", + "firmware": "ezsp", + }, + version=2, +) + +ZHA_CONFIG_ENTRY = MockConfigEntry( + domain="zha", + unique_id="some_unique_id", + data={ + "device": { + "path": "/dev/serial/by-id/usb-Nabu_Casa_Home_Assistant_Connect_ZBT-1_3c0ed67c628beb11b1cd64a0f320645d-if00-port0", + "baudrate": 115200, + "flow_control": None, + }, + "radio_type": "ezsp", + }, + version=4, +) + + +def test_get_usb_service_info() -> None: + """Test `get_usb_service_info` conversion.""" + assert get_usb_service_info(SKYCONNECT_CONFIG_ENTRY) == UsbServiceInfo( + device=SKYCONNECT_CONFIG_ENTRY.data["device"], + vid=SKYCONNECT_CONFIG_ENTRY.data["vid"], + pid=SKYCONNECT_CONFIG_ENTRY.data["pid"], + serial_number=SKYCONNECT_CONFIG_ENTRY.data["serial_number"], + manufacturer=SKYCONNECT_CONFIG_ENTRY.data["manufacturer"], + description=SKYCONNECT_CONFIG_ENTRY.data["product"], + ) + + +def test_get_hardware_variant() -> None: + """Test `get_hardware_variant` extraction.""" + assert get_hardware_variant(SKYCONNECT_CONFIG_ENTRY) == HardwareVariant.SKYCONNECT + assert ( + get_hardware_variant(CONNECT_ZBT1_CONFIG_ENTRY) == HardwareVariant.CONNECT_ZBT1 + ) + + +def test_get_zha_device_path() -> None: + """Test extracting the ZHA device path from its config entry.""" + assert ( + get_zha_device_path(ZHA_CONFIG_ENTRY) == ZHA_CONFIG_ENTRY.data["device"]["path"] + ) + + +async def test_guess_firmware_type_unknown(hass: HomeAssistant) -> None: + """Test guessing the firmware type.""" + + assert (await guess_firmware_type(hass, "/dev/missing")) == FirmwareGuess( + is_running=False, firmware_type=ApplicationType.EZSP, source="unknown" + ) + + +async def test_guess_firmware_type(hass: HomeAssistant) -> None: + """Test guessing the firmware.""" + path = ZHA_CONFIG_ENTRY.data["device"]["path"] + + ZHA_CONFIG_ENTRY.add_to_hass(hass) + + ZHA_CONFIG_ENTRY.mock_state(hass, ConfigEntryState.NOT_LOADED) + assert (await guess_firmware_type(hass, path)) == FirmwareGuess( + is_running=False, firmware_type=ApplicationType.EZSP, source="zha" + ) + + # When ZHA is running, we indicate as such when guessing + ZHA_CONFIG_ENTRY.mock_state(hass, ConfigEntryState.LOADED) + assert (await guess_firmware_type(hass, path)) == FirmwareGuess( + is_running=True, firmware_type=ApplicationType.EZSP, source="zha" + ) + + mock_otbr_addon_manager = AsyncMock() + mock_multipan_addon_manager = AsyncMock() + + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.util.is_hassio", + return_value=True, + ), + patch( + "homeassistant.components.homeassistant_sky_connect.util.get_otbr_addon_manager", + return_value=mock_otbr_addon_manager, + ), + patch( + "homeassistant.components.homeassistant_sky_connect.util.get_multiprotocol_addon_manager", + return_value=mock_multipan_addon_manager, + ), + ): + mock_otbr_addon_manager.async_get_addon_info.side_effect = AddonError() + mock_multipan_addon_manager.async_get_addon_info.side_effect = AddonError() + + # Hassio errors are ignored and we still go with ZHA + assert (await guess_firmware_type(hass, path)) == FirmwareGuess( + is_running=True, firmware_type=ApplicationType.EZSP, source="zha" + ) + + mock_otbr_addon_manager.async_get_addon_info.side_effect = None + mock_otbr_addon_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={"device": "/some/other/device"}, + state=AddonState.RUNNING, + update_available=False, + version="1.0.0", + ) + + # We will prefer ZHA, as it is running (and actually pointing to the device) + assert (await guess_firmware_type(hass, path)) == FirmwareGuess( + is_running=True, firmware_type=ApplicationType.EZSP, source="zha" + ) + + mock_otbr_addon_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={"device": path}, + state=AddonState.NOT_RUNNING, + update_available=False, + version="1.0.0", + ) + + # We will still prefer ZHA, as it is the one actually running + assert (await guess_firmware_type(hass, path)) == FirmwareGuess( + is_running=True, firmware_type=ApplicationType.EZSP, source="zha" + ) + + mock_otbr_addon_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={"device": path}, + state=AddonState.RUNNING, + update_available=False, + version="1.0.0", + ) + + # Finally, ZHA loses out to OTBR + assert (await guess_firmware_type(hass, path)) == FirmwareGuess( + is_running=True, firmware_type=ApplicationType.SPINEL, source="otbr" + ) + + mock_multipan_addon_manager.async_get_addon_info.side_effect = None + mock_multipan_addon_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={"device": path}, + state=AddonState.RUNNING, + update_available=False, + version="1.0.0", + ) + + # Which will lose out to multi-PAN + assert (await guess_firmware_type(hass, path)) == FirmwareGuess( + is_running=True, firmware_type=ApplicationType.CPC, source="multiprotocol" + ) diff --git a/tests/components/usb/__init__.py b/tests/components/usb/__init__.py index f5f32336931..96d671d0958 100644 --- a/tests/components/usb/__init__.py +++ b/tests/components/usb/__init__.py @@ -26,3 +26,19 @@ electro_lama_device = USBDevice( manufacturer=None, description="USB2.0-Serial", ) +skyconnect_macos_correct = USBDevice( + device="/dev/cu.SLAB_USBtoUART", + vid="10C4", + pid="EA60", + serial_number="9ab1da1ea4b3ed11956f4eaca7669f5d", + manufacturer="Nabu Casa", + description="SkyConnect v1.0", +) +skyconnect_macos_incorrect = USBDevice( + device="/dev/cu.usbserial-2110", + vid="10C4", + pid="EA60", + serial_number="9ab1da1ea4b3ed11956f4eaca7669f5d", + manufacturer="Nabu Casa", + description="SkyConnect v1.0", +) diff --git a/tests/components/zha/test_repairs.py b/tests/components/zha/test_repairs.py index 5e128cc464a..5b57ec7fcc2 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 import ( +from homeassistant.components.homeassistant_sky_connect.const import ( DOMAIN as SKYCONNECT_DOMAIN, ) from homeassistant.components.repairs import DOMAIN as REPAIRS_DOMAIN @@ -59,8 +59,10 @@ def test_detect_radio_hardware(hass: HomeAssistant) -> None: "pid": "EA60", "serial_number": "3c0ed67c628beb11b1cd64a0f320645d", "manufacturer": "Nabu Casa", - "description": "SkyConnect v1.0", + "product": "SkyConnect v1.0", + "firmware": "ezsp", }, + version=2, domain=SKYCONNECT_DOMAIN, options={}, title="Home Assistant SkyConnect", @@ -74,8 +76,10 @@ def test_detect_radio_hardware(hass: HomeAssistant) -> None: "pid": "EA60", "serial_number": "3c0ed67c628beb11b1cd64a0f320645d", "manufacturer": "Nabu Casa", - "description": "Home Assistant Connect ZBT-1", + "product": "Home Assistant Connect ZBT-1", + "firmware": "ezsp", }, + version=2, domain=SKYCONNECT_DOMAIN, options={}, title="Home Assistant Connect ZBT-1", From 2beab34de8fdc16f8239ed37034136067bb69805 Mon Sep 17 00:00:00 2001 From: Manuel Dipolt Date: Wed, 24 Apr 2024 17:06:46 +0200 Subject: [PATCH 861/967] Add sensor platform to romy integration (#112388) * poc romy status sensor working * poc romy adc sensor working * code review changes * code review changes base enitity.py see branch romy_binary_sensor * code review change: move CoordinatorEntity to the base class * code review changes: sensors disabled per default * code review: icons.json added * checkout main entity.py * code review changes: sensors enabled per default again * disable rssi sensor per default * Update homeassistant/components/romy/strings.json Co-authored-by: Joost Lekkerkerker * code review changes * code review changes * code review changes * pylint fix --------- Co-authored-by: Joost Lekkerkerker --- .coveragerc | 1 + .../components/romy/binary_sensor.py | 2 +- homeassistant/components/romy/const.py | 2 +- homeassistant/components/romy/icons.json | 17 +++ homeassistant/components/romy/sensor.py | 112 ++++++++++++++++++ homeassistant/components/romy/strings.json | 17 +++ 6 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/romy/sensor.py diff --git a/.coveragerc b/.coveragerc index ca2cce2719f..1ccb9e461df 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1161,6 +1161,7 @@ omit = homeassistant/components/romy/binary_sensor.py homeassistant/components/romy/coordinator.py homeassistant/components/romy/entity.py + homeassistant/components/romy/sensor.py homeassistant/components/romy/vacuum.py homeassistant/components/roomba/__init__.py homeassistant/components/roomba/binary_sensor.py diff --git a/homeassistant/components/romy/binary_sensor.py b/homeassistant/components/romy/binary_sensor.py index 263c5840e5f..d8f6216007f 100644 --- a/homeassistant/components/romy/binary_sensor.py +++ b/homeassistant/components/romy/binary_sensor.py @@ -62,7 +62,7 @@ class RomyBinarySensor(RomyEntity, BinarySensorEntity): coordinator: RomyVacuumCoordinator, entity_description: BinarySensorEntityDescription, ) -> None: - """Initialize ROMYs StatusSensor.""" + """Initialize the RomyBinarySensor.""" super().__init__(coordinator) self._attr_unique_id = f"{entity_description.key}_{self.romy.unique_id}" self.entity_description = entity_description diff --git a/homeassistant/components/romy/const.py b/homeassistant/components/romy/const.py index 0fa039e8d1b..a41482ffe59 100644 --- a/homeassistant/components/romy/const.py +++ b/homeassistant/components/romy/const.py @@ -6,6 +6,6 @@ import logging from homeassistant.const import Platform DOMAIN = "romy" -PLATFORMS = [Platform.BINARY_SENSOR, Platform.VACUUM] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.VACUUM] UPDATE_INTERVAL = timedelta(seconds=5) LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/romy/icons.json b/homeassistant/components/romy/icons.json index c27b36af64c..3425d5cfade 100644 --- a/homeassistant/components/romy/icons.json +++ b/homeassistant/components/romy/icons.json @@ -15,6 +15,23 @@ "on": "mdi:basket-check" } } + }, + "sensor": { + "dustbin_sensor": { + "default": "mdi:basket-fill" + }, + "total_cleaning_time": { + "default": "mdi:clock" + }, + "total_number_of_cleaning_runs": { + "default": "mdi:counter" + }, + "total_area_cleaned": { + "default": "mdi:texture-box" + }, + "total_distance_driven": { + "default": "mdi:run" + } } } } diff --git a/homeassistant/components/romy/sensor.py b/homeassistant/components/romy/sensor.py new file mode 100644 index 00000000000..bdd486c4f8f --- /dev/null +++ b/homeassistant/components/romy/sensor.py @@ -0,0 +1,112 @@ +"""Sensor checking adc and status values from your ROMY.""" + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + AREA_SQUARE_METERS, + PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + EntityCategory, + UnitOfLength, + UnitOfTime, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import RomyVacuumCoordinator +from .entity import RomyEntity + +SENSORS: list[SensorEntityDescription] = [ + SensorEntityDescription( + key="battery_level", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="rssi", + entity_registry_enabled_default=False, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="dustbin_sensor", + translation_key="dustbin_sensor", + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="total_cleaning_time", + translation_key="total_cleaning_time", + state_class=SensorStateClass.TOTAL, + native_unit_of_measurement=UnitOfTime.HOURS, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="total_number_of_cleaning_runs", + translation_key="total_number_of_cleaning_runs", + state_class=SensorStateClass.TOTAL, + native_unit_of_measurement="runs", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="total_area_cleaned", + translation_key="total_area_cleaned", + state_class=SensorStateClass.TOTAL, + native_unit_of_measurement=AREA_SQUARE_METERS, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="total_distance_driven", + translation_key="total_distance_driven", + state_class=SensorStateClass.TOTAL, + native_unit_of_measurement=UnitOfLength.METERS, + entity_category=EntityCategory.DIAGNOSTIC, + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up ROMY vacuum cleaner.""" + + coordinator: RomyVacuumCoordinator = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities( + RomySensor(coordinator, entity_description) + for entity_description in SENSORS + if entity_description.key in coordinator.romy.sensors + ) + + +class RomySensor(RomyEntity, SensorEntity): + """RomySensor Class.""" + + entity_description: SensorEntityDescription + + def __init__( + self, + coordinator: RomyVacuumCoordinator, + entity_description: SensorEntityDescription, + ) -> None: + """Initialize ROMYs StatusSensor.""" + super().__init__(coordinator) + self._attr_unique_id = f"{entity_description.key}_{self.romy.unique_id}" + self.entity_description = entity_description + + @property + def native_value(self) -> int: + """Return the value of the sensor.""" + value: int = self.romy.sensors[self.entity_description.key] + return value diff --git a/homeassistant/components/romy/strings.json b/homeassistant/components/romy/strings.json index f4bc4d191ff..78721da17ba 100644 --- a/homeassistant/components/romy/strings.json +++ b/homeassistant/components/romy/strings.json @@ -60,6 +60,23 @@ "water_tank_empty": { "name": "Watertank empty" } + }, + "sensor": { + "dustbin_sensor": { + "name": "Dustbin dirt level" + }, + "total_cleaning_time": { + "name": "Total cleaning time" + }, + "total_number_of_cleaning_runs": { + "name": "Total cleaning runs" + }, + "total_area_cleaned": { + "name": "Total cleaned area" + }, + "total_distance_driven": { + "name": "Total distance driven" + } } } } From f83ee963bfc2efbb7cfdf70ef7e2da749068e6ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 24 Apr 2024 17:08:56 +0200 Subject: [PATCH 862/967] Add binary sensor entities to Traccar Server (#114719) --- .../components/traccar_server/__init__.py | 6 +- .../traccar_server/binary_sensor.py | 99 +++++++++++++++++++ .../traccar_server/device_tracker.py | 11 +-- .../components/traccar_server/icons.json | 9 ++ .../components/traccar_server/strings.json | 16 +++ .../snapshots/test_diagnostics.ambr | 51 +++++++++- 6 files changed, 177 insertions(+), 15 deletions(-) create mode 100644 homeassistant/components/traccar_server/binary_sensor.py diff --git a/homeassistant/components/traccar_server/__init__.py b/homeassistant/components/traccar_server/__init__.py index 703df6cbfa4..c7a65d2d4a8 100644 --- a/homeassistant/components/traccar_server/__init__.py +++ b/homeassistant/components/traccar_server/__init__.py @@ -30,7 +30,11 @@ from .const import ( ) from .coordinator import TraccarServerCoordinator -PLATFORMS: list[Platform] = [Platform.DEVICE_TRACKER, Platform.SENSOR] +PLATFORMS: list[Platform] = [ + Platform.BINARY_SENSOR, + Platform.DEVICE_TRACKER, + Platform.SENSOR, +] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/traccar_server/binary_sensor.py b/homeassistant/components/traccar_server/binary_sensor.py new file mode 100644 index 00000000000..6ee5757dcea --- /dev/null +++ b/homeassistant/components/traccar_server/binary_sensor.py @@ -0,0 +1,99 @@ +"""Support for Traccar server binary sensors.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Generic, Literal, TypeVar, cast + +from pytraccar import DeviceModel + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + 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 TraccarServerCoordinator +from .entity import TraccarServerEntity + +_T = TypeVar("_T") + + +@dataclass(frozen=True, kw_only=True) +class TraccarServerBinarySensorEntityDescription( + Generic[_T], BinarySensorEntityDescription +): + """Describe Traccar Server sensor entity.""" + + data_key: Literal["position", "device", "geofence", "attributes"] + entity_registry_enabled_default = False + entity_category = EntityCategory.DIAGNOSTIC + value_fn: Callable[[_T], bool | None] + + +TRACCAR_SERVER_BINARY_SENSOR_ENTITY_DESCRIPTIONS = ( + TraccarServerBinarySensorEntityDescription[DeviceModel]( + key="attributes.motion", + data_key="position", + translation_key="motion", + device_class=BinarySensorDeviceClass.MOTION, + value_fn=lambda x: x["attributes"].get("motion", False), + ), + TraccarServerBinarySensorEntityDescription[DeviceModel]( + key="status", + data_key="device", + translation_key="status", + value_fn=lambda x: None if (s := x["status"]) == "unknown" else s == "online", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up binary sensor entities.""" + coordinator: TraccarServerCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + TraccarServerBinarySensor( + coordinator=coordinator, + device=entry["device"], + description=cast(TraccarServerBinarySensorEntityDescription, description), + ) + for entry in coordinator.data.values() + for description in TRACCAR_SERVER_BINARY_SENSOR_ENTITY_DESCRIPTIONS + ) + + +class TraccarServerBinarySensor(TraccarServerEntity, BinarySensorEntity): + """Represent a traccar server binary sensor.""" + + _attr_has_entity_name = True + entity_description: TraccarServerBinarySensorEntityDescription + + def __init__( + self, + coordinator: TraccarServerCoordinator, + device: DeviceModel, + description: TraccarServerBinarySensorEntityDescription[_T], + ) -> None: + """Initialize the Traccar Server sensor.""" + super().__init__(coordinator, device) + self.entity_description = description + self._attr_unique_id = ( + f"{device['uniqueId']}_{description.data_key}_{description.key}" + ) + + @property + def is_on(self) -> bool | None: + """Return if the binary sensor is on or not.""" + return self.entity_description.value_fn( + getattr(self, f"traccar_{self.entity_description.data_key}") + ) diff --git a/homeassistant/components/traccar_server/device_tracker.py b/homeassistant/components/traccar_server/device_tracker.py index d15ba084dad..e7dba3ad99d 100644 --- a/homeassistant/components/traccar_server/device_tracker.py +++ b/homeassistant/components/traccar_server/device_tracker.py @@ -9,14 +9,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ( - ATTR_CATEGORY, - ATTR_MOTION, - ATTR_STATUS, - ATTR_TRACCAR_ID, - ATTR_TRACKER, - DOMAIN, -) +from .const import ATTR_CATEGORY, ATTR_TRACCAR_ID, ATTR_TRACKER, DOMAIN from .coordinator import TraccarServerCoordinator from .entity import TraccarServerEntity @@ -46,8 +39,6 @@ class TraccarServerDeviceTracker(TraccarServerEntity, TrackerEntity): return { **self.traccar_attributes, ATTR_CATEGORY: self.traccar_device["category"], - ATTR_MOTION: self.traccar_position["attributes"].get("motion", False), - ATTR_STATUS: self.traccar_device["status"], ATTR_TRACCAR_ID: self.traccar_device["id"], ATTR_TRACKER: DOMAIN, } diff --git a/homeassistant/components/traccar_server/icons.json b/homeassistant/components/traccar_server/icons.json index 59fc663e712..a10b154fbff 100644 --- a/homeassistant/components/traccar_server/icons.json +++ b/homeassistant/components/traccar_server/icons.json @@ -1,5 +1,14 @@ { "entity": { + "binary_sensor": { + "status": { + "default": "mdi:access-point-minus", + "state": { + "off": "mdi:access-point-off", + "on": "mdi:access-point" + } + } + }, "sensor": { "altitude": { "default": "mdi:altimeter" diff --git a/homeassistant/components/traccar_server/strings.json b/homeassistant/components/traccar_server/strings.json index 41adaace77e..8bec4b112ac 100644 --- a/homeassistant/components/traccar_server/strings.json +++ b/homeassistant/components/traccar_server/strings.json @@ -43,6 +43,22 @@ } }, "entity": { + "binary_sensor": { + "motion": { + "name": "Motion", + "state": { + "off": "Stopped", + "on": "Moving" + } + }, + "status": { + "name": "Status", + "state": { + "off": "Offline", + "on": "Online" + } + } + }, "sensor": { "address": { "name": "Address" diff --git a/tests/components/traccar_server/snapshots/test_diagnostics.ambr b/tests/components/traccar_server/snapshots/test_diagnostics.ambr index 300444f10f1..89a6416c303 100644 --- a/tests/components/traccar_server/snapshots/test_diagnostics.ambr +++ b/tests/components/traccar_server/snapshots/test_diagnostics.ambr @@ -82,9 +82,7 @@ 'gps_accuracy': 3.5, 'latitude': '**REDACTED**', 'longitude': '**REDACTED**', - 'motion': False, 'source_type': 'gps', - 'status': 'online', 'traccar_id': 0, 'tracker': 'traccar_server', }), @@ -92,6 +90,29 @@ }), 'unit_of_measurement': None, }), + dict({ + 'disabled': False, + 'enity_id': 'binary_sensor.x_wing_motion', + 'state': dict({ + 'attributes': dict({ + 'device_class': 'motion', + 'friendly_name': 'X-Wing Motion', + }), + 'state': 'off', + }), + 'unit_of_measurement': None, + }), + dict({ + 'disabled': False, + 'enity_id': 'binary_sensor.x_wing_status', + 'state': dict({ + 'attributes': dict({ + 'friendly_name': 'X-Wing Status', + }), + 'state': 'on', + }), + 'unit_of_measurement': None, + }), dict({ 'disabled': False, 'enity_id': 'sensor.x_wing_battery', @@ -231,6 +252,18 @@ }), }), 'entities': list([ + dict({ + 'disabled': True, + 'enity_id': 'binary_sensor.x_wing_motion', + 'state': None, + 'unit_of_measurement': None, + }), + dict({ + 'disabled': True, + 'enity_id': 'binary_sensor.x_wing_status', + 'state': None, + 'unit_of_measurement': None, + }), dict({ 'disabled': True, 'enity_id': 'sensor.x_wing_battery', @@ -343,6 +376,18 @@ }), }), 'entities': list([ + dict({ + 'disabled': True, + 'enity_id': 'binary_sensor.x_wing_motion', + 'state': None, + 'unit_of_measurement': None, + }), + dict({ + 'disabled': True, + 'enity_id': 'binary_sensor.x_wing_status', + 'state': None, + 'unit_of_measurement': None, + }), dict({ 'disabled': True, 'enity_id': 'sensor.x_wing_battery', @@ -384,9 +429,7 @@ 'gps_accuracy': 3.5, 'latitude': '**REDACTED**', 'longitude': '**REDACTED**', - 'motion': False, 'source_type': 'gps', - 'status': 'online', 'traccar_id': 0, 'tracker': 'traccar_server', }), From 5c3ffb8f550a36894bb2980491f4fdfd5b7bc30c Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Wed, 24 Apr 2024 17:24:43 +0200 Subject: [PATCH 863/967] Bump ZHA dependencies (#116106) --- 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 9b7788ff129..452f11db85b 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,12 +21,12 @@ "universal_silabs_flasher" ], "requirements": [ - "bellows==0.38.1", + "bellows==0.38.2", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.115", "zigpy-deconz==0.23.1", - "zigpy==0.63.5", + "zigpy==0.64.0", "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 256c5c3500e..14e88a30354 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -541,7 +541,7 @@ beautifulsoup4==4.12.3 # beewi-smartclim==0.0.10 # homeassistant.components.zha -bellows==0.38.1 +bellows==0.38.2 # homeassistant.components.bmw_connected_drive bimmer-connected[china]==0.14.6 @@ -2959,7 +2959,7 @@ zigpy-zigate==0.12.0 zigpy-znp==0.12.1 # homeassistant.components.zha -zigpy==0.63.5 +zigpy==0.64.0 # homeassistant.components.zoneminder zm-py==0.5.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 63a3563ebaf..9c698476e11 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -466,7 +466,7 @@ base36==0.1.1 beautifulsoup4==4.12.3 # homeassistant.components.zha -bellows==0.38.1 +bellows==0.38.2 # homeassistant.components.bmw_connected_drive bimmer-connected[china]==0.14.6 @@ -2300,7 +2300,7 @@ zigpy-zigate==0.12.0 zigpy-znp==0.12.1 # homeassistant.components.zha -zigpy==0.63.5 +zigpy==0.64.0 # homeassistant.components.zwave_js zwave-js-server-python==0.55.3 From 7d5af09aecbc72b388259620e9a0bbe32e86c57c Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Wed, 24 Apr 2024 17:32:12 +0200 Subject: [PATCH 864/967] Add quality scale to Comelit (#116041) add quality scale --- homeassistant/components/comelit/manifest.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/comelit/manifest.json b/homeassistant/components/comelit/manifest.json index d93ec349bba..b9264d16f69 100644 --- a/homeassistant/components/comelit/manifest.json +++ b/homeassistant/components/comelit/manifest.json @@ -4,7 +4,9 @@ "codeowners": ["@chemelli74"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/comelit", + "integration_type": "hub", "iot_class": "local_polling", "loggers": ["aiocomelit"], + "quality_scale": "silver", "requirements": ["aiocomelit==0.9.0"] } From 41a86d24044b9f4371cf70394525a2f1e546bed4 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Wed, 24 Apr 2024 17:36:31 +0200 Subject: [PATCH 865/967] Add quality scale to Vodafone Station (#116040) Add quality scale --- homeassistant/components/vodafone_station/manifest.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/vodafone_station/manifest.json b/homeassistant/components/vodafone_station/manifest.json index ced871b7616..7e2e974e709 100644 --- a/homeassistant/components/vodafone_station/manifest.json +++ b/homeassistant/components/vodafone_station/manifest.json @@ -4,7 +4,9 @@ "codeowners": ["@paoloantinori", "@chemelli74"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/vodafone_station", + "integration_type": "hub", "iot_class": "local_polling", "loggers": ["aiovodafone"], + "quality_scale": "silver", "requirements": ["aiovodafone==0.5.4"] } From d565c1a84bf0b3df764b19837b43e774a6493e44 Mon Sep 17 00:00:00 2001 From: Steve Easley Date: Wed, 24 Apr 2024 11:36:50 -0400 Subject: [PATCH 866/967] Add select platform to jvc_projector component (#111638) * Initial commit of jvc_projector select platform * Move icon to icons.json * Apply suggestions from code review * Update tests/components/jvc_projector/test_select.py --------- Co-authored-by: Erik Montnemery Co-authored-by: Joost Lekkerkerker --- .../components/jvc_projector/__init__.py | 2 +- .../components/jvc_projector/icons.json | 5 ++ .../components/jvc_projector/select.py | 77 +++++++++++++++++++ .../components/jvc_projector/strings.json | 9 +++ tests/components/jvc_projector/test_select.py | 44 +++++++++++ 5 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/jvc_projector/select.py create mode 100644 tests/components/jvc_projector/test_select.py diff --git a/homeassistant/components/jvc_projector/__init__.py b/homeassistant/components/jvc_projector/__init__.py index 28e4cc995bb..8ce1fb46e3d 100644 --- a/homeassistant/components/jvc_projector/__init__.py +++ b/homeassistant/components/jvc_projector/__init__.py @@ -18,7 +18,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from .const import DOMAIN from .coordinator import JvcProjectorDataUpdateCoordinator -PLATFORMS = [Platform.BINARY_SENSOR, Platform.REMOTE, Platform.SENSOR] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.REMOTE, Platform.SELECT, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/jvc_projector/icons.json b/homeassistant/components/jvc_projector/icons.json index c70ded78cb4..a0404b328e1 100644 --- a/homeassistant/components/jvc_projector/icons.json +++ b/homeassistant/components/jvc_projector/icons.json @@ -8,6 +8,11 @@ } } }, + "select": { + "input": { + "default": "mdi:hdmi-port" + } + }, "sensor": { "jvc_power_status": { "default": "mdi:power-plug-off", diff --git a/homeassistant/components/jvc_projector/select.py b/homeassistant/components/jvc_projector/select.py new file mode 100644 index 00000000000..1395637fad1 --- /dev/null +++ b/homeassistant/components/jvc_projector/select.py @@ -0,0 +1,77 @@ +"""Select platform for the jvc_projector integration.""" + +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from typing import Final + +from jvcprojector import JvcProjector, const + +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 . import JvcProjectorDataUpdateCoordinator +from .const import DOMAIN +from .entity import JvcProjectorEntity + + +@dataclass(frozen=True, kw_only=True) +class JvcProjectorSelectDescription(SelectEntityDescription): + """Describes JVC Projector select entities.""" + + command: Callable[[JvcProjector, str], Awaitable[None]] + + +OPTIONS: Final[dict[str, dict[str, str]]] = { + "input": {const.HDMI1: const.REMOTE_HDMI_1, const.HDMI2: const.REMOTE_HDMI_2} +} + +SELECTS: Final[list[JvcProjectorSelectDescription]] = [ + JvcProjectorSelectDescription( + key="input", + translation_key="input", + options=list(OPTIONS["input"]), + command=lambda device, option: device.remote(OPTIONS["input"][option]), + ) +] + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the JVC Projector platform from a config entry.""" + coordinator: JvcProjectorDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + JvcProjectorSelectEntity(coordinator, description) for description in SELECTS + ) + + +class JvcProjectorSelectEntity(JvcProjectorEntity, SelectEntity): + """Representation of a JVC Projector select entity.""" + + entity_description: JvcProjectorSelectDescription + + def __init__( + self, + coordinator: JvcProjectorDataUpdateCoordinator, + description: JvcProjectorSelectDescription, + ) -> None: + """Initialize the entity.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"{coordinator.unique_id}_{description.key}" + + @property + def current_option(self) -> str | None: + """Return the selected entity option to represent the entity state.""" + return self.coordinator.data[self.entity_description.key] + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + await self.entity_description.command(self.coordinator.device, option) diff --git a/homeassistant/components/jvc_projector/strings.json b/homeassistant/components/jvc_projector/strings.json index 9991fa1cf67..b89139cbab3 100644 --- a/homeassistant/components/jvc_projector/strings.json +++ b/homeassistant/components/jvc_projector/strings.json @@ -38,6 +38,15 @@ "name": "[%key:component::sensor::entity_component::power::name%]" } }, + "select": { + "input": { + "name": "Input", + "state": { + "hdmi1": "HDMI 1", + "hdmi2": "HDMI 2" + } + } + }, "sensor": { "jvc_power_status": { "name": "Power status", diff --git a/tests/components/jvc_projector/test_select.py b/tests/components/jvc_projector/test_select.py new file mode 100644 index 00000000000..a52133bd688 --- /dev/null +++ b/tests/components/jvc_projector/test_select.py @@ -0,0 +1,44 @@ +"""Tests for JVC Projector select platform.""" + +from unittest.mock import MagicMock + +from jvcprojector import const + +from homeassistant.components.select import ( + ATTR_OPTIONS, + DOMAIN as SELECT_DOMAIN, + SERVICE_SELECT_OPTION, +) +from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_OPTION +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry + +INPUT_ENTITY_ID = "select.jvc_projector_input" + + +async def test_input_select( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_device: MagicMock, + mock_integration: MockConfigEntry, +) -> None: + """Test input select.""" + entity = hass.states.get(INPUT_ENTITY_ID) + assert entity + assert entity.attributes.get(ATTR_FRIENDLY_NAME) == "JVC Projector Input" + assert entity.attributes.get(ATTR_OPTIONS) == [const.HDMI1, const.HDMI2] + assert entity.state == const.HDMI1 + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: INPUT_ENTITY_ID, + ATTR_OPTION: const.HDMI2, + }, + blocking=True, + ) + + mock_device.remote.assert_called_once_with(const.REMOTE_HDMI_2) From bc7fa8cf9e2b152f448a2ae9dd03e48345e0ea6f Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 24 Apr 2024 11:41:17 -0500 Subject: [PATCH 867/967] Bump intents to 2024.4.24 (#116111) --- 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 8ee27986bb8..82e2adca680 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.6.1", "home-assistant-intents==2024.4.3"] + "requirements": ["hassil==1.6.1", "home-assistant-intents==2024.4.24"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 74c4d185847..b88f2aefffa 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -33,7 +33,7 @@ hass-nabucasa==0.78.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 home-assistant-frontend==20240424.1 -home-assistant-intents==2024.4.3 +home-assistant-intents==2024.4.24 httpx==0.27.0 ifaddr==0.2.0 Jinja2==3.1.3 diff --git a/requirements_all.txt b/requirements_all.txt index 14e88a30354..75a7411c64b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1081,7 +1081,7 @@ holidays==0.47 home-assistant-frontend==20240424.1 # homeassistant.components.conversation -home-assistant-intents==2024.4.3 +home-assistant-intents==2024.4.24 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9c698476e11..4e8f9ecb69f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -883,7 +883,7 @@ holidays==0.47 home-assistant-frontend==20240424.1 # homeassistant.components.conversation -home-assistant-intents==2024.4.3 +home-assistant-intents==2024.4.24 # homeassistant.components.home_connect homeconnect==0.7.2 From 67021be27456afbfee6e44be180644febd4a36f7 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Wed, 24 Apr 2024 19:41:46 +0200 Subject: [PATCH 868/967] Add notification service for Bring component (#109222) * Add notification service for Bring component * change to async * update to new library and raise for urgent message without item name * add icons.json and replace string with reference in strings.json * Incorporate proposed changes from https://github.com/home-assistant/core/pull/115510 * Remove unnecessary exception, rewrite translations strings * remove unused constants --- homeassistant/components/bring/const.py | 8 +++ homeassistant/components/bring/icons.json | 3 ++ homeassistant/components/bring/services.yaml | 23 +++++++++ homeassistant/components/bring/strings.json | 36 ++++++++++++++ homeassistant/components/bring/todo.py | 52 ++++++++++++++++++-- 5 files changed, 119 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/bring/services.yaml diff --git a/homeassistant/components/bring/const.py b/homeassistant/components/bring/const.py index 64a6ec67f85..911c08a835d 100644 --- a/homeassistant/components/bring/const.py +++ b/homeassistant/components/bring/const.py @@ -1,3 +1,11 @@ """Constants for the Bring! integration.""" +from typing import Final + DOMAIN = "bring" + +ATTR_SENDER: Final = "sender" +ATTR_ITEM_NAME: Final = "item" +ATTR_NOTIFICATION_TYPE: Final = "message" + +SERVICE_PUSH_NOTIFICATION = "send_message" diff --git a/homeassistant/components/bring/icons.json b/homeassistant/components/bring/icons.json index a757b20a4cc..1c6c3bdeca0 100644 --- a/homeassistant/components/bring/icons.json +++ b/homeassistant/components/bring/icons.json @@ -5,5 +5,8 @@ "default": "mdi:cart" } } + }, + "services": { + "send_message": "mdi:cellphone-message" } } diff --git a/homeassistant/components/bring/services.yaml b/homeassistant/components/bring/services.yaml new file mode 100644 index 00000000000..98d5c68de13 --- /dev/null +++ b/homeassistant/components/bring/services.yaml @@ -0,0 +1,23 @@ +send_message: + target: + entity: + domain: todo + integration: bring + fields: + message: + example: urgent_message + required: true + default: "going_shopping" + selector: + select: + translation_key: "notification_type_selector" + options: + - "going_shopping" + - "changed_list" + - "shopping_done" + - "urgent_message" + item: + example: Cilantro + required: false + selector: + text: diff --git a/homeassistant/components/bring/strings.json b/homeassistant/components/bring/strings.json index 6d61034bea8..e6df885cbbc 100644 --- a/homeassistant/components/bring/strings.json +++ b/homeassistant/components/bring/strings.json @@ -38,6 +38,42 @@ }, "setup_authentication_exception": { "message": "Authentication failed for {email}, check your email and password" + }, + "notify_missing_argument_item": { + "message": "Failed to call service {service}. 'URGENT_MESSAGE' requires a value @ data['item']. Got None" + }, + "notify_request_failed": { + "message": "Failed to send push notification for bring due to a connection error, try again later" + } + }, + "services": { + "send_message": { + "name": "[%key:component::notify::services::notify::name%]", + "description": "Send a mobile push notification to members of a shared Bring! list.", + "fields": { + "entity_id": { + "name": "List", + "description": "Bring! list whose members (except sender) will be notified." + }, + "message": { + "name": "Notification type", + "description": "Type of push notification to send to list members." + }, + "item": { + "name": "Item (Required if message type `Breaking news` selected)", + "description": "Item name to include in a breaking news message e.g. `Breaking news - Please get cilantro!`" + } + } + } + }, + "selector": { + "notification_type_selector": { + "options": { + "going_shopping": "I'm going shopping! - Last chance for adjustments", + "changed_list": "List changed - Check it out", + "shopping_done": "Shopping done - you can relax", + "urgent_message": "Breaking news - Please get `item`!" + } } } } diff --git a/homeassistant/components/bring/todo.py b/homeassistant/components/bring/todo.py index e631dc32951..5eabcc01553 100644 --- a/homeassistant/components/bring/todo.py +++ b/homeassistant/components/bring/todo.py @@ -6,7 +6,8 @@ from typing import TYPE_CHECKING import uuid from bring_api.exceptions import BringRequestException -from bring_api.types import BringItem, BringItemOperation +from bring_api.types import BringItem, BringItemOperation, BringNotificationType +import voluptuous as vol from homeassistant.components.todo import ( TodoItem, @@ -16,11 +17,18 @@ from homeassistant.components.todo import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError +from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers.config_validation import make_entity_service_schema from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN +from .const import ( + ATTR_ITEM_NAME, + ATTR_NOTIFICATION_TYPE, + DOMAIN, + SERVICE_PUSH_NOTIFICATION, +) from .coordinator import BringData, BringDataUpdateCoordinator @@ -46,6 +54,21 @@ async def async_setup_entry( for bring_list in coordinator.data.values() ) + platform = entity_platform.async_get_current_platform() + + platform.async_register_entity_service( + SERVICE_PUSH_NOTIFICATION, + make_entity_service_schema( + { + vol.Required(ATTR_NOTIFICATION_TYPE): vol.All( + vol.Upper, cv.enum(BringNotificationType) + ), + vol.Optional(ATTR_ITEM_NAME): cv.string, + } + ), + "async_send_message", + ) + class BringTodoListEntity( CoordinatorEntity[BringDataUpdateCoordinator], TodoListEntity @@ -231,3 +254,26 @@ class BringTodoListEntity( ) from e await self.coordinator.async_refresh() + + async def async_send_message( + self, + message: BringNotificationType, + item: str | None = None, + ) -> None: + """Send a push notification to members of a shared bring list.""" + + try: + await self.coordinator.bring.notify(self._list_uuid, message, item or None) + except BringRequestException as e: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="notify_request_failed", + ) from e + except ValueError as e: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="notify_missing_argument_item", + translation_placeholders={ + "service": f"{DOMAIN}.{SERVICE_PUSH_NOTIFICATION}", + }, + ) from e From 830e8d7b946c1e7c37509884e60c82b9c21974e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 24 Apr 2024 20:00:06 +0200 Subject: [PATCH 869/967] Fix statistic bug in Tibber sensor (#116112) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Handle keyError in Tibber sensor Signed-off-by: Daniel Hjelseth Høyer * Constant Signed-off-by: Daniel Hjelseth Høyer --------- Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/tibber/sensor.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index da2fd881a54..7da0a2b7947 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -53,6 +53,8 @@ from homeassistant.util import Throttle, dt as dt_util from .const import DOMAIN as TIBBER_DOMAIN, MANUFACTURER +FIVE_YEARS = 5 * 365 * 24 + _LOGGER = logging.getLogger(__name__) ICON = "mdi:currency-usd" @@ -724,9 +726,16 @@ class TibberDataCoordinator(DataUpdateCoordinator[None]): # pylint: disable=has None, {"sum"}, ) - first_stat = stat[statistic_id][0] - _sum = cast(float, first_stat["sum"]) - last_stats_time = first_stat["start"] + if statistic_id in stat: + first_stat = stat[statistic_id][0] + _sum = cast(float, first_stat["sum"]) + last_stats_time = first_stat["start"] + else: + hourly_data = await home.get_historic_data( + FIVE_YEARS, production=is_production + ) + _sum = 0.0 + last_stats_time = None statistics = [] From 4b53471b6098e0bbf53e71ab29dcad1a22f39136 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 24 Apr 2024 20:09:40 +0200 Subject: [PATCH 870/967] Bump aiopegelonline to 0.0.10 (#116114) bump aiopegelonline to 0.0.10 --- homeassistant/components/pegel_online/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/pegel_online/manifest.json b/homeassistant/components/pegel_online/manifest.json index d193fd7487a..d51278d0c1b 100644 --- a/homeassistant/components/pegel_online/manifest.json +++ b/homeassistant/components/pegel_online/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["aiopegelonline"], - "requirements": ["aiopegelonline==0.0.9"] + "requirements": ["aiopegelonline==0.0.10"] } diff --git a/requirements_all.txt b/requirements_all.txt index 75a7411c64b..ee8a074bf6b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -318,7 +318,7 @@ aioopenexchangerates==0.4.0 aiooui==0.1.5 # homeassistant.components.pegel_online -aiopegelonline==0.0.9 +aiopegelonline==0.0.10 # homeassistant.components.acmeda aiopulse==0.4.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4e8f9ecb69f..2eb9a80281f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -291,7 +291,7 @@ aioopenexchangerates==0.4.0 aiooui==0.1.5 # homeassistant.components.pegel_online -aiopegelonline==0.0.9 +aiopegelonline==0.0.10 # homeassistant.components.acmeda aiopulse==0.4.4 From f8c38fad0024f38ecd99524427fb3d6054b53708 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 24 Apr 2024 20:47:22 +0200 Subject: [PATCH 871/967] Split out event handling from Axis hub (#113837) * Split out event handling from Axis hub * Improve test coverage * Mark internal methods with '_' * Rename to event source --- .../components/axis/hub/event_source.py | 93 +++++++++++++++++++ homeassistant/components/axis/hub/hub.py | 77 +++------------ tests/components/axis/conftest.py | 9 +- tests/components/axis/test_hub.py | 15 ++- 4 files changed, 126 insertions(+), 68 deletions(-) create mode 100644 homeassistant/components/axis/hub/event_source.py diff --git a/homeassistant/components/axis/hub/event_source.py b/homeassistant/components/axis/hub/event_source.py new file mode 100644 index 00000000000..7f2bfe7c982 --- /dev/null +++ b/homeassistant/components/axis/hub/event_source.py @@ -0,0 +1,93 @@ +"""Axis network device abstraction.""" + +from __future__ import annotations + +import axis +from axis.errors import Unauthorized +from axis.interfaces.mqtt import mqtt_json_to_event +from axis.models.mqtt import ClientState +from axis.stream_manager import Signal, State + +from homeassistant.components import mqtt +from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN +from homeassistant.components.mqtt.models import ReceiveMessage +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.setup import async_when_setup + + +class AxisEventSource: + """Manage connection to event sources from an Axis device.""" + + def __init__( + self, hass: HomeAssistant, config_entry: ConfigEntry, api: axis.AxisDevice + ) -> None: + """Initialize the device.""" + self.hass = hass + self.config_entry = config_entry + self.api = api + + self.signal_reachable = f"axis_reachable_{config_entry.entry_id}" + + self.available = True + + @callback + def setup(self) -> None: + """Set up the device events.""" + self.api.stream.connection_status_callback.append(self._connection_status_cb) + self.api.enable_events() + self.api.stream.start() + + if self.api.vapix.mqtt.supported: + async_when_setup(self.hass, MQTT_DOMAIN, self._async_use_mqtt) + + @callback + def teardown(self) -> None: + """Tear down connections.""" + self._disconnect_from_stream() + + @callback + def _disconnect_from_stream(self) -> None: + """Stop stream.""" + if self.api.stream.state != State.STOPPED: + self.api.stream.connection_status_callback.clear() + self.api.stream.stop() + + async def _async_use_mqtt(self, hass: HomeAssistant, component: str) -> None: + """Set up to use MQTT.""" + try: + status = await self.api.vapix.mqtt.get_client_status() + except Unauthorized: + # This means the user has too low privileges + return + + if status.status.state == ClientState.ACTIVE: + self.config_entry.async_on_unload( + await mqtt.async_subscribe( + hass, f"{status.config.device_topic_prefix}/#", self._mqtt_message + ) + ) + + @callback + def _mqtt_message(self, message: ReceiveMessage) -> None: + """Receive Axis MQTT message.""" + self._disconnect_from_stream() + + if message.topic.endswith("event/connection"): + return + + event = mqtt_json_to_event(message.payload) + self.api.event.handler(event) + + @callback + def _connection_status_cb(self, status: Signal) -> None: + """Handle signals of device connection status. + + This is called on every RTSP keep-alive message. + Only signal state change if state change is true. + """ + + if self.available != (status == Signal.PLAYING): + self.available = not self.available + async_dispatcher_send(self.hass, self.signal_reachable) diff --git a/homeassistant/components/axis/hub/hub.py b/homeassistant/components/axis/hub/hub.py index 4abd1358417..4e58e3be7c6 100644 --- a/homeassistant/components/axis/hub/hub.py +++ b/homeassistant/components/axis/hub/hub.py @@ -5,24 +5,17 @@ from __future__ import annotations from typing import Any import axis -from axis.errors import Unauthorized -from axis.interfaces.mqtt import mqtt_json_to_event -from axis.models.mqtt import ClientState -from axis.stream_manager import Signal, State -from homeassistant.components import mqtt -from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN -from homeassistant.components.mqtt.models import ReceiveMessage from homeassistant.config_entries import ConfigEntry from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.setup import async_when_setup from ..const import ATTR_MANUFACTURER, DOMAIN as AXIS_DOMAIN from .config import AxisConfig from .entity_loader import AxisEntityLoader +from .event_source import AxisEventSource class AxisHub: @@ -35,9 +28,9 @@ class AxisHub: self.hass = hass self.config = AxisConfig.from_config_entry(config_entry) self.entity_loader = AxisEntityLoader(self) + self.event_source = AxisEventSource(hass, config_entry, api) self.api = api - self.available = True self.fw_version = api.vapix.firmware_version self.product_type = api.vapix.product_type self.unique_id = format_mac(api.vapix.serial_number) @@ -51,32 +44,23 @@ class AxisHub: hub: AxisHub = hass.data[AXIS_DOMAIN][config_entry.entry_id] return hub + @property + def available(self) -> bool: + """Connection state to the device.""" + return self.event_source.available + # Signals @property def signal_reachable(self) -> str: """Device specific event to signal a change in connection status.""" - return f"axis_reachable_{self.config.entry.entry_id}" + return self.event_source.signal_reachable @property def signal_new_address(self) -> str: """Device specific event to signal a change in device address.""" return f"axis_new_address_{self.config.entry.entry_id}" - # Callbacks - - @callback - def connection_status_callback(self, status: Signal) -> None: - """Handle signals of device connection status. - - This is called on every RTSP keep-alive message. - Only signal state change if state change is true. - """ - - if self.available != (status == Signal.PLAYING): - self.available = not self.available - async_dispatcher_send(self.hass, self.signal_reachable) - @staticmethod async def async_new_address_callback( hass: HomeAssistant, config_entry: ConfigEntry @@ -89,6 +73,7 @@ class AxisHub: """ hub = AxisHub.get_hub(hass, config_entry) hub.config = AxisConfig.from_config_entry(config_entry) + hub.event_source.config_entry = config_entry hub.api.config.host = hub.config.host async_dispatcher_send(hass, hub.signal_new_address) @@ -106,57 +91,19 @@ class AxisHub: sw_version=self.fw_version, ) - async def async_use_mqtt(self, hass: HomeAssistant, component: str) -> None: - """Set up to use MQTT.""" - try: - status = await self.api.vapix.mqtt.get_client_status() - except Unauthorized: - # This means the user has too low privileges - return - if status.status.state == ClientState.ACTIVE: - self.config.entry.async_on_unload( - await mqtt.async_subscribe( - hass, f"{status.config.device_topic_prefix}/#", self.mqtt_message - ) - ) - - @callback - def mqtt_message(self, message: ReceiveMessage) -> None: - """Receive Axis MQTT message.""" - self.disconnect_from_stream() - if message.topic.endswith("event/connection"): - return - event = mqtt_json_to_event(message.payload) - self.api.event.handler(event) - # Setup and teardown methods @callback def setup(self) -> None: """Set up the device events.""" self.entity_loader.initialize_platforms() - - self.api.stream.connection_status_callback.append( - self.connection_status_callback - ) - self.api.enable_events() - self.api.stream.start() - - if self.api.vapix.mqtt.supported: - async_when_setup(self.hass, MQTT_DOMAIN, self.async_use_mqtt) - - @callback - def disconnect_from_stream(self) -> None: - """Stop stream.""" - if self.api.stream.state != State.STOPPED: - self.api.stream.connection_status_callback.clear() - self.api.stream.stop() + self.event_source.setup() async def shutdown(self, event: Event) -> None: """Stop the event stream.""" - self.disconnect_from_stream() + self.event_source.teardown() @callback def teardown(self) -> None: """Reset this device to default state.""" - self.disconnect_from_stream() + self.event_source.teardown() diff --git a/tests/components/axis/conftest.py b/tests/components/axis/conftest.py index b50a28df49f..7a4e446a0cc 100644 --- a/tests/components/axis/conftest.py +++ b/tests/components/axis/conftest.py @@ -114,6 +114,7 @@ def default_request_fixture( port_management_payload: dict[str, Any], param_properties_payload: dict[str, Any], param_ports_payload: dict[str, Any], + mqtt_status_code: int, ) -> Callable[[str], None]: """Mock default Vapix requests responses.""" @@ -131,7 +132,7 @@ def default_request_fixture( json=port_management_payload, ) respx.post("/axis-cgi/mqtt/client.cgi").respond( - json=MQTT_CLIENT_RESPONSE, + json=MQTT_CLIENT_RESPONSE, status_code=mqtt_status_code ) respx.post("/axis-cgi/streamprofile.cgi").respond( json=STREAM_PROFILES_RESPONSE, @@ -239,6 +240,12 @@ def param_ports_data_fixture() -> dict[str, Any]: return PORTS_RESPONSE +@pytest.fixture(name="mqtt_status_code") +def mqtt_status_code_fixture(): + """Property parameter data.""" + return 200 + + @pytest.fixture(name="setup_default_vapix_requests") def default_vapix_requests_fixture(mock_vapix_requests: Callable[[str], None]) -> None: """Mock default Vapix requests responses.""" diff --git a/tests/components/axis/test_hub.py b/tests/components/axis/test_hub.py index 1ae6db05427..5948874f0bf 100644 --- a/tests/components/axis/test_hub.py +++ b/tests/components/axis/test_hub.py @@ -2,7 +2,7 @@ from ipaddress import ip_address from unittest import mock -from unittest.mock import Mock, patch +from unittest.mock import Mock, call, patch import axis as axislib import pytest @@ -91,7 +91,8 @@ async def test_device_support_mqtt( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_config_entry ) -> None: """Successful setup.""" - mqtt_mock.async_subscribe.assert_called_with(f"axis/{MAC}/#", mock.ANY, 0, "utf-8") + mqtt_call = call(f"axis/{MAC}/#", mock.ANY, 0, "utf-8") + assert mqtt_call in mqtt_mock.async_subscribe.call_args_list topic = f"axis/{MAC}/event/tns:onvif/Device/tns:axis/Sensor/PIR/$source/sensor/0" message = ( @@ -109,6 +110,16 @@ async def test_device_support_mqtt( assert pir.name == f"{NAME} PIR 0" +@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: + """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 ) -> None: From 0c44051d2ab8a78d8b979aaf950f5dc1c5c5bfa4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 24 Apr 2024 21:05:09 +0200 Subject: [PATCH 872/967] Bump version to 2024.5.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 ba83eca58d8..1abfe08b93c 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 = 5 -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 7e3038f6ee2..34c7d648795 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.5.0.dev0" +version = "2024.5.0b0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From c65187cbfbd236186aa3cc78f8554b36da649e09 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 25 Apr 2024 21:06:52 +0200 Subject: [PATCH 873/967] Fix climate entity creation when Shelly WallDisplay uses external relay as actuator (#115216) * Fix climate entity creation when Shelly WallDisplay uses external relay as actuator * More comments * Wrap condition into function --------- Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/shelly/climate.py | 6 +++- homeassistant/components/shelly/switch.py | 16 ++++++--- homeassistant/components/shelly/utils.py | 5 +++ tests/components/shelly/test_climate.py | 40 +++++++++++++++++++++- tests/components/shelly/test_switch.py | 1 + 5 files changed, 62 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index b368b38820e..81289bc1a9b 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -132,7 +132,11 @@ def async_setup_rpc_entry( climate_ids = [] for id_ in climate_key_ids: climate_ids.append(id_) - + # There are three configuration scenarios for WallDisplay: + # - relay mode (no thermostat) + # - thermostat mode using the internal relay as an actuator + # - thermostat mode using an external (from another device) relay as + # an actuator if is_rpc_thermostat_internal_actuator(coordinator.device.status): # Wall Display relay is used as the thermostat actuator, # we need to remove a switch entity diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py index 14fec43c58b..81b16d48ab8 100644 --- a/homeassistant/components/shelly/switch.py +++ b/homeassistant/components/shelly/switch.py @@ -43,6 +43,7 @@ from .utils import ( is_block_channel_type_light, is_rpc_channel_type_light, is_rpc_thermostat_internal_actuator, + is_rpc_thermostat_mode, ) @@ -140,12 +141,19 @@ def async_setup_rpc_entry( continue if coordinator.model == MODEL_WALL_DISPLAY: - if not is_rpc_thermostat_internal_actuator(coordinator.device.status): - # Wall Display relay is not used as the thermostat actuator, - # we need to remove a climate entity + # There are three configuration scenarios for WallDisplay: + # - relay mode (no thermostat) + # - thermostat mode using the internal relay as an actuator + # - thermostat mode using an external (from another device) relay as + # an actuator + if not is_rpc_thermostat_mode(id_, coordinator.device.status): + # The device is not in thermostat mode, we need to remove a climate + # entity unique_id = f"{coordinator.mac}-thermostat:{id_}" async_remove_shelly_entity(hass, "climate", unique_id) - else: + elif is_rpc_thermostat_internal_actuator(coordinator.device.status): + # The internal relay is an actuator, skip this ID so as not to create + # a switch entity continue switch_ids.append(id_) diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index ce98e0d5c12..b7cb2f1476a 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -500,3 +500,8 @@ def async_remove_shelly_rpc_entities( if entity_id := entity_reg.async_get_entity_id(domain, DOMAIN, f"{mac}-{key}"): LOGGER.debug("Removing entity: %s", entity_id) entity_reg.async_remove(entity_id) + + +def is_rpc_thermostat_mode(ident: int, status: dict[str, Any]) -> bool: + """Return True if 'thermostat:' is present in the status.""" + return f"thermostat:{ident}" in status diff --git a/tests/components/shelly/test_climate.py b/tests/components/shelly/test_climate.py index 9fee3468f11..9946dd7640d 100644 --- a/tests/components/shelly/test_climate.py +++ b/tests/components/shelly/test_climate.py @@ -25,7 +25,12 @@ from homeassistant.components.climate import ( from homeassistant.components.shelly.const import DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState -from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_UNAVAILABLE +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_TEMPERATURE, + STATE_ON, + STATE_UNAVAILABLE, +) from homeassistant.core import HomeAssistant, State from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers.device_registry import DeviceRegistry @@ -711,3 +716,36 @@ async def test_wall_display_thermostat_mode( entry = entity_registry.async_get(climate_entity_id) assert entry assert entry.unique_id == "123456789ABC-thermostat:0" + + +async def test_wall_display_thermostat_mode_external_actuator( + hass: HomeAssistant, + mock_rpc_device: Mock, + entity_registry: EntityRegistry, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test Wall Display in thermostat mode with an external actuator.""" + climate_entity_id = "climate.test_name" + switch_entity_id = "switch.test_switch_0" + + new_status = deepcopy(mock_rpc_device.status) + new_status["sys"]["relay_in_thermostat"] = False + monkeypatch.setattr(mock_rpc_device, "status", new_status) + + await init_integration(hass, 2, model=MODEL_WALL_DISPLAY) + + # the switch entity should be created + state = hass.states.get(switch_entity_id) + assert state + assert state.state == STATE_ON + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 + + # the climate entity should be created + state = hass.states.get(climate_entity_id) + assert state + assert state.state == HVACMode.HEAT + assert len(hass.states.async_entity_ids(CLIMATE_DOMAIN)) == 1 + + entry = entity_registry.async_get(climate_entity_id) + assert entry + assert entry.unique_id == "123456789ABC-thermostat:0" diff --git a/tests/components/shelly/test_switch.py b/tests/components/shelly/test_switch.py index fe2c4354afc..dd214c8841d 100644 --- a/tests/components/shelly/test_switch.py +++ b/tests/components/shelly/test_switch.py @@ -330,6 +330,7 @@ async def test_wall_display_relay_mode( new_status = deepcopy(mock_rpc_device.status) new_status["sys"]["relay_in_thermostat"] = False + new_status.pop("thermostat:0") monkeypatch.setattr(mock_rpc_device, "status", new_status) await init_integration(hass, 2, model=MODEL_WALL_DISPLAY) From 0eace572c6f879443c261dfd524ed19680273370 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sat, 27 Apr 2024 09:24:11 +0200 Subject: [PATCH 874/967] Don't create event entries for lighting4 rfxtrx devices (#115716) These have no standardized command need to be reworked in the backing library to support exposing as events. Fixes #115545 --- homeassistant/components/rfxtrx/event.py | 6 +++++- tests/components/rfxtrx/test_event.py | 23 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/rfxtrx/event.py b/homeassistant/components/rfxtrx/event.py index 7e73919aacd..5c3944dc74b 100644 --- a/homeassistant/components/rfxtrx/event.py +++ b/homeassistant/components/rfxtrx/event.py @@ -15,6 +15,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify from . import DeviceTuple, RfxtrxEntity, async_setup_platform_entry +from .const import DEVICE_PACKET_TYPE_LIGHTING4 _LOGGER = logging.getLogger(__name__) @@ -27,7 +28,10 @@ async def async_setup_entry( """Set up config entry.""" def _supported(event: RFXtrxEvent) -> bool: - return isinstance(event, (ControlEvent, SensorEvent)) + return ( + isinstance(event, (ControlEvent, SensorEvent)) + and event.device.packettype != DEVICE_PACKET_TYPE_LIGHTING4 + ) def _constructor( event: RFXtrxEvent, diff --git a/tests/components/rfxtrx/test_event.py b/tests/components/rfxtrx/test_event.py index 1a4305d97f6..035949efe3b 100644 --- a/tests/components/rfxtrx/test_event.py +++ b/tests/components/rfxtrx/test_event.py @@ -10,6 +10,7 @@ from syrupy.assertion import SnapshotAssertion from homeassistant.components.rfxtrx import get_rfx_object from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from .conftest import setup_rfx_test_cfg @@ -101,3 +102,25 @@ async def test_invalid_event_type( await hass.async_block_till_done() assert hass.states.get("event.arc_c1") == state + + +async def test_ignoring_lighting4(hass: HomeAssistant, rfxtrx) -> None: + """Test with 1 sensor.""" + entry = await setup_rfx_test_cfg( + hass, + devices={ + "0913000022670e013970": { + "data_bits": 4, + "command_on": 0xE, + "command_off": 0x7, + } + }, + ) + + registry = er.async_get(hass) + entries = [ + entry + for entry in registry.entities.get_entries_for_config_entry_id(entry.entry_id) + if entry.domain == Platform.EVENT + ] + assert entries == [] From d6f1d0666c3fec1f47d9f88e0bcd9a36febedcce Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 25 Apr 2024 19:58:13 +0200 Subject: [PATCH 875/967] Update rfxtrx to 0.31.1 (#116125) --- homeassistant/components/rfxtrx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rfxtrx/manifest.json b/homeassistant/components/rfxtrx/manifest.json index ec902855f27..bb3701e2e31 100644 --- a/homeassistant/components/rfxtrx/manifest.json +++ b/homeassistant/components/rfxtrx/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/rfxtrx", "iot_class": "local_push", "loggers": ["RFXtrx"], - "requirements": ["pyRFXtrx==0.31.0"] + "requirements": ["pyRFXtrx==0.31.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index ee8a074bf6b..b58b5948cad 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1661,7 +1661,7 @@ pyEmby==1.9 pyHik==0.3.2 # homeassistant.components.rfxtrx -pyRFXtrx==0.31.0 +pyRFXtrx==0.31.1 # homeassistant.components.sony_projector pySDCP==1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2eb9a80281f..75eb06c924d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1311,7 +1311,7 @@ pyDuotecno==2024.3.2 pyElectra==1.2.0 # homeassistant.components.rfxtrx -pyRFXtrx==0.31.0 +pyRFXtrx==0.31.1 # homeassistant.components.tibber pyTibber==0.28.2 From f91266908dd3159cad0accfb99a1e14170b7492b Mon Sep 17 00:00:00 2001 From: rappenze Date: Thu, 25 Apr 2024 19:57:15 +0200 Subject: [PATCH 876/967] Bump pyfibaro to 0.7.8 (#116126) --- homeassistant/components/fibaro/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/fibaro/manifest.json b/homeassistant/components/fibaro/manifest.json index bb1558f998b..39850672d06 100644 --- a/homeassistant/components/fibaro/manifest.json +++ b/homeassistant/components/fibaro/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["pyfibaro"], - "requirements": ["pyfibaro==0.7.7"] + "requirements": ["pyfibaro==0.7.8"] } diff --git a/requirements_all.txt b/requirements_all.txt index b58b5948cad..1e18b1833c0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1818,7 +1818,7 @@ pyevilgenius==2.0.0 pyezviz==0.2.1.2 # homeassistant.components.fibaro -pyfibaro==0.7.7 +pyfibaro==0.7.8 # homeassistant.components.fido pyfido==2.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 75eb06c924d..74205a57b64 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1417,7 +1417,7 @@ pyevilgenius==2.0.0 pyezviz==0.2.1.2 # homeassistant.components.fibaro -pyfibaro==0.7.7 +pyfibaro==0.7.8 # homeassistant.components.fido pyfido==2.1.2 From 74f32cfa90ec66eb8c8dfae596c931e6fc6218db Mon Sep 17 00:00:00 2001 From: On Freund Date: Sat, 27 Apr 2024 10:26:11 +0300 Subject: [PATCH 877/967] Avoid blocking the event loop when unloading Monoprice (#116141) * Avoid blocking the event loop when unloading Monoprice * Code review suggestions --- .../components/monoprice/__init__.py | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/monoprice/__init__.py b/homeassistant/components/monoprice/__init__.py index 57282fb6545..c7683ebedd6 100644 --- a/homeassistant/components/monoprice/__init__.py +++ b/homeassistant/components/monoprice/__init__.py @@ -57,11 +57,25 @@ 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][entry.entry_id][UNDO_UPDATE_LISTENER]() - hass.data[DOMAIN].pop(entry.entry_id) + if not unload_ok: + return False - return unload_ok + hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() + + def _cleanup(monoprice) -> None: + """Destroy the Monoprice object. + + Destroying the Monoprice closes the serial connection, do it in an executor so the garbage + collection does not block. + """ + del monoprice + + monoprice = hass.data[DOMAIN][entry.entry_id][MONOPRICE_OBJECT] + hass.data[DOMAIN].pop(entry.entry_id) + + await hass.async_add_executor_job(_cleanup, monoprice) + + return True async def _update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: From c8d025f52546d117ba6130615d4d8d6a44df5a39 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 25 Apr 2024 13:02:18 +0200 Subject: [PATCH 878/967] Remove deprecation warnings for relative_time (#116144) * Remove deprecation warnings for relative_time * Update homeassistant/helpers/template.py Co-authored-by: Simon <80467011+sorgfresser@users.noreply.github.com> --------- Co-authored-by: Simon <80467011+sorgfresser@users.noreply.github.com> --- .../components/homeassistant/strings.json | 4 --- homeassistant/helpers/template.py | 26 +++---------------- tests/helpers/test_template.py | 6 +---- 3 files changed, 4 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/homeassistant/strings.json b/homeassistant/components/homeassistant/strings.json index 5cdd47d8be4..09b2f17c947 100644 --- a/homeassistant/components/homeassistant/strings.json +++ b/homeassistant/components/homeassistant/strings.json @@ -56,10 +56,6 @@ "config_entry_reauth": { "title": "[%key:common::config_flow::title::reauth%]", "description": "Reauthentication is needed" - }, - "template_function_relative_time_deprecated": { - "title": "The {relative_time} template function is deprecated", - "description": "The {relative_time} template function is deprecated in Home Assistant. Please use the {time_since} or {time_until} template functions instead." } }, "system_health": { diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 335d6842548..ea45ac4e74a 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -59,7 +59,6 @@ from homeassistant.const import ( UnitOfLength, ) from homeassistant.core import ( - DOMAIN as HA_DOMAIN, Context, HomeAssistant, State, @@ -2480,30 +2479,11 @@ def relative_time(hass: HomeAssistant, value: Any) -> Any: Make sure date is not in the future, or else it will return None. If the input are not a datetime object the input will be returned unmodified. + + Note: This template function is deprecated in favor of `time_until`, but is still + supported so as not to break old templates. """ - def warn_relative_time_deprecated() -> None: - ir = issue_registry.async_get(hass) - issue_id = "template_function_relative_time_deprecated" - if ir.async_get_issue(HA_DOMAIN, issue_id): - return - issue_registry.async_create_issue( - hass, - HA_DOMAIN, - issue_id, - breaks_in_ha_version="2024.11.0", - is_fixable=False, - severity=issue_registry.IssueSeverity.WARNING, - translation_key=issue_id, - translation_placeholders={ - "relative_time": "relative_time()", - "time_since": "time_since()", - "time_until": "time_until()", - }, - ) - _LOGGER.warning("Template function 'relative_time' is deprecated") - - warn_relative_time_deprecated() if (render_info := _render_info.get()) is not None: render_info.has_time = True diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index d134570d119..a241f6b7234 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -31,7 +31,7 @@ from homeassistant.const import ( UnitOfTemperature, UnitOfVolume, ) -from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant +from homeassistant.core import HomeAssistant from homeassistant.exceptions import TemplateError from homeassistant.helpers import ( area_registry as ar, @@ -2240,7 +2240,6 @@ def test_relative_time(mock_is_safe, hass: HomeAssistant) -> None: """Test relative_time method.""" hass.config.set_time_zone("UTC") now = datetime.strptime("2000-01-01 10:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z") - issue_registry = ir.async_get(hass) relative_time_template = ( '{{relative_time(strptime("2000-01-01 09:00:00", "%Y-%m-%d %H:%M:%S"))}}' ) @@ -2250,9 +2249,6 @@ def test_relative_time(mock_is_safe, hass: HomeAssistant) -> None: hass, ).async_render() assert result == "1 hour" - assert issue_registry.async_get_issue( - HA_DOMAIN, "template_function_relative_time_deprecated" - ) result = template.Template( ( "{{" From 18f1c0c9f3e82b2d62b33f39ad48019ed77081e3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 25 Apr 2024 13:01:41 +0200 Subject: [PATCH 879/967] Fix lying docstring for relative_time template function (#116146) * Fix lying docstring for relative_time template function * Update homeassistant/helpers/template.py Co-authored-by: Martin Hjelmare --------- Co-authored-by: Martin Hjelmare --- homeassistant/helpers/template.py | 3 ++- tests/helpers/test_template.py | 32 +++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index ea45ac4e74a..c12494ba71b 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -2476,7 +2476,8 @@ def relative_time(hass: HomeAssistant, value: Any) -> Any: The age can be in second, minute, hour, day, month or year. Only the biggest unit is considered, e.g. if it's 2 days and 3 hours, "2 days" will be returned. - Make sure date is not in the future, or else it will return None. + If the input datetime is in the future, + the input datetime will be returned. If the input are not a datetime object the input will be returned unmodified. diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index a241f6b7234..1e2e512cf3d 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -2303,6 +2303,38 @@ def test_relative_time(mock_is_safe, hass: HomeAssistant) -> None: ).async_render() assert result == "string" + # Test behavior when current time is same as the input time + result = template.Template( + ( + "{{" + " relative_time(" + " strptime(" + ' "2000-01-01 10:00:00 +00:00",' + ' "%Y-%m-%d %H:%M:%S %z"' + " )" + " )" + "}}" + ), + hass, + ).async_render() + assert result == "0 seconds" + + # Test behavior when the input time is in the future + result = template.Template( + ( + "{{" + " relative_time(" + " strptime(" + ' "2000-01-01 11:00:00 +00:00",' + ' "%Y-%m-%d %H:%M:%S %z"' + " )" + " )" + "}}" + ), + hass, + ).async_render() + assert result == "2000-01-01 11:00:00+00:00" + info = template.Template(relative_time_template, hass).async_render_to_info() assert info.has_time is True From 571c86cb91a48bdc5bcf2fe221598690c21f9bae Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 27 Apr 2024 09:26:26 +0200 Subject: [PATCH 880/967] Handle invalid device type in onewire (#116153) * Make device type optional in onewire * Add comment --- homeassistant/components/onewire/binary_sensor.py | 2 +- homeassistant/components/onewire/model.py | 2 +- homeassistant/components/onewire/onewirehub.py | 10 +++++++--- homeassistant/components/onewire/sensor.py | 4 ++-- homeassistant/components/onewire/switch.py | 2 +- tests/components/onewire/const.py | 8 +++++++- .../onewire/snapshots/test_binary_sensor.ambr | 12 ++++++++++++ tests/components/onewire/snapshots/test_sensor.ambr | 12 ++++++++++++ tests/components/onewire/snapshots/test_switch.ambr | 12 ++++++++++++ 9 files changed, 55 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/onewire/binary_sensor.py b/homeassistant/components/onewire/binary_sensor.py index d2e66609103..3c2ca3529cc 100644 --- a/homeassistant/components/onewire/binary_sensor.py +++ b/homeassistant/components/onewire/binary_sensor.py @@ -117,7 +117,7 @@ def get_entities(onewire_hub: OneWireHub) -> list[OneWireBinarySensor]: device_type = device.type device_info = device.device_info device_sub_type = "std" - if "EF" in family: + if device_type and "EF" in family: device_sub_type = "HobbyBoard" family = device_type diff --git a/homeassistant/components/onewire/model.py b/homeassistant/components/onewire/model.py index 9deaca2d121..a59953dcd25 100644 --- a/homeassistant/components/onewire/model.py +++ b/homeassistant/components/onewire/model.py @@ -16,4 +16,4 @@ class OWDeviceDescription: family: str id: str path: str - type: str + type: str | None diff --git a/homeassistant/components/onewire/onewirehub.py b/homeassistant/components/onewire/onewirehub.py index b01cc6ba3d6..2dc617ba039 100644 --- a/homeassistant/components/onewire/onewirehub.py +++ b/homeassistant/components/onewire/onewirehub.py @@ -45,7 +45,7 @@ DEVICE_MANUFACTURER = { _LOGGER = logging.getLogger(__name__) -def _is_known_device(device_family: str, device_type: str) -> bool: +def _is_known_device(device_family: str, device_type: str | None) -> bool: """Check if device family/type is known to the library.""" if device_family in ("7E", "EF"): # EDS or HobbyBoard return device_type in DEVICE_SUPPORT[device_family] @@ -144,11 +144,15 @@ class OneWireHub: return devices - def _get_device_type(self, device_path: str) -> str: + def _get_device_type(self, device_path: str) -> str | None: """Get device model.""" if TYPE_CHECKING: assert self.owproxy - device_type = self.owproxy.read(f"{device_path}type").decode() + try: + device_type = self.owproxy.read(f"{device_path}type").decode() + except protocol.ProtocolError as exc: + _LOGGER.debug("Unable to read `%stype`: %s", device_path, exc) + return None _LOGGER.debug("read `%stype`: %s", device_path, device_type) if device_type == "EDS": device_type = self.owproxy.read(f"{device_path}device_type").decode() diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index 46f18842d51..3e43df4dddd 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -377,10 +377,10 @@ def get_entities( device_info = device.device_info device_sub_type = "std" device_path = device.path - if "EF" in family: + if device_type and "EF" in family: device_sub_type = "HobbyBoard" family = device_type - elif "7E" in family: + elif device_type and "7E" in family: device_sub_type = "EDS" family = device_type elif "A6" in family: diff --git a/homeassistant/components/onewire/switch.py b/homeassistant/components/onewire/switch.py index 41276218540..94a7d41ab85 100644 --- a/homeassistant/components/onewire/switch.py +++ b/homeassistant/components/onewire/switch.py @@ -178,7 +178,7 @@ def get_entities(onewire_hub: OneWireHub) -> list[OneWireSwitch]: device_id = device.id device_info = device.device_info device_sub_type = "std" - if "EF" in family: + if device_type and "EF" in family: device_sub_type = "HobbyBoard" family = device_type elif "A6" in family: diff --git a/tests/components/onewire/const.py b/tests/components/onewire/const.py index e5f8ac575e9..a1bab9807d5 100644 --- a/tests/components/onewire/const.py +++ b/tests/components/onewire/const.py @@ -1,6 +1,6 @@ """Constants for 1-Wire integration.""" -from pyownet.protocol import Error as ProtocolError +from pyownet.protocol import ProtocolError from homeassistant.components.onewire.const import Platform @@ -58,6 +58,12 @@ MOCK_OWPROXY_DEVICES = { {ATTR_INJECT_READS: b" 248125"}, ], }, + "16.111111111111": { + # Test case for issue #115984, where the device type cannot be read + ATTR_INJECT_READS: [ + ProtocolError(), # read device type + ], + }, "1F.111111111111": { ATTR_INJECT_READS: [ b"DS2409", # read device type diff --git a/tests/components/onewire/snapshots/test_binary_sensor.ambr b/tests/components/onewire/snapshots/test_binary_sensor.ambr index 3123dfb6a5e..999794ec20d 100644 --- a/tests/components/onewire/snapshots/test_binary_sensor.ambr +++ b/tests/components/onewire/snapshots/test_binary_sensor.ambr @@ -219,6 +219,18 @@ }), ]) # --- +# name: test_binary_sensors[16.111111111111] + list([ + ]) +# --- +# name: test_binary_sensors[16.111111111111].1 + list([ + ]) +# --- +# name: test_binary_sensors[16.111111111111].2 + list([ + ]) +# --- # name: test_binary_sensors[1D.111111111111] list([ DeviceRegistryEntrySnapshot({ diff --git a/tests/components/onewire/snapshots/test_sensor.ambr b/tests/components/onewire/snapshots/test_sensor.ambr index aa8c914ece5..59ed167197d 100644 --- a/tests/components/onewire/snapshots/test_sensor.ambr +++ b/tests/components/onewire/snapshots/test_sensor.ambr @@ -278,6 +278,18 @@ }), ]) # --- +# name: test_sensors[16.111111111111] + list([ + ]) +# --- +# name: test_sensors[16.111111111111].1 + list([ + ]) +# --- +# name: test_sensors[16.111111111111].2 + list([ + ]) +# --- # name: test_sensors[1D.111111111111] list([ DeviceRegistryEntrySnapshot({ diff --git a/tests/components/onewire/snapshots/test_switch.ambr b/tests/components/onewire/snapshots/test_switch.ambr index 2ac542d203c..8fd1e2aeef6 100644 --- a/tests/components/onewire/snapshots/test_switch.ambr +++ b/tests/components/onewire/snapshots/test_switch.ambr @@ -351,6 +351,18 @@ }), ]) # --- +# name: test_switches[16.111111111111] + list([ + ]) +# --- +# name: test_switches[16.111111111111].1 + list([ + ]) +# --- +# name: test_switches[16.111111111111].2 + list([ + ]) +# --- # name: test_switches[1D.111111111111] list([ DeviceRegistryEntrySnapshot({ From 0b74f02c4e9700443c5450fd1f1ea28d46dd819c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 25 Apr 2024 10:48:32 +0200 Subject: [PATCH 881/967] Fix language in strict connection guard page (#116154) --- homeassistant/components/http/strict_connection_guard_page.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/http/strict_connection_guard_page.html b/homeassistant/components/http/strict_connection_guard_page.html index 86ea8e00e90..8567e500c9d 100644 --- a/homeassistant/components/http/strict_connection_guard_page.html +++ b/homeassistant/components/http/strict_connection_guard_page.html @@ -123,7 +123,7 @@

You need access

- This device is not known on + This device is not known to Home Assistant.

From 29ab68fd24f82955dd0a1bc4f66631854b94ff82 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 25 Apr 2024 11:21:19 +0200 Subject: [PATCH 882/967] Update unlocked icon for locks (#116157) --- homeassistant/components/lock/icons.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lock/icons.json b/homeassistant/components/lock/icons.json index 1bf48f2ab40..0ce2e70d372 100644 --- a/homeassistant/components/lock/icons.json +++ b/homeassistant/components/lock/icons.json @@ -5,7 +5,7 @@ "state": { "jammed": "mdi:lock-alert", "locking": "mdi:lock-clock", - "unlocked": "mdi:lock-open", + "unlocked": "mdi:lock-open-variant", "unlocking": "mdi:lock-clock" } } @@ -13,6 +13,6 @@ "services": { "lock": "mdi:lock", "open": "mdi:door-open", - "unlock": "mdi:lock-open" + "unlock": "mdi:lock-open-variant" } } From 4612f18186f5ccf4f5b3c44636515f31fc133230 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 26 Apr 2024 08:38:24 +0200 Subject: [PATCH 883/967] Remove early return when validating entity registry items (#116160) --- homeassistant/helpers/entity_registry.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 4e77df49ea6..436fc5a18de 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -636,7 +636,6 @@ def _validate_item( unique_id, report_issue, ) - return if ( disabled_by and disabled_by is not UNDEFINED From 12bce5451ee7fe32c4b560a4fe5a3a268ed04629 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 25 Apr 2024 18:15:57 +0200 Subject: [PATCH 884/967] Revert orjson to 3.9.15 due to segmentation faults (#116168) --- 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 b88f2aefffa..aa29713a849 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -39,7 +39,7 @@ ifaddr==0.2.0 Jinja2==3.1.3 lru-dict==1.3.0 mutagen==1.47.0 -orjson==3.10.1 +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 34c7d648795..0427019a29e 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.1", + "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 34ee8237921..44c60aec07a 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.1 +orjson==3.9.15 packaging>=23.1 pip>=21.3.1 psutil-home-assistant==0.0.1 From 5ac8488d2a0473a667f1191d87f8edea2f4e1541 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Thu, 25 Apr 2024 13:35:29 -0500 Subject: [PATCH 885/967] Update Ollama model names list (#116172) --- homeassistant/components/ollama/const.py | 145 ++++++++++++----------- 1 file changed, 78 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/ollama/const.py b/homeassistant/components/ollama/const.py index 853370066dc..e25ae1f0877 100644 --- a/homeassistant/components/ollama/const.py +++ b/homeassistant/components/ollama/const.py @@ -81,75 +81,86 @@ DEFAULT_MAX_HISTORY = 20 MAX_HISTORY_SECONDS = 60 * 60 # 1 hour MODEL_NAMES = [ # https://ollama.com/library - "gemma", - "llama2", - "mistral", - "mixtral", - "llava", - "neural-chat", - "codellama", - "dolphin-mixtral", - "qwen", - "llama2-uncensored", - "mistral-openorca", - "deepseek-coder", - "nous-hermes2", - "phi", - "orca-mini", - "dolphin-mistral", - "wizard-vicuna-uncensored", - "vicuna", - "tinydolphin", - "llama2-chinese", - "nomic-embed-text", - "openhermes", - "zephyr", - "tinyllama", - "openchat", - "wizardcoder", - "starcoder", - "phind-codellama", - "starcoder2", - "yi", - "orca2", - "falcon", - "wizard-math", - "dolphin-phi", - "starling-lm", - "nous-hermes", - "stable-code", - "medllama2", - "bakllava", - "codeup", - "wizardlm-uncensored", - "solar", - "everythinglm", - "sqlcoder", - "dolphincoder", - "nous-hermes2-mixtral", - "stable-beluga", - "yarn-mistral", - "stablelm2", - "samantha-mistral", - "meditron", - "stablelm-zephyr", - "magicoder", - "yarn-llama2", - "llama-pro", - "deepseek-llm", - "wizard-vicuna", - "codebooga", - "mistrallite", - "all-minilm", - "nexusraven", - "open-orca-platypus2", - "goliath", - "notux", - "megadolphin", "alfred", - "xwinlm", - "wizardlm", + "all-minilm", + "bakllava", + "codebooga", + "codegemma", + "codellama", + "codeqwen", + "codeup", + "command-r", + "command-r-plus", + "dbrx", + "deepseek-coder", + "deepseek-llm", + "dolphin-llama3", + "dolphin-mistral", + "dolphin-mixtral", + "dolphin-phi", + "dolphincoder", "duckdb-nsql", + "everythinglm", + "falcon", + "gemma", + "goliath", + "llama-pro", + "llama2", + "llama2-chinese", + "llama2-uncensored", + "llama3", + "llava", + "magicoder", + "meditron", + "medllama2", + "megadolphin", + "mistral", + "mistral-openorca", + "mistrallite", + "mixtral", + "mxbai-embed-large", + "neural-chat", + "nexusraven", + "nomic-embed-text", "notus", + "notux", + "nous-hermes", + "nous-hermes2", + "nous-hermes2-mixtral", + "open-orca-platypus2", + "openchat", + "openhermes", + "orca-mini", + "orca2", + "phi", + "phi3", + "phind-codellama", + "qwen", + "samantha-mistral", + "snowflake-arctic-embed", + "solar", + "sqlcoder", + "stable-beluga", + "stable-code", + "stablelm-zephyr", + "stablelm2", + "starcoder", + "starcoder2", + "starling-lm", + "tinydolphin", + "tinyllama", + "vicuna", + "wizard-math", + "wizard-vicuna", + "wizard-vicuna-uncensored", + "wizardcoder", + "wizardlm", + "wizardlm-uncensored", + "wizardlm2", + "xwinlm", + "yarn-llama2", + "yarn-mistral", + "yi", + "zephyr", ] DEFAULT_MODEL = "llama2:latest" From e0cc9198aa105ae68d5f1a2efc50c113991b00bb Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 25 Apr 2024 17:32:42 +0200 Subject: [PATCH 886/967] Revert "Return specific group state if there is one" (#116176) Revert "Return specific group state if there is one (#115866)" This reverts commit 350ca48d4c10b2105e1e3513da7137498dd6ad83. --- homeassistant/components/group/entity.py | 95 ++++------------------ homeassistant/components/group/registry.py | 14 +--- tests/components/group/test_init.py | 24 +----- 3 files changed, 24 insertions(+), 109 deletions(-) diff --git a/homeassistant/components/group/entity.py b/homeassistant/components/group/entity.py index 5ac913dde8d..a8fd9027984 100644 --- a/homeassistant/components/group/entity.py +++ b/homeassistant/components/group/entity.py @@ -8,7 +8,7 @@ from collections.abc import Callable, Collection, Mapping import logging from typing import Any -from homeassistant.const import ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.const import ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, STATE_ON from homeassistant.core import ( CALLBACK_TYPE, Event, @@ -131,9 +131,6 @@ class Group(Entity): _unrecorded_attributes = frozenset({ATTR_ENTITY_ID, ATTR_ORDER, ATTR_AUTO}) _attr_should_poll = False - # In case there is only one active domain we use specific ON or OFF - # values, if all ON or OFF states are equal - single_active_domain: str | None tracking: tuple[str, ...] trackable: tuple[str, ...] @@ -290,7 +287,6 @@ class Group(Entity): if not entity_ids: self.tracking = () self.trackable = () - self.single_active_domain = None return registry: GroupIntegrationRegistry = self.hass.data[REG_KEY] @@ -298,22 +294,12 @@ class Group(Entity): tracking: list[str] = [] trackable: list[str] = [] - self.single_active_domain = None - multiple_domains: bool = False for ent_id in entity_ids: ent_id_lower = ent_id.lower() domain = split_entity_id(ent_id_lower)[0] tracking.append(ent_id_lower) - if domain in excluded_domains: - continue - - trackable.append(ent_id_lower) - - if not multiple_domains and self.single_active_domain is None: - self.single_active_domain = domain - if self.single_active_domain != domain: - multiple_domains = True - self.single_active_domain = None + if domain not in excluded_domains: + trackable.append(ent_id_lower) self.trackable = tuple(trackable) self.tracking = tuple(tracking) @@ -409,36 +395,10 @@ class Group(Entity): self._on_off[entity_id] = state in registry.on_off_mapping else: entity_on_state = registry.on_states_by_domain[domain] - self._on_states.update(entity_on_state) + if domain in registry.on_states_by_domain: + self._on_states.update(entity_on_state) self._on_off[entity_id] = state in entity_on_state - def _detect_specific_on_off_state(self, group_is_on: bool) -> set[str]: - """Check if a specific ON or OFF state is possible.""" - # In case the group contains entities of the same domain with the same ON - # or an OFF state (one or more domains), we want to use that specific state. - # If we have more then one ON or OFF state we default to STATE_ON or STATE_OFF. - registry: GroupIntegrationRegistry = self.hass.data[REG_KEY] - active_on_states: set[str] = set() - active_off_states: set[str] = set() - for entity_id in self.trackable: - if (state := self.hass.states.get(entity_id)) is None: - continue - current_state = state.state - if ( - group_is_on - and (domain_on_states := registry.on_states_by_domain.get(state.domain)) - and current_state in domain_on_states - ): - active_on_states.add(current_state) - # If we have more than one on state, the group state - # will result in STATE_ON and we can stop checking - if len(active_on_states) > 1: - break - elif current_state in registry.off_on_mapping: - active_off_states.add(current_state) - - return active_on_states if group_is_on else active_off_states - @callback def _async_update_group_state(self, tr_state: State | None = None) -> None: """Update group state. @@ -465,48 +425,27 @@ class Group(Entity): elif tr_state.attributes.get(ATTR_ASSUMED_STATE): self._assumed_state = True - # If we do not have an on state for any domains - # we use None (which will be STATE_UNKNOWN) - if (num_on_states := len(self._on_states)) == 0: - self._state = None - return - - group_is_on = self.mode(self._on_off.values()) - + num_on_states = len(self._on_states) # If all the entity domains we are tracking # have the same on state we use this state # and its hass.data[REG_KEY].on_off_mapping to off if num_on_states == 1: - on_state = next(iter(self._on_states)) + on_state = list(self._on_states)[0] + # If we do not have an on state for any domains + # we use None (which will be STATE_UNKNOWN) + elif num_on_states == 0: + self._state = None + return # If the entity domains have more than one - # on state, we use STATE_ON/STATE_OFF, unless there is - # only one specific `on` state in use for one specific domain - elif self.single_active_domain and num_on_states: - active_on_states = self._detect_specific_on_off_state(True) - on_state = ( - list(active_on_states)[0] if len(active_on_states) == 1 else STATE_ON - ) - elif group_is_on: + # on state, we use STATE_ON/STATE_OFF + else: on_state = STATE_ON + group_is_on = self.mode(self._on_off.values()) if group_is_on: self._state = on_state - return - - registry: GroupIntegrationRegistry = self.hass.data[REG_KEY] - if ( - active_domain := self.single_active_domain - ) and active_domain in registry.off_state_by_domain: - # If there is only one domain used, - # then we return the off state for that domain.s - self._state = registry.off_state_by_domain[active_domain] else: - active_off_states = self._detect_specific_on_off_state(False) - # If there is one off state in use then we return that specific state, - # also if there a multiple domains involved, e.g. - # person and device_tracker, with a shared state. - self._state = ( - list(active_off_states)[0] if len(active_off_states) == 1 else STATE_OFF - ) + registry: GroupIntegrationRegistry = self.hass.data[REG_KEY] + self._state = registry.on_off_mapping[on_state] def async_get_component(hass: HomeAssistant) -> EntityComponent[Group]: diff --git a/homeassistant/components/group/registry.py b/homeassistant/components/group/registry.py index 474448db68a..6cdb929d60c 100644 --- a/homeassistant/components/group/registry.py +++ b/homeassistant/components/group/registry.py @@ -49,12 +49,9 @@ class GroupIntegrationRegistry: def __init__(self) -> None: """Imitialize registry.""" - self.on_off_mapping: dict[str, dict[str | None, str]] = { - STATE_ON: {None: STATE_OFF} - } + 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.off_state_by_domain: dict[str, str] = {} self.exclude_domains: set[str] = set() def exclude_domain(self) -> None: @@ -63,14 +60,11 @@ class GroupIntegrationRegistry: def on_off_states(self, on_states: set, off_state: str) -> None: """Register on and off states for the current domain.""" - domain = current_domain.get() for on_state in on_states: if on_state not in self.on_off_mapping: - self.on_off_mapping[on_state] = {domain: off_state} - else: - self.on_off_mapping[on_state][domain] = off_state + self.on_off_mapping[on_state] = off_state + if len(on_states) == 1 and off_state not in self.off_on_mapping: self.off_on_mapping[off_state] = list(on_states)[0] - self.on_states_by_domain[domain] = set(on_states) - self.off_state_by_domain[domain] = off_state + self.on_states_by_domain[current_domain.get()] = set(on_states) diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py index b9cdfcb1590..d3f2747933e 100644 --- a/tests/components/group/test_init.py +++ b/tests/components/group/test_init.py @@ -9,7 +9,7 @@ from unittest.mock import patch import pytest -from homeassistant.components import group, vacuum +from homeassistant.components import group from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_FRIENDLY_NAME, @@ -659,24 +659,6 @@ async def test_is_on(hass: HomeAssistant) -> None: (STATE_ON, True), (STATE_OFF, False), ), - ( - ("vacuum", "vacuum"), - # Cleaning is the only on state - (vacuum.STATE_DOCKED, vacuum.STATE_CLEANING), - # Returning is the only on state - (vacuum.STATE_RETURNING, vacuum.STATE_PAUSED), - (vacuum.STATE_CLEANING, True), - (vacuum.STATE_RETURNING, True), - ), - ( - ("vacuum", "vacuum"), - # Multiple on states, so group state will be STATE_ON - (vacuum.STATE_RETURNING, vacuum.STATE_CLEANING), - # Only off states, so group state will be off - (vacuum.STATE_PAUSED, vacuum.STATE_IDLE), - (STATE_ON, True), - (STATE_OFF, False), - ), ], ) async def test_is_on_and_state_mixed_domains( @@ -1238,7 +1220,7 @@ async def test_group_climate_all_cool(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert hass.states.get("group.group_zero").state == "cool" + assert hass.states.get("group.group_zero").state == STATE_ON async def test_group_climate_all_off(hass: HomeAssistant) -> None: @@ -1352,7 +1334,7 @@ async def test_group_vacuum_on(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert hass.states.get("group.group_zero").state == "cleaning" + assert hass.states.get("group.group_zero").state == STATE_ON async def test_device_tracker_not_home(hass: HomeAssistant) -> None: From 1defd18cf56cec25f52e96719f0eccde1929f54a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 25 Apr 2024 12:56:33 -0500 Subject: [PATCH 887/967] Bump govee-ble to 0.31.2 (#116177) changelog: https://github.com/Bluetooth-Devices/govee-ble/compare/v0.31.0...v0.31.2 Fixes some unrelated BLE devices being detected as a GVH5106 --- 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 64feedc44c1..98b802f8233 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.0"] + "requirements": ["govee-ble==0.31.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1e18b1833c0..46d842fb7d1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -980,7 +980,7 @@ goslide-api==0.5.1 gotailwind==0.2.2 # homeassistant.components.govee_ble -govee-ble==0.31.0 +govee-ble==0.31.2 # homeassistant.components.govee_light_local govee-local-api==1.4.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 74205a57b64..c0ddfe00a17 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -803,7 +803,7 @@ googlemaps==2.5.1 gotailwind==0.2.2 # homeassistant.components.govee_ble -govee-ble==0.31.0 +govee-ble==0.31.2 # homeassistant.components.govee_light_local govee-local-api==1.4.4 From 7cabb04bc9163f34a4db75f93e058d7f8fe00775 Mon Sep 17 00:00:00 2001 From: On Freund Date: Thu, 25 Apr 2024 20:43:31 +0300 Subject: [PATCH 888/967] Bump pyrisco to 0.6.1 (#116182) --- homeassistant/components/risco/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/risco/manifest.json b/homeassistant/components/risco/manifest.json index 4c590b95e52..22e73a10d6d 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.0"] + "requirements": ["pyrisco==0.6.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 46d842fb7d1..bb5fbd528bf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2093,7 +2093,7 @@ pyrecswitch==1.0.2 pyrepetierng==0.1.0 # homeassistant.components.risco -pyrisco==0.6.0 +pyrisco==0.6.1 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c0ddfe00a17..4c6f5d590e9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1635,7 +1635,7 @@ pyqwikswitch==0.93 pyrainbird==4.0.2 # homeassistant.components.risco -pyrisco==0.6.0 +pyrisco==0.6.1 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 From 8ac6593b5392d9740552d00b5542e55f72731e2f Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Thu, 25 Apr 2024 14:26:11 -0400 Subject: [PATCH 889/967] Make Roborock listener update thread safe (#116184) Co-authored-by: J. Nick Koston --- homeassistant/components/roborock/device.py | 2 +- tests/components/roborock/test_sensor.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/roborock/device.py b/homeassistant/components/roborock/device.py index 69384d6e23a..6450d849859 100644 --- a/homeassistant/components/roborock/device.py +++ b/homeassistant/components/roborock/device.py @@ -137,4 +137,4 @@ class RoborockCoordinatedEntity( else: self.coordinator.roborock_device_info.props.consumable = value self.coordinator.data = self.coordinator.roborock_device_info.props - self.async_write_ha_state() + self.schedule_update_ha_state() diff --git a/tests/components/roborock/test_sensor.py b/tests/components/roborock/test_sensor.py index 23d16f643b2..88ed6e1098c 100644 --- a/tests/components/roborock/test_sensor.py +++ b/tests/components/roborock/test_sensor.py @@ -89,6 +89,7 @@ async def test_listener_update( ) ] ) + await hass.async_block_till_done() assert hass.states.get("sensor.roborock_s7_maxv_filter_time_left").state == str( FILTER_REPLACE_TIME - 743 ) From 63ef52a312ec77917647444362ea5e39b5b8c250 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 25 Apr 2024 14:07:07 -0500 Subject: [PATCH 890/967] Fix smartthings doing I/O in the event loop to import platforms (#116190) --- homeassistant/components/smartthings/__init__.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index 8136806cd0b..9bfa11d3293 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -28,6 +28,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_loaded_integration +from homeassistant.setup import SetupPhases, async_pause_setup from .config_flow import SmartThingsFlowHandler # noqa: F401 from .const import ( @@ -170,7 +171,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) # Setup device broker - broker = DeviceBroker(hass, entry, token, smart_app, devices, scenes) + with async_pause_setup(hass, SetupPhases.WAIT_IMPORT_PLATFORMS): + # DeviceBroker has a side effect of importing platform + # modules when its created. In the future this should be + # refactored to not do this. + broker = await hass.async_add_import_executor_job( + DeviceBroker, hass, entry, token, smart_app, devices, scenes + ) broker.connect() hass.data[DOMAIN][DATA_BROKERS][entry.entry_id] = broker From a9b9d7f566807f8cb170234b8b007a36bc226182 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 25 Apr 2024 21:20:24 +0200 Subject: [PATCH 891/967] Fix flaky traccar_server tests (#116191) --- .../components/traccar_server/diagnostics.py | 4 +- .../snapshots/test_diagnostics.ambr | 190 +++++++++--------- .../traccar_server/test_diagnostics.py | 14 +- 3 files changed, 110 insertions(+), 98 deletions(-) diff --git a/homeassistant/components/traccar_server/diagnostics.py b/homeassistant/components/traccar_server/diagnostics.py index 80dc7a9c7cd..68f1e4fca8a 100644 --- a/homeassistant/components/traccar_server/diagnostics.py +++ b/homeassistant/components/traccar_server/diagnostics.py @@ -57,7 +57,7 @@ async def async_get_config_entry_diagnostics( "coordinator_data": coordinator.data, "entities": [ { - "enity_id": entity.entity_id, + "entity_id": entity.entity_id, "disabled": entity.disabled, "unit_of_measurement": entity.unit_of_measurement, "state": _entity_state(hass, entity, coordinator), @@ -92,7 +92,7 @@ async def async_get_device_diagnostics( "coordinator_data": coordinator.data, "entities": [ { - "enity_id": entity.entity_id, + "entity_id": entity.entity_id, "disabled": entity.disabled, "unit_of_measurement": entity.unit_of_measurement, "state": _entity_state(hass, entity, coordinator), diff --git a/tests/components/traccar_server/snapshots/test_diagnostics.ambr b/tests/components/traccar_server/snapshots/test_diagnostics.ambr index 89a6416c303..39e67db8df7 100644 --- a/tests/components/traccar_server/snapshots/test_diagnostics.ambr +++ b/tests/components/traccar_server/snapshots/test_diagnostics.ambr @@ -73,7 +73,30 @@ 'entities': list([ dict({ 'disabled': False, - 'enity_id': 'device_tracker.x_wing', + 'entity_id': 'binary_sensor.x_wing_motion', + 'state': dict({ + 'attributes': dict({ + 'device_class': 'motion', + 'friendly_name': 'X-Wing Motion', + }), + 'state': 'off', + }), + 'unit_of_measurement': None, + }), + dict({ + 'disabled': False, + 'entity_id': 'binary_sensor.x_wing_status', + 'state': dict({ + 'attributes': dict({ + 'friendly_name': 'X-Wing Status', + }), + 'state': 'on', + }), + 'unit_of_measurement': None, + }), + dict({ + 'disabled': False, + 'entity_id': 'device_tracker.x_wing', 'state': dict({ 'attributes': dict({ 'category': 'starfighter', @@ -92,30 +115,31 @@ }), dict({ 'disabled': False, - 'enity_id': 'binary_sensor.x_wing_motion', + 'entity_id': 'sensor.x_wing_address', 'state': dict({ 'attributes': dict({ - 'device_class': 'motion', - 'friendly_name': 'X-Wing Motion', + 'friendly_name': 'X-Wing Address', }), - 'state': 'off', + 'state': '**REDACTED**', }), 'unit_of_measurement': None, }), dict({ 'disabled': False, - 'enity_id': 'binary_sensor.x_wing_status', + 'entity_id': 'sensor.x_wing_altitude', 'state': dict({ 'attributes': dict({ - 'friendly_name': 'X-Wing Status', + 'friendly_name': 'X-Wing Altitude', + 'state_class': 'measurement', + 'unit_of_measurement': 'm', }), - 'state': 'on', + 'state': '546841384638', }), - 'unit_of_measurement': None, + 'unit_of_measurement': 'm', }), dict({ 'disabled': False, - 'enity_id': 'sensor.x_wing_battery', + 'entity_id': 'sensor.x_wing_battery', 'state': dict({ 'attributes': dict({ 'device_class': 'battery', @@ -129,7 +153,18 @@ }), dict({ 'disabled': False, - 'enity_id': 'sensor.x_wing_speed', + 'entity_id': 'sensor.x_wing_geofence', + 'state': dict({ + 'attributes': dict({ + 'friendly_name': 'X-Wing Geofence', + }), + 'state': 'Tatooine', + }), + 'unit_of_measurement': None, + }), + dict({ + 'disabled': False, + 'entity_id': 'sensor.x_wing_speed', 'state': dict({ 'attributes': dict({ 'device_class': 'speed', @@ -141,41 +176,6 @@ }), 'unit_of_measurement': 'kn', }), - dict({ - 'disabled': False, - 'enity_id': 'sensor.x_wing_altitude', - 'state': dict({ - 'attributes': dict({ - 'friendly_name': 'X-Wing Altitude', - 'state_class': 'measurement', - 'unit_of_measurement': 'm', - }), - 'state': '546841384638', - }), - 'unit_of_measurement': 'm', - }), - dict({ - 'disabled': False, - 'enity_id': 'sensor.x_wing_address', - 'state': dict({ - 'attributes': dict({ - 'friendly_name': 'X-Wing Address', - }), - 'state': '**REDACTED**', - }), - 'unit_of_measurement': None, - }), - dict({ - 'disabled': False, - 'enity_id': 'sensor.x_wing_geofence', - 'state': dict({ - 'attributes': dict({ - 'friendly_name': 'X-Wing Geofence', - }), - 'state': 'Tatooine', - }), - 'unit_of_measurement': None, - }), ]), 'subscription_status': 'disconnected', }) @@ -254,51 +254,51 @@ 'entities': list([ dict({ 'disabled': True, - 'enity_id': 'binary_sensor.x_wing_motion', + 'entity_id': 'binary_sensor.x_wing_motion', 'state': None, 'unit_of_measurement': None, }), dict({ 'disabled': True, - 'enity_id': 'binary_sensor.x_wing_status', + 'entity_id': 'binary_sensor.x_wing_status', 'state': None, 'unit_of_measurement': None, }), dict({ 'disabled': True, - 'enity_id': 'sensor.x_wing_battery', + 'entity_id': 'device_tracker.x_wing', 'state': None, - 'unit_of_measurement': '%', + 'unit_of_measurement': None, }), dict({ 'disabled': True, - 'enity_id': 'sensor.x_wing_speed', + 'entity_id': 'sensor.x_wing_address', 'state': None, - 'unit_of_measurement': 'kn', + 'unit_of_measurement': None, }), dict({ 'disabled': True, - 'enity_id': 'sensor.x_wing_altitude', + 'entity_id': 'sensor.x_wing_altitude', 'state': None, 'unit_of_measurement': 'm', }), dict({ 'disabled': True, - 'enity_id': 'sensor.x_wing_address', + 'entity_id': 'sensor.x_wing_battery', + 'state': None, + 'unit_of_measurement': '%', + }), + dict({ + 'disabled': True, + 'entity_id': 'sensor.x_wing_geofence', 'state': None, 'unit_of_measurement': None, }), dict({ 'disabled': True, - 'enity_id': 'sensor.x_wing_geofence', + 'entity_id': 'sensor.x_wing_speed', 'state': None, - 'unit_of_measurement': None, - }), - dict({ - 'disabled': True, - 'enity_id': 'device_tracker.x_wing', - 'state': None, - 'unit_of_measurement': None, + 'unit_of_measurement': 'kn', }), ]), 'subscription_status': 'disconnected', @@ -378,49 +378,19 @@ 'entities': list([ dict({ 'disabled': True, - 'enity_id': 'binary_sensor.x_wing_motion', + 'entity_id': 'binary_sensor.x_wing_motion', 'state': None, 'unit_of_measurement': None, }), dict({ 'disabled': True, - 'enity_id': 'binary_sensor.x_wing_status', - 'state': None, - 'unit_of_measurement': None, - }), - dict({ - 'disabled': True, - 'enity_id': 'sensor.x_wing_battery', - 'state': None, - 'unit_of_measurement': '%', - }), - dict({ - 'disabled': True, - 'enity_id': 'sensor.x_wing_speed', - 'state': None, - 'unit_of_measurement': 'kn', - }), - dict({ - 'disabled': True, - 'enity_id': 'sensor.x_wing_altitude', - 'state': None, - 'unit_of_measurement': 'm', - }), - dict({ - 'disabled': True, - 'enity_id': 'sensor.x_wing_address', - 'state': None, - 'unit_of_measurement': None, - }), - dict({ - 'disabled': True, - 'enity_id': 'sensor.x_wing_geofence', + 'entity_id': 'binary_sensor.x_wing_status', 'state': None, 'unit_of_measurement': None, }), dict({ 'disabled': False, - 'enity_id': 'device_tracker.x_wing', + 'entity_id': 'device_tracker.x_wing', 'state': dict({ 'attributes': dict({ 'category': 'starfighter', @@ -437,6 +407,36 @@ }), 'unit_of_measurement': None, }), + dict({ + 'disabled': True, + 'entity_id': 'sensor.x_wing_address', + 'state': None, + 'unit_of_measurement': None, + }), + dict({ + 'disabled': True, + 'entity_id': 'sensor.x_wing_altitude', + 'state': None, + 'unit_of_measurement': 'm', + }), + dict({ + 'disabled': True, + 'entity_id': 'sensor.x_wing_battery', + 'state': None, + 'unit_of_measurement': '%', + }), + dict({ + 'disabled': True, + 'entity_id': 'sensor.x_wing_geofence', + 'state': None, + 'unit_of_measurement': None, + }), + dict({ + 'disabled': True, + 'entity_id': 'sensor.x_wing_speed', + 'state': None, + 'unit_of_measurement': 'kn', + }), ]), 'subscription_status': 'disconnected', }) diff --git a/tests/components/traccar_server/test_diagnostics.py b/tests/components/traccar_server/test_diagnostics.py index 493f0ae92d1..9019cd0ebf1 100644 --- a/tests/components/traccar_server/test_diagnostics.py +++ b/tests/components/traccar_server/test_diagnostics.py @@ -33,6 +33,10 @@ async def test_entry_diagnostics( hass_client, mock_config_entry, ) + # Sort the list of entities + result["entities"] = sorted( + result["entities"], key=lambda entity: entity["entity_id"] + ) assert result == snapshot(name="entry") @@ -64,13 +68,17 @@ async def test_device_diagnostics( device_id=device.id, include_disabled_entities=True, ) - # Enable all entitits to show everything in snapshots + # Enable all entities to show everything in snapshots for entity in entities: entity_registry.async_update_entity(entity.entity_id, disabled_by=None) result = await get_diagnostics_for_device( hass, hass_client, mock_config_entry, device=device ) + # Sort the list of entities + result["entities"] = sorted( + result["entities"], key=lambda entity: entity["entity_id"] + ) assert result == snapshot(name=device.name) @@ -110,5 +118,9 @@ async def test_device_diagnostics_with_disabled_entity( result = await get_diagnostics_for_device( hass, hass_client, mock_config_entry, device=device ) + # Sort the list of entities + result["entities"] = sorted( + result["entities"], key=lambda entity: entity["entity_id"] + ) assert result == snapshot(name=device.name) From 9f84c38f081ba22670aab5ba1d839ba106575ce5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 25 Apr 2024 15:37:38 -0500 Subject: [PATCH 892/967] Bump bluetooth-auto-recovery to 1.4.2 (#116192) --- 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 f6adcbed7d8..ed1e11d8ddd 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -17,7 +17,7 @@ "bleak==0.21.1", "bleak-retry-connector==3.5.0", "bluetooth-adapters==0.19.0", - "bluetooth-auto-recovery==1.4.1", + "bluetooth-auto-recovery==1.4.2", "bluetooth-data-tools==1.19.0", "dbus-fast==2.21.1", "habluetooth==2.8.0" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index aa29713a849..442db45e714 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -18,7 +18,7 @@ bcrypt==4.1.2 bleak-retry-connector==3.5.0 bleak==0.21.1 bluetooth-adapters==0.19.0 -bluetooth-auto-recovery==1.4.1 +bluetooth-auto-recovery==1.4.2 bluetooth-data-tools==1.19.0 cached_ipaddress==0.3.0 certifi>=2021.5.30 diff --git a/requirements_all.txt b/requirements_all.txt index bb5fbd528bf..6ab10019d78 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -582,7 +582,7 @@ bluemaestro-ble==0.2.3 bluetooth-adapters==0.19.0 # homeassistant.components.bluetooth -bluetooth-auto-recovery==1.4.1 +bluetooth-auto-recovery==1.4.2 # homeassistant.components.bluetooth # homeassistant.components.ld2410_ble diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4c6f5d590e9..bc69a55c955 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -497,7 +497,7 @@ bluemaestro-ble==0.2.3 bluetooth-adapters==0.19.0 # homeassistant.components.bluetooth -bluetooth-auto-recovery==1.4.1 +bluetooth-auto-recovery==1.4.2 # homeassistant.components.bluetooth # homeassistant.components.ld2410_ble From 1be5249269b0699a7b9009db3f73b148876b1659 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 26 Apr 2024 05:00:07 +0200 Subject: [PATCH 893/967] Reduce scope of bootstrap test fixture to module (#116195) --- tests/test_bootstrap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 2e35e4ffddb..96caf5d10c8 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -44,7 +44,7 @@ async def apply_stop_hass(stop_hass: None) -> None: """Make sure all hass are stopped.""" -@pytest.fixture(scope="session", autouse=True) +@pytest.fixture(scope="module", autouse=True) def mock_http_start_stop() -> Generator[None, None, None]: """Mock HTTP start and stop.""" with ( From 8f02ed4bf3a9699b710a44ae3eee5c6dd7c150e5 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Fri, 26 Apr 2024 23:44:13 +1000 Subject: [PATCH 894/967] Breakfix to handle null value in Teslemetry (#116206) * Fixes * Remove unused test --- homeassistant/components/teslemetry/coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/teslemetry/coordinator.py b/homeassistant/components/teslemetry/coordinator.py index 75794c7cdec..be34386a508 100644 --- a/homeassistant/components/teslemetry/coordinator.py +++ b/homeassistant/components/teslemetry/coordinator.py @@ -119,7 +119,7 @@ class TeslemetryEnergyDataCoordinator(TeslemetryDataCoordinator): # Convert Wall Connectors from array to dict data["response"]["wall_connectors"] = { - wc["din"]: wc for wc in data["response"].get("wall_connectors", []) + wc["din"]: wc for wc in (data["response"].get("wall_connectors") or []) } return data["response"] From 5fb08e8b256b3ba486340bf34b5e7028df14a6e0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 26 Apr 2024 12:28:40 +0200 Subject: [PATCH 895/967] Restore default timezone after electric_kiwi sensor tests (#116217) --- tests/components/electric_kiwi/conftest.py | 3 --- tests/components/electric_kiwi/test_sensor.py | 19 ++++++++++++++++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/tests/components/electric_kiwi/conftest.py b/tests/components/electric_kiwi/conftest.py index 8819b1e134d..8052ae5e129 100644 --- a/tests/components/electric_kiwi/conftest.py +++ b/tests/components/electric_kiwi/conftest.py @@ -5,7 +5,6 @@ from __future__ import annotations from collections.abc import Awaitable, Callable, Generator from time import time from unittest.mock import AsyncMock, patch -import zoneinfo from electrickiwi_api.model import AccountBalance, Hop, HopIntervals import pytest @@ -24,8 +23,6 @@ CLIENT_ID = "1234" CLIENT_SECRET = "5678" REDIRECT_URI = "https://example.com/auth/external/callback" -TZ_NAME = "Pacific/Auckland" -TIMEZONE = zoneinfo.ZoneInfo(TZ_NAME) YieldFixture = Generator[AsyncMock, None, None] ComponentSetup = Callable[[], Awaitable[bool]] diff --git a/tests/components/electric_kiwi/test_sensor.py b/tests/components/electric_kiwi/test_sensor.py index f91e4d9c58c..a247497b263 100644 --- a/tests/components/electric_kiwi/test_sensor.py +++ b/tests/components/electric_kiwi/test_sensor.py @@ -2,6 +2,7 @@ from datetime import UTC, datetime from unittest.mock import AsyncMock, Mock +import zoneinfo from freezegun import freeze_time import pytest @@ -19,10 +20,22 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_registry import EntityRegistry import homeassistant.util.dt as dt_util -from .conftest import TIMEZONE, ComponentSetup, YieldFixture +from .conftest import ComponentSetup, YieldFixture from tests.common import MockConfigEntry +DEFAULT_TIME_ZONE = dt_util.DEFAULT_TIME_ZONE +TEST_TZ_NAME = "Pacific/Auckland" +TEST_TIMEZONE = zoneinfo.ZoneInfo(TEST_TZ_NAME) + + +@pytest.fixture(autouse=True) +def restore_timezone(): + """Restore default timezone.""" + yield + + dt_util.set_default_time_zone(DEFAULT_TIME_ZONE) + @pytest.mark.parametrize( ("sensor", "sensor_state"), @@ -124,8 +137,8 @@ async def test_check_and_move_time(ek_api: AsyncMock) -> None: """Test correct time is returned depending on time of day.""" hop = await ek_api(Mock()).get_hop() - test_time = datetime(2023, 6, 21, 18, 0, 0, tzinfo=TIMEZONE) - dt_util.set_default_time_zone(TIMEZONE) + test_time = datetime(2023, 6, 21, 18, 0, 0, tzinfo=TEST_TIMEZONE) + dt_util.set_default_time_zone(TEST_TIMEZONE) with freeze_time(test_time): value = _check_and_move_time(hop, "4:00 PM") From 2861ac4ac9a0f21c02856c297ec46e624f20ad59 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Fri, 26 Apr 2024 11:22:04 +0200 Subject: [PATCH 896/967] Use None as default value for strict connection cloud store (#116219) --- homeassistant/components/cloud/prefs.py | 15 +++++++++------ tests/components/cloud/test_prefs.py | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index 9fce615128b..72207513ca9 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -365,13 +365,16 @@ class CloudPreferences: @property def strict_connection(self) -> http.const.StrictConnectionMode: """Return the strict connection mode.""" - mode = self._prefs.get( - PREF_STRICT_CONNECTION, http.const.StrictConnectionMode.DISABLED - ) + mode = self._prefs.get(PREF_STRICT_CONNECTION) - if not isinstance(mode, http.const.StrictConnectionMode): + if mode is None: + # Set to default value + # We store None in the store as the default value to detect if the user has changed the + # value or not. + mode = http.const.StrictConnectionMode.DISABLED + elif not isinstance(mode, http.const.StrictConnectionMode): mode = http.const.StrictConnectionMode(mode) - return mode # type: ignore[no-any-return] + return mode async def get_cloud_user(self) -> str: """Return ID of Home Assistant Cloud system user.""" @@ -430,5 +433,5 @@ class CloudPreferences: PREF_REMOTE_DOMAIN: None, PREF_REMOTE_ALLOW_REMOTE_ENABLE: True, PREF_USERNAME: username, - PREF_STRICT_CONNECTION: http.const.StrictConnectionMode.DISABLED, + PREF_STRICT_CONNECTION: None, } diff --git a/tests/components/cloud/test_prefs.py b/tests/components/cloud/test_prefs.py index 1ed2e1d524f..57715fe2bdf 100644 --- a/tests/components/cloud/test_prefs.py +++ b/tests/components/cloud/test_prefs.py @@ -197,3 +197,21 @@ async def test_strict_connection_convertion( await hass.async_block_till_done() assert cloud.client.prefs.strict_connection is mode + + +@pytest.mark.parametrize("storage_data", [{}, {PREF_STRICT_CONNECTION: None}]) +async def test_strict_connection_default( + hass: HomeAssistant, + cloud: MagicMock, + hass_storage: dict[str, Any], + storage_data: dict[str, Any], +) -> None: + """Test strict connection default values.""" + hass_storage[STORAGE_KEY] = { + "version": 1, + "data": storage_data, + } + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + + assert cloud.client.prefs.strict_connection is StrictConnectionMode.DISABLED From e9c4185cf64bd258ce85d668b78f643a0d30918c Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Fri, 26 Apr 2024 13:03:16 +0100 Subject: [PATCH 897/967] Fix state classes for ovo energy sensors (#116225) * Fix state classes for ovo energy sensors * Restore monetary values Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --------- Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- homeassistant/components/ovo_energy/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ovo_energy/sensor.py b/homeassistant/components/ovo_energy/sensor.py index 5b16e8cdef5..3012a130a1a 100644 --- a/homeassistant/components/ovo_energy/sensor.py +++ b/homeassistant/components/ovo_energy/sensor.py @@ -54,7 +54,7 @@ SENSOR_TYPES_ELECTRICITY: tuple[OVOEnergySensorEntityDescription, ...] = ( key=KEY_LAST_ELECTRICITY_COST, translation_key=KEY_LAST_ELECTRICITY_COST, device_class=SensorDeviceClass.MONETARY, - state_class=SensorStateClass.TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL, value=lambda usage: usage.electricity[-1].cost.amount if usage.electricity[-1].cost is not None else None, @@ -88,7 +88,7 @@ SENSOR_TYPES_GAS: tuple[OVOEnergySensorEntityDescription, ...] = ( key=KEY_LAST_GAS_COST, translation_key=KEY_LAST_GAS_COST, device_class=SensorDeviceClass.MONETARY, - state_class=SensorStateClass.TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL, value=lambda usage: usage.gas[-1].cost.amount if usage.gas[-1].cost is not None else None, From 603f46184cc4b9d722b2bcf8d38e092c61174886 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Fri, 26 Apr 2024 15:40:32 +0200 Subject: [PATCH 898/967] Update frontend to 20240426.0 (#116230) --- 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 ad63bdbed84..a5446f688ba 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==20240424.1"] + "requirements": ["home-assistant-frontend==20240426.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 442db45e714..1b4223d7b33 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ habluetooth==2.8.0 hass-nabucasa==0.78.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240424.1 +home-assistant-frontend==20240426.0 home-assistant-intents==2024.4.24 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 6ab10019d78..ebef89bd0e0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1078,7 +1078,7 @@ hole==0.8.0 holidays==0.47 # homeassistant.components.frontend -home-assistant-frontend==20240424.1 +home-assistant-frontend==20240426.0 # homeassistant.components.conversation home-assistant-intents==2024.4.24 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bc69a55c955..18b9c0c31e3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -880,7 +880,7 @@ hole==0.8.0 holidays==0.47 # homeassistant.components.frontend -home-assistant-frontend==20240424.1 +home-assistant-frontend==20240426.0 # homeassistant.components.conversation home-assistant-intents==2024.4.24 From 8d11a9f21aa2c8b187ae73d49437663275a3a760 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Apr 2024 02:25:19 -0500 Subject: [PATCH 899/967] Move thread safety check in entity_registry sooner (#116263) * Move thread safety check in entity_registry sooner It turns out we have a lot of custom components that are writing to the entity registry using the async APIs from threads. We now catch it at the point async_fire is called. Instread we should check sooner and use async_fire_internal so we catch the unsafe operation before it can corrupt the registry. * coverage * Apply suggestions from code review --- homeassistant/helpers/entity_registry.py | 10 ++++-- tests/helpers/test_entity_registry.py | 44 ++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 436fc5a18de..589b379cf08 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -819,6 +819,7 @@ class EntityRegistry(BaseRegistry): unit_of_measurement=unit_of_measurement, ) + self.hass.verify_event_loop_thread("async_get_or_create") _validate_item( self.hass, domain, @@ -879,7 +880,7 @@ class EntityRegistry(BaseRegistry): _LOGGER.info("Registered new %s.%s entity: %s", domain, platform, entity_id) self.async_schedule_save() - self.hass.bus.async_fire( + self.hass.bus.async_fire_internal( EVENT_ENTITY_REGISTRY_UPDATED, _EventEntityRegistryUpdatedData_CreateRemove( action="create", entity_id=entity_id @@ -891,6 +892,7 @@ class EntityRegistry(BaseRegistry): @callback def async_remove(self, entity_id: str) -> None: """Remove an entity from registry.""" + self.hass.verify_event_loop_thread("async_remove") entity = self.entities.pop(entity_id) config_entry_id = entity.config_entry_id key = (entity.domain, entity.platform, entity.unique_id) @@ -904,7 +906,7 @@ class EntityRegistry(BaseRegistry): platform=entity.platform, unique_id=entity.unique_id, ) - self.hass.bus.async_fire( + self.hass.bus.async_fire_internal( EVENT_ENTITY_REGISTRY_UPDATED, _EventEntityRegistryUpdatedData_CreateRemove( action="remove", entity_id=entity_id @@ -1085,6 +1087,8 @@ class EntityRegistry(BaseRegistry): if not new_values: return old + self.hass.verify_event_loop_thread("_async_update_entity") + new = self.entities[entity_id] = attr.evolve(old, **new_values) self.async_schedule_save() @@ -1098,7 +1102,7 @@ class EntityRegistry(BaseRegistry): if old.entity_id != entity_id: data["old_entity_id"] = old.entity_id - self.hass.bus.async_fire(EVENT_ENTITY_REGISTRY_UPDATED, data) + self.hass.bus.async_fire_internal(EVENT_ENTITY_REGISTRY_UPDATED, data) return new diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 60971d98df2..bc3b2d6f705 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -1,6 +1,7 @@ """Tests for the Entity Registry.""" from datetime import timedelta +from functools import partial from typing import Any from unittest.mock import patch @@ -1988,3 +1989,46 @@ async def test_entries_for_category(entity_registry: er.EntityRegistry) -> None: assert not er.async_entries_for_category(entity_registry, "", "id") assert not er.async_entries_for_category(entity_registry, "scope1", "unknown") assert not er.async_entries_for_category(entity_registry, "scope1", "") + + +async def test_get_or_create_thread_safety( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: + """Test call async_get_or_create_from a thread.""" + with pytest.raises( + RuntimeError, + match="Detected code that calls async_get_or_create from a thread. Please report this issue.", + ): + await hass.async_add_executor_job( + entity_registry.async_get_or_create, "light", "hue", "1234" + ) + + +async def test_async_update_entity_thread_safety( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: + """Test call async_get_or_create from a thread.""" + entry = entity_registry.async_get_or_create("light", "hue", "1234") + with pytest.raises( + RuntimeError, + match="Detected code that calls _async_update_entity from a thread. Please report this issue.", + ): + await hass.async_add_executor_job( + partial( + entity_registry.async_update_entity, + entry.entity_id, + new_unique_id="5678", + ) + ) + + +async def test_async_remove_thread_safety( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: + """Test call async_remove from a thread.""" + entry = entity_registry.async_get_or_create("light", "hue", "1234") + with pytest.raises( + RuntimeError, + match="Detected code that calls async_remove from a thread. Please report this issue.", + ): + await hass.async_add_executor_job(entity_registry.async_remove, entry.entity_id) From 85baa2508d4f82a110cc9a7d171dd3de779ebbef Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Apr 2024 02:24:55 -0500 Subject: [PATCH 900/967] Move thread safety check in device_registry sooner (#116264) It turns out we have custom components that are writing to the device registry using the async APIs from threads. We now catch it at the point async_fire is called. Instead we should check sooner and use async_fire_internal so we catch the unsafe operation before it can corrupt the registry. --- homeassistant/helpers/device_registry.py | 6 ++- tests/helpers/test_device_registry.py | 47 ++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 00d0a0ba62f..0e64540f11a 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -904,6 +904,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): if not new_values: return old + self.hass.verify_event_loop_thread("async_update_device") new = attr.evolve(old, **new_values) self.devices[device_id] = new @@ -923,13 +924,14 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): else: data = {"action": "update", "device_id": new.id, "changes": old_values} - self.hass.bus.async_fire(EVENT_DEVICE_REGISTRY_UPDATED, data) + self.hass.bus.async_fire_internal(EVENT_DEVICE_REGISTRY_UPDATED, data) return new @callback def async_remove_device(self, device_id: str) -> None: """Remove a device from the device registry.""" + self.hass.verify_event_loop_thread("async_remove_device") device = self.devices.pop(device_id) self.deleted_devices[device_id] = DeletedDeviceEntry( config_entries=device.config_entries, @@ -941,7 +943,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): for other_device in list(self.devices.values()): if other_device.via_device_id == device_id: self.async_update_device(other_device.id, via_device_id=None) - self.hass.bus.async_fire( + self.hass.bus.async_fire_internal( EVENT_DEVICE_REGISTRY_UPDATED, _EventDeviceRegistryUpdatedData_CreateRemove( action="remove", device_id=device_id diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index ee895e3fd3e..6b167f8ee49 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -2,6 +2,7 @@ from collections.abc import Iterable from contextlib import AbstractContextManager, nullcontext +from functools import partial import time from typing import Any from unittest.mock import patch @@ -2473,3 +2474,49 @@ async def test_device_name_translation_placeholders_errors( ) assert expected_error in caplog.text + + +async def test_async_get_or_create_thread_safety( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + mock_config_entry: MockConfigEntry, +) -> None: + """Test async_get_or_create raises when called from wrong thread.""" + + with pytest.raises( + RuntimeError, + match="Detected code that calls async_update_device from a thread. Please report this issue.", + ): + await hass.async_add_executor_job( + partial( + 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", + ) + ) + + +async def test_async_remove_device_thread_safety( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + mock_config_entry: MockConfigEntry, +) -> None: + """Test async_remove_device raises when called from wrong thread.""" + 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", + ) + + with pytest.raises( + RuntimeError, + match="Detected code that calls async_remove_device from a thread. Please report this issue.", + ): + await hass.async_add_executor_job( + device_registry.async_remove_device, device.id + ) From 46dff86d1adbbd253c21ca21f03a0a8f41a91188 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Apr 2024 02:26:35 -0500 Subject: [PATCH 901/967] Move thread safety check in area_registry sooner (#116265) It turns out we have custom components that are writing to the area registry using the async APIs from threads. We now catch it at the point async_fire is called. Instead we should check sooner and use async_fire_internal so we catch the unsafe operation before it can corrupt the registry. --- homeassistant/helpers/area_registry.py | 11 ++++++-- tests/helpers/test_area_registry.py | 38 ++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py index b39fee9c185..4dba510396f 100644 --- a/homeassistant/helpers/area_registry.py +++ b/homeassistant/helpers/area_registry.py @@ -202,6 +202,7 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]): picture: str | None = None, ) -> AreaEntry: """Create a new area.""" + self.hass.verify_event_loop_thread("async_create") normalized_name = normalize_name(name) if self.async_get_area_by_name(name): @@ -221,7 +222,7 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]): assert area.id is not None self.areas[area.id] = area self.async_schedule_save() - self.hass.bus.async_fire( + self.hass.bus.async_fire_internal( EVENT_AREA_REGISTRY_UPDATED, EventAreaRegistryUpdatedData(action="create", area_id=area.id), ) @@ -230,6 +231,7 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]): @callback def async_delete(self, area_id: str) -> None: """Delete area.""" + self.hass.verify_event_loop_thread("async_delete") device_registry = dr.async_get(self.hass) entity_registry = er.async_get(self.hass) device_registry.async_clear_area_id(area_id) @@ -237,7 +239,7 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]): del self.areas[area_id] - self.hass.bus.async_fire( + self.hass.bus.async_fire_internal( EVENT_AREA_REGISTRY_UPDATED, EventAreaRegistryUpdatedData(action="remove", area_id=area_id), ) @@ -266,6 +268,10 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]): name=name, picture=picture, ) + # Since updated may be the old or the new and we always fire + # an event even if nothing has changed we cannot use async_fire_internal + # here because we do not know if the thread safety check already + # happened or not in _async_update. self.hass.bus.async_fire( EVENT_AREA_REGISTRY_UPDATED, EventAreaRegistryUpdatedData(action="update", area_id=area_id), @@ -306,6 +312,7 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]): if not new_values: return old + self.hass.verify_event_loop_thread("_async_update") new = self.areas[area_id] = dataclasses.replace(old, **new_values) # type: ignore[arg-type] self.async_schedule_save() diff --git a/tests/helpers/test_area_registry.py b/tests/helpers/test_area_registry.py index 1ee8a42b6b9..22f1dc8e534 100644 --- a/tests/helpers/test_area_registry.py +++ b/tests/helpers/test_area_registry.py @@ -1,5 +1,6 @@ """Tests for the Area Registry.""" +from functools import partial from typing import Any import pytest @@ -491,3 +492,40 @@ async def test_entries_for_label( assert not ar.async_entries_for_label(area_registry, "unknown") assert not ar.async_entries_for_label(area_registry, "") + + +async def test_async_get_or_create_thread_checks( + hass: HomeAssistant, area_registry: ar.AreaRegistry +) -> None: + """We raise when trying to create in the wrong thread.""" + with pytest.raises( + RuntimeError, + match="Detected code that calls async_create from a thread. Please report this issue.", + ): + await hass.async_add_executor_job(area_registry.async_create, "Mock1") + + +async def test_async_update_thread_checks( + hass: HomeAssistant, area_registry: ar.AreaRegistry +) -> None: + """We raise when trying to update in the wrong thread.""" + area = area_registry.async_create("Mock1") + with pytest.raises( + RuntimeError, + match="Detected code that calls _async_update from a thread. Please report this issue.", + ): + await hass.async_add_executor_job( + partial(area_registry.async_update, area.id, name="Mock2") + ) + + +async def test_async_delete_thread_checks( + hass: HomeAssistant, area_registry: ar.AreaRegistry +) -> None: + """We raise when trying to delete in the wrong thread.""" + area = area_registry.async_create("Mock1") + with pytest.raises( + RuntimeError, + match="Detected code that calls async_delete from a thread. Please report this issue.", + ): + await hass.async_add_executor_job(area_registry.async_delete, area.id) From 3c48c4173494894fd8e7561865e94346e9fe232b Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sat, 27 Apr 2024 03:24:23 -0400 Subject: [PATCH 902/967] Bump zwave-js-server-python to 0.55.4 (#116278) --- 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 a06de5cb8ee..83a139331bb 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.55.3"], + "requirements": ["pyserial==3.5", "zwave-js-server-python==0.55.4"], "usb": [ { "vid": "0658", diff --git a/requirements_all.txt b/requirements_all.txt index ebef89bd0e0..d78a00ca68e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2965,7 +2965,7 @@ zigpy==0.64.0 zm-py==0.5.4 # homeassistant.components.zwave_js -zwave-js-server-python==0.55.3 +zwave-js-server-python==0.55.4 # homeassistant.components.zwave_me zwave-me-ws==0.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 18b9c0c31e3..ed5ec38af1d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2303,7 +2303,7 @@ zigpy-znp==0.12.1 zigpy==0.64.0 # homeassistant.components.zwave_js -zwave-js-server-python==0.55.3 +zwave-js-server-python==0.55.4 # homeassistant.components.zwave_me zwave-me-ws==0.4.3 From 9819cdfec22fc99d78e7570676b977f66f023011 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 27 Apr 2024 07:27:57 +0000 Subject: [PATCH 903/967] Bump version to 2024.5.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 1abfe08b93c..a56405d810a 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 = 5 -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 0427019a29e..fc2f658a9c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.5.0b0" +version = "2024.5.0b1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From ee4f55a5a94fafda384cad595bbd88728d6f5218 Mon Sep 17 00:00:00 2001 From: Marco van 't Wout Date: Mon, 29 Apr 2024 12:02:49 +0200 Subject: [PATCH 904/967] Improve error handling for HTTP errors on Growatt Server (#110633) * Update dependency growattServer for improved error details Updating to latest version. Since version 1.3.1 it will raise requests.exceptions.HTTPError for unexpected API responses such as HTTP 405 (rate limiting/firewall) * Improve error details by raising ConfigEntryAuthFailed Previous code was returning None which the caller couldn't handle * Use a more appropiate exception type * Update homeassistant/components/growatt_server/sensor.py * Update homeassistant/components/growatt_server/sensor.py * Fix --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/growatt_server/manifest.json | 2 +- homeassistant/components/growatt_server/sensor.py | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/growatt_server/manifest.json b/homeassistant/components/growatt_server/manifest.json index d872474f1da..98ceb35ee17 100644 --- a/homeassistant/components/growatt_server/manifest.json +++ b/homeassistant/components/growatt_server/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/growatt_server", "iot_class": "cloud_polling", "loggers": ["growattServer"], - "requirements": ["growattServer==1.3.0"] + "requirements": ["growattServer==1.5.0"] } diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index 3cf1fa30c99..c41d3ac486f 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -12,6 +12,7 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_URL, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryError from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import Throttle, dt as dt_util @@ -46,8 +47,7 @@ def get_device_list(api, config): not login_response["success"] and login_response["msg"] == LOGIN_INVALID_AUTH_CODE ): - _LOGGER.error("Username, Password or URL may be incorrect!") - return + raise ConfigEntryError("Username, Password or URL may be incorrect!") user_id = login_response["user"]["id"] if plant_id == DEFAULT_PLANT_ID: plant_info = api.plant_list(user_id) diff --git a/requirements_all.txt b/requirements_all.txt index d78a00ca68e..205907c1288 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1004,7 +1004,7 @@ greenwavereality==0.5.1 gridnet==5.0.0 # homeassistant.components.growatt_server -growattServer==1.3.0 +growattServer==1.5.0 # homeassistant.components.google_sheets gspread==5.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ed5ec38af1d..5e5aecc63e0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -821,7 +821,7 @@ greeneye_monitor==3.0.3 gridnet==5.0.0 # homeassistant.components.growatt_server -growattServer==1.3.0 +growattServer==1.5.0 # homeassistant.components.google_sheets gspread==5.5.0 From 6d8066afa2d767350753b3a64dbe335455bcce8f Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 29 Apr 2024 10:59:36 +0200 Subject: [PATCH 905/967] Add matter during onboarding (#116163) * Add matter during onboarding * test_zeroconf_not_onboarded_running * test_zeroconf_not_onboarded_installed * test_zeroconf_not_onboarded_not_installed * test_zeroconf_discovery_not_onboarded_not_supervisor * Clean up * Add udp address * Test zeroconf udp info too * test_addon_installed_failures_zeroconf * test_addon_running_failures_zeroconf * test_addon_not_installed_failures_zeroconf * Clean up stale changes * Set unique id for discovery step * Fix tests for background flow * Fix flow running in background * Test already discovered zeroconf * Mock unload entry --- .../components/matter/config_flow.py | 27 +- homeassistant/components/matter/manifest.json | 2 +- homeassistant/generated/zeroconf.py | 5 + tests/components/matter/test_config_flow.py | 414 +++++++++++++++++- 4 files changed, 436 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/matter/config_flow.py b/homeassistant/components/matter/config_flow.py index 7dc06807a98..b079dcd9b54 100644 --- a/homeassistant/components/matter/config_flow.py +++ b/homeassistant/components/matter/config_flow.py @@ -17,6 +17,8 @@ from homeassistant.components.hassio import ( HassioServiceInfo, is_hassio, ) +from homeassistant.components.onboarding import async_is_onboarded +from homeassistant.components.zeroconf import ZeroconfServiceInfo from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant @@ -64,6 +66,7 @@ class MatterConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Set up flow instance.""" + self._running_in_background = False self.ws_address: str | None = None # If we install the add-on we should uninstall it on entry remove. self.integration_created_addon = False @@ -78,7 +81,7 @@ class MatterConfigFlow(ConfigFlow, domain=DOMAIN): if not self.install_task: self.install_task = self.hass.async_create_task(self._async_install_addon()) - if not self.install_task.done(): + if not self._running_in_background and not self.install_task.done(): return self.async_show_progress( step_id="install_addon", progress_action="install_addon", @@ -89,12 +92,16 @@ class MatterConfigFlow(ConfigFlow, domain=DOMAIN): await self.install_task except AddonError as err: LOGGER.error(err) + if self._running_in_background: + return await self.async_step_install_failed() return self.async_show_progress_done(next_step_id="install_failed") finally: self.install_task = None self.integration_created_addon = True + if self._running_in_background: + return await self.async_step_start_addon() return self.async_show_progress_done(next_step_id="start_addon") async def async_step_install_failed( @@ -125,7 +132,7 @@ class MatterConfigFlow(ConfigFlow, domain=DOMAIN): """Start Matter Server add-on.""" if not self.start_task: self.start_task = self.hass.async_create_task(self._async_start_addon()) - if not self.start_task.done(): + if not self._running_in_background and not self.start_task.done(): return self.async_show_progress( step_id="start_addon", progress_action="start_addon", @@ -136,10 +143,14 @@ class MatterConfigFlow(ConfigFlow, domain=DOMAIN): await self.start_task except (FailedConnect, AddonError, AbortFlow) as err: LOGGER.error(err) + if self._running_in_background: + return await self.async_step_start_failed() return self.async_show_progress_done(next_step_id="start_failed") finally: self.start_task = None + if self._running_in_background: + return await self.async_step_finish_addon_setup() return self.async_show_progress_done(next_step_id="finish_addon_setup") async def async_step_start_failed( @@ -223,6 +234,18 @@ class MatterConfigFlow(ConfigFlow, domain=DOMAIN): step_id="manual", data_schema=get_manual_schema(user_input), errors=errors ) + async def async_step_zeroconf( + self, discovery_info: ZeroconfServiceInfo + ) -> ConfigFlowResult: + """Handle zeroconf discovery.""" + if not async_is_onboarded(self.hass) and is_hassio(self.hass): + await self._async_handle_discovery_without_unique_id() + self._running_in_background = True + return await self.async_step_on_supervisor( + user_input={CONF_USE_ADDON: True} + ) + return await self._async_step_discovery_without_unique_id() + async def async_step_hassio( self, discovery_info: HassioServiceInfo ) -> ConfigFlowResult: diff --git a/homeassistant/components/matter/manifest.json b/homeassistant/components/matter/manifest.json index 0e27eb36f85..b3acc0d547c 100644 --- a/homeassistant/components/matter/manifest.json +++ b/homeassistant/components/matter/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/matter", "iot_class": "local_push", "requirements": ["python-matter-server==5.7.0"], - "zeroconf": ["_matter._tcp.local."] + "zeroconf": ["_matter._tcp.local.", "_matterc._udp.local."] } diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 3441026994b..7b1bbff9de0 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -608,6 +608,11 @@ ZEROCONF = { "domain": "matter", }, ], + "_matterc._udp.local.": [ + { + "domain": "matter", + }, + ], "_mediaremotetv._tcp.local.": [ { "domain": "apple_tv", diff --git a/tests/components/matter/test_config_flow.py b/tests/components/matter/test_config_flow.py index 33e94a743f7..39ae40172c1 100644 --- a/tests/components/matter/test_config_flow.py +++ b/tests/components/matter/test_config_flow.py @@ -24,6 +24,37 @@ ADDON_DISCOVERY_INFO = { "host": "host1", "port": 5581, } +ZEROCONF_INFO_TCP = ZeroconfServiceInfo( + ip_address=ip_address("fd11:be53:8d46:0:729e:5a4f:539d:1ee6"), + ip_addresses=[ip_address("fd11:be53:8d46:0:729e:5a4f:539d:1ee6")], + port=5540, + hostname="CDEFGHIJ12345678.local.", + type="_matter._tcp.local.", + name="ABCDEFGH123456789-0000000012345678._matter._tcp.local.", + properties={"SII": "3300", "SAI": "1100", "T": "0"}, +) + +ZEROCONF_INFO_UDP = ZeroconfServiceInfo( + ip_address=ip_address("fd11:be53:8d46:0:729e:5a4f:539d:1ee6"), + ip_addresses=[ip_address("fd11:be53:8d46:0:729e:5a4f:539d:1ee6")], + port=5540, + hostname="CDEFGHIJ12345678.local.", + type="_matterc._udp.local.", + name="ABCDEFGH123456789._matterc._udp.local.", + properties={ + "VP": "4874+77", + "DT": "21", + "DN": "Eve Door", + "SII": "3300", + "SAI": "1100", + "T": "0", + "D": "183", + "CM": "2", + "RI": "0400530980B950D59BF473CFE42BD7DDBF2D", + "PH": "36", + "PI": None, + }, +) @pytest.fixture(name="setup_entry", autouse=True) @@ -35,6 +66,15 @@ def setup_entry_fixture() -> Generator[AsyncMock, None, None]: yield mock_setup_entry +@pytest.fixture(name="unload_entry", autouse=True) +def unload_entry_fixture() -> Generator[AsyncMock, None, None]: + """Mock entry unload.""" + with patch( + "homeassistant.components.matter.async_unload_entry", return_value=True + ) as mock_unload_entry: + yield mock_unload_entry + + @pytest.fixture(name="client_connect", autouse=True) def client_connect_fixture() -> Generator[AsyncMock, None, None]: """Mock server version.""" @@ -80,6 +120,16 @@ def addon_setup_time_fixture() -> Generator[int, None, None]: yield addon_setup_time +@pytest.fixture(name="not_onboarded") +def mock_onboarded_fixture() -> Generator[MagicMock, None, None]: + """Mock that Home Assistant is not yet onboarded.""" + with patch( + "homeassistant.components.matter.config_flow.async_is_onboarded", + return_value=False, + ) as mock_onboarded: + yield mock_onboarded + + async def test_manual_create_entry( hass: HomeAssistant, client_connect: AsyncMock, @@ -179,24 +229,18 @@ async def test_manual_already_configured( assert setup_entry.call_count == 1 +@pytest.mark.parametrize("zeroconf_info", [ZEROCONF_INFO_TCP, ZEROCONF_INFO_UDP]) async def test_zeroconf_discovery( hass: HomeAssistant, client_connect: AsyncMock, setup_entry: AsyncMock, + zeroconf_info: ZeroconfServiceInfo, ) -> None: """Test flow started from Zeroconf discovery.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=ZeroconfServiceInfo( - ip_address=ip_address("fd11:be53:8d46:0:729e:5a4f:539d:1ee6"), - ip_addresses=[ip_address("fd11:be53:8d46:0:729e:5a4f:539d:1ee6")], - port=5540, - hostname="CDEFGHIJ12345678.local.", - type="_matter._tcp.local.", - name="ABCDEFGH123456789-0000000012345678._matter._tcp.local.", - properties={"SII": "3300", "SAI": "1100", "T": "0"}, - ), + data=zeroconf_info, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" @@ -221,6 +265,185 @@ async def test_zeroconf_discovery( assert setup_entry.call_count == 1 +@pytest.mark.parametrize("zeroconf_info", [ZEROCONF_INFO_TCP, ZEROCONF_INFO_UDP]) +async def test_zeroconf_discovery_not_onboarded_not_supervisor( + hass: HomeAssistant, + client_connect: AsyncMock, + setup_entry: AsyncMock, + not_onboarded: MagicMock, + zeroconf_info: ZeroconfServiceInfo, +) -> None: + """Test flow started from Zeroconf discovery when not onboarded.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf_info, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "manual" + assert result["errors"] is None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "url": "ws://localhost:5580/ws", + }, + ) + await hass.async_block_till_done() + + assert client_connect.call_count == 1 + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Matter" + assert result["data"] == { + "url": "ws://localhost:5580/ws", + "integration_created_addon": False, + "use_addon": False, + } + assert setup_entry.call_count == 1 + + +@pytest.mark.parametrize("zeroconf_info", [ZEROCONF_INFO_TCP, ZEROCONF_INFO_UDP]) +@pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}]) +async def test_zeroconf_not_onboarded_already_discovered( + hass: HomeAssistant, + supervisor: MagicMock, + addon_info: AsyncMock, + addon_running: AsyncMock, + client_connect: AsyncMock, + setup_entry: AsyncMock, + not_onboarded: MagicMock, + zeroconf_info: ZeroconfServiceInfo, +) -> None: + """Test flow Zeroconf discovery when not onboarded and already discovered.""" + result_flow_1 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf_info, + ) + result_flow_2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf_info, + ) + await hass.async_block_till_done() + assert result_flow_2["type"] is FlowResultType.ABORT + assert result_flow_2["reason"] == "already_configured" + assert addon_info.call_count == 1 + assert client_connect.call_count == 1 + assert result_flow_1["type"] is FlowResultType.CREATE_ENTRY + assert result_flow_1["title"] == "Matter" + assert result_flow_1["data"] == { + "url": "ws://host1:5581/ws", + "use_addon": True, + "integration_created_addon": False, + } + assert setup_entry.call_count == 1 + + +@pytest.mark.parametrize("zeroconf_info", [ZEROCONF_INFO_TCP, ZEROCONF_INFO_UDP]) +@pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}]) +async def test_zeroconf_not_onboarded_running( + hass: HomeAssistant, + supervisor: MagicMock, + addon_info: AsyncMock, + addon_running: AsyncMock, + client_connect: AsyncMock, + setup_entry: AsyncMock, + not_onboarded: MagicMock, + zeroconf_info: ZeroconfServiceInfo, +) -> None: + """Test flow Zeroconf discovery when not onboarded and add-on running.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf_info, + ) + await hass.async_block_till_done() + + assert addon_info.call_count == 1 + assert client_connect.call_count == 1 + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Matter" + assert result["data"] == { + "url": "ws://host1:5581/ws", + "use_addon": True, + "integration_created_addon": False, + } + assert setup_entry.call_count == 1 + + +@pytest.mark.parametrize("zeroconf_info", [ZEROCONF_INFO_TCP, ZEROCONF_INFO_UDP]) +@pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}]) +async def test_zeroconf_not_onboarded_installed( + hass: HomeAssistant, + supervisor: MagicMock, + addon_info: AsyncMock, + addon_installed: AsyncMock, + start_addon: AsyncMock, + client_connect: AsyncMock, + setup_entry: AsyncMock, + not_onboarded: MagicMock, + zeroconf_info: ZeroconfServiceInfo, +) -> None: + """Test flow Zeroconf discovery when not onboarded and add-on installed.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf_info, + ) + await hass.async_block_till_done() + + assert addon_info.call_count == 1 + assert start_addon.call_args == call(hass, "core_matter_server") + assert client_connect.call_count == 1 + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Matter" + assert result["data"] == { + "url": "ws://host1:5581/ws", + "use_addon": True, + "integration_created_addon": False, + } + assert setup_entry.call_count == 1 + + +@pytest.mark.parametrize("zeroconf_info", [ZEROCONF_INFO_TCP, ZEROCONF_INFO_UDP]) +@pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}]) +async def test_zeroconf_not_onboarded_not_installed( + hass: HomeAssistant, + supervisor: MagicMock, + addon_info: AsyncMock, + addon_store_info: AsyncMock, + addon_not_installed: AsyncMock, + install_addon: AsyncMock, + start_addon: AsyncMock, + client_connect: AsyncMock, + setup_entry: AsyncMock, + not_onboarded: MagicMock, + zeroconf_info: ZeroconfServiceInfo, +) -> None: + """Test flow Zeroconf discovery when not onboarded and add-on not installed.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf_info, + ) + await hass.async_block_till_done() + + assert addon_info.call_count == 0 + assert addon_store_info.call_count == 2 + assert install_addon.call_args == call(hass, "core_matter_server") + assert start_addon.call_args == call(hass, "core_matter_server") + assert client_connect.call_count == 1 + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Matter" + assert result["data"] == { + "url": "ws://host1:5581/ws", + "use_addon": True, + "integration_created_addon": True, + } + assert setup_entry.call_count == 1 + + @pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}]) async def test_supervisor_discovery( hass: HomeAssistant, @@ -702,6 +925,90 @@ async def test_addon_running_failures( assert result["reason"] == abort_reason +@pytest.mark.parametrize("zeroconf_info", [ZEROCONF_INFO_TCP, ZEROCONF_INFO_UDP]) +@pytest.mark.parametrize( + ( + "discovery_info", + "discovery_info_error", + "client_connect_error", + "addon_info_error", + "abort_reason", + "discovery_info_called", + "client_connect_called", + ), + [ + ( + {"config": ADDON_DISCOVERY_INFO}, + HassioAPIError(), + None, + None, + "addon_get_discovery_info_failed", + True, + False, + ), + ( + {"config": ADDON_DISCOVERY_INFO}, + None, + CannotConnect(Exception("Boom")), + None, + "cannot_connect", + True, + True, + ), + ( + None, + None, + None, + None, + "addon_get_discovery_info_failed", + True, + False, + ), + ( + {"config": ADDON_DISCOVERY_INFO}, + None, + None, + HassioAPIError(), + "addon_info_failed", + False, + False, + ), + ], +) +async def test_addon_running_failures_zeroconf( + hass: HomeAssistant, + supervisor: MagicMock, + addon_running: AsyncMock, + addon_info: AsyncMock, + get_addon_discovery_info: AsyncMock, + client_connect: AsyncMock, + discovery_info_error: Exception | None, + client_connect_error: Exception | None, + addon_info_error: Exception | None, + abort_reason: str, + discovery_info_called: bool, + client_connect_called: bool, + not_onboarded: MagicMock, + zeroconf_info: ZeroconfServiceInfo, +) -> None: + """Test all failures when add-on is running and not onboarded.""" + get_addon_discovery_info.side_effect = discovery_info_error + client_connect.side_effect = client_connect_error + addon_info.side_effect = addon_info_error + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf_info, + ) + await hass.async_block_till_done() + + assert addon_info.call_count == 1 + assert get_addon_discovery_info.called is discovery_info_called + assert client_connect.called is client_connect_called + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == abort_reason + + @pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}]) async def test_addon_running_already_configured( hass: HomeAssistant, @@ -854,6 +1161,71 @@ async def test_addon_installed_failures( assert result["reason"] == "addon_start_failed" +@pytest.mark.parametrize("zeroconf_info", [ZEROCONF_INFO_TCP, ZEROCONF_INFO_UDP]) +@pytest.mark.parametrize( + ( + "discovery_info", + "start_addon_error", + "client_connect_error", + "discovery_info_called", + "client_connect_called", + ), + [ + ( + {"config": ADDON_DISCOVERY_INFO}, + HassioAPIError(), + None, + False, + False, + ), + ( + {"config": ADDON_DISCOVERY_INFO}, + None, + CannotConnect(Exception("Boom")), + True, + True, + ), + ( + None, + None, + None, + True, + False, + ), + ], +) +async def test_addon_installed_failures_zeroconf( + hass: HomeAssistant, + supervisor: MagicMock, + addon_installed: AsyncMock, + addon_info: AsyncMock, + start_addon: AsyncMock, + get_addon_discovery_info: AsyncMock, + client_connect: AsyncMock, + start_addon_error: Exception | None, + client_connect_error: Exception | None, + discovery_info_called: bool, + client_connect_called: bool, + not_onboarded: MagicMock, + zeroconf_info: ZeroconfServiceInfo, +) -> None: + """Test add-on start failure when add-on is installed and not onboarded.""" + start_addon.side_effect = start_addon_error + client_connect.side_effect = client_connect_error + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf_info + ) + await hass.async_block_till_done() + + assert addon_info.call_count == 1 + assert start_addon.call_args == call(hass, "core_matter_server") + assert get_addon_discovery_info.called is discovery_info_called + assert client_connect.called is client_connect_called + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "addon_start_failed" + + @pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}]) async def test_addon_installed_already_configured( hass: HomeAssistant, @@ -985,6 +1357,30 @@ async def test_addon_not_installed_failures( assert result["reason"] == "addon_install_failed" +@pytest.mark.parametrize("zeroconf_info", [ZEROCONF_INFO_TCP, ZEROCONF_INFO_UDP]) +async def test_addon_not_installed_failures_zeroconf( + hass: HomeAssistant, + supervisor: MagicMock, + addon_not_installed: AsyncMock, + addon_info: AsyncMock, + install_addon: AsyncMock, + not_onboarded: MagicMock, + zeroconf_info: ZeroconfServiceInfo, +) -> None: + """Test add-on install failure.""" + install_addon.side_effect = HassioAPIError() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf_info + ) + await hass.async_block_till_done() + + assert install_addon.call_args == call(hass, "core_matter_server") + assert addon_info.call_count == 0 + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "addon_install_failed" + + @pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}]) async def test_addon_not_installed_already_configured( hass: HomeAssistant, From 2c46db16d4cee8863b531aa7e4feffb8b56509fb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Apr 2024 07:08:29 -0500 Subject: [PATCH 906/967] Fix script in restart mode that is fired from the same trigger (#116247) --- homeassistant/helpers/script.py | 20 +++--- tests/components/automation/test_init.py | 82 +++++++++++++++++++++++- 2 files changed, 91 insertions(+), 11 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index d925bf215ab..d739fbfef98 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -1692,7 +1692,7 @@ class Script: script_stack = script_stack_cv.get() if ( self.script_mode in (SCRIPT_MODE_RESTART, SCRIPT_MODE_QUEUED) - and (script_stack := script_stack_cv.get()) is not None + and script_stack is not None and id(self) in script_stack ): script_execution_set("disallowed_recursion_detected") @@ -1706,15 +1706,19 @@ class Script: run = cls( self._hass, self, cast(dict, variables), context, self._log_exceptions ) + has_existing_runs = bool(self._runs) self._runs.append(run) - if self.script_mode == SCRIPT_MODE_RESTART: + if self.script_mode == SCRIPT_MODE_RESTART and has_existing_runs: # When script mode is SCRIPT_MODE_RESTART, first add the new run and then # stop any other runs. If we stop other runs first, self.is_running will # return false after the other script runs were stopped until our task - # resumes running. + # resumes running. Its important that we check if there are existing + # 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. + # 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) @@ -1730,9 +1734,7 @@ class Script: self._changed() raise - async def _async_stop( - self, aws: list[asyncio.Task], update_state: bool, spare: _ScriptRun | None - ) -> None: + async def _async_stop(self, aws: list[asyncio.Task], update_state: bool) -> None: await asyncio.wait(aws) if update_state: self._changed() @@ -1749,9 +1751,7 @@ class Script: ] if not aws: return - await asyncio.shield( - create_eager_task(self._async_stop(aws, update_state, spare)) - ) + await asyncio.shield(create_eager_task(self._async_stop(aws, update_state))) async def _async_get_condition(self, config): if isinstance(config, template.Template): diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 61e6d0e4660..edf0eff878b 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -8,7 +8,7 @@ from unittest.mock import Mock, patch import pytest -from homeassistant.components import automation +from homeassistant.components import automation, input_boolean, script from homeassistant.components.automation import ( ATTR_SOURCE, DOMAIN, @@ -41,6 +41,7 @@ from homeassistant.core import ( ) from homeassistant.exceptions import HomeAssistantError, Unauthorized from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.script import ( SCRIPT_MODE_CHOICES, SCRIPT_MODE_PARALLEL, @@ -2980,3 +2981,82 @@ async def test_automation_turns_off_other_automation( async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=5)) await hass.async_block_till_done() assert len(calls) == 0 + + +async def test_two_automations_call_restart_script_same_time( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test two automations that call a restart mode script at the same.""" + hass.states.async_set("binary_sensor.presence", "off") + await hass.async_block_till_done() + events = [] + + @callback + def _save_event(event): + events.append(event) + + assert await async_setup_component( + hass, + input_boolean.DOMAIN, + { + input_boolean.DOMAIN: { + "test_1": None, + } + }, + ) + cancel = async_track_state_change_event(hass, "input_boolean.test_1", _save_event) + + assert await async_setup_component( + hass, + script.DOMAIN, + { + script.DOMAIN: { + "fire_toggle": { + "sequence": [ + { + "service": "input_boolean.toggle", + "target": {"entity_id": "input_boolean.test_1"}, + } + ] + }, + } + }, + ) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "state", + "entity_id": "binary_sensor.presence", + "to": "on", + }, + "action": { + "service": "script.fire_toggle", + }, + "id": "automation_0", + "mode": "single", + }, + { + "trigger": { + "platform": "state", + "entity_id": "binary_sensor.presence", + "to": "on", + }, + "action": { + "service": "script.fire_toggle", + }, + "id": "automation_1", + "mode": "single", + }, + ] + }, + ) + + hass.states.async_set("binary_sensor.presence", "on") + await hass.async_block_till_done() + assert len(events) == 2 + cancel() From abf45a0e0c9e32aa5b5e8b34d6da970b0855338c Mon Sep 17 00:00:00 2001 From: hopkins-tk Date: Sat, 27 Apr 2024 10:02:52 +0200 Subject: [PATCH 907/967] Fix Aseko binary sensors names (#116251) * Fix Aseko binary sensors names * Fix add missing key to strings.json * Fix remove setting shorthand translation key attribute * Update homeassistant/components/aseko_pool_live/strings.json --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/aseko_pool_live/binary_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/aseko_pool_live/binary_sensor.py b/homeassistant/components/aseko_pool_live/binary_sensor.py index dbbdff38200..79953565769 100644 --- a/homeassistant/components/aseko_pool_live/binary_sensor.py +++ b/homeassistant/components/aseko_pool_live/binary_sensor.py @@ -42,6 +42,7 @@ UNIT_BINARY_SENSORS: tuple[AsekoBinarySensorEntityDescription, ...] = ( ), AsekoBinarySensorEntityDescription( key="has_error", + translation_key="error", value_fn=lambda unit: unit.has_error, device_class=BinarySensorDeviceClass.PROBLEM, ), @@ -78,7 +79,6 @@ class AsekoUnitBinarySensorEntity(AsekoEntity, BinarySensorEntity): """Initialize the unit binary sensor.""" super().__init__(unit, coordinator) self.entity_description = entity_description - self._attr_name = f"{self._device_name} {entity_description.name}" self._attr_unique_id = f"{self._unit.serial_number}_{entity_description.key}" @property From bfcffb5cb16fa62e744f3fb3f7a9b4f2367db7bb Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 28 Apr 2024 01:42:38 +0200 Subject: [PATCH 908/967] Fix no will published when mqtt is down (#116319) --- homeassistant/components/mqtt/client.py | 3 ++- tests/components/mqtt/test_init.py | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 133991ade16..7f58a21a1f1 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -711,7 +711,8 @@ class MQTT: async with self._connection_lock: self._should_reconnect = False self._async_cancel_reconnect() - self._mqttc.disconnect() + # We do not gracefully disconnect to ensure + # the broker publishes the will message @callback def async_restore_tracked_subscriptions( diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 9d135b89f36..cfb8ce7ac04 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -141,17 +141,17 @@ async def test_mqtt_connects_on_home_assistant_mqtt_setup( assert mqtt_client_mock.connect.call_count == 1 -async def test_mqtt_disconnects_on_home_assistant_stop( +async def test_mqtt_does_not_disconnect_on_home_assistant_stop( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, mqtt_client_mock: MqttMockPahoClient, ) -> None: - """Test if client stops on HA stop.""" + """Test if client is not disconnected on HA stop.""" await mqtt_mock_entry() hass.bus.fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() await hass.async_block_till_done() - assert mqtt_client_mock.disconnect.call_count == 1 + assert mqtt_client_mock.disconnect.call_count == 0 async def test_mqtt_await_ack_at_disconnect( From f2a101128f862117ac7e56f84f0a32eca6e6f6c1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 28 Apr 2024 08:13:37 -0500 Subject: [PATCH 909/967] Make discovery flow tasks background tasks (#116327) --- homeassistant/config_entries.py | 1 + homeassistant/helpers/discovery_flow.py | 2 +- tests/components/gardena_bluetooth/test_config_flow.py | 2 +- tests/components/hassio/test_init.py | 2 +- tests/components/homeassistant_yellow/test_init.py | 8 ++++---- tests/components/plex/test_config_flow.py | 2 +- 6 files changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 056814bbc4d..88230a78428 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1157,6 +1157,7 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager[ConfigFlowResult]): cooldown=DISCOVERY_COOLDOWN, immediate=True, function=self._async_discovery, + background=True, ) async def async_wait_import_flow_initialized(self, handler: str) -> None: diff --git a/homeassistant/helpers/discovery_flow.py b/homeassistant/helpers/discovery_flow.py index 314777733c3..e479a47ecfd 100644 --- a/homeassistant/helpers/discovery_flow.py +++ b/homeassistant/helpers/discovery_flow.py @@ -30,7 +30,7 @@ def async_create_flow( if not dispatcher or dispatcher.started: if init_coro := _async_init_flow(hass, domain, context, data): - hass.async_create_task( + hass.async_create_background_task( init_coro, f"discovery flow {domain} {context}", eager_start=True ) return diff --git a/tests/components/gardena_bluetooth/test_config_flow.py b/tests/components/gardena_bluetooth/test_config_flow.py index 7707a13180f..3b4e9c242b3 100644 --- a/tests/components/gardena_bluetooth/test_config_flow.py +++ b/tests/components/gardena_bluetooth/test_config_flow.py @@ -103,7 +103,7 @@ async def test_bluetooth( # Inject the service info will trigger the flow to start inject_bluetooth_service_info(hass, WATER_TIMER_SERVICE_INFO) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) result = next(iter(hass.config_entries.flow.async_progress_by_handler(DOMAIN))) diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index da49b8d9f16..572593d642b 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -1098,7 +1098,7 @@ async def test_setup_hardware_integration( ) as mock_setup_entry, ): result = await async_setup_component(hass, "hassio", {"hassio": {}}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert result assert aioclient_mock.call_count == 19 diff --git a/tests/components/homeassistant_yellow/test_init.py b/tests/components/homeassistant_yellow/test_init.py index 0631c2cb983..ec3ba4e7005 100644 --- a/tests/components/homeassistant_yellow/test_init.py +++ b/tests/components/homeassistant_yellow/test_init.py @@ -44,7 +44,7 @@ async def test_setup_entry( ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert len(mock_get_os_info.mock_calls) == 1 @@ -90,7 +90,7 @@ async def test_setup_zha(hass: HomeAssistant, addon_store_info) -> None: ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert len(mock_get_os_info.mock_calls) == 1 # Finish setting up ZHA @@ -144,7 +144,7 @@ async def test_setup_zha_multipan( ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert len(mock_get_os_info.mock_calls) == 1 # Finish setting up ZHA @@ -198,7 +198,7 @@ async def test_setup_zha_multipan_other_device( ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert len(mock_get_os_info.mock_calls) == 1 # Finish setting up ZHA diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index 33e1b3637d8..5f2531992d4 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -722,7 +722,7 @@ async def test_integration_discovery(hass: HomeAssistant) -> None: with patch("homeassistant.components.plex.config_flow.GDM", return_value=mock_gdm): await config_flow.async_discover(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) flows = hass.config_entries.flow.async_progress() From d1e74710940eeded63d82b79995914da459ab543 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 28 Apr 2024 08:13:51 -0500 Subject: [PATCH 910/967] Prevent setup retry from delaying shutdown (#116328) --- homeassistant/config_entries.py | 2 +- .../components/gardena_bluetooth/test_init.py | 2 +- .../specific_devices/test_ecobee3.py | 1 + .../homekit_controller/test_init.py | 6 +++-- tests/components/teslemetry/test_init.py | 2 +- tests/components/wiz/test_init.py | 4 ++-- tests/components/yeelight/test_init.py | 22 +++++++++---------- tests/components/zha/test_init.py | 3 ++- 8 files changed, 23 insertions(+), 19 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 88230a78428..73e1d8debd6 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -698,7 +698,7 @@ class ConfigEntry: # Check again when we fire in case shutdown # has started so we do not block shutdown if not hass.is_stopping: - hass.async_create_task( + hass.async_create_background_task( self._async_setup_retry(hass), f"config entry retry {self.domain} {self.title}", eager_start=True, diff --git a/tests/components/gardena_bluetooth/test_init.py b/tests/components/gardena_bluetooth/test_init.py index 1f294c6169d..53688846c07 100644 --- a/tests/components/gardena_bluetooth/test_init.py +++ b/tests/components/gardena_bluetooth/test_init.py @@ -57,6 +57,6 @@ async def test_setup_retry( mock_client.read_char.side_effect = original_read_char async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert mock_entry.state is ConfigEntryState.LOADED diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee3.py b/tests/components/homekit_controller/specific_devices/test_ecobee3.py index 3f93ca1a896..059993e3bef 100644 --- a/tests/components/homekit_controller/specific_devices/test_ecobee3.py +++ b/tests/components/homekit_controller/specific_devices/test_ecobee3.py @@ -203,6 +203,7 @@ async def test_ecobee3_setup_connection_failure( # We just advance time by 5 minutes so that the retry happens, rather # than manually invoking async_setup_entry. await time_changed(hass, 5 * 60) + await hass.async_block_till_done(wait_background_tasks=True) climate = entity_registry.async_get("climate.homew") assert climate.unique_id == "00:00:00:00:00:00_1_16" diff --git a/tests/components/homekit_controller/test_init.py b/tests/components/homekit_controller/test_init.py index 59fdf555a50..9d2022f6b1c 100644 --- a/tests/components/homekit_controller/test_init.py +++ b/tests/components/homekit_controller/test_init.py @@ -160,7 +160,7 @@ async def test_offline_device_raises(hass: HomeAssistant, controller) -> None: is_connected = True async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert config_entry.state is ConfigEntryState.LOADED assert hass.states.get("light.testdevice").state == STATE_OFF @@ -217,16 +217,18 @@ async def test_ble_device_only_checks_is_available( is_available = True async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert config_entry.state is ConfigEntryState.LOADED assert hass.states.get("light.testdevice").state == STATE_OFF is_available = False async_fire_time_changed(hass, utcnow() + timedelta(hours=1)) + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get("light.testdevice").state == STATE_UNAVAILABLE is_available = True async_fire_time_changed(hass, utcnow() + timedelta(hours=1)) + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get("light.testdevice").state == STATE_OFF diff --git a/tests/components/teslemetry/test_init.py b/tests/components/teslemetry/test_init.py index fb405e2ee03..f21a421ed6e 100644 --- a/tests/components/teslemetry/test_init.py +++ b/tests/components/teslemetry/test_init.py @@ -74,7 +74,7 @@ async def test_vehicle_first_refresh( # Wait for the retry freezer.tick(timedelta(seconds=60)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # Verify we have loaded assert entry.state is ConfigEntryState.LOADED diff --git a/tests/components/wiz/test_init.py b/tests/components/wiz/test_init.py index 3fa369c4d9d..c3438aed1b2 100644 --- a/tests/components/wiz/test_init.py +++ b/tests/components/wiz/test_init.py @@ -32,9 +32,9 @@ async def test_setup_retry(hass: HomeAssistant) -> None: bulb.getMac = AsyncMock(return_value=FAKE_MAC) with _patch_discovery(), _patch_wizlight(device=bulb): - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) async_fire_time_changed(hass, utcnow() + datetime.timedelta(minutes=15)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert entry.state is ConfigEntryState.LOADED diff --git a/tests/components/yeelight/test_init.py b/tests/components/yeelight/test_init.py index af442d1c8d0..0bff635fb6e 100644 --- a/tests/components/yeelight/test_init.py +++ b/tests/components/yeelight/test_init.py @@ -69,7 +69,7 @@ async def test_ip_changes_fallback_discovery(hass: HomeAssistant) -> None: assert config_entry.state is ConfigEntryState.SETUP_RETRY async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=2)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # The discovery should update the ip address assert config_entry.data[CONF_HOST] == IP_ADDRESS @@ -78,7 +78,7 @@ async def test_ip_changes_fallback_discovery(hass: HomeAssistant) -> None: with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), _patch_discovery(): async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert config_entry.state is ConfigEntryState.LOADED binary_sensor_entity_id = ENTITY_BINARY_SENSOR_TEMPLATE.format( @@ -362,7 +362,7 @@ async def test_bulb_off_while_adding_in_ha(hass: HomeAssistant) -> None: _patch_discovery_interval(), ): async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=2)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert config_entry.state is ConfigEntryState.LOADED @@ -380,7 +380,7 @@ async def test_async_listen_error_late_discovery( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert config_entry.state is ConfigEntryState.SETUP_RETRY await hass.async_block_till_done() @@ -388,7 +388,7 @@ async def test_async_listen_error_late_discovery( with _patch_discovery(), patch(f"{MODULE}.AsyncBulb", return_value=_mocked_bulb()): async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert config_entry.state is ConfigEntryState.LOADED assert config_entry.data[CONF_DETECTED_MODEL] == MODEL @@ -411,7 +411,7 @@ async def test_fail_to_fetch_initial_state( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert config_entry.state is ConfigEntryState.SETUP_RETRY await hass.async_block_till_done() @@ -419,7 +419,7 @@ async def test_fail_to_fetch_initial_state( with _patch_discovery(), patch(f"{MODULE}.AsyncBulb", return_value=_mocked_bulb()): async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert config_entry.state is ConfigEntryState.LOADED @@ -502,7 +502,7 @@ async def test_async_setup_with_missing_id(hass: HomeAssistant) -> None: assert config_entry.state is ConfigEntryState.SETUP_RETRY assert config_entry.data[CONF_ID] == ID async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=2)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) with ( _patch_discovery(), @@ -511,7 +511,7 @@ async def test_async_setup_with_missing_id(hass: HomeAssistant) -> None: patch(f"{MODULE}.AsyncBulb", return_value=_mocked_bulb()), ): async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=4)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert config_entry.state is ConfigEntryState.LOADED @@ -535,7 +535,7 @@ async def test_async_setup_with_missing_unique_id(hass: HomeAssistant) -> None: assert config_entry.state is ConfigEntryState.SETUP_RETRY assert config_entry.unique_id == ID async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=2)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) with ( _patch_discovery(), @@ -544,7 +544,7 @@ async def test_async_setup_with_missing_unique_id(hass: HomeAssistant) -> None: patch(f"{MODULE}.AsyncBulb", return_value=_mocked_bulb()), ): async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=4)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert config_entry.state is ConfigEntryState.LOADED diff --git a/tests/components/zha/test_init.py b/tests/components/zha/test_init.py index 99d6a78924b..70ba88ee6e7 100644 --- a/tests/components/zha/test_init.py +++ b/tests/components/zha/test_init.py @@ -255,10 +255,11 @@ async def test_zha_retry_unique_ids( lambda hass, delay, action: async_call_later(hass, 0, action), ): await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # Wait for the config entry setup to retry await asyncio.sleep(0.1) + await hass.async_block_till_done(wait_background_tasks=True) assert len(mock_connect.mock_calls) == 2 From 624eed4b83b2d5612b86ee2f9182cc17e6dfc1a3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 28 Apr 2024 08:14:30 -0500 Subject: [PATCH 911/967] Fix august delaying shutdown (#116329) --- homeassistant/components/august/subscriber.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/august/subscriber.py b/homeassistant/components/august/subscriber.py index 7294f8bc90f..bec8e2f0b97 100644 --- a/homeassistant/components/august/subscriber.py +++ b/homeassistant/components/august/subscriber.py @@ -47,7 +47,9 @@ class AugustSubscriberMixin: @callback def _async_scheduled_refresh(self, now: datetime) -> None: """Call the refresh method.""" - self._hass.async_create_task(self._async_refresh(now), eager_start=True) + 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: From 1309fc5eda6f063b1c65213b2f0ff341c4634c71 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 28 Apr 2024 08:14:04 -0500 Subject: [PATCH 912/967] Fix unifiprotect delaying shutdown if websocket if offline (#116331) --- homeassistant/components/unifiprotect/data.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index c0a6d65ff7a..55ddf91d3cb 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -269,7 +269,12 @@ class ProtectData: this will be a no-op. If the websocket is disconnected, this will trigger a reconnect and refresh. """ - self._hass.async_create_task(self.async_refresh(), eager_start=True) + self._entry.async_create_background_task( + self._hass, + self.async_refresh(), + name=f"{DOMAIN} {self._entry.title} refresh", + eager_start=True, + ) @callback def async_subscribe_device_id( From c3cb79e0e9b4140ecee9cf30a28e30bd40e55827 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 28 Apr 2024 08:14:45 -0500 Subject: [PATCH 913/967] Fix wemo push updates delaying shutdown (#116333) --- homeassistant/components/wemo/wemo_device.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/wemo/wemo_device.py b/homeassistant/components/wemo/wemo_device.py index 148646736bc..7326e0b42f5 100644 --- a/homeassistant/components/wemo/wemo_device.py +++ b/homeassistant/components/wemo/wemo_device.py @@ -5,6 +5,7 @@ from __future__ import annotations import asyncio from dataclasses import dataclass, fields from datetime import timedelta +from functools import partial import logging from typing import TYPE_CHECKING, Literal @@ -130,7 +131,14 @@ class DeviceCoordinator(DataUpdateCoordinator[None]): # pylint: disable=hass-en ) else: updated = self.wemo.subscription_update(event_type, params) - self.hass.create_task(self._async_subscription_callback(updated)) + self.hass.loop.call_soon_threadsafe( + partial( + self.hass.async_create_background_task, + self._async_subscription_callback(updated), + f"{self.name} subscription_callback", + eager_start=True, + ) + ) async def async_shutdown(self) -> None: """Unregister push subscriptions and remove from coordinators dict.""" From c4c21bc8ea19d99ba391f741c8a86a37790dbd00 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 28 Apr 2024 08:57:31 -0500 Subject: [PATCH 914/967] Fix bluetooth adapter discovery delaying startup and shutdown (#116335) --- homeassistant/components/bluetooth/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 4768d58379a..acc38cad58b 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -152,6 +152,7 @@ async def _async_start_adapter_discovery( cooldown=BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS, immediate=False, function=_async_rediscover_adapters, + background=True, ) @hass_callback From 6786479a816af20c31df215cf2d510299a971e12 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 28 Apr 2024 08:54:34 -0500 Subject: [PATCH 915/967] Fix sonos events delaying shutdown (#116337) --- homeassistant/components/sonos/speaker.py | 22 ++++++++++++++-------- tests/components/sonos/test_switch.py | 4 ++-- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 667e2bb405f..e2529ddfe94 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -407,8 +407,8 @@ class SonosSpeaker: @callback def async_renew_failed(self, exception: Exception) -> None: """Handle a failed subscription renewal.""" - self.hass.async_create_task( - self._async_renew_failed(exception), eager_start=True + self.hass.async_create_background_task( + self._async_renew_failed(exception), "sonos renew failed", eager_start=True ) async def _async_renew_failed(self, exception: Exception) -> None: @@ -451,16 +451,20 @@ class SonosSpeaker: """Add the soco instance associated with the event to the callback.""" if "alarm_list_version" not in event.variables: return - self.hass.async_create_task( - self.alarms.async_process_event(event, self), eager_start=True + self.hass.async_create_background_task( + self.alarms.async_process_event(event, self), + "sonos process event", + eager_start=True, ) @callback def async_dispatch_device_properties(self, event: SonosEvent) -> None: """Update device properties from an event.""" self.event_stats.process(event) - self.hass.async_create_task( - self.async_update_device_properties(event), eager_start=True + self.hass.async_create_background_task( + self.async_update_device_properties(event), + "sonos device properties", + eager_start=True, ) async def async_update_device_properties(self, event: SonosEvent) -> None: @@ -483,8 +487,10 @@ class SonosSpeaker: return if "container_update_i_ds" not in event.variables: return - self.hass.async_create_task( - self.favorites.async_process_event(event, self), eager_start=True + self.hass.async_create_background_task( + self.favorites.async_process_event(event, self), + "sonos dispatch favorites", + eager_start=True, ) @callback diff --git a/tests/components/sonos/test_switch.py b/tests/components/sonos/test_switch.py index eb31d991a3a..d6814886d55 100644 --- a/tests/components/sonos/test_switch.py +++ b/tests/components/sonos/test_switch.py @@ -157,7 +157,7 @@ async def test_alarm_create_delete( alarm_event.variables["alarm_list_version"] = two_alarms["CurrentAlarmListVersion"] sub_callback(event=alarm_event) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert "switch.sonos_alarm_14" in entity_registry.entities assert "switch.sonos_alarm_15" in entity_registry.entities @@ -169,7 +169,7 @@ async def test_alarm_create_delete( alarm_clock.ListAlarms.return_value = one_alarm sub_callback(event=alarm_event) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert "switch.sonos_alarm_14" in entity_registry.entities assert "switch.sonos_alarm_15" not in entity_registry.entities From 66538ba34eeffd00675afaa3959a77c3188a6af4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 28 Apr 2024 17:29:00 -0500 Subject: [PATCH 916/967] Add thread safety checks to async_create_task (#116339) * Add thread safety checks to async_create_task Calling async_create_task from a thread almost always results in an fast crash. Since most internals are using async_create_background_task or other task APIs, and this is the one integrations seem to get wrong the most, add a thread safety check here * Add thread safety checks to async_create_task Calling async_create_task from a thread almost always results in an fast crash. Since most internals are using async_create_background_task or other task APIs, and this is the one integrations seem to get wrong the most, add a thread safety check here * missed one * Update homeassistant/core.py * fix mocks * one more internal * more places where internal can be used * more places where internal can be used * more places where internal can be used * internal one more place since this is high volume and was already eager_start --- homeassistant/bootstrap.py | 2 +- homeassistant/config_entries.py | 4 +- homeassistant/core.py | 37 ++++++++++++++++++- homeassistant/helpers/entity.py | 2 +- homeassistant/helpers/entity_component.py | 2 +- homeassistant/helpers/entity_platform.py | 2 +- homeassistant/helpers/integration_platform.py | 4 +- homeassistant/helpers/intent.py | 2 +- homeassistant/helpers/restore_state.py | 4 +- homeassistant/helpers/script.py | 4 +- homeassistant/helpers/storage.py | 2 +- homeassistant/setup.py | 2 +- tests/common.py | 8 ++-- tests/test_core.py | 18 +++++++-- 14 files changed, 70 insertions(+), 23 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index cbc808eb0fa..fc5eedffc39 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -731,7 +731,7 @@ async def async_setup_multi_components( # to wait to be imported, and the sooner we can get the base platforms # loaded the sooner we can start loading the rest of the integrations. futures = { - domain: hass.async_create_task( + domain: hass.async_create_task_internal( async_setup_component(hass, domain, config), f"setup component {domain}", eager_start=True, diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 73e1d8debd6..619b2a4b48a 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1087,7 +1087,7 @@ class ConfigEntry: target: target to call. """ - task = hass.async_create_task( + task = hass.async_create_task_internal( target, f"{name} {self.title} {self.domain} {self.entry_id}", eager_start ) if eager_start and task.done(): @@ -1643,7 +1643,7 @@ class ConfigEntries: # starting a new flow with the 'unignore' step. If the integration doesn't # implement async_step_unignore then this will be a no-op. if entry.source == SOURCE_IGNORE: - self.hass.async_create_task( + self.hass.async_create_task_internal( self.hass.config_entries.flow.async_init( entry.domain, context={"source": SOURCE_UNIGNORE}, diff --git a/homeassistant/core.py b/homeassistant/core.py index a3150adc221..2b1b9756a50 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -785,7 +785,9 @@ class HomeAssistant: target: target to call. """ self.loop.call_soon_threadsafe( - functools.partial(self.async_create_task, target, name, eager_start=True) + functools.partial( + self.async_create_task_internal, target, name, eager_start=True + ) ) @callback @@ -800,6 +802,37 @@ class HomeAssistant: This method must be run in the event loop. If you are using this in your integration, use the create task methods on the config entry instead. + 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("async_create_task") + return self.async_create_task_internal(target, name, eager_start) + + @callback + def async_create_task_internal( + self, + target: Coroutine[Any, Any, _R], + name: str | None = None, + eager_start: bool = True, + ) -> asyncio.Task[_R]: + """Create a task from within the event loop, internal use only. + + This method is intended to only be used by core internally + and should not be considered a stable API. We will make + breaking change to this function in the future and it + should not be used in integrations. + + This method must be run in the event loop. If you are using this in your + integration, use the create task methods on the config entry instead. + target: target to call. """ if eager_start: @@ -2695,7 +2728,7 @@ class ServiceRegistry: coro = self._execute_service(handler, service_call) if not blocking: - self._hass.async_create_task( + self._hass.async_create_task_internal( self._run_service_call_catch_exceptions(coro, service_call), f"service call background {service_call.domain}.{service_call.service}", eager_start=True, diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index a91b4c32d21..6352a56dc90 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -1490,7 +1490,7 @@ class Entity( is_remove = action == "remove" self._removed_from_registry = is_remove if action == "update" or is_remove: - self.hass.async_create_task( + self.hass.async_create_task_internal( self._async_process_registry_update_or_remove(event), eager_start=True ) diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index f467b5683a9..eb54d83e1dd 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -146,7 +146,7 @@ class EntityComponent(Generic[_EntityT]): # Look in config for Domain, Domain 2, Domain 3 etc and load them for p_type, p_config in conf_util.config_per_platform(config, self.domain): if p_type is not None: - self.hass.async_create_task( + self.hass.async_create_task_internal( self.async_setup_platform(p_type, p_config), f"EntityComponent setup platform {p_type} {self.domain}", eager_start=True, diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 2b9a5d436ed..f95c0a0b66a 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -477,7 +477,7 @@ class EntityPlatform: self, new_entities: Iterable[Entity], update_before_add: bool = False ) -> None: """Schedule adding entities for a single platform async.""" - task = self.hass.async_create_task( + task = self.hass.async_create_task_internal( self.async_add_entities(new_entities, update_before_add=update_before_add), f"EntityPlatform async_add_entities {self.domain}.{self.platform_name}", eager_start=True, diff --git a/homeassistant/helpers/integration_platform.py b/homeassistant/helpers/integration_platform.py index be525b384e0..fbd26019b64 100644 --- a/homeassistant/helpers/integration_platform.py +++ b/homeassistant/helpers/integration_platform.py @@ -85,7 +85,7 @@ def _async_integration_platform_component_loaded( # At least one of the platforms is not loaded, we need to load them # so we have to fall back to creating a task. - hass.async_create_task( + hass.async_create_task_internal( _async_process_integration_platforms_for_component( hass, integration, platforms_that_exist, integration_platforms_by_name ), @@ -206,7 +206,7 @@ async def async_process_integration_platforms( # We use hass.async_create_task instead of asyncio.create_task because # we want to make sure that startup waits for the task to complete. # - future = hass.async_create_task( + future = hass.async_create_task_internal( _async_process_integration_platforms( hass, platform_name, top_level_components.copy(), process_job ), diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index 0ddf4a1e329..119142ec14a 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -659,7 +659,7 @@ class DynamicServiceIntentHandler(IntentHandler): ) await self._run_then_background( - hass.async_create_task( + hass.async_create_task_internal( hass.services.async_call( domain, service, diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index 40c898fe1d2..2b3afc2f57b 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -236,7 +236,9 @@ class RestoreStateData: # Dump the initial states now. This helps minimize the risk of having # old states loaded by overwriting the last states once Home Assistant # has started and the old states have been read. - self.hass.async_create_task(_async_dump_states(), "RestoreStateData dump") + self.hass.async_create_task_internal( + _async_dump_states(), "RestoreStateData dump" + ) # Dump states periodically cancel_interval = async_track_time_interval( diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index d739fbfef98..1bbe7749ff7 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -734,7 +734,7 @@ class _ScriptRun: ) trace_set_result(params=params, running_script=running_script) response_data = await self._async_run_long_action( - self._hass.async_create_task( + self._hass.async_create_task_internal( self._hass.services.async_call( **params, blocking=True, @@ -1208,7 +1208,7 @@ class _ScriptRun: async def _async_run_script(self, script: Script) -> None: """Execute a script.""" result = await self._async_run_long_action( - self._hass.async_create_task( + self._hass.async_create_task_internal( script.async_run(self._variables, self._context), eager_start=True ) ) diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 20054274275..315d28e06e6 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -468,7 +468,7 @@ class Store(Generic[_T]): # wrote. Reschedule the timer to the next write time. self._async_reschedule_delayed_write(self._next_write_time) return - self.hass.async_create_task( + self.hass.async_create_task_internal( self._async_callback_delayed_write(), eager_start=True ) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index fab70e31d9d..7ba51b644e5 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -600,7 +600,7 @@ def _async_when_setup( _LOGGER.exception("Error handling when_setup callback for %s", component) if component in hass.config.components: - hass.async_create_task( + hass.async_create_task_internal( when_setup(), f"when setup {component}", eager_start=True ) return diff --git a/tests/common.py b/tests/common.py index b5fe0f7bae1..a3af2a3103b 100644 --- a/tests/common.py +++ b/tests/common.py @@ -234,7 +234,7 @@ async def async_test_home_assistant( orig_async_add_job = hass.async_add_job orig_async_add_executor_job = hass.async_add_executor_job - orig_async_create_task = hass.async_create_task + orig_async_create_task_internal = hass.async_create_task_internal orig_tz = dt_util.DEFAULT_TIME_ZONE def async_add_job(target, *args, eager_start: bool = False): @@ -263,18 +263,18 @@ async def async_test_home_assistant( return orig_async_add_executor_job(target, *args) - def async_create_task(coroutine, name=None, eager_start=True): + def async_create_task_internal(coroutine, name=None, eager_start=True): """Create task.""" if isinstance(coroutine, Mock) and not isinstance(coroutine, AsyncMock): fut = asyncio.Future() fut.set_result(None) return fut - return orig_async_create_task(coroutine, name, eager_start) + return orig_async_create_task_internal(coroutine, name, eager_start) hass.async_add_job = async_add_job hass.async_add_executor_job = async_add_executor_job - hass.async_create_task = async_create_task + hass.async_create_task_internal = async_create_task_internal hass.data[loader.DATA_CUSTOM_COMPONENTS] = {} diff --git a/tests/test_core.py b/tests/test_core.py index a553d5bbbed..66b5be718b1 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -329,7 +329,7 @@ async def test_async_create_task_schedule_coroutine() -> None: async def job(): pass - ha.HomeAssistant.async_create_task(hass, job(), eager_start=False) + ha.HomeAssistant.async_create_task_internal(hass, job(), eager_start=False) assert len(hass.loop.call_soon.mock_calls) == 0 assert len(hass.loop.create_task.mock_calls) == 1 assert len(hass.add_job.mock_calls) == 0 @@ -342,7 +342,7 @@ async def test_async_create_task_eager_start_schedule_coroutine() -> None: async def job(): pass - ha.HomeAssistant.async_create_task(hass, job(), eager_start=True) + ha.HomeAssistant.async_create_task_internal(hass, job(), eager_start=True) # Should create the task directly since 3.12 supports eager_start assert len(hass.loop.create_task.mock_calls) == 0 assert len(hass.add_job.mock_calls) == 0 @@ -355,7 +355,7 @@ async def test_async_create_task_schedule_coroutine_with_name() -> None: async def job(): pass - task = ha.HomeAssistant.async_create_task( + task = ha.HomeAssistant.async_create_task_internal( hass, job(), "named task", eager_start=False ) assert len(hass.loop.call_soon.mock_calls) == 0 @@ -3480,3 +3480,15 @@ async def test_async_remove_thread_safety(hass: HomeAssistant) -> None: await hass.async_add_executor_job( hass.services.async_remove, "test_domain", "test_service" ) + + +async def test_async_create_task_thread_safety(hass: HomeAssistant) -> None: + """Test async_create_task thread safety.""" + + async def _any_coro(): + pass + + with pytest.raises( + RuntimeError, match="Detected code that calls async_create_task from a thread." + ): + await hass.async_add_executor_job(hass.async_create_task, _any_coro) From c533ca50b1d7cf5ceac79e4d5c0e013b10a9b77b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 28 Apr 2024 10:36:03 -0500 Subject: [PATCH 917/967] Fix homeassistant_alerts delaying shutdown (#116340) --- homeassistant/components/homeassistant_alerts/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homeassistant_alerts/__init__.py b/homeassistant/components/homeassistant_alerts/__init__.py index ef5e330699a..5b5e758fba4 100644 --- a/homeassistant/components/homeassistant_alerts/__init__.py +++ b/homeassistant/components/homeassistant_alerts/__init__.py @@ -87,7 +87,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if not coordinator.last_update_success: return - hass.async_create_task(async_update_alerts(), eager_start=True) + hass.async_create_background_task( + async_update_alerts(), "homeassistant_alerts update", eager_start=True + ) coordinator = AlertUpdateCoordinator(hass) coordinator.async_add_listener(async_schedule_update_alerts) From 5ca91190f2c8e1aa3421c9e046659455c8157098 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 28 Apr 2024 17:34:27 +0200 Subject: [PATCH 918/967] Fix Netatmo indoor sensor (#116342) * Debug netatmo indoor sensor * Debug netatmo indoor sensor * Fix --- homeassistant/components/netatmo/sensor.py | 5 ++++- .../components/netatmo/snapshots/test_sensor.ambr | 14 +++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index fd40bbf88b6..7d99ef9d32c 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -529,7 +529,10 @@ class NetatmoWeatherSensor(NetatmoWeatherModuleEntity, SensorEntity): @property def available(self) -> bool: """Return True if entity is available.""" - return self.device.reachable or False + return ( + self.device.reachable + or getattr(self.device, self.entity_description.netatmo_name) is not None + ) @callback def async_update_callback(self) -> None: diff --git a/tests/components/netatmo/snapshots/test_sensor.ambr b/tests/components/netatmo/snapshots/test_sensor.ambr index 0684956adb8..6ab1e4b1e1a 100644 --- a/tests/components/netatmo/snapshots/test_sensor.ambr +++ b/tests/components/netatmo/snapshots/test_sensor.ambr @@ -901,13 +901,15 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'friendly_name': 'Bedroom Reachability', + 'latitude': 13.377726, + 'longitude': 52.516263, }), 'context': , 'entity_id': 'sensor.bedroom_reachability', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'False', }) # --- # name: test_entity[sensor.bedroom_temperature-entry] @@ -1050,13 +1052,15 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'friendly_name': 'Bedroom Wi-Fi', + 'latitude': 13.377726, + 'longitude': 52.516263, }), 'context': , 'entity_id': 'sensor.bedroom_wi_fi', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'High', }) # --- # name: test_entity[sensor.bureau_modulate_battery-entry] @@ -6692,7 +6696,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': '27', }) # --- # name: test_entity[sensor.villa_outdoor_humidity-entry] @@ -6791,7 +6795,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'High', }) # --- # name: test_entity[sensor.villa_outdoor_reachability-entry] @@ -6838,7 +6842,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'False', }) # --- # name: test_entity[sensor.villa_outdoor_temperature-entry] From 0cec3781267eb510dce27568e592be58a6fd3ef9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 28 Apr 2024 09:21:32 -0500 Subject: [PATCH 919/967] Fix some flapping sonos tests (#116343) --- tests/components/sonos/test_repairs.py | 1 + tests/components/sonos/test_switch.py | 1 + 2 files changed, 2 insertions(+) diff --git a/tests/components/sonos/test_repairs.py b/tests/components/sonos/test_repairs.py index 49b87b272d6..2fa951c6a79 100644 --- a/tests/components/sonos/test_repairs.py +++ b/tests/components/sonos/test_repairs.py @@ -47,3 +47,4 @@ async def test_subscription_repair_issues( sub_callback(event) await hass.async_block_till_done() assert not issue_registry.async_get_issue(DOMAIN, SUB_FAIL_ISSUE_ID) + await hass.async_block_till_done(wait_background_tasks=True) diff --git a/tests/components/sonos/test_switch.py b/tests/components/sonos/test_switch.py index d6814886d55..11ce1aa5ddb 100644 --- a/tests/components/sonos/test_switch.py +++ b/tests/components/sonos/test_switch.py @@ -122,6 +122,7 @@ async def test_switch_attributes( # Trigger subscription callback for speaker discovery await fire_zgs_event() + await hass.async_block_till_done(wait_background_tasks=True) status_light_state = hass.states.get(status_light.entity_id) assert status_light_state.state == STATE_ON From 88015986addd8ba5f0c8d5d92aa4d74b270249e8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 28 Apr 2024 10:57:08 -0500 Subject: [PATCH 920/967] Fix bond update delaying shutdown when push updated are not available (#116344) If push updates are not available, bond could delay shutdown. The update task should have been marked as a background task --- homeassistant/components/bond/entity.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bond/entity.py b/homeassistant/components/bond/entity.py index f547707d5f1..4495e76859d 100644 --- a/homeassistant/components/bond/entity.py +++ b/homeassistant/components/bond/entity.py @@ -128,7 +128,9 @@ class BondEntity(Entity): _FALLBACK_SCAN_INTERVAL, ) return - self.hass.async_create_task(self._async_update(), eager_start=True) + self.hass.async_create_background_task( + self._async_update(), f"{DOMAIN} {self.name} update", eager_start=True + ) async def _async_update(self) -> None: """Fetch via the API.""" From 9445b84ab5da7c63f8c204c9352d5d1b01fb99ec Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 28 Apr 2024 11:19:38 -0500 Subject: [PATCH 921/967] Fix shelly delaying shutdown (#116346) --- .../components/shelly/coordinator.py | 36 +++++++++++++++---- tests/components/shelly/test_binary_sensor.py | 10 +++--- tests/components/shelly/test_climate.py | 18 +++++----- tests/components/shelly/test_coordinator.py | 16 ++++----- tests/components/shelly/test_number.py | 10 +++--- tests/components/shelly/test_sensor.py | 18 +++++----- tests/components/shelly/test_update.py | 6 ++-- 7 files changed, 69 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index bd6686198ed..d3d7b86de11 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -361,7 +361,12 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): ) -> None: """Handle device update.""" if update_type is BlockUpdateType.ONLINE: - self.hass.async_create_task(self._async_device_connect(), eager_start=True) + self.entry.async_create_background_task( + self.hass, + self._async_device_connect(), + "block device online", + eager_start=True, + ) elif update_type is BlockUpdateType.COAP_PERIODIC: self._push_update_failures = 0 ir.async_delete_issue( @@ -654,12 +659,24 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): ) -> None: """Handle device update.""" if update_type is RpcUpdateType.ONLINE: - self.hass.async_create_task(self._async_device_connect(), eager_start=True) + self.entry.async_create_background_task( + self.hass, + self._async_device_connect(), + "rpc device online", + eager_start=True, + ) elif update_type is RpcUpdateType.INITIALIZED: - self.hass.async_create_task(self._async_connected(), eager_start=True) + self.entry.async_create_background_task( + self.hass, self._async_connected(), "rpc device init", eager_start=True + ) self.async_set_updated_data(None) elif update_type is RpcUpdateType.DISCONNECTED: - self.hass.async_create_task(self._async_disconnected(), eager_start=True) + self.entry.async_create_background_task( + self.hass, + self._async_disconnected(), + "rpc device disconnected", + eager_start=True, + ) elif update_type is RpcUpdateType.STATUS: self.async_set_updated_data(None) if self.sleep_period: @@ -673,7 +690,9 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): self.device.subscribe_updates(self._async_handle_update) if self.device.initialized: # If we are already initialized, we are connected - self.hass.async_create_task(self._async_connected(), eager_start=True) + self.entry.async_create_task( + self.hass, self._async_connected(), eager_start=True + ) async def shutdown(self) -> None: """Shutdown the coordinator.""" @@ -756,4 +775,9 @@ async def async_reconnect_soon(hass: HomeAssistant, entry: ConfigEntry) -> None: and (entry_data := get_entry_data(hass).get(entry.entry_id)) and (coordinator := entry_data.rpc) ): - hass.async_create_task(coordinator.async_request_refresh(), eager_start=True) + entry.async_create_background_task( + hass, + coordinator.async_request_refresh(), + "reconnect soon", + eager_start=True, + ) diff --git a/tests/components/shelly/test_binary_sensor.py b/tests/components/shelly/test_binary_sensor.py index 624eb82f060..524bc1e8ffc 100644 --- a/tests/components/shelly/test_binary_sensor.py +++ b/tests/components/shelly/test_binary_sensor.py @@ -145,7 +145,7 @@ async def test_block_sleeping_binary_sensor( # Make device online mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == STATE_OFF @@ -181,7 +181,7 @@ async def test_block_restored_sleeping_binary_sensor( # Make device online monkeypatch.setattr(mock_block_device, "initialized", True) mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == STATE_OFF @@ -207,7 +207,7 @@ async def test_block_restored_sleeping_binary_sensor_no_last_state( # Make device online monkeypatch.setattr(mock_block_device, "initialized", True) mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == STATE_OFF @@ -275,7 +275,7 @@ async def test_rpc_sleeping_binary_sensor( # Make device online mock_rpc_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == STATE_OFF @@ -346,7 +346,7 @@ async def test_rpc_restored_sleeping_binary_sensor_no_last_state( # Make device online monkeypatch.setattr(mock_rpc_device, "initialized", True) mock_rpc_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # Mock update mock_rpc_device.mock_update() diff --git a/tests/components/shelly/test_climate.py b/tests/components/shelly/test_climate.py index 9946dd7640d..a70cdef3fb1 100644 --- a/tests/components/shelly/test_climate.py +++ b/tests/components/shelly/test_climate.py @@ -70,7 +70,7 @@ async def test_climate_hvac_mode( # Make device online mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # Test initial hvac mode - off state = hass.states.get(ENTITY_ID) @@ -131,7 +131,7 @@ async def test_climate_set_temperature( # Make device online mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == HVACMode.OFF @@ -198,7 +198,7 @@ async def test_climate_set_preset_mode( # Make device online mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE @@ -284,7 +284,7 @@ async def test_block_restored_climate( # Make device online monkeypatch.setattr(mock_block_device, "initialized", True) mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == HVACMode.OFF assert hass.states.get(entity_id).attributes.get("temperature") == 4.0 @@ -355,7 +355,7 @@ async def test_block_restored_climate_us_customery( monkeypatch.setattr(mock_block_device.blocks[SENSOR_BLOCK_ID], "targetTemp", 4.0) monkeypatch.setattr(mock_block_device.blocks[SENSOR_BLOCK_ID], "temp", 18.2) mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == HVACMode.OFF assert hass.states.get(entity_id).attributes.get("temperature") == 39 @@ -457,7 +457,7 @@ async def test_block_set_mode_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( @@ -482,7 +482,7 @@ async def test_block_set_mode_auth_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) assert entry.state is ConfigEntryState.LOADED @@ -540,7 +540,7 @@ async def test_block_restored_climate_auth_error( return_value={}, side_effect=InvalidAuthError ) mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert entry.state is ConfigEntryState.LOADED @@ -567,7 +567,7 @@ async def test_device_not_calibrated( # Make device online mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) mock_status = MOCK_STATUS_COAP.copy() mock_status["calibrated"] = False diff --git a/tests/components/shelly/test_coordinator.py b/tests/components/shelly/test_coordinator.py index 9f251d1e008..1e581e156c5 100644 --- a/tests/components/shelly/test_coordinator.py +++ b/tests/components/shelly/test_coordinator.py @@ -224,7 +224,7 @@ async def test_block_sleeping_device_firmware_unsupported( # Make device online mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert entry.state is ConfigEntryState.LOADED assert ( @@ -299,7 +299,7 @@ async def test_block_sleeping_device_no_periodic_updates( # Make device online mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert get_entity_state(hass, entity_id) == "22.1" @@ -542,7 +542,7 @@ async def test_rpc_update_entry_sleep_period( # Make device online mock_rpc_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert entry.data["sleep_period"] == 600 @@ -550,7 +550,7 @@ async def test_rpc_update_entry_sleep_period( monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", 3600) freezer.tick(timedelta(seconds=600 * SLEEP_PERIOD_MULTIPLIER)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert entry.data["sleep_period"] == 3600 @@ -575,14 +575,14 @@ async def test_rpc_sleeping_device_no_periodic_updates( # Make device online mock_rpc_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert get_entity_state(hass, entity_id) == "22.9" # Move time to generate polling freezer.tick(timedelta(seconds=SLEEP_PERIOD_MULTIPLIER * 1000)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert get_entity_state(hass, entity_id) is STATE_UNAVAILABLE @@ -599,7 +599,7 @@ async def test_rpc_sleeping_device_firmware_unsupported( # Make device online mock_rpc_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert entry.state is ConfigEntryState.LOADED assert ( @@ -765,7 +765,7 @@ async def test_rpc_update_entry_fw_ver( # Make device online mock_rpc_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert entry.unique_id device = dev_reg.async_get_device( diff --git a/tests/components/shelly/test_number.py b/tests/components/shelly/test_number.py index 99ad5709d29..0b9fee9e47f 100644 --- a/tests/components/shelly/test_number.py +++ b/tests/components/shelly/test_number.py @@ -44,7 +44,7 @@ async def test_block_number_update( # Make device online mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == "50" @@ -99,7 +99,7 @@ async def test_block_restored_number( # Make device online monkeypatch.setattr(mock_block_device, "initialized", True) mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == "50" @@ -136,7 +136,7 @@ async def test_block_restored_number_no_last_state( # Make device online monkeypatch.setattr(mock_block_device, "initialized", True) mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == "50" @@ -156,7 +156,7 @@ async def test_block_number_set_value( # Make device online mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) mock_block_device.reset_mock() await hass.services.async_call( @@ -217,7 +217,7 @@ async def test_block_set_value_auth_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) assert entry.state is ConfigEntryState.LOADED diff --git a/tests/components/shelly/test_sensor.py b/tests/components/shelly/test_sensor.py index 6151cac10ab..ceaa9b66b8d 100644 --- a/tests/components/shelly/test_sensor.py +++ b/tests/components/shelly/test_sensor.py @@ -165,7 +165,7 @@ async def test_block_sleeping_sensor( # Make device online mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == "22.1" @@ -207,7 +207,7 @@ async def test_block_restored_sleeping_sensor( # Make device online monkeypatch.setattr(mock_block_device, "initialized", True) mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == "22.1" @@ -233,7 +233,7 @@ async def test_block_restored_sleeping_sensor_no_last_state( # Make device online monkeypatch.setattr(mock_block_device, "initialized", True) mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == "22.1" @@ -306,7 +306,7 @@ async def test_block_not_matched_restored_sleeping_sensor( ) monkeypatch.setattr(mock_block_device, "initialized", True) mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == "20.4" @@ -464,7 +464,7 @@ async def test_rpc_sleeping_sensor( # Make device online mock_rpc_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == "22.9" @@ -503,7 +503,7 @@ async def test_rpc_restored_sleeping_sensor( # Make device online monkeypatch.setattr(mock_rpc_device, "initialized", True) mock_rpc_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # Mock update mock_rpc_device.mock_update() @@ -539,7 +539,7 @@ async def test_rpc_restored_sleeping_sensor_no_last_state( # Make device online monkeypatch.setattr(mock_rpc_device, "initialized", True) mock_rpc_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # Mock update mock_rpc_device.mock_update() @@ -607,7 +607,7 @@ async def test_rpc_sleeping_update_entity_service( # Make device online mock_rpc_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(entity_id) assert state.state == "22.9" @@ -657,7 +657,7 @@ async def test_block_sleeping_update_entity_service( # Make device online mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == "22.1" diff --git a/tests/components/shelly/test_update.py b/tests/components/shelly/test_update.py index 93b0f55c415..0f26fd14d12 100644 --- a/tests/components/shelly/test_update.py +++ b/tests/components/shelly/test_update.py @@ -352,7 +352,7 @@ async def test_rpc_sleeping_update( # Make device online mock_rpc_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(entity_id) assert state.state == STATE_ON @@ -413,7 +413,7 @@ async def test_rpc_restored_sleeping_update( # Make device online monkeypatch.setattr(mock_rpc_device, "initialized", True) mock_rpc_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # Mock update mock_rpc_device.mock_update() @@ -462,7 +462,7 @@ async def test_rpc_restored_sleeping_update_no_last_state( # Make device online monkeypatch.setattr(mock_rpc_device, "initialized", True) mock_rpc_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # Mock update mock_rpc_device.mock_update() From 087b6533cddd9ebd9ac8af141d3acb2d4234b2d8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 28 Apr 2024 12:54:53 -0500 Subject: [PATCH 922/967] Fix another case of homeassistant_alerts delaying shutdown (#116352) --- homeassistant/components/homeassistant_alerts/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/homeassistant_alerts/__init__.py b/homeassistant/components/homeassistant_alerts/__init__.py index 5b5e758fba4..b33bfe5ed1e 100644 --- a/homeassistant/components/homeassistant_alerts/__init__.py +++ b/homeassistant/components/homeassistant_alerts/__init__.py @@ -101,6 +101,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: cooldown=COMPONENT_LOADED_COOLDOWN, immediate=False, function=coordinator.async_refresh, + background=True, ) @callback From a61650e38f94758f6d90a8e5cc4692e3f6827e5c Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 29 Apr 2024 16:03:57 +0300 Subject: [PATCH 923/967] Prevent Shelly raising in a task (#116355) Co-authored-by: J. Nick Koston --- .../components/shelly/coordinator.py | 24 +++-- tests/components/shelly/test_coordinator.py | 96 ++++++++++++++++++- 2 files changed, 109 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index d3d7b86de11..e321f393ba3 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -154,24 +154,27 @@ class ShellyCoordinatorBase(DataUpdateCoordinator[None], Generic[_DeviceT]): ) self.device_id = device_entry.id - async def _async_device_connect(self) -> None: - """Connect to a Shelly Block device.""" + async def _async_device_connect_task(self) -> bool: + """Connect to a Shelly device task.""" LOGGER.debug("Connecting to Shelly Device - %s", self.name) try: await self.device.initialize() update_device_fw_info(self.hass, self.device, self.entry) except DeviceConnectionError as err: - raise UpdateFailed(f"Device disconnected: {repr(err)}") from err + LOGGER.debug( + "Error connecting to Shelly device %s, error: %r", self.name, err + ) + return False except InvalidAuthError: self.entry.async_start_reauth(self.hass) - return + return False if not self.device.firmware_supported: async_create_issue_unsupported_firmware(self.hass, self.entry) - return + return False if not self._pending_platforms: - return + return True LOGGER.debug("Device %s is online, resuming setup", self.entry.title) platforms = self._pending_platforms @@ -193,6 +196,8 @@ class ShellyCoordinatorBase(DataUpdateCoordinator[None], Generic[_DeviceT]): # Resume platform setup await self.hass.config_entries.async_forward_entry_setups(self.entry, platforms) + return True + async def _async_reload_entry(self) -> None: """Reload entry.""" self._debounced_reload.async_cancel() @@ -363,7 +368,7 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): if update_type is BlockUpdateType.ONLINE: self.entry.async_create_background_task( self.hass, - self._async_device_connect(), + self._async_device_connect_task(), "block device online", eager_start=True, ) @@ -591,7 +596,8 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): if self.device.connected: return - await self._async_device_connect() + if not await self._async_device_connect_task(): + raise UpdateFailed("Device reconnect error") async def _async_disconnected(self) -> None: """Handle device disconnected.""" @@ -661,7 +667,7 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): if update_type is RpcUpdateType.ONLINE: self.entry.async_create_background_task( self.hass, - self._async_device_connect(), + self._async_device_connect_task(), "rpc device online", eager_start=True, ) diff --git a/tests/components/shelly/test_coordinator.py b/tests/components/shelly/test_coordinator.py index 1e581e156c5..1dc45a98c44 100644 --- a/tests/components/shelly/test_coordinator.py +++ b/tests/components/shelly/test_coordinator.py @@ -24,10 +24,11 @@ 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 +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, @@ -40,10 +41,11 @@ from . import ( inject_rpc_device_event, mock_polling_rpc_update, mock_rest_update, + register_device, register_entity, ) -from tests.common import async_fire_time_changed +from tests.common import async_fire_time_changed, mock_restore_cache RELAY_BLOCK_ID = 0 LIGHT_BLOCK_ID = 2 @@ -806,3 +808,93 @@ async def test_rpc_runs_connected_events_when_initialized( # BLE script list is called during connected events assert call.script_list() in mock_rpc_device.mock_calls + + +async def test_block_sleeping_device_connection_error( + hass: HomeAssistant, + 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) + entity_id = register_entity( + hass, BINARY_SENSOR_DOMAIN, "test_name_motion", "sensor_0-motion", entry + ) + mock_restore_cache(hass, [State(entity_id, STATE_ON)]) + monkeypatch.setattr(mock_block_device, "initialized", False) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert get_entity_state(hass, entity_id) == STATE_ON + + # Make device online event with connection error + monkeypatch.setattr( + mock_block_device, + "initialize", + AsyncMock( + side_effect=DeviceConnectionError, + ), + ) + mock_block_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) + + assert "Error connecting to Shelly device" in caplog.text + 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)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + assert "Sleeping device did not update" in caplog.text + assert get_entity_state(hass, entity_id) == STATE_UNAVAILABLE + + +async def test_rpc_sleeping_device_connection_error( + hass: HomeAssistant, + 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) + entity_id = register_entity( + hass, BINARY_SENSOR_DOMAIN, "test_name_cloud", "cloud-cloud", entry + ) + mock_restore_cache(hass, [State(entity_id, STATE_ON)]) + monkeypatch.setattr(mock_rpc_device, "initialized", False) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert get_entity_state(hass, entity_id) == STATE_ON + + # Make device online event with connection error + monkeypatch.setattr( + mock_rpc_device, + "initialize", + AsyncMock( + side_effect=DeviceConnectionError, + ), + ) + mock_rpc_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) + + assert "Error connecting to Shelly device" in caplog.text + 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)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + assert "Sleeping device did not update" in caplog.text + assert get_entity_state(hass, entity_id) == STATE_UNAVAILABLE From 6fe20be095db62fb49c7b0f0cad0cb81c7820381 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 29 Apr 2024 09:07:48 -0500 Subject: [PATCH 924/967] Fix usb scan delaying shutdown (#116390) If the integration page is accessed right before shutdown it can trigger the usb scan debouncer which was not marked as background so shutdown would wait for the scan to finish --- homeassistant/components/usb/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/usb/__init__.py b/homeassistant/components/usb/__init__.py index 959a8f5894c..46950ba5b91 100644 --- a/homeassistant/components/usb/__init__.py +++ b/homeassistant/components/usb/__init__.py @@ -394,6 +394,7 @@ class USBDiscovery: cooldown=REQUEST_SCAN_COOLDOWN, immediate=True, function=self._async_scan, + background=True, ) await self._request_debouncer.async_call() From 0a9ac6b7a90c7a102c313e5cbf59d2d07d6b9ede Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 29 Apr 2024 14:09:46 +0000 Subject: [PATCH 925/967] Bump version to 2024.5.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 a56405d810a..07f4058ea19 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 = 5 -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 fc2f658a9c0..68b38bb516b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.5.0b1" +version = "2024.5.0b2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From ac45d20e1f00b7b49123e20ccb3b98cb5cb5f6d7 Mon Sep 17 00:00:00 2001 From: dontinelli <73341522+dontinelli@users.noreply.github.com> Date: Mon, 29 Apr 2024 17:08:36 +0200 Subject: [PATCH 926/967] Bump fyta_cli to 0.4.1 (#115918) * bump fyta_cli to 0.4.0 * Update PLANT_STATUS and add PLANT_MEASUREMENT_STATUS * bump fyta_cli to v0.4.0 * minor adjustments of states to API documentation --- homeassistant/components/fyta/manifest.json | 2 +- homeassistant/components/fyta/sensor.py | 28 +++++++---- homeassistant/components/fyta/strings.json | 53 +++++++++++---------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 49 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/fyta/manifest.json b/homeassistant/components/fyta/manifest.json index 55255777994..020ab330152 100644 --- a/homeassistant/components/fyta/manifest.json +++ b/homeassistant/components/fyta/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/fyta", "integration_type": "hub", "iot_class": "cloud_polling", - "requirements": ["fyta_cli==0.3.5"] + "requirements": ["fyta_cli==0.4.1"] } diff --git a/homeassistant/components/fyta/sensor.py b/homeassistant/components/fyta/sensor.py index 2b9e8e3de07..c3e90cef28e 100644 --- a/homeassistant/components/fyta/sensor.py +++ b/homeassistant/components/fyta/sensor.py @@ -7,7 +7,7 @@ from dataclasses import dataclass from datetime import datetime from typing import Final -from fyta_cli.fyta_connector import PLANT_STATUS +from fyta_cli.fyta_connector import PLANT_MEASUREMENT_STATUS, PLANT_STATUS from homeassistant.components.sensor import ( SensorDeviceClass, @@ -34,7 +34,15 @@ class FytaSensorEntityDescription(SensorEntityDescription): ) -PLANT_STATUS_LIST: list[str] = ["too_low", "low", "perfect", "high", "too_high"] +PLANT_STATUS_LIST: list[str] = ["deleted", "doing_great", "need_attention", "no_sensor"] +PLANT_MEASUREMENT_STATUS_LIST: list[str] = [ + "no_data", + "too_low", + "low", + "perfect", + "high", + "too_high", +] SENSORS: Final[list[FytaSensorEntityDescription]] = [ FytaSensorEntityDescription( @@ -52,29 +60,29 @@ SENSORS: Final[list[FytaSensorEntityDescription]] = [ key="temperature_status", translation_key="temperature_status", device_class=SensorDeviceClass.ENUM, - options=PLANT_STATUS_LIST, - value_fn=PLANT_STATUS.get, + options=PLANT_MEASUREMENT_STATUS_LIST, + value_fn=PLANT_MEASUREMENT_STATUS.get, ), FytaSensorEntityDescription( key="light_status", translation_key="light_status", device_class=SensorDeviceClass.ENUM, - options=PLANT_STATUS_LIST, - value_fn=PLANT_STATUS.get, + options=PLANT_MEASUREMENT_STATUS_LIST, + value_fn=PLANT_MEASUREMENT_STATUS.get, ), FytaSensorEntityDescription( key="moisture_status", translation_key="moisture_status", device_class=SensorDeviceClass.ENUM, - options=PLANT_STATUS_LIST, - value_fn=PLANT_STATUS.get, + options=PLANT_MEASUREMENT_STATUS_LIST, + value_fn=PLANT_MEASUREMENT_STATUS.get, ), FytaSensorEntityDescription( key="salinity_status", translation_key="salinity_status", device_class=SensorDeviceClass.ENUM, - options=PLANT_STATUS_LIST, - value_fn=PLANT_STATUS.get, + options=PLANT_MEASUREMENT_STATUS_LIST, + value_fn=PLANT_MEASUREMENT_STATUS.get, ), FytaSensorEntityDescription( key="temperature", diff --git a/homeassistant/components/fyta/strings.json b/homeassistant/components/fyta/strings.json index 3df851489bc..bacd24555b0 100644 --- a/homeassistant/components/fyta/strings.json +++ b/homeassistant/components/fyta/strings.json @@ -36,6 +36,16 @@ "plant_status": { "name": "Plant state", "state": { + "deleted": "Deleted", + "doing_great": "Doing great", + "need_attention": "Needs attention", + "no_sensor": "No sensor" + } + }, + "temperature_status": { + "name": "Temperature state", + "state": { + "no_data": "No data", "too_low": "Too low", "low": "Low", "perfect": "Perfect", @@ -43,44 +53,37 @@ "too_high": "Too high" } }, - "temperature_status": { - "name": "Temperature state", - "state": { - "too_low": "[%key:component::fyta::entity::sensor::plant_status::state::too_low%]", - "low": "[%key:component::fyta::entity::sensor::plant_status::state::low%]", - "perfect": "[%key:component::fyta::entity::sensor::plant_status::state::perfect%]", - "high": "[%key:component::fyta::entity::sensor::plant_status::state::high%]", - "too_high": "[%key:component::fyta::entity::sensor::plant_status::state::too_high%]" - } - }, "light_status": { "name": "Light state", "state": { - "too_low": "[%key:component::fyta::entity::sensor::plant_status::state::too_low%]", - "low": "[%key:component::fyta::entity::sensor::plant_status::state::low%]", - "perfect": "[%key:component::fyta::entity::sensor::plant_status::state::perfect%]", - "high": "[%key:component::fyta::entity::sensor::plant_status::state::high%]", - "too_high": "[%key:component::fyta::entity::sensor::plant_status::state::too_high%]" + "no_data": "[%key:component::fyta::entity::sensor::temperature_status::state::no_data%]", + "too_low": "[%key:component::fyta::entity::sensor::temperature_status::state::too_low%]", + "low": "[%key:component::fyta::entity::sensor::temperature_status::state::low%]", + "perfect": "[%key:component::fyta::entity::sensor::temperature_status::state::perfect%]", + "high": "[%key:component::fyta::entity::sensor::temperature_status::state::high%]", + "too_high": "[%key:component::fyta::entity::sensor::temperature_status::state::too_high%]" } }, "moisture_status": { "name": "Moisture state", "state": { - "too_low": "[%key:component::fyta::entity::sensor::plant_status::state::too_low%]", - "low": "[%key:component::fyta::entity::sensor::plant_status::state::low%]", - "perfect": "[%key:component::fyta::entity::sensor::plant_status::state::perfect%]", - "high": "[%key:component::fyta::entity::sensor::plant_status::state::high%]", - "too_high": "[%key:component::fyta::entity::sensor::plant_status::state::too_high%]" + "no_data": "[%key:component::fyta::entity::sensor::temperature_status::state::no_data%]", + "too_low": "[%key:component::fyta::entity::sensor::temperature_status::state::too_low%]", + "low": "[%key:component::fyta::entity::sensor::temperature_status::state::low%]", + "perfect": "[%key:component::fyta::entity::sensor::temperature_status::state::perfect%]", + "high": "[%key:component::fyta::entity::sensor::temperature_status::state::high%]", + "too_high": "[%key:component::fyta::entity::sensor::temperature_status::state::too_high%]" } }, "salinity_status": { "name": "Salinity state", "state": { - "too_low": "[%key:component::fyta::entity::sensor::plant_status::state::too_low%]", - "low": "[%key:component::fyta::entity::sensor::plant_status::state::low%]", - "perfect": "[%key:component::fyta::entity::sensor::plant_status::state::perfect%]", - "high": "[%key:component::fyta::entity::sensor::plant_status::state::high%]", - "too_high": "[%key:component::fyta::entity::sensor::plant_status::state::too_high%]" + "no_data": "[%key:component::fyta::entity::sensor::temperature_status::state::no_data%]", + "too_low": "[%key:component::fyta::entity::sensor::temperature_status::state::too_low%]", + "low": "[%key:component::fyta::entity::sensor::temperature_status::state::low%]", + "perfect": "[%key:component::fyta::entity::sensor::temperature_status::state::perfect%]", + "high": "[%key:component::fyta::entity::sensor::temperature_status::state::high%]", + "too_high": "[%key:component::fyta::entity::sensor::temperature_status::state::too_high%]" } }, "light": { diff --git a/requirements_all.txt b/requirements_all.txt index 205907c1288..6e41f3b743e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -900,7 +900,7 @@ freesms==0.2.0 fritzconnection[qr]==1.13.2 # homeassistant.components.fyta -fyta_cli==0.3.5 +fyta_cli==0.4.1 # homeassistant.components.google_translate gTTS==2.2.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5e5aecc63e0..a2625b50e87 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -735,7 +735,7 @@ freebox-api==1.1.0 fritzconnection[qr]==1.13.2 # homeassistant.components.fyta -fyta_cli==0.3.5 +fyta_cli==0.4.1 # homeassistant.components.google_translate gTTS==2.2.4 From 7ee79002b392c28abd07d6234163fa139614ed5f Mon Sep 17 00:00:00 2001 From: dontinelli <73341522+dontinelli@users.noreply.github.com> Date: Mon, 29 Apr 2024 17:09:07 +0200 Subject: [PATCH 927/967] Store access token in entry for Fyta (#116260) * save access_token and expiration date in ConfigEntry * add MINOR_VERSION and async_migrate_entry * shorten reading of expiration from config entry * add additional consts and test for config entry migration * Update homeassistant/components/fyta/coordinator.py Co-authored-by: Joost Lekkerkerker * Update homeassistant/components/fyta/__init__.py Co-authored-by: Joost Lekkerkerker * omit check for datetime data type * Update homeassistant/components/fyta/__init__.py Co-authored-by: Joost Lekkerkerker * Update homeassistant/components/fyta/coordinator.py Co-authored-by: Joost Lekkerkerker --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/fyta/__init__.py | 52 ++++++++++++++++++-- homeassistant/components/fyta/config_flow.py | 17 +++++-- homeassistant/components/fyta/const.py | 1 + homeassistant/components/fyta/coordinator.py | 25 ++++++++-- tests/components/fyta/conftest.py | 26 +++++++++- tests/components/fyta/test_config_flow.py | 36 ++++++++++---- tests/components/fyta/test_init.py | 42 ++++++++++++++++ 7 files changed, 179 insertions(+), 20 deletions(-) create mode 100644 tests/components/fyta/test_init.py diff --git a/homeassistant/components/fyta/__init__.py b/homeassistant/components/fyta/__init__.py index febd5b94469..205dd97a42f 100644 --- a/homeassistant/components/fyta/__init__.py +++ b/homeassistant/components/fyta/__init__.py @@ -2,15 +2,23 @@ from __future__ import annotations +from datetime import datetime import logging +from typing import Any +from zoneinfo import ZoneInfo from fyta_cli.fyta_connector import FytaConnector from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform +from homeassistant.const import ( + CONF_ACCESS_TOKEN, + CONF_PASSWORD, + CONF_USERNAME, + Platform, +) from homeassistant.core import HomeAssistant -from .const import DOMAIN +from .const import CONF_EXPIRATION, DOMAIN from .coordinator import FytaCoordinator _LOGGER = logging.getLogger(__name__) @@ -22,11 +30,16 @@ PLATFORMS = [ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the Fyta integration.""" + tz: str = hass.config.time_zone username = entry.data[CONF_USERNAME] password = entry.data[CONF_PASSWORD] + access_token: str = entry.data[CONF_ACCESS_TOKEN] + expiration: datetime = datetime.fromisoformat( + entry.data[CONF_EXPIRATION] + ).astimezone(ZoneInfo(tz)) - fyta = FytaConnector(username, password) + fyta = FytaConnector(username, password, access_token, expiration, tz) coordinator = FytaCoordinator(hass, fyta) @@ -47,3 +60,36 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +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: + new = {**config_entry.data} + if config_entry.minor_version < 2: + fyta = FytaConnector( + config_entry.data[CONF_USERNAME], config_entry.data[CONF_PASSWORD] + ) + credentials: dict[str, Any] = await fyta.login() + await fyta.client.close() + + new[CONF_ACCESS_TOKEN] = credentials[CONF_ACCESS_TOKEN] + new[CONF_EXPIRATION] = credentials[CONF_EXPIRATION].isoformat() + + 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/fyta/config_flow.py b/homeassistant/components/fyta/config_flow.py index e11c024ec1f..3d83c099ac3 100644 --- a/homeassistant/components/fyta/config_flow.py +++ b/homeassistant/components/fyta/config_flow.py @@ -17,7 +17,7 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from .const import DOMAIN +from .const import CONF_EXPIRATION, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -31,14 +31,19 @@ class FytaConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Fyta.""" VERSION = 1 - _entry: ConfigEntry | None = None + MINOR_VERSION = 2 + + def __init__(self) -> None: + """Initialize FytaConfigFlow.""" + self.credentials: dict[str, Any] = {} + self._entry: ConfigEntry | None = None async def async_auth(self, user_input: Mapping[str, Any]) -> dict[str, str]: """Reusable Auth Helper.""" fyta = FytaConnector(user_input[CONF_USERNAME], user_input[CONF_PASSWORD]) try: - await fyta.login() + self.credentials = await fyta.login() except FytaConnectionError: return {"base": "cannot_connect"} except FytaAuthentificationError: @@ -51,6 +56,10 @@ class FytaConfigFlow(ConfigFlow, domain=DOMAIN): finally: await fyta.client.close() + self.credentials[CONF_EXPIRATION] = self.credentials[ + CONF_EXPIRATION + ].isoformat() + return {} async def async_step_user( @@ -62,6 +71,7 @@ class FytaConfigFlow(ConfigFlow, domain=DOMAIN): self._async_abort_entries_match({CONF_USERNAME: user_input[CONF_USERNAME]}) if not (errors := await self.async_auth(user_input)): + user_input |= self.credentials return self.async_create_entry( title=user_input[CONF_USERNAME], data=user_input ) @@ -85,6 +95,7 @@ class FytaConfigFlow(ConfigFlow, domain=DOMAIN): assert self._entry is not None if user_input and not (errors := await self.async_auth(user_input)): + user_input |= self.credentials return self.async_update_reload_and_abort( self._entry, data={**self._entry.data, **user_input} ) diff --git a/homeassistant/components/fyta/const.py b/homeassistant/components/fyta/const.py index f99735dc6fa..bf4636a713a 100644 --- a/homeassistant/components/fyta/const.py +++ b/homeassistant/components/fyta/const.py @@ -1,3 +1,4 @@ """Const for fyta integration.""" DOMAIN = "fyta" +CONF_EXPIRATION = "expiration" diff --git a/homeassistant/components/fyta/coordinator.py b/homeassistant/components/fyta/coordinator.py index 65bd0cb532c..021bddf2cf8 100644 --- a/homeassistant/components/fyta/coordinator.py +++ b/homeassistant/components/fyta/coordinator.py @@ -12,10 +12,13 @@ from fyta_cli.fyta_exceptions import ( ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from .const import CONF_EXPIRATION + _LOGGER = logging.getLogger(__name__) @@ -39,17 +42,33 @@ class FytaCoordinator(DataUpdateCoordinator[dict[int, dict[str, Any]]]): ) -> dict[int, dict[str, Any]]: """Fetch data from API endpoint.""" - if self.fyta.expiration is None or self.fyta.expiration < datetime.now(): + if ( + self.fyta.expiration is None + or self.fyta.expiration.timestamp() < datetime.now().timestamp() + ): await self.renew_authentication() return await self.fyta.update_all_plants() - async def renew_authentication(self) -> None: + async def renew_authentication(self) -> bool: """Renew access token for FYTA API.""" + credentials: dict[str, Any] = {} try: - await self.fyta.login() + credentials = await self.fyta.login() except FytaConnectionError as ex: raise ConfigEntryNotReady from ex except (FytaAuthentificationError, FytaPasswordError) as ex: raise ConfigEntryAuthFailed from ex + + new_config_entry = {**self.config_entry.data} + new_config_entry[CONF_ACCESS_TOKEN] = credentials[CONF_ACCESS_TOKEN] + new_config_entry[CONF_EXPIRATION] = credentials[CONF_EXPIRATION].isoformat() + + self.hass.config_entries.async_update_entry( + self.config_entry, data=new_config_entry + ) + + _LOGGER.debug("Credentials successfully updated") + + return True diff --git a/tests/components/fyta/conftest.py b/tests/components/fyta/conftest.py index efebf9827b9..9250c26926a 100644 --- a/tests/components/fyta/conftest.py +++ b/tests/components/fyta/conftest.py @@ -5,6 +5,11 @@ from unittest.mock import AsyncMock, patch import pytest +from homeassistant.components.fyta.const import CONF_EXPIRATION +from homeassistant.const import CONF_ACCESS_TOKEN + +from .test_config_flow import ACCESS_TOKEN, EXPIRATION + @pytest.fixture def mock_fyta(): @@ -15,7 +20,26 @@ def mock_fyta(): "homeassistant.components.fyta.config_flow.FytaConnector", return_value=mock_fyta_api, ) as mock_fyta_api: - mock_fyta_api.return_value.login.return_value = {} + mock_fyta_api.return_value.login.return_value = { + CONF_ACCESS_TOKEN: ACCESS_TOKEN, + CONF_EXPIRATION: EXPIRATION, + } + yield mock_fyta_api + + +@pytest.fixture +def mock_fyta_init(): + """Build a fixture for the Fyta API that connects successfully and returns one device.""" + + mock_fyta_api = AsyncMock() + with patch( + "homeassistant.components.fyta.FytaConnector", + return_value=mock_fyta_api, + ) as mock_fyta_api: + mock_fyta_api.return_value.login.return_value = { + CONF_ACCESS_TOKEN: ACCESS_TOKEN, + CONF_EXPIRATION: EXPIRATION, + } yield mock_fyta_api diff --git a/tests/components/fyta/test_config_flow.py b/tests/components/fyta/test_config_flow.py index 6aad6295819..69478d04ca0 100644 --- a/tests/components/fyta/test_config_flow.py +++ b/tests/components/fyta/test_config_flow.py @@ -1,5 +1,6 @@ """Test the fyta config flow.""" +from datetime import UTC, datetime from unittest.mock import AsyncMock from fyta_cli.fyta_exceptions import ( @@ -10,8 +11,8 @@ from fyta_cli.fyta_exceptions import ( import pytest from homeassistant import config_entries -from homeassistant.components.fyta.const import DOMAIN -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.components.fyta.const import CONF_EXPIRATION, DOMAIN +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -19,10 +20,12 @@ from tests.common import MockConfigEntry USERNAME = "fyta_user" PASSWORD = "fyta_pass" +ACCESS_TOKEN = "123xyz" +EXPIRATION = datetime.fromisoformat("2024-12-31T10:00:00").astimezone(UTC) async def test_user_flow( - hass: HomeAssistant, mock_fyta: AsyncMock, mock_setup_entry + hass: HomeAssistant, mock_fyta: AsyncMock, mock_setup_entry: AsyncMock ) -> None: """Test we get the form.""" @@ -39,7 +42,12 @@ async def test_user_flow( assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == USERNAME - assert result2["data"] == {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + assert result2["data"] == { + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_ACCESS_TOKEN: ACCESS_TOKEN, + CONF_EXPIRATION: "2024-12-31T10:00:00+00:00", + } assert len(mock_setup_entry.mock_calls) == 1 @@ -57,7 +65,7 @@ async def test_form_exceptions( exception: Exception, error: dict[str, str], mock_fyta: AsyncMock, - mock_setup_entry, + mock_setup_entry: AsyncMock, ) -> None: """Test we can handle Form exceptions.""" @@ -89,6 +97,8 @@ async def test_form_exceptions( assert result["title"] == USERNAME assert result["data"][CONF_USERNAME] == USERNAME assert result["data"][CONF_PASSWORD] == PASSWORD + assert result["data"][CONF_ACCESS_TOKEN] == ACCESS_TOKEN + assert result["data"][CONF_EXPIRATION] == "2024-12-31T10:00:00+00:00" assert len(mock_setup_entry.mock_calls) == 1 @@ -134,14 +144,19 @@ async def test_reauth( exception: Exception, error: dict[str, str], mock_fyta: AsyncMock, - mock_setup_entry, + mock_setup_entry: AsyncMock, ) -> None: """Test reauth-flow works.""" entry = MockConfigEntry( domain=DOMAIN, title=USERNAME, - data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, + data={ + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_ACCESS_TOKEN: ACCESS_TOKEN, + CONF_EXPIRATION: "2024-06-30T10:00:00+00:00", + }, ) entry.add_to_hass(hass) @@ -157,7 +172,8 @@ async def test_reauth( # tests with connection error result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + result["flow_id"], + {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) await hass.async_block_till_done() @@ -178,5 +194,5 @@ async def test_reauth( assert result["reason"] == "reauth_successful" assert entry.data[CONF_USERNAME] == "other_username" assert entry.data[CONF_PASSWORD] == "other_password" - - assert len(mock_setup_entry.mock_calls) == 1 + assert entry.data[CONF_ACCESS_TOKEN] == ACCESS_TOKEN + assert entry.data[CONF_EXPIRATION] == "2024-12-31T10:00:00+00:00" diff --git a/tests/components/fyta/test_init.py b/tests/components/fyta/test_init.py new file mode 100644 index 00000000000..844a818df85 --- /dev/null +++ b/tests/components/fyta/test_init.py @@ -0,0 +1,42 @@ +"""Test the initialization.""" + +from unittest.mock import AsyncMock + +from homeassistant.components.fyta.const import CONF_EXPIRATION, DOMAIN +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant + +from .test_config_flow import ACCESS_TOKEN, PASSWORD, USERNAME + +from tests.common import MockConfigEntry + + +async def test_migrate_config_entry( + hass: HomeAssistant, + mock_fyta_init: AsyncMock, +) -> None: + """Test successful migration of entry data.""" + entry = MockConfigEntry( + domain=DOMAIN, + title=USERNAME, + data={ + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + }, + version=1, + minor_version=1, + ) + entry.add_to_hass(hass) + + assert entry.version == 1 + assert entry.minor_version == 1 + + 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.data[CONF_USERNAME] == USERNAME + assert entry.data[CONF_PASSWORD] == PASSWORD + assert entry.data[CONF_ACCESS_TOKEN] == ACCESS_TOKEN + assert entry.data[CONF_EXPIRATION] == "2024-12-31T10:00:00+00:00" From 99e3236fb7b6fe2a12a09d17ceff0590694e4bf3 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Mon, 29 Apr 2024 17:00:13 +0200 Subject: [PATCH 928/967] Deprecate YAML configuration of Habitica (#116374) Add deprecation issue for yaml import --- .../components/habitica/config_flow.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/habitica/config_flow.py b/homeassistant/components/habitica/config_flow.py index 9ee2aef40ba..9a8852b731d 100644 --- a/homeassistant/components/habitica/config_flow.py +++ b/homeassistant/components/habitica/config_flow.py @@ -10,9 +10,10 @@ import voluptuous as vol from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_URL -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from .const import CONF_API_USER, DEFAULT_URL, DOMAIN @@ -79,6 +80,20 @@ class HabiticaConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_import(self, import_data): """Import habitica config from configuration.yaml.""" + + async_create_issue( + self.hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + is_fixable=False, + breaks_in_ha_version="2024.11.0", + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Habitica", + }, + ) return await self.async_step_user(import_data) From 39d923dc0273e1728d01e36bf15d2be0d60a86c6 Mon Sep 17 00:00:00 2001 From: Steve Easley Date: Mon, 29 Apr 2024 12:25:16 -0400 Subject: [PATCH 929/967] Fix jvcprojector command timeout with some projectors (#116392) * Fix projector timeout in pyprojector lib v1.0.10 * Fix projector timeout by increasing time between power command and refresh. * Bump jvcprojector lib to ensure unknown power states are handled --- homeassistant/components/jvc_projector/manifest.json | 2 +- homeassistant/components/jvc_projector/remote.py | 3 +++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/jvc_projector/manifest.json b/homeassistant/components/jvc_projector/manifest.json index de7e77197f2..d3e1bf3d940 100644 --- a/homeassistant/components/jvc_projector/manifest.json +++ b/homeassistant/components/jvc_projector/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["jvcprojector"], - "requirements": ["pyjvcprojector==1.0.9"] + "requirements": ["pyjvcprojector==1.0.11"] } diff --git a/homeassistant/components/jvc_projector/remote.py b/homeassistant/components/jvc_projector/remote.py index dcc9e5cff51..b69d3b0118b 100644 --- a/homeassistant/components/jvc_projector/remote.py +++ b/homeassistant/components/jvc_projector/remote.py @@ -2,6 +2,7 @@ from __future__ import annotations +import asyncio from collections.abc import Iterable import logging from typing import Any @@ -74,11 +75,13 @@ class JvcProjectorRemote(JvcProjectorEntity, RemoteEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" await self.device.power_on() + await asyncio.sleep(1) await self.coordinator.async_refresh() async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" await self.device.power_off() + await asyncio.sleep(1) await self.coordinator.async_refresh() async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None: diff --git a/requirements_all.txt b/requirements_all.txt index 6e41f3b743e..1f83f00bb53 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1902,7 +1902,7 @@ pyisy==3.1.14 pyitachip2ir==0.0.7 # homeassistant.components.jvc_projector -pyjvcprojector==1.0.9 +pyjvcprojector==1.0.11 # homeassistant.components.kaleidescape pykaleidescape==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a2625b50e87..060287d637a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1483,7 +1483,7 @@ pyiss==1.0.1 pyisy==3.1.14 # homeassistant.components.jvc_projector -pyjvcprojector==1.0.9 +pyjvcprojector==1.0.11 # homeassistant.components.kaleidescape pykaleidescape==1.0.1 From 8f2d10c49a762dd54edae9d0809cab6406be258a Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Mon, 29 Apr 2024 19:33:31 +0200 Subject: [PATCH 930/967] Remove strict connection (#116396) --- homeassistant/components/cloud/__init__.py | 8 -------- homeassistant/components/cloud/prefs.py | 11 +---------- homeassistant/components/http/__init__.py | 18 +++--------------- tests/components/cloud/test_http_api.py | 2 -- tests/components/cloud/test_init.py | 2 ++ tests/components/cloud/test_prefs.py | 1 + .../components/cloud/test_strict_connection.py | 1 + tests/components/http/test_init.py | 2 ++ tests/helpers/test_service.py | 5 ++--- tests/scripts/test_check_config.py | 2 -- 10 files changed, 12 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 2552fe4bf5c..13f1d34b5cd 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -30,7 +30,6 @@ from homeassistant.core import ( HomeAssistant, ServiceCall, ServiceResponse, - SupportsResponse, callback, ) from homeassistant.exceptions import ( @@ -458,10 +457,3 @@ def _setup_services(hass: HomeAssistant, prefs: CloudPreferences) -> None: "url": f"https://login.home-assistant.io?u={quote_plus(url)}", "direct_url": url, } - - hass.services.async_register( - DOMAIN, - "create_temporary_strict_connection_url", - create_temporary_strict_connection_url, - supports_response=SupportsResponse.ONLY, - ) diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index 72207513ca9..b4e692d02c4 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -365,16 +365,7 @@ class CloudPreferences: @property def strict_connection(self) -> http.const.StrictConnectionMode: """Return the strict connection mode.""" - mode = self._prefs.get(PREF_STRICT_CONNECTION) - - if mode is None: - # Set to default value - # We store None in the store as the default value to detect if the user has changed the - # value or not. - mode = http.const.StrictConnectionMode.DISABLED - elif not isinstance(mode, http.const.StrictConnectionMode): - mode = http.const.StrictConnectionMode(mode) - return mode + return http.const.StrictConnectionMode.DISABLED async def get_cloud_user(self) -> str: """Return ID of Home Assistant Cloud system user.""" diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 83601599d88..c783d2f0b71 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -10,7 +10,7 @@ import os import socket import ssl from tempfile import NamedTemporaryFile -from typing import Any, Final, Required, TypedDict, cast +from typing import Any, Final, TypedDict, cast from urllib.parse import quote_plus, urljoin from aiohttp import web @@ -36,7 +36,6 @@ from homeassistant.core import ( HomeAssistant, ServiceCall, ServiceResponse, - SupportsResponse, callback, ) from homeassistant.exceptions import ( @@ -146,9 +145,6 @@ HTTP_SCHEMA: Final = vol.All( [SSL_INTERMEDIATE, SSL_MODERN] ), vol.Optional(CONF_USE_X_FRAME_OPTIONS, default=True): cv.boolean, - vol.Optional( - CONF_STRICT_CONNECTION, default=StrictConnectionMode.DISABLED - ): vol.Coerce(StrictConnectionMode), } ), ) @@ -172,7 +168,6 @@ class ConfData(TypedDict, total=False): login_attempts_threshold: int ip_ban_enabled: bool ssl_profile: str - strict_connection: Required[StrictConnectionMode] @bind_hass @@ -239,7 +234,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: login_threshold=login_threshold, is_ban_enabled=is_ban_enabled, use_x_frame_options=use_x_frame_options, - strict_connection_non_cloud=conf[CONF_STRICT_CONNECTION], + strict_connection_non_cloud=StrictConnectionMode.DISABLED, ) async def stop_server(event: Event) -> None: @@ -620,7 +615,7 @@ def _setup_services(hass: HomeAssistant, conf: ConfData) -> None: if not user.is_admin: raise Unauthorized(context=call.context) - if conf[CONF_STRICT_CONNECTION] is StrictConnectionMode.DISABLED: + if StrictConnectionMode.DISABLED is StrictConnectionMode.DISABLED: raise ServiceValidationError( translation_domain=DOMAIN, translation_key="strict_connection_not_enabled_non_cloud", @@ -652,10 +647,3 @@ def _setup_services(hass: HomeAssistant, conf: ConfData) -> None: "url": f"https://login.home-assistant.io?u={quote_plus(url)}", "direct_url": url, } - - hass.services.async_register( - DOMAIN, - "create_temporary_strict_connection_url", - create_temporary_strict_connection_url, - supports_response=SupportsResponse.ONLY, - ) diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index d9d2b5c6742..1e4dc3173e2 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -915,7 +915,6 @@ async def test_websocket_update_preferences( "google_secure_devices_pin": "1234", "tts_default_voice": ["en-GB", "RyanNeural"], "remote_allow_remote_enable": False, - "strict_connection": StrictConnectionMode.DROP_CONNECTION, } ) response = await client.receive_json() @@ -926,7 +925,6 @@ async def test_websocket_update_preferences( assert cloud.client.prefs.google_secure_devices_pin == "1234" assert cloud.client.prefs.remote_allow_remote_enable is False assert cloud.client.prefs.tts_default_voice == ("en-GB", "RyanNeural") - assert cloud.client.prefs.strict_connection is StrictConnectionMode.DROP_CONNECTION @pytest.mark.parametrize( diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index bc4526975da..d917dc12a7c 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -303,6 +303,7 @@ async def test_cloud_logout( assert cloud.is_logged_in is False +@pytest.mark.skip(reason="Remove strict connection config option") async def test_service_create_temporary_strict_connection_url_strict_connection_disabled( hass: HomeAssistant, ) -> None: @@ -323,6 +324,7 @@ async def test_service_create_temporary_strict_connection_url_strict_connection_ ) +@pytest.mark.skip(reason="Remove strict connection config option") @pytest.mark.parametrize( ("mode"), [ diff --git a/tests/components/cloud/test_prefs.py b/tests/components/cloud/test_prefs.py index 57715fe2bdf..a8ce88f5700 100644 --- a/tests/components/cloud/test_prefs.py +++ b/tests/components/cloud/test_prefs.py @@ -181,6 +181,7 @@ async def test_tts_default_voice_legacy_gender( assert cloud.client.prefs.tts_default_voice == (expected_language, voice) +@pytest.mark.skip(reason="Remove strict connection config option") @pytest.mark.parametrize("mode", list(StrictConnectionMode)) async def test_strict_connection_convertion( hass: HomeAssistant, diff --git a/tests/components/cloud/test_strict_connection.py b/tests/components/cloud/test_strict_connection.py index f275bc4d2dd..c3329740207 100644 --- a/tests/components/cloud/test_strict_connection.py +++ b/tests/components/cloud/test_strict_connection.py @@ -226,6 +226,7 @@ async def _guard_page_unauthorized_request( assert await req.text() == await hass.async_add_executor_job(read_guard_page) +@pytest.mark.skip(reason="Remove strict connection config option") @pytest.mark.parametrize( "test_func", [ diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index b554737e7b3..9e576e10f4d 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -527,6 +527,7 @@ async def test_logging( assert "GET /api/states/logging.entity" not in caplog.text +@pytest.mark.skip(reason="Remove strict connection config option") async def test_service_create_temporary_strict_connection_url_strict_connection_disabled( hass: HomeAssistant, ) -> None: @@ -544,6 +545,7 @@ async def test_service_create_temporary_strict_connection_url_strict_connection_ ) +@pytest.mark.skip(reason="Remove strict connection config option") @pytest.mark.parametrize( ("mode"), [ diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index e32768ee33e..c9d92c2f25a 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -800,11 +800,10 @@ async def test_async_get_all_descriptions(hass: HomeAssistant) -> None: assert proxy_load_services_files.mock_calls[0][1][1] == unordered( [ await async_get_integration(hass, DOMAIN_GROUP), - await async_get_integration(hass, "http"), # system_health requires http ] ) - assert len(descriptions) == 2 + assert len(descriptions) == 1 assert DOMAIN_GROUP in descriptions assert "description" in descriptions[DOMAIN_GROUP]["reload"] assert "fields" in descriptions[DOMAIN_GROUP]["reload"] @@ -838,7 +837,7 @@ async def test_async_get_all_descriptions(hass: HomeAssistant) -> None: await async_setup_component(hass, DOMAIN_LOGGER, logger_config) descriptions = await service.async_get_all_descriptions(hass) - assert len(descriptions) == 3 + assert len(descriptions) == 2 assert DOMAIN_LOGGER in descriptions assert descriptions[DOMAIN_LOGGER]["set_default_level"]["name"] == "Translated name" assert ( diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index 76acb2ff678..79c64259f8b 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -5,7 +5,6 @@ from unittest.mock import patch import pytest -from homeassistant.components.http.const import StrictConnectionMode from homeassistant.config import YAML_CONFIG_FILE from homeassistant.scripts import check_config @@ -135,7 +134,6 @@ def test_secrets(mock_is_file, event_loop, mock_hass_config_yaml: None) -> None: "login_attempts_threshold": -1, "server_port": 8123, "ssl_profile": "modern", - "strict_connection": StrictConnectionMode.DISABLED, "use_x_frame_options": True, "server_host": ["0.0.0.0", "::"], } From 06e032b8386280537c2f8b3bf152c523002bf97b Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 29 Apr 2024 18:34:20 +0200 Subject: [PATCH 931/967] Update frontend to 20240429.0 (#116404) --- 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 a5446f688ba..e271903a27d 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==20240426.0"] + "requirements": ["home-assistant-frontend==20240429.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1b4223d7b33..a2eb0f1254c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ habluetooth==2.8.0 hass-nabucasa==0.78.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240426.0 +home-assistant-frontend==20240429.0 home-assistant-intents==2024.4.24 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 1f83f00bb53..4e788f9aa80 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1078,7 +1078,7 @@ hole==0.8.0 holidays==0.47 # homeassistant.components.frontend -home-assistant-frontend==20240426.0 +home-assistant-frontend==20240429.0 # homeassistant.components.conversation home-assistant-intents==2024.4.24 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 060287d637a..16975128dae 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -880,7 +880,7 @@ hole==0.8.0 holidays==0.47 # homeassistant.components.frontend -home-assistant-frontend==20240426.0 +home-assistant-frontend==20240429.0 # homeassistant.components.conversation home-assistant-intents==2024.4.24 From a7faf2710f2c5ed62a414b7ec275de7244ce91f1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 29 Apr 2024 19:44:22 +0200 Subject: [PATCH 932/967] Bump version to 2024.5.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 07f4058ea19..35be5835088 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 = 5 -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 68b38bb516b..575063541e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.5.0b2" +version = "2024.5.0b3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 8843780aab7469fb3b928da3744fc40ad422f378 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 30 Apr 2024 09:49:35 +0200 Subject: [PATCH 933/967] Set Synology camera device name as entity name (#109123) --- homeassistant/components/synology_dsm/camera.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/synology_dsm/camera.py b/homeassistant/components/synology_dsm/camera.py index 901fcb1d565..1d03fd4f027 100644 --- a/homeassistant/components/synology_dsm/camera.py +++ b/homeassistant/components/synology_dsm/camera.py @@ -74,7 +74,7 @@ class SynoDSMCamera(SynologyDSMBaseEntity[SynologyDSMCameraUpdateCoordinator], C api_key=SynoSurveillanceStation.CAMERA_API_KEY, key=str(camera_id), camera_id=camera_id, - name=coordinator.data["cameras"][camera_id].name, + name=None, entity_registry_enabled_default=coordinator.data["cameras"][ camera_id ].is_enabled, From 5d9abf9ac52b49c210650626b1c75c8d31479d17 Mon Sep 17 00:00:00 2001 From: Collin Fair Date: Tue, 30 Apr 2024 01:18:09 -0600 Subject: [PATCH 934/967] Fix stale prayer times from `islamic-prayer-times` (#115683) --- .../islamic_prayer_times/coordinator.py | 83 +++++++++---------- .../islamic_prayer_times/__init__.py | 23 ++++- .../islamic_prayer_times/test_init.py | 49 ++++++++++- .../islamic_prayer_times/test_sensor.py | 31 +++++-- 4 files changed, 131 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/islamic_prayer_times/coordinator.py b/homeassistant/components/islamic_prayer_times/coordinator.py index 2785f69534c..7005bee3585 100644 --- a/homeassistant/components/islamic_prayer_times/coordinator.py +++ b/homeassistant/components/islamic_prayer_times/coordinator.py @@ -2,7 +2,7 @@ from __future__ import annotations -from datetime import datetime, timedelta +from datetime import date, datetime, timedelta import logging from typing import Any, cast @@ -70,8 +70,8 @@ class IslamicPrayerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, datetim """Return the school.""" return self.config_entry.options.get(CONF_SCHOOL, DEFAULT_SCHOOL) - def get_new_prayer_times(self) -> dict[str, Any]: - """Fetch prayer times for today.""" + def get_new_prayer_times(self, for_date: date) -> dict[str, Any]: + """Fetch prayer times for the specified date.""" calc = PrayerTimesCalculator( latitude=self.latitude, longitude=self.longitude, @@ -79,7 +79,7 @@ class IslamicPrayerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, datetim latitudeAdjustmentMethod=self.lat_adj_method, midnightMode=self.midnight_mode, school=self.school, - date=str(dt_util.now().date()), + date=str(for_date), iso8601=True, ) return cast(dict[str, Any], calc.fetch_prayer_times()) @@ -88,51 +88,18 @@ class IslamicPrayerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, datetim def async_schedule_future_update(self, midnight_dt: datetime) -> None: """Schedule future update for sensors. - Midnight is a calculated time. The specifics of the calculation - depends on the method of the prayer time calculation. This calculated - midnight is the time at which the time to pray the Isha prayers have - expired. + The least surprising behaviour is to load the next day's prayer times only + after the current day's prayers are complete. We will take the fiqhi opinion + that Isha should be prayed before Islamic midnight (which may be before or after 12:00 midnight), + and thus we will switch to the next day's timings at Islamic midnight. - Calculated Midnight: The Islamic midnight. - Traditional Midnight: 12:00AM - - Update logic for prayer times: - - If the Calculated Midnight is before the traditional midnight then wait - until the traditional midnight to run the update. This way the day - will have changed over and we don't need to do any fancy calculations. - - If the Calculated Midnight is after the traditional midnight, then wait - until after the calculated Midnight. We don't want to update the prayer - times too early or else the timings might be incorrect. - - Example: - calculated midnight = 11:23PM (before traditional midnight) - Update time: 12:00AM - - calculated midnight = 1:35AM (after traditional midnight) - update time: 1:36AM. + The +1s is to ensure that any automations predicated on the arrival of Islamic midnight will run. """ _LOGGER.debug("Scheduling next update for Islamic prayer times") - now = dt_util.utcnow() - - if now > midnight_dt: - next_update_at = midnight_dt + timedelta(days=1, minutes=1) - _LOGGER.debug( - "Midnight is after the day changes so schedule update for after Midnight the next day" - ) - else: - _LOGGER.debug( - "Midnight is before the day changes so schedule update for the next start of day" - ) - next_update_at = dt_util.start_of_local_day(now + timedelta(days=1)) - - _LOGGER.debug("Next update scheduled for: %s", next_update_at) - self.event_unsub = async_track_point_in_time( - self.hass, self.async_request_update, next_update_at + self.hass, self.async_request_update, midnight_dt + timedelta(seconds=1) ) async def async_request_update(self, _: datetime) -> None: @@ -140,8 +107,34 @@ class IslamicPrayerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, datetim await self.async_request_refresh() async def _async_update_data(self) -> dict[str, datetime]: - """Update sensors with new prayer times.""" - prayer_times = self.get_new_prayer_times() + """Update sensors with new prayer times. + + Prayer time calculations "roll over" at 12:00 midnight - but this does not mean that all prayers + occur within that Gregorian calendar day. For instance Jasper, Alta. sees Isha occur after 00:00 in the summer. + It is similarly possible (albeit less likely) that Fajr occurs before 00:00. + + As such, to ensure that no prayer times are "unreachable" (e.g. we always see the Isha timestamp pass before loading the next day's times), + we calculate 3 days' worth of times (-1, 0, +1 days) and select the appropriate set based on Islamic midnight. + + The calculation is inexpensive, so there is no need to cache it. + """ + + # Zero out the us component to maintain consistent rollover at T+1s + now = dt_util.now().replace(microsecond=0) + yesterday_times = self.get_new_prayer_times((now - timedelta(days=1)).date()) + today_times = self.get_new_prayer_times(now.date()) + tomorrow_times = self.get_new_prayer_times((now + timedelta(days=1)).date()) + + if ( + yesterday_midnight := dt_util.parse_datetime(yesterday_times["Midnight"]) + ) and now <= yesterday_midnight: + prayer_times = yesterday_times + elif ( + tomorrow_midnight := dt_util.parse_datetime(today_times["Midnight"]) + ) and now > tomorrow_midnight: + prayer_times = tomorrow_times + else: + prayer_times = today_times # introduced in prayer-times-calculator 0.0.8 prayer_times.pop("date", None) diff --git a/tests/components/islamic_prayer_times/__init__.py b/tests/components/islamic_prayer_times/__init__.py index 1e6d6815921..522006b0847 100644 --- a/tests/components/islamic_prayer_times/__init__.py +++ b/tests/components/islamic_prayer_times/__init__.py @@ -12,6 +12,17 @@ MOCK_USER_INPUT = { MOCK_CONFIG = {CONF_LATITUDE: 12.34, CONF_LONGITUDE: 23.45} + +PRAYER_TIMES_YESTERDAY = { + "Fajr": "2019-12-31T06:09:00+00:00", + "Sunrise": "2019-12-31T07:24:00+00:00", + "Dhuhr": "2019-12-31T12:29:00+00:00", + "Asr": "2019-12-31T15:31:00+00:00", + "Maghrib": "2019-12-31T17:34:00+00:00", + "Isha": "2019-12-31T18:52:00+00:00", + "Midnight": "2020-01-01T00:44:00+00:00", +} + PRAYER_TIMES = { "Fajr": "2020-01-01T06:10:00+00:00", "Sunrise": "2020-01-01T07:25:00+00:00", @@ -19,7 +30,17 @@ PRAYER_TIMES = { "Asr": "2020-01-01T15:32:00+00:00", "Maghrib": "2020-01-01T17:35:00+00:00", "Isha": "2020-01-01T18:53:00+00:00", - "Midnight": "2020-01-01T00:45:00+00:00", + "Midnight": "2020-01-02T00:45:00+00:00", +} + +PRAYER_TIMES_TOMORROW = { + "Fajr": "2020-01-02T06:11:00+00:00", + "Sunrise": "2020-01-02T07:26:00+00:00", + "Dhuhr": "2020-01-02T12:31:00+00:00", + "Asr": "2020-01-02T15:33:00+00:00", + "Maghrib": "2020-01-02T17:36:00+00:00", + "Isha": "2020-01-02T18:54:00+00:00", + "Midnight": "2020-01-03T00:46:00+00:00", } NOW = datetime(2020, 1, 1, 00, 00, 0, tzinfo=dt_util.UTC) diff --git a/tests/components/islamic_prayer_times/test_init.py b/tests/components/islamic_prayer_times/test_init.py index c5d4933e24a..2a2597ef0ce 100644 --- a/tests/components/islamic_prayer_times/test_init.py +++ b/tests/components/islamic_prayer_times/test_init.py @@ -1,5 +1,6 @@ """Tests for Islamic Prayer Times init.""" +from datetime import timedelta from unittest.mock import patch from freezegun import freeze_time @@ -12,10 +13,11 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +import homeassistant.util.dt as dt_util from . import NOW, PRAYER_TIMES -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed @pytest.fixture(autouse=True) @@ -76,13 +78,16 @@ async def test_options_listener(hass: HomeAssistant) -> None: ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert mock_fetch_prayer_times.call_count == 1 + # Each scheduling run calls this 3 times (yesterday, today, tomorrow) + assert mock_fetch_prayer_times.call_count == 3 + mock_fetch_prayer_times.reset_mock() hass.config_entries.async_update_entry( entry, options={CONF_CALC_METHOD: "makkah"} ) await hass.async_block_till_done() - assert mock_fetch_prayer_times.call_count == 2 + # Each scheduling run calls this 3 times (yesterday, today, tomorrow) + assert mock_fetch_prayer_times.call_count == 3 @pytest.mark.parametrize( @@ -155,3 +160,41 @@ async def test_migration_from_1_1_to_1_2(hass: HomeAssistant) -> None: CONF_LONGITUDE: hass.config.longitude, } assert entry.minor_version == 2 + + +async def test_update_scheduling(hass: HomeAssistant) -> None: + """Test that integration schedules update immediately after Islamic midnight.""" + entry = MockConfigEntry(domain=islamic_prayer_times.DOMAIN, data={}) + entry.add_to_hass(hass) + + with ( + patch( + "prayer_times_calculator_offline.PrayerTimesCalculator.fetch_prayer_times", + return_value=PRAYER_TIMES, + ), + freeze_time(NOW), + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.LOADED + + with patch( + "prayer_times_calculator_offline.PrayerTimesCalculator.fetch_prayer_times", + return_value=PRAYER_TIMES, + ) as mock_fetch_prayer_times: + midnight_time = dt_util.parse_datetime(PRAYER_TIMES["Midnight"]) + assert midnight_time + with freeze_time(midnight_time): + async_fire_time_changed(hass, midnight_time) + await hass.async_block_till_done() + + mock_fetch_prayer_times.assert_not_called() + + midnight_time += timedelta(seconds=1) + with freeze_time(midnight_time): + async_fire_time_changed(hass, midnight_time) + await hass.async_block_till_done() + + # Each scheduling run calls this 3 times (yesterday, today, tomorrow) + assert mock_fetch_prayer_times.call_count == 3 diff --git a/tests/components/islamic_prayer_times/test_sensor.py b/tests/components/islamic_prayer_times/test_sensor.py index 1f8d28dfb6f..7bd1a1192ad 100644 --- a/tests/components/islamic_prayer_times/test_sensor.py +++ b/tests/components/islamic_prayer_times/test_sensor.py @@ -1,5 +1,6 @@ """The tests for the Islamic prayer times sensor platform.""" +from datetime import timedelta from unittest.mock import patch from freezegun import freeze_time @@ -8,7 +9,7 @@ import pytest from homeassistant.components.islamic_prayer_times.const import DOMAIN from homeassistant.core import HomeAssistant -from . import NOW, PRAYER_TIMES +from . import NOW, PRAYER_TIMES, PRAYER_TIMES_TOMORROW, PRAYER_TIMES_YESTERDAY from tests.common import MockConfigEntry @@ -31,20 +32,38 @@ def set_utc(hass: HomeAssistant) -> None: ("Midnight", "sensor.islamic_prayer_times_midnight_time"), ], ) +# In our example data, Islamic midnight occurs at 00:44 (yesterday's times, occurs today) and 00:45 (today's times, occurs tomorrow), +# hence we check that the times roll over at exactly the desired minute +@pytest.mark.parametrize( + ("offset", "prayer_times"), + [ + (timedelta(days=-1), PRAYER_TIMES_YESTERDAY), + (timedelta(minutes=44), PRAYER_TIMES_YESTERDAY), + (timedelta(minutes=44, seconds=1), PRAYER_TIMES), # Rolls over at 00:44 + 1 sec + (timedelta(days=1, minutes=45), PRAYER_TIMES), + ( + timedelta(days=1, minutes=45, seconds=1), # Rolls over at 00:45 + 1 sec + PRAYER_TIMES_TOMORROW, + ), + ], +) async def test_islamic_prayer_times_sensors( - hass: HomeAssistant, key: str, sensor_name: str + hass: HomeAssistant, + key: str, + sensor_name: str, + offset: timedelta, + prayer_times: dict[str, str], ) -> None: """Test minimum Islamic prayer times configuration.""" entry = MockConfigEntry(domain=DOMAIN, data={}) entry.add_to_hass(hass) - with ( patch( "prayer_times_calculator_offline.PrayerTimesCalculator.fetch_prayer_times", - return_value=PRAYER_TIMES, + side_effect=(PRAYER_TIMES_YESTERDAY, PRAYER_TIMES, PRAYER_TIMES_TOMORROW), ), - freeze_time(NOW), + freeze_time(NOW + offset), ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert hass.states.get(sensor_name).state == PRAYER_TIMES[key] + assert hass.states.get(sensor_name).state == prayer_times[key] From 3477c81ed1c8146c4a30aa794b1f55651b0cfdf6 Mon Sep 17 00:00:00 2001 From: Graham Wetzler Date: Tue, 30 Apr 2024 02:47:06 -0500 Subject: [PATCH 935/967] Bump smart_meter_texas to 0.5.5 (#116321) --- homeassistant/components/smart_meter_texas/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/smart_meter_texas/conftest.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/smart_meter_texas/manifest.json b/homeassistant/components/smart_meter_texas/manifest.json index 1b18bbb2bc9..8bf44fbed15 100644 --- a/homeassistant/components/smart_meter_texas/manifest.json +++ b/homeassistant/components/smart_meter_texas/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/smart_meter_texas", "iot_class": "cloud_polling", "loggers": ["smart_meter_texas"], - "requirements": ["smart-meter-texas==0.4.7"] + "requirements": ["smart-meter-texas==0.5.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4e788f9aa80..dca62841c08 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2563,7 +2563,7 @@ slackclient==2.5.0 slixmpp==1.8.5 # homeassistant.components.smart_meter_texas -smart-meter-texas==0.4.7 +smart-meter-texas==0.5.5 # homeassistant.components.smhi smhi-pkg==1.0.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 16975128dae..17411a81818 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1982,7 +1982,7 @@ simplisafe-python==2024.01.0 slackclient==2.5.0 # homeassistant.components.smart_meter_texas -smart-meter-texas==0.4.7 +smart-meter-texas==0.5.5 # homeassistant.components.smhi smhi-pkg==1.0.16 diff --git a/tests/components/smart_meter_texas/conftest.py b/tests/components/smart_meter_texas/conftest.py index 04a3344b5cc..d06571fe05e 100644 --- a/tests/components/smart_meter_texas/conftest.py +++ b/tests/components/smart_meter_texas/conftest.py @@ -58,7 +58,7 @@ def mock_connection( """Mock all calls to the API.""" aioclient_mock.get(BASE_URL) - auth_endpoint = f"{BASE_ENDPOINT}{AUTH_ENDPOINT}" + auth_endpoint = AUTH_ENDPOINT if not auth_fail and not auth_timeout: aioclient_mock.post( auth_endpoint, From 1a1dfbd4891f277810a1b53d5d623467089be862 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 29 Apr 2024 20:13:36 +0200 Subject: [PATCH 936/967] Remove semicolon in Modbus (#116399) --- homeassistant/components/modbus/modbus.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index bd7eed8235c..a5c0867dedb 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -245,7 +245,7 @@ async def async_modbus_setup( translation_key="deprecated_restart", ) _LOGGER.warning( - "`modbus.restart`: is deprecated and will be removed in version 2024.11" + "`modbus.restart` is deprecated and will be removed in version 2024.11" ) async_dispatcher_send(hass, SIGNAL_START_ENTITY) hub = hub_collect[service.data[ATTR_HUB]] From bd8ded1e55c4b148dc323f5843381ccd761f5724 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 29 Apr 2024 20:19:14 +0200 Subject: [PATCH 937/967] Fix error handling in Shell Command integration (#116409) * raise proper HomeAssistantError on command timeout * raise proper HomeAssistantError on non-utf8 command output * add error translation and test it * Update homeassistant/components/shell_command/strings.json * Update tests/components/shell_command/test_init.py --------- Co-authored-by: G Johansson --- .../components/shell_command/__init__.py | 21 ++++++++++++++----- .../components/shell_command/strings.json | 10 +++++++++ tests/components/shell_command/test_init.py | 12 ++++++++--- 3 files changed, 35 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/shell_command/strings.json diff --git a/homeassistant/components/shell_command/__init__.py b/homeassistant/components/shell_command/__init__.py index 95bbb01bcfb..c2c384e39aa 100644 --- a/homeassistant/components/shell_command/__init__.py +++ b/homeassistant/components/shell_command/__init__.py @@ -15,7 +15,7 @@ from homeassistant.core import ( ServiceResponse, SupportsResponse, ) -from homeassistant.exceptions import TemplateError +from homeassistant.exceptions import HomeAssistantError, TemplateError from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.typing import ConfigType from homeassistant.util.json import JsonObjectType @@ -91,7 +91,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: try: async with asyncio.timeout(COMMAND_TIMEOUT): stdout_data, stderr_data = await process.communicate() - except TimeoutError: + except TimeoutError as err: _LOGGER.error( "Timed out running command: `%s`, after: %ss", cmd, COMMAND_TIMEOUT ) @@ -103,7 +103,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: process._transport.close() # type: ignore[attr-defined] del process - raise + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="timeout", + translation_placeholders={ + "command": cmd, + "timeout": str(COMMAND_TIMEOUT), + }, + ) from err if stdout_data: _LOGGER.debug( @@ -135,11 +142,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: service_response["stdout"] = stdout_data.decode("utf-8").strip() if stderr_data: service_response["stderr"] = stderr_data.decode("utf-8").strip() - except UnicodeDecodeError: + except UnicodeDecodeError as err: _LOGGER.exception( "Unable to handle non-utf8 output of command: `%s`", cmd ) - raise + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="non_utf8_output", + translation_placeholders={"command": cmd}, + ) from err return service_response return None diff --git a/homeassistant/components/shell_command/strings.json b/homeassistant/components/shell_command/strings.json new file mode 100644 index 00000000000..c87dac15b2d --- /dev/null +++ b/homeassistant/components/shell_command/strings.json @@ -0,0 +1,10 @@ +{ + "exceptions": { + "timeout": { + "message": "Timed out running command: `{command}`, after: {timeout} seconds" + }, + "non_utf8_output": { + "message": "Unable to handle non-utf8 output of command: `{command}`" + } + } +} diff --git a/tests/components/shell_command/test_init.py b/tests/components/shell_command/test_init.py index 93b06ddf9d8..526ac1643ec 100644 --- a/tests/components/shell_command/test_init.py +++ b/tests/components/shell_command/test_init.py @@ -11,7 +11,7 @@ import pytest from homeassistant.components import shell_command from homeassistant.core import HomeAssistant -from homeassistant.exceptions import TemplateError +from homeassistant.exceptions import HomeAssistantError, TemplateError from homeassistant.setup import async_setup_component @@ -199,7 +199,10 @@ async def test_non_text_stdout_capture( assert not response # Non-text output throws with 'return_response' - with pytest.raises(UnicodeDecodeError): + with pytest.raises( + HomeAssistantError, + match="Unable to handle non-utf8 output of command: `curl -o - https://raw.githubusercontent.com/home-assistant/assets/master/misc/loading-screen.gif`", + ): response = await hass.services.async_call( "shell_command", "output_image", blocking=True, return_response=True ) @@ -258,7 +261,10 @@ async def test_do_not_run_forever( side_effect=mock_create_subprocess_shell, ), ): - with pytest.raises(TimeoutError): + with pytest.raises( + HomeAssistantError, + match="Timed out running command: `mock_sleep 10000`, after: 0.001 seconds", + ): await hass.services.async_call( shell_command.DOMAIN, "test_service", From 5510315b87916402877116dbdf480bc3b95458f2 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 30 Apr 2024 09:48:58 +0200 Subject: [PATCH 938/967] Fix zoneminder async (#116436) --- homeassistant/components/zoneminder/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zoneminder/__init__.py b/homeassistant/components/zoneminder/__init__.py index 0510ff58d35..b4a406cec4e 100644 --- a/homeassistant/components/zoneminder/__init__.py +++ b/homeassistant/components/zoneminder/__init__.py @@ -55,7 +55,7 @@ SET_RUN_STATE_SCHEMA = vol.Schema( ) -def setup(hass: HomeAssistant, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the ZoneMinder component.""" hass.data[DOMAIN] = {} @@ -99,7 +99,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: state_name, ) - hass.services.register( + hass.services.async_register( DOMAIN, SERVICE_SET_RUN_STATE, set_active_state, schema=SET_RUN_STATE_SCHEMA ) From 7cbb2892c115d97510adfffa429ee5c19c4c8929 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 30 Apr 2024 12:41:34 +0200 Subject: [PATCH 939/967] Add user id to coordinator name in Withings (#116440) * Add user id to coordinator name in Withings * Add user id to coordinator name in Withings * Fix --- homeassistant/components/withings/coordinator.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/withings/coordinator.py b/homeassistant/components/withings/coordinator.py index 19b362dfa0a..0aef11aaa6b 100644 --- a/homeassistant/components/withings/coordinator.py +++ b/homeassistant/components/withings/coordinator.py @@ -45,9 +45,10 @@ class WithingsDataUpdateCoordinator(DataUpdateCoordinator[_T]): super().__init__( hass, LOGGER, - name=f"Withings {self.coordinator_name}", + name="", update_interval=self._default_update_interval, ) + self.name = f"Withings {self.config_entry.unique_id} {self.coordinator_name}" self._client = client self.notification_categories: set[NotificationCategory] = set() @@ -63,7 +64,11 @@ class WithingsDataUpdateCoordinator(DataUpdateCoordinator[_T]): self, notification_category: NotificationCategory ) -> None: """Update data when webhook is called.""" - LOGGER.debug("Withings webhook triggered for %s", notification_category) + LOGGER.debug( + "Withings webhook triggered for category %s for user %s", + notification_category, + self.config_entry.unique_id, + ) await self.async_request_refresh() async def _async_update_data(self) -> _T: From 5b7e09b8868d77e6abd5694ee2c729eb23b14d42 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 30 Apr 2024 12:47:51 +0200 Subject: [PATCH 940/967] Bump version to 2024.5.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 35be5835088..3a0d35b8324 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 = 5 -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 575063541e9..a04e9fd218a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.5.0b3" +version = "2024.5.0b4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From c0d529b072c0f5ef6d62f42e7e9029003a2b77c5 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 1 May 2024 11:46:52 +0200 Subject: [PATCH 941/967] Some fixes for the Matter light discovery schema (#116108) * Fix discovery schema for light platform * fix switch platform discovery schema * extend light tests * Update switch.py * clarify comment * use parameter for supported_color_modes --- homeassistant/components/matter/light.py | 41 +-- homeassistant/components/matter/switch.py | 6 +- ...onoff-light-with-levelcontrol-present.json | 244 ++++++++++++++++++ tests/components/matter/test_light.py | 29 ++- 4 files changed, 273 insertions(+), 47 deletions(-) create mode 100644 tests/components/matter/fixtures/nodes/onoff-light-with-levelcontrol-present.json diff --git a/homeassistant/components/matter/light.py b/homeassistant/components/matter/light.py index fce780896a4..c9556fd2e2e 100644 --- a/homeassistant/components/matter/light.py +++ b/homeassistant/components/matter/light.py @@ -295,7 +295,10 @@ class MatterLight(MatterEntity, LightEntity): # brightness support if self._entity_info.endpoint.has_attribute( None, clusters.LevelControl.Attributes.CurrentLevel - ): + ) and self._entity_info.endpoint.device_types != {device_types.OnOffLight}: + # We need to filter out the OnOffLight device type here because + # that can have an optional LevelControl cluster present + # which we should ignore. supported_color_modes.add(ColorMode.BRIGHTNESS) self._supports_brightness = True # colormode(s) @@ -406,11 +409,11 @@ DISCOVERY_SCHEMAS = [ entity_class=MatterLight, required_attributes=( clusters.OnOff.Attributes.OnOff, - clusters.LevelControl.Attributes.CurrentLevel, clusters.ColorControl.Attributes.CurrentHue, clusters.ColorControl.Attributes.CurrentSaturation, ), optional_attributes=( + clusters.LevelControl.Attributes.CurrentLevel, clusters.ColorControl.Attributes.ColorTemperatureMireds, clusters.ColorControl.Attributes.ColorMode, clusters.ColorControl.Attributes.CurrentX, @@ -426,11 +429,11 @@ DISCOVERY_SCHEMAS = [ entity_class=MatterLight, required_attributes=( clusters.OnOff.Attributes.OnOff, - clusters.LevelControl.Attributes.CurrentLevel, clusters.ColorControl.Attributes.CurrentX, clusters.ColorControl.Attributes.CurrentY, ), optional_attributes=( + clusters.LevelControl.Attributes.CurrentLevel, clusters.ColorControl.Attributes.ColorTemperatureMireds, clusters.ColorControl.Attributes.ColorMode, clusters.ColorControl.Attributes.CurrentHue, @@ -451,36 +454,4 @@ DISCOVERY_SCHEMAS = [ ), optional_attributes=(clusters.ColorControl.Attributes.ColorMode,), ), - # Additional schema to match generic dimmable lights with incorrect/missing device type - MatterDiscoverySchema( - platform=Platform.LIGHT, - entity_description=LightEntityDescription( - key="MatterDimmableLightFallback", name=None - ), - entity_class=MatterLight, - required_attributes=( - clusters.OnOff.Attributes.OnOff, - clusters.LevelControl.Attributes.CurrentLevel, - ), - optional_attributes=( - clusters.ColorControl.Attributes.ColorMode, - clusters.ColorControl.Attributes.CurrentHue, - clusters.ColorControl.Attributes.CurrentSaturation, - clusters.ColorControl.Attributes.CurrentX, - clusters.ColorControl.Attributes.CurrentY, - clusters.ColorControl.Attributes.ColorTemperatureMireds, - ), - # important: make sure to rule out all device types that are also based on the - # onoff and levelcontrol clusters ! - not_device_type=( - device_types.Fan, - device_types.GenericSwitch, - device_types.OnOffPlugInUnit, - device_types.HeatingCoolingUnit, - device_types.Pump, - device_types.CastingVideoClient, - device_types.VideoRemoteControl, - device_types.Speaker, - ), - ), ] diff --git a/homeassistant/components/matter/switch.py b/homeassistant/components/matter/switch.py index 9bc858d40c0..f148102cfcd 100644 --- a/homeassistant/components/matter/switch.py +++ b/homeassistant/components/matter/switch.py @@ -81,12 +81,8 @@ DISCOVERY_SCHEMAS = [ device_types.ColorTemperatureLight, device_types.DimmableLight, device_types.ExtendedColorLight, - device_types.OnOffLight, - device_types.DoorLock, device_types.ColorDimmerSwitch, - device_types.DimmerSwitch, - device_types.Thermostat, - device_types.RoomAirConditioner, + device_types.OnOffLight, ), ), ] diff --git a/tests/components/matter/fixtures/nodes/onoff-light-with-levelcontrol-present.json b/tests/components/matter/fixtures/nodes/onoff-light-with-levelcontrol-present.json new file mode 100644 index 00000000000..c1264f5b7ea --- /dev/null +++ b/tests/components/matter/fixtures/nodes/onoff-light-with-levelcontrol-present.json @@ -0,0 +1,244 @@ +{ + "node_id": 8, + "date_commissioned": "2024-03-07T01:39:20.590755", + "last_interview": "2024-04-02T14:16:31.045880", + "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": [ + { + "254": 1 + }, + { + "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": "Leviton", + "0/40/2": 4251, + "0/40/3": "D215S", + "0/40/4": 4097, + "0/40/5": "", + "0/40/6": "**REDACTED**", + "0/40/7": 1, + "0/40/8": "1.0", + "0/40/9": 131365, + "0/40/10": "2.1.25", + "0/40/15": "12345678", + "0/40/18": "abcdefgh", + "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/2": 10, + "0/49/3": 20, + "0/49/4": true, + "0/49/5": 0, + "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/1": 1, + "0/51/2": 2380987, + "0/51/3": 661, + "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/1": 49792, + "0/52/2": 262528, + "0/52/3": 272704, + "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": "blah", + "0/54/1": 4, + "0/54/2": 3, + "0/54/3": 11, + "0/54/4": -43, + "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/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": 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/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": 0, + "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": 256, + "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 9c3c2610d92..775790701d1 100644 --- a/tests/components/matter/test_light.py +++ b/tests/components/matter/test_light.py @@ -18,21 +18,31 @@ from .common import ( # This tests needs to be adjusted to remove lingering tasks @pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize( - ("fixture", "entity_id"), + ("fixture", "entity_id", "supported_color_modes"), [ - ("extended-color-light", "light.mock_extended_color_light"), - ("color-temperature-light", "light.mock_color_temperature_light"), - ("dimmable-light", "light.mock_dimmable_light"), - ("onoff-light", "light.mock_onoff_light"), + ( + "extended-color-light", + "light.mock_extended_color_light", + ["color_temp", "hs", "xy"], + ), + ( + "color-temperature-light", + "light.mock_color_temperature_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"]), ], ) -async def test_on_off_light( +async def test_light_turn_on_off( hass: HomeAssistant, matter_client: MagicMock, fixture: str, entity_id: str, + supported_color_modes: list[str], ) -> None: - """Test an on/off light.""" + """Test basic light discovery and turn on/off.""" light_node = await setup_integration_with_node_fixture( hass, @@ -48,6 +58,11 @@ async def test_on_off_light( assert state is not None assert state.state == "off" + # check the supported_color_modes + # especially important is the onoff light device type that does have + # a levelcontrol cluster present which we should ignore + assert state.attributes["supported_color_modes"] == supported_color_modes + # Test that the light is on set_node_attribute(light_node, 1, 6, 0, True) await trigger_subscription_callback(hass, matter_client) From 78d19854dda6f60a6b36d20efc889c32c71a91b8 Mon Sep 17 00:00:00 2001 From: Richard Kroegel <42204099+rikroe@users.noreply.github.com> Date: Tue, 30 Apr 2024 22:49:28 +0200 Subject: [PATCH 942/967] Bump bimmer_connected to 0.15.2 (#116424) Co-authored-by: Richard --- .../bmw_connected_drive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../bmw_connected_drive/conftest.py | 3 +- .../snapshots/test_diagnostics.ambr | 1618 +++++++++-------- 5 files changed, 871 insertions(+), 756 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index 854a2f87410..c6b180ca728 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", "iot_class": "cloud_polling", "loggers": ["bimmer_connected"], - "requirements": ["bimmer-connected[china]==0.14.6"] + "requirements": ["bimmer-connected[china]==0.15.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index dca62841c08..04097f0a9bc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -544,7 +544,7 @@ beautifulsoup4==4.12.3 bellows==0.38.2 # homeassistant.components.bmw_connected_drive -bimmer-connected[china]==0.14.6 +bimmer-connected[china]==0.15.2 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 17411a81818..bbc60f01547 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -469,7 +469,7 @@ beautifulsoup4==4.12.3 bellows==0.38.2 # homeassistant.components.bmw_connected_drive -bimmer-connected[china]==0.14.6 +bimmer-connected[china]==0.15.2 # homeassistant.components.eq3btsmart # homeassistant.components.esphome diff --git a/tests/components/bmw_connected_drive/conftest.py b/tests/components/bmw_connected_drive/conftest.py index c3a89e28bd6..f43a7c089c7 100644 --- a/tests/components/bmw_connected_drive/conftest.py +++ b/tests/components/bmw_connected_drive/conftest.py @@ -2,7 +2,7 @@ from collections.abc import Generator -from bimmer_connected.tests import ALL_CHARGING_SETTINGS, ALL_STATES +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 @@ -23,6 +23,7 @@ def bmw_fixture( "WBA00000000DEMO03", "WBY00000000REXI01", ], + profiles=ALL_PROFILES, states=ALL_STATES, charging_settings=ALL_CHARGING_SETTINGS, ) diff --git a/tests/components/bmw_connected_drive/snapshots/test_diagnostics.ambr b/tests/components/bmw_connected_drive/snapshots/test_diagnostics.ambr index b3af5bc59b6..351c0f062fd 100644 --- a/tests/components/bmw_connected_drive/snapshots/test_diagnostics.ambr +++ b/tests/components/bmw_connected_drive/snapshots/test_diagnostics.ambr @@ -140,19 +140,8 @@ ]), }), 'climate': dict({ - 'account_timezone': dict({ - '_dst_offset': '0:00:00', - '_dst_saved': '0:00:00', - '_hasdst': False, - '_std_offset': '0:00:00', - '_tznames': list([ - 'UTC', - 'UTC', - ]), - }), 'activity': 'INACTIVE', 'activity_end_time': None, - 'activity_end_time_no_tz': None, 'is_climate_on': False, }), 'condition_based_services': dict({ @@ -206,9 +195,7 @@ ]), }), 'data': dict({ - 'appVehicleType': 'DEMO', 'attributes': dict({ - 'a4aType': 'BLUETOOTH', 'bodyType': 'I20', 'brand': 'BMW_I', 'color': 4285537312, @@ -223,7 +210,6 @@ 'headUnitRaw': 'HU_MGU', 'headUnitType': 'MGU', 'hmiVersion': 'ID8', - 'lastFetched': '2023-01-04T14:57:06.019Z', 'model': 'iX xDrive50', 'softwareVersionCurrent': dict({ 'iStep': 300, @@ -413,14 +399,6 @@ 'servicePack': 'WAVE_01', }), 'fetched_at': '2022-07-10T11:00:00+00:00', - 'mappingInfo': dict({ - 'isAssociated': False, - 'isLmmEnabled': False, - 'isPrimaryUser': True, - 'lmmStatusReasons': list([ - ]), - 'mappingStatus': 'CONFIRMED', - }), 'state': dict({ 'chargingProfile': dict({ 'chargingControlType': 'WEEKLY_PLANNER', @@ -786,19 +764,8 @@ 'is_pre_entry_climatization_enabled', ]), 'fuel_and_battery': dict({ - 'account_timezone': dict({ - '_dst_offset': '0:00:00', - '_dst_saved': '0:00:00', - '_hasdst': False, - '_std_offset': '0:00:00', - '_tznames': list([ - 'UTC', - 'UTC', - ]), - }), 'charging_end_time': '2022-07-10T11:10:00+00:00', 'charging_start_time': None, - 'charging_start_time_no_tz': None, 'charging_status': 'CHARGING', 'charging_target': 80, 'is_charger_connected': True, @@ -829,6 +796,7 @@ 'software_version': '07/2021.00', }), 'is_charging_plan_supported': True, + 'is_charging_settings_supported': True, 'is_lsc_enabled': True, 'is_remote_charge_start_enabled': True, 'is_remote_charge_stop_enabled': True, @@ -1026,19 +994,8 @@ ]), }), 'climate': dict({ - 'account_timezone': dict({ - '_dst_offset': '0:00:00', - '_dst_saved': '0:00:00', - '_hasdst': False, - '_std_offset': '0:00:00', - '_tznames': list([ - 'UTC', - 'UTC', - ]), - }), 'activity': 'HEATING', 'activity_end_time': '2022-07-10T11:29:50+00:00', - 'activity_end_time_no_tz': '2022-07-10T11:29:50', 'is_climate_on': True, }), 'condition_based_services': dict({ @@ -1092,9 +1049,7 @@ ]), }), 'data': dict({ - 'appVehicleType': 'DEMO', 'attributes': dict({ - 'a4aType': 'NOT_SUPPORTED', 'bodyType': 'G26', 'brand': 'BMW', 'color': 4284245350, @@ -1109,7 +1064,6 @@ 'headUnitRaw': 'HU_MGU', 'headUnitType': 'MGU', 'hmiVersion': 'ID8', - 'lastFetched': '2023-01-04T14:57:06.019Z', 'model': 'i4 eDrive40', 'softwareVersionCurrent': dict({ 'iStep': 470, @@ -1287,14 +1241,6 @@ 'servicePack': 'WAVE_01', }), 'fetched_at': '2022-07-10T11:00:00+00:00', - 'mappingInfo': dict({ - 'isAssociated': False, - 'isLmmEnabled': False, - 'isPrimaryUser': True, - 'lmmStatusReasons': list([ - ]), - 'mappingStatus': 'CONFIRMED', - }), 'state': dict({ 'chargingProfile': dict({ 'chargingControlType': 'WEEKLY_PLANNER', @@ -1651,19 +1597,8 @@ 'is_pre_entry_climatization_enabled', ]), 'fuel_and_battery': dict({ - 'account_timezone': dict({ - '_dst_offset': '0:00:00', - '_dst_saved': '0:00:00', - '_hasdst': False, - '_std_offset': '0:00:00', - '_tznames': list([ - 'UTC', - 'UTC', - ]), - }), 'charging_end_time': '2022-07-10T11:10:00+00:00', 'charging_start_time': None, - 'charging_start_time_no_tz': None, 'charging_status': 'NOT_CHARGING', 'charging_target': 80, 'is_charger_connected': False, @@ -1694,6 +1629,7 @@ 'software_version': '11/2021.70', }), 'is_charging_plan_supported': True, + 'is_charging_settings_supported': True, 'is_lsc_enabled': True, 'is_remote_charge_start_enabled': False, 'is_remote_charge_stop_enabled': False, @@ -1770,23 +1706,7 @@ 'windows', ]), 'brand': 'bmw', - 'charging_profile': dict({ - 'ac_available_limits': None, - 'ac_current_limit': None, - 'charging_mode': 'IMMEDIATE_CHARGING', - 'charging_preferences': 'NO_PRESELECTION', - 'charging_preferences_service_pack': None, - 'departure_times': list([ - ]), - 'is_pre_entry_climatization_enabled': False, - 'preferred_charging_window': dict({ - '_window_dict': dict({ - }), - 'end_time': '00:00:00', - 'start_time': '00:00:00', - }), - 'timer_type': 'UNKNOWN', - }), + 'charging_profile': None, 'check_control_messages': dict({ 'has_check_control_messages': False, 'messages': list([ @@ -1803,19 +1723,8 @@ ]), }), 'climate': dict({ - 'account_timezone': dict({ - '_dst_offset': '0:00:00', - '_dst_saved': '0:00:00', - '_hasdst': False, - '_std_offset': '0:00:00', - '_tznames': list([ - 'UTC', - 'UTC', - ]), - }), 'activity': 'INACTIVE', 'activity_end_time': None, - 'activity_end_time_no_tz': None, 'is_climate_on': False, }), 'condition_based_services': dict({ @@ -1878,9 +1787,7 @@ ]), }), 'data': dict({ - 'appVehicleType': 'DEMO', 'attributes': dict({ - 'a4aType': 'NOT_SUPPORTED', 'bodyType': 'G20', 'brand': 'BMW', 'color': 4280233344, @@ -1895,7 +1802,6 @@ 'headUnitRaw': 'HU_MGU', 'headUnitType': 'MGU', 'hmiVersion': 'ID7', - 'lastFetched': '2023-01-04T14:57:06.019Z', 'model': 'M340i xDrive', 'softwareVersionCurrent': dict({ 'iStep': 470, @@ -1974,17 +1880,8 @@ 'vehicleFinder': True, 'vehicleStateSource': 'LAST_STATE_CALL', }), - 'charging_settings': dict({ - }), + 'charging_settings': None, 'fetched_at': '2022-07-10T11:00:00+00:00', - 'mappingInfo': dict({ - 'isAssociated': False, - 'isLmmEnabled': False, - 'isPrimaryUser': True, - 'lmmStatusReasons': list([ - ]), - 'mappingStatus': 'CONFIRMED', - }), 'state': dict({ 'chargingProfile': dict({ 'chargingMode': 'IMMEDIATE_CHARGING', @@ -2288,19 +2185,8 @@ 'remaining_fuel_percent', ]), 'fuel_and_battery': dict({ - 'account_timezone': dict({ - '_dst_offset': '0:00:00', - '_dst_saved': '0:00:00', - '_hasdst': False, - '_std_offset': '0:00:00', - '_tznames': list([ - 'UTC', - 'UTC', - ]), - }), 'charging_end_time': None, 'charging_start_time': None, - 'charging_start_time_no_tz': None, 'charging_status': None, 'charging_target': None, 'is_charger_connected': False, @@ -2331,6 +2217,7 @@ 'software_version': '07/2021.70', }), 'is_charging_plan_supported': False, + 'is_charging_settings_supported': False, 'is_lsc_enabled': True, 'is_remote_charge_start_enabled': False, 'is_remote_charge_stop_enabled': False, @@ -2540,19 +2427,8 @@ ]), }), 'climate': dict({ - 'account_timezone': dict({ - '_dst_offset': '0:00:00', - '_dst_saved': '0:00:00', - '_hasdst': False, - '_std_offset': '0:00:00', - '_tznames': list([ - 'UTC', - 'UTC', - ]), - }), 'activity': 'UNKNOWN', 'activity_end_time': None, - 'activity_end_time_no_tz': None, 'is_climate_on': False, }), 'condition_based_services': dict({ @@ -2588,9 +2464,7 @@ ]), }), 'data': dict({ - 'appVehicleType': 'CONNECTED', 'attributes': dict({ - 'a4aType': 'USB_ONLY', 'bodyType': 'I01', 'brand': 'BMW_I', 'color': 4284110934, @@ -2602,9 +2476,9 @@ 'iosAppScheme': 'bmwdriversguide:///open', 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', }), + 'headUnitRaw': 'MGU_02_L', 'headUnitType': 'NBT', 'hmiVersion': 'ID4', - 'lastFetched': '2022-06-01T19:48:46.540Z', 'model': 'i3 (+ REX)', 'softwareVersionCurrent': dict({ 'iStep': 510, @@ -2622,6 +2496,7 @@ }), 'seriesCluster': 'I001', }), + 'telematicsUnit': 'WAVE01', 'year': 2015, }), 'capabilities': dict({ @@ -2731,10 +2606,6 @@ 'servicePack': 'TCB1', }), 'fetched_at': '2022-07-10T11:00:00+00:00', - 'mappingInfo': dict({ - 'isPrimaryUser': True, - 'mappingStatus': 'CONFIRMED', - }), 'state': dict({ 'chargingProfile': dict({ 'chargingControlType': 'WEEKLY_PLANNER', @@ -2989,19 +2860,8 @@ 'remaining_fuel_percent', ]), 'fuel_and_battery': dict({ - 'account_timezone': dict({ - '_dst_offset': '0:00:00', - '_dst_saved': '0:00:00', - '_hasdst': False, - '_std_offset': '0:00:00', - '_tznames': list([ - 'UTC', - 'UTC', - ]), - }), 'charging_end_time': None, 'charging_start_time': '2022-07-10T18:01:00+00:00', - 'charging_start_time_no_tz': '2022-07-10T18:01:00', 'charging_status': 'WAITING_FOR_CHARGING', 'charging_target': 100, 'is_charger_connected': True, @@ -3032,6 +2892,7 @@ 'software_version': '11/2021.10', }), 'is_charging_plan_supported': True, + 'is_charging_settings_supported': False, 'is_lsc_enabled': True, 'is_remote_charge_start_enabled': False, 'is_remote_charge_stop_enabled': False, @@ -3066,204 +2927,297 @@ ]), 'fingerprint': list([ dict({ - 'content': list([ - dict({ - 'appVehicleType': 'DEMO', - 'attributes': dict({ - 'a4aType': 'BLUETOOTH', - 'bodyType': 'I20', - 'brand': 'BMW_I', - 'color': 4285537312, - 'countryOfOrigin': 'DE', - 'driveTrain': 'ELECTRIC', - 'driverGuideInfo': dict({ - 'androidAppScheme': 'com.bmwgroup.driversguide.row', - 'androidStoreUrl': 'https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row', - 'iosAppScheme': 'bmwdriversguide:///open', - 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', - }), - 'headUnitRaw': 'HU_MGU', - 'headUnitType': 'MGU', - 'hmiVersion': 'ID8', - 'lastFetched': '2023-01-04T14:57:06.019Z', - 'model': 'iX xDrive50', - 'softwareVersionCurrent': dict({ - 'iStep': 300, - 'puStep': dict({ - 'month': 7, - 'year': 21, - }), - 'seriesCluster': 'S21A', - }), - 'softwareVersionExFactory': dict({ - 'iStep': 300, - 'puStep': dict({ - 'month': 7, - 'year': 21, - }), - 'seriesCluster': 'S21A', - }), - 'telematicsUnit': 'WAVE01', - 'year': 2021, + 'content': dict({ + 'capabilities': dict({ + 'climateFunction': 'AIR_CONDITIONING', + 'climateNow': True, + 'climateTimerTrigger': 'DEPARTURE_TIMER', + 'horn': True, + 'isBmwChargingSupported': True, + 'isCarSharingSupported': False, + 'isChargeNowForBusinessSupported': False, + 'isChargingHistorySupported': True, + 'isChargingHospitalityEnabled': False, + 'isChargingLoudnessEnabled': False, + 'isChargingPlanSupported': True, + 'isChargingPowerLimitEnabled': False, + 'isChargingSettingsEnabled': False, + 'isChargingTargetSocEnabled': False, + 'isClimateTimerSupported': True, + 'isCustomerEsimSupported': False, + 'isDCSContractManagementSupported': True, + 'isDataPrivacyEnabled': False, + 'isEasyChargeEnabled': False, + 'isEvGoChargingSupported': False, + 'isMiniChargingSupported': False, + 'isNonLscFeatureEnabled': False, + 'isRemoteEngineStartSupported': False, + 'isRemoteHistoryDeletionSupported': False, + 'isRemoteHistorySupported': True, + 'isRemoteParkingSupported': False, + 'isRemoteServicesActivationRequired': False, + 'isRemoteServicesBookingRequired': False, + 'isScanAndChargeSupported': False, + 'isSustainabilitySupported': False, + 'isWifiHotspotServiceSupported': False, + 'lastStateCallState': 'ACTIVATED', + 'lights': True, + 'lock': True, + 'remoteChargingCommands': dict({ }), - 'mappingInfo': dict({ - 'isAssociated': False, - 'isLmmEnabled': False, - 'isPrimaryUser': True, - 'lmmStatusReasons': list([ + 'sendPoi': True, + 'specialThemeSupport': list([ + ]), + 'unlock': True, + 'vehicleFinder': False, + 'vehicleStateSource': 'LAST_STATE_CALL', + }), + 'state': dict({ + 'chargingProfile': dict({ + 'chargingControlType': 'WEEKLY_PLANNER', + 'chargingMode': 'DELAYED_CHARGING', + 'chargingPreference': 'CHARGING_WINDOW', + 'chargingSettings': dict({ + 'hospitality': 'NO_ACTION', + 'idcc': 'NO_ACTION', + 'targetSoc': 100, + }), + 'climatisationOn': False, + 'departureTimes': list([ + dict({ + 'action': 'DEACTIVATE', + 'id': 1, + 'timeStamp': dict({ + 'hour': 7, + 'minute': 35, + }), + 'timerWeekDays': list([ + 'MONDAY', + 'TUESDAY', + 'WEDNESDAY', + 'THURSDAY', + 'FRIDAY', + ]), + }), + dict({ + 'action': 'DEACTIVATE', + 'id': 2, + 'timeStamp': dict({ + 'hour': 18, + 'minute': 0, + }), + 'timerWeekDays': list([ + 'MONDAY', + 'TUESDAY', + 'WEDNESDAY', + 'THURSDAY', + 'FRIDAY', + 'SATURDAY', + 'SUNDAY', + ]), + }), + dict({ + 'action': 'DEACTIVATE', + 'id': 3, + 'timeStamp': dict({ + 'hour': 7, + 'minute': 0, + }), + 'timerWeekDays': list([ + ]), + }), + dict({ + 'action': 'DEACTIVATE', + 'id': 4, + 'timerWeekDays': list([ + ]), + }), ]), - 'mappingStatus': 'CONFIRMED', + 'reductionOfChargeCurrent': dict({ + 'end': dict({ + 'hour': 1, + 'minute': 30, + }), + 'start': dict({ + 'hour': 18, + 'minute': 1, + }), + }), + }), + 'checkControlMessages': list([ + ]), + 'climateTimers': list([ + dict({ + 'departureTime': dict({ + 'hour': 6, + 'minute': 40, + }), + 'isWeeklyTimer': True, + 'timerAction': 'ACTIVATE', + 'timerWeekDays': list([ + 'THURSDAY', + 'SUNDAY', + ]), + }), + dict({ + 'departureTime': dict({ + 'hour': 12, + 'minute': 50, + }), + 'isWeeklyTimer': False, + 'timerAction': 'ACTIVATE', + 'timerWeekDays': list([ + 'MONDAY', + ]), + }), + dict({ + 'departureTime': dict({ + 'hour': 18, + 'minute': 59, + }), + 'isWeeklyTimer': True, + 'timerAction': 'DEACTIVATE', + 'timerWeekDays': list([ + 'WEDNESDAY', + ]), + }), + ]), + 'combustionFuelLevel': dict({ + 'range': 105, + 'remainingFuelLiters': 6, + }), + 'currentMileage': 137009, + 'doorsState': dict({ + 'combinedSecurityState': 'UNLOCKED', + 'combinedState': 'CLOSED', + 'hood': 'CLOSED', + 'leftFront': 'CLOSED', + 'leftRear': 'CLOSED', + 'rightFront': 'CLOSED', + 'rightRear': 'CLOSED', + 'trunk': 'CLOSED', + }), + 'driverPreferences': dict({ + 'lscPrivacyMode': 'OFF', + }), + 'electricChargingState': dict({ + 'chargingConnectionType': 'CONDUCTIVE', + 'chargingLevelPercent': 82, + 'chargingStatus': 'WAITING_FOR_CHARGING', + 'chargingTarget': 100, + 'isChargerConnected': True, + 'range': 174, + }), + 'isLeftSteering': True, + 'isLscSupported': True, + 'lastFetched': '2022-06-22T14:24:23.982Z', + 'lastUpdatedAt': '2022-06-22T13:58:52Z', + 'range': 174, + 'requiredServices': list([ + dict({ + 'dateTime': '2022-10-01T00:00:00.000Z', + 'description': 'Next service due by the specified date.', + 'status': 'OK', + 'type': 'BRAKE_FLUID', + }), + dict({ + 'dateTime': '2023-05-01T00:00:00.000Z', + 'description': 'Next vehicle check due after the specified distance or date.', + 'status': 'OK', + 'type': 'VEHICLE_CHECK', + }), + dict({ + 'dateTime': '2023-05-01T00:00:00.000Z', + 'description': 'Next state inspection due by the specified date.', + 'status': 'OK', + 'type': 'VEHICLE_TUV', + }), + ]), + 'roofState': dict({ + 'roofState': 'CLOSED', + 'roofStateType': 'SUN_ROOF', + }), + 'windowsState': dict({ + 'combinedState': 'CLOSED', + 'leftFront': 'CLOSED', + 'rightFront': 'CLOSED', }), - 'vin': '**REDACTED**', }), - dict({ - 'appVehicleType': 'DEMO', - 'attributes': dict({ - 'a4aType': 'NOT_SUPPORTED', - 'bodyType': 'G26', - 'brand': 'BMW', - 'color': 4284245350, - 'countryOfOrigin': 'DE', - 'driveTrain': 'ELECTRIC', - 'driverGuideInfo': dict({ - 'androidAppScheme': 'com.bmwgroup.driversguide.row', - 'androidStoreUrl': 'https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row', - 'iosAppScheme': 'bmwdriversguide:///open', - 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', - }), - 'headUnitRaw': 'HU_MGU', - 'headUnitType': 'MGU', - 'hmiVersion': 'ID8', - 'lastFetched': '2023-01-04T14:57:06.019Z', - 'model': 'i4 eDrive40', - 'softwareVersionCurrent': dict({ - 'iStep': 470, - 'puStep': dict({ - 'month': 11, - 'year': 21, - }), - 'seriesCluster': 'G026', - }), - 'softwareVersionExFactory': dict({ - 'iStep': 470, - 'puStep': dict({ - 'month': 11, - 'year': 21, - }), - 'seriesCluster': 'G026', - }), - 'telematicsUnit': 'WAVE01', - 'year': 2021, - }), - 'mappingInfo': dict({ - 'isAssociated': False, - 'isLmmEnabled': False, - 'isPrimaryUser': True, - 'lmmStatusReasons': list([ - ]), - 'mappingStatus': 'CONFIRMED', - }), - 'vin': '**REDACTED**', - }), - dict({ - 'appVehicleType': 'DEMO', - 'attributes': dict({ - 'a4aType': 'NOT_SUPPORTED', - 'bodyType': 'G20', - 'brand': 'BMW', - 'color': 4280233344, - 'countryOfOrigin': 'PT', - 'driveTrain': 'COMBUSTION', - 'driverGuideInfo': dict({ - 'androidAppScheme': 'com.bmwgroup.driversguide.row', - 'androidStoreUrl': 'https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row', - 'iosAppScheme': 'bmwdriversguide:///open', - 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', - }), - 'headUnitRaw': 'HU_MGU', - 'headUnitType': 'MGU', - 'hmiVersion': 'ID7', - 'lastFetched': '2023-01-04T14:57:06.019Z', - 'model': 'M340i xDrive', - 'softwareVersionCurrent': dict({ - 'iStep': 470, - 'puStep': dict({ - 'month': 7, - 'year': 21, - }), - 'seriesCluster': 'S18A', - }), - 'softwareVersionExFactory': dict({ - 'iStep': 420, - 'puStep': dict({ - 'month': 7, - 'year': 20, - }), - 'seriesCluster': 'S18A', - }), - 'telematicsUnit': 'ATM2', - 'year': 2022, - }), - 'mappingInfo': dict({ - 'isAssociated': False, - 'isLmmEnabled': False, - 'isPrimaryUser': True, - 'lmmStatusReasons': list([ - ]), - 'mappingStatus': 'CONFIRMED', - }), - 'vin': '**REDACTED**', - }), - dict({ - 'appVehicleType': 'CONNECTED', - 'attributes': dict({ - 'a4aType': 'USB_ONLY', - 'bodyType': 'I01', - 'brand': 'BMW_I', - 'color': 4284110934, - 'countryOfOrigin': 'CZ', - 'driveTrain': 'ELECTRIC_WITH_RANGE_EXTENDER', - 'driverGuideInfo': dict({ - 'androidAppScheme': 'com.bmwgroup.driversguide.row', - 'androidStoreUrl': 'https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row', - 'iosAppScheme': 'bmwdriversguide:///open', - 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', - }), - 'headUnitType': 'NBT', - 'hmiVersion': 'ID4', - 'lastFetched': '2022-06-01T19:48:46.540Z', - 'model': 'i3 (+ REX)', - 'softwareVersionCurrent': dict({ - 'iStep': 510, - 'puStep': dict({ - 'month': 11, - 'year': 21, - }), - 'seriesCluster': 'I001', - }), - 'softwareVersionExFactory': dict({ - 'iStep': 502, - 'puStep': dict({ - 'month': 3, - 'year': 15, - }), - 'seriesCluster': 'I001', - }), - 'year': 2015, - }), - 'mappingInfo': dict({ - 'isPrimaryUser': True, - 'mappingStatus': 'CONFIRMED', - }), - 'vin': '**REDACTED**', - }), - ]), - 'filename': 'bmw-eadrax-vcs_v4_vehicles.json', + }), + 'filename': 'bmw-eadrax-vcs_v4_vehicles_state_WBY0FINGERPRINT04.json', }), dict({ - 'content': list([ - ]), - 'filename': 'mini-eadrax-vcs_v4_vehicles.json', + 'content': dict({ + 'chargeAndClimateSettings': dict({ + 'chargeAndClimateTimer': dict({ + 'showDepartureTimers': False, + }), + }), + 'chargeAndClimateTimerDetail': dict({ + 'chargingMode': dict({ + 'chargingPreference': 'CHARGING_WINDOW', + 'endTimeSlot': '0001-01-01T01:30:00', + 'startTimeSlot': '0001-01-01T18:01:00', + 'type': 'TIME_SLOT', + }), + 'departureTimer': dict({ + 'type': 'WEEKLY_DEPARTURE_TIMER', + 'weeklyTimers': list([ + dict({ + 'daysOfTheWeek': list([ + 'MONDAY', + 'TUESDAY', + 'WEDNESDAY', + 'THURSDAY', + 'FRIDAY', + ]), + 'id': 1, + 'time': '0001-01-01T07:35:00', + 'timerAction': 'DEACTIVATE', + }), + dict({ + 'daysOfTheWeek': list([ + 'MONDAY', + 'TUESDAY', + 'WEDNESDAY', + 'THURSDAY', + 'FRIDAY', + 'SATURDAY', + 'SUNDAY', + ]), + 'id': 2, + 'time': '0001-01-01T18:00:00', + 'timerAction': 'DEACTIVATE', + }), + dict({ + 'daysOfTheWeek': list([ + ]), + 'id': 3, + 'time': '0001-01-01T07:00:00', + 'timerAction': 'DEACTIVATE', + }), + dict({ + 'daysOfTheWeek': list([ + ]), + 'id': 4, + 'time': '0001-01-01T00:00:00', + 'timerAction': 'DEACTIVATE', + }), + ]), + }), + 'isPreconditionForDepartureActive': False, + }), + 'servicePack': 'TCB1', + }), + 'filename': 'bmw-eadrax-crccs_v2_vehicles_WBY0FINGERPRINT04.json', + }), + dict({ + 'content': dict({ + 'gcid': 'ceb64158-d2ca-47e9-9ee6-cbffb881434e', + 'mappingInfos': list([ + ]), + }), + 'filename': 'mini-eadrax-vcs_v5_vehicle-list.json', }), dict({ 'content': dict({ @@ -4875,19 +4829,8 @@ ]), }), 'climate': dict({ - 'account_timezone': dict({ - '_dst_offset': '0:00:00', - '_dst_saved': '0:00:00', - '_hasdst': False, - '_std_offset': '0:00:00', - '_tznames': list([ - 'UTC', - 'UTC', - ]), - }), 'activity': 'UNKNOWN', 'activity_end_time': None, - 'activity_end_time_no_tz': None, 'is_climate_on': False, }), 'condition_based_services': dict({ @@ -4923,9 +4866,7 @@ ]), }), 'data': dict({ - 'appVehicleType': 'CONNECTED', 'attributes': dict({ - 'a4aType': 'USB_ONLY', 'bodyType': 'I01', 'brand': 'BMW_I', 'color': 4284110934, @@ -4937,9 +4878,9 @@ 'iosAppScheme': 'bmwdriversguide:///open', 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', }), + 'headUnitRaw': 'MGU_02_L', 'headUnitType': 'NBT', 'hmiVersion': 'ID4', - 'lastFetched': '2022-06-01T19:48:46.540Z', 'model': 'i3 (+ REX)', 'softwareVersionCurrent': dict({ 'iStep': 510, @@ -4957,6 +4898,7 @@ }), 'seriesCluster': 'I001', }), + 'telematicsUnit': 'WAVE01', 'year': 2015, }), 'capabilities': dict({ @@ -5066,10 +5008,6 @@ 'servicePack': 'TCB1', }), 'fetched_at': '2022-07-10T11:00:00+00:00', - 'mappingInfo': dict({ - 'isPrimaryUser': True, - 'mappingStatus': 'CONFIRMED', - }), 'state': dict({ 'chargingProfile': dict({ 'chargingControlType': 'WEEKLY_PLANNER', @@ -5324,19 +5262,8 @@ 'remaining_fuel_percent', ]), 'fuel_and_battery': dict({ - 'account_timezone': dict({ - '_dst_offset': '0:00:00', - '_dst_saved': '0:00:00', - '_hasdst': False, - '_std_offset': '0:00:00', - '_tznames': list([ - 'UTC', - 'UTC', - ]), - }), 'charging_end_time': None, 'charging_start_time': '2022-07-10T18:01:00+00:00', - 'charging_start_time_no_tz': '2022-07-10T18:01:00', 'charging_status': 'WAITING_FOR_CHARGING', 'charging_target': 100, 'is_charger_connected': True, @@ -5367,6 +5294,7 @@ 'software_version': '11/2021.10', }), 'is_charging_plan_supported': True, + 'is_charging_settings_supported': False, 'is_lsc_enabled': True, 'is_remote_charge_start_enabled': False, 'is_remote_charge_stop_enabled': False, @@ -5400,204 +5328,297 @@ }), 'fingerprint': list([ dict({ - 'content': list([ - dict({ - 'appVehicleType': 'DEMO', - 'attributes': dict({ - 'a4aType': 'BLUETOOTH', - 'bodyType': 'I20', - 'brand': 'BMW_I', - 'color': 4285537312, - 'countryOfOrigin': 'DE', - 'driveTrain': 'ELECTRIC', - 'driverGuideInfo': dict({ - 'androidAppScheme': 'com.bmwgroup.driversguide.row', - 'androidStoreUrl': 'https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row', - 'iosAppScheme': 'bmwdriversguide:///open', - 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', - }), - 'headUnitRaw': 'HU_MGU', - 'headUnitType': 'MGU', - 'hmiVersion': 'ID8', - 'lastFetched': '2023-01-04T14:57:06.019Z', - 'model': 'iX xDrive50', - 'softwareVersionCurrent': dict({ - 'iStep': 300, - 'puStep': dict({ - 'month': 7, - 'year': 21, - }), - 'seriesCluster': 'S21A', - }), - 'softwareVersionExFactory': dict({ - 'iStep': 300, - 'puStep': dict({ - 'month': 7, - 'year': 21, - }), - 'seriesCluster': 'S21A', - }), - 'telematicsUnit': 'WAVE01', - 'year': 2021, + 'content': dict({ + 'capabilities': dict({ + 'climateFunction': 'AIR_CONDITIONING', + 'climateNow': True, + 'climateTimerTrigger': 'DEPARTURE_TIMER', + 'horn': True, + 'isBmwChargingSupported': True, + 'isCarSharingSupported': False, + 'isChargeNowForBusinessSupported': False, + 'isChargingHistorySupported': True, + 'isChargingHospitalityEnabled': False, + 'isChargingLoudnessEnabled': False, + 'isChargingPlanSupported': True, + 'isChargingPowerLimitEnabled': False, + 'isChargingSettingsEnabled': False, + 'isChargingTargetSocEnabled': False, + 'isClimateTimerSupported': True, + 'isCustomerEsimSupported': False, + 'isDCSContractManagementSupported': True, + 'isDataPrivacyEnabled': False, + 'isEasyChargeEnabled': False, + 'isEvGoChargingSupported': False, + 'isMiniChargingSupported': False, + 'isNonLscFeatureEnabled': False, + 'isRemoteEngineStartSupported': False, + 'isRemoteHistoryDeletionSupported': False, + 'isRemoteHistorySupported': True, + 'isRemoteParkingSupported': False, + 'isRemoteServicesActivationRequired': False, + 'isRemoteServicesBookingRequired': False, + 'isScanAndChargeSupported': False, + 'isSustainabilitySupported': False, + 'isWifiHotspotServiceSupported': False, + 'lastStateCallState': 'ACTIVATED', + 'lights': True, + 'lock': True, + 'remoteChargingCommands': dict({ }), - 'mappingInfo': dict({ - 'isAssociated': False, - 'isLmmEnabled': False, - 'isPrimaryUser': True, - 'lmmStatusReasons': list([ + 'sendPoi': True, + 'specialThemeSupport': list([ + ]), + 'unlock': True, + 'vehicleFinder': False, + 'vehicleStateSource': 'LAST_STATE_CALL', + }), + 'state': dict({ + 'chargingProfile': dict({ + 'chargingControlType': 'WEEKLY_PLANNER', + 'chargingMode': 'DELAYED_CHARGING', + 'chargingPreference': 'CHARGING_WINDOW', + 'chargingSettings': dict({ + 'hospitality': 'NO_ACTION', + 'idcc': 'NO_ACTION', + 'targetSoc': 100, + }), + 'climatisationOn': False, + 'departureTimes': list([ + dict({ + 'action': 'DEACTIVATE', + 'id': 1, + 'timeStamp': dict({ + 'hour': 7, + 'minute': 35, + }), + 'timerWeekDays': list([ + 'MONDAY', + 'TUESDAY', + 'WEDNESDAY', + 'THURSDAY', + 'FRIDAY', + ]), + }), + dict({ + 'action': 'DEACTIVATE', + 'id': 2, + 'timeStamp': dict({ + 'hour': 18, + 'minute': 0, + }), + 'timerWeekDays': list([ + 'MONDAY', + 'TUESDAY', + 'WEDNESDAY', + 'THURSDAY', + 'FRIDAY', + 'SATURDAY', + 'SUNDAY', + ]), + }), + dict({ + 'action': 'DEACTIVATE', + 'id': 3, + 'timeStamp': dict({ + 'hour': 7, + 'minute': 0, + }), + 'timerWeekDays': list([ + ]), + }), + dict({ + 'action': 'DEACTIVATE', + 'id': 4, + 'timerWeekDays': list([ + ]), + }), ]), - 'mappingStatus': 'CONFIRMED', + 'reductionOfChargeCurrent': dict({ + 'end': dict({ + 'hour': 1, + 'minute': 30, + }), + 'start': dict({ + 'hour': 18, + 'minute': 1, + }), + }), + }), + 'checkControlMessages': list([ + ]), + 'climateTimers': list([ + dict({ + 'departureTime': dict({ + 'hour': 6, + 'minute': 40, + }), + 'isWeeklyTimer': True, + 'timerAction': 'ACTIVATE', + 'timerWeekDays': list([ + 'THURSDAY', + 'SUNDAY', + ]), + }), + dict({ + 'departureTime': dict({ + 'hour': 12, + 'minute': 50, + }), + 'isWeeklyTimer': False, + 'timerAction': 'ACTIVATE', + 'timerWeekDays': list([ + 'MONDAY', + ]), + }), + dict({ + 'departureTime': dict({ + 'hour': 18, + 'minute': 59, + }), + 'isWeeklyTimer': True, + 'timerAction': 'DEACTIVATE', + 'timerWeekDays': list([ + 'WEDNESDAY', + ]), + }), + ]), + 'combustionFuelLevel': dict({ + 'range': 105, + 'remainingFuelLiters': 6, + }), + 'currentMileage': 137009, + 'doorsState': dict({ + 'combinedSecurityState': 'UNLOCKED', + 'combinedState': 'CLOSED', + 'hood': 'CLOSED', + 'leftFront': 'CLOSED', + 'leftRear': 'CLOSED', + 'rightFront': 'CLOSED', + 'rightRear': 'CLOSED', + 'trunk': 'CLOSED', + }), + 'driverPreferences': dict({ + 'lscPrivacyMode': 'OFF', + }), + 'electricChargingState': dict({ + 'chargingConnectionType': 'CONDUCTIVE', + 'chargingLevelPercent': 82, + 'chargingStatus': 'WAITING_FOR_CHARGING', + 'chargingTarget': 100, + 'isChargerConnected': True, + 'range': 174, + }), + 'isLeftSteering': True, + 'isLscSupported': True, + 'lastFetched': '2022-06-22T14:24:23.982Z', + 'lastUpdatedAt': '2022-06-22T13:58:52Z', + 'range': 174, + 'requiredServices': list([ + dict({ + 'dateTime': '2022-10-01T00:00:00.000Z', + 'description': 'Next service due by the specified date.', + 'status': 'OK', + 'type': 'BRAKE_FLUID', + }), + dict({ + 'dateTime': '2023-05-01T00:00:00.000Z', + 'description': 'Next vehicle check due after the specified distance or date.', + 'status': 'OK', + 'type': 'VEHICLE_CHECK', + }), + dict({ + 'dateTime': '2023-05-01T00:00:00.000Z', + 'description': 'Next state inspection due by the specified date.', + 'status': 'OK', + 'type': 'VEHICLE_TUV', + }), + ]), + 'roofState': dict({ + 'roofState': 'CLOSED', + 'roofStateType': 'SUN_ROOF', + }), + 'windowsState': dict({ + 'combinedState': 'CLOSED', + 'leftFront': 'CLOSED', + 'rightFront': 'CLOSED', }), - 'vin': '**REDACTED**', }), - dict({ - 'appVehicleType': 'DEMO', - 'attributes': dict({ - 'a4aType': 'NOT_SUPPORTED', - 'bodyType': 'G26', - 'brand': 'BMW', - 'color': 4284245350, - 'countryOfOrigin': 'DE', - 'driveTrain': 'ELECTRIC', - 'driverGuideInfo': dict({ - 'androidAppScheme': 'com.bmwgroup.driversguide.row', - 'androidStoreUrl': 'https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row', - 'iosAppScheme': 'bmwdriversguide:///open', - 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', - }), - 'headUnitRaw': 'HU_MGU', - 'headUnitType': 'MGU', - 'hmiVersion': 'ID8', - 'lastFetched': '2023-01-04T14:57:06.019Z', - 'model': 'i4 eDrive40', - 'softwareVersionCurrent': dict({ - 'iStep': 470, - 'puStep': dict({ - 'month': 11, - 'year': 21, - }), - 'seriesCluster': 'G026', - }), - 'softwareVersionExFactory': dict({ - 'iStep': 470, - 'puStep': dict({ - 'month': 11, - 'year': 21, - }), - 'seriesCluster': 'G026', - }), - 'telematicsUnit': 'WAVE01', - 'year': 2021, - }), - 'mappingInfo': dict({ - 'isAssociated': False, - 'isLmmEnabled': False, - 'isPrimaryUser': True, - 'lmmStatusReasons': list([ - ]), - 'mappingStatus': 'CONFIRMED', - }), - 'vin': '**REDACTED**', - }), - dict({ - 'appVehicleType': 'DEMO', - 'attributes': dict({ - 'a4aType': 'NOT_SUPPORTED', - 'bodyType': 'G20', - 'brand': 'BMW', - 'color': 4280233344, - 'countryOfOrigin': 'PT', - 'driveTrain': 'COMBUSTION', - 'driverGuideInfo': dict({ - 'androidAppScheme': 'com.bmwgroup.driversguide.row', - 'androidStoreUrl': 'https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row', - 'iosAppScheme': 'bmwdriversguide:///open', - 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', - }), - 'headUnitRaw': 'HU_MGU', - 'headUnitType': 'MGU', - 'hmiVersion': 'ID7', - 'lastFetched': '2023-01-04T14:57:06.019Z', - 'model': 'M340i xDrive', - 'softwareVersionCurrent': dict({ - 'iStep': 470, - 'puStep': dict({ - 'month': 7, - 'year': 21, - }), - 'seriesCluster': 'S18A', - }), - 'softwareVersionExFactory': dict({ - 'iStep': 420, - 'puStep': dict({ - 'month': 7, - 'year': 20, - }), - 'seriesCluster': 'S18A', - }), - 'telematicsUnit': 'ATM2', - 'year': 2022, - }), - 'mappingInfo': dict({ - 'isAssociated': False, - 'isLmmEnabled': False, - 'isPrimaryUser': True, - 'lmmStatusReasons': list([ - ]), - 'mappingStatus': 'CONFIRMED', - }), - 'vin': '**REDACTED**', - }), - dict({ - 'appVehicleType': 'CONNECTED', - 'attributes': dict({ - 'a4aType': 'USB_ONLY', - 'bodyType': 'I01', - 'brand': 'BMW_I', - 'color': 4284110934, - 'countryOfOrigin': 'CZ', - 'driveTrain': 'ELECTRIC_WITH_RANGE_EXTENDER', - 'driverGuideInfo': dict({ - 'androidAppScheme': 'com.bmwgroup.driversguide.row', - 'androidStoreUrl': 'https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row', - 'iosAppScheme': 'bmwdriversguide:///open', - 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', - }), - 'headUnitType': 'NBT', - 'hmiVersion': 'ID4', - 'lastFetched': '2022-06-01T19:48:46.540Z', - 'model': 'i3 (+ REX)', - 'softwareVersionCurrent': dict({ - 'iStep': 510, - 'puStep': dict({ - 'month': 11, - 'year': 21, - }), - 'seriesCluster': 'I001', - }), - 'softwareVersionExFactory': dict({ - 'iStep': 502, - 'puStep': dict({ - 'month': 3, - 'year': 15, - }), - 'seriesCluster': 'I001', - }), - 'year': 2015, - }), - 'mappingInfo': dict({ - 'isPrimaryUser': True, - 'mappingStatus': 'CONFIRMED', - }), - 'vin': '**REDACTED**', - }), - ]), - 'filename': 'bmw-eadrax-vcs_v4_vehicles.json', + }), + 'filename': 'bmw-eadrax-vcs_v4_vehicles_state_WBY0FINGERPRINT04.json', }), dict({ - 'content': list([ - ]), - 'filename': 'mini-eadrax-vcs_v4_vehicles.json', + 'content': dict({ + 'chargeAndClimateSettings': dict({ + 'chargeAndClimateTimer': dict({ + 'showDepartureTimers': False, + }), + }), + 'chargeAndClimateTimerDetail': dict({ + 'chargingMode': dict({ + 'chargingPreference': 'CHARGING_WINDOW', + 'endTimeSlot': '0001-01-01T01:30:00', + 'startTimeSlot': '0001-01-01T18:01:00', + 'type': 'TIME_SLOT', + }), + 'departureTimer': dict({ + 'type': 'WEEKLY_DEPARTURE_TIMER', + 'weeklyTimers': list([ + dict({ + 'daysOfTheWeek': list([ + 'MONDAY', + 'TUESDAY', + 'WEDNESDAY', + 'THURSDAY', + 'FRIDAY', + ]), + 'id': 1, + 'time': '0001-01-01T07:35:00', + 'timerAction': 'DEACTIVATE', + }), + dict({ + 'daysOfTheWeek': list([ + 'MONDAY', + 'TUESDAY', + 'WEDNESDAY', + 'THURSDAY', + 'FRIDAY', + 'SATURDAY', + 'SUNDAY', + ]), + 'id': 2, + 'time': '0001-01-01T18:00:00', + 'timerAction': 'DEACTIVATE', + }), + dict({ + 'daysOfTheWeek': list([ + ]), + 'id': 3, + 'time': '0001-01-01T07:00:00', + 'timerAction': 'DEACTIVATE', + }), + dict({ + 'daysOfTheWeek': list([ + ]), + 'id': 4, + 'time': '0001-01-01T00:00:00', + 'timerAction': 'DEACTIVATE', + }), + ]), + }), + 'isPreconditionForDepartureActive': False, + }), + 'servicePack': 'TCB1', + }), + 'filename': 'bmw-eadrax-crccs_v2_vehicles_WBY0FINGERPRINT04.json', + }), + dict({ + 'content': dict({ + 'gcid': 'ceb64158-d2ca-47e9-9ee6-cbffb881434e', + 'mappingInfos': list([ + ]), + }), + 'filename': 'mini-eadrax-vcs_v5_vehicle-list.json', }), dict({ 'content': dict({ @@ -7062,204 +7083,297 @@ 'data': None, 'fingerprint': list([ dict({ - 'content': list([ - dict({ - 'appVehicleType': 'DEMO', - 'attributes': dict({ - 'a4aType': 'BLUETOOTH', - 'bodyType': 'I20', - 'brand': 'BMW_I', - 'color': 4285537312, - 'countryOfOrigin': 'DE', - 'driveTrain': 'ELECTRIC', - 'driverGuideInfo': dict({ - 'androidAppScheme': 'com.bmwgroup.driversguide.row', - 'androidStoreUrl': 'https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row', - 'iosAppScheme': 'bmwdriversguide:///open', - 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', - }), - 'headUnitRaw': 'HU_MGU', - 'headUnitType': 'MGU', - 'hmiVersion': 'ID8', - 'lastFetched': '2023-01-04T14:57:06.019Z', - 'model': 'iX xDrive50', - 'softwareVersionCurrent': dict({ - 'iStep': 300, - 'puStep': dict({ - 'month': 7, - 'year': 21, - }), - 'seriesCluster': 'S21A', - }), - 'softwareVersionExFactory': dict({ - 'iStep': 300, - 'puStep': dict({ - 'month': 7, - 'year': 21, - }), - 'seriesCluster': 'S21A', - }), - 'telematicsUnit': 'WAVE01', - 'year': 2021, + 'content': dict({ + 'capabilities': dict({ + 'climateFunction': 'AIR_CONDITIONING', + 'climateNow': True, + 'climateTimerTrigger': 'DEPARTURE_TIMER', + 'horn': True, + 'isBmwChargingSupported': True, + 'isCarSharingSupported': False, + 'isChargeNowForBusinessSupported': False, + 'isChargingHistorySupported': True, + 'isChargingHospitalityEnabled': False, + 'isChargingLoudnessEnabled': False, + 'isChargingPlanSupported': True, + 'isChargingPowerLimitEnabled': False, + 'isChargingSettingsEnabled': False, + 'isChargingTargetSocEnabled': False, + 'isClimateTimerSupported': True, + 'isCustomerEsimSupported': False, + 'isDCSContractManagementSupported': True, + 'isDataPrivacyEnabled': False, + 'isEasyChargeEnabled': False, + 'isEvGoChargingSupported': False, + 'isMiniChargingSupported': False, + 'isNonLscFeatureEnabled': False, + 'isRemoteEngineStartSupported': False, + 'isRemoteHistoryDeletionSupported': False, + 'isRemoteHistorySupported': True, + 'isRemoteParkingSupported': False, + 'isRemoteServicesActivationRequired': False, + 'isRemoteServicesBookingRequired': False, + 'isScanAndChargeSupported': False, + 'isSustainabilitySupported': False, + 'isWifiHotspotServiceSupported': False, + 'lastStateCallState': 'ACTIVATED', + 'lights': True, + 'lock': True, + 'remoteChargingCommands': dict({ }), - 'mappingInfo': dict({ - 'isAssociated': False, - 'isLmmEnabled': False, - 'isPrimaryUser': True, - 'lmmStatusReasons': list([ + 'sendPoi': True, + 'specialThemeSupport': list([ + ]), + 'unlock': True, + 'vehicleFinder': False, + 'vehicleStateSource': 'LAST_STATE_CALL', + }), + 'state': dict({ + 'chargingProfile': dict({ + 'chargingControlType': 'WEEKLY_PLANNER', + 'chargingMode': 'DELAYED_CHARGING', + 'chargingPreference': 'CHARGING_WINDOW', + 'chargingSettings': dict({ + 'hospitality': 'NO_ACTION', + 'idcc': 'NO_ACTION', + 'targetSoc': 100, + }), + 'climatisationOn': False, + 'departureTimes': list([ + dict({ + 'action': 'DEACTIVATE', + 'id': 1, + 'timeStamp': dict({ + 'hour': 7, + 'minute': 35, + }), + 'timerWeekDays': list([ + 'MONDAY', + 'TUESDAY', + 'WEDNESDAY', + 'THURSDAY', + 'FRIDAY', + ]), + }), + dict({ + 'action': 'DEACTIVATE', + 'id': 2, + 'timeStamp': dict({ + 'hour': 18, + 'minute': 0, + }), + 'timerWeekDays': list([ + 'MONDAY', + 'TUESDAY', + 'WEDNESDAY', + 'THURSDAY', + 'FRIDAY', + 'SATURDAY', + 'SUNDAY', + ]), + }), + dict({ + 'action': 'DEACTIVATE', + 'id': 3, + 'timeStamp': dict({ + 'hour': 7, + 'minute': 0, + }), + 'timerWeekDays': list([ + ]), + }), + dict({ + 'action': 'DEACTIVATE', + 'id': 4, + 'timerWeekDays': list([ + ]), + }), ]), - 'mappingStatus': 'CONFIRMED', + 'reductionOfChargeCurrent': dict({ + 'end': dict({ + 'hour': 1, + 'minute': 30, + }), + 'start': dict({ + 'hour': 18, + 'minute': 1, + }), + }), + }), + 'checkControlMessages': list([ + ]), + 'climateTimers': list([ + dict({ + 'departureTime': dict({ + 'hour': 6, + 'minute': 40, + }), + 'isWeeklyTimer': True, + 'timerAction': 'ACTIVATE', + 'timerWeekDays': list([ + 'THURSDAY', + 'SUNDAY', + ]), + }), + dict({ + 'departureTime': dict({ + 'hour': 12, + 'minute': 50, + }), + 'isWeeklyTimer': False, + 'timerAction': 'ACTIVATE', + 'timerWeekDays': list([ + 'MONDAY', + ]), + }), + dict({ + 'departureTime': dict({ + 'hour': 18, + 'minute': 59, + }), + 'isWeeklyTimer': True, + 'timerAction': 'DEACTIVATE', + 'timerWeekDays': list([ + 'WEDNESDAY', + ]), + }), + ]), + 'combustionFuelLevel': dict({ + 'range': 105, + 'remainingFuelLiters': 6, + }), + 'currentMileage': 137009, + 'doorsState': dict({ + 'combinedSecurityState': 'UNLOCKED', + 'combinedState': 'CLOSED', + 'hood': 'CLOSED', + 'leftFront': 'CLOSED', + 'leftRear': 'CLOSED', + 'rightFront': 'CLOSED', + 'rightRear': 'CLOSED', + 'trunk': 'CLOSED', + }), + 'driverPreferences': dict({ + 'lscPrivacyMode': 'OFF', + }), + 'electricChargingState': dict({ + 'chargingConnectionType': 'CONDUCTIVE', + 'chargingLevelPercent': 82, + 'chargingStatus': 'WAITING_FOR_CHARGING', + 'chargingTarget': 100, + 'isChargerConnected': True, + 'range': 174, + }), + 'isLeftSteering': True, + 'isLscSupported': True, + 'lastFetched': '2022-06-22T14:24:23.982Z', + 'lastUpdatedAt': '2022-06-22T13:58:52Z', + 'range': 174, + 'requiredServices': list([ + dict({ + 'dateTime': '2022-10-01T00:00:00.000Z', + 'description': 'Next service due by the specified date.', + 'status': 'OK', + 'type': 'BRAKE_FLUID', + }), + dict({ + 'dateTime': '2023-05-01T00:00:00.000Z', + 'description': 'Next vehicle check due after the specified distance or date.', + 'status': 'OK', + 'type': 'VEHICLE_CHECK', + }), + dict({ + 'dateTime': '2023-05-01T00:00:00.000Z', + 'description': 'Next state inspection due by the specified date.', + 'status': 'OK', + 'type': 'VEHICLE_TUV', + }), + ]), + 'roofState': dict({ + 'roofState': 'CLOSED', + 'roofStateType': 'SUN_ROOF', + }), + 'windowsState': dict({ + 'combinedState': 'CLOSED', + 'leftFront': 'CLOSED', + 'rightFront': 'CLOSED', }), - 'vin': '**REDACTED**', }), - dict({ - 'appVehicleType': 'DEMO', - 'attributes': dict({ - 'a4aType': 'NOT_SUPPORTED', - 'bodyType': 'G26', - 'brand': 'BMW', - 'color': 4284245350, - 'countryOfOrigin': 'DE', - 'driveTrain': 'ELECTRIC', - 'driverGuideInfo': dict({ - 'androidAppScheme': 'com.bmwgroup.driversguide.row', - 'androidStoreUrl': 'https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row', - 'iosAppScheme': 'bmwdriversguide:///open', - 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', - }), - 'headUnitRaw': 'HU_MGU', - 'headUnitType': 'MGU', - 'hmiVersion': 'ID8', - 'lastFetched': '2023-01-04T14:57:06.019Z', - 'model': 'i4 eDrive40', - 'softwareVersionCurrent': dict({ - 'iStep': 470, - 'puStep': dict({ - 'month': 11, - 'year': 21, - }), - 'seriesCluster': 'G026', - }), - 'softwareVersionExFactory': dict({ - 'iStep': 470, - 'puStep': dict({ - 'month': 11, - 'year': 21, - }), - 'seriesCluster': 'G026', - }), - 'telematicsUnit': 'WAVE01', - 'year': 2021, - }), - 'mappingInfo': dict({ - 'isAssociated': False, - 'isLmmEnabled': False, - 'isPrimaryUser': True, - 'lmmStatusReasons': list([ - ]), - 'mappingStatus': 'CONFIRMED', - }), - 'vin': '**REDACTED**', - }), - dict({ - 'appVehicleType': 'DEMO', - 'attributes': dict({ - 'a4aType': 'NOT_SUPPORTED', - 'bodyType': 'G20', - 'brand': 'BMW', - 'color': 4280233344, - 'countryOfOrigin': 'PT', - 'driveTrain': 'COMBUSTION', - 'driverGuideInfo': dict({ - 'androidAppScheme': 'com.bmwgroup.driversguide.row', - 'androidStoreUrl': 'https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row', - 'iosAppScheme': 'bmwdriversguide:///open', - 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', - }), - 'headUnitRaw': 'HU_MGU', - 'headUnitType': 'MGU', - 'hmiVersion': 'ID7', - 'lastFetched': '2023-01-04T14:57:06.019Z', - 'model': 'M340i xDrive', - 'softwareVersionCurrent': dict({ - 'iStep': 470, - 'puStep': dict({ - 'month': 7, - 'year': 21, - }), - 'seriesCluster': 'S18A', - }), - 'softwareVersionExFactory': dict({ - 'iStep': 420, - 'puStep': dict({ - 'month': 7, - 'year': 20, - }), - 'seriesCluster': 'S18A', - }), - 'telematicsUnit': 'ATM2', - 'year': 2022, - }), - 'mappingInfo': dict({ - 'isAssociated': False, - 'isLmmEnabled': False, - 'isPrimaryUser': True, - 'lmmStatusReasons': list([ - ]), - 'mappingStatus': 'CONFIRMED', - }), - 'vin': '**REDACTED**', - }), - dict({ - 'appVehicleType': 'CONNECTED', - 'attributes': dict({ - 'a4aType': 'USB_ONLY', - 'bodyType': 'I01', - 'brand': 'BMW_I', - 'color': 4284110934, - 'countryOfOrigin': 'CZ', - 'driveTrain': 'ELECTRIC_WITH_RANGE_EXTENDER', - 'driverGuideInfo': dict({ - 'androidAppScheme': 'com.bmwgroup.driversguide.row', - 'androidStoreUrl': 'https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row', - 'iosAppScheme': 'bmwdriversguide:///open', - 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', - }), - 'headUnitType': 'NBT', - 'hmiVersion': 'ID4', - 'lastFetched': '2022-06-01T19:48:46.540Z', - 'model': 'i3 (+ REX)', - 'softwareVersionCurrent': dict({ - 'iStep': 510, - 'puStep': dict({ - 'month': 11, - 'year': 21, - }), - 'seriesCluster': 'I001', - }), - 'softwareVersionExFactory': dict({ - 'iStep': 502, - 'puStep': dict({ - 'month': 3, - 'year': 15, - }), - 'seriesCluster': 'I001', - }), - 'year': 2015, - }), - 'mappingInfo': dict({ - 'isPrimaryUser': True, - 'mappingStatus': 'CONFIRMED', - }), - 'vin': '**REDACTED**', - }), - ]), - 'filename': 'bmw-eadrax-vcs_v4_vehicles.json', + }), + 'filename': 'bmw-eadrax-vcs_v4_vehicles_state_WBY0FINGERPRINT04.json', }), dict({ - 'content': list([ - ]), - 'filename': 'mini-eadrax-vcs_v4_vehicles.json', + 'content': dict({ + 'chargeAndClimateSettings': dict({ + 'chargeAndClimateTimer': dict({ + 'showDepartureTimers': False, + }), + }), + 'chargeAndClimateTimerDetail': dict({ + 'chargingMode': dict({ + 'chargingPreference': 'CHARGING_WINDOW', + 'endTimeSlot': '0001-01-01T01:30:00', + 'startTimeSlot': '0001-01-01T18:01:00', + 'type': 'TIME_SLOT', + }), + 'departureTimer': dict({ + 'type': 'WEEKLY_DEPARTURE_TIMER', + 'weeklyTimers': list([ + dict({ + 'daysOfTheWeek': list([ + 'MONDAY', + 'TUESDAY', + 'WEDNESDAY', + 'THURSDAY', + 'FRIDAY', + ]), + 'id': 1, + 'time': '0001-01-01T07:35:00', + 'timerAction': 'DEACTIVATE', + }), + dict({ + 'daysOfTheWeek': list([ + 'MONDAY', + 'TUESDAY', + 'WEDNESDAY', + 'THURSDAY', + 'FRIDAY', + 'SATURDAY', + 'SUNDAY', + ]), + 'id': 2, + 'time': '0001-01-01T18:00:00', + 'timerAction': 'DEACTIVATE', + }), + dict({ + 'daysOfTheWeek': list([ + ]), + 'id': 3, + 'time': '0001-01-01T07:00:00', + 'timerAction': 'DEACTIVATE', + }), + dict({ + 'daysOfTheWeek': list([ + ]), + 'id': 4, + 'time': '0001-01-01T00:00:00', + 'timerAction': 'DEACTIVATE', + }), + ]), + }), + 'isPreconditionForDepartureActive': False, + }), + 'servicePack': 'TCB1', + }), + 'filename': 'bmw-eadrax-crccs_v2_vehicles_WBY0FINGERPRINT04.json', + }), + dict({ + 'content': dict({ + 'gcid': 'ceb64158-d2ca-47e9-9ee6-cbffb881434e', + 'mappingInfos': list([ + ]), + }), + 'filename': 'mini-eadrax-vcs_v5_vehicle-list.json', }), dict({ 'content': dict({ From 3351b826672f228257f232dd3122e2064db9f369 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 30 Apr 2024 15:07:15 +0200 Subject: [PATCH 943/967] Fix zoneminder async v2 (#116451) --- homeassistant/components/zoneminder/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zoneminder/__init__.py b/homeassistant/components/zoneminder/__init__.py index b4a406cec4e..e87a2b1531d 100644 --- a/homeassistant/components/zoneminder/__init__.py +++ b/homeassistant/components/zoneminder/__init__.py @@ -78,7 +78,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.data[DOMAIN][host_name] = zm_client try: - success = zm_client.login() and success + success = await hass.async_add_executor_job(zm_client.login) and success except RequestsConnectionError as ex: _LOGGER.error( "ZoneMinder connection failure to %s: %s", From c77cef039107d67e44a1dfb2f19d6befdd7ff759 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 30 Apr 2024 11:53:55 -0500 Subject: [PATCH 944/967] Bump bluetooth-adapters to 0.19.1 (#116465) --- 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 ed1e11d8ddd..4bb84ab6dc3 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -16,7 +16,7 @@ "requirements": [ "bleak==0.21.1", "bleak-retry-connector==3.5.0", - "bluetooth-adapters==0.19.0", + "bluetooth-adapters==0.19.1", "bluetooth-auto-recovery==1.4.2", "bluetooth-data-tools==1.19.0", "dbus-fast==2.21.1", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a2eb0f1254c..4ba38346e83 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.21.1 -bluetooth-adapters==0.19.0 +bluetooth-adapters==0.19.1 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 04097f0a9bc..8d926776063 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -579,7 +579,7 @@ bluemaestro-ble==0.2.3 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.19.0 +bluetooth-adapters==0.19.1 # homeassistant.components.bluetooth bluetooth-auto-recovery==1.4.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bbc60f01547..ab140526378 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -494,7 +494,7 @@ bluecurrent-api==1.2.3 bluemaestro-ble==0.2.3 # homeassistant.components.bluetooth -bluetooth-adapters==0.19.0 +bluetooth-adapters==0.19.1 # homeassistant.components.bluetooth bluetooth-auto-recovery==1.4.2 From 3d13345575d490565001e72e5d8dcb513fd34cd8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 30 Apr 2024 15:31:40 -0500 Subject: [PATCH 945/967] Ensure MQTT resubscribes happen before birth message (#116471) --- homeassistant/components/mqtt/client.py | 56 +++++++++++++++---------- 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 7f58a21a1f1..74fa8fb3302 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -878,6 +878,22 @@ class MQTT: await self._wait_for_mid(mid) + async def _async_resubscribe_and_publish_birth_message( + self, birth_message: PublishMessage + ) -> None: + """Resubscribe to all topics and publish birth message.""" + await self._async_perform_subscriptions() + 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 + self._subscribe_debouncer.set_timeout(SUBSCRIBE_COOLDOWN) + await self.async_publish( + topic=birth_message.topic, + payload=birth_message.payload, + qos=birth_message.qos, + retain=birth_message.retain, + ) + @callback def _async_mqtt_on_connect( self, @@ -919,36 +935,33 @@ class MQTT: result_code, ) - self.hass.async_create_task(self._async_resubscribe()) - + self._async_queue_resubscribe() + birth: dict[str, Any] if birth := self.conf.get(CONF_BIRTH_MESSAGE, DEFAULT_BIRTH): - - async def publish_birth_message(birth_message: PublishMessage) -> None: - await self._ha_started.wait() # Wait for Home Assistant to start - await self._discovery_cooldown() # Wait for MQTT discovery to cool down - # Update subscribe cooldown period to a shorter time - self._subscribe_debouncer.set_timeout(SUBSCRIBE_COOLDOWN) - await self.async_publish( - topic=birth_message.topic, - payload=birth_message.payload, - qos=birth_message.qos, - retain=birth_message.retain, - ) - birth_message = PublishMessage(**birth) self.config_entry.async_create_background_task( self.hass, - publish_birth_message(birth_message), - name="mqtt birth message", + self._async_resubscribe_and_publish_birth_message(birth_message), + name="mqtt re-subscribe and birth", ) 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) self._async_connection_result(True) - async def _async_resubscribe(self) -> None: - """Resubscribe on reconnect.""" + @callback + def _async_queue_resubscribe(self) -> None: + """Queue subscriptions on reconnect. + + self._async_perform_subscriptions must be called + after this method to actually subscribe. + """ self._max_qos.clear() self._retained_topics.clear() # Group subscriptions to only re-subscribe once for each topic. @@ -963,7 +976,6 @@ class MQTT: ], queue_only=True, ) - await self._async_perform_subscriptions() @lru_cache(None) # pylint: disable=method-cache-max-size-none def _matching_subscriptions(self, topic: str) -> list[Subscription]: @@ -1052,7 +1064,9 @@ class MQTT: # The callback signature for on_unsubscribe is different from on_subscribe # see https://github.com/eclipse/paho.mqtt.python/issues/687 # properties and reasoncodes are not used in Home Assistant - self.hass.async_create_task(self._mqtt_handle_mid(mid)) + self.config_entry.async_create_task( + self.hass, self._mqtt_handle_mid(mid), name=f"mqtt handle mid {mid}" + ) async def _mqtt_handle_mid(self, mid: int) -> None: # Create the mid event if not created, either _mqtt_handle_mid or _wait_for_mid From c574d86ddbafd6c18995ad9efb297fda3ce4292c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 30 Apr 2024 15:31:09 -0500 Subject: [PATCH 946/967] Fix local_todo blocking the event loop (#116473) --- homeassistant/components/local_todo/todo.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/local_todo/todo.py b/homeassistant/components/local_todo/todo.py index 5b25abf8e21..ccd3d8db759 100644 --- a/homeassistant/components/local_todo/todo.py +++ b/homeassistant/components/local_todo/todo.py @@ -18,6 +18,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.setup import SetupPhases, async_pause_setup from homeassistant.util import dt as dt_util from .const import CONF_TODO_LIST_NAME, DOMAIN @@ -67,9 +68,16 @@ async def async_setup_entry( ) -> None: """Set up the local_todo todo platform.""" - store = hass.data[DOMAIN][config_entry.entry_id] + store: LocalTodoListStore = hass.data[DOMAIN][config_entry.entry_id] ics = await store.async_load() - calendar = IcsCalendarStream.calendar_from_ics(ics) + + with async_pause_setup(hass, SetupPhases.WAIT_IMPORT_PACKAGES): + # calendar_from_ics will dynamically load packages + # the first time it is called, so we need to do it + # in a separate thread to avoid blocking the event loop + calendar: Calendar = await hass.async_add_import_executor_job( + IcsCalendarStream.calendar_from_ics, ics + ) migrated = _migrate_calendar(calendar) calendar.prodid = PRODID From c54d53b88a1dd5f4dea719924d9e395bb6b51060 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 30 Apr 2024 16:47:17 -0400 Subject: [PATCH 947/967] Change SkyConnect integration type back to `hardware` and fix multi-PAN migration bug (#116474) Co-authored-by: Joost Lekkerkerker --- .../homeassistant_sky_connect/config_flow.py | 15 ++++++++ .../homeassistant_sky_connect/manifest.json | 2 +- homeassistant/generated/integrations.json | 5 --- .../test_config_flow.py | 38 +++++++++++++++++++ 4 files changed, 54 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homeassistant_sky_connect/config_flow.py b/homeassistant/components/homeassistant_sky_connect/config_flow.py index 6ffb2783165..9d0aa902cc4 100644 --- a/homeassistant/components/homeassistant_sky_connect/config_flow.py +++ b/homeassistant/components/homeassistant_sky_connect/config_flow.py @@ -597,6 +597,21 @@ class HomeAssistantSkyConnectMultiPanOptionsFlowHandler( """Return the name of the hardware.""" return self._hw_variant.full_name + async def async_step_flashing_complete( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Finish flashing and update the config entry.""" + self.hass.config_entries.async_update_entry( + entry=self.config_entry, + data={ + **self.config_entry.data, + "firmware": ApplicationType.EZSP.value, + }, + options=self.config_entry.options, + ) + + return await super().async_step_flashing_complete(user_input) + class HomeAssistantSkyConnectOptionsFlowHandler( BaseFirmwareInstallFlow, OptionsFlowWithConfigEntry diff --git a/homeassistant/components/homeassistant_sky_connect/manifest.json b/homeassistant/components/homeassistant_sky_connect/manifest.json index c90ea2c075f..f56fd24de61 100644 --- a/homeassistant/components/homeassistant_sky_connect/manifest.json +++ b/homeassistant/components/homeassistant_sky_connect/manifest.json @@ -5,7 +5,7 @@ "config_flow": true, "dependencies": ["hardware", "usb", "homeassistant_hardware"], "documentation": "https://www.home-assistant.io/integrations/homeassistant_sky_connect", - "integration_type": "device", + "integration_type": "hardware", "usb": [ { "vid": "10C4", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index cf5f352f22c..e6a103989d1 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -2565,11 +2565,6 @@ "integration_type": "virtual", "supported_by": "netatmo" }, - "homeassistant_sky_connect": { - "name": "Home Assistant SkyConnect", - "integration_type": "device", - "config_flow": true - }, "homematic": { "name": "Homematic", "integrations": { diff --git a/tests/components/homeassistant_sky_connect/test_config_flow.py b/tests/components/homeassistant_sky_connect/test_config_flow.py index c34e3ebe186..611dda4a917 100644 --- a/tests/components/homeassistant_sky_connect/test_config_flow.py +++ b/tests/components/homeassistant_sky_connect/test_config_flow.py @@ -11,6 +11,8 @@ from universal_silabs_flasher.const import ApplicationType from homeassistant.components import usb from homeassistant.components.hassio.addon_manager import AddonInfo, AddonState from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import ( + CONF_DISABLE_MULTI_PAN, + get_flasher_addon_manager, get_multiprotocol_addon_manager, ) from homeassistant.components.homeassistant_sky_connect.config_flow import ( @@ -869,11 +871,25 @@ async def test_options_flow_multipan_uninstall( version="1.0.0", ) + mock_flasher_manager = Mock(spec_set=get_flasher_addon_manager(hass)) + mock_flasher_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={}, + state=AddonState.NOT_RUNNING, + update_available=False, + version="1.0.0", + ) + with ( patch( "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.get_multiprotocol_addon_manager", return_value=mock_multipan_manager, ), + patch( + "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.get_flasher_addon_manager", + return_value=mock_flasher_manager, + ), patch( "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", return_value=True, @@ -883,3 +899,25 @@ async def test_options_flow_multipan_uninstall( assert result["type"] is FlowResultType.MENU assert result["step_id"] == "addon_menu" assert "uninstall_addon" in result["menu_options"] + + # Pick the uninstall option + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"next_step_id": "uninstall_addon"}, + ) + + # Check the box + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={CONF_DISABLE_MULTI_PAN: True} + ) + + # Finish the flow + result = await hass.config_entries.options.async_configure(result["flow_id"]) + await hass.async_block_till_done(wait_background_tasks=True) + result = await hass.config_entries.options.async_configure(result["flow_id"]) + await hass.async_block_till_done(wait_background_tasks=True) + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.CREATE_ENTRY + + # We've reverted the firmware back to Zigbee + assert config_entry.data["firmware"] == "ezsp" From 6971898a43deaefa94c0ad3e46864be90aaae819 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 30 Apr 2024 15:47:27 -0500 Subject: [PATCH 948/967] Fix non-thread-safe operation in roon volume callback (#116475) --- homeassistant/components/roon/event.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/roon/event.py b/homeassistant/components/roon/event.py index ea5014c8755..073b58160f6 100644 --- a/homeassistant/components/roon/event.py +++ b/homeassistant/components/roon/event.py @@ -72,7 +72,6 @@ class RoonEventEntity(EventEntity): via_device=(DOMAIN, self._server.roon_id), ) - @callback def _roonapi_volume_callback( self, control_key: str, event: str, value: int ) -> None: @@ -88,7 +87,7 @@ class RoonEventEntity(EventEntity): event = "volume_down" self._trigger_event(event) - self.async_write_ha_state() + self.schedule_update_ha_state() async def async_added_to_hass(self) -> None: """Register volume hooks with the roon api.""" From 3d86577cabc2a6e645055e7ecc3d09db258317e0 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 1 May 2024 01:15:46 +0200 Subject: [PATCH 949/967] Add test MQTT subscription is completed when birth message is sent (#116476) --- tests/components/mqtt/test_init.py | 69 ++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index cfb8ce7ac04..f948889fd80 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -2534,6 +2534,75 @@ async def test_delayed_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, + }, + } + ], +) +async def test_subscription_done_when_birth_message_is_sent( + hass: HomeAssistant, + mqtt_client_mock: MqttMockPahoClient, + 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() + + 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) + 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() + + 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) + # 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 + assert ("homeassistant/+/+/config", 0) in help_all_subscribe_calls( + mqtt_client_mock + ) + assert ("homeassistant/+/+/+/config", 0) in help_all_subscribe_calls( + mqtt_client_mock + ) + mqtt_client_mock.publish.assert_called_with( + "homeassistant/status", "online", 0, False + ) + + @pytest.mark.parametrize( "mqtt_config_entry_data", [ From ac241057772073ed51f7816e064b74aef45326c1 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 1 May 2024 00:46:25 +0200 Subject: [PATCH 950/967] Update frontend to 20240430.0 (#116481) --- 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 e271903a27d..aa1d8ee3d3c 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==20240429.0"] + "requirements": ["home-assistant-frontend==20240430.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 4ba38346e83..afb7d894a51 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ habluetooth==2.8.0 hass-nabucasa==0.78.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240429.0 +home-assistant-frontend==20240430.0 home-assistant-intents==2024.4.24 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 8d926776063..6e2b7a55a13 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1078,7 +1078,7 @@ hole==0.8.0 holidays==0.47 # homeassistant.components.frontend -home-assistant-frontend==20240429.0 +home-assistant-frontend==20240430.0 # homeassistant.components.conversation home-assistant-intents==2024.4.24 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ab140526378..2d7fe7158bf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -880,7 +880,7 @@ hole==0.8.0 holidays==0.47 # homeassistant.components.frontend -home-assistant-frontend==20240429.0 +home-assistant-frontend==20240430.0 # homeassistant.components.conversation home-assistant-intents==2024.4.24 From 7d51556e1ea06dba2892f36541666220d79dc3ca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 30 Apr 2024 18:47:12 -0500 Subject: [PATCH 951/967] Hold a lock to prevent concurrent setup of config entries (#116482) --- homeassistant/config_entries.py | 30 +++-- homeassistant/setup.py | 2 +- .../androidtv_remote/test_config_flow.py | 3 + .../components/config/test_config_entries.py | 5 + tests/components/mqtt/test_init.py | 1 + tests/components/opower/test_config_flow.py | 2 + tests/test_config_entries.py | 104 +++++++++++++++++- 7 files changed, 129 insertions(+), 18 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 619b2a4b48a..f982f63b948 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -292,7 +292,7 @@ class ConfigEntry: update_listeners: list[UpdateListenerType] _async_cancel_retry_setup: Callable[[], Any] | None _on_unload: list[Callable[[], Coroutine[Any, Any, None] | None]] | None - reload_lock: asyncio.Lock + setup_lock: asyncio.Lock _reauth_lock: asyncio.Lock _reconfigure_lock: asyncio.Lock _tasks: set[asyncio.Future[Any]] @@ -400,7 +400,7 @@ class ConfigEntry: _setter(self, "_on_unload", None) # Reload lock to prevent conflicting reloads - _setter(self, "reload_lock", asyncio.Lock()) + _setter(self, "setup_lock", asyncio.Lock()) # Reauth lock to prevent concurrent reauth flows _setter(self, "_reauth_lock", asyncio.Lock()) # Reconfigure lock to prevent concurrent reconfigure flows @@ -699,19 +699,17 @@ class ConfigEntry: # has started so we do not block shutdown if not hass.is_stopping: hass.async_create_background_task( - self._async_setup_retry(hass), + self.async_setup_locked(hass), f"config entry retry {self.domain} {self.title}", eager_start=True, ) - async def _async_setup_retry(self, hass: HomeAssistant) -> None: - """Retry setup. - - We hold the reload lock during setup retry to ensure - that nothing can reload the entry while we are retrying. - """ - async with self.reload_lock: - await self.async_setup(hass) + async def async_setup_locked( + self, hass: HomeAssistant, integration: loader.Integration | None = None + ) -> None: + """Set up while holding the setup lock.""" + async with self.setup_lock: + await self.async_setup(hass, integration=integration) @callback def async_shutdown(self) -> None: @@ -1791,7 +1789,15 @@ class ConfigEntries: # attempts. entry.async_cancel_retry_setup() - async with entry.reload_lock: + if entry.domain not in self.hass.config.components: + # If the component is not loaded, just load it as + # the config entry will be loaded as well. We need + # to do this before holding the lock to avoid a + # deadlock. + await async_setup_component(self.hass, entry.domain, self._hass_config) + return entry.state is ConfigEntryState.LOADED + + async with entry.setup_lock: unload_result = await self.async_unload(entry_id) if not unload_result or entry.disabled_by: diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 7ba51b644e5..86df6417169 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -449,7 +449,7 @@ async def _async_setup_component( await asyncio.gather( *( create_eager_task( - entry.async_setup(hass, integration=integration), + entry.async_setup_locked(hass, integration=integration), name=f"config entry setup {entry.title} {entry.domain} {entry.entry_id}", ) for entry in entries diff --git a/tests/components/androidtv_remote/test_config_flow.py b/tests/components/androidtv_remote/test_config_flow.py index 8778630be8d..062b9a4a55c 100644 --- a/tests/components/androidtv_remote/test_config_flow.py +++ b/tests/components/androidtv_remote/test_config_flow.py @@ -324,6 +324,7 @@ async def test_user_flow_already_configured_host_changed_reloads_entry( state=ConfigEntryState.LOADED, ) mock_config_entry.add_to_hass(hass) + hass.config.components.add(DOMAIN) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -640,6 +641,7 @@ async def test_zeroconf_flow_already_configured_host_changed_reloads_entry( state=ConfigEntryState.LOADED, ) mock_config_entry.add_to_hass(hass) + hass.config.components.add(DOMAIN) result = await hass.config_entries.flow.async_init( DOMAIN, @@ -769,6 +771,7 @@ async def test_reauth_flow_success( state=ConfigEntryState.LOADED, ) mock_config_entry.add_to_hass(hass) + hass.config.components.add(DOMAIN) mock_config_entry.async_start_reauth(hass) await hass.async_block_till_done() diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index dd46921c339..87c712b3716 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -251,6 +251,7 @@ async def test_reload_entry(hass: HomeAssistant, client) -> None: domain="kitchen_sink", state=core_ce.ConfigEntryState.LOADED ) entry.add_to_hass(hass) + hass.config.components.add("kitchen_sink") resp = await client.post( f"/api/config/config_entries/entry/{entry.entry_id}/reload" ) @@ -298,6 +299,7 @@ async def test_reload_entry_in_failed_state( """Test reloading an entry via the API that has already failed to unload.""" entry = MockConfigEntry(domain="demo", state=core_ce.ConfigEntryState.FAILED_UNLOAD) entry.add_to_hass(hass) + hass.config.components.add("demo") resp = await client.post( f"/api/config/config_entries/entry/{entry.entry_id}/reload" ) @@ -326,6 +328,7 @@ async def test_reload_entry_in_setup_retry( entry = MockConfigEntry(domain="comp", state=core_ce.ConfigEntryState.SETUP_RETRY) entry.supports_unload = True entry.add_to_hass(hass) + hass.config.components.add("comp") with patch.dict(HANDLERS, {"comp": ConfigFlow, "test": ConfigFlow}): resp = await client.post( @@ -1109,6 +1112,7 @@ async def test_update_prefrences( domain="kitchen_sink", state=core_ce.ConfigEntryState.LOADED ) entry.add_to_hass(hass) + hass.config.components.add("kitchen_sink") assert entry.pref_disable_new_entities is False assert entry.pref_disable_polling is False @@ -1209,6 +1213,7 @@ async def test_disable_entry( ) entry.add_to_hass(hass) assert entry.disabled_by is None + hass.config.components.add("kitchen_sink") # Disable await ws_client.send_json( diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index f948889fd80..a1264b52739 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1873,6 +1873,7 @@ async def test_reload_entry_with_restored_subscriptions( # Setup the MQTT entry 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 entry.async_setup(hass) diff --git a/tests/components/opower/test_config_flow.py b/tests/components/opower/test_config_flow.py index 512a602a043..18a7caf23df 100644 --- a/tests/components/opower/test_config_flow.py +++ b/tests/components/opower/test_config_flow.py @@ -279,6 +279,7 @@ async def test_form_valid_reauth( ) -> None: """Test that we can handle a valid reauth.""" mock_config_entry.mock_state(hass, ConfigEntryState.LOADED) + hass.config.components.add(DOMAIN) mock_config_entry.async_start_reauth(hass) await hass.async_block_till_done() @@ -328,6 +329,7 @@ async def test_form_valid_reauth_with_mfa( }, ) mock_config_entry.mock_state(hass, ConfigEntryState.LOADED) + hass.config.components.add(DOMAIN) mock_config_entry.async_start_reauth(hass) await hass.async_block_till_done() diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 68f770631ed..8d7efad8918 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -825,7 +825,7 @@ async def test_as_dict(snapshot: SnapshotAssertion) -> None: "error_reason_translation_placeholders", "_async_cancel_retry_setup", "_on_unload", - "reload_lock", + "setup_lock", "_reauth_lock", "_tasks", "_background_tasks", @@ -1632,7 +1632,6 @@ async def test_entry_reload_succeed( mock_platform(hass, "comp.config_flow", None) assert await manager.async_reload(entry.entry_id) - assert len(async_unload_entry.mock_calls) == 1 assert len(async_setup.mock_calls) == 1 assert len(async_setup_entry.mock_calls) == 1 assert entry.state is config_entries.ConfigEntryState.LOADED @@ -1707,6 +1706,8 @@ async def test_entry_reload_error( ), ) + hass.config.components.add("comp") + with pytest.raises(config_entries.OperationNotAllowed, match=str(state)): assert await manager.async_reload(entry.entry_id) @@ -1738,8 +1739,11 @@ async def test_entry_disable_succeed( ), ) mock_platform(hass, "comp.config_flow", None) + hass.config.components.add("comp") # Disable + assert len(async_setup.mock_calls) == 0 + assert len(async_setup_entry.mock_calls) == 0 assert await manager.async_set_disabled_by( entry.entry_id, config_entries.ConfigEntryDisabler.USER ) @@ -1751,7 +1755,7 @@ async def test_entry_disable_succeed( # Enable assert await manager.async_set_disabled_by(entry.entry_id, None) assert len(async_unload_entry.mock_calls) == 1 - assert len(async_setup.mock_calls) == 1 + assert len(async_setup.mock_calls) == 0 assert len(async_setup_entry.mock_calls) == 1 assert entry.state is config_entries.ConfigEntryState.LOADED @@ -1775,6 +1779,7 @@ async def test_entry_disable_without_reload_support( ), ) mock_platform(hass, "comp.config_flow", None) + hass.config.components.add("comp") # Disable assert not await manager.async_set_disabled_by( @@ -1951,7 +1956,7 @@ async def test_reload_entry_entity_registry_works( ) await hass.async_block_till_done() - assert len(mock_unload_entry.mock_calls) == 2 + assert len(mock_unload_entry.mock_calls) == 1 async def test_unique_id_persisted( @@ -3392,6 +3397,7 @@ async def test_entry_reload_calls_on_unload_listeners( ), ) mock_platform(hass, "comp.config_flow", None) + hass.config.components.add("comp") mock_unload_callback = Mock() @@ -3944,8 +3950,9 @@ async def test_deprecated_disabled_by_str_set( caplog: pytest.LogCaptureFixture, ) -> None: """Test deprecated str set disabled_by enumizes and logs a warning.""" - entry = MockConfigEntry() + entry = MockConfigEntry(domain="comp") entry.add_to_manager(manager) + hass.config.components.add("comp") assert await manager.async_set_disabled_by( entry.entry_id, config_entries.ConfigEntryDisabler.USER.value ) @@ -3963,6 +3970,47 @@ async def test_entry_reload_concurrency( async_setup = AsyncMock(return_value=True) loaded = 1 + async def _async_setup_entry(*args, **kwargs): + await asyncio.sleep(0) + nonlocal loaded + loaded += 1 + return loaded == 1 + + async def _async_unload_entry(*args, **kwargs): + await asyncio.sleep(0) + nonlocal loaded + loaded -= 1 + return loaded == 0 + + mock_integration( + hass, + MockModule( + "comp", + async_setup=async_setup, + async_setup_entry=_async_setup_entry, + async_unload_entry=_async_unload_entry, + ), + ) + mock_platform(hass, "comp.config_flow", None) + hass.config.components.add("comp") + tasks = [ + asyncio.create_task(manager.async_reload(entry.entry_id)) for _ in range(15) + ] + await asyncio.gather(*tasks) + assert entry.state is config_entries.ConfigEntryState.LOADED + assert loaded == 1 + + +async def test_entry_reload_concurrency_not_setup_setup( + hass: HomeAssistant, manager: config_entries.ConfigEntries +) -> None: + """Test multiple reload calls do not cause a reload race.""" + entry = MockConfigEntry(domain="comp", state=config_entries.ConfigEntryState.LOADED) + entry.add_to_hass(hass) + + async_setup = AsyncMock(return_value=True) + loaded = 0 + async def _async_setup_entry(*args, **kwargs): await asyncio.sleep(0) nonlocal loaded @@ -4074,6 +4122,7 @@ async def test_disallow_entry_reload_with_setup_in_progress( domain="comp", state=config_entries.ConfigEntryState.SETUP_IN_PROGRESS ) entry.add_to_hass(hass) + hass.config.components.add("comp") with pytest.raises( config_entries.OperationNotAllowed, @@ -5016,3 +5065,48 @@ async def test_updating_non_added_entry_raises(hass: HomeAssistant) -> None: with pytest.raises(config_entries.UnknownEntry, match=entry.entry_id): hass.config_entries.async_update_entry(entry, unique_id="new_id") + + +async def test_reload_during_setup(hass: HomeAssistant) -> None: + """Test reload during setup waits.""" + entry = MockConfigEntry(domain="comp", data={"value": "initial"}) + entry.add_to_hass(hass) + + setup_start_future = hass.loop.create_future() + setup_finish_future = hass.loop.create_future() + in_setup = False + setup_calls = 0 + + async def mock_async_setup_entry(hass, entry): + """Mock setting up an entry.""" + nonlocal in_setup + nonlocal setup_calls + setup_calls += 1 + assert not in_setup + in_setup = True + setup_start_future.set_result(None) + await setup_finish_future + in_setup = False + return True + + mock_integration( + hass, + MockModule( + "comp", + async_setup_entry=mock_async_setup_entry, + async_unload_entry=AsyncMock(return_value=True), + ), + ) + mock_platform(hass, "comp.config_flow", None) + + setup_task = hass.async_create_task(async_setup_component(hass, "comp", {})) + + await setup_start_future # ensure we are in the setup + reload_task = hass.async_create_task( + hass.config_entries.async_reload(entry.entry_id) + ) + await asyncio.sleep(0) + setup_finish_future.set_result(None) + await setup_task + await reload_task + assert setup_calls == 2 From 31cfabc44d8c340757865c6194c5f9b00173c306 Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Wed, 1 May 2024 05:56:02 -0400 Subject: [PATCH 952/967] Fix roborock image crashes (#116487) --- .../components/roborock/coordinator.py | 2 +- homeassistant/components/roborock/image.py | 31 ++++++++-- tests/components/roborock/conftest.py | 4 ++ tests/components/roborock/test_image.py | 62 +++++++++++++++++++ 4 files changed, 92 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/roborock/coordinator.py b/homeassistant/components/roborock/coordinator.py index 293415360bd..32b7a487ac8 100644 --- a/homeassistant/components/roborock/coordinator.py +++ b/homeassistant/components/roborock/coordinator.py @@ -49,7 +49,7 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]): ) device_data = DeviceData(device, product_info.model, device_networking.ip) self.api: RoborockLocalClientV1 | RoborockMqttClientV1 = RoborockLocalClientV1( - device_data + device_data, queue_timeout=5 ) self.cloud_api = cloud_api self.device_info = DeviceInfo( diff --git a/homeassistant/components/roborock/image.py b/homeassistant/components/roborock/image.py index 775ab98fd59..2aef39ce59b 100644 --- a/homeassistant/components/roborock/image.py +++ b/homeassistant/components/roborock/image.py @@ -66,17 +66,26 @@ class RoborockMap(RoborockCoordinatedEntity, ImageEntity): ) self._attr_image_last_updated = dt_util.utcnow() self.map_flag = map_flag - self.cached_map = self._create_image(starting_map) + try: + self.cached_map = self._create_image(starting_map) + except HomeAssistantError: + # If we failed to update the image on init, we set cached_map to empty bytes so that we are unavailable and can try again later. + self.cached_map = b"" self._attr_entity_category = EntityCategory.DIAGNOSTIC + @property + def available(self): + """Determines if the entity is available.""" + return self.cached_map != b"" + @property def is_selected(self) -> bool: """Return if this map is the currently selected map.""" return self.map_flag == self.coordinator.current_map def is_map_valid(self) -> bool: - """Update this map if it is the current active map, and the vacuum is cleaning.""" - return ( + """Update this map if it is the current active map, and the vacuum is cleaning or if it has never been set at all.""" + return self.cached_map == b"" or ( self.is_selected and self.image_last_updated is not None and self.coordinator.roborock_device_info.props.status is not None @@ -96,7 +105,16 @@ class RoborockMap(RoborockCoordinatedEntity, ImageEntity): async def async_image(self) -> bytes | None: """Update the image if it is not cached.""" if self.is_map_valid(): - map_data: bytes = await self.cloud_api.get_map_v1() + response = await asyncio.gather( + *(self.cloud_api.get_map_v1(), self.coordinator.get_rooms()), + return_exceptions=True, + ) + if not isinstance(response[0], bytes): + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="map_failure", + ) + map_data = response[0] self.cached_map = self._create_image(map_data) return self.cached_map @@ -141,9 +159,10 @@ async def create_coordinator_maps( await asyncio.sleep(MAP_SLEEP) # Get the map data map_update = await asyncio.gather( - *[coord.cloud_api.get_map_v1(), coord.get_rooms()] + *[coord.cloud_api.get_map_v1(), coord.get_rooms()], return_exceptions=True ) - api_data: bytes = map_update[0] + # If we fail to get the map -> We should set it to empty byte, still create it, and set it as unavailable. + 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}", diff --git a/tests/components/roborock/conftest.py b/tests/components/roborock/conftest.py index 0f3689da161..d3bb0a221b1 100644 --- a/tests/components/roborock/conftest.py +++ b/tests/components/roborock/conftest.py @@ -91,6 +91,10 @@ def bypass_api_fixture() -> None: RoomMapping(18, "2362041"), ], ), + patch( + "homeassistant.components.roborock.coordinator.RoborockMqttClientV1.get_map_v1", + return_value=b"123", + ), ): yield diff --git a/tests/components/roborock/test_image.py b/tests/components/roborock/test_image.py index 445f90f4a05..bc45c6dec05 100644 --- a/tests/components/roborock/test_image.py +++ b/tests/components/roborock/test_image.py @@ -5,7 +5,12 @@ from datetime import timedelta from http import HTTPStatus from unittest.mock import patch +from roborock import RoborockException + +from homeassistant.components.roborock import DOMAIN +from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry, async_fire_time_changed @@ -82,3 +87,60 @@ async def test_floorplan_image_failed_parse( async_fire_time_changed(hass, now) resp = await client.get("/api/image_proxy/image.roborock_s7_maxv_upstairs") assert not resp.ok + + +async def test_fail_parse_on_startup( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + mock_roborock_entry: MockConfigEntry, + bypass_api_fixture, +) -> None: + """Test that if we fail parsing on startup, we create the entity but set it as unavailable.""" + map_data = copy.deepcopy(MAP_DATA) + map_data.image = None + with patch( + "homeassistant.components.roborock.image.RoborockMapDataParser.parse", + return_value=map_data, + ): + await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + assert ( + image_entity := hass.states.get("image.roborock_s7_maxv_upstairs") + ) is not None + assert image_entity.state == STATE_UNAVAILABLE + + +async def test_fail_updating_image( + hass: HomeAssistant, + setup_entry: MockConfigEntry, + hass_client: ClientSessionGenerator, +) -> None: + """Test that we handle failing getting the image after it has already been setup..""" + client = await hass_client() + map_data = copy.deepcopy(MAP_DATA) + map_data.image = None + now = dt_util.utcnow() + timedelta(seconds=91) + # Copy the device prop so we don't override it + prop = copy.deepcopy(PROP) + prop.status.in_cleaning = 1 + # Update image, but get none for parse image. + with ( + patch( + "homeassistant.components.roborock.image.RoborockMapDataParser.parse", + return_value=map_data, + ), + patch( + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.get_prop", + return_value=prop, + ), + patch( + "homeassistant.components.roborock.image.dt_util.utcnow", return_value=now + ), + patch( + "homeassistant.components.roborock.coordinator.RoborockMqttClientV1.get_map_v1", + side_effect=RoborockException, + ), + ): + async_fire_time_changed(hass, now) + resp = await client.get("/api/image_proxy/image.roborock_s7_maxv_upstairs") + assert not resp.ok From fabc3d751e01f4043a0c39c378fd3383bc80f700 Mon Sep 17 00:00:00 2001 From: max2697 <143563471+max2697@users.noreply.github.com> Date: Wed, 1 May 2024 00:11:47 -0500 Subject: [PATCH 953/967] Bump opower to 0.4.4 (#116489) --- 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 51ad669733b..91e4fbc960c 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.3"] + "requirements": ["opower==0.4.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6e2b7a55a13..2b9fbebcd8b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1483,7 +1483,7 @@ openwrt-luci-rpc==1.1.17 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.4.3 +opower==0.4.4 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2d7fe7158bf..28f1ffb7d62 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1183,7 +1183,7 @@ openhomedevice==2.2.0 openwebifpy==4.2.4 # homeassistant.components.opower -opower==0.4.3 +opower==0.4.4 # homeassistant.components.oralb oralb-ble==0.17.6 From ad16c5bc254d8e95c4e3e14de6de9bbcf71e474b Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 1 May 2024 12:53:45 +0200 Subject: [PATCH 954/967] Update frontend to 20240501.0 (#116503) --- 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 aa1d8ee3d3c..6abe8df1d7c 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==20240430.0"] + "requirements": ["home-assistant-frontend==20240501.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index afb7d894a51..b1c0391022a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ habluetooth==2.8.0 hass-nabucasa==0.78.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240430.0 +home-assistant-frontend==20240501.0 home-assistant-intents==2024.4.24 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 2b9fbebcd8b..e23a81ccb4d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1078,7 +1078,7 @@ hole==0.8.0 holidays==0.47 # homeassistant.components.frontend -home-assistant-frontend==20240430.0 +home-assistant-frontend==20240501.0 # homeassistant.components.conversation home-assistant-intents==2024.4.24 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 28f1ffb7d62..2393cdd9db1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -880,7 +880,7 @@ hole==0.8.0 holidays==0.47 # homeassistant.components.frontend -home-assistant-frontend==20240430.0 +home-assistant-frontend==20240501.0 # homeassistant.components.conversation home-assistant-intents==2024.4.24 From 0eb734b6bfbfad902616fab30918d86bed126130 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 1 May 2024 13:41:34 +0200 Subject: [PATCH 955/967] Bump version to 2024.5.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 3a0d35b8324..1d0486c75c7 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 = 5 -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 a04e9fd218a..7dcbc5afdd5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.5.0b4" +version = "2024.5.0b5" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From f89677cd763762f28206440a042006c2e9c9a428 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 1 May 2024 10:00:17 -0400 Subject: [PATCH 956/967] Bump ZHA dependencies (#116509) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 452f11db85b..b1511b2f5bb 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,7 +21,7 @@ "universal_silabs_flasher" ], "requirements": [ - "bellows==0.38.2", + "bellows==0.38.3", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.115", diff --git a/requirements_all.txt b/requirements_all.txt index e23a81ccb4d..8c653ec38e1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -541,7 +541,7 @@ beautifulsoup4==4.12.3 # beewi-smartclim==0.0.10 # homeassistant.components.zha -bellows==0.38.2 +bellows==0.38.3 # homeassistant.components.bmw_connected_drive bimmer-connected[china]==0.15.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2393cdd9db1..9a0477cea8b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -466,7 +466,7 @@ base36==0.1.1 beautifulsoup4==4.12.3 # homeassistant.components.zha -bellows==0.38.2 +bellows==0.38.3 # homeassistant.components.bmw_connected_drive bimmer-connected[china]==0.15.2 From 4312f36dbe010ac1f9e087e900731cabcc57b5b3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 1 May 2024 08:23:33 -0500 Subject: [PATCH 957/967] Fix non-thread-safe operations in ihc (#116513) --- homeassistant/components/ihc/service_functions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/ihc/service_functions.py b/homeassistant/components/ihc/service_functions.py index cfd91f0960c..61eba4791ac 100644 --- a/homeassistant/components/ihc/service_functions.py +++ b/homeassistant/components/ihc/service_functions.py @@ -90,24 +90,24 @@ def setup_service_functions(hass: HomeAssistant) -> None: ihc_controller = _get_controller(call) await async_pulse(hass, ihc_controller, ihc_id) - hass.services.async_register( + hass.services.register( DOMAIN, SERVICE_SET_RUNTIME_VALUE_BOOL, async_set_runtime_value_bool, schema=SET_RUNTIME_VALUE_BOOL_SCHEMA, ) - hass.services.async_register( + hass.services.register( DOMAIN, SERVICE_SET_RUNTIME_VALUE_INT, async_set_runtime_value_int, schema=SET_RUNTIME_VALUE_INT_SCHEMA, ) - hass.services.async_register( + hass.services.register( DOMAIN, SERVICE_SET_RUNTIME_VALUE_FLOAT, async_set_runtime_value_float, schema=SET_RUNTIME_VALUE_FLOAT_SCHEMA, ) - hass.services.async_register( + hass.services.register( DOMAIN, SERVICE_PULSE, async_pulse_runtime_input, schema=PULSE_SCHEMA ) From 082721e1ab0a1aefd71469e38b9bd44801b77662 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 1 May 2024 15:42:53 +0200 Subject: [PATCH 958/967] Bump python matter server library to 5.10.0 (#116514) --- homeassistant/components/matter/entity.py | 3 --- homeassistant/components/matter/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/matter/entity.py b/homeassistant/components/matter/entity.py index dcb3586934b..a47147e874a 100644 --- a/homeassistant/components/matter/entity.py +++ b/homeassistant/components/matter/entity.py @@ -107,9 +107,6 @@ class MatterEntity(Entity): attr_path_filter=attr_path, ) ) - await self.matter_client.subscribe_attribute( - self._endpoint.node.node_id, sub_paths - ) # subscribe to node (availability changes) self._unsubscribes.append( self.matter_client.subscribe_events( diff --git a/homeassistant/components/matter/manifest.json b/homeassistant/components/matter/manifest.json index b3acc0d547c..20988e387fe 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==5.7.0"], + "requirements": ["python-matter-server==5.10.0"], "zeroconf": ["_matter._tcp.local.", "_matterc._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 8c653ec38e1..f391511e607 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2257,7 +2257,7 @@ python-kasa[speedups]==0.6.2.1 # python-lirc==1.2.3 # homeassistant.components.matter -python-matter-server==5.7.0 +python-matter-server==5.10.0 # homeassistant.components.xiaomi_miio python-miio==0.5.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9a0477cea8b..140741518d1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1751,7 +1751,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.6.2.1 # homeassistant.components.matter -python-matter-server==5.7.0 +python-matter-server==5.10.0 # homeassistant.components.xiaomi_miio python-miio==0.5.12 From 780a6b314ff39d3e6caec5edbbbcaea99217ed0d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 1 May 2024 08:22:50 -0500 Subject: [PATCH 959/967] Fix blocking I/O to import modules in mysensors (#116516) --- homeassistant/components/mysensors/gateway.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py index 0a037dfce31..11f27f8a108 100644 --- a/homeassistant/components/mysensors/gateway.py +++ b/homeassistant/components/mysensors/gateway.py @@ -10,7 +10,7 @@ import socket import sys from typing import Any -from mysensors import BaseAsyncGateway, Message, Sensor, mysensors +from mysensors import BaseAsyncGateway, Message, Sensor, get_const, mysensors import voluptuous as vol from homeassistant.components.mqtt import ( @@ -24,6 +24,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, callback import homeassistant.helpers.config_validation as cv +from homeassistant.setup import SetupPhases, async_pause_setup from homeassistant.util.unit_system import METRIC_SYSTEM from .const import ( @@ -162,6 +163,12 @@ async def _get_gateway( ) -> BaseAsyncGateway | None: """Return gateway after setup of the gateway.""" + with async_pause_setup(hass, SetupPhases.WAIT_IMPORT_PACKAGES): + # get_const will import a const module based on the version + # so we need to import it here to avoid it being imported + # in the event loop + await hass.async_add_import_executor_job(get_const, version) + if persistence_file is not None: # Interpret relative paths to be in hass config folder. # Absolute paths will be left as they are. From 15aa8949eee2b6f43d44d9e76e87e5ea05d497a4 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 1 May 2024 16:22:25 +0200 Subject: [PATCH 960/967] Improve scrape strings (#116519) --- homeassistant/components/scrape/strings.json | 42 +++++++++++--------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/scrape/strings.json b/homeassistant/components/scrape/strings.json index 217e69b27df..9b534aed77b 100644 --- a/homeassistant/components/scrape/strings.json +++ b/homeassistant/components/scrape/strings.json @@ -21,13 +21,13 @@ "encoding": "Character encoding" }, "data_description": { - "resource": "The URL to the website that contains the value", - "authentication": "Type of the HTTP authentication. Either basic or digest", - "verify_ssl": "Enables/disables verification of SSL/TLS certificate, for example if it is self-signed", - "headers": "Headers to use for the web request", - "timeout": "Timeout for connection to website", - "encoding": "Character encoding to use. Defaults to UTF-8", - "payload": "Payload to use when method is POST" + "resource": "The URL to the website that contains the value.", + "authentication": "Type of the HTTP authentication. Either basic or digest.", + "verify_ssl": "Enables/disables verification of SSL/TLS certificate, for example if it is self-signed.", + "headers": "Headers to use for the web request.", + "timeout": "Timeout for connection to website.", + "encoding": "Character encoding to use. Defaults to UTF-8.", + "payload": "Payload to use when method is POST." } }, "sensor": { @@ -36,19 +36,21 @@ "attribute": "Attribute", "index": "Index", "select": "Select", - "value_template": "Value Template", - "device_class": "Device Class", - "state_class": "State Class", - "unit_of_measurement": "Unit of Measurement" + "value_template": "Value template", + "availability": "Availability template", + "device_class": "Device class", + "state_class": "State class", + "unit_of_measurement": "Unit of measurement" }, "data_description": { - "select": "Defines what tag to search for. Check Beautifulsoup CSS selectors for details", - "attribute": "Get value of an attribute on the selected tag", - "index": "Defines which of the elements returned by the CSS selector to use", - "value_template": "Defines a template to get the state of the sensor", - "device_class": "The type/class of the sensor to set the icon in the frontend", - "state_class": "The state_class of the sensor", - "unit_of_measurement": "Choose temperature measurement or create your own" + "select": "Defines what tag to search for. Check Beautifulsoup CSS selectors for details.", + "attribute": "Get value of an attribute on the selected tag.", + "index": "Defines which of the elements returned by the CSS selector to use.", + "value_template": "Defines a template to get the state of the sensor.", + "availability": "Defines a template to get the availability of the sensor.", + "device_class": "The type/class of the sensor to set the icon in the frontend.", + "state_class": "The state_class of the sensor.", + "unit_of_measurement": "Choose unit of measurement or create your own." } } } @@ -70,6 +72,7 @@ "index": "[%key:component::scrape::config::step::sensor::data::index%]", "select": "[%key:component::scrape::config::step::sensor::data::select%]", "value_template": "[%key:component::scrape::config::step::sensor::data::value_template%]", + "availability": "[%key:component::scrape::config::step::sensor::data::availability%]", "device_class": "[%key:component::scrape::config::step::sensor::data::device_class%]", "state_class": "[%key:component::scrape::config::step::sensor::data::state_class%]", "unit_of_measurement": "[%key:component::scrape::config::step::sensor::data::unit_of_measurement%]" @@ -79,6 +82,7 @@ "attribute": "[%key:component::scrape::config::step::sensor::data_description::attribute%]", "index": "[%key:component::scrape::config::step::sensor::data_description::index%]", "value_template": "[%key:component::scrape::config::step::sensor::data_description::value_template%]", + "availability": "[%key:component::scrape::config::step::sensor::data_description::availability%]", "device_class": "[%key:component::scrape::config::step::sensor::data_description::device_class%]", "state_class": "[%key:component::scrape::config::step::sensor::data_description::state_class%]", "unit_of_measurement": "[%key:component::scrape::config::step::sensor::data_description::unit_of_measurement%]" @@ -91,6 +95,7 @@ "index": "[%key:component::scrape::config::step::sensor::data::index%]", "select": "[%key:component::scrape::config::step::sensor::data::select%]", "value_template": "[%key:component::scrape::config::step::sensor::data::value_template%]", + "availability": "[%key:component::scrape::config::step::sensor::data::availability%]", "device_class": "[%key:component::scrape::config::step::sensor::data::device_class%]", "state_class": "[%key:component::scrape::config::step::sensor::data::state_class%]", "unit_of_measurement": "[%key:component::scrape::config::step::sensor::data::unit_of_measurement%]" @@ -100,6 +105,7 @@ "attribute": "[%key:component::scrape::config::step::sensor::data_description::attribute%]", "index": "[%key:component::scrape::config::step::sensor::data_description::index%]", "value_template": "[%key:component::scrape::config::step::sensor::data_description::value_template%]", + "availability": "[%key:component::scrape::config::step::sensor::data_description::availability%]", "device_class": "[%key:component::scrape::config::step::sensor::data_description::device_class%]", "state_class": "[%key:component::scrape::config::step::sensor::data_description::state_class%]", "unit_of_measurement": "[%key:component::scrape::config::step::sensor::data_description::unit_of_measurement%]" From 1e4e891f0b95b373e6c593935721d906adc86e70 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 1 May 2024 16:24:03 +0200 Subject: [PATCH 961/967] Bump version to 2024.5.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 1d0486c75c7..3c3787c7e80 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 = 5 -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 7dcbc5afdd5..118f2f91d2c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.5.0b5" +version = "2024.5.0b6" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From b42f3671288c9b7948792886c2051b34d6927597 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 1 May 2024 18:30:59 +0200 Subject: [PATCH 962/967] Add blocklist for known Matter devices with faulty transitions (#116524) --- homeassistant/components/matter/light.py | 36 +++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/matter/light.py b/homeassistant/components/matter/light.py index c9556fd2e2e..9d80ebc38f6 100644 --- a/homeassistant/components/matter/light.py +++ b/homeassistant/components/matter/light.py @@ -43,6 +43,18 @@ COLOR_MODE_MAP = { } DEFAULT_TRANSITION = 0.2 +# there's a bug in (at least) Espressif's implementation of light transitions +# on devices based on Matter 1.0. Mark potential devices with this issue. +# https://github.com/home-assistant/core/issues/113775 +# vendorid (attributeKey 0/40/2) +# productid (attributeKey 0/40/4) +# hw version (attributeKey 0/40/8) +# sw version (attributeKey 0/40/10) +TRANSITION_BLOCKLIST = ( + (4488, 514, "1.0", "1.0.0"), + (5010, 769, "3.0", "1.0.0"), +) + async def async_setup_entry( hass: HomeAssistant, @@ -61,6 +73,7 @@ class MatterLight(MatterEntity, LightEntity): _supports_brightness = False _supports_color = False _supports_color_temperature = False + _transitions_disabled = False async def _set_xy_color( self, xy_color: tuple[float, float], transition: float = 0.0 @@ -260,6 +273,8 @@ class MatterLight(MatterEntity, LightEntity): color_temp = kwargs.get(ATTR_COLOR_TEMP) brightness = kwargs.get(ATTR_BRIGHTNESS) transition = kwargs.get(ATTR_TRANSITION, DEFAULT_TRANSITION) + if self._transitions_disabled: + transition = 0 if self.supported_color_modes is not None: if hs_color is not None and ColorMode.HS in self.supported_color_modes: @@ -336,8 +351,12 @@ class MatterLight(MatterEntity, LightEntity): supported_color_modes = filter_supported_color_modes(supported_color_modes) self._attr_supported_color_modes = supported_color_modes + self._check_transition_blocklist() # flag support for transition as soon as we support setting brightness and/or color - if supported_color_modes != {ColorMode.ONOFF}: + if ( + supported_color_modes != {ColorMode.ONOFF} + and not self._transitions_disabled + ): self._attr_supported_features |= LightEntityFeature.TRANSITION LOGGER.debug( @@ -376,6 +395,21 @@ class MatterLight(MatterEntity, LightEntity): else: self._attr_color_mode = ColorMode.ONOFF + def _check_transition_blocklist(self) -> None: + """Check if this device is reported to have non working transitions.""" + device_info = self._endpoint.device_info + if ( + device_info.vendorID, + device_info.productID, + device_info.hardwareVersionString, + device_info.softwareVersionString, + ) in TRANSITION_BLOCKLIST: + self._transitions_disabled = True + LOGGER.warning( + "Detected a device that has been reported to have firmware issues " + "with light transitions. Transitions will be disabled for this light" + ) + # Discovery schema(s) to map Matter Attributes to HA entities DISCOVERY_SCHEMAS = [ From e1c08959b0d5dfa8fcacfb1e1d68c712c14ee81f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 1 May 2024 10:22:50 -0500 Subject: [PATCH 963/967] Fix stop event cleanup when reloading MQTT (#116525) --- homeassistant/components/mqtt/client.py | 42 ++++++++++--------------- 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 74fa8fb3302..d79492ccb27 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -25,19 +25,12 @@ from homeassistant.const import ( CONF_PORT, CONF_PROTOCOL, CONF_USERNAME, - EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import ( - CALLBACK_TYPE, - CoreState, - Event, - HassJob, - HomeAssistant, - callback, -) +from homeassistant.core import CALLBACK_TYPE, Event, HassJob, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.start import async_at_started from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from homeassistant.util import dt as dt_util @@ -429,25 +422,22 @@ class MQTT: UNSUBSCRIBE_COOLDOWN, self._async_perform_unsubscribes ) self._pending_unsubscribes: set[str] = set() # topic - - if self.hass.state is CoreState.running: - self._ha_started.set() - else: - - @callback - def ha_started(_: Event) -> None: - self._ha_started.set() - - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, ha_started) - - async def async_stop_mqtt(_event: Event) -> None: - """Stop MQTT component.""" - await self.async_disconnect() - - self._cleanup_on_unload.append( - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_mqtt) + self._cleanup_on_unload.extend( + ( + async_at_started(hass, self._async_ha_started), + hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, self._async_ha_stop), + ) ) + @callback + def _async_ha_started(self, _hass: HomeAssistant) -> None: + """Handle HA started.""" + self._ha_started.set() + + async def _async_ha_stop(self, _event: Event) -> None: + """Handle HA stop.""" + await self.async_disconnect() + def start( self, mqtt_data: MqttData, From 1641f24314d6181e1f91ed61fd90e8e112342406 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 1 May 2024 18:32:41 +0200 Subject: [PATCH 964/967] Bump version to 2024.5.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 3c3787c7e80..38457221bc9 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 = 5 -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 118f2f91d2c..57489b42fec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.5.0b6" +version = "2024.5.0b7" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 858874f0dafc274d27414d0761fda39142714078 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 1 May 2024 18:59:07 +0200 Subject: [PATCH 965/967] Bump version to 2024.5.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 38457221bc9..eb46817bd34 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 = 5 -PATCH_VERSION: Final = "0b7" +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 57489b42fec..4dd5653f8ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.5.0b7" +version = "2024.5.0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 21466180aad4ca1d0fb1487007c6e2419ba0ca77 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 1 May 2024 12:45:47 -0500 Subject: [PATCH 966/967] Ensure mock mqtt handler is cleaned up after test_bootstrap_dependencies (#116544) --- tests/test_bootstrap.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 96caf5d10c8..782b082e639 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -1143,16 +1143,10 @@ async def test_bootstrap_empty_integrations( await hass.async_block_till_done() -@pytest.mark.parametrize("integration", ["mqtt_eventstream", "mqtt_statestream"]) -@pytest.mark.parametrize("load_registries", [False]) -async def test_bootstrap_dependencies( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - integration: str, -) -> None: - """Test dependencies are set up correctly,.""" +@pytest.fixture(name="mock_mqtt_config_flow") +def mock_mqtt_config_flow_fixture() -> Generator[None, None, None]: + """Mock MQTT config flow.""" - # Prepare MQTT config entry @HANDLERS.register("mqtt") class MockConfigFlow: """Mock the MQTT config flow.""" @@ -1160,6 +1154,19 @@ async def test_bootstrap_dependencies( VERSION = 1 MINOR_VERSION = 1 + yield + HANDLERS.pop("mqtt") + + +@pytest.mark.parametrize("integration", ["mqtt_eventstream", "mqtt_statestream"]) +@pytest.mark.parametrize("load_registries", [False]) +async def test_bootstrap_dependencies( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + integration: str, + mock_mqtt_config_flow: None, +) -> None: + """Test dependencies are set up correctly,.""" entry = MockConfigEntry(domain="mqtt", data={"broker": "test-broker"}) entry.add_to_hass(hass) From 343d97527c52bc2a4b18e322aab5dfdbd080ecb5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 1 May 2024 13:22:18 -0500 Subject: [PATCH 967/967] Ensure mqtt handler is restored if its already registered in bootstrap test (#116549) --- tests/test_bootstrap.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 782b082e639..3d2735d9c1c 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -1146,6 +1146,7 @@ async def test_bootstrap_empty_integrations( @pytest.fixture(name="mock_mqtt_config_flow") def mock_mqtt_config_flow_fixture() -> Generator[None, None, None]: """Mock MQTT config flow.""" + original_mqtt = HANDLERS.get("mqtt") @HANDLERS.register("mqtt") class MockConfigFlow: @@ -1155,7 +1156,10 @@ def mock_mqtt_config_flow_fixture() -> Generator[None, None, None]: MINOR_VERSION = 1 yield - HANDLERS.pop("mqtt") + if original_mqtt: + HANDLERS["mqtt"] = original_mqtt + else: + HANDLERS.pop("mqtt") @pytest.mark.parametrize("integration", ["mqtt_eventstream", "mqtt_statestream"])